Add tile driven weather provider adapter

This commit is contained in:
2026-05-15 23:08:30 -07:00
parent ca2c3ee3db
commit a4aa2095be
8 changed files with 458 additions and 1 deletions
@@ -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&current=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;
};