# 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** : `_F` et `_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-` ; `OpenUseFurniture` les met en `Ignore` Pawn hors show (non restaure) | | 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 ```cpp 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`) ```cpp 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& Parents, const TArray& Local, TArray& OutComponent); // Ajoute les positions d'os (espace composant) dans la boite, via MeshToActor. LPANIMMEASURE_API void AccumulatePoints( const TArray& 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& BoneNames, const TArray& 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) ```cpp 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) ```cpp 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 IsWalkableSupport; // jeu : IsBedMesh TFunction 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 Move; TEnumAsByte 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 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 Comps, ECollisionChannel Channel, ECollisionResponse From, ECollisionResponse To, TArray& Saved); LPANIMMEASURE_API void RestoreResponses(TArray& 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.