From a7ca8d10f829ae696fdde78f9c7251f657af3366 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 18 May 2026 19:29:35 -0700 Subject: [PATCH] Add server autosave interval --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 4 +- Docs/PersistenceDesignDocument.md | 6 +++ Scripts/verify_server_save_interval.py | 47 ++++++++++++++++++++ Source/AgrarianGame/AgrarianGameGameMode.cpp | 35 +++++++++++++++ Source/AgrarianGame/AgrarianGameGameMode.h | 9 ++++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_server_save_interval.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 8dd3b1d..e7bb7a3 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -767,7 +767,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe item stacks, and owner player ID; 0.1.M has no placed container actor yet, so the current craftable `simple_container` remains covered by player inventory persistence until placed containers arrive. -- [ ] Add server-side save interval. +- [x] Add server-side save interval. `AAgrarianGameGameMode` now starts an + authoritative repeating autosave timer from `ServerAutoSaveIntervalSeconds` + and calls `SaveCurrentWorld`, with `0` disabling the MVP timer. - [x] Add manual admin save command. - [ ] Add load-on-server-start. - [ ] Add initial tile registry persistence for Ground Zero. diff --git a/Docs/PersistenceDesignDocument.md b/Docs/PersistenceDesignDocument.md index 0128c7c..fc12330 100644 --- a/Docs/PersistenceDesignDocument.md +++ b/Docs/PersistenceDesignDocument.md @@ -447,6 +447,12 @@ reapplies the mapped weather inputs; otherwise it restores the saved enum weather state. This gives the MVP a deterministic fallback while preserving the latest real/provider-mapped tile weather payload when one was available. +The server-side autosave interval lives on `AAgrarianGameGameMode` as +`ServerAutoSaveIntervalSeconds`, defaulting to five minutes. On authority, +`BeginPlay` starts a repeating timer that calls `RunServerAutoSave`, which uses +`UAgrarianPersistenceSubsystem::SaveCurrentWorld`. Setting the interval to zero +disables the MVP autosave timer. + ## Testing Gates Minimum persistence smoke test: diff --git a/Scripts/verify_server_save_interval.py b/Scripts/verify_server_save_interval.py new file mode 100644 index 0000000..d5b1f0d --- /dev/null +++ b/Scripts/verify_server_save_interval.py @@ -0,0 +1,47 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +EXPECTED = { + ROOT / "Source" / "AgrarianGame" / "AgrarianGameGameMode.h": [ + "virtual void BeginPlay() override;", + "float ServerAutoSaveIntervalSeconds = 300.0f;", + "void RunServerAutoSave();", + "FTimerHandle ServerAutoSaveTimerHandle;", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianGameGameMode.cpp": [ + "GetWorldTimerManager().SetTimer", + "&AAgrarianGameGameMode::RunServerAutoSave", + "ServerAutoSaveIntervalSeconds > 0.0f", + "Persistence->SaveCurrentWorld()", + ], + ROOT / "Docs" / "PersistenceDesignDocument.md": [ + "`ServerAutoSaveIntervalSeconds`", + "`UAgrarianPersistenceSubsystem::SaveCurrentWorld`", + "disables the MVP autosave timer", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Add server-side save interval.", + "`AAgrarianGameGameMode`", + "`SaveCurrentWorld`", + ], +} + + +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("Server save interval verification failed: " + "; ".join(missing)) + + print("PASS: authoritative server autosave interval is wired to world persistence.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGameGameMode.cpp b/Source/AgrarianGame/AgrarianGameGameMode.cpp index 718ebb1..6ebf9da 100644 --- a/Source/AgrarianGame/AgrarianGameGameMode.cpp +++ b/Source/AgrarianGame/AgrarianGameGameMode.cpp @@ -5,6 +5,7 @@ #include "AgrarianDebugHUD.h" #include "AgrarianGameState.h" #include "AgrarianPersistenceSubsystem.h" +#include "TimerManager.h" AAgrarianGameGameMode::AAgrarianGameGameMode() { @@ -12,6 +13,22 @@ AAgrarianGameGameMode::AAgrarianGameGameMode() HUDClass = AAgrarianDebugHUD::StaticClass(); } +void AAgrarianGameGameMode::BeginPlay() +{ + Super::BeginPlay(); + + if (HasAuthority() && ServerAutoSaveIntervalSeconds > 0.0f) + { + GetWorldTimerManager().SetTimer( + ServerAutoSaveTimerHandle, + this, + &AAgrarianGameGameMode::RunServerAutoSave, + ServerAutoSaveIntervalSeconds, + true, + ServerAutoSaveIntervalSeconds); + } +} + void AAgrarianGameGameMode::RestartPlayer(AController* NewPlayer) { Super::RestartPlayer(NewPlayer); @@ -35,3 +52,21 @@ void AAgrarianGameGameMode::Logout(AController* Exiting) Super::Logout(Exiting); } + +void AAgrarianGameGameMode::RunServerAutoSave() +{ + if (!HasAuthority()) + { + return; + } + + UAgrarianPersistenceSubsystem* Persistence = GetGameInstance() ? GetGameInstance()->GetSubsystem() : nullptr; + if (!Persistence) + { + UE_LOG(LogTemp, Warning, TEXT("Agrarian server autosave skipped: persistence subsystem unavailable.")); + return; + } + + const bool bSaved = Persistence->SaveCurrentWorld(); + UE_LOG(LogTemp, Log, TEXT("Agrarian server autosave %s."), bSaved ? TEXT("completed") : TEXT("failed")); +} diff --git a/Source/AgrarianGame/AgrarianGameGameMode.h b/Source/AgrarianGame/AgrarianGameGameMode.h index 83754c6..bcc9cd4 100644 --- a/Source/AgrarianGame/AgrarianGameGameMode.h +++ b/Source/AgrarianGame/AgrarianGameGameMode.h @@ -19,8 +19,17 @@ public: /** Constructor */ AAgrarianGameGameMode(); + virtual void BeginPlay() override; virtual void RestartPlayer(AController* NewPlayer) override; virtual void Logout(AController* Exiting) override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence", meta = (ClampMin = "0")) + float ServerAutoSaveIntervalSeconds = 300.0f; + +protected: + void RunServerAutoSave(); + + FTimerHandle ServerAutoSaveTimerHandle; };