mirror of
https://github.com/OpenFactorioServerManager/factorio-server-manager.git
synced 2025-01-28 05:36:33 +02:00
Merge pull request #94 from knoxfighter/feature/mod-compatibility
Feature/mod compatibility
This commit is contained in:
commit
1fa3adf4c0
@ -12,6 +12,7 @@ install:
|
||||
- go get github.com/hpcloud/tail
|
||||
- go get github.com/gorilla/websocket
|
||||
- go get github.com/majormjr/rcon
|
||||
- go get github.com/Masterminds/semver
|
||||
- export GOPATH="$HOME/gopath/src/github.com/mroote/factorio-server-manager/:$GOPATH"
|
||||
|
||||
script:
|
||||
|
@ -145,6 +145,7 @@ go get github.com/gorilla/mux
|
||||
go get github.com/hpcloud/tail
|
||||
go get github.com/gorilla/websocket
|
||||
go get github.com/majormjr/rcon
|
||||
go get github.com/Masterminds/semver
|
||||
```
|
||||
|
||||
3. Now you will want to go into the src folder for example "C:\FS\factorio-server-manager\src" once there hold down left shift and right click an empty area of the folder. Then click "Open command windows here"
|
||||
|
@ -69,5 +69,12 @@ div.console-prompt-box {
|
||||
}
|
||||
|
||||
.sweet-alert table tr:nth-child(even) {
|
||||
background: rgba(68, 68, 68, 0.31);
|
||||
background: rgba(68, 68, 68, 0.21);
|
||||
}
|
||||
|
||||
.sweet-alert table tr.incompatible {
|
||||
background: rgba(255, 68, 68, 0.21);
|
||||
}
|
||||
.sweet-alert table tr:nth-child(even).incompatible {
|
||||
background: rgba(255, 0, 0, 0.31);
|
||||
}
|
@ -3,4 +3,5 @@ github.com/go-ini/ini
|
||||
github.com/gorilla/mux
|
||||
github.com/gorilla/websocket
|
||||
github.com/hpcloud/tail
|
||||
github.com/majormjr/rcon
|
||||
github.com/majormjr/rcon
|
||||
github.com/Masterminds/semver
|
@ -17,21 +17,26 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/majormjr/rcon"
|
||||
"regexp"
|
||||
"github.com/Masterminds/semver"
|
||||
)
|
||||
|
||||
type FactorioServer struct {
|
||||
Cmd *exec.Cmd `json:"-"`
|
||||
Savefile string `json:"savefile"`
|
||||
Latency int `json:"latency"`
|
||||
BindIP string `json:"bindip"`
|
||||
Port int `json:"port"`
|
||||
Running bool `json:"running"`
|
||||
StdOut io.ReadCloser `json:"-"`
|
||||
StdErr io.ReadCloser `json:"-"`
|
||||
StdIn io.WriteCloser `json:"-"`
|
||||
Settings map[string]interface{} `json:"-"`
|
||||
Rcon *rcon.RemoteConsole `json:"-"`
|
||||
LogChan chan []string `json:"-"`
|
||||
Cmd *exec.Cmd `json:"-"`
|
||||
Savefile string `json:"savefile"`
|
||||
Latency int `json:"latency"`
|
||||
BindIP string `json:"bindip"`
|
||||
Port int `json:"port"`
|
||||
Running bool `json:"running"`
|
||||
Version string `json:"fac_version"`
|
||||
BaseModVersion string `json:"base_mod_version"`
|
||||
SemVerVersion *semver.Version `json:"-"`
|
||||
StdOut io.ReadCloser `json:"-"`
|
||||
StdErr io.ReadCloser `json:"-"`
|
||||
StdIn io.WriteCloser `json:"-"`
|
||||
Settings map[string]interface{} `json:"-"`
|
||||
Rcon *rcon.RemoteConsole `json:"-"`
|
||||
LogChan chan []string `json:"-"`
|
||||
}
|
||||
|
||||
func randomPort() int {
|
||||
@ -92,6 +97,38 @@ func initFactorio() (f *FactorioServer, err error) {
|
||||
|
||||
log.Printf("Loaded Factorio settings from %s\n", settingsPath)
|
||||
|
||||
|
||||
//Load factorio version
|
||||
out, err := exec.Command(config.FactorioBinary, "--version").Output()
|
||||
if err != nil {
|
||||
log.Printf("error on loading factorio version: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
reg := regexp.MustCompile("Version.*?((\\d+\\.)?(\\d+\\.)?(\\*|\\d+)+)")
|
||||
found := reg.FindStringSubmatch(string(out))
|
||||
f.Version = found[1]
|
||||
|
||||
//Load baseMod version
|
||||
baseModInfoFile := filepath.Join(config.FactorioDir, "data", "base", "info.json")
|
||||
bmifBa, err := ioutil.ReadFile(baseModInfoFile)
|
||||
if err != nil {
|
||||
log.Printf("couldn't open baseMods info.json: %s", err)
|
||||
return
|
||||
}
|
||||
var modInfo ModInfo
|
||||
err = json.Unmarshal(bmifBa, &modInfo)
|
||||
if err != nil {
|
||||
log.Printf("error unmarshalling baseMods info.json to a modInfo: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
f.BaseModVersion = modInfo.Version
|
||||
f.SemVerVersion, err = semver.NewVersion(modInfo.Version)
|
||||
if err != nil {
|
||||
log.Fatalf("error loading semver-factorio-version: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -435,6 +435,24 @@ func CheckServer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func FactorioVersion(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: true,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
status := map[string]string{}
|
||||
status["version"] = FactorioServ.Version
|
||||
status["base_mod_version"] = FactorioServ.BaseModVersion
|
||||
|
||||
resp.Data = status
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error loading Factorio Version: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func LoginUser(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
|
@ -58,6 +58,7 @@ func (mods *Mods) listInstalledMods() ModsResultList {
|
||||
modsResult.Title = modInfo.Title
|
||||
modsResult.Version = modInfo.Version
|
||||
modsResult.FactorioVersion = modInfo.FactorioVersion
|
||||
modsResult.Compatibility = modInfo.Compatibility
|
||||
|
||||
for _, simpleMod := range mods.ModSimpleList.Mods {
|
||||
if simpleMod.Name == modsResult.Name {
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ModInfoList struct {
|
||||
@ -17,12 +18,14 @@ type ModInfoList struct {
|
||||
Destination string `json:"-"`
|
||||
}
|
||||
type ModInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
FileName string `json:"file_name"`
|
||||
FactorioVersion string `json:"factorio_version"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
FileName string `json:"file_name"`
|
||||
FactorioVersion string `json:"factorio_version"`
|
||||
Dependencies []string `json:"dependencies"`
|
||||
Compatibility bool `json:"compatibility"`
|
||||
}
|
||||
|
||||
func newModInfoList(destination string) (ModInfoList, error) {
|
||||
@ -46,6 +49,7 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
|
||||
|
||||
err = filepath.Walk(modInfoList.Destination, func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() && filepath.Ext(path) == ".zip" {
|
||||
|
||||
err = fileLock.RLock(path)
|
||||
if err != nil && err == lockfile.ErrorAlreadyLocked {
|
||||
log.Println(err)
|
||||
@ -69,6 +73,29 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
|
||||
}
|
||||
|
||||
modInfo.FileName = info.Name()
|
||||
|
||||
var baseDependency string
|
||||
for _, dependency := range modInfo.Dependencies {
|
||||
if strings.HasPrefix(dependency, "base") {
|
||||
baseDependency = strings.Split(dependency, "=")[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if baseDependency != "" {
|
||||
modInfo.Compatibility, err = checkModCompatibility(baseDependency)
|
||||
if err != nil {
|
||||
log.Printf("error checking mod compatibility: %s", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Println("error finding basemodDependency. Using FactorioVersion...")
|
||||
modInfo.Compatibility, err = checkModCompatibility(modInfo.FactorioVersion + ".0")
|
||||
if err != nil {
|
||||
log.Printf("error checking Compatibility with FactorioVersion: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
modInfoList.Mods = append(modInfoList.Mods, modInfo)
|
||||
}
|
||||
|
||||
|
21
src/mods.go
21
src/mods.go
@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"github.com/Masterminds/semver"
|
||||
)
|
||||
|
||||
type LoginErrorResponse struct {
|
||||
@ -274,9 +275,9 @@ func modStartUp() {
|
||||
},
|
||||
},
|
||||
}
|
||||
new_json, _ := json.Marshal(modSimpleList)
|
||||
newJson, _ := json.Marshal(modSimpleList)
|
||||
|
||||
err = ioutil.WriteFile(modSimpleList.Destination+"/mod-list.json", new_json, 0664)
|
||||
err = ioutil.WriteFile(modSimpleList.Destination+"/mod-list.json", newJson, 0664)
|
||||
if err != nil {
|
||||
log.Printf("error when writing new mod-list: %s", err)
|
||||
return err
|
||||
@ -344,3 +345,19 @@ func modStartUp() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkModCompatibility(modVersion string) (compatible bool, err error) {
|
||||
compatible = false
|
||||
modVersion = strings.TrimSpace(modVersion)
|
||||
if !strings.HasPrefix(modVersion, "~") {
|
||||
modVersion = "~" + modVersion
|
||||
}
|
||||
|
||||
constraint, err := semver.NewConstraint(modVersion)
|
||||
if err != nil {
|
||||
log.Printf("error loading constraint: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
return constraint.Check(FactorioServ.SemVerVersion), nil
|
||||
}
|
||||
|
@ -273,6 +273,11 @@ var apiRoutes = Routes{
|
||||
"GET",
|
||||
"/server/status",
|
||||
CheckServer,
|
||||
}, {
|
||||
"FactorioVersion",
|
||||
"GET",
|
||||
"/server/facVersion",
|
||||
FactorioVersion,
|
||||
}, {
|
||||
"LogoutUser",
|
||||
"GET",
|
||||
|
@ -15,9 +15,12 @@ class App extends React.Component {
|
||||
this.getSaves = this.getSaves.bind(this);
|
||||
this.getStatus = this.getStatus.bind(this);
|
||||
this.connectWebSocket = this.connectWebSocket.bind(this);
|
||||
this.getFactorioVersion = this.getFactorioVersion.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverRunning: "stopped",
|
||||
serverStatus: {},
|
||||
factorioVersion: "",
|
||||
saves: [],
|
||||
loggedIn: false,
|
||||
username: "",
|
||||
@ -35,6 +38,7 @@ class App extends React.Component {
|
||||
}
|
||||
}, 1000);
|
||||
this.connectWebSocket();
|
||||
this.getFactorioVersion(); //Init serverStatus, so i know, which factorio-version is installed
|
||||
}
|
||||
|
||||
connectWebSocket() {
|
||||
@ -55,8 +59,10 @@ class App extends React.Component {
|
||||
dataType: "json",
|
||||
success: (data) => {
|
||||
if (data.success === true) {
|
||||
this.setState({loggedIn: true,
|
||||
username: data.data.Username})
|
||||
this.setState({
|
||||
loggedIn: true,
|
||||
username: data.data.Username
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -67,7 +73,9 @@ class App extends React.Component {
|
||||
url: "/api/server/status",
|
||||
dataType: "json",
|
||||
success: (data) => {
|
||||
this.setState({serverRunning: data.data.status})
|
||||
this.setState({
|
||||
serverRunning: data.data.status
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -98,7 +106,24 @@ class App extends React.Component {
|
||||
url: "/api/server/status",
|
||||
dataType: "json",
|
||||
success: (data) => {
|
||||
this.setState({serverStatus: data.data})
|
||||
this.setState({
|
||||
serverStatus: data.data
|
||||
})
|
||||
},
|
||||
error: (xhr, status, err) => {
|
||||
console.log('api/server/status', status, err.toString());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getFactorioVersion() {
|
||||
$.ajax({
|
||||
url: "/api/server/facVersion",
|
||||
// dataType: "json",
|
||||
success: (data) => {
|
||||
this.setState({
|
||||
factorioVersion: data.data.base_mod_version
|
||||
});
|
||||
},
|
||||
error: (xhr, status, err) => {
|
||||
console.log('api/server/status', status, err.toString());
|
||||
@ -128,16 +153,19 @@ class App extends React.Component {
|
||||
// Render react-router components and pass in props
|
||||
{React.cloneElement(
|
||||
this.props.children,
|
||||
{message: "",
|
||||
messages: this.state.messages,
|
||||
flashMessage: this.flashMessage,
|
||||
facServStatus: this.facServStatus,
|
||||
serverStatus: this.state.serverStatus,
|
||||
getStatus: this.getStatus,
|
||||
saves: this.state.saves,
|
||||
getSaves: this.getSaves,
|
||||
username: this.state.username,
|
||||
socket: this.socket}
|
||||
{
|
||||
message: "",
|
||||
messages: this.state.messages,
|
||||
flashMessage: this.flashMessage,
|
||||
facServStatus: this.facServStatus,
|
||||
serverStatus: this.state.serverStatus,
|
||||
factorioVersion: this.state.factorioVersion,
|
||||
getStatus: this.getStatus,
|
||||
saves: this.state.saves,
|
||||
getSaves: this.getSaves,
|
||||
username: this.state.username,
|
||||
socket: this.socket
|
||||
}
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import SemVer from 'semver';
|
||||
|
||||
class Mod extends React.Component {
|
||||
constructor(props) {
|
||||
@ -34,8 +35,20 @@ class Mod extends React.Component {
|
||||
dataType: "JSON",
|
||||
success: (data) => {
|
||||
let newData = JSON.parse(data.data);
|
||||
let newestRelease = newData.releases[newData.releases.length - 1];
|
||||
if(newestRelease.version != this.props.mod.version) {
|
||||
//get newest COMPATIBLE release
|
||||
let newestRelease;
|
||||
newData.releases.forEach((release) => {
|
||||
//TODO change to info_json dependency (when mod-portal-api is working again)
|
||||
if(SemVer.satisfies(this.props.factorioVersion, release.info_json.factorio_version + ".x")) {
|
||||
if(!newestRelease) {
|
||||
newestRelease = release;
|
||||
} else if(SemVer.gt(release.version, newestRelease.version)) {
|
||||
newestRelease = release;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(newestRelease && newestRelease.version != this.props.mod.version) {
|
||||
if(this.props.updateCountAdd)
|
||||
this.props.updateCountAdd();
|
||||
|
||||
@ -43,7 +56,8 @@ class Mod extends React.Component {
|
||||
newVersionAvailable: true,
|
||||
newVersion: {
|
||||
downloadUrl: newestRelease.download_url,
|
||||
file_name: newestRelease.file_name
|
||||
file_name: newestRelease.file_name,
|
||||
version: newestRelease.version
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -83,6 +97,13 @@ class Mod extends React.Component {
|
||||
|
||||
let version;
|
||||
if(this.state.newVersionAvailable) {
|
||||
let faArrow;
|
||||
if(SemVer.gt(this.state.newVersion.version, this.props.mod.version)) {
|
||||
faArrow = "fa fa-arrow-circle-up";
|
||||
} else {
|
||||
faArrow = "fa fa-arrow-circle-down";
|
||||
}
|
||||
|
||||
version = <span>{this.props.mod.version}
|
||||
<a className="btn btn-xs btn-default update-button"
|
||||
style={{
|
||||
@ -104,7 +125,7 @@ class Mod extends React.Component {
|
||||
this.state.updateInProgress ?
|
||||
<div className='loader' style={{width: 15, height: 15, marginRight: 0, borderWidth: 3,}}></div>
|
||||
:
|
||||
<i className="fa fa-arrow-circle-up" title="Update Mod" style={{fontSize: "15pt"}}></i>
|
||||
<i className={faArrow} title="Update Mod" style={{fontSize: "15pt"}}></i>
|
||||
}
|
||||
</a>
|
||||
</span>;
|
||||
@ -112,6 +133,16 @@ class Mod extends React.Component {
|
||||
version = this.props.mod.version;
|
||||
}
|
||||
|
||||
let factorioVersion;
|
||||
if(!this.props.mod.compatibility) {
|
||||
factorioVersion = <span style={{color: "red"}}>
|
||||
{this.props.mod.factorio_version}
|
||||
<sup>not compatible</sup>
|
||||
</span>
|
||||
} else {
|
||||
factorioVersion = this.props.mod.factorio_version;
|
||||
}
|
||||
|
||||
return(
|
||||
<tr data-mod-name={this.props.mod.name}
|
||||
data-file-name={this.props.mod.file_name}
|
||||
@ -120,7 +151,7 @@ class Mod extends React.Component {
|
||||
<td>{this.props.mod.title}</td>
|
||||
<td>{modStatus}</td>
|
||||
<td>{version}</td>
|
||||
<td>{this.props.mod.factorio_version}</td>
|
||||
<td>{factorioVersion}</td>
|
||||
<td>
|
||||
<input className='btn btn-default btn-sm'
|
||||
ref='modName'
|
||||
@ -150,6 +181,7 @@ Mod.propTypes = {
|
||||
deleteMod: React.PropTypes.func.isRequired,
|
||||
updateMod: React.PropTypes.func.isRequired,
|
||||
updateCountAdd: React.PropTypes.func,
|
||||
factorioVersion: React.PropTypes.string,
|
||||
};
|
||||
|
||||
export default Mod
|
||||
|
@ -446,6 +446,7 @@ class ModPackOverview extends React.Component {
|
||||
</div>
|
||||
<div className="box-body">
|
||||
<ModManager
|
||||
{...this.props}
|
||||
installedMods={modpack.mods.mods}
|
||||
deleteMod={this.modPackDeleteModHandler}
|
||||
toggleMod={this.modPackToggleModHandler}
|
||||
|
@ -3,6 +3,7 @@ import ReactDOMServer from 'react-dom/server';
|
||||
import {IndexLink} from 'react-router';
|
||||
import ModOverview from './Mods/ModOverview.jsx';
|
||||
import locks from "locks";
|
||||
import SemVer from 'semver';
|
||||
|
||||
class ModsContent extends React.Component {
|
||||
constructor(props) {
|
||||
@ -24,6 +25,7 @@ class ModsContent extends React.Component {
|
||||
this.updateCountSubtract = this.updateCountSubtract.bind(this);
|
||||
this.updateCountAdd = this.updateCountAdd.bind(this);
|
||||
|
||||
|
||||
this.state = {
|
||||
loggedIn: false,
|
||||
installedMods: null,
|
||||
@ -178,19 +180,31 @@ class ModsContent extends React.Component {
|
||||
|
||||
let correctData = JSON.parse(data.data);
|
||||
|
||||
let checkboxes = []
|
||||
let checkboxes = [];
|
||||
let compatibleReleaseFound = false;
|
||||
|
||||
correctData.releases.reverse();
|
||||
correctData.releases.forEach((release, index) => {
|
||||
correctData.releases.forEach((release) => {
|
||||
let incompatibleClass = "";
|
||||
let isChecked = false;
|
||||
|
||||
if(!SemVer.satisfies(this.props.factorioVersion, release.info_json.factorio_version + ".x")) {
|
||||
incompatibleClass = "incompatible";
|
||||
} else if(compatibleReleaseFound == false) {
|
||||
compatibleReleaseFound = true;
|
||||
isChecked = true;
|
||||
}
|
||||
|
||||
let date = new Date(release.released_at);
|
||||
|
||||
let singleBox = <tr>
|
||||
let singleBox = <tr className={incompatibleClass}>
|
||||
<td>
|
||||
<input type="radio"
|
||||
name="version"
|
||||
data-link={release.download_url}
|
||||
data-filename={release.file_name}
|
||||
data-modid={modId}
|
||||
checked={index == 0 ? true : false}
|
||||
checked={isChecked}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@ -483,7 +497,6 @@ class ModsContent extends React.Component {
|
||||
e.stopPropagation();
|
||||
|
||||
let updateButtons = $('#manage-mods').find(".update-button");
|
||||
// $('.update-button').click();
|
||||
$.each(updateButtons, (k, v) => {
|
||||
v.click();
|
||||
});
|
||||
@ -524,6 +537,7 @@ class ModsContent extends React.Component {
|
||||
<section className="content">
|
||||
<ModOverview
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
loadDownloadList={this.loadDownloadList}
|
||||
submitFactorioLogin={this.handlerFactorioLogin}
|
||||
toggleMod={this.toggleModHandler}
|
||||
@ -542,4 +556,8 @@ class ModsContent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ModsContent.propTypes = {
|
||||
factorioVersion: React.PropTypes.string,
|
||||
};
|
||||
|
||||
export default ModsContent;
|
@ -21,7 +21,8 @@
|
||||
"react-dom": "^15.0.1",
|
||||
"react-native-listener": "^1.0.2",
|
||||
"react-router": "^2.3.0",
|
||||
"sweetalert": "^1.1.3"
|
||||
"sweetalert": "^1.1.3",
|
||||
"semver": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "^0.23.1",
|
||||
|
Loading…
x
Reference in New Issue
Block a user