2026-06-08 19:31:16 +08:00

8.9 KiB

name: th1-game-archive description: TH1 project-specific guide for the new local save/archive system: GameRecord, GameRecordKind Ended/Manual/Quick, GameArchiveManager, begin/quick_continue/continue/end files, single-player and multiplayer resume from records, manual saves, quick saves, end records, archive validation, spectator/OSS/bug-report archive packaging, and removal of legacy map_archive/PlayerPrefs continue flows. Use whenever Codex works on TH1 continue game, save/load, game records, local archives, surrender/end save behavior, bug report save attachments, spectator archive loading, or any change that might affect single/multiplayer resume reliability.

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.cs
  • Unity/Assets/Scripts/TH1_Logic/GameRecord/GameRecordManager.cs
  • Unity/Assets/Scripts/TH1_Logic/Core/Main.cs
  • Unity/Assets/Scripts/TH1_Data/MapData.cs
  • Unity/Assets/Scripts/TH1_Logic/Core/GameLogic.cs
  • Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMenuView.cs
  • Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMultiplayView.cs
  • Unity/Assets/Scripts/TH1_Logic/Editor/SpectatorEditorWindow.cs
  • Unity/Assets/Scripts/TH1_Logic/Oss/PlayerBugReportService.cs
  • Tools/PlayerBugViewer/player_bug_viewer.py when 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 indexes BeginArchiveId and EndArchiveId; it is not resumable.
  • Manual: player-created general save. It indexes BeginArchiveId and ContinueArchiveId; it is resumable and can have many records.
  • Quick: automatic quick save. It indexes BeginArchiveId and ContinueArchiveId; it is resumable, but only one exists globally.

Archive files are grouped by folder:

  • Config/GameArchives/begin/{beginArchiveId}.dat
  • Config/GameArchives/quick_continue/quick.dat
  • Config/GameArchives/continue/{continueArchiveId}.dat
  • Config/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 MapData files are stored by default when the normal begin/quick_continue/continue/end flow writes them.
  • GameRecord.DiscardArchiveIndex is used by cleaned Ended records so they stop protecting begin/end archive files during map cleanup.
  • Manual cleanup deletes the record; it does not need a discard flag.
  • Quick cleanup is a no-op through the user-facing cleanup interface.
  • Append new GameRecord fields 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 GameStart broadcast succeeds.
  • New multiplayer member game: write a local begin after NetStartGame succeeds. This supports local Ended records and replay for players who were present from game start.
  • Ended game: FinishState.Enter calls GameArchiveManager.SaveEndRecord(Main.MapData).
  • Per-turn quick save: MapData.RefreshTurn calls SaveQuickContinueRecord.
  • 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 delete MapData files here.
    • Ended: keep the record but set DiscardArchiveIndex = true; do not delete MapData files here.
  • GameArchiveManager.CleanupUnlinkedLocalMapData() is the only map-file cleanup flow. It builds the valid archive index set from records, skips Ended records with DiscardArchiveIndex == true, and deletes local MapData files not referenced by the valid index.

Archive cleanup should cover both current and legacy local data:

  • current Config/GameArchives/begin, quick_continue, continue, and end;
  • legacy Config/map_archive_begin|continue|end[_multi]_{MapID}.dat and sidecars;
  • old Config/begin, continue, end, begincontinue / begin_continue, and quick_continue folders 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 Quick or Manual continue archive via GameArchiveManager.TryLoadContinueArchive;
  • rejects null records, bad archive files, wrong NetMode, and surrendered continue saves;
  • calls GameArchiveManager.SetActiveRecord(record) so future quick/manual/end saves keep using record.BeginArchiveId;
  • dispatches by record.NetMode to 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 PlayerPrefs keys Archive / MultiArchive
  • no map_archive_*.dat scanning 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 TryLoadContinueArchive or HasUsableResumeArchive passes.
  • 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 GameStart succeeds.
  • Do not let a failed resume leave GameArchiveManager.CurrentBeginArchiveId bound to the attempted record.
  • Do not delete manual saves on game end.
  • Do not write end from action execution before settlement refresh; let FinishState.Enter own end saving.