LifePlay-Revival/Source/LifePlay/LPCityWaterSubsystem.cpp
edujiro5972 56dfcfc3d2 LifePlay UE 5.8 : état de développement au 2026-10-20
Tickets : PUB-20261020-T1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:35:10 +02:00

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);
}