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

16 KiB
Raw Blame 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

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

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)

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

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

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.