323 lines
10 KiB
C++
323 lines
10 KiB
C++
// Lot STM1 (STM1-SPEC-03 § 2.6). Voir LPCityWaterSubsystem.h.
|
|
|
|
#include "LPCityWaterSubsystem.h"
|
|
|
|
#include "LifePlay.h"
|
|
#include "LPCityWaterMask.h"
|
|
|
|
#include "Components/InstancedStaticMeshComponent.h"
|
|
#include "Components/StaticMeshComponent.h"
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/StaticMesh.h"
|
|
#include "Engine/World.h"
|
|
#include "EngineUtils.h"
|
|
#include "GameFramework/Actor.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "UObject/UnrealType.h"
|
|
|
|
namespace LPCityWater
|
|
{
|
|
/** Hauteur de l'eau au-dessus du plan de pave (cm). */
|
|
constexpr double WaterAbovePlaneCm = 2.0;
|
|
|
|
/** Repli si le plan est introuvable : RelativeLocation Z du Plane de MapMarkers (-300, lecture LPDumpComponents du
|
|
* 2026-10-20, STM1-B4) + 2 cm, sous l'acteur de la carte. */
|
|
constexpr double FallbackBelowMapCm = 298.0;
|
|
|
|
/** Attente maximale du plan de MapMarkers apres un changement de carte, avant le repli (s). */
|
|
constexpr double MaxWaitPlaneSeconds = 5.0;
|
|
|
|
/** STM1-SPEC-08 : aire (m2) a partir de laquelle un batiment dans l'eau est epargne ; 0 = aucun (option D, choix de
|
|
* l'utilisateur du 2026-10-20). Une valeur positive retablit les options A (1100) ou B (1500) de STM1-O3-LISTE. */
|
|
constexpr float SpareAboveAreaM2 = 0.f;
|
|
|
|
/** STM1-SPEC-08 : ecart maximal en XY entre une instance et un centre du masque (cm). */
|
|
constexpr double MatchToleranceCm = 50.0;
|
|
|
|
/** STM1-SPEC-08 : taille des cases de rangement des centres (cm). */
|
|
constexpr double MaskCellCm = 100.0;
|
|
|
|
FIntPoint MaskCell(double X, double Y)
|
|
{
|
|
return FIntPoint(FMath::FloorToInt32(X / MaskCellCm), FMath::FloorToInt32(Y / MaskCellCm));
|
|
}
|
|
|
|
/** Maisons generiques de MapMarkers (House1-4, Skyscraper1-7), pas les arbres ni les jeeps. */
|
|
bool IsHouseComponent(const UInstancedStaticMeshComponent* Component)
|
|
{
|
|
const UStaticMesh* Mesh = Component ? Component->GetStaticMesh() : nullptr;
|
|
const FString Name = Mesh ? Mesh->GetName() : FString();
|
|
return Name.StartsWith(TEXT("House")) || Name.StartsWith(TEXT("Skyscraper"));
|
|
}
|
|
|
|
AActor* FindMapMarkers(const UWorld* World)
|
|
{
|
|
for (TActorIterator<AActor> It(World); It; ++It)
|
|
{
|
|
if (It->GetClass()->GetName().Contains(TEXT("MapMarkers")))
|
|
{
|
|
return *It;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
UObject* ObjectProperty(const UObject* Owner, const TCHAR* Name)
|
|
{
|
|
const FObjectPropertyBase* Prop = Owner ? CastField<FObjectPropertyBase>(Owner->GetClass()->FindPropertyByName(Name)) : nullptr;
|
|
return Prop ? Prop->GetObjectPropertyValue_InContainer(Owner) : nullptr;
|
|
}
|
|
}
|
|
|
|
void ULPCityWaterSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPCityWaterSubsystem::Tick), 0.25f);
|
|
}
|
|
|
|
void ULPCityWaterSubsystem::Deinitialize()
|
|
{
|
|
if (TickHandle.IsValid())
|
|
{
|
|
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
|
|
TickHandle.Reset();
|
|
}
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
bool ULPCityWaterSubsystem::FindPlaneZ(const UWorld* World, double& OutZ) const
|
|
{
|
|
if (!World)
|
|
{
|
|
return false;
|
|
}
|
|
for (TActorIterator<AActor> It(World); It; ++It)
|
|
{
|
|
if (!It->GetClass()->GetName().Contains(TEXT("MapMarkers")))
|
|
{
|
|
continue;
|
|
}
|
|
TInlineComponentArray<UStaticMeshComponent*> Components(*It);
|
|
for (const UStaticMeshComponent* Component : Components)
|
|
{
|
|
if (Component && Component->GetFName() == TEXT("Plane"))
|
|
{
|
|
OutZ = Component->GetComponentLocation().Z;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ULPCityWaterSubsystem::Tick(float DeltaTime)
|
|
{
|
|
using namespace LPCityWater;
|
|
|
|
const UGameInstance* GI = GetGameInstance();
|
|
AActor* Street = GI ? Cast<AActor>(ObjectProperty(GI, TEXT("StreetMapStatic"))) : nullptr;
|
|
UStaticMeshComponent* MapComp = Street ? Cast<UStaticMeshComponent>(ObjectProperty(Street, TEXT("StaticMeshComponent"))) : nullptr;
|
|
if (!MapComp)
|
|
{
|
|
return true;
|
|
}
|
|
const UStaticMesh* MapMesh = MapComp->GetStaticMesh();
|
|
const FString MeshPath = MapMesh ? MapMesh->GetPathName() : FString();
|
|
|
|
if (MeshPath != LastMapMesh)
|
|
{
|
|
// Nouvelle carte : attendre (au plus MaxWaitPlaneSeconds) que MapMarkers et son plan existent.
|
|
double PlaneZ = 0.0;
|
|
const bool bPlane = FindPlaneZ(Street->GetWorld(), PlaneZ);
|
|
WaitPlaneSeconds += 0.25;
|
|
if (!bPlane && MapMesh && WaitPlaneSeconds < MaxWaitPlaneSeconds)
|
|
{
|
|
return true;
|
|
}
|
|
WaitPlaneSeconds = 0.0;
|
|
LastMapMesh = MeshPath;
|
|
UpdateWater(Street, MapComp);
|
|
}
|
|
else if (bUsedFallbackZ && IsValid(WaterComponent) && WaterComponent->IsVisible())
|
|
{
|
|
// Pose faite avec le repli : corriger des que le plan apparait.
|
|
double PlaneZ = 0.0;
|
|
if (FindPlaneZ(Street->GetWorld(), PlaneZ))
|
|
{
|
|
const FVector Loc = WaterComponent->GetComponentLocation();
|
|
WaterComponent->SetWorldLocation(FVector(Loc.X, Loc.Y, PlaneZ + WaterAbovePlaneCm));
|
|
bUsedFallbackZ = false;
|
|
UE_LOG(LogLPCity, Log, TEXT("[LPCity] EAU Z corrige : plan MapMarkers Z=%.1f -> eau Z=%.1f"), PlaneZ, PlaneZ + WaterAbovePlaneCm);
|
|
}
|
|
}
|
|
if (Mask && MeshPath == LastMapMesh)
|
|
{
|
|
HideHousesInWater(MapComp, false);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ULPCityWaterSubsystem::LoadMask(const FString& City)
|
|
{
|
|
Mask = nullptr;
|
|
MaskCells.Reset();
|
|
LastHouseInstances = -1;
|
|
LastMaskReport.Reset();
|
|
const FString MaskPackage = FString::Printf(TEXT("/Game/Heavy/StreetMap/Water/%s_Mask"), *City);
|
|
if (City.IsEmpty() || !FPackageName::DoesPackageExist(MaskPackage))
|
|
{
|
|
return;
|
|
}
|
|
Mask = LoadObject<ULPCityWaterMask>(nullptr, *FString::Printf(TEXT("%s.%s_Mask"), *MaskPackage, *City));
|
|
if (!Mask)
|
|
{
|
|
UE_LOG(LogLPCity, Warning, TEXT("[LPCity] EAU %s : masque %s illisible"), *City, *MaskPackage);
|
|
return;
|
|
}
|
|
for (int32 Index = 0; Index < Mask->Centers.Num(); ++Index)
|
|
{
|
|
MaskCells.Add(LPCityWater::MaskCell(Mask->Centers[Index].X, Mask->Centers[Index].Y), Index);
|
|
}
|
|
}
|
|
|
|
void ULPCityWaterSubsystem::HideHousesInWater(UStaticMeshComponent* MapComp, bool bForce)
|
|
{
|
|
using namespace LPCityWater;
|
|
|
|
AActor* Markers = Mask && MapComp ? FindMapMarkers(MapComp->GetWorld()) : nullptr;
|
|
if (!Markers)
|
|
{
|
|
return;
|
|
}
|
|
TInlineComponentArray<UInstancedStaticMeshComponent*> AllComponents(Markers);
|
|
TArray<UInstancedStaticMeshComponent*, TInlineAllocator<16>> Houses;
|
|
int32 Total = 0;
|
|
for (UInstancedStaticMeshComponent* Component : AllComponents)
|
|
{
|
|
if (IsHouseComponent(Component))
|
|
{
|
|
Houses.Add(Component);
|
|
Total += Component->GetInstanceCount();
|
|
}
|
|
}
|
|
if (!bForce && Total == LastHouseInstances)
|
|
{
|
|
return;
|
|
}
|
|
LastHouseInstances = Total;
|
|
|
|
// Position monde de l'instance ramenee dans le repere de la carte (celui de l'UStreetMap et du masque).
|
|
const FTransform MapTransform = MapComp->GetComponentTransform();
|
|
TBitArray<> CenterUsed(false, Mask->Centers.Num());
|
|
int32 Masked = 0, Spared = 0;
|
|
for (UInstancedStaticMeshComponent* Component : Houses)
|
|
{
|
|
bool bDirty = false;
|
|
for (int32 Instance = 0; Instance < Component->GetInstanceCount(); ++Instance)
|
|
{
|
|
FTransform WorldTransform;
|
|
Component->GetInstanceTransform(Instance, WorldTransform, true);
|
|
const FVector Local = MapTransform.InverseTransformPosition(WorldTransform.GetLocation());
|
|
const FIntPoint Cell = MaskCell(Local.X, Local.Y);
|
|
int32 Hit = INDEX_NONE;
|
|
for (int32 DX = -1; DX <= 1 && Hit == INDEX_NONE; ++DX)
|
|
{
|
|
for (int32 DY = -1; DY <= 1 && Hit == INDEX_NONE; ++DY)
|
|
{
|
|
for (auto It = MaskCells.CreateConstKeyIterator(Cell + FIntPoint(DX, DY)); It; ++It)
|
|
{
|
|
if (FVector2D::Distance(Mask->Centers[It.Value()], FVector2D(Local.X, Local.Y)) <= MatchToleranceCm)
|
|
{
|
|
Hit = It.Value();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (Hit == INDEX_NONE)
|
|
{
|
|
continue;
|
|
}
|
|
CenterUsed[Hit] = true;
|
|
if (SpareAboveAreaM2 > 0.f && Mask->AreasM2.IsValidIndex(Hit) && Mask->AreasM2[Hit] >= SpareAboveAreaM2)
|
|
{
|
|
++Spared;
|
|
continue;
|
|
}
|
|
++Masked;
|
|
FTransform Relative;
|
|
Component->GetInstanceTransform(Instance, Relative, false);
|
|
if (Relative.GetScale3D().IsNearlyZero())
|
|
{
|
|
continue;
|
|
}
|
|
Relative.SetScale3D(FVector::ZeroVector);
|
|
Component->UpdateInstanceTransform(Instance, Relative, false, false, true);
|
|
bDirty = true;
|
|
}
|
|
if (bDirty)
|
|
{
|
|
Component->MarkRenderStateDirty();
|
|
}
|
|
}
|
|
|
|
const int32 Unmatched = Mask->Centers.Num() - CenterUsed.CountSetBits();
|
|
const FString Report = FString::Printf(TEXT("[LPCity] EAU %s : masque %d centres ; instances maisons %d ; masquees %d ; epargnees %d (seuil %.0f m2) ; centres sans instance %d"),
|
|
*Mask->City, Mask->Centers.Num(), Total, Masked, Spared, SpareAboveAreaM2, Unmatched);
|
|
if (Report != LastMaskReport)
|
|
{
|
|
LastMaskReport = Report;
|
|
UE_LOG(LogLPCity, Log, TEXT("%s"), *Report);
|
|
}
|
|
}
|
|
|
|
void ULPCityWaterSubsystem::UpdateWater(AActor* Street, UStaticMeshComponent* MapComp)
|
|
{
|
|
using namespace LPCityWater;
|
|
|
|
const UStaticMesh* MapMesh = MapComp->GetStaticMesh();
|
|
const FString City = MapMesh ? MapMesh->GetName() : FString();
|
|
const FString WaterPackage = FString::Printf(TEXT("/Game/Heavy/StreetMap/Water/%s"), *City);
|
|
LoadMask(City);
|
|
|
|
UStaticMesh* WaterMesh = nullptr;
|
|
if (!City.IsEmpty() && FPackageName::DoesPackageExist(WaterPackage))
|
|
{
|
|
WaterMesh = LoadObject<UStaticMesh>(nullptr, *FString::Printf(TEXT("%s.%s"), *WaterPackage, *City));
|
|
}
|
|
if (!WaterMesh)
|
|
{
|
|
if (IsValid(WaterComponent))
|
|
{
|
|
WaterComponent->SetVisibility(false);
|
|
}
|
|
bUsedFallbackZ = false;
|
|
UE_LOG(LogLPCity, Log, TEXT("[LPCity] EAU %s : aucun asset d'eau, rien de pose"), City.IsEmpty() ? TEXT("(aucune carte)") : *City);
|
|
return;
|
|
}
|
|
|
|
if (!IsValid(WaterComponent) || WaterComponent->GetOwner() != Street)
|
|
{
|
|
WaterComponent = NewObject<UStaticMeshComponent>(Street, TEXT("LPCityWater"));
|
|
WaterComponent->SetupAttachment(MapComp);
|
|
WaterComponent->SetMobility(EComponentMobility::Movable);
|
|
WaterComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
|
WaterComponent->SetCastShadow(false);
|
|
WaterComponent->bReceivesDecals = false;
|
|
WaterComponent->RegisterComponent();
|
|
}
|
|
WaterComponent->SetStaticMesh(WaterMesh);
|
|
WaterComponent->SetVisibility(true);
|
|
|
|
// Meme repere que la carte (rattache a son composant), hauteur = plan de pave + 2 cm.
|
|
double PlaneZ = 0.0;
|
|
const bool bPlane = FindPlaneZ(Street->GetWorld(), PlaneZ);
|
|
const FVector MapLoc = MapComp->GetComponentLocation();
|
|
const double WaterZ = bPlane ? PlaneZ + WaterAbovePlaneCm : MapLoc.Z - FallbackBelowMapCm;
|
|
WaterComponent->SetWorldLocation(FVector(MapLoc.X, MapLoc.Y, WaterZ));
|
|
bUsedFallbackZ = !bPlane;
|
|
|
|
const FString ZSource = bPlane ? FString::Printf(TEXT("plan MapMarkers Z=%.1f + 2 cm"), PlaneZ) : FString(TEXT("repli : Z carte - 298 cm"));
|
|
UE_LOG(LogLPCity, Log, TEXT("[LPCity] EAU %s : pose %s ; Z eau=%.1f (%s) ; Z carte=%.1f"),
|
|
*City, *WaterMesh->GetPathName(), WaterZ, *ZSource, MapLoc.Z);
|
|
}
|