LifePlay-Revival/Docs/Migration/etat/sandbox/S5/S5-IMPL-H.md

258 lines
16 KiB
Markdown
Raw Normal View History

# S5-IMPL-H — `LPCoupleSim` : source de vérité de la géométrie du couple (fiche S5-T2)
- **Date :** 2026-10-06
- **Fiche :** S5-T2 | **Agent :** A-IMPL-H | **Rôle :** Implémentation
- **Modèle prévu :** `claude-opus-5-thinking-high` (M3) | **Modèle utilisé :** `claude-opus-5-thinking-high`
- **Amont :** `S5/S5-CART.md` (S5-T1, lecture seule), `S4/S4-IMPL-H.md`
- **Statut :** **compilé** — `LPSandboxEditor Win64 Development` **et** `LifePlayEditor Win64 Development`, 0 erreur / 0 avertissement
- **Commit addon :** `c1fdd29` — `S5-T2: LPCoupleSim dans LPAnimMeasure, source de verite geometrie du couple` (poussé sur `origin/main`)
- **Repo jeu :** `Source/LifePlay/LPShowCoupleSubsystem.{h,cpp}` modifiés **sans commit** (ces deux fichiers ne sont pas encore suivis par git dans le repo jeu ; la fiche ne demande que le commit addon).
---
## 1. Où vit le code
Il n'existe **qu'une seule copie** de `LPAnimMeasure` : celle de l'addon. `LifePlay.uproject` déclare
`"AdditionalPluginDirectories": ["../LifePlay-Sandbox/Plugins"]` et `LifePlay.Build.cs` dépend
publiquement de `LPAnimMeasure` (l. 45). Un header posé dans l'addon est donc immédiatement visible
par le jeu, **sans aucun changement de `.Build.cs`** et sans recopie.
| Fichier | LOC | État |
|---|---:|---|
| `Plugins/LPSandbox/Source/LPAnimMeasure/LPCoupleSim.h` | **215** | nouveau |
| `Plugins/LPSandbox/Source/LPAnimMeasure/LPCoupleSimCore.cpp` | **474** | nouveau |
| `Plugins/LPSandbox/Source/LPSandbox/LPCoupleSim.cpp` | 477 → **384** | −93 (−119 / +26) |
| `Source/LifePlay/LPShowCoupleSubsystem.cpp` | 1048 → **805** | −243 |
| `Source/LifePlay/LPShowCoupleSubsystem.h` | 77 → **72** | `FSavedMove` imbriquée supprimée |
Nom de fichier d'implémentation : `LPCoupleSimCore.cpp`, comme le recommandait la cartographie
(risque n° 7), pour ne pas collisionner avec `LPSandbox/LPCoupleSim.cpp`.
**336 lignes de code dupliqué disparaissent des deux appelants**, contre 689 lignes de module
partagé. Le solde est positif en volume (+353) et assumé : il paie les commentaires du contrat,
les deux structures d'options, et les **deux** stratégies de balayage, qui vivaient auparavant
dans deux modules différents sans aucun lien.
## 2. API livrée (`namespace LPCoupleSim`, exports `LPANIMMEASURE_API`)
### 2.1 Constantes
```cpp
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;
inline constexpr double MeshYawDegrees = -90.0;
```
### 2.2 Paramètres de mesure
```cpp
struct FBoxFitParams
{
double CapsuleHalf = DefaultCapsuleHalf;
FVector AnimPad = FVector(12.0, 12.0, 6.0); // voie animations
double AnimMaxSize = 800.0;
FVector LivePad = FVector(4.0, 4.0, 4.0); // voie corps vivants
FVector LiveMinExtent = FVector(8.0, 8.0, 8.0);
double LiveMaxSize = 400.0;
double LiveAnchorRadius = 140.0;
double FloorLocalZ = -DefaultCapsuleHalf; // fond de boîte, Z local
double MinExtentZ = 20.0;
};
struct FBoxFit { FVector Center; FVector Extent; bool bValid = false; };
```
`FloorLocalZ` par défaut = `-DefaultCapsuleHalf`, comme demandé par la fiche.
### 2.3 Niveau pur (aucun `UObject`, aucun `UWorld` — testable sans monde)
| Fonction | Rôle |
|---|---|
| `FTransform MeshToActor(double CapsuleHalf)` | lacet −90, décalage `-CapsuleHalf` |
| `void ComposeComponentSpace(const TArray<int32>& Parents, const TArray<FTransform>& Local, TArray<FTransform>& OutComponent)` | locales + parents → espace composant |
| `void AccumulatePoints(const TArray<FTransform>& Component, const FTransform& ToActor, FBox&)` | ajoute chaque os à la boîte |
| `bool IsExcludedLiveBone(const FString& BoneName)` | hand, finger, foot, toe, twist, breast, hair, eye, jaw, weapon, ik |
| `FBoxFit FitFromAnimBounds(const FBox&, const FBoxFitParams&)` | padding, plafond, recalage du fond sur `FloorLocalZ` |
| `FBoxFit FitFromLiveBounds(const FBox&, const FBoxFitParams&)` | padding, étendue minimale, plafond (pas de recalage) |
| `FVector SlideWish(const FVector& Wish, const FVector& Normal, float TimeFraction, bool bFlattenZ)` | glissement, `Z = 0` optionnel |
### 2.4 Niveau lecture (assets et composants, sans effet de bord)
```cpp
double CapsuleHalfOf(const ACharacter* Who, double Fallback = DefaultCapsuleHalf);
bool AccumulateAnimBounds(const UAnimSequence*, const FTransform& ToActor, FBox&, int32 Samples = PoseSamples);
void AccumulateLiveBody(const USkeletalMeshComponent*, const FBoxFitParams&, FBox&);
```
### 2.5 Niveau effets de bord
```cpp
UBoxComponent* EnsureBox(ACharacter* Lead, UBoxComponent* Existing, FName Name, bool bEnabled);
void ApplyBoxFit(UBoxComponent* Box, const FBoxFit& Fit);
void PlaceBodyMesh(ACharacter* Body, double CapsuleHalf);
struct FSavedMove { TWeakObjectPtr<UCharacterMovementComponent> Move; TEnumAsByte<EMovementMode> Mode; float Gravity; bool bTick; };
FSavedMove SaveAndFreeze(ACharacter* Body);
void RestoreMove(FSavedMove& Saved);
void AttachPartner(ACharacter* Lead, ACharacter* Partner);
void Unweld(ACharacter* Partner);
```
`EnsureBox` prend et **retourne** un `UBoxComponent*` au lieu du `UBoxComponent*&` proposé par la
cartographie : la sandbox stocke sa boîte dans un `TObjectPtr`, le jeu dans un `TWeakObjectPtr`,
aucun des deux ne peut lier une référence à `UBoxComponent*`.
### 2.6 Balayage
```cpp
struct FSweepOptions
{
int32 MaxIterations = 3;
bool bMultiSweep = true; // SweepMulti (jeu) ou SweepSingle (sandbox)
bool bFlattenZ = false;
bool bAcceptWalkableStep = false; // voie simple seulement
bool bDepenetrate = true;
bool bIgnoreAllCharacters = true;
TFunction<bool(const UPrimitiveComponent*)> IsWalkableSupport; // jeu : IsBedMesh
TFunction<bool(const AActor*)> IsIgnoredShell; // jeu : Wall / Wall_*
TFunction<void(const AActor*)> OnIgnoredShell; // journal de l'appelant
static FSweepOptions Show(); // 3, multi, dépénétration, PNJ + coque ignorés
static FSweepOptions Sandbox(); // 2, simple, arrêt net, marches, Z à plat, rien d'ignoré
};
struct FSweepResult { FVector Location; FString Blocker; FVector BlockerNormal; float BlockerDepth; bool bMoved; };
FSweepResult ResolveCoupleMove(UWorld*, const UBoxComponent*, const ACharacter* Lead,
const ACharacter* Partner, const FVector& From, const FVector& Wish, const FSweepOptions&);
```
`ResolveCoupleMove` **ne déplace rien** : elle rend la position d'acteur retenue, et chaque appelant
l'applique à sa façon (`AddActorWorldOffset` pour la sandbox, `SetActorLocation` + `TeleportPhysics`
pour le jeu). C'est ce qui permet de partager la résolution sans toucher au contrat de chaque côté.
**Deux stratégies, une seule API.** `bMultiSweep` choisit `ResolveSingle` (sandbox) ou `ResolveMulti`
(jeu), deux fonctions statiques de `LPCoupleSimCore.cpp`. C'est un choix délibéré : la fiche demande
de garder `SweepSingle` 2 itérations d'un côté et `SweepMulti` 3 itérations de l'autre, et les deux
séquences ne sont pas superposables sans changer un comportement (voir §5.1). Tout le reste du
balayage — paramètres de requête, liste des ignorés, forme, rotation, décalage de la boîte, tolérance
finale — est **commun**.
## 3. Branchements
| Appelant | Avant | Après |
|---|---|---|
| **Sandbox** `FitCoupleBox` | lambda `Accumulate` (53 l.), padding + clamp inline, création de boîte inline, `ResetBody` inline | `MeshToActor` + `AccumulateAnimBounds` + `FitFromAnimBounds` + `EnsureBox` + `ApplyBoxFit` + `PlaceBodyMesh` |
| **Sandbox** `Weld` | 3 appels de gel + capsule + attache | `SaveAndFreeze` + capsule + `AttachPartner` |
| **Sandbox** `MoveCouple` | boucle de balayage (30 l.) | `ResolveCoupleMove(..., FSweepOptions::Sandbox())` |
| **Jeu** `FitBox` | `AddPose` (51 l.) + padding + clamp + boîte inline | mêmes helpers, `FloorLocalZ = -CapsuleHalfOf(Lead)` |
| **Jeu** `FitToBodies` | lambda `AddBody` (54 l.) + étendues + boîte inline | `AccumulateLiveBody` + `FitFromLiveBounds` + `EnsureBox` + `ApplyBoxFit` |
| **Jeu** `Weld` | attache + placement des deux meshes inline | `AttachPartner` + 2 × `PlaceBodyMesh` |
| **Jeu** `Engage` / `Release` | `FSavedMove` imbriquée, gel et restitution inline | `SaveAndFreeze` / `Unweld` + `RestoreMove` |
| **Jeu** `KeepTogether` | boucle de balayage (95 l.) | `ResolveCoupleMove(..., FSweepOptions::Show())` + 3 lambdas d'options |
`AddPose` (jeu) et la lambda `Accumulate` (sandbox) — les deux copies « caractère par caractère »
signalées au §1 de la cartographie — **n'existent plus**. Idem pour les deux blocs de création de
`UBoxComponent` et les deux placements de mesh.
## 4. Options retenues de chaque côté (écarts volontairement conservés)
| Réglage | Sandbox | Jeu |
|---|---|---|
| Fonction de balayage | `SweepSingleByChannel` | `SweepMultiByChannel` |
| Itérations | 2 | 3 |
| Point de départ | position courante du porteur | `LastSafe` |
| Départ en pénétration | arrêt net | dépénétration (profondeur ≥ 1) |
| Marches | `Normal.Z > 0.6` → pas entier accepté | pas de marche générique ; lits seulement |
| Glissement | `Z` annulé | `Z` libre |
| Ignorés | porteur + partenaire | + tous les `ACharacter` + coque `Wall*` |
| `FloorLocalZ` | `18 - PlayerSpot.Z` (= −90) | `-CapsuleHalfOf(Lead)` (= −88) |
| Nom du composant | `CoupleBox` | `LPShowCoupleBox` |
L'écart de plancher de 2 cm (C4 / E7 de la cartographie) est **conservé** et devient une seule ligne
visible dans `FitCoupleBox` ; l'aligner est désormais un changement d'un caractère, à valider en S5-T3.
## 5. Écarts de comportement introduits (liste exhaustive)
### 5.1 Ce qui n'a pas bougé
Les deux séquences de balayage sont recopiées pas à pas, y compris les points où elles divergent :
- le jeu teste `Wish.IsNearlyZero()` **après** la dépénétration, la sandbox **en condition de boucle** ;
les deux ordres sont préservés dans leur fonction respective (les unifier ferait sauter une passe de
dépénétration au jeu) ;
- la garde `!StepDelta.IsNearlyZero() && !Hit.bStartPenetrating` de la sandbox est conservée ;
- le jeu projette `Wish` **sans** facteur de temps sur la normale du plus profond, puis **avec**
`1 - Time` sur celle du bloqueur : les deux appels à `SlideWish` passent `1.f` et `1.f - Time`.
### 5.2 Les cinq différences réelles
1. **Position de la boîte pendant la résolution.** Les deux versions recalculaient le point de départ
différemment (`Box->GetComponentLocation()` relu à chaque passe côté sandbox,
`FTransform(ActorQuat, Base).TransformPosition(Rel)` côté jeu). La version commune fige
`BoxOffset = Box->GetComponentLocation() - Lead->GetActorLocation()` avant la boucle. Identique
tant que la rotation du porteur ne change pas pendant la résolution (c'est le cas des deux) ;
plus robuste que la version jeu, qui ignorait l'échelle de l'acteur.
2. **Déplacement de la sandbox appliqué en une fois.** `AddActorWorldOffset` était appelé à chaque
passe ; il l'est maintenant une fois, avec la somme. Même résultat à l'arrondi `double` près,
`bSweep` restant `false`.
3. **Journal `bloque par` du jeu : une ligne par frame au lieu d'une par passe.** `FSweepResult` ne
retient que le dernier bloqueur en pénétration. Si deux bloqueurs différents apparaissent dans
deux passes d'une même frame, seul le second est journalisé. La déduplication `LastBlocker` et le
format du message sont inchangés ; la coque reste journalisée une seule fois, via `OnIgnoredShell`.
4. **Étiquette de requête de collision.** `LPPairBox` (sandbox) et `LPShowCouple` (jeu) deviennent
`LPCoupleSim`. Visible uniquement dans `STATGROUP_CollisionTags`.
5. **Précision `float` → `double` dans le recalage du fond.** Les deux copies calculaient `Bottom` et
`Cut` en `float` ; `FitFromAnimBounds` les calcule en `double`. Écart de l'ordre de 10⁻⁵ cm.
Le seuil `PlayerSpot.Z + Center.Z - Extent.Z < 18` de la sandbox est réécrit en
`Center.Z - Extent.Z < 18 - PlayerSpot.Z`, algébriquement identique.
Hors balayage, aucune autre formule n'a été retouchée : padding, plafonds, étendue minimale, ancre de
pelvis, os exclus, échantillonnage à 8 pas, `GetBoneTransform(..., raw = false)`, os animés seuls,
rejet des poses NaN, profil de collision de la boîte et rotation relative nulle sont recopiés.
## 6. Compilation
| Cible | Mode d'unité | Résultat |
|---|---|---|
| `LPSandboxEditor Win64 Development` | **non-unity** (`LPCoupleSim.cpp` et `LPCoupleSimCore.cpp` exclus du blob, car dans le working set git de l'addon) | succès, 0 erreur / 0 avertissement |
| `LifePlayEditor Win64 Development` | **unity** (`Module.LPAnimMeasure.cpp`, `Module.LPSandbox.cpp`, `Module.LifePlay.cpp`) | succès, 0 erreur / 0 avertissement |
Les deux modes sont couverts (risque n° 8 de la cartographie, `UseExplicitOrSharedPCHs`) : le passage
non-unity prouve que les `#include` de `LPCoupleSimCore.cpp` se suffisent, le passage unity valide le
header dans le blob. Aucun include n'a dû être corrigé ; la compilation est passée du premier coup
des deux côtés. `LPCoupleSim.h` ne déclare que des types anticipés (`ACharacter`, `UBoxComponent`,
`UAnimSequence`, `USkeletalMeshComponent`, `UCharacterMovementComponent`, `UPrimitiveComponent`,
`AActor`, `UWorld`) ; les en-têtes lourds restent dans le `.cpp`.
UHT : 1 fichier de réflexion régénéré (`LPShowCoupleSubsystem.h` a changé, aucune `UCLASS` /
`UPROPERTY` touchée — la structure supprimée était une structure C++ nue dans la partie privée).
Deux includes devenus inutiles ont été retirés : `Animation/Skeleton.h` dans
`LPSandbox/LPCoupleSim.cpp` et dans `LPShowCoupleSubsystem.cpp` (seul `AddPose` / `Accumulate` les
utilisaient).
## 7. Ce qui n'a pas été partagé, et pourquoi
| Élément | Décision |
|---|---|
| `PairSibling`, `IsIdleClip`, `CompletePair`, `KeepShowClips` | restent dans le jeu. La sandbox ne gère pas `_0/_1` et n'a pas de `LastAnimation` ; les déplacer sans appelant côté addon n'apporte rien à cette fiche (§4.4 de la cartographie, à reprendre si la sandbox gagne la complétion de paire). |
| `FlipResponse` / `RestoreResponses` (canaux des meubles, capsules assises) | hors fiche S5-T2. La sandbox bascule un tableau `Furniture`, le jeu parcourt la chambre par réflexion : le partage demande d'abord d'unifier la **collecte** des primitives, pas seulement l'inversion de réponse. |
| Réflexion `GI` / `Room`, `IsBedMesh`, `IsUseFurniture`, `OpenUseFurniture`, `lp.ShowCouple`, sélection du couple | restent dans le jeu (risque n° 5 de la cartographie). `IsBedMesh` est injecté dans le balayage par `FSweepOptions::IsWalkableSupport`, il n'a pas besoin de migrer. |
| Mode « corps vivants » côté sandbox | `AccumulateLiveBody` et `FitFromLiveBounds` sont exportés et prêts ; aucun appelant sandbox n'a été ajouté (risque n° 2 de la cartographie). |
| `CapsuleRadius`, vitesse 250, verrouillage de `PlayerSpot.Z`, `ConfigureCapsule`, corps Quaternius | spécifiques sandbox, inchangés. |
## 8. Reste à faire
- **S5-T3 (validation / parité).** Rien n'a été exécuté ici : seuls les deux builds sont la preuve.
Comparer, sur une même paire, les lignes `Sandbox boite %s : centre %s, etendue %s` et
`Show couple : boite %s centre %s etendue %s` avant / après ce lot (écart attendu : **aucun**, le
plancher restant différent par option). Passer `LPPairCollisionTest`.
- **Commit du repo jeu.** `LPShowCoupleSubsystem.{h,cpp}` ne sont pas suivis par git côté jeu ; leur
première mise sous suivi sort du périmètre de cette fiche.
- **Alignement du plancher** (−90 → −88 côté sandbox) : une ligne de `FitCoupleBox`, à décider en
S5-T3 avec une capture avant / après.
- **Tests unitaires du niveau pur.** `MeshToActor`, `ComposeComponentSpace`, `FitFromAnimBounds`,
`FitFromLiveBounds` et `SlideWish` ne touchent ni `UObject` ni `UWorld` : ils sont testables dans
un commandlet sans monde, ce qui n'était pas possible avant ce lot.