LifePlay-Revival/Docs/Migration/etat/sandbox/S5/S5-CART.md
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

28 KiB

S5-T1 : comparaison CoupleSim (sandbox) / LPShowCoupleSubsystem (jeu)

  • Fiche : S5-T1 | Role : A-CART | Modele prevu : claude-sonnet-5-5-high (M2)
  • Mode : lecture seule. Aucun fichier de code modifie, seul ce document est cree.
  • Amont : S4/S4-CART.md (decoupage CoupleSim), 21_Sandbox_Addon.md (S5 : "LPShowCoupleSubsystem reste dans LifePlay ; aligner le contrat avec l'addon").

0. Sources lues

Cote Fichier Lignes
Sandbox D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\Source\LPSandbox\LPCoupleSim.cpp 477
Sandbox (membres, constantes) LPPairSandbox.h (l. 100-129, 196-203), LPSandboxRoomBuilder.cpp (l. 117-226), LPSandboxInteraction.cpp (l. 61-101)
Sandbox (cible API) Plugins\LPSandbox\Source\LPAnimMeasure\ (LPAnimMeasure.h = seulement LogLPAnimMeasure), LPAnimMeasure.Build.cs
Jeu Source/LifePlay/LPShowCoupleSubsystem.h (77) et .cpp (1048)

Remarque : LPCoupleSim n'a pas de header propre dans la sandbox. Les methodes sont celles de ALPPairSandboxStage / ALPPairSandboxCharacter declarees dans LPPairSandbox.h. LPAnimMeasure ne depend que de Core, CoreUObject, Engine (+ Json en prive) et ne doit jamais dependre de LifePlay ni de LPSandbox. LifePlay.Build.cs depend deja publiquement de LPAnimMeasure (l. 46), ce qui rend un header partage utilisable des deux cotes sans changer les .Build.cs.

1. Ce qui est identique (a ne pas dupliquer plus longtemps)

Element Sandbox Jeu
Transformation mesh vers acteur FTransform(FRotator(0,-90,0), FVector(0,0,-CapsuleHalf)) (l. 195) idem (l. 886, MeshToActor)
Echantillonnage de pose lambda Accumulate (l. 197-249) AddPose (l. 270-320)
Details de l'echantillonnage 8 pas a Length*Step/8, Length = max(PlayLength, 0.01), os animes via GetCompressedTrackToSkeletonMapTable, GetBoneTransform(..., false), rejet NaN, tous les os du squelette de reference identique, caractere par caractere hors le 0.01f / 0.01
Padding anime FVector(12, 12, 6) FVector(12, 12, 6)
Plafond de taille mesuree GetSize().GetMax() > 800 : rejet > 800.0 : rejet
Recalage bas de boite Extent.Z = max(20, Extent.Z - Cut*0.5), Center.Z += Cut*0.5 identique (formule), seul le seuil change (voir 2.2)
Composant boite UBoxComponent outer = Lead, attache a la racine du Lead, objet ECC_Pawn, toutes reponses Ignore, WorldDynamic = Block, cache, sans navigation identique
Soudure de base AttachToActor(Lead, SnapToTargetNotIncludingScale), relatif location/rotation = zero identique
Mesh des deux corps relatif (0,0,-Half), rotation (0,-90,0) identique
Canal des balayages ECC_Pawn ECC_Pawn

2. Tableau des ecarts

2.1 Constantes

