1041 lines
40 KiB
C++
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);
|
|
}
|
|
}
|