From ff6fc61af379a5d52a45a28e5a334c73b2637125 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 15 May 2026 23:47:35 -0700 Subject: [PATCH] Map provider weather inputs --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- Docs/TechnicalDesignDocument.md | 8 ++ Scripts/verify_weather_input_mapping.py | 73 +++++++++++++++++++ Source/AgrarianGame/AgrarianGameState.cpp | 28 +++++++ Source/AgrarianGame/AgrarianGameState.h | 6 ++ Source/AgrarianGame/AgrarianTypes.h | 48 ++++++++++++ .../AgrarianWeatherProviderSubsystem.cpp | 40 ++++++++-- .../AgrarianWeatherProviderSubsystem.h | 6 ++ 8 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 Scripts/verify_weather_input_mapping.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 1cbadc8..e226d6d 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -429,7 +429,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Use Open-Meteo as the first global MVP weather source. Added `Data/Weather/open_meteo_mvp_source.json` as the provider contract, documented the global tile lookup rule, and added `Scripts/verify_open_meteo_mvp_source.py` to validate the static contract plus live Open-Meteo responses for every source-backed tile in the generated weather manifest. - [x] Add NOAA/NWS fallback or enrichment for US tiles where useful. Added a US/NWS-eligible coordinate check, NOAA/NWS points and forecast-grid request hooks, fallback parsing for gridded temperature, precipitation probability, and wind speed, `Data/Weather/noaa_nws_us_fallback.json`, and live/static verification for source-backed US tiles. - [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. -- [ ] Map real weather inputs into Agrarian weather states: temperature, precipitation, wind, cloud cover, humidity, pressure, visibility, and weather code. +- [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. - [ ] Add deterministic fallback weather simulation when external weather data is unavailable. - [ ] Store weather source, provider timestamp, tile coordinate, and applied in-game weather state for debugging and persistence. - [ ] Add weather save/load support. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index d443a9b..220a696 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -182,6 +182,14 @@ call Open-Meteo or NOAA/NWS. Clients never call Open-Meteo or NOAA/NWS directly. 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, +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. + ## Terrain And Tile Delivery ### MVP Tile diff --git a/Scripts/verify_weather_input_mapping.py b/Scripts/verify_weather_input_mapping.py new file mode 100644 index 0000000..b0e852c --- /dev/null +++ b/Scripts/verify_weather_input_mapping.py @@ -0,0 +1,73 @@ +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" +PROVIDER_H = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.h" +PROVIDER_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.cpp" +TDD = ROOT / "Docs" / "TechnicalDesignDocument.md" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + +EXPECTED = { + TYPES_H: [ + "FAgrarianMappedWeatherInputs", + "TemperatureC", + "PrecipitationMm", + "WindSpeedKmh", + "CloudCoverPercent", + "RelativeHumidityPercent", + "PressureMslHpa", + "VisibilityMeters", + "ProviderWeatherCode", + ], + GAME_STATE_H: [ + "FAgrarianMappedWeatherInputs ActiveWeatherInputs", + "ApplyMappedWeatherInputs", + ], + GAME_STATE_CPP: [ + "DOREPLIFETIME(AAgrarianGameState, ActiveWeatherInputs);", + "void AAgrarianGameState::ApplyMappedWeatherInputs", + "SetRegionalTemperatureProfile", + "SetRegionalObservedTemperature", + "SetWeather(ActiveWeatherInputs.MappedWeather)", + ], + PROVIDER_H: [ + "VisibilityMeters", + "MapSnapshotToAgrarianWeatherInputs", + ], + PROVIDER_CPP: [ + "FAgrarianMappedWeatherInputs UAgrarianWeatherProviderSubsystem::MapSnapshotToAgrarianWeatherInputs", + "GameState->ApplyMappedWeatherInputs", + "OutSnapshot.VisibilityMeters", + "relativeHumidity", + "skyCover", + "pressure", + "visibility", + ], + TDD: [ + "Real-weather provider values are mapped into `FAgrarianMappedWeatherInputs`", + "weather code available alongside the collapsed Agrarian weather state", + ], + ROADMAP: [ + "[x] Map real weather inputs into Agrarian weather states: temperature, precipitation, wind, cloud cover, humidity, pressure, visibility, and weather code.", + ], +} + + +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 input mapping verification failed: " + "; ".join(missing)) + print("Agrarian weather input mapping verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGameState.cpp b/Source/AgrarianGame/AgrarianGameState.cpp index f5c86d9..589bb6f 100644 --- a/Source/AgrarianGame/AgrarianGameState.cpp +++ b/Source/AgrarianGame/AgrarianGameState.cpp @@ -98,6 +98,7 @@ void AAgrarianGameState::GetLifetimeReplicatedProps(TArray& O DOREPLIFETIME(AAgrarianGameState, ObservedTemperatureBlend); DOREPLIFETIME(AAgrarianGameState, bHasRegionalObservedTemperature); DOREPLIFETIME(AAgrarianGameState, RegionalWeatherSource); + DOREPLIFETIME(AAgrarianGameState, ActiveWeatherInputs); DOREPLIFETIME(AAgrarianGameState, DaysPerAgrarianYear); DOREPLIFETIME(AAgrarianGameState, ActiveSolarTileId); DOREPLIFETIME(AAgrarianGameState, ActiveTileLatitude); @@ -160,6 +161,33 @@ void AAgrarianGameState::SetRegionalObservedTemperature(float ObservedTemperatur UpdateAmbientTemperature(); } +void AAgrarianGameState::ApplyMappedWeatherInputs(const FAgrarianMappedWeatherInputs& MappedInputs) +{ + if (!HasAuthority()) + { + return; + } + + ActiveWeatherInputs = MappedInputs; + ActiveWeatherInputs.TemperatureC = FMath::Clamp(ActiveWeatherInputs.TemperatureC, -80.0f, 70.0f); + ActiveWeatherInputs.DailyLowTemperatureC = FMath::Clamp(ActiveWeatherInputs.DailyLowTemperatureC, -80.0f, 70.0f); + ActiveWeatherInputs.DailyHighTemperatureC = FMath::Clamp(ActiveWeatherInputs.DailyHighTemperatureC, -80.0f, 70.0f); + ActiveWeatherInputs.PrecipitationMm = FMath::Max(0.0f, ActiveWeatherInputs.PrecipitationMm); + ActiveWeatherInputs.WindSpeedKmh = FMath::Max(0.0f, ActiveWeatherInputs.WindSpeedKmh); + ActiveWeatherInputs.CloudCoverPercent = FMath::Clamp(ActiveWeatherInputs.CloudCoverPercent, 0.0f, 100.0f); + ActiveWeatherInputs.RelativeHumidityPercent = FMath::Clamp(ActiveWeatherInputs.RelativeHumidityPercent, 0.0f, 100.0f); + ActiveWeatherInputs.PressureMslHpa = FMath::Clamp(ActiveWeatherInputs.PressureMslHpa, 800.0f, 1100.0f); + ActiveWeatherInputs.VisibilityMeters = FMath::Max(0.0f, ActiveWeatherInputs.VisibilityMeters); + ActiveWeatherInputs.bHasProviderData = true; + + SetRegionalTemperatureProfile(ActiveWeatherInputs.DailyLowTemperatureC, ActiveWeatherInputs.DailyHighTemperatureC); + SetRegionalObservedTemperature( + ActiveWeatherInputs.TemperatureC, + 1.0f, + FString::Printf(TEXT("%s:%s"), *ActiveWeatherInputs.Provider, *ActiveWeatherInputs.ProviderTimestamp)); + SetWeather(ActiveWeatherInputs.MappedWeather); +} + float AAgrarianGameState::GetClearSkyTemperatureForHour(float HourOfDay) const { const float LowTemperature = FMath::Min(RegionalDailyLowTemperatureC, RegionalDailyHighTemperatureC); diff --git a/Source/AgrarianGame/AgrarianGameState.h b/Source/AgrarianGame/AgrarianGameState.h index 7b6c1de..28f3fa5 100644 --- a/Source/AgrarianGame/AgrarianGameState.h +++ b/Source/AgrarianGame/AgrarianGameState.h @@ -52,6 +52,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Temperature") FString RegionalWeatherSource = TEXT("deterministic_tile_curve"); + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Weather") + FAgrarianMappedWeatherInputs ActiveWeatherInputs; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") FName ActiveSolarTileId = TEXT("gz_us_ca_pacifica_utm10n_e544_n4160"); @@ -103,6 +106,9 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Temperature") void SetRegionalObservedTemperature(float ObservedTemperatureC, float BlendWeight, const FString& WeatherSource); + UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Weather") + void ApplyMappedWeatherInputs(const FAgrarianMappedWeatherInputs& MappedInputs); + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Temperature") float GetClearSkyTemperatureForHour(float HourOfDay) const; diff --git a/Source/AgrarianGame/AgrarianTypes.h b/Source/AgrarianGame/AgrarianTypes.h index 9ed1b29..95d6ed4 100644 --- a/Source/AgrarianGame/AgrarianTypes.h +++ b/Source/AgrarianGame/AgrarianTypes.h @@ -14,6 +14,54 @@ enum class EAgrarianWeatherType : uint8 Storm UMETA(DisplayName = "Storm") }; +USTRUCT(BlueprintType) +struct FAgrarianMappedWeatherInputs +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") + float TemperatureC = 12.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") + float DailyLowTemperatureC = 9.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") + float DailyHighTemperatureC = 18.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") + int32 ProviderWeatherCode = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") + EAgrarianWeatherType MappedWeather = EAgrarianWeatherType::Clear; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") + FString Provider = TEXT("deterministic"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") + FString ProviderTimestamp; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") + bool bHasProviderData = false; +}; + UENUM(BlueprintType) enum class EAgrarianSeason : uint8 { diff --git a/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp b/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp index ddc7ba4..7c667bb 100644 --- a/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp +++ b/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp @@ -111,15 +111,30 @@ bool UAgrarianWeatherProviderSubsystem::ApplySnapshotToGameState(const FAgrarian return false; } - GameState->SetRegionalTemperatureProfile(Snapshot.DailyLowTemperatureC, Snapshot.DailyHighTemperatureC); - GameState->SetRegionalObservedTemperature( - Snapshot.CurrentTemperatureC, - 1.0f, - FString::Printf(TEXT("%s:%s"), *Snapshot.Provider, *Snapshot.ProviderTimestamp)); - GameState->SetWeather(Snapshot.MappedWeather); + GameState->ApplyMappedWeatherInputs(MapSnapshotToAgrarianWeatherInputs(Snapshot)); return true; } +FAgrarianMappedWeatherInputs UAgrarianWeatherProviderSubsystem::MapSnapshotToAgrarianWeatherInputs(const FAgrarianWeatherProviderSnapshot& Snapshot) const +{ + FAgrarianMappedWeatherInputs MappedInputs; + MappedInputs.TemperatureC = Snapshot.CurrentTemperatureC; + MappedInputs.DailyLowTemperatureC = Snapshot.DailyLowTemperatureC; + MappedInputs.DailyHighTemperatureC = Snapshot.DailyHighTemperatureC; + MappedInputs.PrecipitationMm = Snapshot.PrecipitationMm; + MappedInputs.WindSpeedKmh = Snapshot.WindSpeedKmh; + MappedInputs.CloudCoverPercent = Snapshot.CloudCoverPercent; + MappedInputs.RelativeHumidityPercent = Snapshot.RelativeHumidityPercent; + MappedInputs.PressureMslHpa = Snapshot.PressureMslHpa > 0.0f ? Snapshot.PressureMslHpa : 1013.25f; + MappedInputs.VisibilityMeters = Snapshot.VisibilityMeters; + MappedInputs.ProviderWeatherCode = Snapshot.WeatherCode; + MappedInputs.MappedWeather = Snapshot.MappedWeather; + MappedInputs.Provider = Snapshot.Provider; + MappedInputs.ProviderTimestamp = Snapshot.ProviderTimestamp; + MappedInputs.bHasProviderData = Snapshot.bIsValid; + return MappedInputs; +} + bool UAgrarianWeatherProviderSubsystem::TryApplyCachedSnapshot(FName TileId, const FString& Provider, AAgrarianGameState* GameState) { if (!GameState || !GameState->HasAuthority()) @@ -331,6 +346,7 @@ bool UAgrarianWeatherProviderSubsystem::ParseOpenMeteoForecast(const FString& Re OutSnapshot.CloudCoverPercent = static_cast(CloudCover); OutSnapshot.RelativeHumidityPercent = static_cast(RelativeHumidity); OutSnapshot.PressureMslHpa = static_cast(Pressure); + OutSnapshot.VisibilityMeters = FMath::Clamp(10000.0f - (OutSnapshot.CloudCoverPercent * 35.0f) - (OutSnapshot.PrecipitationMm * 250.0f), 250.0f, 10000.0f); OutSnapshot.WeatherCode = FMath::RoundToInt(WeatherCode); OutSnapshot.MappedWeather = MapOpenMeteoWeatherCode(OutSnapshot.WeatherCode, OutSnapshot.PrecipitationMm, OutSnapshot.WindSpeedKmh); OutSnapshot.bIsValid = true; @@ -378,8 +394,16 @@ bool UAgrarianWeatherProviderSubsystem::ParseNoaaNwsGridData(const FString& Resp double PrecipitationPercent = 0.0; double WindSpeedKmh = 0.0; + double RelativeHumidity = 0.0; + double SkyCover = 0.0; + double Pressure = 0.0; + double Visibility = 0.0; ReadFirstGridValue(*PropertiesObject, TEXT("probabilityOfPrecipitation"), PrecipitationPercent); ReadFirstGridValue(*PropertiesObject, TEXT("windSpeed"), WindSpeedKmh); + ReadFirstGridValue(*PropertiesObject, TEXT("relativeHumidity"), RelativeHumidity); + ReadFirstGridValue(*PropertiesObject, TEXT("skyCover"), SkyCover); + ReadFirstGridValue(*PropertiesObject, TEXT("pressure"), Pressure); + ReadFirstGridValue(*PropertiesObject, TEXT("visibility"), Visibility); OutSnapshot.TileId = TileId; OutSnapshot.Latitude = FMath::Clamp(Latitude, -90.0f, 90.0f); @@ -391,6 +415,10 @@ bool UAgrarianWeatherProviderSubsystem::ParseNoaaNwsGridData(const FString& Resp OutSnapshot.DailyHighTemperatureC = static_cast(TemperatureC + 4.0); OutSnapshot.PrecipitationMm = PrecipitationPercent > 30.0 ? 0.1f : 0.0f; OutSnapshot.WindSpeedKmh = static_cast(WindSpeedKmh); + OutSnapshot.CloudCoverPercent = static_cast(SkyCover); + OutSnapshot.RelativeHumidityPercent = static_cast(RelativeHumidity); + OutSnapshot.PressureMslHpa = Pressure > 0.0 ? static_cast(Pressure / 100.0) : 1013.25f; + OutSnapshot.VisibilityMeters = Visibility > 0.0 ? static_cast(Visibility) : FMath::Clamp(10000.0f - (OutSnapshot.CloudCoverPercent * 35.0f), 250.0f, 10000.0f); OutSnapshot.WeatherCode = PrecipitationPercent > 30.0 ? 61 : 0; OutSnapshot.MappedWeather = MapOpenMeteoWeatherCode(OutSnapshot.WeatherCode, OutSnapshot.PrecipitationMm, OutSnapshot.WindSpeedKmh); OutSnapshot.bIsValid = true; diff --git a/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.h b/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.h index 1dd89e2..fe9b63c 100644 --- a/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.h +++ b/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.h @@ -56,6 +56,9 @@ struct FAgrarianWeatherProviderSnapshot UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") float PressureMslHpa = 0.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") + float VisibilityMeters = 10000.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather") int32 WeatherCode = 0; @@ -142,6 +145,9 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Weather") bool ApplySnapshotToGameState(const FAgrarianWeatherProviderSnapshot& Snapshot, AAgrarianGameState* GameState) const; + UFUNCTION(BlueprintPure, Category = "Agrarian|Weather") + FAgrarianMappedWeatherInputs MapSnapshotToAgrarianWeatherInputs(const FAgrarianWeatherProviderSnapshot& Snapshot) const; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Weather") bool TryApplyCachedSnapshot(FName TileId, const FString& Provider, AAgrarianGameState* GameState);