diff --git a/README.md b/README.md
index 3f73baad..02a77127 100644
--- a/README.md
+++ b/README.md
@@ -177,6 +177,9 @@ go build
* **Mitch Roote** - [roote.ca](https://roote.ca)
+## Special Thanks
+- **mickael9** for reverseengineering the factorio-save-file: https://forums.factorio.com/viewtopic.php?f=5&t=8568#
+
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
diff --git a/src/mods_handler.go b/src/mods_handler.go
index 94abb4c2..101e6489 100644
--- a/src/mods_handler.go
+++ b/src/mods_handler.go
@@ -12,8 +12,28 @@ import (
"net/http"
"os"
"path/filepath"
+ "factorioSave"
+ "time"
)
+type ModPortalStruct struct {
+ DownloadsCount int `json:"downloads_count"`
+ Name string `json:"name"`
+ Owner string `json:"owner"`
+ Releases []struct {
+ DownloadURL string `json:"download_url"`
+ FileName string `json:"file_name"`
+ InfoJSON struct {
+ FactorioVersion string `json:"factorio_version"`
+ } `json:"info_json"`
+ ReleasedAt time.Time `json:"released_at"`
+ Sha1 string `json:"sha1"`
+ Version string `json:"version"`
+ } `json:"releases"`
+ Summary string `json:"summary"`
+ Title string `json:"title"`
+}
+
// Returns JSON response of all mods installed in factorio/mods
func listInstalledModsHandler(w http.ResponseWriter, r *http.Request) {
var err error
@@ -211,7 +231,7 @@ func ModPortalInstallHandler(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
- w.WriteHeader(500)
+ w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in installMod: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in installMod: %s", err)
@@ -227,6 +247,90 @@ func ModPortalInstallHandler(w http.ResponseWriter, r *http.Request) {
}
}
+func ModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) {
+ var err error
+ resp := JSONResponse{
+ Success: false,
+ }
+
+ w.Header().Set("Content-Type", "application/json;charset=UTF-8")
+ r.ParseForm()
+
+ modsList := make([]string, 0)
+ versionsList := make([]string, 0)
+
+ //Parse incoming data
+ for key, values := range r.PostForm {
+ if key == "mod_name" {
+ for _, v := range values {
+ modsList = append(modsList, v)
+ }
+ } else if key == "mod_version" {
+ for _, v := range values {
+ versionsList = append(versionsList, v)
+ }
+ }
+ }
+
+ mods, err := newMods(config.FactorioModsDir)
+ if err != nil {
+ log.Printf("error creating mods: %s", err)
+
+ w.WriteHeader(http.StatusInternalServerError)
+ resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ log.Printf("Error in searchModPortal: %s", err)
+ }
+ return
+ }
+
+ for modIndex, mod := range modsList {
+ var err error
+
+ //get details of mod
+ modDetails, err, statusCode := getModDetails(mod)
+ if err != nil {
+ w.WriteHeader(statusCode)
+ resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ log.Printf("Error in searchModPortal: %s", err)
+ }
+ return
+ }
+
+ modDetailsArray := []byte(modDetails)
+ var modDetailsStruct ModPortalStruct
+
+ //read mod-data into Struct
+ err = json.Unmarshal(modDetailsArray, &modDetailsStruct)
+ if err != nil {
+ log.Printf("error reading modPortalDetails: %s", err)
+
+ w.WriteHeader(http.StatusInternalServerError)
+ resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ log.Printf("Error in searchModPortal: %s", err)
+ }
+ return
+ }
+
+ //find correct mod-version
+ for _, release := range modDetailsStruct.Releases {
+ if release.Version == versionsList[modIndex] {
+ mods.downloadMod(release.DownloadURL, release.FileName, modDetailsStruct.Name)
+ break
+ }
+ }
+ }
+
+ resp.Data = mods.listInstalledMods()
+
+ resp.Success = true
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ log.Printf("Error in ToggleModHandler: %s", err)
+ }
+}
+
func ToggleModHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
@@ -244,7 +348,7 @@ func ToggleModHandler(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
- w.WriteHeader(500)
+ w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in listInstalledModsByFolder: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in listInstalledModsByFolder: %s", err)
@@ -276,7 +380,7 @@ func DeleteModHandler(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
- w.WriteHeader(500)
+ w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
@@ -455,6 +559,45 @@ func DownloadModsHandler(w http.ResponseWriter, r *http.Request) {
writerHeader.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", "all_installed_mods.zip"))
}
+//Returns JSON response with the found mods
+func LoadModsFromSaveHandler(w http.ResponseWriter, r *http.Request) {
+ var err error
+ resp := JSONResponse{
+ Success: false,
+ }
+
+ w.Header().Set("Content-Type", "application/json;charset=UTF-8")
+
+ //Get Data out of the request
+ SaveFile := r.FormValue("saveFile")
+
+ SaveFileComplete := filepath.Join(config.FactorioSavesDir, SaveFile)
+ resp.Data, err = factorioSave.ReadHeader(SaveFileComplete)
+
+ if err == factorioSave.ErrorIncompatible {
+ w.WriteHeader(http.StatusInternalServerError)
+ resp.Data = fmt.Sprintf("%s
Only can read 0.16.x save files", err)
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ log.Printf("Error in loadModsFromSave: %s", err)
+ }
+ return
+ }
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ log.Printf("Error in loadModsFromSave: %s", err)
+ }
+ return
+ }
+
+ resp.Success = true
+
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ log.Printf("Error in LoadModsFromSave: %s", err)
+ }
+}
+
func ListModPacksHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
diff --git a/src/routes.go b/src/routes.go
index 8d9c7134..4ee0d329 100644
--- a/src/routes.go
+++ b/src/routes.go
@@ -183,6 +183,11 @@ var apiRoutes = Routes{
"POST",
"/mods/install",
ModPortalInstallHandler,
+ }, {
+ "ModPortalInstallMultiple",
+ "POST",
+ "/mods/install/multiple",
+ ModPortalInstallMultipleHandler,
}, {
"ToggleMod",
"POST",
@@ -213,6 +218,11 @@ var apiRoutes = Routes{
"GET",
"/mods/download",
DownloadModsHandler,
+ }, {
+ "LoadModsFromSave",
+ "POST",
+ "/mods/save/load",
+ LoadModsFromSaveHandler,
}, {
"ListSaves",
"GET",
diff --git a/src/vendor/factorioSave/factorioSave.go b/src/vendor/factorioSave/factorioSave.go
new file mode 100644
index 00000000..b258575f
--- /dev/null
+++ b/src/vendor/factorioSave/factorioSave.go
@@ -0,0 +1,405 @@
+package factorioSave
+
+import (
+ "log"
+ "encoding/binary"
+ "errors"
+ "io"
+ "strconv"
+ "github.com/Masterminds/semver"
+)
+
+type version16 struct {
+ versionShort16
+ Revision uint16 `json:"revision"`
+}
+type versionShort16 struct {
+ Major uint16 `json:"major"`
+ Minor uint16 `json:"minor"`
+ Build uint16 `json:"build"`
+}
+type versionShort8 struct {
+ Major uint8 `json:"major"`
+ Minor uint8 `json:"minor"`
+ Build uint8 `json:"build"`
+}
+type Header struct {
+ FactorioVersion version16 `json:"factorio_version"`
+ Campaign string `json:"campaign"`
+ Name string `json:"name"`
+ BaseMod string `json:"base_mod"`
+ Difficulty uint8 `json:"difficulty"`
+ Finished bool `json:"finished"`
+ PlayerWon bool `json:"player_won"`
+ NextLevel string `json:"next_level"`
+ CanContinue bool `json:"can_continue"`
+ FinishedButContinuing bool `json:"finished_but_continuing"`
+ SavingReplay bool `json:"saving_replay"`
+ AllowNonAdminDebugOptions bool `json:"allow_non_admin_debug_options"`
+ LoadedFrom versionShort8 `json:"loaded_from"`
+ LoadedFromBuild uint16 `json:"loaded_from_build"`
+ AllowedCommads uint8 `json:"allowed_commads"`
+ NumMods uint32 `json:"num_mods"`
+ Mods []singleMod `json:"mods"`
+}
+type singleMod struct {
+ Name string `json:"name"`
+ Version versionShort8 `json:"version"`
+ CRC uint32 `json:"crc"`
+}
+
+var ErrorIncompatible = errors.New("incompatible save")
+var data Header
+var constraintGreaterThan016 *semver.Constraints
+
+func ReadHeader(filePath string) (Header, error) {
+ var err error
+ data = Header{}
+
+ constraintGreaterThan016, _ = semver.NewConstraint(">= 0.16.0")
+
+ datFile, err := openSave(filePath)
+ if err != nil {
+ log.Printf("error opening file: %s", err)
+ return data, err
+ }
+ defer datFile.Close()
+
+ data.FactorioVersion, err = readVersion16(datFile)
+ if err != nil {
+ log.Printf("Cant read FactorioVersion: %s", err)
+ return data, err
+ }
+
+ Constraint, _ := semver.NewConstraint("0.14.14 - 0.17.0")
+ Compatible, err := data.FactorioVersion.CheckCompatibility(Constraint)
+ if err != nil {
+ log.Printf("Error checking compatibility: %s", err)
+ return data, err
+ }
+ if !Compatible {
+ log.Printf("NOT COMPATIBLE Save-File")
+ log.Println(data)
+ return data, ErrorIncompatible
+ }
+
+ data.Campaign, err = readUTF8String(datFile, false)
+ if err != nil {
+ log.Printf("Cant read Campaign: %s", err)
+ return data, err
+ }
+
+ data.Name, err = readUTF8String(datFile, false)
+ if err != nil {
+ log.Printf("Cant read Name: %s", err)
+ return data, err
+ }
+
+ data.BaseMod, err = readUTF8String(datFile, false)
+ if err != nil {
+ log.Printf("Cant read BaseMod: %s", err)
+ return data, err
+ }
+
+ data.Difficulty, err = readUint8(datFile)
+ if err != nil {
+ log.Printf("Cant read Difficulty: %s", err)
+ return data, err
+ }
+
+ data.Finished, err = readBool(datFile)
+ if err != nil {
+ log.Printf("Couln't read Finished bool: %s", err)
+ return data, err
+ }
+
+ data.PlayerWon, err = readBool(datFile)
+ if err != nil {
+ log.Printf("Couldn't read PlayerWon: %s", err)
+ return data, err
+ }
+
+ data.NextLevel, err = readUTF8String(datFile, false)
+ if err != nil {
+ log.Printf("Couldn't read NextLevel: %s", err)
+ return data, err
+ }
+
+ data.CanContinue, err = readBool(datFile)
+ if err != nil {
+ log.Printf("Couldn't read CanContinue: %s", err)
+ return data, err
+ }
+
+ data.FinishedButContinuing, err = readBool(datFile)
+ if err != nil {
+ log.Printf("Couldn't read FinishedButContinuing: %s", err)
+ return data, err
+ }
+
+ data.SavingReplay, err = readBool(datFile)
+ if err != nil {
+ log.Printf("Couldn't read SavingReplay: %s", err)
+ return data, err
+ }
+
+ Used, err := data.FactorioVersion.CheckCompatibility(constraintGreaterThan016)
+ if err != nil {
+ log.Printf("Error checking if used: %s", err)
+ return data, err
+ }
+ if Used {
+ data.AllowNonAdminDebugOptions, err = readBool(datFile)
+ if err != nil {
+ log.Printf("Couldn't read allow_non_admin_debug_options: %s", err)
+ return data, err
+ }
+ }
+
+ data.LoadedFrom, err = readVersionShort8(datFile)
+ if err != nil {
+ log.Printf("Couldn't read LoadedFrom: %s", err)
+ return data, err
+ }
+
+ data.LoadedFromBuild, err = readUint16(datFile)
+ if err != nil {
+ log.Printf("Couldn't read LoadedFromBuild: %s", err)
+ return data, err
+ }
+
+ data.AllowedCommads, err = readUint8(datFile)
+ if err != nil {
+ log.Printf("Couldn't read AllowedCommands: %s", err)
+ return data, err
+ }
+
+ New, err := data.FactorioVersion.CheckCompatibility(constraintGreaterThan016)
+ if err != nil {
+ log.Printf("error checking compatibility: %s", err)
+ return data, err
+ }
+
+ if New {
+ numMods8, err2 := readUint8(datFile) //TODO read Optim. int
+ err = err2
+ data.NumMods = uint32(numMods8)
+ } else {
+ data.NumMods, err = readUint32(datFile)
+ }
+ if err != nil {
+ log.Printf("Couldn't read NumMods: %s", err)
+ return data, err
+ }
+
+ for i := uint32(0); i < data.NumMods; i++ {
+ SingleMod, err := readSingleMod(datFile)
+ if err != nil {
+ log.Printf("Couldn't read SingleMod: %s", err)
+ return data, err
+ }
+
+ data.Mods = append(data.Mods, SingleMod)
+ }
+
+ return data, nil
+}
+
+func readUTF8String(file io.ReadCloser, forcedOptim bool) (string, error) {
+ var err error
+
+ New, err := data.FactorioVersion.CheckCompatibility(constraintGreaterThan016)
+ if err != nil {
+ log.Printf("Couldn't checkCompatibility: %s", err)
+ return "", err
+ }
+
+ var infoByteStringLength uint32
+ if New || forcedOptim {
+ infoByteInt8, err2 := readUint8(file) //TODO read optimized int
+ err = err2
+ infoByteStringLength = uint32(infoByteInt8)
+ } else {
+ infoByteStringLength, err = readUint32(file) //uint32
+ }
+
+ if err != nil {
+ log.Printf("Couldn't read infoByteStringLength: %s", err)
+ return "", err
+ }
+
+ stringBytes := make([]byte, infoByteStringLength)
+ _, err = file.Read(stringBytes)
+ if err != nil {
+ log.Printf("error reading bytes: %s", err)
+ return "", err
+ }
+ finalizedString := string(stringBytes[:])
+
+ return finalizedString, nil
+}
+
+func readUint8(file io.ReadCloser) (uint8, error) {
+ var err error
+ var temp [1]byte
+ _, err = file.Read(temp[:])
+ if err != nil {
+ log.Printf("error reading byte: %s", err)
+ return 0, nil
+ }
+
+ return uint8(temp[0]), nil
+}
+
+func readUint16(file io.ReadCloser) (uint16, error) {
+ var err error
+ var temp [2]byte
+
+ _, err = file.Read(temp[:])
+ if err != nil {
+ log.Printf("error reading bytes: %s", err)
+ return 0, err
+ }
+
+ return binary.LittleEndian.Uint16(temp[:]), nil
+}
+
+func readUint32(file io.ReadCloser) (uint32, error) {
+ var err error
+ var temp [4]byte
+
+ _, err = file.Read(temp[:])
+ if err != nil {
+ log.Printf("error reading bytes: %s", err)
+ return 0, err
+ }
+
+ return binary.LittleEndian.Uint32(temp[:]), nil
+}
+
+func readBool(file io.ReadCloser) (bool, error) {
+ byteAsInt, err := readUint8(file)
+ if err != nil {
+ log.Printf("error loading Uint8: %s", err)
+ return false, err
+ }
+
+ return byteAsInt != 0, nil
+}
+
+func readVersion16(file io.ReadCloser) (version16, error) {
+ var Version version16
+ var VersionShort versionShort16
+ var err error
+
+ VersionShort, err = readVersionShort16(file)
+ if err != nil {
+ log.Printf("error reading VersionShort")
+ return Version, err
+ }
+
+ Version.Major = VersionShort.Major
+ Version.Minor = VersionShort.Minor
+ Version.Build = VersionShort.Build
+
+ Version.Revision, err = readUint16(file)
+ if err != nil {
+ log.Printf("error reading revision: %s", err)
+ return Version, err
+ }
+
+ return Version, nil
+}
+
+func readVersionShort16(file io.ReadCloser) (versionShort16, error) {
+ var Version versionShort16
+ var err error
+
+ Version.Major, err = readUint16(file)
+ if err != nil {
+ log.Printf("error reading major: %s", err)
+ return Version, err
+ }
+
+ Version.Minor, err = readUint16(file)
+ if err != nil {
+ log.Printf("error reading minor: %s", err)
+ return Version, err
+ }
+
+ Version.Build, err = readUint16(file)
+ if err != nil {
+ log.Printf("error reading build: %s", err)
+ return Version, err
+ }
+
+ return Version, err
+}
+
+func readVersionShort8(file io.ReadCloser) (versionShort8, error) {
+ var Version versionShort8
+ var err error
+
+ Version.Major, err = readUint8(file)
+ if err != nil {
+ log.Printf("error reading major: %s", err)
+ return Version, err
+ }
+
+ Version.Minor, err = readUint8(file)
+ if err != nil {
+ log.Printf("error reading minor: %s", err)
+ return Version, err
+ }
+
+ Version.Build, err = readUint8(file)
+ if err != nil {
+ log.Printf("error reading build: %s", err)
+ return Version, err
+ }
+
+ return Version, nil
+}
+
+func readSingleMod(file io.ReadCloser) (singleMod, error) {
+ var Mod singleMod
+ var err error
+
+ Mod.Name, err = readUTF8String(file, true)
+ if err != nil {
+ log.Printf("error loading modName: %s", err)
+ return Mod, err
+ }
+
+ Mod.Version, err = readVersionShort8(file)
+ if err != nil {
+ log.Printf("error loading modVersion: %s", err)
+ return Mod, err
+ }
+
+ Constraint, _ := semver.NewConstraint("> 0.15.0")
+ Used, err := data.FactorioVersion.CheckCompatibility(Constraint)
+ if err != nil {
+ log.Printf("Error checking used of CRC: %s", err)
+ return Mod, err
+ }
+ if Used {
+ Mod.CRC, err = readUint32(file)
+ if err != nil {
+ log.Printf("error loading CRC: %s", err)
+ return Mod, err
+ }
+ }
+
+ return Mod, err
+}
+
+func (Version *versionShort16) CheckCompatibility(constraints *semver.Constraints) (bool, error) {
+ Ver, err := semver.NewVersion(strconv.Itoa(int(Version.Major)) + "." + strconv.Itoa(int(Version.Minor)) + "." + strconv.Itoa(int(Version.Build)))
+ if err != nil {
+ log.Printf("Error creating semver-version: %s", err)
+ return false, err
+ }
+
+ return constraints.Check(Ver), nil
+}
diff --git a/src/vendor/factorioSave/openSave.go b/src/vendor/factorioSave/openSave.go
new file mode 100644
index 00000000..bd5c426d
--- /dev/null
+++ b/src/vendor/factorioSave/openSave.go
@@ -0,0 +1,35 @@
+package factorioSave
+
+import (
+ "archive/zip"
+ "log"
+ "io"
+ "errors"
+)
+
+var ErrorLevelDatNotFound = errors.New("couldn't find level.dat")
+
+func openSave(filePath string) (io.ReadCloser, error) {
+ var err error
+
+ saveFile, err := zip.OpenReader(filePath)
+ if err != nil {
+ log.Printf("error opening saveFile: %s", err)
+ return nil, err
+ }
+
+ for _, singleFile := range saveFile.File {
+ if singleFile.FileInfo().Name() == "level.dat" {
+ //open level.dat
+ rc, err := singleFile.Open()
+ if err != nil {
+ log.Printf("Couldn't open level.dat: %s", err)
+ return nil, err
+ }
+
+ return rc, nil
+ }
+ }
+
+ return nil, ErrorLevelDatNotFound
+}
diff --git a/ui/App/components/Mods/ModLoadSave.jsx b/ui/App/components/Mods/ModLoadSave.jsx
new file mode 100644
index 00000000..cb6d1bfc
--- /dev/null
+++ b/ui/App/components/Mods/ModLoadSave.jsx
@@ -0,0 +1,160 @@
+import React from 'react';
+import ReactDOMServer from 'react-dom/server';
+import {instanceOfModsContent} from "./ModsPropTypes";
+
+class ModLoadSave extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.loadMods = this.loadMods.bind(this);
+ this.loadModsSwalHandler = this.loadModsSwalHandler.bind(this);
+ }
+
+ componentDidMount() {
+ //Load Saves
+ this.props.getSaves();
+ }
+
+ loadMods(e) {
+ e.preventDefault();
+
+ $.ajax({
+ url: "/api/mods/save/load",
+ method: "POST",
+ data: $(e.target).serialize(),
+ dataType: "JSON",
+ success: (data) => {
+ let checkboxes = [];
+
+ data.data.mods.forEach((mod) => {
+ if(mod.name == "base") return;
+
+ let modVersion = mod.version.major + "." + mod.version.minor + "." + mod.version.build;
+ let singleCheckbox =