LifePlay-Revival/Source/LifePlay/LPCityWaterSubsystem.cpp
2026-10-22 21:10:39 +02:00

1041 lines
40 KiB
C++

// 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<int32> 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<float> 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<float> CVarIconNearM(TEXT("lp.City.IconNearM"), 120.f,
TEXT("Sous l'altitude basse : distance horizontale (m) des icones gardees autour de la camera."));
static TAutoConsoleVariable<float> 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<int32> 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<int32> CVarCityGround(TEXT("lp.City.Ground"), 1,
TEXT("Villes de pack : 1 = chaussee, paves (materiau Ground/<Ville>) 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<float> 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<FVector2D>& 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<FObjectPropertyBase>(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<FBoolProperty>(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<AActor> It(World); It; ++It)
{
if (It->GetClass()->GetName().Contains(TEXT("MapMarkers")))
{
++Count;
First = First ? First : *It;
}
}
}
if (OutCount)
{
*OutCount = Count;
}
AActor* FromGI = Cast<AActor>(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<UStaticMeshComponent*> 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<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 || 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<ULPCityWaterMask>(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<UInstancedStaticMeshComponent*> AllComponents(Markers);
TArray<UInstancedStaticMeshComponent*, TInlineAllocator<16>> Houses;
int32 Total = 0;
TArray<UInstancedStaticMeshComponent*, TInlineAllocator<4>> 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<FIntPoint, int32>& Cells, const TArray<FVector2D>& 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<UStaticMesh>(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<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);
}
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<ULPCityLandmarks>(nullptr, *Object);
if (!Landmarks)
{
UE_LOG(LogLPCity, Warning, TEXT("[LPCity] MONUMENTS %s : %s illisible"), *City, *Object);
return;
}
TArray<FString> 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<UStaticMeshComponent>(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<ULPCityHouses>(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<TArray<FTransform>> PerVariant;
PerVariant.SetNum(HouseTable->Variants.Num());
TArray<UStaticMesh*> Meshes;
TArray<FBox> Boxes;
for (const TSoftObjectPtr<UStaticMesh>& 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<UHierarchicalInstancedStaticMeshComponent>(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<UStaticMesh>(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<UStaticMeshComponent>(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<USceneComponent>& Weak : IconsHiddenByFilter)
{
if (USceneComponent* Icon = Weak.Get())
{
Icon->SetVisibility(true);
++Restored;
}
}
IconsHiddenByFilter.Empty();
LastIconCount = -1;
LastIconCamera = FVector(TNumericLimits<double>::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<UMaterialInterface>(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<UStaticMesh>(nullptr, *SidewalkObject))
{
// Meme repere que la carte (rattache a son composant), quelques cm sous les routes ; visibilite par SyncHousesVisibility.
SidewalkComponent = NewObject<UStaticMeshComponent>(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<FArrayProperty>(Markers->GetClass()->FindPropertyByName(TEXT("WidgetComponents")));
const FObjectPropertyBase* Inner = ArrayProp ? CastField<FObjectPropertyBase>(ArrayProp->Inner) : nullptr;
if (!Inner)
{
return;
}
FScriptArrayHelper Icons(ArrayProp, ArrayProp->ContainerPtrToValuePtr<void>(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<FIntPoint> Cells;
int32 Total = 0, Shown = 0, Filtered = 0;
for (int32 Index = 0; Index < Icons.Num(); ++Index)
{
USceneComponent* Icon = Cast<USceneComponent>(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<USceneComponent> 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);
}
}