diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 4bab440..c52d780 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -782,7 +782,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe `bBackupBeforeSave` enabled by default; before overwriting an existing slot, `WriteSave` copies the current `.sav` into `Saved/SaveGames/Backups` with a UTC timestamp. -- [ ] Add recovery plan for corrupted save. +- [x] Add recovery plan for corrupted save. Added + `Docs/Ops/PersistenceSaveRecoveryPlan.md` covering suspect-save symptoms, + backup restore steps, failed-backup fallback, and current MVP limitations. - [ ] Document persistence limitations. ## 0.1.N MVP UI And UX diff --git a/Docs/Ops/PersistenceSaveRecoveryPlan.md b/Docs/Ops/PersistenceSaveRecoveryPlan.md new file mode 100644 index 0000000..66c2bbd --- /dev/null +++ b/Docs/Ops/PersistenceSaveRecoveryPlan.md @@ -0,0 +1,45 @@ +# Agrarian MVP Save Recovery Plan + +This runbook covers the 0.1.M file-based `USaveGame` persistence path. + +## Scope + +- Save slot: `AgrarianMVP` +- Expected primary save file: `Saved/SaveGames/AgrarianMVP.sav` +- Automatic backup folder: `Saved/SaveGames/Backups` +- Backup naming: `AgrarianMVP-.sav` + +## Symptoms + +Treat the save as suspect if: + +- server startup logs show load warnings or repeated world-state restore failure; +- players spawn without expected survival, inventory, or placed-world state; +- the save file is zero bytes or much smaller than the latest backup; +- the server crashes immediately after startup load. + +## Immediate Response + +1. Stop the gameplay server or editor session. +2. Copy the current `AgrarianMVP.sav` aside with a `.suspect` suffix. +3. Identify the newest backup in `Saved/SaveGames/Backups`. +4. Copy that backup to `Saved/SaveGames/AgrarianMVP.sav`. +5. Restart the server. +6. Confirm startup logs report an Agrarian startup load attempt and no crash. +7. Run a manual admin save only after confirming the restored world is correct. + +## If All Backups Fail + +1. Move the primary save and backups to an incident folder. +2. Start with no `AgrarianMVP.sav` to generate a clean MVP world state. +3. Preserve the failed files for later format/migration debugging. +4. Document the failed save size, timestamp, commit hash, and server log excerpt + in the handoff. + +## Current Limitations + +- There is no automated save-file validation tool yet. +- There is no multi-slot rollback UI yet. +- Backups are local to the running machine unless external VM/project backups + copy `Saved/SaveGames`. +- The current recovery path is operational/manual by design for the MVP. diff --git a/Docs/PersistenceDesignDocument.md b/Docs/PersistenceDesignDocument.md index 33b371a..1758a24 100644 --- a/Docs/PersistenceDesignDocument.md +++ b/Docs/PersistenceDesignDocument.md @@ -473,6 +473,12 @@ existing save, `WriteSave` copies the current slot file from `Saved/SaveGames` into `Saved/SaveGames/Backups` with a UTC timestamp. Missing save files simply skip backup creation, which keeps the first save path simple. +Corrupted-save recovery for 0.1.M is documented in +`Docs/Ops/PersistenceSaveRecoveryPlan.md`. The MVP recovery path is manual: +stop the server, preserve the suspect save, restore the newest timestamped +backup, restart, validate the world, and only then allow a new manual/admin +save. + ## Testing Gates Minimum persistence smoke test: diff --git a/Scripts/verify_save_recovery_plan.py b/Scripts/verify_save_recovery_plan.py new file mode 100644 index 0000000..badb693 --- /dev/null +++ b/Scripts/verify_save_recovery_plan.py @@ -0,0 +1,42 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +EXPECTED = { + ROOT / "Docs" / "Ops" / "PersistenceSaveRecoveryPlan.md": [ + "Saved/SaveGames/AgrarianMVP.sav", + "Saved/SaveGames/Backups", + "Copy the current `AgrarianMVP.sav` aside with a `.suspect` suffix.", + "Copy that backup to `Saved/SaveGames/AgrarianMVP.sav`.", + "There is no automated save-file validation tool yet.", + ], + ROOT / "Docs" / "PersistenceDesignDocument.md": [ + "`Docs/Ops/PersistenceSaveRecoveryPlan.md`", + "preserve the suspect save", + "restore the newest timestamped", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Add recovery plan for corrupted save.", + "`Docs/Ops/PersistenceSaveRecoveryPlan.md`", + "current MVP limitations", + ], +} + + +def main() -> None: + missing = [] + for path, snippets in EXPECTED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)}: {snippet}") + + if missing: + raise RuntimeError("Save recovery plan verification failed: " + "; ".join(missing)) + + print("PASS: corrupted-save recovery plan is documented.") + + +if __name__ == "__main__": + main()