// Lot STM1 (STM1-SPEC-03 § 2.6). Voir LPCityWaterSubsystem.h. #include "LPCityWaterSubsystem.h" #include "LifePlay.h" #include "LPAssetRegistryHelpers.h" #include "LPCityHouses.h" #include "LPCityLandmarks.h" #include "Components/HierarchicalInstancedStaticMeshComponent.h" #include "LPCityWaterMask.h" #include "Components/InstancedStaticMeshComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/GameInstance.h" #include "Engine/StaticMesh.h" #include "Materials/MaterialInterface.h" #include "Engine/World.h" #include "Camera/PlayerCameraManager.h" #include "EngineUtils.h" #include "GameFramework/Actor.h" #include "GameFramework/PlayerController.h" #include "HAL/IConsoleManager.h" #include "Misc/PackageName.h" #include "UObject/UnrealType.h" namespace LPCityWater { // COMMUN-28 (STM1-SPEC-15, option b, choix de l'utilisateur : 150 m, 120 m, 60 m) : filtre des icones des villes de pack. static TAutoConsoleVariable CVarIconFilter(TEXT("lp.City.IconFilter"), 1, TEXT("Villes de pack (lpm_city_*) : 1 = icones de la carte filtrees selon la camera, 0 = toutes visibles.")); static TAutoConsoleVariable CVarIconLowAltitudeM(TEXT("lp.City.IconLowAltitudeM"), 150.f, TEXT("Altitude de camera (m, au-dessus du plan de la carte) sous laquelle seules les icones proches restent visibles.")); static TAutoConsoleVariable CVarIconNearM(TEXT("lp.City.IconNearM"), 120.f, TEXT("Sous l'altitude basse : distance horizontale (m) des icones gardees autour de la camera.")); static TAutoConsoleVariable CVarIconCellM(TEXT("lp.City.IconCellM"), 60.f, TEXT("Au-dessus de l'altitude basse : une icone au plus par case de ce cote (m).")); // COMMUN-29 (S3) : en COMMUN-28, la distance etait prise a la camera, loin des icones d'une vue inclinee : tout etait cache. static TAutoConsoleVariable CVarIconNearFromView(TEXT("lp.City.IconNearFromView"), 1, TEXT("Sous l'altitude basse : 1 = distance mesuree au point regarde (axe de la camera sur le plan de la carte), 0 = a la camera.")); // 1.4.0 (STM1-SPEC-17) : retour arriere en jeu sans rebuild. static TAutoConsoleVariable CVarCityGround(TEXT("lp.City.Ground"), 1, TEXT("Villes de pack : 1 = chaussee, paves (materiau Ground/) et trottoirs en mode normal ; 0 = materiau du jeu et trottoirs caches.")); // 1.7.7 (STM1-SPEC-MOB1 § 4) : mobilier (feux, abribus), variantes de la table Houses dont le maillage commence par // "Mobilier_" : efface au-dela de cette distance (mats fins, moins d'un pixel plus loin) et sans ombre. Lu a la pose. static TAutoConsoleVariable CVarFurnitureCullM(TEXT("lp.City.FurnitureCullM"), 300.f, TEXT("Villes de pack : distance (m) au-dela de laquelle le mobilier urbain (feux, abribus) n'est plus dessine ; 0 = jamais efface.")); /** Deplacement de camera (cm) au-dela duquel le filtre est recalcule. */ constexpr double IconRecomputeMoveCm = 500.0; /** 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)); } /** STM1-SPEC-10 (G3) : taille des cases de rangement des anneaux d'eau (cm). */ constexpr double WaterCellCm = 10000.0; FIntPoint WaterCell(double X, double Y) { return FIntPoint(FMath::FloorToInt32(X / WaterCellCm), FMath::FloorToInt32(Y / WaterCellCm)); } /** Point dans un anneau (regle des croisements). */ bool InRing(const TArray& Points, int32 Begin, int32 End, const FVector2D& P) { bool bInside = false; for (int32 I = Begin, J = End - 1; I < End; J = I++) { const FVector2D& A = Points[I]; const FVector2D& B = Points[J]; if ((A.Y > P.Y) != (B.Y > P.Y) && P.X < (B.X - A.X) * (P.Y - A.Y) / (B.Y - A.Y) + A.X) { bInside = !bInside; } } return bInside; } /** STM1-SPEC-10 (G3) : arbres de MapMarkers (maillage LP_tree), pas les jeeps. */ bool IsTreeComponent(const UInstancedStaticMeshComponent* Component) { const UStaticMesh* Mesh = Component ? Component->GetStaticMesh() : nullptr; return Mesh && Mesh->GetName().Contains(TEXT("tree"), ESearchCase::IgnoreCase); } /** 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")); } 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; } /** Booleen du GI lu par reflexion : 1, 0, ou -1 si la propriete est absente. */ int32 BoolProperty(const UObject* Owner, const TCHAR* Name) { const FBoolProperty* Prop = Owner ? CastField(Owner->GetClass()->FindPropertyByName(Name)) : nullptr; return Prop ? (Prop->GetPropertyValue_InContainer(Owner) ? 1 : 0) : -1; } /** STM1-SPEC-10 (C1) : l'acteur MapMarkers est celui du GI (GI.MapMarkers, ecrit par GI.SpawnMapMarkers) ; repli sur le * premier acteur MapMarkers du monde. OutCount : nombre d'acteurs MapMarkers presents (temoin). */ AActor* FindMapMarkers(const UGameInstance* GI, const UWorld* World, int32* OutCount = nullptr) { int32 Count = 0; AActor* First = nullptr; if (World) { for (TActorIterator It(World); It; ++It) { if (It->GetClass()->GetName().Contains(TEXT("MapMarkers"))) { ++Count; First = First ? First : *It; } } } if (OutCount) { *OutCount = Count; } AActor* FromGI = Cast(ObjectProperty(GI, TEXT("MapMarkers"))); return IsValid(FromGI) && FromGI->GetWorld() == World ? FromGI : First; } } void ULPCityWaterSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPCityWaterSubsystem::Tick), 0.25f); PostActorTickHandle = FWorldDelegates::OnWorldPostActorTick.AddUObject(this, &ULPCityWaterSubsystem::OnPostActorTick); } void ULPCityWaterSubsystem::Deinitialize() { if (TickHandle.IsValid()) { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); TickHandle.Reset(); } if (PostActorTickHandle.IsValid()) { FWorldDelegates::OnWorldPostActorTick.Remove(PostActorTickHandle); PostActorTickHandle.Reset(); } Super::Deinitialize(); } bool ULPCityWaterSubsystem::FindPlaneZ(const UWorld* World, double& OutZ) const { AActor* Markers = World ? LPCityWater::FindMapMarkers(GetGameInstance(), World) : nullptr; if (!Markers) { return false; } TInlineComponentArray Components(Markers); 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 || Landmarks || HouseTable) && MeshPath == LastMapMesh) { HideHousesInWater(MapComp, false); } SyncHousesVisibility(); // Le filtre des icones passe apres le tick des acteurs (OnPostActorTick), a chaque image. IconMapComp = MapComp; bIconPackCity = MeshPath.StartsWith(TEXT("/lpm_city_")); return true; } void ULPCityWaterSubsystem::LoadMask(const FString& City) { Mask = nullptr; MaskCells.Reset(); LastHouseInstances = -1; LastTreeInstances = -1; WaterRingBoxes.Reset(); WaterOuterCells.Reset(); LastMarkers.Reset(); LastMaskReport.Reset(); // STM1-SPEC-09 (lot A) : le masque d'un pack lpm_city_* remplace celui du jeu de base. const FString MaskObject = ULPAssetRegistryHelpers::ResolveCityPath(FString::Printf(TEXT("/Game/Heavy/StreetMap/Water/%s_Mask.%s_Mask"), *City, *City)); const FString MaskPackage = FPackageName::ObjectPathToPackageName(MaskObject); if (City.IsEmpty() || !FPackageName::DoesPackageExist(MaskPackage)) { return; } Mask = LoadObject(nullptr, *MaskObject); 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); } // STM1-SPEC-10 (G3) : boites des anneaux d'eau, exterieurs ranges par case (masque anterieur a G3 : aucun anneau). const int32 Rings = Mask->WaterRingStarts.Num(); for (int32 Ring = 0; Ring < Rings; ++Ring) { const int32 Begin = Mask->WaterRingStarts[Ring]; const int32 End = Ring + 1 < Rings ? Mask->WaterRingStarts[Ring + 1] : Mask->WaterPoints.Num(); FBox2D Box(ForceInit); for (int32 P = Begin; P < End; ++P) { Box += Mask->WaterPoints[P]; } WaterRingBoxes.Add(Box); if (Mask->WaterRingOuter.IsValidIndex(Ring) && Mask->WaterRingOuter[Ring] == INDEX_NONE && Box.bIsValid) { const FIntPoint Min = LPCityWater::WaterCell(Box.Min.X, Box.Min.Y); const FIntPoint Max = LPCityWater::WaterCell(Box.Max.X, Box.Max.Y); for (int32 X = Min.X; X <= Max.X; ++X) { for (int32 Y = Min.Y; Y <= Max.Y; ++Y) { WaterOuterCells.Add(FIntPoint(X, Y), Ring); } } } } } bool ULPCityWaterSubsystem::IsInWater(const FVector2D& Point) const { using namespace LPCityWater; if (!Mask || WaterOuterCells.IsEmpty()) { return false; } const int32 Rings = Mask->WaterRingStarts.Num(); auto RingEnd = [&](int32 Ring) { return Ring + 1 < Rings ? Mask->WaterRingStarts[Ring + 1] : Mask->WaterPoints.Num(); }; for (auto It = WaterOuterCells.CreateConstKeyIterator(WaterCell(Point.X, Point.Y)); It; ++It) { const int32 Outer = It.Value(); if (!WaterRingBoxes[Outer].IsInside(Point) || !InRing(Mask->WaterPoints, Mask->WaterRingStarts[Outer], RingEnd(Outer), Point)) { continue; } bool bInHole = false; for (int32 Ring = Outer + 1; Ring < Rings && Mask->WaterRingOuter[Ring] == Outer && !bInHole; ++Ring) { bInHole = WaterRingBoxes[Ring].IsInside(Point) && InRing(Mask->WaterPoints, Mask->WaterRingStarts[Ring], RingEnd(Ring), Point); } if (!bInHole) { return true; } } return false; } void ULPCityWaterSubsystem::HideHousesInWater(UStaticMeshComponent* MapComp, bool bForce) { using namespace LPCityWater; int32 MarkersCount = 0; AActor* Markers = (Mask || Landmarks || HouseTable) && MapComp ? FindMapMarkers(GetGameInstance(), MapComp->GetWorld(), &MarkersCount) : nullptr; if (!Markers) { return; } // STM1-SPEC-10 (C1) : nouvelle passe si GI.SpawnMapMarkers a recree l'acteur (meme ville, meme nombre possible). bForce |= LastMarkers.Get() != Markers; LastMarkers = Markers; TInlineComponentArray AllComponents(Markers); TArray> Houses; int32 Total = 0; TArray> Trees; int32 TotalTrees = 0; for (UInstancedStaticMeshComponent* Component : AllComponents) { if (IsHouseComponent(Component)) { Houses.Add(Component); Total += Component->GetInstanceCount(); } else if (IsTreeComponent(Component)) { Trees.Add(Component); TotalTrees += Component->GetInstanceCount(); } } // STM1-SPEC-12 (G4-b) : la ville a sa table de maisons : les maisons generiques (et les tours) de MapMarkers sont cachees. if (HouseTable) { int32 Hidden = 0; for (UInstancedStaticMeshComponent* Component : AllComponents) { const UStaticMesh* Mesh = Component ? Component->GetStaticMesh() : nullptr; const FString Name = Mesh ? Mesh->GetName() : FString(); if (Name.StartsWith(TEXT("House")) || (HouseTable->bReplaceSkyscrapers && Name.StartsWith(TEXT("Skyscraper")))) { if (Component->IsVisible()) { Component->SetVisibility(false); } ++Hidden; } } const FString HousesReport = FString::Printf(TEXT("[LPCity] MAISONS %s : table %d ; variantes %d ; posees %d ; composants generiques caches %d (acteur %s)"), *HouseTable->City, HouseTable->Centers.Num(), HouseTable->Variants.Num(), HouseComponents.Num(), Hidden, *Markers->GetName()); if (HousesReport != LastHousesReport) { LastHousesReport = HousesReport; UE_LOG(LogLPCity, Log, TEXT("%s"), *HousesReport); } } if (!bForce && Total == LastHouseInstances && TotalTrees == LastTreeInstances) { return; } LastHouseInstances = Total; LastTreeInstances = TotalTrees; // 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 ? Mask->Centers.Num() : 0); TBitArray<> LandmarkUsed(false, LandmarkHideCenters.Num()); int32 Masked = 0, Spared = 0, LandmarkMasked = 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); auto FindIn = [&](const TMultiMap& Cells, const TArray& Centers) { for (int32 DX = -1; DX <= 1; ++DX) { for (int32 DY = -1; DY <= 1; ++DY) { for (auto It = Cells.CreateConstKeyIterator(Cell + FIntPoint(DX, DY)); It; ++It) { if (FVector2D::Distance(Centers[It.Value()], FVector2D(Local.X, Local.Y)) <= MatchToleranceCm) { return It.Value(); } } } } return int32(INDEX_NONE); }; const int32 Hit = Mask ? FindIn(MaskCells, Mask->Centers) : INDEX_NONE; // STM1-SPEC-11 (M2) : maison generique a l'emplacement d'un monument pose. const int32 LandmarkHit = Hit == INDEX_NONE ? FindIn(LandmarkCells, LandmarkHideCenters) : INDEX_NONE; if (Hit == INDEX_NONE && LandmarkHit == INDEX_NONE) { continue; } if (LandmarkHit != INDEX_NONE) { LandmarkUsed[LandmarkHit] = true; ++LandmarkMasked; } else { 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(); } } // STM1-SPEC-10 (G3) : arbres poses dans l'eau (le filtre Bridge de MapMarkers ne voit pas les ponts non anglais ; // decalage lateral de 10 m le long des quais). int32 TreesMasked = 0; for (UInstancedStaticMeshComponent* Component : Trees) { 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()); if (!IsInWater(FVector2D(Local.X, Local.Y))) { continue; } ++TreesMasked; 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(); } } if (Landmarks) { const FString LandmarkReport = FString::Printf(TEXT("[LPCity] MONUMENTS %s : %d poses ; maisons masquees %d (centres %d, sans instance %d)"), *Landmarks->City, LandmarkComponents.Num(), LandmarkMasked, LandmarkHideCenters.Num(), LandmarkHideCenters.Num() - LandmarkUsed.CountSetBits()); if (LandmarkReport != LastLandmarkReport) { LastLandmarkReport = LandmarkReport; UE_LOG(LogLPCity, Log, TEXT("%s"), *LandmarkReport); } } if (!Mask) { return; } 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 ; arbres %d ; arbres masques %d (anneaux d'eau %d) ; acteur %s (cache %d) ; acteurs MapMarkers %d ; ClassicMap %d"), *Mask->City, Mask->Centers.Num(), Total, Masked, Spared, SpareAboveAreaM2, Unmatched, TotalTrees, TreesMasked, Mask->WaterRingStarts.Num(), *Markers->GetName(), Markers->IsHidden() ? 1 : 0, MarkersCount, BoolProperty(GetGameInstance(), TEXT("ClassicMap"))); 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(); // STM1-SPEC-09 (lot A) : l'eau d'un pack lpm_city_* remplace celle du jeu de base. const FString WaterObject = ULPAssetRegistryHelpers::ResolveCityPath(FString::Printf(TEXT("/Game/Heavy/StreetMap/Water/%s.%s"), *City, *City)); const FString WaterPackage = FPackageName::ObjectPathToPackageName(WaterObject); LoadMask(City); UpdateLandmarks(Street, MapComp, City); UpdateHouses(Street, MapComp, City); UpdateClassic(Street, MapComp, City); UpdateGround(Street, MapComp, City); UStaticMesh* WaterMesh = nullptr; if (!City.IsEmpty() && FPackageName::DoesPackageExist(WaterPackage)) { WaterMesh = LoadObject(nullptr, *WaterObject); } 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); } void ULPCityWaterSubsystem::UpdateLandmarks(AActor* Street, UStaticMeshComponent* MapComp, const FString& City) { // STM1-SPEC-11 (M2) : retirer les monuments de la carte precedente. for (UStaticMeshComponent* Component : LandmarkComponents) { if (IsValid(Component)) { Component->DestroyComponent(); } } LandmarkComponents.Reset(); LandmarkHideCenters.Reset(); LandmarkCells.Reset(); LastLandmarkReport.Reset(); Landmarks = nullptr; const FString Object = ULPAssetRegistryHelpers::ResolveCityPath(FString::Printf(TEXT("/Game/Heavy/StreetMap/Landmarks/%s.%s"), *City, *City)); if (City.IsEmpty() || !FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(Object))) { return; } Landmarks = LoadObject(nullptr, *Object); if (!Landmarks) { UE_LOG(LogLPCity, Warning, TEXT("[LPCity] MONUMENTS %s : %s illisible"), *City, *Object); return; } TArray Placed, Missing; for (const FLPCityLandmark& Landmark : Landmarks->Landmarks) { for (const FVector2D& Center : Landmark.HideCenters) { LandmarkCells.Add(LPCityWater::MaskCell(Center.X, Center.Y), LandmarkHideCenters.Add(Center)); } UStaticMesh* Mesh = Landmark.Mesh.LoadSynchronous(); if (!Mesh) { Missing.Add(Landmark.Name); continue; } UStaticMeshComponent* Component = NewObject(Street, MakeUniqueObjectName(Street, UStaticMeshComponent::StaticClass(), TEXT("LPCityLandmark"))); Component->SetupAttachment(MapComp); Component->SetMobility(EComponentMobility::Movable); Component->SetCollisionEnabled(ECollisionEnabled::NoCollision); Component->SetStaticMesh(Mesh); Component->SetRelativeLocation(FVector(Landmark.CenterCm.X, Landmark.CenterCm.Y, Landmark.ZOffsetCm)); Component->SetRelativeRotation(FRotator(0.0, Landmark.YawDeg, 0.0)); Component->SetRelativeScale3D(FVector(Landmark.Scale)); Component->RegisterComponent(); LandmarkComponents.Add(Component); Placed.Add(Landmark.Name); } UE_LOG(LogLPCity, Log, TEXT("[LPCity] MONUMENTS %s : %s ; poses %d (%s) ; sans maillage %d (%s) ; centres a masquer %d"), *City, *Object, Placed.Num(), *FString::Join(Placed, TEXT(", ")), Missing.Num(), *FString::Join(Missing, TEXT(", ")), LandmarkHideCenters.Num()); } void ULPCityWaterSubsystem::UpdateHouses(AActor* Street, UStaticMeshComponent* MapComp, const FString& City) { // STM1-SPEC-12 (G4-b) : retirer les maisons de la carte precedente. for (UHierarchicalInstancedStaticMeshComponent* Component : HouseComponents) { if (IsValid(Component)) { Component->DestroyComponent(); } } HouseComponents.Reset(); LastHousesReport.Reset(); LastHousesClassic = -2; HouseTable = nullptr; const FString Object = ULPAssetRegistryHelpers::ResolveCityPath(FString::Printf(TEXT("/Game/Heavy/StreetMap/Houses/%s.%s"), *City, *City)); if (City.IsEmpty() || !FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(Object))) { return; } HouseTable = LoadObject(nullptr, *Object); const int32 Count = HouseTable ? FMath::Min(FMath::Min(HouseTable->Centers.Num(), HouseTable->YawDeg.Num()), FMath::Min(HouseTable->SizeCm.Num(), HouseTable->Variant.Num())) : 0; if (!HouseTable || Count == 0) { UE_LOG(LogLPCity, Warning, TEXT("[LPCity] MAISONS %s : table %s illisible ou vide"), *City, *Object); HouseTable = nullptr; return; } // Un HISM par variante. Chaque instance : boite du maillage ramenee aux dimensions de l'entree, pied au sol, centre de la // boite sur le centre du batiment, tournee du lacet de l'arete la plus longue. TArray> PerVariant; PerVariant.SetNum(HouseTable->Variants.Num()); TArray Meshes; TArray Boxes; for (const TSoftObjectPtr& Soft : HouseTable->Variants) { UStaticMesh* Mesh = Soft.LoadSynchronous(); Meshes.Add(Mesh); Boxes.Add(Mesh ? Mesh->GetBoundingBox() : FBox(ForceInit)); } for (int32 Index = 0; Index < Count; ++Index) { const int32 V = HouseTable->Variant[Index]; if (!Meshes.IsValidIndex(V) || !Meshes[V] || !Boxes[V].IsValid) { continue; } const FBox& Box = Boxes[V]; const FVector BoxSize = Box.GetSize().ComponentMax(FVector(1.0)); const FVector Scale = HouseTable->SizeCm[Index] / BoxSize; const FVector Foot((Box.Min.X + Box.Max.X) * 0.5, (Box.Min.Y + Box.Max.Y) * 0.5, Box.Min.Z); const FRotator Rotation(0.0, HouseTable->YawDeg[Index], 0.0); const FVector Location = FVector(HouseTable->Centers[Index].X, HouseTable->Centers[Index].Y, 0.0) - Rotation.RotateVector(Foot * Scale); PerVariant[V].Add(FTransform(Rotation, Location, Scale)); } int32 Placed = 0, Furniture = 0; for (int32 V = 0; V < Meshes.Num(); ++V) { if (!Meshes[V] || PerVariant[V].IsEmpty()) { continue; } UHierarchicalInstancedStaticMeshComponent* Component = NewObject(Street, MakeUniqueObjectName(Street, UHierarchicalInstancedStaticMeshComponent::StaticClass(), TEXT("LPCityHouses"))); Component->SetupAttachment(MapComp); Component->SetMobility(EComponentMobility::Movable); Component->SetCollisionEnabled(ECollisionEnabled::NoCollision); Component->SetStaticMesh(Meshes[V]); // 1.7.7 : mobilier urbain (voir lp.City.FurnitureCullM). if (Meshes[V]->GetName().StartsWith(TEXT("Mobilier_"))) { const float CullM = LPCityWater::CVarFurnitureCullM.GetValueOnGameThread(); if (CullM > 0.f) { Component->SetCullDistances(0, FMath::RoundToInt32(CullM * 100.f)); } Component->SetCastShadow(false); ++Furniture; } Component->RegisterComponent(); Component->AddInstances(PerVariant[V], false, false); HouseComponents.Add(Component); Placed += PerVariant[V].Num(); } UE_LOG(LogLPCity, Log, TEXT("[LPCity] MAISONS %s : %s ; table %d ; variantes %d ; instances posees %d ; composants %d ; mobilier %d variante(s), efface a %.0f m"), *City, *Object, Count, Meshes.Num(), Placed, HouseComponents.Num(), Furniture, LPCityWater::CVarFurnitureCullM.GetValueOnGameThread()); SyncHousesVisibility(); } void ULPCityWaterSubsystem::UpdateClassic(AActor* Street, UStaticMeshComponent* MapComp, const FString& City) { // 1.3.0 (STM1-SPEC-14) : retirer la carte classique en 3D de la carte precedente. if (IsValid(ClassicComponent)) { ClassicComponent->DestroyComponent(); } ClassicComponent = nullptr; const FString Object = ULPAssetRegistryHelpers::ResolveCityPath(FString::Printf(TEXT("/Game/Heavy/StreetMap/Classic/%s.%s"), *City, *City)); if (City.IsEmpty() || !FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(Object))) { return; } UStaticMesh* Mesh = LoadObject(nullptr, *Object); if (!Mesh) { UE_LOG(LogLPCity, Warning, TEXT("[LPCity] CLASSIQUE %s : %s illisible"), *City, *Object); return; } // Meme repere que la carte (rattache a son composant), batiments extrudes seuls ; visibilite reglee par SyncHousesVisibility. ClassicComponent = NewObject(Street, MakeUniqueObjectName(Street, UStaticMeshComponent::StaticClass(), TEXT("LPCityClassic"))); ClassicComponent->SetupAttachment(MapComp); ClassicComponent->SetMobility(EComponentMobility::Movable); ClassicComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); ClassicComponent->SetStaticMesh(Mesh); ClassicComponent->SetVisibility(false); ClassicComponent->RegisterComponent(); LastHousesClassic = -2; UE_LOG(LogLPCity, Log, TEXT("[LPCity] CLASSIQUE %s : pose %s (visible en mode classique seulement)"), *City, *Mesh->GetPathName()); SyncHousesVisibility(); } void ULPCityWaterSubsystem::RestoreIcons() { int32 Restored = 0; for (const TWeakObjectPtr& Weak : IconsHiddenByFilter) { if (USceneComponent* Icon = Weak.Get()) { Icon->SetVisibility(true); ++Restored; } } IconsHiddenByFilter.Empty(); LastIconCount = -1; LastIconCamera = FVector(TNumericLimits::Max()); if (!LastIconsReport.IsEmpty()) { UE_LOG(LogLPCity, Log, TEXT("[LPCity] ICONES : filtre inactif ; %d icone(s) rendue(s)"), Restored); LastIconsReport.Reset(); } } void ULPCityWaterSubsystem::OnPostActorTick(UWorld* World, ELevelTick TickType, float DeltaSeconds) { UStaticMeshComponent* MapComp = IconMapComp.Get(); if (MapComp && MapComp->GetWorld() == World) { ApplyGround(MapComp); UpdateIcons(MapComp, bIconPackCity); } } void ULPCityWaterSubsystem::UpdateGround(AActor* Street, UStaticMeshComponent* MapComp, const FString& City) { // Carte precedente : rendre au composant le materiau du jeu (le composant sert aussi a la ville suivante), retirer ses trottoirs. if (GroundMaterial && GroundReplaced && MapComp->GetMaterial(0) == GroundMaterial) { MapComp->SetMaterial(0, GroundReplaced); } if (IsValid(SidewalkComponent)) { SidewalkComponent->DestroyComponent(); } SidewalkComponent = nullptr; GroundMaterial = nullptr; GroundReplaced = nullptr; GroundCity = City; LastGroundState = -2; GroundReapplied = 0; // Pack seulement : les villes du jeu de base ne changent pas. const FString MaterialObject = ULPAssetRegistryHelpers::ResolveCityPath(FString::Printf(TEXT("/Game/Heavy/StreetMap/Ground/%s.%s"), *City, *City)); if (City.IsEmpty() || !MaterialObject.StartsWith(FString(TEXT("/")) + ULPAssetRegistryHelpers::CityPackPrefix()) || !FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(MaterialObject))) { return; } GroundMaterial = LoadObject(nullptr, *MaterialObject); if (!GroundMaterial) { UE_LOG(LogLPCity, Warning, TEXT("[LPCity] SOL %s : %s illisible"), *City, *MaterialObject); return; } int32 Triangles = 0; const FString SidewalkObject = ULPAssetRegistryHelpers::ResolveCityPath(FString::Printf(TEXT("/Game/Heavy/StreetMap/Sidewalks/%s.%s"), *City, *City)); if (FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(SidewalkObject))) { if (UStaticMesh* Mesh = LoadObject(nullptr, *SidewalkObject)) { // Meme repere que la carte (rattache a son composant), quelques cm sous les routes ; visibilite par SyncHousesVisibility. SidewalkComponent = NewObject(Street, MakeUniqueObjectName(Street, UStaticMeshComponent::StaticClass(), TEXT("LPCitySidewalks"))); SidewalkComponent->SetupAttachment(MapComp); SidewalkComponent->SetMobility(EComponentMobility::Movable); SidewalkComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); SidewalkComponent->SetCastShadow(false); SidewalkComponent->SetStaticMesh(Mesh); SidewalkComponent->SetVisibility(false); SidewalkComponent->RegisterComponent(); Triangles = Mesh->GetNumTriangles(0); } } LastHousesClassic = -2; UE_LOG(LogLPCity, Log, TEXT("[LPCity] SOL %s : materiau %s ; trottoirs %d triangles"), *City, *GroundMaterial->GetPathName(), Triangles); SyncHousesVisibility(); } void ULPCityWaterSubsystem::ApplyGround(UStaticMeshComponent* MapComp) { if (!GroundMaterial) { return; } // 0 : mode classique ; 1 : materiau du sol pose ; 2 : lp.City.Ground 0. const int32 Classic = LPCityWater::BoolProperty(GetGameInstance(), TEXT("ClassicMap")); const int32 State = Classic == 1 ? 0 : (LPCityWater::CVarCityGround.GetValueOnGameThread() != 0 ? 1 : 2); UMaterialInterface* Current = MapComp->GetMaterial(0); if (State == 1 && Current != GroundMaterial) { GroundReplaced = Current; MapComp->SetMaterial(0, GroundMaterial); if (LastGroundState == 1) { // Le BP a remis son materiau (HideBuildings) : repris dans la meme image. ++GroundReapplied; UE_LOG(LogLPCity, Log, TEXT("[LPCity] SOL %s : materiau repose apres le BP (%d) ; remplace %s"), *GroundCity, GroundReapplied, *GetPathNameSafe(Current)); } } else if (State == 2 && Current == GroundMaterial && GroundReplaced) { MapComp->SetMaterial(0, GroundReplaced); } if (State != LastGroundState) { LastGroundState = State; if (State == 1) { UE_LOG(LogLPCity, Log, TEXT("[LPCity] SOL %s : materiau %s pose (mode normal) ; remplace %s"), *GroundCity, *GroundMaterial->GetPathName(), *GetPathNameSafe(GroundReplaced)); } else { UE_LOG(LogLPCity, Log, TEXT("[LPCity] SOL %s : %s, materiau du jeu laisse (%s)"), *GroundCity, State == 0 ? TEXT("mode classique") : TEXT("lp.City.Ground 0"), *GetPathNameSafe(MapComp->GetMaterial(0))); } SyncHousesVisibility(); } } void ULPCityWaterSubsystem::UpdateIcons(UStaticMeshComponent* MapComp, bool bPackCity) { using namespace LPCityWater; UWorld* World = MapComp ? MapComp->GetWorld() : nullptr; if (!World || !bPackCity || CVarIconFilter.GetValueOnGameThread() == 0) { if (!IconsHiddenByFilter.IsEmpty() || !LastIconsReport.IsEmpty()) { RestoreIcons(); } return; } AActor* Markers = FindMapMarkers(GetGameInstance(), World); const APlayerController* Controller = World->GetFirstPlayerController(); if (!Markers || !Controller || !Controller->PlayerCameraManager) { return; } // MapMarkers.WidgetComponents : une icone (WidgetComponent) par batiment dont l'amenite a une image (STM1-SPEC-15 § 1). const FArrayProperty* ArrayProp = CastField(Markers->GetClass()->FindPropertyByName(TEXT("WidgetComponents"))); const FObjectPropertyBase* Inner = ArrayProp ? CastField(ArrayProp->Inner) : nullptr; if (!Inner) { return; } FScriptArrayHelper Icons(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Markers)); const FVector Camera = Controller->PlayerCameraManager->GetCameraLocation(); // Plan de la carte : celui de l'acteur de la carte (le plan de MapMarkers est cherche seulement au recalcul). const double MapZ = MapComp->GetComponentLocation().Z; const FVector Forward = Controller->PlayerCameraManager->GetCameraRotation().Vector(); FVector ViewPoint = Camera; if (CVarIconNearFromView.GetValueOnGameThread() != 0 && Forward.Z < -0.05) { ViewPoint = Camera + Forward * FMath::Min((MapZ - Camera.Z) / Forward.Z, 5000000.0); } if (Markers == LastIconMarkers.Get() && Icons.Num() == LastIconCount && FVector::Dist(Camera, LastIconCamera) < IconRecomputeMoveCm && FVector::Dist(ViewPoint, LastIconViewPoint) < IconRecomputeMoveCm) { return; } const bool bNewMarkers = Markers != LastIconMarkers.Get(); LastIconMarkers = Markers; LastIconCount = Icons.Num(); LastIconCamera = Camera; LastIconViewPoint = ViewPoint; if (bNewMarkers) { IconsHiddenByFilter.Empty(); } double PlaneZ = MapZ; FindPlaneZ(World, PlaneZ); const double AltitudeCm = Camera.Z - PlaneZ; // Hysteresis : le mode bas tient jusqu'a 1,1 x l'altitude seuil ; une icone visible le reste jusqu'a 1,15 x la distance. const double LowCm = double(CVarIconLowAltitudeM.GetValueOnGameThread()) * 100.0; const bool bLow = AltitudeCm < (bLastIconLow ? LowCm * 1.1 : LowCm); bLastIconLow = bLow; const double NearCm = double(CVarIconNearM.GetValueOnGameThread()) * 100.0; const double CellCm = FMath::Max(100.0, double(CVarIconCellM.GetValueOnGameThread()) * 100.0); TSet Cells; int32 Total = 0, Shown = 0, Filtered = 0; for (int32 Index = 0; Index < Icons.Num(); ++Index) { USceneComponent* Icon = Cast(Inner->GetObjectPropertyValue(Icons.GetRawPtr(Index))); if (!IsValid(Icon)) { continue; } ++Total; const FVector P = Icon->GetComponentLocation(); bool bShow = true; if (bLow) { const bool bWasShown = Icon->IsVisible(); bShow = FVector2D::Distance(FVector2D(P.X, P.Y), FVector2D(ViewPoint.X, ViewPoint.Y)) <= (bWasShown ? NearCm * 1.15 : NearCm); } else { bool bTaken = false; Cells.Add(FIntPoint(FMath::FloorToInt32(P.X / CellCm), FMath::FloorToInt32(P.Y / CellCm)), &bTaken); bShow = !bTaken; } const TWeakObjectPtr Weak(Icon); if (bShow) { if (IconsHiddenByFilter.Remove(Weak) > 0) { Icon->SetVisibility(true); } Shown += Icon->IsVisible() ? 1 : 0; } else { // Seules les icones visibles sont cachees et retenues : celles que MapMarkers cache lui-meme restent a lui. if (Icon->IsVisible()) { Icon->SetVisibility(false); IconsHiddenByFilter.Add(Weak); } Filtered += IconsHiddenByFilter.Contains(Weak) ? 1 : 0; } } const FString City = MapComp->GetStaticMesh() ? MapComp->GetStaticMesh()->GetName() : FString(); const FString Report = FString::Printf(TEXT("[LPCity] ICONES %s : %d icone(s) ; mode %s (seuils %.0f / %.0f / %.0f m) ; visibles %d ; cachees par le filtre %d"), *City, Total, bLow ? TEXT("bas") : TEXT("haut"), CVarIconLowAltitudeM.GetValueOnGameThread(), CVarIconNearM.GetValueOnGameThread(), CVarIconCellM.GetValueOnGameThread(), Shown, Filtered); // Temoin au changement de mode ou du nombre d'icones (pas a chaque deplacement de la camera). const FString Key = FString::Printf(TEXT("%s|%d|%d"), *City, Total, bLow ? 1 : 0); if (Key != LastIconsReport) { LastIconsReport = Key; UE_LOG(LogLPCity, Log, TEXT("%s ; camera a %.0f m ; point regarde a %.0f m"), *Report, AltitudeCm / 100.0, FVector2D::Distance(FVector2D(Camera.X, Camera.Y), FVector2D(ViewPoint.X, ViewPoint.Y)) / 100.0); } } void ULPCityWaterSubsystem::SyncHousesVisibility() { if (HouseComponents.IsEmpty() && !IsValid(ClassicComponent) && !IsValid(SidewalkComponent)) { return; } // 1.2.2 (COMMUN-25b.6) : la visibilite voulue est imposee a chaque appel, pas seulement au changement de ClassicMap : // le jeu peut rendre visibles les enfants du composant de la carte (au rechoix de la ville), donc nos HISM. const int32 Classic = LPCityWater::BoolProperty(GetGameInstance(), TEXT("ClassicMap")); const bool bVisible = Classic != 1; int32 Restored = 0; for (UHierarchicalInstancedStaticMeshComponent* Component : HouseComponents) { if (IsValid(Component) && Component->IsVisible() != bVisible) { Component->SetVisibility(bVisible); ++Restored; } } // 1.4.0 : trottoirs du pack comme les maisons, et caches si lp.City.Ground vaut 0. const bool bSidewalks = bVisible && LPCityWater::CVarCityGround.GetValueOnGameThread() != 0; if (IsValid(SidewalkComponent) && SidewalkComponent->IsVisible() != bSidewalks) { SidewalkComponent->SetVisibility(bSidewalks); ++Restored; } // 1.3.0 : la carte classique en 3D est visible seulement en mode classique. if (IsValid(ClassicComponent) && ClassicComponent->IsVisible() == bVisible) { ClassicComponent->SetVisibility(!bVisible); ++Restored; } if (Classic != LastHousesClassic) { LastHousesClassic = Classic; UE_LOG(LogLPCity, Log, TEXT("[LPCity] MAISONS : ClassicMap %d -> maisons du pack %s"), Classic, bVisible ? TEXT("visibles") : TEXT("cachees")); } else if (Restored > 0) { UE_LOG(LogLPCity, Log, TEXT("[LPCity] MAISONS : ClassicMap %d -> visibilite des maisons du pack retablie (%d composant(s))"), Classic, Restored); } }