diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 45e5c20..5f65ab4 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -432,7 +432,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Map real weather inputs into Agrarian weather states: temperature, precipitation, wind, cloud cover, humidity, pressure, visibility, and weather code. Added replicated `FAgrarianMappedWeatherInputs`, provider snapshot mapping, Open-Meteo visibility derivation, NOAA/NWS grid enrichment for humidity/sky cover/pressure/visibility, and game-state application that preserves raw mapped inputs alongside the collapsed Agrarian weather state. - [x] Add deterministic fallback weather simulation when external weather data is unavailable. Added tile/day-seeded fallback snapshots for temperature, daily low/high, cloud cover, humidity, wind, pressure, precipitation, visibility, provider code, and mapped Agrarian state; fallback snapshots use the normal server cache and apply through the same mapped-weather path as live providers. - [x] Store weather source, provider timestamp, tile coordinate, and applied in-game weather state for debugging and persistence. Added tile ID/coordinate fields to mapped weather inputs, a replicated `FAgrarianWeatherDebugSnapshot` on game state, provider snapshot mapping into the debug path, and save fields for mapped inputs plus applied weather debug state. -- [ ] Add weather save/load support. +- [x] Add weather save/load support. Added `LoadCurrentWorld` as the unified persistence load path, restored weather/time before players and world actors, updated the admin load command to use the combined path, and extended the persistence smoke test to prove provider-backed weather metadata survives save/load. - [x] Connect weather to body temperature. - [~] Connect shelter to weather protection. - [ ] Add first-pass sky and lighting. diff --git a/Docs/PersistenceDesignDocument.md b/Docs/PersistenceDesignDocument.md index 95312ff..d5158ba 100644 --- a/Docs/PersistenceDesignDocument.md +++ b/Docs/PersistenceDesignDocument.md @@ -129,6 +129,12 @@ provider timestamp, tile ID, tile center coordinate, provider weather code, and the final applied in-game weather state so weather-related save/load issues can be inspected per tile. +`LoadCurrentWorld` is the unified MVP load entry point. It restores world +weather/time state first, then restores matching player records and persistent +world actors. Admin load commands should call this combined path so weather, +players, and structures are rehydrated from the same save slot instead of +partially restoring only actors or players. + ## Care History Snapshot Reserve a long-term care history snapshot beside the immediate survival diff --git a/Scripts/verify_weather_save_load_support.py b/Scripts/verify_weather_save_load_support.py new file mode 100644 index 0000000..dc8d513 --- /dev/null +++ b/Scripts/verify_weather_save_load_support.py @@ -0,0 +1,62 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PERSISTENCE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.h" +PERSISTENCE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.cpp" +CONTROLLER_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp" +AUTOMATION_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianEditorAutomationLibrary.cpp" +PERSISTENCE_DOC = ROOT / "Docs" / "PersistenceDesignDocument.md" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + +EXPECTED = { + PERSISTENCE_H: [ + "bool LoadCurrentWorld(int32& RestoredPlayerCount, int32& RestoredWorldActorCount, bool bClearExistingActors = true) const;", + "bool RestoreWorldState(const UAgrarianSaveGame* SaveGame) const;", + ], + PERSISTENCE_CPP: [ + "SaveGame->WorldHours = GameState->WorldHours;", + "SaveGame->WeatherInputs = GameState->ActiveWeatherInputs;", + "GameState->WorldHours = SaveGame->WorldHours;", + "GameState->ApplyMappedWeatherInputs(SaveGame->WeatherInputs);", + "bool UAgrarianPersistenceSubsystem::LoadCurrentWorld", + "RestoreWorldState(SaveGame)", + "RestorePlayers(SaveGame)", + "RestoreWorldActors(SaveGame, bClearExistingActors)", + ], + CONTROLLER_CPP: [ + "Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount)", + ], + AUTOMATION_CPP: [ + "SavedWeatherInputs.Provider = TEXT(\"automation-weather\");", + "SavedWeatherInputs.ProviderTimestamp = TEXT(\"2026-05-16T08:00:00Z\");", + "GameState->ApplyMappedWeatherInputs(SavedWeatherInputs);", + "Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount, true)", + "weather state did not survive save/load", + ], + PERSISTENCE_DOC: [ + "LoadCurrentWorld", + "weather/time state", + "players, and structures", + ], + ROADMAP: [ + "[x] Add weather save/load support.", + ], +} + + +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("Weather save/load support verification failed: " + "; ".join(missing)) + print("Agrarian weather save/load support verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianEditorAutomationLibrary.cpp b/Source/AgrarianGame/AgrarianEditorAutomationLibrary.cpp index 6f9469c..4a3cfc8 100644 --- a/Source/AgrarianGame/AgrarianEditorAutomationLibrary.cpp +++ b/Source/AgrarianGame/AgrarianEditorAutomationLibrary.cpp @@ -5,6 +5,7 @@ #include "AgrarianBuildingPlacementComponent.h" #include "AgrarianCraftingComponent.h" #include "AgrarianGameCharacter.h" +#include "AgrarianGameState.h" #include "AgrarianInteractable.h" #include "AgrarianInventoryComponent.h" #include "AgrarianPersistentActorComponent.h" @@ -628,6 +629,36 @@ FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubc Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), ShelterClass); UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0); + AAgrarianGameState* GameState = TestWorld->GetGameState(); + if (!GameState) + { + Persistence->DefaultSlotName = PreviousSlotName; + Persistence->UserIndex = PreviousUserIndex; + Persistence->WorldActorClassRegistry = PreviousRegistry; + return TEXT("FAIL: no Agrarian game state found for persistence test"); + } + + FAgrarianMappedWeatherInputs SavedWeatherInputs; + SavedWeatherInputs.TileId = TEXT("automation_weather_tile"); + SavedWeatherInputs.Latitude = 37.5925f; + SavedWeatherInputs.Longitude = -122.4995f; + SavedWeatherInputs.TemperatureC = 9.5f; + SavedWeatherInputs.DailyLowTemperatureC = 7.0f; + SavedWeatherInputs.DailyHighTemperatureC = 13.0f; + SavedWeatherInputs.PrecipitationMm = 4.0f; + SavedWeatherInputs.WindSpeedKmh = 18.0f; + SavedWeatherInputs.CloudCoverPercent = 85.0f; + SavedWeatherInputs.RelativeHumidityPercent = 92.0f; + SavedWeatherInputs.PressureMslHpa = 1007.0f; + SavedWeatherInputs.VisibilityMeters = 6000.0f; + SavedWeatherInputs.ProviderWeatherCode = 61; + SavedWeatherInputs.MappedWeather = EAgrarianWeatherType::Rain; + SavedWeatherInputs.Provider = TEXT("automation-weather"); + SavedWeatherInputs.ProviderTimestamp = TEXT("2026-05-16T08:00:00Z"); + SavedWeatherInputs.bHasProviderData = true; + GameState->WorldHours = 14.25f; + GameState->ApplyMappedWeatherInputs(SavedWeatherInputs); + FActorSpawnParameters SpawnParams; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; AActor* TestShelter = TestWorld->SpawnActor( @@ -686,7 +717,20 @@ FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubc return TEXT("FAIL: loaded save did not include persistent world actors"); } - const int32 RestoredActorCount = Persistence->RestoreWorldActors(LoadedSave, true); + GameState->WorldHours = 3.5f; + GameState->SetWeather(EAgrarianWeatherType::Storm); + + int32 RestoredPlayerCount = 0; + int32 RestoredActorCount = 0; + if (!Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount, true)) + { + UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0); + Persistence->DefaultSlotName = PreviousSlotName; + Persistence->UserIndex = PreviousUserIndex; + Persistence->WorldActorClassRegistry = PreviousRegistry; + return TEXT("FAIL: LoadCurrentWorld failed to restore world state"); + } + if (RestoredActorCount != SavedActorCount) { UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0); @@ -696,6 +740,23 @@ FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubc return FString::Printf(TEXT("FAIL: restored actor count mismatch, saved=%d restored=%d"), SavedActorCount, RestoredActorCount); } + const FAgrarianWeatherDebugSnapshot RestoredWeather = GameState->GetWeatherDebugSnapshot(); + const bool bWeatherRestored = + FMath::IsNearlyEqual(GameState->WorldHours, 14.25f, 0.01f) && + GameState->Weather == EAgrarianWeatherType::Rain && + RestoredWeather.TileId == SavedWeatherInputs.TileId && + RestoredWeather.Provider == SavedWeatherInputs.Provider && + RestoredWeather.ProviderTimestamp == SavedWeatherInputs.ProviderTimestamp && + RestoredWeather.AppliedWeather == EAgrarianWeatherType::Rain; + if (!bWeatherRestored) + { + UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0); + Persistence->DefaultSlotName = PreviousSlotName; + Persistence->UserIndex = PreviousUserIndex; + Persistence->WorldActorClassRegistry = PreviousRegistry; + return TEXT("FAIL: weather state did not survive save/load"); + } + int32 PersistentActorCountAfterRestore = 0; for (TActorIterator ActorIt(TestWorld); ActorIt; ++ActorIt) { @@ -713,9 +774,12 @@ FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubc Persistence->WorldActorClassRegistry = PreviousRegistry; return FString::Printf( - TEXT("PASS: live persistence subsystem saved %d actor(s), restored %d actor(s), world now has %d persistent actor(s)"), + TEXT("PASS: live persistence subsystem saved %d actor(s), restored %d actor(s), restored %d player(s), restored weather %s from %s, world now has %d persistent actor(s)"), SavedActorCount, RestoredActorCount, + RestoredPlayerCount, + *UEnum::GetValueAsString(GameState->Weather), + *RestoredWeather.Provider, PersistentActorCountAfterRestore); #else return TEXT("FAIL: editor automation is only available in editor builds"); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index cd1513a..14abbf9 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -168,10 +168,14 @@ void AAgrarianGamePlayerController::ServerAgrarianLoadWorld_Implementation() } Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass()); - const UAgrarianSaveGame* SaveGame = Persistence->LoadOrCreateSave(); - const int32 RestoredPlayerCount = Persistence->RestorePlayers(SaveGame); - const int32 RestoredCount = Persistence->RestoreWorldActors(SaveGame); - ClientMessage(FString::Printf(TEXT("Agrarian world loaded. Restored players: %d. Restored actors: %d."), RestoredPlayerCount, RestoredCount)); + int32 RestoredPlayerCount = 0; + int32 RestoredActorCount = 0; + const bool bLoaded = Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount); + ClientMessage(FString::Printf( + TEXT("%s Restored players: %d. Restored actors: %d."), + bLoaded ? TEXT("Agrarian world loaded.") : TEXT("Agrarian world load restored actors/players, but world state restore failed."), + RestoredPlayerCount, + RestoredActorCount)); } void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation() diff --git a/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp b/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp index c75b313..7786fff 100644 --- a/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp +++ b/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp @@ -254,6 +254,23 @@ bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const return WriteSave(SaveGame); } +bool UAgrarianPersistenceSubsystem::LoadCurrentWorld(int32& RestoredPlayerCount, int32& RestoredWorldActorCount, bool bClearExistingActors) const +{ + RestoredPlayerCount = 0; + RestoredWorldActorCount = 0; + + const UAgrarianSaveGame* SaveGame = LoadOrCreateSave(); + if (!SaveGame) + { + return false; + } + + const bool bRestoredWorldState = RestoreWorldState(SaveGame); + RestoredPlayerCount = RestorePlayers(SaveGame); + RestoredWorldActorCount = RestoreWorldActors(SaveGame, bClearExistingActors); + return bRestoredWorldState; +} + void UAgrarianPersistenceSubsystem::FindPersistentComponents(TArray& OutComponents) const { OutComponents.Reset(); diff --git a/Source/AgrarianGame/AgrarianPersistenceSubsystem.h b/Source/AgrarianGame/AgrarianPersistenceSubsystem.h index ee58c4f..0ba9008 100644 --- a/Source/AgrarianGame/AgrarianPersistenceSubsystem.h +++ b/Source/AgrarianGame/AgrarianPersistenceSubsystem.h @@ -61,6 +61,9 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") bool SaveCurrentWorld() const; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + bool LoadCurrentWorld(int32& RestoredPlayerCount, int32& RestoredWorldActorCount, bool bClearExistingActors = true) const; + protected: void FindPersistentComponents(TArray& OutComponents) const; void FindAgrarianPlayers(TArray& OutPlayers) const;