# Constante Sandbox Jeu Ecart / risque
C1 Demi-hauteur de capsule CapsuleHalf du CDO Person (RoomBuilder l. 124-125), 88 par defaut du membre (LPPairSandbox.h l. 203), reecrite sur le corps a chaque FitCoupleBox capsule du Lead, GetUnscaledCapsuleHalfHeight() si > 1, sinon 88.f (l. 762, 884) Meme valeur nominale, source differente. A passer en parametre unique.
C2 Rayon de capsule CapsuleRadius, 20 par defaut ; reecrit sur les deux corps non utilise (la capsule du jeu n'est pas redimensionnee) Sandbox seule force la taille.
C3 Vitesse du couple 250.f * dt dans MoveCouple (l. 142) aucune (le Blueprint deplace le Lead) Specifique sandbox, hors API.
C4 Plancher de boite 18 en Z monde : Bottom = PlayerSpot.Z + Center.Z - Extent.Z, clamp si < 18.f (l. 259-260) Bottom = Center.Z - Extent.Z en local, clamp si < -Half (l. 898-899) Sandbox : fond a 18 - PlayerSpot.Z = -90 local (PlayerSpot.Z = Half + 20 = 108), soit 2 cm sous la plante. Jeu : fond a -Half = -88 (pile a la plante). Ecart de 2 cm.
C5 Marge pas de 2 pour le sol Z = CapsuleHalf + 20 (RoomBuilder l. 204) : le sol sandbox est a z = +10 inconnue (le jeu n'a pas ce sol) Explique le 18 de C4 ; pas de contrepartie jeu.
C6 Distance max de recherche du partenaire aucune (joueur + partenaire fixes) ShowReach = 400.0 (l. 35) Specifique jeu (selection du couple).
C7 Rayon de recherche du mobilier de la chambre non applicable 4000.0 (l. 632) Specifique jeu.
C8 Nombre d'iterations de balayage 2 (Step < 2, l. 157) 3 (Step < 3, l. 977) Ecart.
C9 Seuil "marchable" Hit.Normal.Z > 0.6f : le pas est accepte (l. 163) Normal.Z > 0.6f pour les lits uniquement (l. 989, 993) ; > 0.6f aussi dans le test de normale horizontale (l. 1016, abs(Z) <= 0.6f) Meme 0.6 mais sens different.
C10 Profondeur de penetration ignoree Hit.bStartPenetrating : mouvement stoppe PenetrationDepth < 1.f ignoree (l. 1002) Ecart.
C11 Seuil de mouvement Wish.IsNearlyZero() Delta.IsNearlyZero(), Base.Equals(Now, 0.1f) Meme esprit, tolerances differentes.
C12 Ancre de pelvis du corps vivant non applicable (pas de corps vivant) 140 cm autour de pelvis / hip (l. 832) Specifique jeu.
C13 Nom du composant CoupleBox (l. 268) LPShowCoupleBox (l. 853, 908) Cosmetique ; utile pour les captures, a unifier en une constante.

2.2 Boite et extents

# Sujet Sandbox Jeu Ecart / risque
E1 Source de mesure uniquement les animations : <Pair>_F et <Pair>_M sous DazAnimRoot, chemin "%s/%s%s.%s%s" (l. 199) deux chemins : (a) FitToBodies (os vivants des deux meshes, chaque tick tant qu'il reussit, l. 776-873) ; (b) FitBox sur les animations seulement si (a) echoue et si la cle de paire n'a pas deja ete essayee (l. 429-438) Le jeu mesure d'abord les corps reels, la sandbox ne le peut pas. Le comportement jeu est donc plus fin, la sandbox prevoit le cas (b).
E2 Noms de clips de la paire _F et _M fixes PairSibling : _F/_M et _0/_1 ; retrait des clips idle ; completion de paire depuis le dossier Sandbox ne gere pas _0/_1.
E3 Padding anime (12, 12, 6) (12, 12, 6) Identique.
E4 Padding corps vivants non applicable (4, 4, 4), puis max(.., 8) sur X/Y/Z (l. 846-849) Specifique jeu.
E5 Plafond corps vivants non applicable GetSize().GetMax() > 400.0 : rejet (l. 841) ; la mesure anime tombe a 800 Deux plafonds dans le jeu.
E6 Os exclus (corps vivants) non applicable hand, finger, foot, toe, twist, breast, hair, eye, jaw, weapon, ik (l. 822-829) La sandbox mesure TOUS les os en anime (mains, pieds, ik compris) ; le jeu les exclut en vivant mais pas en anime. La meme pose peut donner deux boites differentes.
E7 Recalage bas clamp dans le monde a 18 (voir C4), toujours applique clamp a -Half, applique seulement dans FitBox (pas dans FitToBodies) La boite vivante du jeu peut depasser sous la plante.
E8 Recalcul a chaque Apply() (toggles, PlayPair) vivant : a chaque tick ; anime : une fois par Key de paire ; FittedPair / RejectedPair sont remis a zero dans Release Frequence differente ; pas un probleme de parite tant que le resultat est a peu pres stable.
E9 Rotation SetRelativeRotation(Zero) idem Identique.
E10 Etat de collision du composant a la creation defaut du composant, puis bCapsulesOn ? QueryAndPhysics : NoCollision QueryAndPhysics a la creation et a chaque ajustement Le jeu n'a pas de bascule "boite coupee".
E11 Journal Sandbox boite %s : centre %s, etendue %s (l. 298) Show couple : boite ... (l. 923) Les deux formats sont lisibles, mais pas homogenes pour un diff automatique.
E12 Debug draw DrawDebugBox Vert / Rouge par bCapsulesOn (LPPairSandbox.cpp l. 565-567) DrawDebugBox vert tous les ticks (l. 440-443) Comparable.

2.3 Balayage (sweep) du couple

# Sujet Sandbox (MoveCouple, l. 130-186) Jeu (KeepTogether, l. 927-1048)
S1 Qui bouge la sandbox deplace elle-meme le Lead (AddActorWorldOffset) le Blueprint a deja deplace le Lead ; le sous-systeme reconstruit un point sur sans collision (LastSafe) et teleporte
S2 Declencheur clavier, via MoveCouple OnWorldPostActorTick (post tick acteurs), chaque frame
S3 Demarrage Box->GetComponentLocation() BoxAt(LastSafe) : transforme le decalage relatif de la boite par la rotation courante du Lead
S4 Fonction SweepSingleByChannel SweepMultiByChannel
S5 Forme MakeBox(Box->GetScaledBoxExtent()) idem
S6 Rotation Box->GetComponentQuat() idem pour le balayage ; Who->GetActorQuat() pour le point de depart
S7 Ignores Lead, Partner Lead, Partner, tous les ACharacter, tous les acteurs nommes Wall / Wall_* (coque, l. 955-975)
S8 Iterations 2 3
S9 Marches Normal.Z > 0.6 : pas complet accepte pas de marche generique ; seuls les lits (voir 2.4) sont franchissables
S10 Depart en penetration arret net (bStartPenetrating : break, pas de mouvement) depenetration : on prend le hit le plus profond (>= 1), Exit = Normal * Depth, Base += Exit, reboucle ; si la normale est quasi horizontale et que Wish y pointe, le mouvement est projete sur le plan
S11 Glissement VectorPlaneProject(Wish*(1-Time), Normal), puis Wish.Z = 0 VectorPlaneProject(Wish*(1-Time), Normal) sans Z = 0
S12 Z verrouille a PlayerSpot.Z si bCapsulesOn (l. 180-185) laisse libre (le Blueprint gere le sol)
S13 Application AddActorWorldOffset(.., false) SetActorLocation(Base, false, nullptr, TeleportPhysics)
S14 Etat de reprise aucun LastSafe memorise ; Engage l'initialise a la position du Lead
S15 Journal aucun Show couple : bloque par %s normal %s profondeur %.0f (une fois par blocage) ; coque ... ignoree
S16 Coque les 4 murs sont BlockAllDynamic donc bloquent le couple la coque est ignoree par nom (Wall) : sinon le couple restait coince

Constat : les deux algorithmes sont des variantes du meme slide-and-project, mais le jeu est un sur-ensemble (depenetration, lits, coque) et la sandbox est volontairement simple. Un test de parite (S5-T3) devra donc tourner sur le meme algorithme pour que la sandbox prevoie le jeu.

2.4 Meubles Pawn et lits

# Sujet Sandbox Jeu
F1 Meubles de la chambre Furniture[] : UStaticMeshComponent crees par la sandbox, Pawn = Block des la creation (RoomBuilder l. 146) Room du GI : acteurs lus par reflexion (proprietes objet / tableaux de Room, acteurs attaches, rayon 4000), hors ACharacter
F2 Bascule bFurnitureBlocks : SetCollisionResponseToChannel(ECC_Pawn, Block/Ignore) sur tout Furniture[] (Apply, l. 82-85), touche 1 BlockRoomPawn : tout primitif (hors mesh squelettique, hors NoCollision) dont la reponse Pawn est Ignore passe a Block ; l'ancien etat est memorise dans SavedFurniture et restaure par Release (l. 646-693, 722-728)
F3 Reponse "Overlap" non traitee comptee (Overlap) mais non modifiee (seul Ignore est inverse)
F4 Meubles "d'usage" (assis / couche / utilise / travail) non traites IsUseFurniture : existence de `/Game/Heavy/New2/Daily/-f-0-<sit
F5 Lits aucun traitement IsBedMesh : nom contient bed, ou fnt_ext011, fnt_new045, fnt_new235. Hors penetration, Normal.Z > 0.6 et Wish.Z < -1 : on pose le couple dessus. En penetration avec Normal.Z > 0.6 et Wish.Z < 0 : on annule le Z et on ignore le lit
F6 Assis / couches vs capsules non traites LetSittersPass : pour tout ACharacter dont l'anim contient -sit, -lie, -use, -work ou sitting : capsule WorldDynamic = Ignore (memorise puis restaure dans SavedSeats)
F7 Debrayage global touches 1 / 2 CVar lp.ShowCouple (1 : nouveau, 0 : ancien chemin Blueprint)

2.5 Soudure (weld) et drapeaux

# Drapeau / action Sandbox (Weld + Apply + FitCoupleBox.ResetBody) Jeu (Engage + Weld + Release)
W1 Mouvement du partenaire StopMovementImmediately, DisableMovement, SetComponentTickEnabled(false) a chaque Weld pareil, mais une seule fois dans Engage (l. 451-461) ; ancien etat sauvegarde (FSavedMove : mode, gravite, tick)
W2 Restauration jamais (la sandbox detache plus tard dans BeginInteraction en coupant juste les mouvements) Release : SetComponentTickEnabled, SetMovementMode, GravityScale, detache en KeepWorldTransform (l. 710-720)
W3 Capsule du partenaire SetCollisionEnabled(NoCollision) a chaque Weld non touchee : le sous-systeme exige CapsuleOff (capsule deja coupee par le Blueprint) pour reconnaitre un acteur du show (l. 98-102, 375)
W4 Capsule du Lead Apply : NoCollision, DisableMovement, GravityScale = 0, teleport a PlayerSpot non touchee (idem : CapsuleOff requis) ; aucune gravite forcee
W5 Taille des capsules ResetBody : SetCapsuleSize(CapsuleRadius, CapsuleHalf, true) sur Lead et Partner jamais redimensionnees
W6 Placement des meshes seulement dans FitCoupleBox / ApplyBody a chaque tick (Weld l. 760-774)
W7 Pied du mesh mannequin ApplyBody : -Half - MinZ (MinZ = bas des bounds du mesh Mannequin) ; mais FitCoupleBox.ResetBody ecrase par -Half (incoherence interne a la sandbox) -Half uniquement
W8 Attache if Parent != Lead attache puis relatif zero ; a chaque Weld idem, a chaque tick
W9 Reglages "Interaction" BeginInteraction : detache, capsule Lead QueryAndPhysics, Block sur WorldStatic et WorldDynamic, gravite 1, MaxWalkSpeed = 180, MOVE_Walking, boite NoCollision n'existe pas : le jeu rend la main via Release

2.6 Toggles de canaux

# Toggle Sandbox Jeu
T1 ECC_Pawn des meubles bFurnitureBlocks (touche 1), sans restauration par composant BlockRoomPawn (Ignore -> Block, memorise) / Release (restaure) / OpenUseFurniture (Block -> Ignore, non restaure)
T2 Collision de la boite bCapsulesOn (touche 2) : QueryAndPhysics / NoCollision toujours QueryAndPhysics tant que le show est branche ; boite detruite par Release
T3 ECC_WorldDynamic des capsules Block en Interaction pour le Lead (BeginInteraction) ; sinon profil copie de Person (ConfigureCapsule) Ignore pour les capsules assises, memorise puis restaure
T4 ECC_WorldDynamic de la boite Block Block
T5 Profil de collision de capsule copie complete du CDO Person : profil, type d'objet, tableau de reponses (ConfigureCapsule, l. 36-55) non touche
T6 Murs BlockAllDynamic (RoomBuilder l. 170) coque Wall* ignoree dans le balayage (pas de changement de canal)

3. Principaux ecarts comportementaux (resume)

  1. Mesure des extents : le jeu mesure d'abord les os vivants (anchor pelvis 140 cm, os exclus, plafond 400, pad 4/min 8) ; la sandbox ne mesure que les animations (tous os, pad 12/12/6, plafond 800). Le jeu retombe sur la mesure anime si la vivante echoue.
  2. Plancher : -88 (jeu) contre -90 (sandbox) en local, et seule la voie anime du jeu clampe.
  3. Sweep : 2 iterations simple / depart en penetration = arret, contre 3 iterations multi / depenetration, lits, coque ignoree, tous les ACharacter ignores.
  4. Weld : le jeu sauvegarde et restaure (mode, gravite, tick) ; la sandbox ne restaure rien et force aussi capsules, gravite et tailles. Le jeu depend d'un etat deja coupe par le Blueprint.
  5. Pawn : sandbox = interrupteur global du tableau Furniture ; jeu = inversion Ignore -> Block memorisee, ouverture des meubles d'usage, capsules assises ouvertes.
  6. Hors sujet de la sandbox : lits, meubles d'usage, assis/couches, selection du couple, lp.ShowCouple.

4. API commune proposee : LPCoupleSim.h dans LPAnimMeasure

4.1 Principes

  • Header Plugins/LPSandbox/Source/LPAnimMeasure/LPCoupleSim.h, namespace LPCoupleSim, exports LPANIMMEASURE_API. Aucun changement de .Build.cs (les deux modules dependent deja de LPAnimMeasure, qui n'utilise que Core, CoreUObject, Engine).
  • Nom d'implementation : LPCoupleSimCore.cpp (et non LPCoupleSim.cpp) pour eviter une collision de nom avec LPSandbox/LPCoupleSim.cpp (autre module, autres intermediaires, mais confusion d'IDE et de diff).
  • Trois niveaux, du plus pur au plus lie a Unreal :
    1. Pur (aucun UObject, aucun UWorld) : constantes, FTransform, FBox, FVector. Testables sans monde.
    2. Lectures d'assets (const UAnimSequence*, const USkeletalMeshComponent*) : sans effet de bord.
    3. Effets de bord (composants, canaux) : fonctions fines, chacune avec etat sauvegarde / restaure.
  • Le sous-systeme jeu et la sandbox appellent les memes fonctions avec des options differentes, pas avec deux copies de l'algorithme.

4.2 Constantes et parametres

namespace LPCoupleSim
{
	// Valeurs communes (jeu = reference) ; la sandbox peut les surcharger par option.
	inline constexpr double DefaultCapsuleHalf = 88.0;
	inline constexpr int32 PoseSamples = 8;
	inline constexpr float WalkableNormalZ = 0.6f;
	inline constexpr float MinPenetrationDepth = 1.f;
	inline constexpr float MoveEpsilon = 0.1f;

	struct FBoxFitParams
	{
		double CapsuleHalf = DefaultCapsuleHalf;
		// Mesures par animation (voies sandbox et FitBox du jeu)
		FVector AnimPad = FVector(12.0, 12.0, 6.0);
		double AnimMaxSize = 800.0;
		// Mesures par corps vivants (FitToBodies du jeu)
		FVector LivePad = FVector(4.0, 4.0, 4.0);
		FVector LiveMinExtent = FVector(8.0, 8.0, 8.0);
		double LiveMaxSize = 400.0;
		double LiveAnchorRadius = 140.0;
		// Fond de boite en Z local de l'acteur (la plante = -CapsuleHalf).
		double FloorLocalZ = -DefaultCapsuleHalf;
		double MinExtentZ = 20.0;
	};

	struct FBoxFit
	{
		FVector Center = FVector::ZeroVector;
		FVector Extent = FVector::ZeroVector;
		bool bValid = false;
	};
}

4.3 Fonctions pures (sans UObject)

namespace LPCoupleSim
{
	// Transformation commune mesh -> acteur (rotation -90 yaw, decalage -CapsuleHalf).
	LPANIMMEASURE_API FTransform MeshToActor(double CapsuleHalf);

	// Espace composant a partir de locales + parents (remplace les deux boucles dupliquees).
	LPANIMMEASURE_API void ComposeComponentSpace(
		const TArray<int32>& Parents, const TArray<FTransform>& Local, TArray<FTransform>& OutComponent);

	// Ajoute les positions d'os (espace composant) dans la boite, via MeshToActor.
	LPANIMMEASURE_API void AccumulatePoints(
		const TArray<FTransform>& Component, const FTransform& MeshToActor, FBox& InOutBounds);

	// Os exclus de la mesure vivante (hand, finger, foot, toe, twist, breast, hair, eye, jaw, weapon, ik).
	LPANIMMEASURE_API bool IsExcludedLiveBone(const FName BoneName);

	// Mesure vivante : ancre pelvis/hip, rayon, os exclus. Nom des os fournis par l'appelant.
	LPANIMMEASURE_API void AccumulateLiveBones(
		const TArray<FName>& BoneNames, const TArray<FTransform>& Component, const FTransform& MeshToActor,
		const FBoxFitParams& Params, FBox& InOutBounds);

	// Transforme une boite brute en boite finale (padding, plafond, recalage du fond).
	LPANIMMEASURE_API FBoxFit FitFromAnimBounds(const FBox& Bounds, const FBoxFitParams& Params);
	LPANIMMEASURE_API FBoxFit FitFromLiveBounds(const FBox& Bounds, const FBoxFitParams& Params);

	// Resolution de glissement : cas pur, pour tests unitaires et pour les deux algorithmes.
	LPANIMMEASURE_API FVector SlideWish(const FVector& Wish, const FVector& Normal, float TimeFraction, bool bFlattenZ);
}

FitFromAnimBounds reprend la logique commune (pad, plafond 800, fond clampe : Bottom = Center.Z - Extent.Z, si < FloorLocalZ alors Cut = FloorLocalZ - Bottom, Extent.Z = max(MinExtentZ, Extent.Z - Cut*0.5), Center.Z += Cut*0.5). FloorLocalZ : jeu = -Half (-88), sandbox actuellement 18 - PlayerSpot.Z (-90). Valeur de reference proposee : -Half (jeu), la sandbox s'aligne (a valider en S5-T3 avec LPPairCollisionTest).

4.4 Lectures d'assets (sans effet de bord)

namespace LPCoupleSim
{
	// Echantillonne une AnimSequence (8 pas, os animes, GetBoneTransform raw=false) et ajoute au boite.
	// Remplace Accumulate (sandbox) et AddPose (jeu).
	LPANIMMEASURE_API bool AccumulateAnimBounds(
		const UAnimSequence* Anim, const FTransform& MeshToActor, FBox& InOutBounds,
		int32 Samples = PoseSamples);

	// Os vivants d'un SkeletalMeshComponent (espace composant -> acteur, rotation incluse).
	LPANIMMEASURE_API void AccumulateLiveBody(
		const USkeletalMeshComponent* Mesh, const FBoxFitParams& Params, FBox& InOutBounds);

	// Paire F/M ou 0/1 d'un clip, dans son dossier (remplace PairSibling cote jeu ; la sandbox
	// l'utilise pour _F/_M et gagne _0/_1).
	LPANIMMEASURE_API UAnimSequence* PairSibling(const UAnimSequence* Anim);
	LPANIMMEASURE_API bool IsIdleClip(const UAnimSequence* Anim);
}

Note : LPPoseMeasure::Sample n'est pas un bon remplacement : il echantillonne tout le squelette et appelle GetBoneTransform(..., true) (donnees brutes) alors que les deux copies actuelles utilisent false et seulement les os animes. Garder le comportement des copies pour la parite.

4.5 Boite, sweep, soudure, canaux (effets de bord)

namespace LPCoupleSim
{
	// --- Boite ---
	// Cree (ou reutilise) la boite, nom et profil communs. bEnabled pilote QueryAndPhysics / NoCollision.
	LPANIMMEASURE_API UBoxComponent* EnsureBox(
		ACharacter* Lead, UBoxComponent*& InOutBox, FName Name, bool bEnabled);
	LPANIMMEASURE_API void ApplyBoxFit(UBoxComponent& Box, const FBoxFit& Fit);

	// --- Sweep ---
	struct FSweepOptions
	{
		int32 MaxIterations = 3;                 // sandbox : 2 aujourd'hui
		bool bFlattenZ = false;                  // sandbox : true
		bool bAcceptWalkableStep = false;        // sandbox : true (Normal.Z > 0.6)
		bool bDepenetrate = true;                // sandbox : false (arret net)
		bool bIgnoreAllCharacters = true;        // sandbox : false
		TFunction<bool(const UPrimitiveComponent*)> IsWalkableSupport;  // jeu : IsBedMesh
		TFunction<bool(const AActor*)> IsIgnoredShell;                    // jeu : nom "Wall" / "Wall_*"
	};
	struct FSweepResult
	{
		FVector Location = FVector::ZeroVector;  // position acteur retenue
		FString Blocker;                          // dernier bloqueur (journal)
		bool bMoved = false;
	};
	LPANIMMEASURE_API FSweepResult ResolveCoupleMove(
		UWorld& World, const UBoxComponent& Box, const ACharacter& Lead, const ACharacter* Partner,
		const FVector& From, const FVector& Wish, const FSweepOptions& Options);

	// --- Soudure ---
	struct FWeldOptions
	{
		bool bFreezePartnerMovement = true;
		bool bPartnerCapsuleOff = false;     // sandbox : true, jeu : false (deja coupe par le Blueprint)
		bool bLeadCapsuleOff = false;        // sandbox : true
		bool bLeadGravityZero = false;       // sandbox : true
		bool bResizeCapsules = false;        // sandbox : true
		bool bPlaceMeshes = true;
		double CapsuleHalf = DefaultCapsuleHalf;
		float CapsuleRadius = 20.f;
	};
	struct FSavedMove       // remplace FSavedMove (jeu) ; la sandbox en beneficie a BeginInteraction
	{
		TWeakObjectPtr<UCharacterMovementComponent> Move;
		TEnumAsByte<EMovementMode> Mode = MOVE_None;
		float Gravity = 1.f;
		bool bTick = true;
	};
	LPANIMMEASURE_API FSavedMove SaveAndFreeze(ACharacter& Body);
	LPANIMMEASURE_API void RestoreMove(FSavedMove& Saved);
	LPANIMMEASURE_API void Weld(ACharacter& Lead, ACharacter& Partner, const FWeldOptions& Options);
	LPANIMMEASURE_API void Unweld(ACharacter& Partner);   // DetachFromActor(KeepWorldTransform)

	// --- Canaux (meubles, capsules) ---
	struct FSavedResponse
	{
		TWeakObjectPtr<UPrimitiveComponent> Comp;
		ECollisionResponse Previous = ECR_Ignore;
		ECollisionChannel Channel = ECC_Pawn;
	};
	// Inverse From -> To pour un canal, avec memorisation pour restaurer. Retourne le nombre modifie.
	LPANIMMEASURE_API int32 FlipResponse(
		TArrayView<UPrimitiveComponent* const> Comps, ECollisionChannel Channel,
		ECollisionResponse From, ECollisionResponse To, TArray<FSavedResponse>& Saved);
	LPANIMMEASURE_API void RestoreResponses(TArray<FSavedResponse>& Saved);
}

4.6 Mapping des appelants

Fonction commune Appelant sandbox (LPCoupleSim.cpp) Appelant jeu (LPShowCoupleSubsystem)
MeshToActor, AccumulateAnimBounds, FitFromAnimBounds FitCoupleBox (remplace la lambda Accumulate) FitBox (remplace AddPose)
AccumulateLiveBody, FitFromLiveBounds aucun (la sandbox peut ajouter un mode "corps vivants" pour tester le chemin jeu) FitToBodies
PairSibling, IsIdleClip optionnel CollectShowAnims
EnsureBox, ApplyBoxFit FitCoupleBox FitBox, FitToBodies
ResolveCoupleMove MoveCouple (options sandbox) KeepTogether (options jeu)
SaveAndFreeze, RestoreMove, Weld, Unweld Weld, BeginInteraction Engage, Weld, Release
FlipResponse, RestoreResponses Apply (touche 1) BlockRoomPawn, LetSittersPass, Release
Reste local PlayIdle, ConfigureCapsule, corps Quaternius, RefreshPose, PlayWalk, vitesse 250 selection du couple (SexPersons, AP1/AP2, ShowReach), IsBedMesh, IsUseFurniture, OpenUseFurniture, CVar lp.ShowCouple, reflexion GI / Room

5. Risques

  1. Parite du sweep : tout alignement des options sandbox sur celles du jeu (3 iterations, depenetration, coque) change le comportement de la sandbox. A valider avec LPPairCollisionTest avant de supprimer l'ancien code.
  2. Boite vivante : la sandbox n'a pas de corps vivants au moment du calcul (le mesh est place a la volee). Pour prevoir le jeu, ajouter un mode "vivant" optionnel cote sandbox, sinon l'ecart E1/E6 subsiste.
  3. Plancher : -90 / -88 : la boite sandbox descend 2 cm plus bas. Verifier les lits et le seuil de marche.
  4. GetBoneTransform raw : conserver false pour la parite ; ne pas basculer sur LPPoseMeasure::Sample.
  5. Reflexion GI / Room : reste dans le jeu (ObjectActor, ObjectProperty, ArrayProperty, BlockRoomPawn). Ne pas la deplacer dans LPAnimMeasure (aucun Blueprint GI/Room cote addon).
  6. FSavedMove : le jeu restaure, la sandbox non. Utiliser la meme structure impose de decider si BeginInteraction doit restaurer le mode du partenaire (aujourd'hui il ne coupe que le mouvement).
  7. Nom de fichier : LPCoupleSim.cpp existe deja cote LPSandbox ; utiliser LPCoupleSimCore.cpp cote LPAnimMeasure.
  8. Unity build : LPAnimMeasure utilise PCHUsage = UseExplicitOrSharedPCHs ; ajouter les includes (Components/BoxComponent.h, GameFramework/Character.h, Animation/AnimSequence.h) dans le .cpp, pas dans le header (declarations anticipees class UBoxComponent; class ACharacter; ...).

6. Ordre de migration recommande (S5-T2 et suivants)

  1. LPCoupleSim.h + LPCoupleSimCore.cpp : niveaux pur et lecture d'assets (MeshToActor, ComposeComponentSpace, AccumulateAnimBounds, FitFromAnimBounds). Brancher FitBox (jeu) et FitCoupleBox (sandbox) ; comparer les journaux boite ... centre ... etendue avant / apres sur une meme paire (ecart attendu : seulement le fond, 2 cm).
  2. FSavedMove, Weld, Unweld, FlipResponse, RestoreResponses. Brancher Engage / Release / BlockRoomPawn / LetSittersPass (jeu), puis Weld / Apply (sandbox).
  3. ResolveCoupleMove avec les options jeu ; ajouter les options sandbox ; garder l'ancien MoveCouple jusqu'a validation.
  4. AccumulateLiveBody + FitFromLiveBounds (jeu) ; option sandbox "corps vivants".
  5. Validation : build Win64 Development du jeu et du plugin, test LPPairCollisionTest, comparaison des journaux Show couple et Sandbox boite.

7. Points non verifies

  • Les captures ShowCouple du jeu n'ont pas ete rejouees : toutes les valeurs ci-dessus viennent de la lecture du code.
  • Docs/Migration/etat/sandbox/S5/ n'existait pas avant ce document.