// 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 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(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 It(World); It; ++It) { if (!It->GetClass()->GetName().Contains(TEXT("MapMarkers"))) { continue; } TInlineComponentArray 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(ObjectProperty(GI, TEXT("StreetMapStatic"))) : nullptr; UStaticMeshComponent* MapComp = Street ? Cast(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(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 AllComponents(Markers); TArray> 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(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(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); }