LifePlay-Revival/Source/LifePlay/LPDebugSubsystem.cpp

821 lines
32 KiB
C++
Raw Permalink Normal View History

// Portage UE 5.5 - lot debug. Voir LPDebugSubsystem.h.
#include "LPDebugSubsystem.h"
#include "LifePlay.h"
#if !UE_BUILD_SHIPPING
#include "Chaos/TriangleMeshImplicitObject.h"
#include "Components/CapsuleComponent.h"
#include "Components/PrimitiveComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Animation/AnimInstance.h"
#include "Blueprint/UserWidget.h"
#include "GameFramework/InputSettings.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerInput.h"
#include "LPKeyboardSubsystem.h"
#include "UObject/UObjectIterator.h"
#include "Animation/AnimSingleNodeInstance.h"
#include "Containers/Ticker.h"
#include "Framework/Application/SlateApplication.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Engine/GameInstance.h"
#include "UObject/UnrealType.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "HAL/FileManager.h"
#include "HAL/IConsoleManager.h"
#include "HAL/PlatformMemory.h"
#include "Misc/CommandLine.h"
#include "Misc/CoreDelegates.h"
#include "Misc/FileHelper.h"
#include "Misc/Parse.h"
#include "Misc/Paths.h"
#include "PhysicsEngine/BodySetup.h"
#include "ProfilingDebugging/MiscTrace.h"
namespace
{
FAutoConsoleCommand* MemSnapshotCommand = nullptr;
FAutoConsoleCommand* CollisionDumpCommand = nullptr;
FAutoConsoleCommand* BookmarkCommand = nullptr;
FAutoConsoleCommand* HitchSummaryCommand = nullptr;
ULPDebugSubsystem* CommandOwner = nullptr;
ULPDebugSubsystem* FindDebugSubsystem()
{
return CommandOwner;
}
UWorld* FindPlayWorld()
{
if (!GEngine)
{
return nullptr;
}
if (UWorld* Play = GEngine->GetCurrentPlayWorld())
{
return Play;
}
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
UWorld* World = Context.World();
if (World && (World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE))
{
return World;
}
}
return nullptr;
}
FString DebugDir()
{
const FString Dir = FPaths::ProjectSavedDir() / TEXT("Profiling/LPDebug");
IFileManager::Get().MakeDirectory(*Dir, true);
return Dir;
}
void WriteMemSnapshot()
{
const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
const FString Path = DebugDir() / FString::Printf(TEXT("MemSnapshot_%s.txt"), *Stamp);
const FPlatformMemoryStats Stats = FPlatformMemory::GetStats();
TArray<FString> Lines;
Lines.Add(FString::Printf(TEXT("When=%s"), *FDateTime::Now().ToString()));
Lines.Add(FString::Printf(TEXT("UsedPhysicalMB=%.1f"), Stats.UsedPhysical / (1024.0 * 1024.0)));
Lines.Add(FString::Printf(TEXT("PeakUsedPhysicalMB=%.1f"), Stats.PeakUsedPhysical / (1024.0 * 1024.0)));
Lines.Add(FString::Printf(TEXT("AvailablePhysicalMB=%.1f"), Stats.AvailablePhysical / (1024.0 * 1024.0)));
Lines.Add(FString::Printf(TEXT("UsedVirtualMB=%.1f"), Stats.UsedVirtual / (1024.0 * 1024.0)));
UWorld* World = FindPlayWorld();
if (GEngine)
{
const bool bReport = GEngine->Exec(World, TEXT("memreport"));
Lines.Add(FString::Printf(TEXT("memreport=%s"), bReport ? TEXT("ok") : TEXT("echec")));
}
const FString ReportDir = FPaths::ProjectSavedDir() / TEXT("Profiling/MemReports");
TArray<FString> Reports;
IFileManager::Get().FindFilesRecursive(Reports, *ReportDir, TEXT("*.memreport"), true, false);
if (Reports.Num() > 0)
{
Reports.Sort();
Lines.Add(FString::Printf(TEXT("NewestMemReport=%s"), *Reports.Last()));
}
Lines.Add(TEXT("PhysicsMB=voir LLM (colonne Physics) ou le MemReport ; ce fichier ne remplace pas la session B"));
FFileHelper::SaveStringArrayToFile(Lines, *Path);
UE_LOG(LogLifePlay, Display, TEXT("LPDebug MemSnapshot : %s"), *Path);
}
struct FCollisionOwner
{
FString Actor;
FString ClassName;
FString Component;
FString Profile;
int64 Triangles = 0;
int32 TriMeshes = 0;
int32 Simple = 0;
int32 Capsules = 0;
};
struct FCollisionTally
{
FString WorldName;
int32 Prim = 0;
int32 Simulating = 0;
int32 QueryOnly = 0;
int32 Persons = 0;
int32 Players = 0;
int32 Street = 0;
int32 UniqueBodies = 0;
int32 UniqueSimple = 0;
int32 UniqueTriMeshes = 0;
int32 UniqueCapsules = 0;
int64 Triangles = 0;
TMap<FString, int32> ByClass;
TMap<FString, int64> TrisByClass;
TArray<FCollisionOwner> Owners;
};
int64 CountTriangles(const UBodySetup* Body)
{
int64 Count = 0;
if (!Body)
{
return 0;
}
for (const Chaos::FTriangleMeshImplicitObjectPtr& Mesh : Body->TriMeshGeometries)
{
if (Mesh)
{
Count += Mesh->Elements().GetNumTriangles();
}
}
return Count;
}
FString ComponentLabel(const UPrimitiveComponent* Prim)
{
if (const UStaticMeshComponent* MeshComp = Cast<UStaticMeshComponent>(Prim))
{
if (const UStaticMesh* Mesh = MeshComp->GetStaticMesh())
{
return Mesh->GetName();
}
}
return Prim ? Prim->GetName() : FString();
}
FCollisionTally GatherCollision()
{
FCollisionTally Tally;
UWorld* World = FindPlayWorld();
Tally.WorldName = World ? World->GetName() : TEXT("(aucune)");
if (!World)
{
return Tally;
}
TSet<const UBodySetup*> Bodies;
for (TActorIterator<AActor> It(World); It; ++It)
{
const FString ClassName = It->GetClass()->GetName();
TInlineComponentArray<UPrimitiveComponent*> Comps;
It->GetComponents(Comps);
for (UPrimitiveComponent* Prim : Comps)
{
if (!Prim || !Prim->IsCollisionEnabled())
{
continue;
}
++Tally.Prim;
Tally.ByClass.FindOrAdd(ClassName)++;
if (ClassName.StartsWith(TEXT("Person_C")))
{
++Tally.Persons;
}
else if (ClassName.StartsWith(TEXT("PC_C")))
{
++Tally.Players;
}
else if (ClassName.Contains(TEXT("StreetMap")))
{
++Tally.Street;
}
if (Prim->IsSimulatingPhysics())
{
++Tally.Simulating;
}
if (Prim->GetCollisionEnabled() == ECollisionEnabled::QueryOnly)
{
++Tally.QueryOnly;
}
UBodySetup* Body = Prim->GetBodySetup();
if (!Body || Bodies.Contains(Body))
{
continue;
}
Bodies.Add(Body);
const int64 Tris = CountTriangles(Body);
const int32 Capsules = Body->AggGeom.SphylElems.Num();
const int32 Simple = Body->AggGeom.GetElementCount();
Tally.UniqueBodies++;
Tally.UniqueSimple += Simple;
Tally.UniqueCapsules += Capsules;
Tally.UniqueTriMeshes += Body->TriMeshGeometries.Num();
Tally.Triangles += Tris;
Tally.TrisByClass.FindOrAdd(ClassName) += Tris;
if (Tris > 0 || Simple > 0)
{
FCollisionOwner& Owner = Tally.Owners.AddDefaulted_GetRef();
Owner.Actor = It->GetName();
Owner.ClassName = ClassName;
Owner.Component = ComponentLabel(Prim);
Owner.Profile = Prim->GetCollisionProfileName().ToString();
Owner.Triangles = Tris;
Owner.TriMeshes = Body->TriMeshGeometries.Num();
Owner.Simple = Simple;
Owner.Capsules = Capsules;
}
}
}
Tally.Owners.Sort([](const FCollisionOwner& A, const FCollisionOwner& B)
{
return A.Triangles > B.Triangles;
});
return Tally;
}
FString WriteCollisionDump(const TCHAR* Reason, const FCollisionTally* Existing = nullptr)
{
const FCollisionTally Tally = Existing ? *Existing : GatherCollision();
const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
const FString Path = DebugDir() / FString::Printf(TEXT("CollisionDump_%s.txt"), *Stamp);
TArray<FString> Lines;
Lines.Add(FString::Printf(TEXT("When=%s"), *FDateTime::Now().ToString()));
Lines.Add(FString::Printf(TEXT("Reason=%s"), Reason));
Lines.Add(FString::Printf(TEXT("World=%s"), *Tally.WorldName));
Lines.Add(FString::Printf(TEXT("CollisionPrimitives=%d"), Tally.Prim));
Lines.Add(FString::Printf(TEXT("PersonPrimitives=%d"), Tally.Persons));
Lines.Add(FString::Printf(TEXT("PlayerPrimitives=%d"), Tally.Players));
Lines.Add(FString::Printf(TEXT("StreetMapPrimitives=%d"), Tally.Street));
Lines.Add(FString::Printf(TEXT("Simulating=%d"), Tally.Simulating));
Lines.Add(FString::Printf(TEXT("QueryOnly=%d"), Tally.QueryOnly));
Lines.Add(FString::Printf(TEXT("UniqueBodySetups=%d"), Tally.UniqueBodies));
Lines.Add(FString::Printf(TEXT("UniqueSimpleShapes=%d"), Tally.UniqueSimple));
Lines.Add(FString::Printf(TEXT("UniqueCapsules=%d"), Tally.UniqueCapsules));
Lines.Add(FString::Printf(TEXT("UniqueTriMeshes=%d"), Tally.UniqueTriMeshes));
Lines.Add(FString::Printf(TEXT("Triangles=%lld"), Tally.Triangles));
Lines.Add(TEXT("TopClasses:"));
TArray<TPair<FString, int32>> Classes;
for (const TPair<FString, int32>& Pair : Tally.ByClass)
{
Classes.Add(Pair);
}
Classes.Sort([](const TPair<FString, int32>& A, const TPair<FString, int32>& B) { return A.Value > B.Value; });
for (int32 i = 0; i < Classes.Num() && i < 15; ++i)
{
const int64 Tris = Tally.TrisByClass.FindRef(Classes[i].Key);
Lines.Add(FString::Printf(TEXT(" %d %s triangles=%lld"), Classes[i].Value, *Classes[i].Key, Tris));
}
Lines.Add(TEXT("OwnersByTriangles:"));
for (int32 i = 0; i < Tally.Owners.Num() && i < 12; ++i)
{
const FCollisionOwner& Owner = Tally.Owners[i];
Lines.Add(FString::Printf(
TEXT(" triangles=%lld trimesh=%d simple=%d capsules=%d profile=%s %s %s %s"),
Owner.Triangles, Owner.TriMeshes, Owner.Simple, Owner.Capsules,
*Owner.Profile, *Owner.ClassName, *Owner.Actor, *Owner.Component));
}
if (Tally.WorldName == TEXT("(aucune)"))
{
Lines.Add(TEXT("Note=monde de jeu absent (appeler la commande une fois la partie chargee)"));
}
Lines.Add(TEXT("Switch=-LPCollisionLog pour echantillonner apres le chargement, pas au ExecCmds de demarrage"));
FFileHelper::SaveStringArrayToFile(Lines, *Path);
UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %d primitives, %d trimesh, %lld triangles (%s) -> %s"),
Tally.Prim, Tally.UniqueTriMeshes, Tally.Triangles, Reason, *Path);
TRACE_BOOKMARK(TEXT("LP Collision %lld tris"), Tally.Triangles);
return Path;
}
// Lot STG (mesure M1) : position des personnages par rapport au sol visible (B-SOL) et distance entre eux (B-CHEV).
FAutoConsoleCommand* PersonsDumpCommand = nullptr;
FTSTicker::FDelegateHandle PersonsTickHandle;
constexpr double PersonsMinGapCm = 10.0;
FString ReadObjectName(const UObject* Container, FName Name)
{
const FObjectPropertyBase* Prop = Container ? CastField<FObjectPropertyBase>(Container->GetClass()->FindPropertyByName(Name)) : nullptr;
const UObject* Value = Prop ? Prop->GetObjectPropertyValue_InContainer(Container) : nullptr;
return Value ? Value->GetName() : TEXT("-");
}
FString ReadBool(const UObject* Container, FName Name)
{
const FBoolProperty* Prop = Container ? CastField<FBoolProperty>(Container->GetClass()->FindPropertyByName(Name)) : nullptr;
return Prop ? (Prop->GetPropertyValue_InContainer(Container) ? TEXT("1") : TEXT("0")) : TEXT("-");
}
// Place d'un siege tiree du nom d'animation de meuble (fnt_xxx-<sexe>-<place>-<desc>) ; vide sinon.
FString SeatPlace(const ACharacter* Person)
{
TArray<FString> Parts;
ReadObjectName(Person, TEXT("currentAnimation")).ParseIntoArray(Parts, TEXT("-"));
return Parts.Num() >= 4 ? Parts[2] : FString();
}
// Une ligne par personnage (classe Person_C, joueur compris), puis les paires trop proches. Delta = sol visible - pieds :
// positif = enfonce. Ecrit au journal (LogLifePlay) et, si bFile, dans Saved/Profiling/LPDebug/PersonsDump_*.txt.
void WritePersonsDump(const TCHAR* Reason, bool bFile)
{
UWorld* World = FindPlayWorld();
TArray<FString> Lines;
Lines.Add(FString::Printf(TEXT("PersonsDump %s %s world=%s"), Reason, *FDateTime::Now().ToString(), World ? *World->GetName() : TEXT("(aucun)")));
if (!World)
{
UE_LOG(LogLifePlay, Display, TEXT("%s"), *Lines[0]);
return;
}
if (const UGameInstance* GI = World->GetGameInstance())
{
const FObjectPropertyBase* RoomProp = CastField<FObjectPropertyBase>(GI->GetClass()->FindPropertyByName(TEXT("Room")));
const AActor* Room = RoomProp ? Cast<AActor>(RoomProp->GetObjectPropertyValue_InContainer(GI)) : nullptr;
if (IsValid(Room) && Room->GetRootComponent())
{
const FVector RoomLoc = Room->GetActorLocation();
const FVector RoomScale = Room->GetRootComponent()->GetRelativeScale3D();
Lines.Add(FString::Printf(TEXT("Room loc=(%.1f,%.1f,%.1f) scale=(%.3f,%.3f,%.3f)"),
RoomLoc.X, RoomLoc.Y, RoomLoc.Z, RoomScale.X, RoomScale.Y, RoomScale.Z));
}
}
TArray<ACharacter*> Persons;
TArray<const AActor*> Ignored;
for (TActorIterator<ACharacter> It(World); It; ++It)
{
if (IsValid(*It) && It->GetClass()->GetName().StartsWith(TEXT("Person_C")))
{
Persons.Add(*It);
Ignored.Add(*It);
}
}
// M1c (STG-SF4b) : trace complexe = triangles du decor (pave), pas son volume de collision simple englobant.
FCollisionQueryParams Query(TEXT("LPPersonsDump"), true);
Query.AddIgnoredActors(Ignored);
Lines.Add(TEXT("actor;hidden;isMoving;occupyingFnt;x;y;z;capsuleBottomZ;feetZ;mode;floorHit;floorComp;floorZ;visibleComp;visibleZ;delta"));
for (const ACharacter* Person : Persons)
{
const FVector Loc = Person->GetActorLocation();
const UCapsuleComponent* Capsule = Person->GetCapsuleComponent();
const double CapsuleBottom = Loc.Z - (Capsule ? Capsule->GetScaledCapsuleHalfHeight() : 0.0);
const double Feet = Person->GetMesh() ? Person->GetMesh()->GetComponentLocation().Z : CapsuleBottom;
const UCharacterMovementComponent* Move = Person->GetCharacterMovement();
const FString Mode = Move ? StaticEnum<EMovementMode>()->GetNameStringByValue(static_cast<int64>(Move->MovementMode.GetValue())) : TEXT("-");
const bool bFloor = Move && Move->CurrentFloor.bBlockingHit;
const UPrimitiveComponent* FloorComp = bFloor ? Move->CurrentFloor.HitResult.GetComponent() : nullptr;
FHitResult Hit;
const bool bVisible = World->LineTraceSingleByChannel(Hit, Loc + FVector(0, 0, 100), Loc - FVector(0, 0, 300), ECC_Visibility, Query);
const FString VisibleComp = bVisible && Hit.GetComponent()
? FString::Printf(TEXT("%s.%s"), *GetNameSafe(Hit.GetActor()), *Hit.GetComponent()->GetName()) : TEXT("-");
Lines.Add(FString::Printf(TEXT("%s;%d;%s;%s;%.1f;%.1f;%.1f;%.1f;%.1f;%s;%d;%s;%.1f;%s;%s;%s"),
*Person->GetName(), Person->IsHidden() ? 1 : 0, *ReadBool(Person, TEXT("isMoving")), *ReadObjectName(Person, TEXT("OccupyingFnt")),
Loc.X, Loc.Y, Loc.Z, CapsuleBottom, Feet, *Mode, bFloor ? 1 : 0,
FloorComp ? *FString::Printf(TEXT("%s.%s"), *GetNameSafe(FloorComp->GetOwner()), *FloorComp->GetName()) : TEXT("-"),
bFloor ? Move->CurrentFloor.HitResult.ImpactPoint.Z : 0.0, *VisibleComp,
bVisible ? *FString::Printf(TEXT("%.1f"), Hit.ImpactPoint.Z) : TEXT("-"),
bVisible ? *FString::Printf(TEXT("%.1f"), Hit.ImpactPoint.Z - Feet) : TEXT("-")));
// M1b (B-DEPL) : etat qui peut bloquer la marche ou enfoncer le corps.
const USkeletalMeshComponent* Mesh = Person->GetMesh();
FString Anim = TEXT("-");
if (Mesh && Mesh->GetAnimationMode() == EAnimationMode::AnimationSingleNode)
{
const UAnimSingleNodeInstance* Single = Mesh->GetSingleNodeInstance();
Anim = FString::Printf(TEXT("single:%s"), *GetNameSafe(Single ? Single->GetAnimationAsset() : nullptr));
}
else if (Mesh && Mesh->GetAnimInstance())
{
Anim = FString::Printf(TEXT("bp:%s"), *Mesh->GetAnimInstance()->GetClass()->GetName());
}
Lines.Add(FString::Printf(TEXT("STATE %s coll=%s grav=%.2f moveTick=%d meshZ=%.1f anim=%s currentAnimation=%s"),
*Person->GetName(),
Capsule ? *StaticEnum<ECollisionEnabled::Type>()->GetNameStringByValue(static_cast<int64>(Capsule->GetCollisionEnabled())) : TEXT("-"),
Move ? Move->GravityScale : 0.f, Move && Move->IsComponentTickEnabled() ? 1 : 0,
Mesh ? Mesh->GetRelativeLocation().Z : 0.0, *Anim, *ReadObjectName(Person, TEXT("currentAnimation"))));
}
{
const UGameInstance* GIState = World->GetGameInstance();
const FStrProperty* SceneProp = GIState ? CastField<FStrProperty>(GIState->GetClass()->FindPropertyByName(TEXT("CurrentScene"))) : nullptr;
const FString Scene = SceneProp ? SceneProp->GetPropertyValue_InContainer(GIState) : TEXT("-");
FString Focus = TEXT("-");
if (FSlateApplication::IsInitialized())
{
const TSharedPtr<SWidget> Focused = FSlateApplication::Get().GetKeyboardFocusedWidget();
Focus = Focused.IsValid() ? Focused->GetTypeAsString() : TEXT("aucun");
}
Lines.Add(FString::Printf(TEXT("GI CurrentScene=[%s] focus=%s"), *Scene, *Focus));
// Ce qui bloque ou nourrit la marche directe (memes tests que LPDirectControlSubsystem::MenuOnScreen et AxisValue).
const ULPKeyboardSubsystem* Keys = GIState ? GIState->GetSubsystem<ULPKeyboardSubsystem>() : nullptr;
static const TCHAR* MenuClasses[] = {
TEXT("Journal_C"), TEXT("MainMenu_C"), TEXT("CityMenu_C"), TEXT("StatMenu_C"),
TEXT("ContactMenu_C"), TEXT("Inventory_C"), TEXT("CharGen_C"), TEXT("ModManager_C"),
TEXT("BodyShapeMenu_C"), TEXT("Scene_C")
};
TArray<FString> Menus;
for (TObjectIterator<UUserWidget> It; It; ++It)
{
const UUserWidget* Widget = *It;
if (!Widget || Widget->HasAnyFlags(RF_ClassDefaultObject) || Widget->GetGameInstance() != GIState || !Widget->IsInViewport())
{
continue;
}
const ESlateVisibility Visibility = Widget->GetVisibility();
if (Visibility == ESlateVisibility::Collapsed || Visibility == ESlateVisibility::Hidden)
{
continue;
}
const FString ClassName = Widget->GetClass()->GetName();
for (const TCHAR* Menu : MenuClasses)
{
if (ClassName == Menu)
{
Menus.Add(ClassName);
}
}
}
const APlayerController* PC = World->GetFirstPlayerController();
const UInputSettings* Settings = UInputSettings::GetInputSettings();
double Forward = 0.0;
double Right = 0.0;
if (PC && PC->PlayerInput && Settings)
{
for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings())
{
const double Down = Map.Key.IsDigital() ? (PC->PlayerInput->IsPressed(Map.Key) ? 1.0 : 0.0) : PC->PlayerInput->GetKeyValue(Map.Key);
if (Map.AxisName == FName(TEXT("MoveForward")))
{
Forward += Down * Map.Scale;
}
else if (Map.AxisName == FName(TEXT("MoveRight")))
{
Right += Down * Map.Scale;
}
}
}
const FObjectPropertyBase* PlayerProp = GIState ? CastField<FObjectPropertyBase>(GIState->GetClass()->FindPropertyByName(TEXT("PlayerCharacter"))) : nullptr;
const ACharacter* Player = PlayerProp ? Cast<ACharacter>(PlayerProp->GetObjectPropertyValue_InContainer(GIState)) : nullptr;
const UCharacterMovementComponent* PlayerMove = Player ? Player->GetCharacterMovement() : nullptr;
const FBoolProperty* SexProp = GIState ? CastField<FBoolProperty>(GIState->GetClass()->FindPropertyByName(TEXT("SexOn"))) : nullptr;
const FBoolProperty* SceneOnProp = GIState ? CastField<FBoolProperty>(GIState->GetClass()->FindPropertyByName(TEXT("SceneOn"))) : nullptr;
Lines.Add(FString::Printf(TEXT("WALK keyMenu=%d menus=[%s] sexOn=%d sceneOn=%d forward=%.2f right=%.2f accel=%.1f speed=%.1f"),
Keys && Keys->IsMenuOpen() ? 1 : 0, *FString::Join(Menus, TEXT(",")),
SexProp ? (SexProp->GetPropertyValue_InContainer(GIState) ? 1 : 0) : -1,
SceneOnProp ? (SceneOnProp->GetPropertyValue_InContainer(GIState) ? 1 : 0) : -1, Forward, Right,
PlayerMove ? PlayerMove->GetCurrentAcceleration().Size() : -1.0, Player ? Player->GetVelocity().Size() : -1.0));
}
// M1d (STG-C54) : en scene de sexe, pour chaque corps sans collision : racine du maillage, os le plus bas, bassin et surface
// sous le bassin (trace complexe, personnages ignores), animation et position dans le cycle.
{
const UGameInstance* CoupleGI = World->GetGameInstance();
const FBoolProperty* CoupleSexOn = CoupleGI ? CastField<FBoolProperty>(CoupleGI->GetClass()->FindPropertyByName(TEXT("SexOn"))) : nullptr;
if (CoupleSexOn && CoupleSexOn->GetPropertyValue_InContainer(CoupleGI))
{
for (const ACharacter* Person : Persons)
{
const UCapsuleComponent* Cap = Person->GetCapsuleComponent();
const USkeletalMeshComponent* Body = Person->GetMesh();
if (Person->IsHidden() || !Cap || !Body || Cap->GetCollisionEnabled() != ECollisionEnabled::NoCollision || Body->GetNumBones() == 0)
{
continue;
}
double Lowest = TNumericLimits<double>::Max();
FName LowestBone;
for (int32 Bone = 0; Bone < Body->GetNumBones(); ++Bone)
{
const FName BoneName = Body->GetBoneName(Bone);
const double BoneZ = Body->GetBoneLocation(BoneName).Z;
if (BoneZ < Lowest)
{
Lowest = BoneZ;
LowestBone = BoneName;
}
}
FName PelvisBone(TEXT("hip"));
if (Body->GetBoneIndex(PelvisBone) == INDEX_NONE)
{
PelvisBone = FName(TEXT("pelvis"));
}
if (Body->GetBoneIndex(PelvisBone) == INDEX_NONE)
{
PelvisBone = Body->GetBoneName(0);
}
const FVector Pelvis = Body->GetBoneLocation(PelvisBone);
FHitResult Under;
const bool bUnder = World->LineTraceSingleByChannel(Under, Pelvis + FVector(0.0, 0.0, 5.0), Pelvis - FVector(0.0, 0.0, 300.0), ECC_Visibility, Query);
// GetLength n'est pas const en UE 5.8 (AnimSingleNodeInstance.h) : pointeur non const.
UAnimSingleNodeInstance* Single = Body->GetSingleNodeInstance();
const UAnimationAsset* Playing = Single ? Single->GetAnimationAsset() : nullptr;
Lines.Add(FString::Printf(TEXT("COUPLE %s anim=%s t=%.2f/%.2f root=%.1f lowest=%.1f(%s) pelvis=%.1f(%s) under=%s:%.1f lowestMinusUnder=%.1f"),
*Person->GetName(), Playing ? *Playing->GetName() : TEXT("-"), Single ? Single->GetCurrentTime() : 0.0, Single ? Single->GetLength() : 0.0,
Body->GetComponentLocation().Z, Lowest, *LowestBone.ToString(), Pelvis.Z, *PelvisBone.ToString(),
bUnder && Under.GetComponent() ? *Under.GetComponent()->GetReadableName() : TEXT("-"), bUnder ? Under.ImpactPoint.Z : 0.0,
bUnder ? Lowest - Under.ImpactPoint.Z : 0.0));
}
}
}
// Paires de personnages visibles : ecart entre capsules (distance 2D - rayons), seuil 10 cm.
double MinGap = TNumericLimits<double>::Max();
int32 Close = 0;
for (int32 i = 0; i < Persons.Num(); ++i)
{
for (int32 j = i + 1; j < Persons.Num(); ++j)
{
const ACharacter* A = Persons[i];
const ACharacter* B = Persons[j];
if (A->IsHidden() || B->IsHidden())
{
continue;
}
// Deux places distinctes d'un meme siege partagent le point d'acteur (decalage dans l'animation) : pas un chevauchement.
const FString FntA = ReadObjectName(A, TEXT("OccupyingFnt"));
if (FntA != TEXT("-") && FntA == ReadObjectName(B, TEXT("OccupyingFnt")) && SeatPlace(A) != SeatPlace(B))
{
continue;
}
const double Radii = (A->GetCapsuleComponent() ? A->GetCapsuleComponent()->GetScaledCapsuleRadius() : 0.0)
+ (B->GetCapsuleComponent() ? B->GetCapsuleComponent()->GetScaledCapsuleRadius() : 0.0);
const double Gap = FVector::Dist2D(A->GetActorLocation(), B->GetActorLocation()) - Radii;
MinGap = FMath::Min(MinGap, Gap);
if (Gap < PersonsMinGapCm)
{
++Close;
Lines.Add(FString::Printf(TEXT("CLOSE %s %s gap=%.1f dz=%.1f"), *A->GetName(), *B->GetName(), Gap,
A->GetActorLocation().Z - B->GetActorLocation().Z));
}
}
}
Lines.Add(FString::Printf(TEXT("Persons=%d Close=%d MinGap=%s"), Persons.Num(), Close,
MinGap < TNumericLimits<double>::Max() ? *FString::Printf(TEXT("%.1f"), MinGap) : TEXT("-")));
for (const FString& Line : Lines)
{
UE_LOG(LogLifePlay, Display, TEXT("LPDebug persons : %s"), *Line);
}
if (bFile)
{
const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
const FString Path = DebugDir() / FString::Printf(TEXT("PersonsDump_%s.txt"), *Stamp);
FFileHelper::SaveStringArrayToFile(Lines, *Path);
UE_LOG(LogLifePlay, Display, TEXT("LPDebug persons -> %s"), *Path);
}
}
void WriteBookmark(const TArray<FString>& Args)
{
const FString Text = Args.Num() > 0 ? FString::Join(Args, TEXT(" ")) : TEXT("manuel");
UE_LOG(LogLifePlay, Display, TEXT("LPDebug bookmark : %s"), *Text);
TRACE_BOOKMARK(TEXT("LP %s"), *Text);
}
}
void ULPDebugSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
if (!MemSnapshotCommand)
{
MemSnapshotCommand = new FAutoConsoleCommand(
TEXT("LifePlay.MemSnapshot"),
TEXT("Ecrit Saved/Profiling/LPDebug/MemSnapshot_*.txt et lance memreport."),
FConsoleCommandDelegate::CreateLambda([]() { WriteMemSnapshot(); }));
CollisionDumpCommand = new FAutoConsoleCommand(
TEXT("LifePlay.CollisionDump"),
TEXT("Ecrit un releve des primitives de collision du monde courant."),
FConsoleCommandDelegate::CreateLambda([]() { WriteCollisionDump(TEXT("console")); }));
BookmarkCommand = new FAutoConsoleCommand(
TEXT("LifePlay.Bookmark"),
TEXT("Signet Insights (TRACE_BOOKMARK) et ligne LogLifePlay. Argument : texte du signet."),
FConsoleCommandWithArgsDelegate::CreateStatic(&WriteBookmark));
HitchSummaryCommand = new FAutoConsoleCommand(
TEXT("LifePlay.HitchSummary"),
TEXT("Ecrit le bilan courant des a-coups (-LPHitchLog)."),
FConsoleCommandDelegate::CreateLambda([]()
{
if (ULPDebugSubsystem* Sub = FindDebugSubsystem())
{
Sub->LogHitchSummary(TEXT("console"));
}
else
{
UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : sous-systeme absent"));
}
}));
PersonsDumpCommand = new FAutoConsoleCommand(
TEXT("LifePlay.PersonsDump"),
TEXT("Ecrit la position des personnages par rapport au sol visible et les paires a moins de 10 cm (lot STG)."),
FConsoleCommandDelegate::CreateLambda([]() { WritePersonsDump(TEXT("console"), true); }));
CommandOwner = this;
}
// -LPPersonsLog[=secondes] : releve periodique au journal, sans console (mesure M1 du lot STG).
if (FParse::Param(FCommandLine::Get(), TEXT("LPPersonsLog")) || FCString::Strifind(FCommandLine::Get(), TEXT("-LPPersonsLog=")))
{
float PersonsSeconds = 5.f;
FParse::Value(FCommandLine::Get(), TEXT("LPPersonsLog="), PersonsSeconds);
PersonsSeconds = FMath::Max(PersonsSeconds, 1.f);
PersonsTickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([](float)
{
WritePersonsDump(TEXT("periodique"), false);
return true;
}), PersonsSeconds);
UE_LOG(LogLifePlay, Display, TEXT("LPDebug persons : releve toutes les %.0f s"), PersonsSeconds);
}
bHitchLog = FParse::Param(FCommandLine::Get(), TEXT("LPHitchLog"));
HitchMs = 50.f;
FParse::Value(FCommandLine::Get(), TEXT("LPHitchMs="), HitchMs);
bCollisionLog = FParse::Param(FCommandLine::Get(), TEXT("LPCollisionLog"));
if (bHitchLog || bCollisionLog)
{
StartSeconds = FPlatformTime::Seconds();
LastFrameSeconds = StartSeconds;
}
if (bHitchLog)
{
UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : actif, seuil %.1f ms"), HitchMs);
}
else
{
UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : inactif"));
}
if (bCollisionLog)
{
const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
CollisionTrackPath = DebugDir() / FString::Printf(TEXT("CollisionTrack_%s.txt"), *Stamp);
FFileHelper::SaveStringToFile(
TEXT("t\tworld\tprim\tpersons\tplayers\tstreet\ttrimesh\ttriangles\n"),
*CollisionTrackPath);
NextCollisionSeconds = FPlatformTime::Seconds() + 2.0;
UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : actif, premier echantillon dans 2 s -> %s"), *CollisionTrackPath);
}
else
{
UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : inactif"));
}
if (bHitchLog || bCollisionLog)
{
FCoreDelegates::OnEndFrame.AddUObject(this, &ULPDebugSubsystem::OnEndFrame);
}
}
void ULPDebugSubsystem::Deinitialize()
{
if (bHitchLog)
{
LogHitchSummary(TEXT("fin"));
}
if (bCollisionLog)
{
LogCollisionSummary(TEXT("fin"));
}
if (bHitchLog || bCollisionLog)
{
FCoreDelegates::OnEndFrame.RemoveAll(this);
}
if (PersonsTickHandle.IsValid())
{
FTSTicker::GetCoreTicker().RemoveTicker(PersonsTickHandle);
PersonsTickHandle.Reset();
}
if (CommandOwner == this)
{
CommandOwner = nullptr;
}
Super::Deinitialize();
}
void ULPDebugSubsystem::OnEndFrame()
{
const double Now = FPlatformTime::Seconds();
if (bCollisionLog && Now >= NextCollisionSeconds)
{
NextCollisionSeconds = Now + 2.0;
SampleCollision();
}
if (!bHitchLog)
{
return;
}
const double DeltaMs = (Now - LastFrameSeconds) * 1000.0;
LastFrameSeconds = Now;
if (DeltaMs >= HitchMs)
{
++HitchCount;
MaxHitchMs = FMath::Max(MaxHitchMs, DeltaMs);
if (HitchLines < 100 || (HitchCount % 25) == 0)
{
++HitchLines;
UE_LOG(LogLifePlay, Warning, TEXT("LPDebug hitch : %.1f ms (seuil %.1f) #%d"), DeltaMs, HitchMs, HitchCount);
TRACE_BOOKMARK(TEXT("LP Hitch %.1f ms"), DeltaMs);
}
}
if (!bLoggedTwoMinuteSummary && (Now - StartSeconds) >= 120.0)
{
bLoggedTwoMinuteSummary = true;
LogHitchSummary(TEXT("2 min"));
}
}
void ULPDebugSubsystem::LogHitchSummary(const TCHAR* When) const
{
const double Elapsed = StartSeconds > 0.0 ? FPlatformTime::Seconds() - StartSeconds : 0.0;
UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : %d hitch(s) en %.1f s (seuil %.1f ms), max %.1f ms, %s"),
HitchCount, Elapsed, HitchMs, MaxHitchMs, When);
}
void ULPDebugSubsystem::SampleCollision()
{
const FCollisionTally Tally = GatherCollision();
++CollisionSamples;
PeakPrim = FMath::Max(PeakPrim, Tally.Prim);
PeakTriangles = FMath::Max(PeakTriangles, Tally.Triangles);
const double Elapsed = StartSeconds > 0.0 ? FPlatformTime::Seconds() - StartSeconds : FPlatformTime::Seconds();
const FString Line = FString::Printf(
TEXT("%.1f\t%s\t%d\t%d\t%d\t%d\t%d\t%lld\n"),
Elapsed, *Tally.WorldName, Tally.Prim, Tally.Persons, Tally.Players, Tally.Street,
Tally.UniqueTriMeshes, Tally.Triangles);
FFileHelper::SaveStringToFile(Line, *CollisionTrackPath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), FILEWRITE_Append);
UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %.0f s %s prim=%d pnj=%d trimesh=%d triangles=%lld"),
Elapsed, *Tally.WorldName, Tally.Prim, Tally.Persons, Tally.UniqueTriMeshes, Tally.Triangles);
const bool bGrew = Tally.Triangles >= LastDumpedTriangles + 1000 || Tally.UniqueTriMeshes > LastDumpedTriMeshes;
if (CollisionDumps < 4 && Tally.Triangles > 0 && bGrew)
{
WriteCollisionDump(TEXT("suivi"), &Tally);
++CollisionDumps;
LastDumpedTriangles = Tally.Triangles;
LastDumpedTriMeshes = Tally.UniqueTriMeshes;
}
}
void ULPDebugSubsystem::LogCollisionSummary(const TCHAR* When) const
{
if (CollisionDumps == 0 && PeakPrim > 0)
{
WriteCollisionDump(TEXT("fin"));
}
UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %d echantillon(s), %d dump(s), max %d primitives, %lld triangles, %s"),
CollisionSamples, CollisionDumps, PeakPrim, PeakTriangles, When);
}
#else
void ULPDebugSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
}
void ULPDebugSubsystem::Deinitialize()
{
Super::Deinitialize();
}
void ULPDebugSubsystem::OnEndFrame()
{
}
void ULPDebugSubsystem::LogHitchSummary(const TCHAR* When) const
{
(void)When;
}
void ULPDebugSubsystem::SampleCollision()
{
}
void ULPDebugSubsystem::LogCollisionSummary(const TCHAR* When) const
{
(void)When;
}
#endif