mirror of
https://github.com/OpenFactorioServerManager/factorio-server-manager.git
synced 2025-01-08 04:04:53 +02:00
Make Version four parts; add compatibility for older save file versions
* Extended Version type to support build versions. * Removed special io.Stringer and marshallers on version64. * Added obsolete `stats` field for save files 0.13.0.42 and older. * Normalized `allowed_commands` values on save files 0.13.0.87 and older. * Added `OpenArchiveFile` function to open an archive sub file. * Altered `LoadModsFromSaveHandler` to use new `OpenArchiveFile` function.
This commit is contained in:
parent
27d78ea9c4
commit
a46f69fda4
@ -1,48 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type archiveFile struct {
|
||||
io.ReadCloser
|
||||
archive io.Closer
|
||||
}
|
||||
|
||||
func (af *archiveFile) Close() error {
|
||||
if af.ReadCloser != nil {
|
||||
if err := af.ReadCloser.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if af.archive != nil {
|
||||
if err := af.archive.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func OpenArchiveFile(path string, name string) (r io.ReadCloser, err error) {
|
||||
archive, err := zip.OpenReader(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := &archiveFile{archive: archive}
|
||||
|
||||
for _, file := range archive.File {
|
||||
if file.FileInfo().Name() == name {
|
||||
f.ReadCloser, err = file.Open()
|
||||
if err != nil {
|
||||
archive.Close()
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("file not found")
|
||||
}
|
||||
|
||||
type SaveHeader struct {
|
||||
FactorioVersion version64 `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 version24 `json:"loaded_from"`
|
||||
LoadedFromBuild uint16 `json:"loaded_from_build"`
|
||||
AllowedCommands uint8 `json:"allowed_commands"`
|
||||
Mods []Mod `json:"mods"`
|
||||
FactorioVersion Version `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 Version `json:"loaded_from"`
|
||||
LoadedFromBuild uint16 `json:"loaded_from_build"`
|
||||
AllowedCommands uint8 `json:"allowed_commands"`
|
||||
Stats map[byte][]map[uint16]uint32 `json:"stats,omitempty"`
|
||||
Mods []Mod `json:"mods"`
|
||||
}
|
||||
|
||||
type Mod struct {
|
||||
Name string `json:"name"`
|
||||
Version version24 `json:"version"`
|
||||
CRC uint32 `json:"crc"`
|
||||
Name string `json:"name"`
|
||||
Version Version `json:"version"`
|
||||
CRC uint32 `json:"crc"`
|
||||
}
|
||||
|
||||
func (h *SaveHeader) ReadFrom(r io.Reader) (err error) {
|
||||
var scratch [8]byte
|
||||
|
||||
var fv version64
|
||||
_, err = r.Read(scratch[:8])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.FactorioVersion.UnmarshalBinary(scratch[:8]); err != nil {
|
||||
if err := fv.UnmarshalBinary(scratch[:8]); err != nil {
|
||||
return fmt.Errorf("read FactorioVersion: %v", err)
|
||||
}
|
||||
h.FactorioVersion = Version(fv)
|
||||
|
||||
atLeast016 := !h.FactorioVersion.Less(Version{0, 16, 0})
|
||||
atLeast016 := !h.FactorioVersion.Less(Version{0, 16, 0, 0})
|
||||
|
||||
h.Campaign, err = readString(r, atLeast016)
|
||||
if err != nil {
|
||||
@ -82,7 +128,7 @@ func (h *SaveHeader) ReadFrom(r io.Reader) (err error) {
|
||||
return fmt.Errorf("read NextLevel: %v", err)
|
||||
}
|
||||
|
||||
if !h.FactorioVersion.Less(Version{0, 12, 0}) {
|
||||
if !h.FactorioVersion.Less(Version{0, 12, 0, 0}) {
|
||||
_, err = r.Read(scratch[:1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read CanContinue: %v", err)
|
||||
@ -110,13 +156,15 @@ func (h *SaveHeader) ReadFrom(r io.Reader) (err error) {
|
||||
h.AllowNonAdminDebugOptions = scratch[0] != 0
|
||||
}
|
||||
|
||||
var loadedFrom version24
|
||||
_, err = r.Read(scratch[:3])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.LoadedFrom.UnmarshalBinary(scratch[:3]); err != nil {
|
||||
if err := loadedFrom.UnmarshalBinary(scratch[:3]); err != nil {
|
||||
return fmt.Errorf("read LoadedFrom: %v", err)
|
||||
}
|
||||
h.LoadedFrom = Version(loadedFrom)
|
||||
|
||||
_, err = r.Read(scratch[:2])
|
||||
if err != nil {
|
||||
@ -129,6 +177,20 @@ func (h *SaveHeader) ReadFrom(r io.Reader) (err error) {
|
||||
return fmt.Errorf("read AllowedCommands: %v", err)
|
||||
}
|
||||
h.AllowedCommands = scratch[0]
|
||||
if h.FactorioVersion.Less(Version{0, 13, 0, 87}) {
|
||||
if h.AllowedCommands == 0 {
|
||||
h.AllowedCommands = 2
|
||||
} else {
|
||||
h.AllowedCommands = 1
|
||||
}
|
||||
}
|
||||
|
||||
if h.FactorioVersion.Less(Version{0, 13, 0, 42}) {
|
||||
h.Stats, err = h.readStats(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read Stats: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var n uint32
|
||||
if atLeast016 {
|
||||
@ -146,7 +208,7 @@ func (h *SaveHeader) ReadFrom(r io.Reader) (err error) {
|
||||
|
||||
for i := uint32(0); i < n; i++ {
|
||||
var m Mod
|
||||
if err = (&m).ReadFrom(r, h.FactorioVersion.Version); err != nil {
|
||||
if err = (&m).ReadFrom(r, Version(h.FactorioVersion)); err != nil {
|
||||
return fmt.Errorf("read mod: %v", err)
|
||||
}
|
||||
h.Mods = append(h.Mods, m)
|
||||
@ -197,6 +259,48 @@ func readString(r io.Reader, optimized bool) (s string, err error) {
|
||||
return string(d), nil
|
||||
}
|
||||
|
||||
func (h SaveHeader) readStats(r io.Reader) (stats map[byte][]map[uint16]uint32, err error) {
|
||||
var scratch [4]byte
|
||||
stats = make(map[byte][]map[uint16]uint32)
|
||||
|
||||
_, err = r.Read(scratch[:4])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := binary.LittleEndian.Uint32(scratch[:4])
|
||||
for i := uint32(0); i < n; i++ {
|
||||
_, err := r.Read(scratch[:1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read stat %d force id: %v", i, err)
|
||||
}
|
||||
id := scratch[1]
|
||||
for j := 0; j < 3; j++ {
|
||||
st := make(map[uint16]uint32)
|
||||
_, err = r.Read(scratch[:4])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read stat %d (id %d) length: %v", i, id, err)
|
||||
}
|
||||
length := binary.LittleEndian.Uint32(scratch[:4])
|
||||
for k := uint32(0); k < length; k++ {
|
||||
_, err = r.Read(scratch[:2])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read stat %d (id %d; index %d) key: %v", i, id, k, err)
|
||||
}
|
||||
key := binary.LittleEndian.Uint16(scratch[:2])
|
||||
_, err = r.Read(scratch[:4])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read stat %d (id %d; index %d) val: %v", i, id, k, err)
|
||||
}
|
||||
val := binary.LittleEndian.Uint32(scratch[:4])
|
||||
st[key] = val
|
||||
}
|
||||
stats[id] = append(stats[id], st)
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (m *Mod) ReadFrom(r io.Reader, game Version) (err error) {
|
||||
m.Name, err = readString(r, true)
|
||||
if err != nil {
|
||||
@ -204,15 +308,17 @@ func (m *Mod) ReadFrom(r io.Reader, game Version) (err error) {
|
||||
}
|
||||
|
||||
var scratch [4]byte
|
||||
var version version24
|
||||
_, err = r.Read(scratch[:3])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.Version.UnmarshalBinary(scratch[:3]); err != nil {
|
||||
if err := version.UnmarshalBinary(scratch[:3]); err != nil {
|
||||
return fmt.Errorf("read Version: %v", err)
|
||||
}
|
||||
m.Version = Version(version)
|
||||
|
||||
if game.Greater(Version{0, 15, 0}) {
|
||||
if game.Greater(Version{0, 15, 0, 91}) {
|
||||
_, err = r.Read(scratch[:4])
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -572,46 +572,20 @@ func LoadModsFromSaveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
SaveFile := r.FormValue("saveFile")
|
||||
|
||||
path := filepath.Join(config.FactorioSavesDir, SaveFile)
|
||||
archive, err := zip.OpenReader(path)
|
||||
f, err := OpenArchiveFile(path, "level.dat")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("cannot open save file: %v", err)
|
||||
resp.Data = "Error opening save file"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in loadModsFromSave: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
var data io.ReadCloser
|
||||
for _, file := range archive.File {
|
||||
if file.FileInfo().Name() == "level.dat" {
|
||||
data, err = file.Open()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("cannot open save level file: %v", err)
|
||||
resp.Data = "Error opening save file"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in loadModsFromSave: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer data.Close()
|
||||
}
|
||||
}
|
||||
if data == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Println("could not find level.dat file in save file")
|
||||
log.Printf("cannot open save level file: %v", err)
|
||||
resp.Data = "Error opening save file"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in loadModsFromSave: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(data)
|
||||
err = header.ReadFrom(f)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("cannot read save header: %v", err)
|
||||
|
@ -9,13 +9,13 @@ import (
|
||||
)
|
||||
|
||||
// NilVersion represents an empty version number
|
||||
var NilVersion = Version{0, 0, 0}
|
||||
var NilVersion = Version{0, 0, 0, 0}
|
||||
|
||||
// Version represents a semantic version
|
||||
type Version [3]uint
|
||||
// Version represents a semantic version and build number
|
||||
type Version [4]uint
|
||||
|
||||
func (v Version) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v[0], v[1], v[2])
|
||||
return fmt.Sprintf("%d.%d.%d.%d", v[0], v[1], v[2], v[3])
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaller for Version
|
||||
@ -25,7 +25,7 @@ func (v Version) MarshalText() (text []byte, err error) {
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaller for Version
|
||||
func (v *Version) UnmarshalText(text []byte) error {
|
||||
parts := strings.SplitN(string(text), ".", 3)
|
||||
parts := strings.SplitN(string(text), ".", 4)
|
||||
for i, part := range parts {
|
||||
p, err := strconv.ParseUint(part, 10, 32)
|
||||
if err != nil {
|
||||
@ -38,7 +38,7 @@ func (v *Version) UnmarshalText(text []byte) error {
|
||||
|
||||
// Equals returns true if both version are equal
|
||||
func (v Version) Equals(b Version) bool {
|
||||
return v[0] == b[0] && v[1] == b[1] && v[2] == b[2]
|
||||
return v[0] == b[0] && v[1] == b[1] && v[2] == b[2] && v[3] == b[3]
|
||||
}
|
||||
|
||||
// Less returns true if the receiver version is less than the argument version
|
||||
@ -50,6 +50,8 @@ func (v Version) Less(b Version) bool {
|
||||
return true
|
||||
case v[0] == b[0] && v[1] == b[1] && v[2] < b[2]:
|
||||
return true
|
||||
case v[0] == b[0] && v[1] == b[1] && v[2] == b[2] && v[3] < b[3]:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@ -122,17 +124,14 @@ func (v *version48) UnmarshalBinary(data []byte) error {
|
||||
}
|
||||
|
||||
// version64 is the 64-bit (16, 16, 16, 16) version structure with build component.
|
||||
type version64 struct {
|
||||
Version
|
||||
Build uint
|
||||
}
|
||||
type version64 Version
|
||||
|
||||
func (v version64) MarshalBinary() (data []byte, err error) {
|
||||
data = make([]byte, 8)
|
||||
binary.LittleEndian.PutUint16(data[0:2], uint16(v.Version[0]))
|
||||
binary.LittleEndian.PutUint16(data[2:4], uint16(v.Version[1]))
|
||||
binary.LittleEndian.PutUint16(data[4:6], uint16(v.Version[2]))
|
||||
binary.LittleEndian.PutUint16(data[6:8], uint16(v.Build))
|
||||
binary.LittleEndian.PutUint16(data[0:2], uint16(v[0]))
|
||||
binary.LittleEndian.PutUint16(data[2:4], uint16(v[1]))
|
||||
binary.LittleEndian.PutUint16(data[4:6], uint16(v[2]))
|
||||
binary.LittleEndian.PutUint16(data[6:8], uint16(v[3]))
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@ -140,25 +139,9 @@ func (v *version64) UnmarshalBinary(data []byte) error {
|
||||
if len(data) < 8 {
|
||||
return errors.New("version64.UnmarshalBinary: too few bytes")
|
||||
}
|
||||
v.Version[0] = uint(binary.LittleEndian.Uint16(data[0:2]))
|
||||
v.Version[1] = uint(binary.LittleEndian.Uint16(data[2:4]))
|
||||
v.Version[2] = uint(binary.LittleEndian.Uint16(data[4:6]))
|
||||
v.Build = uint(binary.LittleEndian.Uint16(data[6:8]))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v version64) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", v.Version[0], v.Version[1], v.Version[2], v.Build)
|
||||
}
|
||||
|
||||
func (v *version64) UnmarshalText(text []byte) error {
|
||||
parts := strings.SplitN(string(text), ".", 4)
|
||||
for i, part := range parts {
|
||||
p, err := strconv.ParseUint(part, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Version[i] = uint(p)
|
||||
}
|
||||
v[0] = uint(binary.LittleEndian.Uint16(data[0:2]))
|
||||
v[1] = uint(binary.LittleEndian.Uint16(data[2:4]))
|
||||
v[2] = uint(binary.LittleEndian.Uint16(data[4:6]))
|
||||
v[3] = uint(binary.LittleEndian.Uint16(data[6:8]))
|
||||
return nil
|
||||
}
|
||||
|
@ -30,15 +30,14 @@ class ModLoadSave extends React.Component {
|
||||
data.data.mods.forEach((mod) => {
|
||||
if(mod.name == "base") return;
|
||||
|
||||
let modVersion = mod.version[0] + "." + mod.version[1] + "." + mod.version[2];
|
||||
let singleCheckbox = <tr key={mod.name}>
|
||||
<td>
|
||||
{mod.name}
|
||||
<input type="hidden" name="mod_name" value={mod.name}/>
|
||||
</td>
|
||||
<td>
|
||||
{modVersion}
|
||||
<input type="hidden" name="mod_version" value={modVersion}/>
|
||||
{mod.version}
|
||||
<input type="hidden" name="mod_version" value={mod.version}/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user