Add weather save load support
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "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<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;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
AActor* TestShelter = TestWorld->SpawnActor<AActor>(
|
||||
@@ -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<AActor> 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");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<UAgrarianPersistentActorComponent*>& OutComponents) const
|
||||
{
|
||||
OutComponents.Reset();
|
||||
|
||||
@@ -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<UAgrarianPersistentActorComponent*>& OutComponents) const;
|
||||
void FindAgrarianPlayers(TArray<AAgrarianGameCharacter*>& OutPlayers) const;
|
||||
|
||||
Reference in New Issue
Block a user