Persist applied weather debug state

This commit is contained in:
2026-05-16 00:16:32 -07:00
parent 3740eb32bf
commit 26ddf8ea8e
11 changed files with 256 additions and 2 deletions
+1 -1
View File
@@ -431,7 +431,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [x] Cache real-weather snapshots server-side so clients never call public weather APIs directly. Added provider/tile-keyed server cache entries with TTL, cache reuse before Open-Meteo or NOAA/NWS requests, cache clearing/debug helpers, and documentation that clients consume only replicated game-state weather and temperature.
- [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.
- [ ] Store weather source, provider timestamp, tile coordinate, and applied in-game weather state for debugging and persistence.
- [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] Connect weather to body temperature.
- [~] Connect shelter to weather protection.
+7
View File
@@ -122,6 +122,13 @@ transform, survival snapshot, care history snapshot, and inventory stacks.
`RestorePlayers` reapplies those records to matching live characters before or
alongside world actor restore.
The same save path captures authoritative world weather state from
`AAgrarianGameState`: world hour, collapsed weather enum, mapped provider inputs,
and `FAgrarianWeatherDebugSnapshot`. The debug snapshot includes weather source,
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.
## Care History Snapshot
Reserve a long-term care history snapshot beside the immediate survival
+10 -1
View File
@@ -183,13 +183,22 @@ They receive weather, temperature, source, and state through replicated game
state fields.
Real-weather provider values are mapped into `FAgrarianMappedWeatherInputs`
before they affect gameplay. The mapped snapshot keeps temperature,
before they affect gameplay. The mapped snapshot keeps tile ID, tile center
coordinate, temperature,
precipitation, wind, cloud cover, humidity, pressure, visibility, and provider
weather code available alongside the collapsed Agrarian weather state. Open-Meteo
fills those fields directly where available; NOAA/NWS fills them from grid data
where available and derives provisional visibility/weather-state values until a
deeper provider-specific mapping pass is added.
The applied weather state also has a replicated debug snapshot:
`FAgrarianWeatherDebugSnapshot`. It records the weather source, provider
timestamp, tile ID, tile center coordinate, provider weather code, input values,
and final in-game `EAgrarianWeatherType` after mapping. Save files persist both
the mapped inputs and the applied debug snapshot so weather issues can be traced
back to a specific provider response and tile without inferring those values from
separate systems.
Deterministic fallback weather keeps the game playable when external providers
are disabled, unreachable, or return unusable data. The fallback snapshot is
derived from tile ID and Agrarian day, then mapped through the same
@@ -0,0 +1,87 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TYPES_H = ROOT / "Source" / "AgrarianGame" / "AgrarianTypes.h"
GAME_STATE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianGameState.h"
GAME_STATE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianGameState.cpp"
SAVE_GAME_H = ROOT / "Source" / "AgrarianGame" / "AgrarianSaveGame.h"
PERSISTENCE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.h"
PERSISTENCE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.cpp"
PROVIDER_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.cpp"
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
PERSISTENCE_DOC = ROOT / "Docs" / "PersistenceDesignDocument.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
EXPECTED = {
TYPES_H: [
"FName TileId = NAME_None;",
"float Latitude = 0.0f;",
"float Longitude = 0.0f;",
"struct FAgrarianWeatherDebugSnapshot",
"FString ProviderTimestamp;",
"EAgrarianWeatherType AppliedWeather",
],
PROVIDER_CPP: [
"MappedInputs.TileId = Snapshot.TileId;",
"MappedInputs.Latitude = Snapshot.Latitude;",
"MappedInputs.Longitude = Snapshot.Longitude;",
],
GAME_STATE_H: [
"FAgrarianWeatherDebugSnapshot ActiveWeatherDebug",
"FAgrarianWeatherDebugSnapshot GetWeatherDebugSnapshot() const;",
],
GAME_STATE_CPP: [
"DOREPLIFETIME(AAgrarianGameState, ActiveWeatherDebug);",
"ActiveWeatherDebug.Provider = ActiveWeatherInputs.Provider;",
"ActiveWeatherDebug.ProviderTimestamp = ActiveWeatherInputs.ProviderTimestamp;",
"ActiveWeatherDebug.AppliedWeather = ActiveWeatherInputs.MappedWeather;",
"FAgrarianWeatherDebugSnapshot AAgrarianGameState::GetWeatherDebugSnapshot() const",
],
SAVE_GAME_H: [
"FAgrarianMappedWeatherInputs WeatherInputs;",
"FAgrarianWeatherDebugSnapshot WeatherDebug;",
],
PERSISTENCE_H: [
"bool CaptureWorldState(UAgrarianSaveGame* SaveGame) const;",
"bool RestoreWorldState(const UAgrarianSaveGame* SaveGame) const;",
],
PERSISTENCE_CPP: [
"SaveGame->WeatherInputs = GameState->ActiveWeatherInputs;",
"SaveGame->WeatherDebug = GameState->GetWeatherDebugSnapshot();",
"GameState->ApplyMappedWeatherInputs(SaveGame->WeatherInputs);",
"CaptureWorldState(SaveGame);",
],
TDD: [
"`FAgrarianWeatherDebugSnapshot`",
"weather source",
"provider timestamp",
"tile center coordinate",
],
PERSISTENCE_DOC: [
"`FAgrarianWeatherDebugSnapshot`",
"weather source",
"provider timestamp",
"tile center coordinate",
],
ROADMAP: [
"[x] Store weather source, provider timestamp, tile coordinate, and applied in-game weather state for debugging and persistence.",
],
}
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 debug persistence verification failed: " + "; ".join(missing))
print("Agrarian weather debug persistence verification complete.")
if __name__ == "__main__":
main()
+30
View File
@@ -49,6 +49,13 @@ AAgrarianGameState::AAgrarianGameState()
ActiveGrowingSeason.MinAverageGrowingTempC = 7.0f;
ActiveGrowingSeason.CropSafetyBufferDays = 14;
ActiveGrowingSeason.ClimateProfile = TEXT("coastal_mediterranean_mild");
ActiveWeatherInputs.TileId = ActiveSolarTileId;
ActiveWeatherInputs.Latitude = ActiveTileLatitude;
ActiveWeatherInputs.Longitude = ActiveTileLongitude;
ActiveWeatherDebug.TileId = ActiveSolarTileId;
ActiveWeatherDebug.Latitude = ActiveTileLatitude;
ActiveWeatherDebug.Longitude = ActiveTileLongitude;
ActiveWeatherDebug.AppliedWeather = Weather;
}
void AAgrarianGameState::BeginPlay()
@@ -99,6 +106,7 @@ void AAgrarianGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& O
DOREPLIFETIME(AAgrarianGameState, bHasRegionalObservedTemperature);
DOREPLIFETIME(AAgrarianGameState, RegionalWeatherSource);
DOREPLIFETIME(AAgrarianGameState, ActiveWeatherInputs);
DOREPLIFETIME(AAgrarianGameState, ActiveWeatherDebug);
DOREPLIFETIME(AAgrarianGameState, DaysPerAgrarianYear);
DOREPLIFETIME(AAgrarianGameState, ActiveSolarTileId);
DOREPLIFETIME(AAgrarianGameState, ActiveTileLatitude);
@@ -130,6 +138,7 @@ void AAgrarianGameState::SetWeather(EAgrarianWeatherType NewWeather)
if (HasAuthority())
{
Weather = NewWeather;
ActiveWeatherDebug.AppliedWeather = NewWeather;
UpdateAmbientTemperature();
OnRep_Weather();
}
@@ -180,6 +189,22 @@ void AAgrarianGameState::ApplyMappedWeatherInputs(const FAgrarianMappedWeatherIn
ActiveWeatherInputs.VisibilityMeters = FMath::Max(0.0f, ActiveWeatherInputs.VisibilityMeters);
ActiveWeatherInputs.bHasProviderData = true;
ActiveWeatherDebug.TileId = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.TileId : ActiveSolarTileId;
ActiveWeatherDebug.Latitude = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.Latitude : ActiveTileLatitude;
ActiveWeatherDebug.Longitude = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.Longitude : ActiveTileLongitude;
ActiveWeatherDebug.Provider = ActiveWeatherInputs.Provider;
ActiveWeatherDebug.ProviderTimestamp = ActiveWeatherInputs.ProviderTimestamp;
ActiveWeatherDebug.AppliedWeather = ActiveWeatherInputs.MappedWeather;
ActiveWeatherDebug.ProviderWeatherCode = ActiveWeatherInputs.ProviderWeatherCode;
ActiveWeatherDebug.TemperatureC = ActiveWeatherInputs.TemperatureC;
ActiveWeatherDebug.PrecipitationMm = ActiveWeatherInputs.PrecipitationMm;
ActiveWeatherDebug.WindSpeedKmh = ActiveWeatherInputs.WindSpeedKmh;
ActiveWeatherDebug.CloudCoverPercent = ActiveWeatherInputs.CloudCoverPercent;
ActiveWeatherDebug.RelativeHumidityPercent = ActiveWeatherInputs.RelativeHumidityPercent;
ActiveWeatherDebug.PressureMslHpa = ActiveWeatherInputs.PressureMslHpa;
ActiveWeatherDebug.VisibilityMeters = ActiveWeatherInputs.VisibilityMeters;
ActiveWeatherDebug.bHasProviderData = ActiveWeatherInputs.bHasProviderData;
SetRegionalTemperatureProfile(ActiveWeatherInputs.DailyLowTemperatureC, ActiveWeatherInputs.DailyHighTemperatureC);
SetRegionalObservedTemperature(
ActiveWeatherInputs.TemperatureC,
@@ -188,6 +213,11 @@ void AAgrarianGameState::ApplyMappedWeatherInputs(const FAgrarianMappedWeatherIn
SetWeather(ActiveWeatherInputs.MappedWeather);
}
FAgrarianWeatherDebugSnapshot AAgrarianGameState::GetWeatherDebugSnapshot() const
{
return ActiveWeatherDebug;
}
float AAgrarianGameState::GetClearSkyTemperatureForHour(float HourOfDay) const
{
const float LowTemperature = FMath::Min(RegionalDailyLowTemperatureC, RegionalDailyHighTemperatureC);
+6
View File
@@ -55,6 +55,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Weather")
FAgrarianMappedWeatherInputs ActiveWeatherInputs;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Weather")
FAgrarianWeatherDebugSnapshot ActiveWeatherDebug;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
FName ActiveSolarTileId = TEXT("gz_us_ca_pacifica_utm10n_e544_n4160");
@@ -109,6 +112,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Weather")
void ApplyMappedWeatherInputs(const FAgrarianMappedWeatherInputs& MappedInputs);
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Weather")
FAgrarianWeatherDebugSnapshot GetWeatherDebugSnapshot() const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Temperature")
float GetClearSkyTemperatureForHour(float HourOfDay) const;
@@ -2,6 +2,7 @@
#include "AgrarianPersistenceSubsystem.h"
#include "AgrarianGameCharacter.h"
#include "AgrarianGameState.h"
#include "AgrarianInventoryComponent.h"
#include "AgrarianPersistentActorComponent.h"
#include "AgrarianSaveGame.h"
@@ -120,6 +121,44 @@ int32 UAgrarianPersistenceSubsystem::RestoreWorldActors(const UAgrarianSaveGame*
return RestoredCount;
}
bool UAgrarianPersistenceSubsystem::CaptureWorldState(UAgrarianSaveGame* SaveGame) const
{
UWorld* World = GetWorld();
AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
if (!SaveGame || !GameState)
{
return false;
}
SaveGame->WorldHours = GameState->WorldHours;
SaveGame->Weather = GameState->Weather;
SaveGame->WeatherInputs = GameState->ActiveWeatherInputs;
SaveGame->WeatherDebug = GameState->GetWeatherDebugSnapshot();
return true;
}
bool UAgrarianPersistenceSubsystem::RestoreWorldState(const UAgrarianSaveGame* SaveGame) const
{
UWorld* World = GetWorld();
AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
if (!SaveGame || !GameState || !GameState->HasAuthority())
{
return false;
}
GameState->WorldHours = SaveGame->WorldHours;
if (SaveGame->WeatherInputs.bHasProviderData)
{
GameState->ApplyMappedWeatherInputs(SaveGame->WeatherInputs);
}
else
{
GameState->SetWeather(SaveGame->Weather);
}
return true;
}
int32 UAgrarianPersistenceSubsystem::CapturePlayers(UAgrarianSaveGame* SaveGame) const
{
if (!SaveGame)
@@ -209,6 +248,7 @@ bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const
return false;
}
CaptureWorldState(SaveGame);
CapturePlayers(SaveGame);
CaptureWorldActors(SaveGame);
return WriteSave(SaveGame);
@@ -46,6 +46,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
int32 RestoreWorldActors(const UAgrarianSaveGame* SaveGame, bool bClearExistingActors = true) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
bool CaptureWorldState(UAgrarianSaveGame* SaveGame) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
bool RestoreWorldState(const UAgrarianSaveGame* SaveGame) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
int32 CapturePlayers(UAgrarianSaveGame* SaveGame) const;
+6
View File
@@ -61,6 +61,12 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save")
EAgrarianWeatherType Weather = EAgrarianWeatherType::Clear;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save")
FAgrarianMappedWeatherInputs WeatherInputs;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save")
FAgrarianWeatherDebugSnapshot WeatherDebug;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save")
TArray<FAgrarianSavedPlayer> Players;
+60
View File
@@ -19,6 +19,15 @@ struct FAgrarianMappedWeatherInputs
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
FName TileId = NAME_None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float Latitude = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float Longitude = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float TemperatureC = 12.0f;
@@ -62,6 +71,57 @@ struct FAgrarianMappedWeatherInputs
bool bHasProviderData = false;
};
USTRUCT(BlueprintType)
struct FAgrarianWeatherDebugSnapshot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
FName TileId = NAME_None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float Latitude = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float Longitude = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
FString Provider = TEXT("deterministic");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
FString ProviderTimestamp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
EAgrarianWeatherType AppliedWeather = EAgrarianWeatherType::Clear;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
int32 ProviderWeatherCode = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float TemperatureC = 12.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float PrecipitationMm = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float WindSpeedKmh = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float CloudCoverPercent = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float RelativeHumidityPercent = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float PressureMslHpa = 1013.25f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
float VisibilityMeters = 10000.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
bool bHasProviderData = false;
};
UENUM(BlueprintType)
enum class EAgrarianSeason : uint8
{
@@ -122,6 +122,9 @@ bool UAgrarianWeatherProviderSubsystem::ApplySnapshotToGameState(const FAgrarian
FAgrarianMappedWeatherInputs UAgrarianWeatherProviderSubsystem::MapSnapshotToAgrarianWeatherInputs(const FAgrarianWeatherProviderSnapshot& Snapshot) const
{
FAgrarianMappedWeatherInputs MappedInputs;
MappedInputs.TileId = Snapshot.TileId;
MappedInputs.Latitude = Snapshot.Latitude;
MappedInputs.Longitude = Snapshot.Longitude;
MappedInputs.TemperatureC = Snapshot.CurrentTemperatureC;
MappedInputs.DailyLowTemperatureC = Snapshot.DailyLowTemperatureC;
MappedInputs.DailyHighTemperatureC = Snapshot.DailyHighTemperatureC;