From 9aa2f8bee378178817dfe02cb60311904761444e Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 18 May 2026 19:32:25 -0700 Subject: [PATCH] Load world state on server start --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 4 +- Docs/PersistenceDesignDocument.md | 6 +++ Scripts/verify_load_on_server_start.py | 48 ++++++++++++++++++ Source/AgrarianGame/AgrarianGameGameMode.cpp | 52 ++++++++++++++++++++ Source/AgrarianGame/AgrarianGameGameMode.h | 5 ++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_load_on_server_start.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index e7bb7a3..cac96cf 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -771,7 +771,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe 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. +- [x] Add load-on-server-start. `AAgrarianGameGameMode` now registers MVP + persistent actor classes and, when `bLoadWorldOnServerStart` is enabled, + loads the current world on authoritative `BeginPlay` if a save exists. - [ ] Add initial tile registry persistence for Ground Zero. - [ ] Add backup-before-save option. - [ ] Add recovery plan for corrupted save. diff --git a/Docs/PersistenceDesignDocument.md b/Docs/PersistenceDesignDocument.md index fc12330..f17b2a9 100644 --- a/Docs/PersistenceDesignDocument.md +++ b/Docs/PersistenceDesignDocument.md @@ -453,6 +453,12 @@ The server-side autosave interval lives on `AAgrarianGameGameMode` as `UAgrarianPersistenceSubsystem::SaveCurrentWorld`. Setting the interval to zero disables the MVP autosave timer. +Load-on-server-start also lives on `AAgrarianGameGameMode` through +`bLoadWorldOnServerStart`, enabled by default. On authoritative `BeginPlay`, the +GameMode registers the current MVP persistent actor classes, checks +`DoesSaveExist`, and calls `LoadCurrentWorld` without clearing existing map +actors so map-authored resources and startup content remain owned by the map. + ## Testing Gates Minimum persistence smoke test: diff --git a/Scripts/verify_load_on_server_start.py b/Scripts/verify_load_on_server_start.py new file mode 100644 index 0000000..d621713 --- /dev/null +++ b/Scripts/verify_load_on_server_start.py @@ -0,0 +1,48 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +EXPECTED = { + ROOT / "Source" / "AgrarianGame" / "AgrarianGameGameMode.h": [ + "bool bLoadWorldOnServerStart = true;", + "void RegisterPersistentActorClasses(UAgrarianPersistenceSubsystem* Persistence) const;", + "void LoadWorldOnServerStart();", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianGameGameMode.cpp": [ + "LoadWorldOnServerStart();", + "Persistence->RegisterWorldActorClass(TEXT(\"primitive_shelter\"), AAgrarianShelterActor::StaticClass());", + "Persistence->RegisterWorldActorClass(TEXT(\"campfire\"), AAgrarianCampfire::StaticClass());", + "Persistence->DoesSaveExist()", + "Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount, bClearExistingActors);", + "constexpr bool bClearExistingActors = false;", + ], + ROOT / "Docs" / "PersistenceDesignDocument.md": [ + "`bLoadWorldOnServerStart`", + "`DoesSaveExist`", + "`LoadCurrentWorld` without clearing existing map", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Add load-on-server-start.", + "`bLoadWorldOnServerStart`", + "authoritative `BeginPlay`", + ], +} + + +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("Load-on-server-start verification failed: " + "; ".join(missing)) + + print("PASS: authoritative server startup load is wired to current world persistence.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGameGameMode.cpp b/Source/AgrarianGame/AgrarianGameGameMode.cpp index 6ebf9da..1d03509 100644 --- a/Source/AgrarianGame/AgrarianGameGameMode.cpp +++ b/Source/AgrarianGame/AgrarianGameGameMode.cpp @@ -3,8 +3,10 @@ #include "AgrarianGameGameMode.h" #include "AgrarianGameCharacter.h" #include "AgrarianDebugHUD.h" +#include "AgrarianCampfire.h" #include "AgrarianGameState.h" #include "AgrarianPersistenceSubsystem.h" +#include "AgrarianShelterActor.h" #include "TimerManager.h" AAgrarianGameGameMode::AAgrarianGameGameMode() @@ -17,6 +19,11 @@ void AAgrarianGameGameMode::BeginPlay() { Super::BeginPlay(); + if (HasAuthority()) + { + LoadWorldOnServerStart(); + } + if (HasAuthority() && ServerAutoSaveIntervalSeconds > 0.0f) { GetWorldTimerManager().SetTimer( @@ -53,6 +60,51 @@ void AAgrarianGameGameMode::Logout(AController* Exiting) Super::Logout(Exiting); } +void AAgrarianGameGameMode::RegisterPersistentActorClasses(UAgrarianPersistenceSubsystem* Persistence) const +{ + if (!Persistence) + { + return; + } + + Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass()); + Persistence->RegisterWorldActorClass(TEXT("campfire"), AAgrarianCampfire::StaticClass()); +} + +void AAgrarianGameGameMode::LoadWorldOnServerStart() +{ + if (!HasAuthority() || !bLoadWorldOnServerStart) + { + return; + } + + UAgrarianPersistenceSubsystem* Persistence = GetGameInstance() ? GetGameInstance()->GetSubsystem() : nullptr; + if (!Persistence) + { + UE_LOG(LogTemp, Warning, TEXT("Agrarian startup load skipped: persistence subsystem unavailable.")); + return; + } + + RegisterPersistentActorClasses(Persistence); + if (!Persistence->DoesSaveExist()) + { + UE_LOG(LogTemp, Log, TEXT("Agrarian startup load skipped: no save exists.")); + return; + } + + int32 RestoredPlayerCount = 0; + int32 RestoredActorCount = 0; + constexpr bool bClearExistingActors = false; + const bool bLoaded = Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount, bClearExistingActors); + UE_LOG( + LogTemp, + Log, + TEXT("Agrarian startup load %s. Restored players: %d. Restored actors: %d."), + bLoaded ? TEXT("completed") : TEXT("completed with world-state warning"), + RestoredPlayerCount, + RestoredActorCount); +} + void AAgrarianGameGameMode::RunServerAutoSave() { if (!HasAuthority()) diff --git a/Source/AgrarianGame/AgrarianGameGameMode.h b/Source/AgrarianGame/AgrarianGameGameMode.h index bcc9cd4..315a99b 100644 --- a/Source/AgrarianGame/AgrarianGameGameMode.h +++ b/Source/AgrarianGame/AgrarianGameGameMode.h @@ -26,7 +26,12 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence", meta = (ClampMin = "0")) float ServerAutoSaveIntervalSeconds = 300.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence") + bool bLoadWorldOnServerStart = true; + protected: + void RegisterPersistentActorClasses(UAgrarianPersistenceSubsystem* Persistence) const; + void LoadWorldOnServerStart(); void RunServerAutoSave(); FTimerHandle ServerAutoSaveTimerHandle;