Add weather save load support

This commit is contained in:
2026-05-16 00:56:11 -07:00
parent 26ddf8ea8e
commit 8625583faa
7 changed files with 163 additions and 7 deletions
+1 -1
View File
@@ -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] 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] 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. - [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. - [x] Connect weather to body temperature.
- [~] Connect shelter to weather protection. - [~] Connect shelter to weather protection.
- [ ] Add first-pass sky and lighting. - [ ] Add first-pass sky and lighting.
+6
View File
@@ -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 the final applied in-game weather state so weather-related save/load issues can
be inspected per tile. 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 ## Care History Snapshot
Reserve a long-term care history snapshot beside the immediate survival Reserve a long-term care history snapshot beside the immediate survival
@@ -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()
@@ -5,6 +5,7 @@
#include "AgrarianBuildingPlacementComponent.h" #include "AgrarianBuildingPlacementComponent.h"
#include "AgrarianCraftingComponent.h" #include "AgrarianCraftingComponent.h"
#include "AgrarianGameCharacter.h" #include "AgrarianGameCharacter.h"
#include "AgrarianGameState.h"
#include "AgrarianInteractable.h" #include "AgrarianInteractable.h"
#include "AgrarianInventoryComponent.h" #include "AgrarianInventoryComponent.h"
#include "AgrarianPersistentActorComponent.h" #include "AgrarianPersistentActorComponent.h"
@@ -628,6 +629,36 @@ FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubc
Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), ShelterClass); Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), ShelterClass);
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0); UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
AAgrarianGameState* GameState = TestWorld->GetGameState<AAgrarianGameState>();
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; FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AActor* TestShelter = TestWorld->SpawnActor<AActor>( AActor* TestShelter = TestWorld->SpawnActor<AActor>(
@@ -686,7 +717,20 @@ FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubc
return TEXT("FAIL: loaded save did not include persistent world actors"); 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) if (RestoredActorCount != SavedActorCount)
{ {
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0); 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); 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; int32 PersistentActorCountAfterRestore = 0;
for (TActorIterator<AActor> ActorIt(TestWorld); ActorIt; ++ActorIt) for (TActorIterator<AActor> ActorIt(TestWorld); ActorIt; ++ActorIt)
{ {
@@ -713,9 +774,12 @@ FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubc
Persistence->WorldActorClassRegistry = PreviousRegistry; Persistence->WorldActorClassRegistry = PreviousRegistry;
return FString::Printf( 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, SavedActorCount,
RestoredActorCount, RestoredActorCount,
RestoredPlayerCount,
*UEnum::GetValueAsString(GameState->Weather),
*RestoredWeather.Provider,
PersistentActorCountAfterRestore); PersistentActorCountAfterRestore);
#else #else
return TEXT("FAIL: editor automation is only available in editor builds"); return TEXT("FAIL: editor automation is only available in editor builds");
@@ -168,10 +168,14 @@ void AAgrarianGamePlayerController::ServerAgrarianLoadWorld_Implementation()
} }
Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass()); Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass());
const UAgrarianSaveGame* SaveGame = Persistence->LoadOrCreateSave(); int32 RestoredPlayerCount = 0;
const int32 RestoredPlayerCount = Persistence->RestorePlayers(SaveGame); int32 RestoredActorCount = 0;
const int32 RestoredCount = Persistence->RestoreWorldActors(SaveGame); const bool bLoaded = Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount);
ClientMessage(FString::Printf(TEXT("Agrarian world loaded. Restored players: %d. Restored actors: %d."), RestoredPlayerCount, RestoredCount)); 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() void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation()
@@ -254,6 +254,23 @@ bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const
return WriteSave(SaveGame); 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<UAgrarianPersistentActorComponent*>& OutComponents) const void UAgrarianPersistenceSubsystem::FindPersistentComponents(TArray<UAgrarianPersistentActorComponent*>& OutComponents) const
{ {
OutComponents.Reset(); OutComponents.Reset();
@@ -61,6 +61,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
bool SaveCurrentWorld() const; bool SaveCurrentWorld() const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
bool LoadCurrentWorld(int32& RestoredPlayerCount, int32& RestoredWorldActorCount, bool bClearExistingActors = true) const;
protected: protected:
void FindPersistentComponents(TArray<UAgrarianPersistentActorComponent*>& OutComponents) const; void FindPersistentComponents(TArray<UAgrarianPersistentActorComponent*>& OutComponents) const;
void FindAgrarianPlayers(TArray<AAgrarianGameCharacter*>& OutPlayers) const; void FindAgrarianPlayers(TArray<AAgrarianGameCharacter*>& OutPlayers) const;