Add tile driven weather provider adapter
This commit is contained in:
@@ -425,7 +425,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
||||
- [x] Add storm placeholder.
|
||||
- [x] Add weather transition rules.
|
||||
- [x] Add weather replication.
|
||||
- [ ] Add real-world weather provider adapter for Ground Zero by latitude/longitude.
|
||||
- [x] Add real-world weather provider adapter for Ground Zero by latitude/longitude. Added a tile-driven Open-Meteo adapter subsystem that can request weather for any active tile by center latitude/longitude, map provider weather codes into Agrarian weather states, apply current temperature and daily low/high to `AAgrarianGameState`, and generate a source-backed tile weather manifest so future real tiles become weather-eligible automatically.
|
||||
- [ ] Use Open-Meteo as the first global MVP weather source.
|
||||
- [ ] Add NOAA/NWS fallback or enrichment for US tiles where useful.
|
||||
- [ ] Cache real-weather snapshots server-side so clients never call public weather APIs directly.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"generated_at_utc": "2026-05-16T05:58:06Z",
|
||||
"source_registry": "Data/Tiles/ground_zero_tiles.json",
|
||||
"generation_rule": "Every source-backed/generated tile with center coordinates is emitted; unknown placeholders are skipped.",
|
||||
"tiles": [
|
||||
{
|
||||
"tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160",
|
||||
"center_latitude": 37.5925,
|
||||
"center_longitude": -122.4995,
|
||||
"provider": "open-meteo",
|
||||
"lookup_rule": "Use tile center latitude/longitude for live MVP weather and temperature snapshots."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -147,6 +147,16 @@ clients to call public weather APIs directly. This keeps real-world temperature
|
||||
and weather tied to the represented map tile while preserving a deterministic
|
||||
fallback if an external provider is unavailable.
|
||||
|
||||
The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses
|
||||
Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the
|
||||
current temperature, daily low/high, precipitation, wind, humidity, cloud cover,
|
||||
pressure, provider timestamp, and weather code, then applies the mapped state to
|
||||
`AAgrarianGameState` on the server. It is tile-driven rather than Ground-Zero
|
||||
hard-coded: `Scripts/generate_tile_weather_manifest.py` emits every
|
||||
source-backed, generated, validated, packaged, or published tile with center
|
||||
coordinates, while placeholder/unknown tiles are skipped. Future source-backed
|
||||
tiles therefore become weather-eligible when their registry entries are added.
|
||||
|
||||
## Terrain And Tile Delivery
|
||||
|
||||
### MVP Tile
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate weather lookup coordinates for source-backed Agrarian tiles.
|
||||
|
||||
The weather provider can request live conditions for any active tile with
|
||||
latitude/longitude. This manifest is intentionally generated from the registry
|
||||
instead of per-tile hard-coded overrides, so future source-backed tiles are
|
||||
eligible automatically when they are added to the registry.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = ROOT / "Data" / "Tiles" / "ground_zero_tiles.json"
|
||||
OUTPUT_PATH = ROOT / "Data" / "Tiles" / "tile_weather_manifest.json"
|
||||
WEATHER_READY_STATUSES = {"source_data_found", "generated", "validated", "packaged", "published"}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||
records = []
|
||||
|
||||
for tile in registry.get("tiles", []):
|
||||
status = tile.get("status")
|
||||
tile_id = tile.get("tile_id")
|
||||
grid = tile.get("grid", {})
|
||||
latitude = grid.get("center_latitude")
|
||||
longitude = grid.get("center_longitude")
|
||||
if status not in WEATHER_READY_STATUSES or not tile_id or latitude is None or longitude is None:
|
||||
continue
|
||||
|
||||
records.append(
|
||||
{
|
||||
"tile_id": tile_id,
|
||||
"center_latitude": latitude,
|
||||
"center_longitude": longitude,
|
||||
"provider": "open-meteo",
|
||||
"lookup_rule": "Use tile center latitude/longitude for live MVP weather and temperature snapshots.",
|
||||
}
|
||||
)
|
||||
|
||||
OUTPUT_PATH.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"generated_at_utc": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
||||
"source_registry": str(REGISTRY_PATH.relative_to(ROOT)),
|
||||
"generation_rule": "Every source-backed/generated tile with center coordinates is emitted; unknown placeholders are skipped.",
|
||||
"tiles": records,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print(f"Wrote {len(records)} tile weather manifest record(s) to {OUTPUT_PATH.relative_to(ROOT)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,81 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILD_CS = ROOT / "Source" / "AgrarianGame" / "AgrarianGame.Build.cs"
|
||||
HEADER = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.h"
|
||||
CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.cpp"
|
||||
GENERATOR = ROOT / "Scripts" / "generate_tile_weather_manifest.py"
|
||||
MANIFEST = ROOT / "Data" / "Tiles" / "tile_weather_manifest.json"
|
||||
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
|
||||
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||
|
||||
|
||||
EXPECTED = {
|
||||
BUILD_CS: ['"HTTP"', '"Json"'],
|
||||
HEADER: [
|
||||
"UAgrarianWeatherProviderSubsystem",
|
||||
"FAgrarianWeatherProviderSnapshot",
|
||||
"RequestWeatherForActiveGameState",
|
||||
"RequestWeatherForTile",
|
||||
"BuildOpenMeteoForecastUrl",
|
||||
"ApplySnapshotToGameState",
|
||||
"MapOpenMeteoWeatherCode",
|
||||
],
|
||||
CPP: [
|
||||
"current=temperature_2m,relative_humidity_2m,precipitation",
|
||||
"daily=temperature_2m_max,temperature_2m_min",
|
||||
"GameState->SetRegionalTemperatureProfile",
|
||||
"GameState->SetRegionalObservedTemperature",
|
||||
"GameState->SetWeather",
|
||||
"MapOpenMeteoWeatherCode",
|
||||
],
|
||||
GENERATOR: [
|
||||
"WEATHER_READY_STATUSES",
|
||||
"Every source-backed/generated tile",
|
||||
"tile_weather_manifest.json",
|
||||
],
|
||||
TDD: [
|
||||
"Open-Meteo",
|
||||
"tile center latitude/longitude",
|
||||
"Future source-backed",
|
||||
],
|
||||
ROADMAP: [
|
||||
"[x] Add real-world weather provider adapter for Ground Zero by latitude/longitude.",
|
||||
"tile-driven Open-Meteo adapter",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def assert_contains() -> 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 provider adapter verification failed: " + "; ".join(missing))
|
||||
|
||||
|
||||
def assert_manifest() -> None:
|
||||
data = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
tiles = data.get("tiles", [])
|
||||
if len(tiles) != 1:
|
||||
raise RuntimeError(f"Expected one source-backed weather tile, got {len(tiles)}")
|
||||
tile = tiles[0]
|
||||
if tile.get("tile_id") != "gz_us_ca_pacifica_utm10n_e544_n4160":
|
||||
raise RuntimeError(f"Unexpected weather tile id: {tile.get('tile_id')}")
|
||||
if tile.get("center_latitude") is None or tile.get("center_longitude") is None:
|
||||
raise RuntimeError("Weather manifest tile is missing coordinates")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
assert_contains()
|
||||
assert_manifest()
|
||||
print("Agrarian weather provider adapter verification complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -17,6 +17,8 @@ public class AgrarianGame : ModuleRules
|
||||
"AIModule",
|
||||
"UMG",
|
||||
"Landscape",
|
||||
"HTTP",
|
||||
"Json",
|
||||
"Slate",
|
||||
"SlateCore"
|
||||
});
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright Pacificao. All Rights Reserved.
|
||||
|
||||
#include "AgrarianWeatherProviderSubsystem.h"
|
||||
#include "AgrarianGameState.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Engine/World.h"
|
||||
#include "HttpModule.h"
|
||||
#include "Interfaces/IHttpResponse.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
|
||||
bool UAgrarianWeatherProviderSubsystem::RequestWeatherForActiveGameState()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
|
||||
if (!GameState || !GameState->HasAuthority())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return RequestWeatherForTile(GameState->ActiveSolarTileId, GameState->ActiveTileLatitude, GameState->ActiveTileLongitude);
|
||||
}
|
||||
|
||||
bool UAgrarianWeatherProviderSubsystem::RequestWeatherForTile(FName TileId, float Latitude, float Longitude)
|
||||
{
|
||||
if (!bEnableLiveWeatherRequests || TileId == NAME_None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
if (!World || !World->GetAuthGameMode())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString Url = BuildOpenMeteoForecastUrl(TileId, Latitude, Longitude);
|
||||
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
|
||||
Request->SetURL(Url);
|
||||
Request->SetVerb(TEXT("GET"));
|
||||
Request->SetHeader(TEXT("Accept"), TEXT("application/json"));
|
||||
Request->OnProcessRequestComplete().BindUObject(this, &UAgrarianWeatherProviderSubsystem::OnOpenMeteoResponse, TileId, Latitude, Longitude);
|
||||
return Request->ProcessRequest();
|
||||
}
|
||||
|
||||
FString UAgrarianWeatherProviderSubsystem::BuildOpenMeteoForecastUrl(FName TileId, float Latitude, float Longitude) const
|
||||
{
|
||||
const float ClampedLatitude = FMath::Clamp(Latitude, -90.0f, 90.0f);
|
||||
const float ClampedLongitude = FMath::Clamp(Longitude, -180.0f, 180.0f);
|
||||
return FString::Printf(
|
||||
TEXT("%s?latitude=%.6f&longitude=%.6f¤t=temperature_2m,relative_humidity_2m,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,wind_speed_10m&daily=temperature_2m_max,temperature_2m_min,weather_code,precipitation_sum,wind_speed_10m_max&forecast_days=1&timezone=auto"),
|
||||
*OpenMeteoForecastEndpoint,
|
||||
ClampedLatitude,
|
||||
ClampedLongitude);
|
||||
}
|
||||
|
||||
bool UAgrarianWeatherProviderSubsystem::ApplySnapshotToGameState(const FAgrarianWeatherProviderSnapshot& Snapshot, AAgrarianGameState* GameState) const
|
||||
{
|
||||
if (!Snapshot.bIsValid || !GameState || !GameState->HasAuthority())
|
||||
{
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
EAgrarianWeatherType UAgrarianWeatherProviderSubsystem::MapOpenMeteoWeatherCode(int32 WeatherCode, float PrecipitationMm, float WindSpeedKmh)
|
||||
{
|
||||
if (WeatherCode >= 95 || WindSpeedKmh >= 55.0f)
|
||||
{
|
||||
return EAgrarianWeatherType::Storm;
|
||||
}
|
||||
if (WeatherCode == 71 || WeatherCode == 73 || WeatherCode == 75 || WeatherCode == 77 || WeatherCode == 85 || WeatherCode == 86)
|
||||
{
|
||||
return EAgrarianWeatherType::ColdWind;
|
||||
}
|
||||
if ((WeatherCode >= 51 && WeatherCode <= 67) || (WeatherCode >= 80 && WeatherCode <= 82) || PrecipitationMm > 0.05f)
|
||||
{
|
||||
return EAgrarianWeatherType::Rain;
|
||||
}
|
||||
if (WindSpeedKmh >= 35.0f)
|
||||
{
|
||||
return EAgrarianWeatherType::ColdWind;
|
||||
}
|
||||
return EAgrarianWeatherType::Clear;
|
||||
}
|
||||
|
||||
void UAgrarianWeatherProviderSubsystem::OnOpenMeteoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FName TileId, float Latitude, float Longitude)
|
||||
{
|
||||
if (!bWasSuccessful || !Response.IsValid() || Response->GetResponseCode() < 200 || Response->GetResponseCode() >= 300)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FAgrarianWeatherProviderSnapshot Snapshot;
|
||||
if (!ParseOpenMeteoForecast(Response->GetContentAsString(), TileId, Latitude, Longitude, Snapshot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LastSnapshot = Snapshot;
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
|
||||
ApplySnapshotToGameState(Snapshot, GameState);
|
||||
}
|
||||
|
||||
bool UAgrarianWeatherProviderSubsystem::ParseOpenMeteoForecast(const FString& ResponseContent, FName TileId, float Latitude, float Longitude, FAgrarianWeatherProviderSnapshot& OutSnapshot) const
|
||||
{
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseContent);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* CurrentObject = nullptr;
|
||||
if (!RootObject->TryGetObjectField(TEXT("current"), CurrentObject) || !CurrentObject || !CurrentObject->IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* DailyObject = nullptr;
|
||||
if (!RootObject->TryGetObjectField(TEXT("daily"), DailyObject) || !DailyObject || !DailyObject->IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* DailyLowValues = nullptr;
|
||||
const TArray<TSharedPtr<FJsonValue>>* DailyHighValues = nullptr;
|
||||
if (!(*DailyObject)->TryGetArrayField(TEXT("temperature_2m_min"), DailyLowValues)
|
||||
|| !(*DailyObject)->TryGetArrayField(TEXT("temperature_2m_max"), DailyHighValues)
|
||||
|| !DailyLowValues || !DailyHighValues || DailyLowValues->Num() == 0 || DailyHighValues->Num() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double CurrentTemperature = 0.0;
|
||||
double RelativeHumidity = 0.0;
|
||||
double Precipitation = 0.0;
|
||||
double Rain = 0.0;
|
||||
double Showers = 0.0;
|
||||
double Snowfall = 0.0;
|
||||
double WeatherCode = 0.0;
|
||||
double CloudCover = 0.0;
|
||||
double Pressure = 0.0;
|
||||
double WindSpeed = 0.0;
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("temperature_2m"), CurrentTemperature);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("relative_humidity_2m"), RelativeHumidity);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("precipitation"), Precipitation);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("rain"), Rain);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("showers"), Showers);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("snowfall"), Snowfall);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("weather_code"), WeatherCode);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("cloud_cover"), CloudCover);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("pressure_msl"), Pressure);
|
||||
(*CurrentObject)->TryGetNumberField(TEXT("wind_speed_10m"), WindSpeed);
|
||||
|
||||
OutSnapshot.TileId = TileId;
|
||||
OutSnapshot.Latitude = FMath::Clamp(Latitude, -90.0f, 90.0f);
|
||||
OutSnapshot.Longitude = FMath::Clamp(Longitude, -180.0f, 180.0f);
|
||||
OutSnapshot.Provider = TEXT("open-meteo");
|
||||
(*CurrentObject)->TryGetStringField(TEXT("time"), OutSnapshot.ProviderTimestamp);
|
||||
OutSnapshot.CurrentTemperatureC = static_cast<float>(CurrentTemperature);
|
||||
OutSnapshot.DailyLowTemperatureC = static_cast<float>((*DailyLowValues)[0]->AsNumber());
|
||||
OutSnapshot.DailyHighTemperatureC = static_cast<float>((*DailyHighValues)[0]->AsNumber());
|
||||
OutSnapshot.PrecipitationMm = static_cast<float>(FMath::Max(Precipitation, FMath::Max(Rain + Showers, Snowfall)));
|
||||
OutSnapshot.WindSpeedKmh = static_cast<float>(WindSpeed);
|
||||
OutSnapshot.CloudCoverPercent = static_cast<float>(CloudCover);
|
||||
OutSnapshot.RelativeHumidityPercent = static_cast<float>(RelativeHumidity);
|
||||
OutSnapshot.PressureMslHpa = static_cast<float>(Pressure);
|
||||
OutSnapshot.WeatherCode = FMath::RoundToInt(WeatherCode);
|
||||
OutSnapshot.MappedWeather = MapOpenMeteoWeatherCode(OutSnapshot.WeatherCode, OutSnapshot.PrecipitationMm, OutSnapshot.WindSpeedKmh);
|
||||
OutSnapshot.bIsValid = true;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright Pacificao. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "HttpFwd.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "AgrarianTypes.h"
|
||||
#include "AgrarianWeatherProviderSubsystem.generated.h"
|
||||
|
||||
class AAgrarianGameState;
|
||||
class IHttpRequest;
|
||||
class IHttpResponse;
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FAgrarianWeatherProviderSnapshot
|
||||
{
|
||||
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("open-meteo");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
FString ProviderTimestamp;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
float CurrentTemperatureC = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
float DailyLowTemperatureC = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
float DailyHighTemperatureC = 0.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 = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
int32 WeatherCode = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
EAgrarianWeatherType MappedWeather = EAgrarianWeatherType::Clear;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
bool bIsValid = false;
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class UAgrarianWeatherProviderSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
bool bEnableLiveWeatherRequests = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather")
|
||||
FString OpenMeteoForecastEndpoint = TEXT("https://api.open-meteo.com/v1/forecast");
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather")
|
||||
FAgrarianWeatherProviderSnapshot LastSnapshot;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|Weather")
|
||||
bool RequestWeatherForActiveGameState();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|Weather")
|
||||
bool RequestWeatherForTile(FName TileId, float Latitude, float Longitude);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Agrarian|Weather")
|
||||
FString BuildOpenMeteoForecastUrl(FName TileId, float Latitude, float Longitude) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|Weather")
|
||||
bool ApplySnapshotToGameState(const FAgrarianWeatherProviderSnapshot& Snapshot, AAgrarianGameState* GameState) const;
|
||||
|
||||
static EAgrarianWeatherType MapOpenMeteoWeatherCode(int32 WeatherCode, float PrecipitationMm, float WindSpeedKmh);
|
||||
|
||||
private:
|
||||
void OnOpenMeteoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FName TileId, float Latitude, float Longitude);
|
||||
bool ParseOpenMeteoForecast(const FString& ResponseContent, FName TileId, float Latitude, float Longitude, FAgrarianWeatherProviderSnapshot& OutSnapshot) const;
|
||||
};
|
||||
Reference in New Issue
Block a user