Merge pull request #94 from knoxfighter/feature/mod-compatibility

Feature/mod compatibility
This commit is contained in:
Mitch Roote
2018-06-02 09:49:20 -04:00
committed by GitHub
15 changed files with 242 additions and 47 deletions

View File

@ -12,6 +12,7 @@ install:
- go get github.com/hpcloud/tail - go get github.com/hpcloud/tail
- go get github.com/gorilla/websocket - go get github.com/gorilla/websocket
- go get github.com/majormjr/rcon - go get github.com/majormjr/rcon
- go get github.com/Masterminds/semver
- export GOPATH="$HOME/gopath/src/github.com/mroote/factorio-server-manager/:$GOPATH" - export GOPATH="$HOME/gopath/src/github.com/mroote/factorio-server-manager/:$GOPATH"
script: script:

View File

@ -145,6 +145,7 @@ go get github.com/gorilla/mux
go get github.com/hpcloud/tail go get github.com/hpcloud/tail
go get github.com/gorilla/websocket go get github.com/gorilla/websocket
go get github.com/majormjr/rcon 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" 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"

View File

@ -69,5 +69,12 @@ div.console-prompt-box {
} }
.sweet-alert table tr:nth-child(even) { .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);
} }

View File

@ -4,3 +4,4 @@ github.com/gorilla/mux
github.com/gorilla/websocket github.com/gorilla/websocket
github.com/hpcloud/tail github.com/hpcloud/tail
github.com/majormjr/rcon github.com/majormjr/rcon
github.com/Masterminds/semver

View File

