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] 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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user