8.9 KiB
TH1 Game Archive
Core Rule
The current save system is GameRecord-indexed. Continue/resume must start from a GameRecord, not by scanning files or using MapID.
GameRecord is only an index and display row. The real MapData bytes live under Config/GameArchives.
First Files To Read
Unity/Assets/Scripts/TH1_Logic/GameArchive/GameArchiveManager.csUnity/Assets/Scripts/TH1_Logic/GameRecord/GameRecordManager.csUnity/Assets/Scripts/TH1_Logic/Core/Main.csUnity/Assets/Scripts/TH1_Data/MapData.csUnity/Assets/Scripts/TH1_Logic/Core/GameLogic.csUnity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMenuView.csUnity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMultiplayView.csUnity/Assets/Scripts/TH1_Logic/Editor/SpectatorEditorWindow.csUnity/Assets/Scripts/TH1_Logic/Oss/PlayerBugReportService.csTools/PlayerBugViewer/player_bug_viewer.pywhen changing player bug report archive restore.
If the change touches multiplayer host resume or GameStart, also use th1-network-sync.
If the change touches surrender or other action execution side effects, also use th1-action-logic.
Data Model
GameRecordKind has three business categories:
Ended: complete finished match record. It indexesBeginArchiveIdandEndArchiveId; it is not resumable.Manual: player-created general save. It indexesBeginArchiveIdandContinueArchiveId; it is resumable and can have many records.Quick: automatic quick save. It indexesBeginArchiveIdandContinueArchiveId; it is resumable, but only one exists globally.
Archive files are grouped by folder:
Config/GameArchives/begin/{beginArchiveId}.datConfig/GameArchives/quick_continue/quick.datConfig/GameArchives/continue/{continueArchiveId}.datConfig/GameArchives/end/{endArchiveId}.dat
Quick uses fixed record id and file id quick. Manual/end/begin ids are GUID-like strings from GameArchiveManager.
Record archive discard index:
- All
MapDatafiles are stored by default when the normal begin/quick_continue/continue/end flow writes them. GameRecord.DiscardArchiveIndexis used by cleanedEndedrecords so they stop protecting begin/end archive files during map cleanup.Manualcleanup deletes the record; it does not need a discard flag.Quickcleanup is a no-op through the user-facing cleanup interface.- Append new
GameRecordfields at the end only. If an old serialized field is removed from business logic, keep a serialization placeholder so later fields do not shift.
Write Flow
- New single-player game: write begin after map generation, before first turn refresh.
- New multiplayer host game: write begin only after
GameStartbroadcast succeeds. - New multiplayer member game: write a local begin after
NetStartGamesucceeds. This supports localEndedrecords and replay for players who were present from game start. - Ended game:
FinishState.EntercallsGameArchiveManager.SaveEndRecord(Main.MapData). - Per-turn quick save:
MapData.RefreshTurncallsSaveQuickContinueRecord. - Manual save: UI or upper layer should call
GameArchiveManager.SaveManualGameRecord(recordName). - Record cleanup: call
GameArchiveManager.CleanupGameRecord(record). This must only update/delete the record, not archive files.
Do not save archives directly from surrender action. Surrender should mutate game state through the action flow; AfterExecute refreshes settlement, GameLogic.Update enters Finished, and FinishState.Enter writes end and clears quick.
SaveEndRecord writes end, creates an Ended record except for Tutor, and deletes the current begin's quick record/file. Manual saves survive game end until the player deletes them.
Record And Map Cleanup
Keep record operations and archive-file operations separate:
GameArchiveManager.CleanupGameRecord(record)is the only user-facing record cleanup flow:Quick: no-op, return false.Manual: delete the record only; do not deleteMapDatafiles here.Ended: keep the record but setDiscardArchiveIndex = true; do not deleteMapDatafiles here.
GameArchiveManager.CleanupUnlinkedLocalMapData()is the only map-file cleanup flow. It builds the valid archive index set from records, skipsEndedrecords withDiscardArchiveIndex == true, and deletes localMapDatafiles not referenced by the valid index.
Archive cleanup should cover both current and legacy local data:
- current
Config/GameArchives/begin,quick_continue,continue, andend; - legacy
Config/map_archive_begin|continue|end[_multi]_{MapID}.datand sidecars; - old
Config/begin,continue,end,begincontinue/begin_continue, andquick_continuefolders when they contain archive payloads.
Resume Flow
Public resume should go through:
Main.Instance.ResumeMatch(GameRecord record, MapData prereadMap = null)
That method:
- validates/loads the
QuickorManualcontinue archive viaGameArchiveManager.TryLoadContinueArchive; - rejects null records, bad archive files, wrong
NetMode, and surrendered continue saves; - calls
GameArchiveManager.SetActiveRecord(record)so future quick/manual/end saves keep usingrecord.BeginArchiveId; - dispatches by
record.NetModeto single-player resume or host multiplayer resume.
Resume methods must not create a new begin. New games create begin; record resume reuses the record's begin.
For UI availability:
- single-player quick resume button:
GameArchiveManager.HasQuickResumeArchive(NetMode.Single) - multiplayer host resume toggle:
GameArchiveManager.HasQuickResumeArchive(NetMode.Multi) - per-record validation:
GameArchiveManager.HasUsableResumeArchive(record)
These checks validate only the relevant record/file, not all local archives.
Read/Export Flows
Use GameArchiveManager helpers:
- Continue:
TryLoadContinueArchive(record, out map) - Begin:
TryLoadBeginArchive(record, out map) - End:
TryLoadEndArchive(record, out map) - Current begin for OSS end upload:
TryLoadCurrentBeginArchive(expectedMode, out map) - File path for packaging:
TryGetArchivePath(kind, archiveId, out path)
Spectator tools, OSS collect upload, and player bug reports should build archive pairs from GameRecordData.Records, then use the helper APIs above. Do not parse archive filenames.
Player bug report zip manifests include archiveFolder, and the viewer restores files under Config/GameArchives/{archiveFolder}. The Python viewer cannot safely synthesize game_record.dat because it is MemoryPack data.
Compatibility
GameRecord and GameRecordData are MemoryPack types. Do not reorder existing fields. New fields must be appended at the end, and compatibility-sensitive MemoryPack changes require explicit user confirmation.
MapID is legacy. Keep it for old record display/compatibility if needed, but do not use it to locate current archives or resume games.
The legacy flow is removed:
- no
MapData.SaveMapData - no
MapData.GetMapData - no
MapData.HasMapArchive - no
PlayerPrefskeysArchive/MultiArchive - no
map_archive_*.datscanning or filename parsing - no resume by
mapId
Checks Before Finishing
Run:
dotnet build Unity/Assembly-CSharp.csproj --no-restore
Also run editor build if editor windows/tools changed:
dotnet build Unity/Assembly-CSharp-Editor.csproj --no-restore
Search for legacy regressions:
rg "SaveMapData|GetMapData\\(|HasMapArchive|HasArchive\\(|HasMultiArchive\\(" Unity/Assets/Scripts -n
rg "map_archive|PlayerPrefs\\.(SetInt|GetInt)\\(\"(Archive|MultiArchive)\"" Unity/Assets/Scripts Tools MD -n
rg "useCurrentArchiveSession|ResumeMatch\\(uint|MainMemberResumeMatch\\(" Unity/Assets/Scripts -n
Pitfalls
- Do not scan all local saves to build a continue list; use GameRecord lists and validate one selected record.
- Do not show a record as resumable until
TryLoadContinueArchiveorHasUsableResumeArchivepasses. - Do not let single-player and multiplayer quick resumes share UI state; always filter by
NetMode. - Do not close multiplayer room UI or advance local host state until
GameStartsucceeds. - Do not let a failed resume leave
GameArchiveManager.CurrentBeginArchiveIdbound to the attempted record. - Do not delete manual saves on game end.
- Do not write end from action execution before settlement refresh; let
FinishState.Enterown end saving.