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:
Sean Callahan 2019-01-11 21:42:24 -08:00
parent 27d78ea9c4
commit a46f69fda4
4 changed files with 155 additions and 93 deletions

View File

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

View File

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

View File

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

View File

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