LifePlay-Revival/Docs/Migration/etat/sandbox/S5/S5-CART.md

356 lines
28 KiB
Markdown
Raw Normal View History

# 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/<mesh>-f-0-<sit|lie|use|work>` ; `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<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)
```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<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.