@ -17,21 +17,26 @@ import (
"time" "time"
"github.com/majormjr/rcon" "github.com/majormjr/rcon"
"regexp"
"github.com/Masterminds/semver"
) )
type FactorioServer struct { type FactorioServer struct {
Cmd *exec.Cmd `json:"-"` Cmd *exec.Cmd `json:"-"`
Savefile string `json:"savefile"` Savefile string `json:"savefile"`
Latency int `json:"latency"` Latency int `json:"latency"`
BindIP string `json:"bindip"` BindIP string `json:"bindip"`
Port int `json:"port"` Port int `json:"port"`
Running bool `json:"running"` Running bool `json:"running"`
StdOut io.ReadCloser `json:"-"` Version string `json:"fac_version"`
StdErr io.ReadCloser `json:"-"` BaseModVersion string `json:"base_mod_version"`
StdIn io.WriteCloser `json:"-"` SemVerVersion *semver.Version `json:"-"`
Settings map[string]interface{} `json:"-"` StdOut io.ReadCloser `json:"-"`
Rcon *rcon.RemoteConsole `json:"-"` StdErr io.ReadCloser `json:"-"`
LogChan chan []string `json:"-"` StdIn io.WriteCloser `json:"-"`
Settings map[string]interface{} `json:"-"`
Rcon *rcon.RemoteConsole `json:"-"`
LogChan chan []string `json:"-"`
} }
func randomPort() int { func randomPort() int {
@ -92,6 +97,38 @@ func initFactorio() (f *FactorioServer, err error) {
log.Printf("Loaded Factorio settings from %s\n", settingsPath) 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 return
} }

View File

@ -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) { func LoginUser(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{ resp := JSONResponse{
Success: false, Success: false,

View File

@ -58,6 +58,7 @@ func (mods *Mods) listInstalledMods() ModsResultList {
modsResult.Title = modInfo.Title modsResult.Title = modInfo.Title
modsResult.Version = modInfo.Version modsResult.Version = modInfo.Version
modsResult.FactorioVersion = modInfo.FactorioVersion modsResult.FactorioVersion = modInfo.FactorioVersion
modsResult.Compatibility = modInfo.Compatibility
for _, simpleMod := range mods.ModSimpleList.Mods { for _, simpleMod := range mods.ModSimpleList.Mods {
if simpleMod.Name == modsResult.Name { if simpleMod.Name == modsResult.Name {

View File

@ -10,6 +10,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
type ModInfoList struct { type ModInfoList struct {
@ -17,12 +18,14 @@ type ModInfoList struct {
Destination string `json:"-"` Destination string `json:"-"`
} }
type ModInfo struct { type ModInfo struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
Title string `json:"title"` Title string `json:"title"`
Author string `json:"author"` Author string `json:"author"`
FileName string `json:"file_name"` FileName string `json:"file_name"`
FactorioVersion string `json:"factorio_version"` FactorioVersion string `json:"factorio_version"`
Dependencies []string `json:"dependencies"`
Compatibility bool `json:"compatibility"`
} }
func newModInfoList(destination string) (ModInfoList, error) { 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 { err = filepath.Walk(modInfoList.Destination, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && filepath.Ext(path) == ".zip" { if !info.IsDir() && filepath.Ext(path) == ".zip" {
err = fileLock.RLock(path) err = fileLock.RLock(path)
if err != nil && err == lockfile.ErrorAlreadyLocked { if err != nil && err == lockfile.ErrorAlreadyLocked {
log.Println(err) log.Println(err)
@ -69,6 +73,29 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
} }
modInfo.FileName = info.Name() 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) modInfoList.Mods = append(modInfoList.Mods, modInfo)
} }

View File

@ -12,6 +12,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/Masterminds/semver"
) )
type LoginErrorResponse struct { 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 { if err != nil {
log.Printf("error when writing new mod-list: %s", err) log.Printf("error when writing new mod-list: %s", err)
return 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
}

View File

@ -273,6 +273,11 @@ var apiRoutes = Routes{
"GET", "GET",
"/server/status", "/server/status",
CheckServer, CheckServer,
}, {
"FactorioVersion",
"GET",
"/server/facVersion",
FactorioVersion,
}, { }, {
"LogoutUser", "LogoutUser",
"GET", "GET",

View File

@ -15,9 +15,12 @@ class App extends React.Component {
this.getSaves = this.getSaves.bind(this); this.getSaves = this.getSaves.bind(this);
this.getStatus = this.getStatus.bind(this); this.getStatus = this.getStatus.bind(this);
this.connectWebSocket = this.connectWebSocket.bind(this); this.connectWebSocket = this.connectWebSocket.bind(this);
this.getFactorioVersion = this.getFactorioVersion.bind(this);
this.state = { this.state = {
serverRunning: "stopped", serverRunning: "stopped",
serverStatus: {}, serverStatus: {},
factorioVersion: "",
saves: [], saves: [],
loggedIn: false, loggedIn: false,
username: "", username: "",
@ -35,6 +38,7 @@ class App extends React.Component {
} }
}, 1000); }, 1000);
this.connectWebSocket(); this.connectWebSocket();
this.getFactorioVersion(); //Init serverStatus, so i know, which factorio-version is installed
} }
connectWebSocket() { connectWebSocket() {
@ -55,8 +59,10 @@ class App extends React.Component {
dataType: "json", dataType: "json",
success: (data) => { success: (data) => {
if (data.success === true) { if (data.success === true) {
this.setState({loggedIn: true, this.setState({
username: data.data.Username}) loggedIn: true,
username: data.data.Username
});
} }
} }
}) })
@ -67,7 +73,9 @@ class App extends React.Component {
url: "/api/server/status", url: "/api/server/status",
dataType: "json", dataType: "json",
success: (data) => { 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", url: "/api/server/status",
dataType: "json", dataType: "json",
success: (data) => { 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) => { error: (xhr, status, err) => {
console.log('api/server/status', status, err.toString()); 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 // Render react-router components and pass in props
{React.cloneElement( {React.cloneElement(
this.props.children, this.props.children,
{message: "", {
messages: this.state.messages, message: "",
flashMessage: this.flashMessage, messages: this.state.messages,
facServStatus: this.facServStatus, flashMessage: this.flashMessage,
serverStatus: this.state.serverStatus, facServStatus: this.facServStatus,
getStatus: this.getStatus, serverStatus: this.state.serverStatus,
saves: this.state.saves, factorioVersion: this.state.factorioVersion,
getSaves: this.getSaves, getStatus: this.getStatus,
username: this.state.username, saves: this.state.saves,
socket: this.socket} getSaves: this.getSaves,
username: this.state.username,
socket: this.socket
}
)} )}
<Footer /> <Footer />

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import SemVer from 'semver';
class Mod extends React.Component { class Mod extends React.Component {
constructor(props) { constructor(props) {
@ -34,8 +35,20 @@ class Mod extends React.Component {
dataType: "JSON", dataType: "JSON",
success: (data) => { success: (data) => {
let newData = JSON.parse(data.data); let newData = JSON.parse(data.data);
let newestRelease = newData.releases[newData.releases.length - 1]; //get newest COMPATIBLE release
if(newestRelease.version != this.props.mod.version) { 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) if(this.props.updateCountAdd)
this.props.updateCountAdd(); this.props.updateCountAdd();
@ -43,7 +56,8 @@ class Mod extends React.Component {
newVersionAvailable: true, newVersionAvailable: true,
newVersion: { newVersion: {
downloadUrl: newestRelease.download_url, downloadUrl: newestRelease.download_url,
file_name: newestRelease.file_name file_name: newestRelease.file_name,
version: newestRelease.version
} }
}); });
} else { } else {
@ -83,6 +97,13 @@ class Mod extends React.Component {
let version; let version;
if(this.state.newVersionAvailable) { 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} version = <span>{this.props.mod.version}
<a className="btn btn-xs btn-default update-button" <a className="btn btn-xs btn-default update-button"
style={{ style={{
@ -104,7 +125,7 @@ class Mod extends React.Component {
this.state.updateInProgress ? this.state.updateInProgress ?
<div className='loader' style={{width: 15, height: 15, marginRight: 0, borderWidth: 3,}}></div> <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> </a>
</span>; </span>;
@ -112,6 +133,16 @@ class Mod extends React.Component {
version = this.props.mod.version; version = this.props.mod.version;
} }
let factorioVersion;
if(!this.props.mod.compatibility) {
factorioVersion = <span style={{color: "red"}}>
{this.props.mod.factorio_version}&nbsp;&nbsp;
<sup>not compatible</sup>
</span>
} else {
factorioVersion = this.props.mod.factorio_version;
}
return( return(
<tr data-mod-name={this.props.mod.name} <tr data-mod-name={this.props.mod.name}
data-file-name={this.props.mod.file_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>{this.props.mod.title}</td>
<td>{modStatus}</td> <td>{modStatus}</td>
<td>{version}</td> <td>{version}</td>
<td>{this.props.mod.factorio_version}</td> <td>{factorioVersion}</td>
<td> <td>
<input className='btn btn-default btn-sm' <input className='btn btn-default btn-sm'
ref='modName' ref='modName'
@ -150,6 +181,7 @@ Mod.propTypes = {
deleteMod: React.PropTypes.func.isRequired, deleteMod: React.PropTypes.func.isRequired,
updateMod: React.PropTypes.func.isRequired, updateMod: React.PropTypes.func.isRequired,
updateCountAdd: React.PropTypes.func, updateCountAdd: React.PropTypes.func,
factorioVersion: React.PropTypes.string,
}; };
export default Mod export default Mod

View File

@ -446,6 +446,7 @@ class ModPackOverview extends React.Component {
</div> </div>
<div className="box-body"> <div className="box-body">
<ModManager <ModManager
{...this.props}
installedMods={modpack.mods.mods} installedMods={modpack.mods.mods}
deleteMod={this.modPackDeleteModHandler} deleteMod={this.modPackDeleteModHandler}
toggleMod={this.modPackToggleModHandler} toggleMod={this.modPackToggleModHandler}

View File

@ -3,6 +3,7 @@ import ReactDOMServer from 'react-dom/server';
import {IndexLink} from 'react-router'; import {IndexLink} from 'react-router';
import ModOverview from './Mods/ModOverview.jsx'; import ModOverview from './Mods/ModOverview.jsx';
import locks from "locks"; import locks from "locks";
import SemVer from 'semver';
class ModsContent extends React.Component { class ModsContent extends React.Component {
constructor(props) { constructor(props) {
@ -24,6 +25,7 @@ class ModsContent extends React.Component {
this.updateCountSubtract = this.updateCountSubtract.bind(this); this.updateCountSubtract = this.updateCountSubtract.bind(this);
this.updateCountAdd = this.updateCountAdd.bind(this); this.updateCountAdd = this.updateCountAdd.bind(this);
this.state = { this.state = {
loggedIn: false, loggedIn: false,
installedMods: null, installedMods: null,
@ -178,19 +180,31 @@ class ModsContent extends React.Component {
let correctData = JSON.parse(data.data); let correctData = JSON.parse(data.data);
let checkboxes = [] let checkboxes = [];
let compatibleReleaseFound = false;
correctData.releases.reverse(); 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 date = new Date(release.released_at);
let singleBox = <tr> let singleBox = <tr className={incompatibleClass}>
<td> <td>
<input type="radio" <input type="radio"
name="version" name="version"
data-link={release.download_url} data-link={release.download_url}
data-filename={release.file_name} data-filename={release.file_name}
data-modid={modId} data-modid={modId}
checked={index == 0 ? true : false} checked={isChecked}
/> />
</td> </td>
<td> <td>
@ -483,7 +497,6 @@ class ModsContent extends React.Component {
e.stopPropagation(); e.stopPropagation();
let updateButtons = $('#manage-mods').find(".update-button"); let updateButtons = $('#manage-mods').find(".update-button");
// $('.update-button').click();
$.each(updateButtons, (k, v) => { $.each(updateButtons, (k, v) => {
v.click(); v.click();
}); });
@ -524,6 +537,7 @@ class ModsContent extends React.Component {
<section className="content"> <section className="content">
<ModOverview <ModOverview
{...this.state} {...this.state}
{...this.props}
loadDownloadList={this.loadDownloadList} loadDownloadList={this.loadDownloadList}
submitFactorioLogin={this.handlerFactorioLogin} submitFactorioLogin={this.handlerFactorioLogin}
toggleMod={this.toggleModHandler} toggleMod={this.toggleModHandler}
@ -542,4 +556,8 @@ class ModsContent extends React.Component {
} }
} }
ModsContent.propTypes = {
factorioVersion: React.PropTypes.string,
};
export default ModsContent; export default ModsContent;

View File

@ -21,7 +21,8 @@
"react-dom": "^15.0.1", "react-dom": "^15.0.1",
"react-native-listener": "^1.0.2", "react-native-listener": "^1.0.2",
"react-router": "^2.3.0", "react-router": "^2.3.0",
"sweetalert": "^1.1.3" "sweetalert": "^1.1.3",
"semver": "latest"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "^0.23.1", "css-loader": "^0.23.1",