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
commit 1fa3adf4c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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/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:

View File

@ -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"

View File

@ -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);
}

View File

@ -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

View File

@ -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
}

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) {
resp := JSONResponse{
Success: false,

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

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

View File

@ -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 />

View File

@ -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}&nbsp;&nbsp;
<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

View File

@ -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}

View File

@ -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;

View File

@ -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",