package fmm import ( "archive/zip" "compress/zlib" "errors" "path" ) type SaveFileInfo struct { MapVersion Version BranchVersion uint8 CampaignName string LevelName string ScenarioModName string Difficulty uint8 Finished bool PlayerWon bool NextLevel string CanContinue bool FinishedButContinuing bool SavingReplay bool AllowNonAdminDebugOptions bool ScenarioVersion Version ScenarioBuildVersion uint32 AllowedCommands AllowedCommands LargeBlueprintSize uint32 ActiveMods []ModIdent StartupModSettingsCrc uint32 StartupModSettings PropertyTree } // Returns information about the given save file. func ParseSaveFile(filepath string) (SaveFileInfo, error) { info := SaveFileInfo{} zipReader, err := zip.OpenReader(filepath) if err != nil { return info, err } defer zipReader.Close() var dat *zip.File compressed := false for _, file := range zipReader.File { name := path.Base(file.Name) if name == "level.dat" || name == "level.dat0" { dat = file compressed = name == "level.dat0" break } } if dat == nil { return info, errors.New("invalid save file: could not locate level data") } rawReader, err := dat.Open() if err != nil { return info, err } if compressed { rawReader, err = zlib.NewReader(rawReader) if err != nil { return info, err } } defer rawReader.Close() // TODO: Add tests for various save files. r := newDatReader(rawReader) info.MapVersion = r.ReadVersionUnoptimized() info.BranchVersion = r.ReadUint8() info.CampaignName = r.ReadString() info.LevelName = r.ReadString() info.ScenarioModName = r.ReadString() info.Difficulty = r.ReadUint8() info.Finished = r.ReadBool() info.PlayerWon = r.ReadBool() info.NextLevel = r.ReadString() info.CanContinue = r.ReadBool() info.FinishedButContinuing = r.ReadBool() info.SavingReplay = r.ReadBool() info.AllowNonAdminDebugOptions = r.ReadBool() info.ScenarioVersion = r.ReadVersionOptimized(false) // Why is this uint32/uint16 instead of uint16/uint8!? if info.MapVersion.Cmp(&Version{1, 2, 0, 127}) == VersionGt { info.ScenarioBuildVersion = r.ReadUint32() } else { info.ScenarioBuildVersion = uint32(r.ReadUint16()) } info.AllowedCommands = r.ReadAllowedCommands() if (info.MapVersion.Cmp(&Version{1, 2, 8, 8}) == VersionGt) { info.LargeBlueprintSize = r.ReadUint32() } numMods := r.ReadUint32Optimized() info.ActiveMods = make([]ModIdent, numMods) for i := uint32(0); i < numMods; i++ { info.ActiveMods[i] = r.ReadModWithCRC() } info.StartupModSettingsCrc = r.ReadUint32() info.StartupModSettings = r.ReadPropertyTree() if r.err != nil { return info, errors.Join(errors.New("failed to parse save file"), r.err) } return info, nil }