LifePlay-Revival/Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.cpp
edujiro5972 04b053554f Sandbox addon : brancher le jeu sur LifePlay-Sandbox (plugin LPSandbox).
Charge le plugin via AdditionalPluginDirectories, conserve ShowCouple et les commandlets anim côté jeu, et documente les lots S0–S8 (CHG-212 à CHG-220).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 21:10:53 +02:00

519 lines
19 KiB
C++

// Portage UE 5.5 - voir LPPairCollisionTestCommandlet.h
#include "LPPairCollisionTestCommandlet.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Components/CapsuleComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "Engine/World.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "PhysicsEngine/BodySetup.h"
#include "PreviewScene.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPPairCollision, Log, All);
namespace
{
struct FProp
{
FString Name;
FVector Location = FVector::ZeroVector;
FRotator Rotation = FRotator::ZeroRotator;
FVector Scale = FVector::OneVector;
};
struct FProbe
{
bool bBlocked = false;
bool bStartInside = false;
FString Hit;
float Moved = 0.f;
};
float Token(const FString& Part, const TCHAR* Key)
{
const FString Needle = FString::Printf(TEXT("%s="), Key);
const int32 At = Part.Find(Needle);
return At == INDEX_NONE ? 0.f : FCString::Atof(*Part + At + Needle.Len());
}
bool LoadWorldFile(const FString& Path, FVector& RoomScale, TArray<FProp>& Props)
{
TArray<FString> Lines;
if (!FFileHelper::LoadFileToStringArray(Lines, *Path))
{
return false;
}
bool bWantName = false;
int32 Slash = 0;
FString Name;
for (FString Line : Lines)
{
Line.TrimStartAndEndInline();
if (Line.IsEmpty())
{
continue;
}
if (Line == TEXT("*****"))
{
bWantName = true;
Slash = 0;
Name.Reset();
continue;
}
if (bWantName)
{
Name = Line;
bWantName = false;
continue;
}
if (Line == TEXT("///"))
{
++Slash;
continue;
}
if (Name.IsEmpty() && Line.StartsWith(TEXT("X=")))
{
RoomScale = FVector(Token(Line, TEXT("X")), Token(Line, TEXT("Y")), Token(Line, TEXT("Z")));
continue;
}
if (Slash < 2 || !Line.StartsWith(TEXT("X=")))
{
continue;
}
TArray<FString> Parts;
Line.ParseIntoArray(Parts, TEXT(";"), true);
if (Parts.Num() < 3)
{
continue;
}
FProp Prop;
Prop.Name = Name;
Prop.Location = FVector(Token(Parts[0], TEXT("X")), Token(Parts[0], TEXT("Y")), Token(Parts[0], TEXT("Z")));
Prop.Rotation = FRotator(Token(Parts[1], TEXT("P")), Token(Parts[1], TEXT("Y")), Token(Parts[1], TEXT("R")));
Prop.Scale = FVector(Token(Parts[2], TEXT("X")), Token(Parts[2], TEXT("Y")), Token(Parts[2], TEXT("Z")));
Props.Add(Prop);
Slash = 0;
}
return Props.Num() > 0 && !RoomScale.IsNearlyZero();
}
int32 BodyElems(UStaticMeshComponent* Comp)
{
const UBodySetup* Body = Comp ? Comp->GetBodySetup() : nullptr;
return Body ? Body->AggGeom.GetElementCount() : 0;
}
void SetPawnResponse(const TArray<UStaticMeshComponent*>& Meshes, const ECollisionResponse Response)
{
for (UStaticMeshComponent* Mesh : Meshes)
{
Mesh->SetCollisionResponseToChannel(ECC_Pawn, Response);
}
}
void ApplyBodies(ACharacter* A, ACharacter* B, const bool bOn, const bool bIgnoreEachOther)
{
auto Apply = [bOn](ACharacter* Who)
{
UCapsuleComponent* Cap = Who->GetCapsuleComponent();
Cap->SetCollisionEnabled(bOn ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision);
if (UCharacterMovementComponent* Move = Who->GetCharacterMovement())
{
Move->GravityScale = bOn ? 1.f : 0.f;
}
Cap->ClearMoveIgnoreActors();
};
Apply(A);
Apply(B);
if (bIgnoreEachOther)
{
A->GetCapsuleComponent()->IgnoreActorWhenMoving(B, true);
B->GetCapsuleComponent()->IgnoreActorWhenMoving(A, true);
}
A->GetCapsuleComponent()->RecreatePhysicsState();
B->GetCapsuleComponent()->RecreatePhysicsState();
}
FProbe Probe(ACharacter* Who, const FVector& From, const FVector& Delta)
{
Who->SetActorLocation(From, false, nullptr, ETeleportType::TeleportPhysics);
UCapsuleComponent* Cap = Who->GetCapsuleComponent();
const FVector Start = Cap->GetComponentLocation();
FHitResult Hit;
Cap->MoveComponent(Delta, Cap->GetComponentQuat(), true, &Hit);
FProbe Result;
Result.Moved = FVector::Dist(Start, Cap->GetComponentLocation());
Result.bStartInside = Hit.bStartPenetrating;
Result.bBlocked = Result.Moved < Delta.Size() - 2.f;
if (Hit.bBlockingHit || Hit.bStartPenetrating)
{
Result.Hit = GetNameSafe(Hit.GetActor());
if (const UPrimitiveComponent* Comp = Hit.GetComponent())
{
const UStaticMeshComponent* Mesh = Cast<UStaticMeshComponent>(Comp);
const FString MeshName = (Mesh && Mesh->GetStaticMesh())
? Mesh->GetStaticMesh()->GetName()
: Comp->GetName();
Result.Hit += TEXT("/") + MeshName;
}
}
Who->SetActorLocation(From, false, nullptr, ETeleportType::TeleportPhysics);
return Result;
}
bool FindPairSpot(UWorld* World, float Radius, float HalfHeight, FVector& OutA, FVector& OutB)
{
const FCollisionShape Shape = FCollisionShape::MakeCapsule(Radius, HalfHeight);
FCollisionQueryParams Query(SCENE_QUERY_STAT(LPPairSpot), false);
const float Gap = Radius * 2.f + 40.f;
const float Z = HalfHeight + 20.f;
for (float Y = -700.f; Y <= 700.f; Y += 80.f)
{
for (float X = -700.f; X <= 700.f; X += 80.f)
{
const FVector A(X, Y, Z);
const FVector B(X + Gap, Y, Z);
const bool bA = World->OverlapBlockingTestByChannel(A, FQuat::Identity, ECC_WorldStatic, Shape, Query);
const bool bB = World->OverlapBlockingTestByChannel(B, FQuat::Identity, ECC_WorldStatic, Shape, Query);
if (!bA && !bB)
{
OutA = A;
OutB = B;
return true;
}
}
}
return false;
}
}
ULPPairCollisionTestCommandlet::ULPPairCollisionTestCommandlet()
{
IsClient = false;
IsServer = false;
IsEditor = true;
LogToConsole = true;
}
int32 ULPPairCollisionTestCommandlet::Main(const FString& Params)
{
TArray<FString> Tokens, Switches;
TMap<FString, FString> ParamsMap;
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
const FString WorldFile = ParamsMap.FindRef(TEXT("World")).IsEmpty()
? FPaths::Combine(FPaths::ProjectDir(), TEXT("Content/Modules/vin_Base/World/work___vin01.lpworld"))
: ParamsMap.FindRef(TEXT("World"));
const FString OutPath = ParamsMap.FindRef(TEXT("Out")).IsEmpty()
? FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Logs/portage/LPPairCollisionTest.txt"))
: ParamsMap.FindRef(TEXT("Out"));
FVector RoomScale = FVector::OneVector;
TArray<FProp> Props;
if (!LoadWorldFile(WorldFile, RoomScale, Props))
{
UE_LOG(LogLPPairCollision, Error, TEXT("Chambre illisible : %s"), *WorldFile);
return 1;
}
FAssetRegistryModule& Assets = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
Assets.Get().SearchAllAssets(true);
TArray<FAssetData> MeshAssets;
Assets.Get().GetAssetsByClass(UStaticMesh::StaticClass()->GetClassPathName(), MeshAssets, true);
TMap<FString, FString> MeshPath;
for (const FAssetData& Data : MeshAssets)
{
const FString Name = Data.AssetName.ToString();
const FString Path = FString::Printf(TEXT("%s.%s"), *Data.PackageName.ToString(), *Name);
const bool bFurniture = Path.Contains(TEXT("/Furniture/")) || Path.Contains(TEXT("/Heavy/"));
if (!MeshPath.Contains(Name) || bFurniture)
{
MeshPath.Add(Name, Path);
}
}
if (!GEngine)
{
UE_LOG(LogLPPairCollision, Error, TEXT("Pas de moteur, scene d'essai impossible."));
return 1;
}
FPreviewScene::ConstructionValues SceneValues;
SceneValues.SetCreateDefaultLighting(false).SetEditor(false).SetTransactional(false).SetForceMipsResident(false);
FPreviewScene Scene(SceneValues);
UWorld* World = Scene.GetWorld();
FActorSpawnParameters Spawn;
Spawn.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
Spawn.ObjectFlags |= RF_Transient;
AActor* Room = World->SpawnActor<AActor>(AActor::StaticClass(), FTransform::Identity, Spawn);
USceneComponent* Root = NewObject<USceneComponent>(Room, TEXT("RoomRoot"));
Room->SetRootComponent(Root);
Root->RegisterComponent();
Root->SetWorldScale3D(RoomScale);
TArray<UStaticMeshComponent*> Furniture;
UStaticMeshComponent* Wall = nullptr;
double WallArea = 0.0;
int32 Missing = 0;
int32 WithoutBody = 0;
for (const FProp& Prop : Props)
{
const FString* Path = MeshPath.Find(Prop.Name);
UStaticMesh* Mesh = Path ? LoadObject<UStaticMesh>(nullptr, **Path) : nullptr;
if (!Mesh)
{
++Missing;
UE_LOG(LogLPPairCollision, Warning, TEXT("Maillage absent : %s"), *Prop.Name);
continue;
}
UStaticMeshComponent* Comp = NewObject<UStaticMeshComponent>(Room);
Comp->SetStaticMesh(Mesh);
Comp->SetupAttachment(Root);
Comp->SetRelativeLocation(Prop.Location);
Comp->SetRelativeRotation(Prop.Rotation);
Comp->SetRelativeScale3D(Prop.Scale);
Comp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Comp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
Comp->RegisterComponent();
Furniture.Add(Comp);
if (BodyElems(Comp) == 0)
{
++WithoutBody;
}
const FVector Extent = Comp->Bounds.GetBox().GetExtent();
const double Area = double(Extent.X) * double(Extent.Y);
if ((Prop.Name.StartsWith(TEXT("fnt_new14")) || Prop.Name.StartsWith(TEXT("fnt_new18"))) && Area > WallArea)
{
Wall = Comp;
WallArea = Area;
}
}
if (!Wall && Furniture.Num() > 0)
{
Wall = Furniture[0];
}
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
UStaticMeshComponent* Cube = nullptr;
if (CubeMesh)
{
AActor* CubeActor = World->SpawnActor<AActor>(AActor::StaticClass(), FTransform::Identity, Spawn);
Cube = NewObject<UStaticMeshComponent>(CubeActor, TEXT("Cube"));
CubeActor->SetRootComponent(Cube);
Cube->SetStaticMesh(CubeMesh);
Cube->SetRelativeLocation(FVector(3000.f, 0.f, 100.f));
Cube->SetRelativeScale3D(FVector(1.5f));
Cube->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Cube->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
Cube->RegisterComponent();
Furniture.Add(Cube);
}
UClass* PersonClass = LoadObject<UClass>(nullptr, TEXT("/Game/BPs/Person.Person_C"));
const ACharacter* PersonDefaults = PersonClass ? Cast<ACharacter>(PersonClass->GetDefaultObject()) : nullptr;
const UCapsuleComponent* PersonCap = PersonDefaults ? PersonDefaults->GetCapsuleComponent() : nullptr;
auto MakeBody = [&](const FVector& At) -> ACharacter*
{
ACharacter* Who = World->SpawnActor<ACharacter>(ACharacter::StaticClass(), At, FRotator::ZeroRotator, Spawn);
if (!Who)
{
return nullptr;
}
UCapsuleComponent* Cap = Who->GetCapsuleComponent();
if (PersonCap)
{
Cap->SetCapsuleSize(PersonCap->GetUnscaledCapsuleRadius(), PersonCap->GetUnscaledCapsuleHalfHeight());
Cap->SetCollisionProfileName(PersonCap->GetCollisionProfileName());
Cap->SetCollisionObjectType(PersonCap->GetCollisionObjectType());
Cap->SetCollisionResponseToChannels(PersonCap->GetCollisionResponseToChannels());
}
Cap->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Cap->RecreatePhysicsState();
return Who;
};
auto ResponseText = [](const UCapsuleComponent* Cap, const ECollisionChannel Channel) -> const TCHAR*
{
switch (Cap->GetCollisionResponseToChannel(Channel))
{
case ECR_Ignore: return TEXT("Ignore");
case ECR_Overlap: return TEXT("Overlap");
case ECR_Block: return TEXT("Block");
default: return TEXT("?");
}
};
const FVector Park(0.f, 0.f, 8000.f);
ACharacter* A = MakeBody(Park);
ACharacter* B = MakeBody(Park + FVector(200.f, 0.f, 0.f));
if (!A || !B || !Cube || !Wall)
{
UE_LOG(LogLPPairCollision, Error, TEXT("Scene incomplete (personnes %d, cube %d, mur %d, meubles %d)."),
(A ? 1 : 0) + (B ? 1 : 0), Cube ? 1 : 0, Wall ? 1 : 0, Furniture.Num());
return 1;
}
const float Radius = A->GetCapsuleComponent()->GetScaledCapsuleRadius();
const float Half = A->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
FVector SpotA, SpotB;
const bool bInside = FindPairSpot(World, Radius, Half, SpotA, SpotB);
if (!bInside)
{
SpotA = FVector(0.f, 0.f, 4000.f);
SpotB = SpotA + FVector(Radius * 2.f + 40.f, 0.f, 0.f);
}
const FBox WallBox = Wall->Bounds.GetBox();
const FVector WallCenter = WallBox.GetCenter();
FVector WallStart(WallBox.Max.X + Radius + 30.f, WallCenter.Y, FMath::Clamp(Half + 20.f, WallBox.Min.Z, WallBox.Max.Z));
const FVector WallDelta = FVector(WallCenter.X, WallCenter.Y, WallStart.Z) - WallStart;
const FBox CubeBox = Cube->Bounds.GetBox();
const FVector CubeStart(CubeBox.Max.X + Radius + 30.f, CubeBox.GetCenter().Y, CubeBox.GetCenter().Z);
const FVector CubeDelta = FVector(CubeBox.GetCenter().X, CubeStart.Y, CubeStart.Z) - CubeStart;
const FVector TowardB = (SpotB - SpotA).GetSafeNormal() * 80.f;
struct FCase
{
const TCHAR* Name;
bool bBlockPawn;
bool bCapsulesOn;
bool bIgnore;
bool bExpectOther;
bool bExpectSolid;
};
const FCase Cases[] = {
{ TEXT("Bureau actuel, capsules allumees"), false, true, false, true, false },
{ TEXT("Scene actuelle, capsules coupees"), false, false, false, false, false },
{ TEXT("Meubles bloquent, capsules encore coupees"), true, false, false, false, false },
{ TEXT("Principes : capsules allumees, ignore mutuel, meubles bloquent"), true, true, true, false, true },
{ TEXT("Meubles bloquent, sans ignorer l'autre"), true, true, false, true, true },
};
TArray<FString> Report;
Report.Add(FString::Printf(TEXT("Chambre : %s"), *WorldFile));
Report.Add(FString::Printf(TEXT("Echelle racine : %s"), *RoomScale.ToString()));
Report.Add(FString::Printf(TEXT("Meubles poses : %d, absents : %d, sans volume simple : %d"), Furniture.Num() - (Cube ? 1 : 0), Missing, WithoutBody));
if (Furniture.Num() > (Cube ? 1 : 0))
{
UStaticMeshComponent* Sample = Furniture[0];
Report.Add(FString::Printf(TEXT("Exemple %s : position monde %s, echelle monde %s, volumes %d"),
*Sample->GetStaticMesh()->GetName(), *Sample->GetComponentLocation().ToString(),
*Sample->GetComponentScale().ToString(), BodyElems(Sample)));
}
Report.Add(FString::Printf(TEXT("Mur d'essai : %s, boite %s"),
Wall->GetStaticMesh() ? *Wall->GetStaticMesh()->GetName() : TEXT("?"), *WallBox.ToString()));
Report.Add(FString::Printf(TEXT("Personnes : capsules moteur, rayon %.1f, demi-hauteur %.1f, profil %s, type %d, corps physique %s. CDO Person : %s"),
Radius, Half, *A->GetCapsuleComponent()->GetCollisionProfileName().ToString(),
int32(A->GetCapsuleComponent()->GetCollisionObjectType()),
A->GetCapsuleComponent()->IsPhysicsStateCreated() ? TEXT("oui") : TEXT("non"),
PersonCap ? TEXT("lu") : TEXT("introuvable, valeurs moteur")));
Report.Add(FString::Printf(TEXT("Reponses de la capsule Person : Pawn=%s WorldStatic=%s WorldDynamic=%s"),
ResponseText(A->GetCapsuleComponent(), ECC_Pawn),
ResponseText(A->GetCapsuleComponent(), ECC_WorldStatic),
ResponseText(A->GetCapsuleComponent(), ECC_WorldDynamic)));
const bool bPersonBlocksPawn = A->GetCapsuleComponent()->GetCollisionResponseToChannel(ECC_Pawn) == ECR_Block;
Report.Add(bPersonBlocksPawn
? TEXT("La capsule Person bloque le canal Pawn.")
: TEXT("La capsule Person ne bloque pas le canal Pawn : deux Person se traversent deja."));
Report.Add(FString::Printf(TEXT("Emplacement du couple : %s -> %s (%s)"),
*SpotA.ToString(), *SpotB.ToString(), bInside ? TEXT("dans la chambre") : TEXT("au-dessus, chambre trop occupee")));
Report.Add(TEXT("Deplacement = balayage de la capsule (le meme que la marche). L'animation n'est pas jouee."));
Report.Add(TEXT(""));
auto ForcePawnProfile = [](ACharacter* Who)
{
UCapsuleComponent* Cap = Who->GetCapsuleComponent();
Cap->SetCollisionProfileName(TEXT("Pawn"));
Cap->SetCollisionObjectType(ECC_Pawn);
Cap->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Cap->RecreatePhysicsState();
};
bool bPrinciples = true;
bool bRoom = true;
bool bForced = true;
auto RunCases = [&](const TCHAR* Series, const bool bForcePawn)
{
if (bForcePawn)
{
ForcePawnProfile(A);
ForcePawnProfile(B);
Report.Add(TEXT(""));
Report.Add(TEXT("--- Profil Pawn du moteur (les deux capsules se bloquent, pour isoler IgnoreActorWhenMoving) ---"));
}
for (const FCase& Case : Cases)
{
SetPawnResponse(Furniture, Case.bBlockPawn ? ECR_Block : ECR_Ignore);
ApplyBodies(A, B, Case.bCapsulesOn, Case.bIgnore);
if (bForcePawn && Case.bCapsulesOn)
{
ForcePawnProfile(A);
ForcePawnProfile(B);
if (Case.bIgnore)
{
A->GetCapsuleComponent()->IgnoreActorWhenMoving(B, true);
B->GetCapsuleComponent()->IgnoreActorWhenMoving(A, true);
}
}
B->SetActorLocation(SpotB, false, nullptr, ETeleportType::TeleportPhysics);
const FProbe Other = Probe(A, SpotA, TowardB);
B->SetActorLocation(Park, false, nullptr, ETeleportType::TeleportPhysics);
const FProbe Solid = Probe(A, CubeStart, CubeDelta);
const FProbe RoomHit = Probe(A, WallStart, WallDelta);
const bool bExpectOther = (bForcePawn || bPersonBlocksPawn) ? Case.bExpectOther : false;
const bool bOtherOk = Other.bBlocked == bExpectOther && !Other.bStartInside;
const bool bWallOk = RoomHit.bBlocked == Case.bExpectSolid && !RoomHit.bStartInside;
if (!bForcePawn)
{
bPrinciples &= bOtherOk;
bRoom &= bWallOk;
}
else
{
bForced &= bOtherOk && bWallOk;
}
const FString Line = FString::Printf(
TEXT("%s | %s | autre:%s (attendu %s, %.0f cm, %s) | cube:%s (attendu %s, %.0f cm, %s) | meuble:%s (attendu %s, %.0f cm, %s) | %s"),
Series, Case.Name,
Other.bBlocked ? TEXT("bloque") : TEXT("passe"), bExpectOther ? TEXT("bloque") : TEXT("passe"), Other.Moved, *Other.Hit,
Solid.bBlocked ? TEXT("bloque") : TEXT("passe"), Case.bExpectSolid ? TEXT("bloque") : TEXT("passe"), Solid.Moved, *Solid.Hit,
RoomHit.bBlocked ? TEXT("bloque") : TEXT("passe"), Case.bExpectSolid ? TEXT("bloque") : TEXT("passe"), RoomHit.Moved, *RoomHit.Hit,
(bOtherOk && bWallOk) ? TEXT("OK") : TEXT("ECART"));
Report.Add(Line);
UE_LOG(LogLPPairCollision, Warning, TEXT("%s"), *Line);
}
};
RunCases(TEXT("Person"), false);
RunCases(TEXT("Pawn"), true);
Report.Add(TEXT(""));
Report.Add(FString::Printf(TEXT("Profil Person : %s"), bPrinciples ? TEXT("TIENT") : TEXT("NE TIENT PAS")));
Report.Add(FString::Printf(TEXT("Profil Pawn force, ignore mutuel : %s"), bForced ? TEXT("TIENT") : TEXT("NE TIENT PAS")));
Report.Add(FString::Printf(TEXT("Meubles du bureau : %s"), bRoom ? TEXT("TIENT") : TEXT("NE TIENT PAS")));
Report.Add(TEXT("Le cube du moteur est mesure a part. Le verdict porte sur les meubles de la chambre et sur les deux capsules."));
UE_LOG(LogLPPairCollision, Warning, TEXT("Person : %s. Pawn force : %s. Meubles : %s."),
bPrinciples ? TEXT("TIENT") : TEXT("NE TIENT PAS"),
bForced ? TEXT("TIENT") : TEXT("NE TIENT PAS"),
bRoom ? TEXT("TIENT") : TEXT("NE TIENT PAS"));
FFileHelper::SaveStringArrayToFile(Report, *OutPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
UE_LOG(LogLPPairCollision, Warning, TEXT("Rapport : %s"), *OutPath);
return (bPrinciples && bRoom && bForced) ? 0 : 2;
}