diff --git a/.gitignore b/.gitignore index 8752552..c7b69b4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ Binaries DerivedDataCache Intermediate Saved -Debug +/Debug/ .vscode .vs diff --git a/Config/LPSandbox.ini b/Config/LPSandbox.ini new file mode 100644 index 0000000..333a510 --- /dev/null +++ b/Config/LPSandbox.ini @@ -0,0 +1,42 @@ +; Config/LPSandbox.ini +; Contrat S1-T1 — chemins et options du bac a sable LPPairSandbox. +; Toute cle absente => valeur par defaut du code (identique a celle-ci). +; Chemins "/Game/..." : format LoadObject (/Package/Nom.Nom ou /Package/BP.BP_C). +; Chemins relatifs (sans "/") : relatifs a Content/ sauf [Sandbox.Output] (relatif a Saved/). + +[Sandbox.Mode] +; Mode initial du menu : Pair | Interaction | Mesure +; (Saved/sandbox_mode.txt, s'il existe, reste prioritaire et est consomme.) +Mode=Pair + +[Sandbox.Assets] +PersonBP=/Game/BPs/Person.Person_C +BridgetMesh=/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp +MichaelMesh=/Game/Heavy/Daz/Michael/michael.michael +QuaterniusMannequinMesh=/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard +DazIdleFemale=/Game/Heavy/Daz/Anims/Idle_F_Daz.Idle_F_Daz +DazIdleMale=/Game/Heavy/Daz/Anims/idle_Daz.idle_Daz +FurnitureWallMaterial=/Game/Heavy/New2/Furniture/Materials/FntWall.FntWall + +[Sandbox.Roots] +; Racines de packages UE (sans nom d'asset final) +FurnitureRoot=/Game/Heavy/New2/Furniture +DazAnimRoot=/Game/Heavy/Daz/Anims +QuaterniusRoot=/Game/Quaternius +QuaterniusUALRoot=/Game/Quaternius/UAL + +[Sandbox.Content] +; Chemins disque, relatifs a Content/ +ModuleRoot=Modules/vin_Base +WorldFile=Modules/vin_Base/World/work___vin01.lpworld +ChatAction=Modules/vin_Base/Interactions/chat.lpaction +QuaterniusUALDisk=Quaternius/UAL + +[Sandbox.Sky] +; Optionnel : cubemap du SkyLight du sandbox +SkyCubemap=/Game/Heavy/New2/Buildings/Mats/urban_street_03_1k.urban_street_03_1k +SkyIntensity=0.6 + +[Sandbox.Output] +; Relatif a Saved/ +MeasureOutputRoot=Measurement diff --git a/Docs/Migration/09_Commandes_Utiles.md b/Docs/Migration/09_Commandes_Utiles.md index 878bf02..c389744 100644 --- a/Docs/Migration/09_Commandes_Utiles.md +++ b/Docs/Migration/09_Commandes_Utiles.md @@ -30,6 +30,12 @@ git status; git log --oneline -5; git tag --list '*-valide' # où en est-on & "$UE\Build\BatchFiles\RunUAT.bat" BuildCookRun -project="$Proj" -platform=Win64 -clientconfig=Development ` -build -cook -iterate -stage -archive -archivedirectory=D:\Game\LifePlay_Build_5.5_ -unattended -utf8output -nop4 +# Mesure (DebugGame ou Development) : PDB conserves, lanceurs deposes a cote de LifePlay.exe +.\Tools\BuildDebug.ps1 +.\Tools\BuildDebug.ps1 -Config Development +.\Tools\CollectDebugArtifacts.ps1 -Lot -BuildDir D:\Game\LifePlay_Build_5.5_DebugGame -Session A +.\Tools\SummarizeDebugCsv.ps1 -Csv + (Get-ChildItem D:\Game\LifePlay_Build_5.5_\Windows\LifePlay\Content -Recurse -Filter *.uasset).Count # T-PKG-01 ≥ 8555 ``` @@ -60,6 +66,9 @@ git status; git log --oneline -5; git tag --list '*-valide' # où en est-on # Mod d'assets (H3, 16_H3_Mods_Paquets.md §5-6) : sources dans ModSources\, cuisson et archive en une commande .\Tools\BuildMod.ps1 -Name [-Install \Windows] +.\Tools\BuildMod.ps1 -Name -Retarget bake [-RetargetOnly] +.\Tools\BuildMod.ps1 -Target sandbox -Open +.\Tools\BuildMod.ps1 -Target game -Open # Détail (version de référence puis cuisson DLC, toujours via cmd /c ou un .cmd : la version serait coupée) # cmd /c '"$UE\Build\BatchFiles\RunUAT.bat" BuildCookRun ... -createreleaseversion=2026.10.4 ...' # cmd /c '"$UE\Build\BatchFiles\RunUAT.bat" BuildCookRun ... -cook -stage -pak -iostore -compressed -dlcname="\Mods\\.uplugin" -basedonreleaseversion=2026.10.4 ...' @@ -72,6 +81,39 @@ git status; git log --oneline -5; git tag --list '*-valide' # où en est-on python Tools\DiffRemplacementNoeuds.py CX-B12 ``` +### Sandbox Pair / Mesure (plugin `LPSandbox`, addon sibling) + +Prérequis : clone `LifePlay-Sandbox` à côté du jeu, pin et liaison décrits dans `Docs/Migration/21_Sandbox_Addon.md` (`AdditionalPluginDirectories`, `../LifePlay-Sandbox/Plugins`). + +```powershell +# Lanceurs (GameMode addon : /Script/LPSandbox.LPPairSandboxGameMode) +.\Tools\debug\LifePlay_pair_sandbox.bat # Pair ou Interaction via le menu +.\Tools\debug\LifePlay_interaction_sandbox.bat # même entrée (alias historique) +.\Tools\debug\LifePlay_measure_sandbox.bat # écrit Saved\sandbox_mode.txt = Mesure + +# Cible des clips sandbox vs jeu (LPAnimRedirect) +.\Tools\BuildMod.ps1 -Target sandbox -Open +.\Tools\BuildMod.ps1 -Target game -Open +``` + +Sorties du mode Mesure (relatif à `Saved/`) : `Measurement/retarget_pose_.txt`, `Measurement/tour.csv`, `Measurement/tour/`, `Measurement/vignettes/`, `Measurement/planches/`, etc. (`Config/LPSandbox.ini`, clé `MeasureOutputRoot`). + +Chaîne d'animations headless (`LifePlayEditorTools`, module `LPAnimMeasure` fourni par le plugin) : + +```powershell +# Évaluation courte (1 clip) — archivage journal : RunCommandlet.ps1 +& $Cmd $Proj -run=LPAnimEval "-Clips=Idle_Loop" "-Instants=1" ` + "-Out=Saved/AnimPipeline/quaternius" -unattended -nopause + +.\Tools\RunCommandlet.ps1 -Lot AP4 -Commandlet LPAnimEval ` + -Args '-Out=Saved/AnimPipeline/quaternius' + +& $Cmd $Proj -run=LPAnimTwist "-ClipDir=/Game/Quaternius/UAL" -unattended -nopause +& $Cmd $Proj -run=LPAnimCatalog "-Livraison=Saved/AnimPipeline/quaternius/livraison.json" -unattended -nopause + +python Tools\anim\RapportControles.py Saved\AnimPipeline\quaternius +``` + Réimport scripté d'un correctif (photo, simulation comptée, application, diff, compilation) : ```powershell @@ -101,8 +143,21 @@ Réimport scripté d'un correctif (photo, simulation comptée, application, diff | `-LPDumpDelay=S` | délai avant ce relevé (1,5 s par défaut ; le lieu se charge en arrière-plan) | | `-LPDumpMax=N` | nombre maximal de relevés sur motif (20 par défaut) | | `-LPDumpEvery=S` | relevé périodique toutes les S secondes | +| `-LPHitchLog` | journal des à-coups (`LPDebug hitch`), non-Shipping, inactif sans ce flag | +| `-LPHitchMs=N` | seuil des à-coups en ms (50 par défaut) | +| `-LPCollisionLog` | suit la collision du monde joué (échantillon toutes les 2 s, dump dès qu'un trimesh a des triangles). Pas au `ExecCmds` de démarrage | -Commande console : `LifePlay.LogSummary` (bilan de session à la demande). +Commandes console : `LifePlay.LogSummary` (bilan de session à la demande) ; +`LifePlay.MemSnapshot`, `LifePlay.CollisionDump`, `LifePlay.Bookmark`, `LifePlay.HitchSummary` +(non-Shipping, `18_Build_Debug.md`). + +Lanceurs de session, déposés par `BuildDebug.ps1` à côté de `LifePlay.exe` : +`LifePlay_debug_sessionA_insights.bat`, `LifePlay_debug_sessionB_mem.bat`, +`LifePlay_debug_sessionC_profil.bat`, `LifePlay_debug_bugs.bat`, `LifePlay_debug_gpu.bat`, +`LifePlay_debug_collision.bat`. + +Ne jamais lancer le CSV avec `-csvCaptureFrames` (arrêt avant l'init du RHI). Session C : +`-ExecCmds="csvprofile repeat=20, csvprofile frames=9000"`. Exemple (mesure L7l, `LifePlay_releve_L7l.bat`) : diff --git a/Docs/Migration/20_Chaine_Animations.md b/Docs/Migration/20_Chaine_Animations.md new file mode 100644 index 0000000..b0fd353 --- /dev/null +++ b/Docs/Migration/20_Chaine_Animations.md @@ -0,0 +1,159 @@ +# 20. Chaîne des animations : de l'achat du pack au mod livré (étude, 2026-10-06) + +Objectif : qu'un pack d'animations, quel que soit son squelette d'origine, soit converti pour les corps du jeu +(Genesis 8 : Bridget, Michael), évalué et livré en mod **sans intervention manuelle**, avec un contrôle bloquant à +chaque étape. Base technique : la méthode validée dans la sandbox Mesure (CHG-188 à 204, mémoire « transfert de +pose ») et le mécanisme des mods d'assets (16_H3_Mods_Paquets.md). + +--- + +## 1. État actuel de la chaîne + +| Question | Situation aujourd'hui | Source | +|---|---|---| +| **Comment les animations entrent** | Un seul pack, importé à la main : `Tools/ImportQuaternius.py` lit un `.glb` à un chemin fixe (`D:/Game/Universal Animation Library[Standard]/…`) et écrit dans `/Game/Quaternius/UAL`. Un second pack (`Content/FreeAnimationsPack`, mannequin UE5) est présent mais n'est pas traité. | `ImportQuaternius.py` | +| **Comment le modèle est identifié** | Il ne l'est pas : la correspondance des os est écrite en dur deux fois, pour le seul couple « mannequin UE4 (Quaternius) → Genesis 8 » : en C++ (`Links`, `TSegs`, `TransferMaps`, `DazToT` dans `LPPoseMeasure.cpp`) et en Python (`SOURCE_CHAINS`, `DAZ_CHAINS` dans `RetargetQuaternius.py`). | | +| **Le pack contient-il un modèle de référence** | Quaternius : oui, `UAL1_Standard` (maillage, squelette, repos en pose T). FreeAnimationsPack : oui, `SKM_Manny` / `SKM_Quinn`, plus un clip `MM_T_Pose` et son propre `RTG_Mannequin`. | `Content/Quaternius/UAL`, `Content/FreeAnimationsPack/Demo` | +| **Calques** | Le calque de pose (CHG-203) est le squelette cible posé par la formule, à partir du squelette source du pack : il utilise déjà le modèle de référence du pack (son squelette au repos), pas un modèle à nous. | mode Mesure du plugin `LPSandbox` (`LPSandboxMeasure*.cpp`, ex-`LPPairSandbox.cpp`) | +| **Conversion** | Retargeter Unreal généré par `RetargetQuaternius.py`, pose de retarget lue dans `Saved/Measurement/retarget_pose_.txt` (CHG-204). Écriture : entrée du mode Mesure (`Tools/debug/LifePlay_measure_sandbox.bat`, `Saved/sandbox_mode.txt` = `Mesure`) ou commandlet `-run=LPAnimEval` (`-PoseDir` = `Saved/Measurement` par défaut). Module de mesure : `LPAnimMeasure` dans l'addon `LifePlay-Sandbox` (plugin `LPSandbox`, lot S3). Les os *Twist* sont répartis après cuisson (`-run=LPAnimTwist`, AP3). | `RetargetQuaternius.py`, `21_Sandbox_Addon.md` | +| **Évaluation** | **Interactive :** tournée sandbox Mesure (`Saved/Measurement/tour.csv`, captures `tour/`, vignettes, planches ; CHG-202, AP6). **Headless :** `-run=LPAnimEval` (`LifePlayEditorTools`, dépend de `LPAnimMeasure`) — rapports sous `Saved/AnimPipeline//` ; contrôles indépendants AP4 (`controles_*.csv`, `rapport.json`). | CHG-206 à CHG-208, `S6-VAL.md` | +| **Livraison** | Les clips convertis sont dans le **contenu du jeu** (`/Game/Quaternius/UAL/B_*`, `M_*`), adressés par un chemin en dur (`LPAnimRedirect::ClipPath`). Ils ne passent pas par un mod. `BuildMod.ps1 -Retarget` ne vérifie que la présence d'une ligne de journal. | `LPAnimRedirect.cpp`, `BuildMod.ps1` | +| **Appel depuis un scénario** | Un scénario désigne une animation par une clé courte : `ANIMATION: write` (`.lpaction`), `Actor.animate(martialart)` (`.lpscene`). Le jeu lui associe les assets `_F` (corps féminin) et `_M` (corps masculin), par exemple `Content/Heavy/New3/Anims/Daily/martialart_F` / `_M` ; la même convention sert aux paires de couple (`LPShowCoupleSubsystem`). Les clips convertis, nommés `B_` / `M_`, ne sont donc **pas appelables** par un scénario. Le jeu résout la clé par `Person::PlayDaily`, qui cherche dans `Room.Dailies` (tableau fixe de clips, sans registre d'assets) ; les clips des mods y sont ajoutés par `ULPModDailiesSubsystem` (CHG-209). | `vin_Base/Actions`, `vin_Base/Scenes` | +| **Information pour le moddeur** | Aucune : pour connaître la durée, la posture ou l'usage d'un clip, il faut l'ouvrir dans l'éditeur. | | + +### Écarts à combler + +1. Pas d'identification du squelette : un pack Mixamo, UE5, CC4 ou Genesis 9 serait converti avec une table fausse. +2. Correspondance dupliquée (C++ et Python) : une modification oubliée d'un côté fausse le résultat sans erreur. +3. ~~La pose de retarget dépend d'une ouverture manuelle de la sandbox.~~ **Levé (AP2 / S6)** : `-run=LPAnimEval` écrit les mêmes fichiers sans fenêtre ; la sandbox reste utile pour le contrôle visuel. +4. Torsion des membres non répartie entre *Bend* et *Twist* (peau de l'avant-bras). +5. Michael converti mais jamais mesuré. +6. Livraison hors mod : impossible de distribuer un pack sans recuire le jeu. +7. Aucun contrôle bloquant entre les étapes. +8. Clips nommés hors de la convention `_F` / `_M` : inutilisables depuis un scénario. +9. Aucun catalogue : un moddeur doit ouvrir chaque clip pour savoir ce qu'il fait. + +--- + +## 2. Réponses de fond + +**Le modèle de référence du pack doit-il servir aux calques ?** Oui, et c'est même nécessaire. Le calque est le +squelette cible posé par la transformation ; la transformation part de la **pose T du squelette source**. Chaque pack +apporte donc sa référence : son squelette au repos suffit (un `USkeleton` a toujours une pose de référence, même sans +maillage) ; son maillage ne sert qu'à l'affichage (touche M). Notre mannequin `UAL1_Standard` ne convient que pour les +packs au squelette mannequin UE4. + +**Condition** : le repos du squelette source doit être une pose T connue. Un pack livré en pose A (comme Genesis) doit +déclarer sa conversion A → T dans son profil (comme les réglages Daz). Un contrôle le vérifie (étape 3). + +**Conversion d'un modèle vers un autre** : la méthode validée est générale : +- pose T commune, ajustée segment par segment (phase A) ; +- rotation de chaque os depuis la pose T, passée dans le repère du corps cible (phase B) ; +- chaînes de longueurs différentes réparties par interpolation (colonne, cou) ; +- torsion séparée entre os *Bend* et *Twist* ; +- report dans le retargeter Unreal (phase C), qui applique la même formule quand sa pose de retarget est la pose T + ajustée (0,00 % d'écart sur 129 instants, CHG-204). + +Seule la **table de correspondance** change d'un squelette à l'autre : elle devient une donnée (profil), plus du code. + +--- + +## 3. Chaîne cible + +Un pilote unique, `Tools/AnimPipeline.ps1`, sur le modèle de `HairPipeline.ps1`, appelé par +`BuildMod.ps1 -Anim `. Chaque étape écrit un rapport JSON dans `Saved/AnimPipeline//` et **s'arrête au +premier contrôle en échec**, avec la liste des causes. Une étape réussie n'est pas refaite (reprise par `-From <étape>`). +Rien n'est supprimé : un résultat précédent est déplacé dans `precedent_`. + +| # | Étape | Contenu | Contrôle bloquant | +|---|---|---|---| +| 0 | **Inventaire** | Pack déclaré dans `AnimSources//pack.json` : origine, licence, fichiers (`.fbx`, `.glb`, ou dossier `.uasset`). | Licence présente et compatible (règle existante de `BuildMod` pour les mods payants) ; fichiers trouvés ; empreintes SHA-256 consignées. | +| 1 | **Import** | Import automatisé (généralisation d'`ImportQuaternius.py`) dans `/AnimSources//`. | Nombre de clips importés = nombre attendu ; durée et cadence lues ; aucun clip vide ; un squelette unique. | +| 2 | **Identification** | Empreinte du squelette (noms d'os, hiérarchie) comparée aux profils `Tools/anim/profiles/*.json` : mannequin UE4, mannequin UE5, Mixamo, Genesis 8, Genesis 9, CC4… | Un seul profil reconnu, avec au moins 95 % de ses os obligatoires présents ; sinon arrêt « squelette inconnu » et liste des os manquants. | +| 3 | **Référence et pose T** | Pose T source = repos du squelette (ou conversion déclarée par le profil) ; ajustement de la pose T de chaque corps cible (phase A, sans la sandbox : commandlet). | Repos source vérifié en T (bras à l'horizontale, jambes droites, tolérance fixée) ; après ajustement : tous les segments ≤ 1 %, réglages symétriques gauche / droite (écart ≤ 0,5°), aucun réglage hors bornes ; fichier `retarget_pose_.txt` écrit. | +| 4 | **Conversion** | Rigs et retargeters générés depuis le profil (chaînes, *Interpolated* pour colonne et cou, doigts, orteils, bassin) ; cuisson des copies dans `//Anims//`. Puis **répartition de la torsion** *Bend* / *Twist* sur les clips produits (post-traitement). | Autant de clips produits que de clips source, par corps ; durées et nombres d'images identiques ; aucune valeur non numérique. | +| 5 | **Évaluation** | Tournée sans intervention (fenêtre de jeu automatisée) sur chaque corps cible : écart à la formule, plus des contrôles indépendants (voir § 4). Planches de captures face / profil / dos et rapport HTML. | Chaque clip est classé « accepté », « à revoir » ou « rejeté » selon des seuils fixés ; un clip rejeté bloque la livraison sauf exclusion explicite dans `pack.json`. | +| 6 | **Catalogue** | Une fiche par animation (§ 5), calculée sur les clips produits et complétée par `pack.json`. | Une fiche par clé et par clip ; clés uniques dans le mod **et** sans collision avec les clés du jeu ni des mods connus ; champs obligatoires remplis ; vignettes présentes. | +| 7 | **Livraison** | Mod d'assets (H3) : clips renommés à la convention du jeu, `_F` / `_M`, sous `//Anims/…` ; module texte `.lpmod` ; catalogue joint ; cuisson DLC, contrôle du conteneur et archive `.zip` (étapes existantes de `BuildMod`). La clé peut contenir `_` (coupure au dernier `_`, CHG-209) ; outil : `Tools\anim\DeliverAnims.ps1`. | Conteneur sans asset du jeu ; archive testée ; **essai de chargement** : jeu de test lancé avec le mod, chaque clé du catalogue appelée par `animate()` sur un personnage féminin et un masculin (journal). | + +Le jeu résout les clés par `Room.Dailies` ; les clips des mods y sont ajoutés par `ULPModDailiesSubsystem` (CHG-209). +`LPAnimRedirect` ne sert plus qu'à la sandbox et au personnage piloté. + +--- + +## 4. Évaluation : ce qui est mesuré + +L'écart à la formule (0,00 % aujourd'hui) prouve que la conversion reproduit la transformation ; il ne prouve pas que +le résultat est beau. Des contrôles indépendants s'ajoutent, tous calculés sur la pose et sans rendu : + +| Contrôle | Défaut visé | Seuil « à revoir » (rejet : 3 × le seuil) | +|---|---|---| +| Écart à la formule (direction des segments, rotation du tronc) | conversion fausse | ≤ 1 % | +| Torsion locale des os *Bend* / *Twist* | peau de l'avant-bras ou de la cuisse vrillée | relevée ; bloquante sur option `-SeuilTorsion` | +| Main ou coude dans le volume du tronc (capsules) | « le bras passe à travers le corps » | pénétration ≤ 1 cm | +| Pied sous le sol, glissement au contact | jambes plus longues ou plus courtes que la source | sol ≤ 3 cm, glissement ≤ 3 cm/image | +| Limites articulaires (coude, genou) | hyperextension | genou ≤ 1 cm, flexion ≤ 5° | +| Continuité d'une image à l'autre | saut, tremblement | ≤ 10°, au-delà de 1,5 × la source + 2° | +| Symétrie des réglages de pose T | ajustement aberrant d'un côté | ≤ 0,5° | + +Faute d'actif physique sur les corps, les capsules du tronc sont construites depuis la peau du maillage. + +Les planches de captures restent la validation finale à l'œil, mais seulement pour les clips classés « à revoir ». + +--- + +## 5. Catalogue des animations (livré avec le mod) + +But : qu'un moddeur choisisse une animation et l'écrive dans un scénario **sans ouvrir l'éditeur ni le jeu**. + +**Formats** : +- `Content/Modules//Animations/catalogue.json` : pour les outils (lecture par script, éditeur de scénarios) ; +- `Content/Modules//Animations/catalogue.html` : pour le moddeur, une ligne par animation avec sa vignette, + filtrable par posture, usage et durée ; lisible hors ligne ; +- `Content/Modules//Animations/vignettes/.png` : profil droit à 1/3 de la durée, mode vignettes de la tournée. + +**Fiche d'une animation** : *calculé* = mesuré sur le clip produit, sans saisie ; *déclaré* = repris de `pack.json` +(ou déduit du nom, puis corrigeable). + +| Champ | Exemple | Origine | +|---|---|---| +| `cle` (identifiant) | `qua_walk_loop` | calculé : préfixe du pack + nom source normalisé ; unique et stable d'une version à l'autre | +| `appel` | `Actor.animate(qua_walk_loop)` / `ANIMATION: qua_walk_loop` | calculé | +| `nom` | « Marche » / « Walk » | déclaré (nom source par défaut) | +| `assets` | `//Anims/Female/qua_walk_loop_F`, `…/Male/qua_walk_loop_M` | calculé | +| `duree_s`, `images`, `cadence` | 1,07 s, 32 images, 30 i/s | calculé | +| `taille_ko` | 186 Ko (`_F`) + 190 Ko (`_M`), cuits | calculé (après cuisson) | +| `boucle` | oui (première et dernière pose identiques à 1 % près) | calculé | +| `deplacement` | avance 1,2 m par cycle (1,1 m/s), sans root motion : le personnage marche sur place | calculé (trajet du bassin) | +| `posture` | debout / assis / accroupi / à genoux / allongé | calculé (hauteur du bassin et inclinaison du tronc) | +| `hauteur_assise_cm` | 46 cm (pour placer un siège) | calculé, postures assises | +| `contacts` | pieds au sol ; mains libres / main droite occupée | calculé (hauteur des pieds, vitesse et position des mains) | +| `accessoire` | pistolet, épée, torche, volant | déclaré, proposé d'après le nom (`Pistol`, `Sword`, `Torch`, `Driving`) | +| `categorie` | locomotion, attente, conversation, combat, assis, interaction, sport, danse | déclaré, proposé d'après le nom | +| `style` | neutre, formel, détendu, agressif | déclaré | +| `usage` | « attente en conversation, debout » | déclaré | +| `corps` | féminin, masculin | calculé (clips produits) | +| `qualite` | accepté ; pire écart 0,00 % ; contrôles verts | calculé (étape 5) | +| `source`, `licence`, `credit` | Quaternius UAL, CC0 | déclaré (`pack.json`) | +| `version`, `empreinte` | version du mod, SHA-256 des clips | calculé | + +**Réalisation (AP6, CHG-210)** : les champs calculés viennent de `LPAnimCatalogCommandlet` (`-run=LPAnimCatalog`, mesure image par image du clip féminin source, contrôle que F et M ont même durée et même nombre d'images) ; les champs déclarés viennent de `AnimSources/quaternius/pack.json` ; la qualité vient du rapport AP4 ; la taille, l'empreinte et les chemins viennent des fichiers livrés. `Tools/anim/Catalogue.py` assemble le tout. Définitions : *boucle* si la dernière image diffère de la première de 1 % de la taille ou moins (extrémités relatives à la hanche) ; *déplacement* : trajet horizontal de la hanche, « sur place » sous 5 cm ; *posture* par image (allongé : tronc > 60° ; à genoux : genou < 12 cm et hanche < 72 % ; assis : hanche < 72 % et plus de 20 cm en arrière des chevilles ; accroupi : hanche < 72 % ; debout sinon), posture du clip si elle couvre au moins 60 % des images, sinon « variable ». Les vignettes font 512 px, de profil. `taille_ko` est la taille des `.uasset` non cuits (`taille_cuite: false`) en attendant AP7. La posture peut être déclarée dans `pack.json` (champ `posture`), ce qui remplace la posture calculée. Planches (CHG-211) : chaque fiche porte aussi une planche JPEG de 256 px (12 images par seconde de clip, de 4 à 24, côte à côte) que la page fait défiler au survol de la vignette (au toucher : lance / arrête ; la planche n'est chargée qu'au premier survol) ; un GIF a été écarté (256 couleurs, lourd, aucun encodeur disponible). + +Le catalogue est **régénéré à chaque passage** de la chaîne : il ne peut pas décrire un état antérieur des clips. +Le pilote refuse de livrer un mod dont le catalogue n'a pas été produit dans le même passage. + +--- + +## 6. Découpage en lots proposé + +| Lot | Objet | Contrôle de sortie | +|---|---|---| +| AP1 ✔ | Profils de squelettes en JSON, lus par le C++ et le Python ; suppression des tables en dur | sandbox et retarget identiques à CHG-204 (tournée : 0,00 %) — **fait, CHG-205** | +| AP2 ✔ | Commandlet de pose T et d'évaluation sans fenêtre (étapes 3 et 5, mesures) | mêmes fichiers de pose et même `tour.csv` que la sandbox — **fait, CHG-206** (`-run=LPAnimEval`) | +| AP3 ✔ | Répartition de la torsion *Bend* / *Twist* sur les clips produits | contrôle de torsion vert, comparaison visuelle — **fait, CHG-207** (`-run=LPAnimTwist`) ; comparaison visuelle à faire | +| AP4 ✔ | Contrôles indépendants (§ 4) et rapport HTML | seuils étalonnés sur les 43 clips — **fait, CHG-208** (`-run=LPAnimEval`, `Tools/anim/RapportControles.py`) ; 0 rejeté, 8 clips à revoir (4 par corps : `Death01`, `Fixing_Kneeling`, `Roll` au sol, `Jump_Start` au saut) | +| AP5 ✔ | Livraison en mod : clips renommés `_F` / `_M` sous `//Anims`, ajout à `Room.Dailies` et correctif de `Person::PlayDaily`, essai de chargement par `animate()` | jeu de test avec le mod Quaternius, clés appelées depuis un scénario d'essai — **fait, CHG-209** ; essai empaqueté reporté (nouvelle version de référence nécessaire) | +| AP6 ✔ | Catalogue (§ 5) : fiches calculées, `pack.json`, vignettes, `catalogue.json` et `catalogue.html` | une fiche par clip, champs calculés relus sur 5 clips témoins (marche, assis, pistolet, danse, chute) — **fait, CHG-210** ; taille cuite en AP7 | +| AP7 | Pilote `AnimPipeline.ps1` + `BuildMod -Anim`, identification, deuxième pack (FreeAnimationsPack, mannequin UE5) | pack UE5 converti, évalué, catalogué et livré de bout en bout sans intervention | + +Chaque lot suit `10_Protocole_Operations.md` : instantané, journal `CHG`, retour arrière par `Restore.ps1`. diff --git a/Docs/Migration/21_Sandbox_Addon.md b/Docs/Migration/21_Sandbox_Addon.md new file mode 100644 index 0000000..cf2e64c --- /dev/null +++ b/Docs/Migration/21_Sandbox_Addon.md @@ -0,0 +1,187 @@ +# 21. Migration sandbox → addon éditeur (ébauche, 2026-10-06) + +**Objectif :** extraire le code de sandbox Pair / Interaction / Mesure (aujourd'hui dans `Source/LifePlay/` et +`Source/LifePlayEditorTools/`, en grande partie non suivi par git) vers un **plugin éditeur** dédié, sans casser le +jeu empaqueté ni la chaîne d'animations (`20_Chaine_Animations.md`). Le module `LifePlay` ne garde que le contrat +minimal vers le jeu (dont `LPShowCoupleSubsystem`). + +**Amont :** lot S0 validé (`S0-VAL.md` : PASS). Inventaire gelé : `Docs/Migration/etat/sandbox/S0/S0-CART.md`. + +--- + +## Périmètre S0 (fichiers gelés) + +Liste figée au 2026-10-06 ; toute évolution ultérieure passe par une fiche de lot et une entrée `CHG`. + +### Runtime (`Source/LifePlay/`) + +| Chemin | LOC approx. | Notes | +|---|---:|---| +| `LPPairSandbox.h` | 322 | Stage / Character / GameMode Pair | +| `LPPairSandbox.cpp` | 4261 | Monolith Pair, interaction, mesure | +| ~~`LPInteractionSandbox.*`~~ | — | **Retiré S4-T3** (addon) ; Interaction via menu `LPPairSandbox` | +| `LPPoseMeasure.h` | 100 | API mesure | +| `LPPoseMeasure.cpp` | 1558 | Sample / Transfer / T-fit | +| `LPSkeletonProfile.h` | 38 | Profils JSON | +| `LPSkeletonProfile.cpp` | 157 | | +| `LPPoseOrchestrator.h` | 30 | | +| `LPPoseOrchestrator.cpp` | 487 | | +| `LPAnimRedirect.h` | 15 | Cible `sandbox` \| `game` | +| `LPAnimRedirect.cpp` | 24 | | +| `LPShowCoupleSubsystem.h` | 77 | **Reste dans LifePlay** (sync lot S5) | +| `LPShowCoupleSubsystem.cpp` | 1048 | | + +### Éditeur (`Source/LifePlayEditorTools/`) + +| Chemin | LOC approx. | Notes | +|---|---:|---| +| `LPPairCollisionTestCommandlet.*` | ~546 | PairCol headless | +| `LPAnimEvalCommandlet.*` | ~489 | Mesure headless | +| `LPAnimCatalogCommandlet.*` | ~388 | | +| `LPAnimChecks.*` | ~435 | | +| `LPAnimTwistCommandlet.*` | ~384 | | + +### Lanceurs (`Tools/debug/`) + +| Chemin | Notes | +|---|---| +| `LifePlay_pair_sandbox.bat` | Pair | +| `LifePlay_interaction_sandbox.bat` | Interaction | +| `LifePlay_measure_sandbox.bat` | Mesure | + +**Synthèse S0-CART :** ~7,5 k LOC runtime sandbox + mesure ; trois lanceurs désormais versionnables (motif +`.gitignore` : `/Debug/`). + +--- + +## Contrat (S1 : configuration des chemins) + +Le lot S1 fixe le premier contrat jeu ↔ addon : **tous les chemins d'assets et de fichiers du sandbox Pair passent par +`Config/LPSandbox.ini`**, lu par un point d'accès unique. Détail : `etat/sandbox/S1/S1-SPEC.md` ; implémentation +`S1-IMPL.md` ; audit `S1-CART.md` ; validation `S1-VAL.md` (PASS statique ; compilation et runtime à confirmer). + +- **Fichier** : `Config/LPSandbox.ini` (versionné, pas dans `Saved/`), lecture directe non hiérarchique (`FConfigFile::Read`). +- **API** : `struct LIFEPLAY_API FLPSandboxConfig` (`Source/LifePlay/LPSandboxConfig.h/.cpp`), `static const FLPSandboxConfig& Get()`, chargé une fois. `LPPairSandbox.cpp` et `LPAnimRedirect::ClipPath` n'utilisent plus de littéral `/Game/...`. +- **Repli** : fichier, clé ou valeur vide absents => default = littéral historique (comportement identique sans ini). Un asset introuvable garde le journal `LogLifePlay` d'origine, sans repli silencieux. +- **19 clés, 6 sections** : `Sandbox.Mode` (`Mode` = `Pair` | `Interaction` | `Mesure`, `Saved/sandbox_mode.txt` reste prioritaire) ; `Sandbox.Assets` (`PersonBP`, `BridgetMesh`, `MichaelMesh`, `QuaterniusMannequinMesh`, `DazIdleFemale`, `DazIdleMale`, `FurnitureWallMaterial`) ; `Sandbox.Roots` (`FurnitureRoot`, `DazAnimRoot`, `QuaterniusRoot`, `QuaterniusUALRoot`) ; `Sandbox.Content`, chemins relatifs à `Content/` (`ModuleRoot`, `WorldFile`, `ChatAction`, `QuaterniusUALDisk`) ; `Sandbox.Sky` (`SkyCubemap`, `SkyIntensity`) ; `Sandbox.Output`, relatif à `Saved/` (`MeasureOutputRoot`). +- **Conventions** : asset = chemin UE complet `/Game/...Nom.Nom` (`LoadObject`) ; racine = package sans nom d'asset ; disque = relatif à `ProjectContentDir()` (ou `ProjectSavedDir()` pour `Output`). +- **Hors contrat (volontaire)** : `LPKeyboard.ini`, `Saved/sandbox_mode.txt` / `sandbox_tour.txt`, littéraux `/Engine/...`, noms de clips et poses (`Idle_Loop`, `Walk_Loop`, `A_TPose`, `B_`/`M_`), liste `Names[]`. +- **Reste ouvert (lots suivants)** : points d'accroche ShowCouple (S5) ; staging de `LPSandbox.ini` si le sandbox doit tourner empaqueté. (Dépendances `Build.cs` et répartition des exports : traitées en S2, section suivante.) + +--- + +## Module `LPAnimMeasure` (S2 : extraction de la chaîne de mesure) + +Le lot S2 sort la chaîne de mesure de `LifePlay` vers un module Runtime dédié, `Source/LPAnimMeasure/` (structure plate, comme `LifePlay`). Détail : `etat/sandbox/S2/S2-CART.md` (cartographie), `S2-IMPL-H.md` (implémentation), `S2-VAL.md` (validation : PASS statique et runtime Mesure). + +- **Contenu** : `LPPoseMeasure`, `LPSkeletonProfile`, `LPPoseOrchestrator`, `LPAnimRedirect` (8 fichiers déplacés) + `LPAnimMeasure.Build.cs`, `LPAnimMeasure.h/.cpp` (catégorie de journal `LogLPAnimMeasure`, ex-`LogLifePlay` pour les lignes `POSET` / `PROFIL` / `ORCH` / `MESURE`). +- **Exports** : `LPANIMMEASURE_API` remplace `LIFEPLAY_API` ; 6 symboles ajoutés pour `LPPairSandbox.cpp` (`KeyBones`, `LogReport`, `TFitReport`, `Run`, `Restore`, `SampleAt`) ; les structs de données restent header-only. +- **Rupture du couplage** : `LPAnimRedirect` ne lit plus `LPSandboxConfig`. `FLifePlayModule::StartupModule()` (`LifePlay.cpp`) appelle `LPAnimRedirect::SetUALRoot(FLPSandboxConfig::Get().QuaterniusUALRoot)` ; défaut interne `/Game/Quaternius/UAL` (identique à celui de la config). +- **Déclaration** : entrée `Runtime` / `Default` dans `LifePlay.uproject`, placée avant `LifePlay` ; `ExtraModuleNames` des deux `*.Target.cs`. + +### Graphe de dépendances des modules + +```mermaid +flowchart LR + LPET["LifePlayEditorTools
(Editor)"] -->|Public| LP["LifePlay
(Runtime, module primaire)"] + LPET -->|Public
FLPPose dans LPAnimChecks.h| AM["LPAnimMeasure
(Runtime, nouveau S2)"] + LP -->|Public
LPPairSandbox, LPDirectControl| AM + LP -.->|"StartupModule : SetUALRoot(QuaterniusUALRoot)"| AM + AM -->|Public| ENG["Core, CoreUObject, Engine"] + AM -->|Private| JSON["Json"] +``` + +Règles : `LPAnimMeasure` ne dépend ni de `LifePlay` ni de `LifePlayEditorTools` (graphe acyclique) ; `LPSandboxConfig` reste dans `LifePlay` et pousse la configuration vers `LPAnimMeasure` par un setter, jamais l'inverse. Ordre de chargement `LPAnimMeasure` avant `LifePlay` : dépendance `Build.cs` et ordre du `.uproject`. + +| De | Vers | Type | Motif | +|---|---|---|---| +| `LifePlay` | `LPAnimMeasure` | Public | `LPPairSandbox.h/.cpp`, `LPDirectControlSubsystem.cpp`, `LifePlay.cpp` | +| `LifePlayEditorTools` | `LifePlay` | Public | existant | +| `LifePlayEditorTools` | `LPAnimMeasure` | Public | `LPAnimChecks.h` expose `FLPPose` ; commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist` | +| `LPAnimMeasure` | `Core`, `CoreUObject`, `Engine` / `Json` | Public / Privé | animations, squelettes ; lecture de `Tools/anim/profiles/*.json` | +| `LPAnimMeasure` | `LifePlay`, `LifePlayEditorTools` | **aucune** | évite un cycle de modules | + +--- + +## Stratégie git et liaison jeu ↔ addon (S3) + +Le lot S3 déplace le code sandbox dans un dépôt séparé, `LifePlay-Sandbox` (https://github.com/edujiro5972/LifePlay-Sandbox.git, branche `main`). Détail : `etat/sandbox/S3/S3-INT.md` ; validation : `etat/sandbox/S3/S3-VAL.md` (PASS). + +**Décision : deux dépôts siblings + `AdditionalPluginDirectories`. Pas de submodule imbriqué.** + +``` +D:\Game\ + LifePlay-master 5.5\ <- jeu (repo LifePlay) + LifePlay-Sandbox\ <- addon (repo LifePlay-Sandbox) +``` + +- **Liaison** : `LifePlay.uproject` déclare `"AdditionalPluginDirectories": ["../LifePlay-Sandbox/Plugins"]` (chemin relatif au `.uproject`) et active `LPSandbox` dans `Plugins`. UBT et l'éditeur y trouvent `LPSandbox.uplugin` (modules `LPSandbox` et `LPAnimMeasure`) ; les DLL du plugin sont compilées avec le jeu (`Build.bat LifePlayEditor Win64 Development`). +- **Pourquoi pas un submodule** : l'addon est un `uproject` mince qui contient le plugin sous `Plugins/LPSandbox/`. Un submodule monté sur `Plugins/LPSandbox` du jeu donnerait `Plugins/LPSandbox/Plugins/LPSandbox/` (plugin imbriqué, non scanné au bon niveau) ; un submodule `External/LifePlay-Sandbox` dupliquerait l'addon à côté du sibling et rendrait ambigu l'exemplaire chargé. Un clone sibling évite les deux problèmes. +- **Pin** : le jeu consomme un commit précis de l'addon, consigné à la main dans `S3-INT.md` (aujourd'hui `f09c42e5a2bc613cf269cce76c878461ca41c8b7`, `main`, S7-T2 uplugin ; précédents : `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` S5-T2 `LPCoupleSim`, `f8102647ccdebad37e4977ccbf2c51e583823a80`, S4-T3). Le pin est mis à jour (hash dans `S3-INT.md` et README de l'addon) à chaque évolution de l'addon que le jeu doit prendre. Reproduire : + +```powershell +cd D:\Game +git clone https://github.com/edujiro5972/LifePlay-Sandbox.git +git -C LifePlay-Sandbox checkout f09c42e5a2bc613cf269cce76c878461ca41c8b7 +# le jeu doit être cloné à côté : D:\Game\LifePlay-master 5.5 +``` + +- **Content** : le jeu n'a rien à monter. L'addon utilisé seul (`LPSandbox.uproject`) a besoin du Content du jeu : junction NTFS `LifePlay-Sandbox\Content` → `LifePlay-master 5.5\Content` (`/Game` ne peut pointer que vers `/Content` ; la clé `AdditionalAssetDirectories` n'existe pas en UE 5.5). Voir le README de l'addon. +- **Contenu du jeu après S3-T3** : `Source/LPAnimMeasure/` et les fichiers `LPPairSandbox*`, `LPInteractionSandbox*`, `LPSandboxConfig*` ne sont plus dans le jeu. Le bat Pair lance toujours `LifePlay.uproject` avec `game=/Script/LPSandbox.LPPairSandboxGameMode`. `Source/LifePlay/LifePlay.Build.cs` dépend toujours de `LPAnimMeasure` (module désormais fourni par le plugin). +- **Validation S3-VAL** : bat Pair depuis le jeu, plugin monté et GameMode résolu sans erreur de module ; addon mince lancé avec le même GameMode (réserve : son `.uproject` n'active pas `StreetMap`, d'où des erreurs de chargement de Blueprints du jeu sans effet sur le sandbox). + +--- + +## CoupleBox : `LPCoupleSim` partagé (S5) + +Le lot S5 ferme la **dérive de fork** entre la sandbox (`LPSandbox/LPCoupleSim.cpp`, méthodes de `ALPPairSandboxStage`) et le jeu (`LPShowCoupleSubsystem`) : les deux gardaient une copie de la géométrie du couple (mesure de pose, boîte, balayage, soudure) qui divergeait (plancher −90 / −88, itérations 2 / 3, os exclus). Détail : `etat/sandbox/S5/S5-CART.md` (écarts), `S5-IMPL-H.md` (implémentation), `S5-VAL.md` (validation : PASS runtime Pair, PASS-STATIC ShowCouple). Addon pinné sur `c1fdd29`. + +- **Où** : header `Plugins/LPSandbox/Source/LPAnimMeasure/LPCoupleSim.h` (215 l.) et `LPCoupleSimCore.cpp` (474 l.), module `LPAnimMeasure`, `namespace LPCoupleSim`, exports `LPANIMMEASURE_API`. Une seule copie : le jeu la voit par `LifePlay.Build.cs` (dépendance publique existante), sans recopie ni changement de `.Build.cs`. Le nom `LPCoupleSimCore.cpp` évite la collision avec `LPSandbox/LPCoupleSim.cpp`. +- **Trois niveaux d'API** : + - *pur* (sans `UObject` ni `UWorld`, testable sans monde) : `MeshToActor`, `ComposeComponentSpace`, `AccumulatePoints`, `IsExcludedLiveBone`, `FitFromAnimBounds`, `FitFromLiveBounds`, `SlideWish` ; + - *lecture* : `CapsuleHalfOf`, `AccumulateAnimBounds` (8 pas, os animés, `GetBoneTransform(raw=false)`), `AccumulateLiveBody` ; + - *effets de bord* : `EnsureBox`, `ApplyBoxFit`, `PlaceBodyMesh`, `SaveAndFreeze` / `RestoreMove` (`FSavedMove`), `AttachPartner`, `Unweld`, `ResolveCoupleMove`. +- **Paramètres** : `FBoxFitParams` (demi-capsule 88, pad anime 12/12/6 plafond 800, pad vivant 4/4/4 min 8 plafond 400, ancre pelvis 140, `FloorLocalZ`, `MinExtentZ` 20) ; `FSweepOptions::Show()` (3 itérations, `SweepMulti`, dépénétration, PNJ + coque `Wall*` ignorés) et `FSweepOptions::Sandbox()` (2 itérations, `SweepSingle`, arrêt net, marches, Z à plat). `ResolveCoupleMove` **ne déplace rien** : elle rend la position retenue (`FSweepResult`), chaque appelant l'applique. +- **Appelants** : sandbox `FitCoupleBox` / `Weld` / `MoveCouple` ; jeu `FitBox` / `FitToBodies` / `Weld` / `Engage` / `Release` / `KeepTogether`. Les copies `AddPose` (jeu), `Accumulate` (sandbox), les deux créations de `UBoxComponent` et les deux boucles de balayage n'existent plus (−336 lignes dupliquées, +689 dans le module partagé). +- **Écarts conservés volontairement** (options, pas de copies) : plancher sandbox −90 contre jeu −88 (`FloorLocalZ`), balayage simple 2 itérations contre multi 3 itérations, nom de composant `CoupleBox` contre `LPShowCoupleBox`. L'alignement du plancher est une ligne dans `FitCoupleBox`, à décider après capture en jeu. +- **Reste dans `LifePlay`** : sélection du couple (`SexPersons`, `AP1/AP2`, `ShowReach`), réflexion `GI` / `Room`, `IsBedMesh` (injecté via `IsWalkableSupport`), `IsUseFurniture`, `OpenUseFurniture`, `PairSibling` / `IsIdleClip`, `FlipResponse` / `RestoreResponses` (non partagés, hors S5-T2), CVar `lp.ShowCouple`. +- **Contrat de modification** : tout changement de géométrie du couple se fait dans `LPCoupleSim` (addon), puis bump du pin ; le jeu ne doit pas réintroduire de copie locale. + +--- + +## Acceptation + +État au 2026-10-06 (lot S8). Case cochée = prouvée par la fiche citée ; case vide = non faite. + +- [x] **S0** : hygiène dépôt, inventaire gelé (`S0-VAL.md` PASS). +- [x] **S1** : tous les chemins du sandbox via `Config/LPSandbox.ini` (`S1-VAL.md` PASS statique). +- [x] **S2** : module `LPAnimMeasure` extrait, sans dépendance vers `LifePlay` (`S2-VAL.md` PASS). +- [x] **S3** : addon dans le dépôt `LifePlay-Sandbox`, liaison sibling + `AdditionalPluginDirectories`, pin manuel (`S3-VAL.md` PASS). +- [x] **S4** : sandbox Pair découpé, GameMode Interaction legacy retiré (`S4-VAL.md` PASS). +- [x] **S5** : géométrie du couple partagée via `LPCoupleSim`, une seule copie (`S5-VAL.md` PASS runtime Pair, PASS-STATIC ShowCouple). +- [x] **S6** : stack Mesure sur la liaison addon, `LPAnimEval` court (`S6-VAL.md` PASS). +- [x] **S7 / isolation** : plugin sans `include` / `LIFEPLAY_API` / module `LifePlay` ; `Person` = CDO seul, aucun `GI` / `Room` ; chemins par config (`S7-SPEC.md`, `S7-CART.md` : 0 écart). +- [x] **S7 / configurations** : `LPSandbox` hors Shipping (`TargetConfigurationDenyList`), `LPAnimMeasure` conservé, plugin `Optional` dans `LifePlay.uproject` (`S7-IMPL-L.md`). +- [x] **Smoke Pair** (bat court, plugin monté, boîte du couple identique à S5-VAL, 0 crash) (`S7-VAL.md`). +- [ ] Commandlets anim restants (`LPAnimCatalog`, `LPAnimChecks`, `LPAnimTwist`) déplacés dans un module éditeur de l'addon : **non fait** (restent dans `LifePlayEditorTools`). +- [x] **S8** : bilan du mode multi-agents (`22_Eval_Agents_Sandbox.md` : GO conditionnel) ; code sandbox absent du module jeu depuis S3 ; smoke Pair par le `.bat` réel, boîte du couple identique à S5 / S7, 0 crash (`S8-VAL.md` PASS). +- [ ] `BuildMod.ps1 -Target sandbox -Open` rejoué sur la liaison addon : **non fait**. +- [ ] Compilation / packaging **Shipping** complets avec le plugin (seul le plan UBT est vérifié). +- [ ] Parcours Show complet en jeu et décision d'alignement du plancher (-90 / -88). +- [x] Commit addon `f09c42e` poussé vers `origin/main` (constaté en S8 : `origin/main` = `dae6206`, qui le contient). +- [ ] Migration commitée **côté jeu** (`LifePlay.uproject`, `Config/LPSandbox.ini`, `Build.cs`, `LPShowCoupleSubsystem.*`, docs `21` / `22`, `etat/sandbox/`) : **non fait** (`22` § 6). +## Lots (S0 … S8) + +| Lot | Objet (une ligne) | +|---|---| +| **S0** | Hygiène dépôt : `/Debug/` dans `.gitignore`, lanceurs `Tools/debug` suivis, inventaire et preuves `etat/sandbox/S0/*`. | +| **S1** | Contrat jeu ↔ addon : API minimale, dépendances modules, règles de linking (placeholder ci-dessus). | +| **S2** | Extraction du module `LPAnimMeasure` (mesure, profils, orchestrateur, redirection) hors de `LifePlay` ; dépendances `Build.cs` et exports. **Fait** : `S2-VAL.md` PASS (voir section « Module `LPAnimMeasure` »). Le plugin éditeur (uplugin) reste à faire. | +| **S3** | Création du dépôt addon `LifePlay-Sandbox` (scaffold, transfert du code, retrait côté jeu) et liaison par clone sibling + `AdditionalPluginDirectories`, pin manuel. **Fait** : `S3-VAL.md` PASS (voir section « Stratégie git »). Rejeu stack Mesure : lot **S6** (`S6-VAL.md`). | +| **S4** | Sandbox Pair : découpage `LPPairSandbox`, retrait GM Interaction legacy (S4-T3 **fait**), commandlet `LPPairCollisionTest`, bat Pair. Validation : `S4-VAL.md` (PASS). | +| **S5** | ShowCouple : `LPShowCoupleSubsystem` reste dans `LifePlay` ; géométrie du couple partagée via `LPCoupleSim` (module `LPAnimMeasure` de l'addon, pin `c1fdd29`). **Fait** : `S5-VAL.md` PASS (runtime Pair, PASS-STATIC ShowCouple ; voir section « CoupleBox »). Show complet en jeu et alignement du plancher restent à valider. | +| **S6** | Stack Mesure sur la liaison addon : `LifePlayEditorTools` → `LPAnimMeasure` (plugin), lanceurs `Tools/debug`, sorties `Saved/Measurement`, commandlet `-run=LPAnimEval`. **Fait** : `S6-VAL.md` PASS (doc `20` / `09`, essai court LPAnimEval). | +| **S7** | Isolation du plugin (sans lien vers `LifePlay`, `Person` en CDO seul, config seulement), `LPSandbox` hors Shipping, `Optional` dans `LifePlay.uproject`. **Fait** : `S7-VAL.md` PASS (smoke Pair ; Shipping vérifié par plan UBT). Le déplacement des commandlets Catalog / Checks / Twist reste à planifier (voir § Acceptation). | +| **S8** | Bilan et clôture : évaluation du mode multi-agents + multi-modèles (`22_Eval_Agents_Sandbox.md`, GO conditionnel, 3 recommandations), smoke Pair final. **Fait** : `S8-VAL.md` PASS. `BuildMod -Target` et commit côté jeu restent ouverts (§ Acceptation). | + +Chaque lot suit `10_Protocole_Operations.md` : instantané, entrée `CHG`, retour arrière documenté. diff --git a/Docs/Migration/22_Eval_Agents_Sandbox.md b/Docs/Migration/22_Eval_Agents_Sandbox.md new file mode 100644 index 0000000..6a52420 --- /dev/null +++ b/Docs/Migration/22_Eval_Agents_Sandbox.md @@ -0,0 +1,217 @@ +# 22. Évaluation du mode multi-agents + multi-modèles (migration sandbox → addon, S0–S8) + +- **Date :** 2026-10-06 | **Fiche :** S8-T1 | **Rôle :** A-DOC+Orch | **Entrée journal :** CHG-220 +- **Modèle prévu :** `claude-opus-5-5-medium` (M4) | **Modèle utilisé :** Claude Opus 5.5 (déclaré par l'environnement) +- **Sources :** `etat/sandbox/S0/` à `S7/` (29 rapports `*-CART`, `*-SPEC`, `*-IMPL*`, `*-INT*`, `*-VAL`), `21_Sandbox_Addon.md`, + journal `CHG-212` à `CHG-219`, état git des deux dépôts au 2026-10-06 21:00. +- **Validation de clôture :** [`etat/sandbox/S8/S8-VAL.md`](etat/sandbox/S8/S8-VAL.md) (smoke Pair par le `.bat`, **PASS**). + +**Biais à garder en tête :** ce bilan est écrit par le rôle orchestrateur, qui juge en partie sa propre organisation. Les +faits (verdicts, horodatages, état git) sont vérifiables dans les fiches citées ; les appréciations (sur/sous-classement, +coût) sont des jugements. + +--- + +## 1. Résumé exécutif + +**Fait.** Le code sandbox (Pair, Interaction, Mesure, ~7,5 k LOC) est sorti du module jeu vers le plugin `LPSandbox` du +dépôt séparé `LifePlay-Sandbox`, chargé par le jeu via `AdditionalPluginDirectories` (clone sibling, pin manuel). La +chaîne de mesure est un module à part (`LPAnimMeasure`), la géométrie du couple n'existe plus qu'en une copie +(`LPCoupleSim`, −336 lignes dupliquées), le plugin ne connaît pas le jeu à la compilation (0 include / `LIFEPLAY_API`) et +`LPSandbox` est exclu du Shipping. Huit validations PASS, aucune régression observée : la boîte du couple `AgMakeout` est +identique au millième de cm entre S5, S7 et S8. Aucun retour arrière n'a été nécessaire. Le tout a été fait en une +journée (S1 → S7 en ~3 h d'après les horodatages des fiches, 18:18 → 20:54). + +**Limites.** +- **Rien n'est commité côté jeu** : `LifePlay.uproject`, `Config/LPSandbox.ini`, `21`, `etat/sandbox/**`, et + `LPShowCoupleSubsystem.*` (non suivis). Un clone du jeu aujourd'hui ne monte pas l'addon ; le pin n'a de valeur + que sur ce poste. +- **Show en jeu jamais exécuté** : le partage `LPCoupleSim` côté jeu n'est prouvé que par compilation (PASS-STATIC, + S5) ; l'alignement du plancher −90 / −88 n'est pas décidé. +- **Shipping non compilé** (plan UBT seulement), commandlets anim (`Catalog`, `Checks`, `Twist`) toujours dans + `LifePlayEditorTools`, `BuildMod -Target sandbox` non rejoué. +- **Le build du jeu dépend du clone sibling** (`LifePlay.Build.cs` → `LPAnimMeasure`) malgré `Optional: true`. +- Preuves par journal uniquement : aucun contrôle visuel, modes Interaction et Mesure non exercés après S2. + +--- + +## 2. Lots S0–S8 + +« Rollback » = retour arrière **exécuté**. Tous les lots ont un retour arrière documenté dans le journal ; celui des +lots S1, S2 et S5 côté jeu porte sur des fichiers non suivis par git, donc sans instantané : il est manuel et non testé. + +| Lot | Tenu | Coupé / reporté | Rollback | Verdict VAL | +|---|---|---|---|---| +| **S0** | `.gitignore` `/Debug/`, 3 bats suivis, inventaire gelé | — | aucun | **PASS** (`S0-VAL`) | +| **S1** | `Config/LPSandbox.ini` (19 clés, 6 sections), `FLPSandboxConfig`, 0 littéral oublié | compilation et runtime (DLL plus ancienne que les sources) ; test d'override `WorldFile` laissé « manuel », jamais fait | aucun | **PASS statique** (`S1-VAL`, runtime BLOCKED) | +| **S2** | module `LPAnimMeasure` (8 fichiers, 6 exports), graphe acyclique, runtime Mesure (269 lignes `LogLPAnimMeasure`) | commandlets non relancés (repris en S6 pour `LPAnimEval` seul) ; mise à jour de `S1-CART` demandée, **jamais faite** | aucun | **PASS** (`S2-VAL`) | +| **S3** | dépôt addon, transfert, purge du jeu, `AdditionalPluginDirectories`, pin, build 91/91 | **submodule abandonné** ; addon mince avec réserve (plugin `StreetMap` absent, 2 458 erreurs BP sans effet) | **partiel** : la junction `Plugins\LPSandbox` créée en S3-T3 a été retirée en S3-T4 | **PASS** avec réserve (`S3-VAL`) | +| **S4** | `LPPairSandbox.cpp` 4 274 → 7 unités (max 837 LOC), GameMode Interaction legacy retiré (~543 LOC) | commandlet `LPPairCollisionTest` cité dans l'objet du lot : non traité ; mode Interaction non exercé | aucun | **PASS** (`S4-VAL`) | +| **S5** | `LPCoupleSim` partagé (3 niveaux d'API), 0 erreur / 0 avertissement, parité de la boîte | Show en jeu, décision du plancher, tests unitaires du niveau pur, mise sous git de `LPShowCoupleSubsystem` | aucun | **PASS** runtime Pair / **PASS-STATIC** Show (`S5-VAL`) | +| **S6** | doc `20` / `09` alignées, `LPAnimEval` court (code 0, 1,3 s) | bat Mesure non rejoué (renvoi à S2-VAL) | aucun | **PASS** (`S6-VAL`) | +| **S7** | contrat d'isolation (0 écart), `TargetConfigurationDenyList: Shipping`, `Optional` | compilation Shipping ; déplacement des commandlets ; push (fait après coup, constaté en S8) | aucun | **PASS** (`S7-VAL`) | +| **S8** | ce bilan ; smoke Pair par le `.bat` réel, parité de la boîte | `BuildMod -Target`, bout en bout Show | aucun | **PASS** (`S8-VAL`) | + +--- + +## 3. Qualité des consignes et des handoffs + +**Ce qui a marché.** +- Chaque VAL cite ses références, le pin, le journal copié et une section « Limites » ; les verdicts nuancés + (`PASS statique`, `PASS-STATIC`, `BLOCKED-RUNTIME`, « avec réserve ») ont empêché de sur-vendre un lot. +- L'enchaînement CART → IMPL-H a payé : S3-T2, S4-T2 et S5-T2 ont compilé du premier coup, sans include à corriger + (hors un défaut IWYU révélé par l'unity en S2). +- Les écarts à la fiche sont déclarés au lieu d'être cachés : S4-T2 (7 unités au lieu de 5, justifié par la taille), + S5-T2 (5 écarts de comportement listés exhaustivement), S7-T2 (options rejetées avec motif). + +**Ambiguïtés de consigne observées.** + +| Où | Ambiguïté | Effet | +|---|---|---| +| S1-T2 / S1-T4 | IMPL-L sans consigne de compiler, VAL sans droit de compiler | personne n'a compilé S1 ; le runtime n'a été prouvé qu'indirectement par S2 | +| S3-T3 / S3-T4 | plan « submodule en T4 » alors que la structure de l'addon (plugin imbriqué) le rendait impossible | junction temporaire créée puis retirée, décision d'architecture prise en INT | +| S3-T1, S7-T2 | « push non autorisé » sans dire qui pousse ni quand | pin pointant un commit local entre S7-T2 et la fin de S7 | +| S4 | objet du lot dans `21` (commandlet `LPPairCollisionTest`) ≠ fiches exécutées | item tombé sans trace de décision | +| S7 / S8 | libellés de `21` (S7 « commandlets », S8 « épuration, `BuildMod -Target` ») ≠ fiches reçues | S7-VAL doit signaler un « ancien libellé non traité » ; S8 ici = bilan, pas épuration | +| `21` § Lots | ligne S1 toujours « placeholder ci-dessus » | doc de référence partiellement périmée | + +**Reprises.** S1 runtime → couvert par S2 ; commandlets S2 → `LPAnimEval` en S6 (les autres jamais) ; junction S3-T3 → +retirée S3-T4 ; bat équivalent S7 → bat réel S8 ; push S7 → fait (constaté S8). **Orphelins** (demandés, jamais +repris) : mise à jour `S1-CART`, test d'override `WorldFile`, Show en jeu, commit côté jeu. + +**Forme.** Les fiches S3-INT et S7 sont écrites sans accents et sans en-tête « Modèle », les autres avec : le +gabarit de fiche n'était pas imposé. + +--- + +## 4. Adéquation modèle ↔ complexité + +Paliers d'après les fiches : M0 = `composer-2.5-fast`, M2 = `claude-sonnet-5-5-high`, M3 = `claude-opus-5-thinking-high`, +M4 = `claude-opus-5-5-medium` ; `gemini-3.8-flash-high` n'a pas de palier noté (M1 supposé). « Réel » = champ « Modèle +utilisé » de la fiche ; sinon « non tracé dans VAL », et l'évaluation porte sur le prévu. **12 fiches sur 28 tracent +le modèle réel (11 sur 27 avant S8).** + +| Fiche | Rôle | Modèle prévu | Réel | Résultat | Classement | +|---|---|---|---|---|---| +| S0-T1 | CART | gemini-3.8-flash-high | idem | inventaire exact | adéquat | +| S0-T2 | IMPL (`.gitignore`) | non tracé | non tracé dans VAL | 1 ligne, PASS | — | +| S0-T3 | VAL | composer-2.5-fast (M0) | idem | PASS, preuve `check-ignore` | adéquat | +| S1-T1 | ARCH (SPEC) | non tracé | non tracé dans VAL | contrat solide, réutilisé jusqu'en S7 | — | +| S1-T2 | IMPL-L | non tracé | non tracé dans VAL | correct mais **non compilé** | — (défaut de consigne, pas de modèle) | +| S1-T3 | CART (audit grep) | gemini-3.8-flash-high | idem | 0 oubli, confirmé par VAL | adéquat | +| S1-T4 | VAL | sonnet (M2) | idem | bon diagnostic (DLL > sources) mais checklist statique | **légèrement sur-classé** (M0 a fait des VAL plus lourdes en S4/S6) | +| S2-T1 | CART | sonnet (M2) | non tracé dans VAL | cartographie fiable | adéquat | +| S2-T2 | IMPL-H | opus thinking (M3) | non tracé dans VAL | compilé, 1 fix IWYU | **sur-classé** (déplacement + exports, M2 suffisait) | +| S2-T4 | VAL | sonnet (M2) | idem | PASS runtime Mesure | adéquat | +| S3-T1 | INT (scaffold) | sonnet (M2) | non tracé dans VAL | scaffold correct, push gelé | adéquat | +| S3-T2 | IMPL-H | opus thinking (M3) | non tracé dans VAL | transfert inter-dépôts compilé du premier coup | adéquat | +| S3-T3 | IMPL-L (purge jeu) | composer (M0) | composer (champ unique) | purge correcte, **sans build**, junction contraire à la décision finale | **sous-classé** (opération destructive inter-dépôts) | +| S3-T4 | INT | non tracé | non tracé dans VAL | abandon du submodule, build 91/91, premier push | — | +| S3-T5 | VAL | sonnet (M2) | idem | deux runtimes, cause `StreetMap` identifiée | adéquat | +| S4-T1 | CART | non tracé | non tracé dans VAL | découpage exploitable | — | +| S4-T2 | IMPL-H | opus thinking (M3) | non tracé dans VAL | 4 274 LOC découpés, 0 erreur | adéquat (risque élevé, aucune régression) | +| S4-T3 | IMPL (retrait GM) | non tracé | non tracé dans VAL | suppression propre | — | +| S4-T4a | VAL | composer (M0) | idem | build + runtime, PASS | adéquat | +| S5-T1 | CART | sonnet (M2) | non tracé dans VAL | 28 Ko d'écarts précis, base de S5-T2 | adéquat | +| S5-T2 | IMPL-H | opus thinking (M3) | idem | refonte de comportement, 0 avertissement, écarts listés | adéquat | +| S5-T3 | VAL | sonnet (M2) | idem | parité argumentée, PASS-STATIC honnête | adéquat | +| S6-T1..T3 | VAL (+DOC) | composer (M0) | idem | PASS, commandlet court | adéquat | +| S7-T1 | ARCH (SPEC) | non tracé | non tracé dans VAL | checklist C1–C5 réutilisable | — | +| S7-T2 | IMPL-L | non tracé | non tracé dans VAL | uplugin / uproject corrects | — | +| S7-T3 | CART | non tracé | non tracé dans VAL | 0 écart | — | +| S7-T4 | VAL+DOC | non tracé | non tracé dans VAL | PASS, limites claires | — | +| S8-T1/T2 | DOC+Orch, VAL | opus 5.5 medium (M4) | Claude Opus 5.5 (effort non vérifiable) | ce document, smoke PASS | adéquat pour la synthèse ; M0 suffirait pour le smoke seul | + +**Lecture.** Aucun échec imputable au modèle. Les deux défauts notables viennent de la **consigne** (S1 sans build) et +d'un **palier trop bas pour une opération destructive** (S3-T3). Le sur-classement (S2-T2, S1-T4) n'a rien cassé, il +coûte seulement. M0 s'est montré suffisant pour toutes les VAL à checklist + smoke. + +--- + +## 5. Erreurs d'affectation de rôle + +1. **VAL qui écrit la doc qu'elle valide** : S2-T4b (« A-VAL puis prep DOC »), S6 (VAL a fait la mise à jour de `20` / + `09` puis l'a validée), S7-T4 (« VAL+DOC », coche elle-même `21` § Acceptation). Pas d'erreur constatée, mais la + séparation des rôles perd son intérêt. +2. **Décision d'architecture prise par INT** : l'abandon du submodule (S3-T4) relevait d'A-ARCH ; il a été pris en + intégration, sans SPEC préalable, et documenté a posteriori dans `21`. +3. **Compilation sans propriétaire en S1** : ni IMPL-L ni VAL n'avaient le droit de compiler ; le trou a été comblé par + chance au lot suivant. +4. **Purge destructive confiée à IMPL-L / M0 (S3-T3)** : suppression de fichiers du jeu et modification du + `.uproject`, sans build dans la fiche. +5. **Orchestrateur évaluateur (S8-T1)** : le rôle qui a découpé les lots en fait le bilan ; voir le biais signalé en tête. + +--- + +## 6. Friction git + +- **Submodule abandonné → `AdditionalPluginDirectories`.** Le plan initial (S3-T3 : « jonctions temporaires, avant + submodule T4 ») supposait un submodule. L'addon étant un `uproject` qui contient `Plugins/LPSandbox/`, un submodule + monté sur `Plugins/LPSandbox` du jeu aurait donné `Plugins/LPSandbox/Plugins/LPSandbox/` (non scanné), et un + submodule `External/` aurait doublé le clone sibling. S3-T4 a retiré la junction et retenu deux clones voisins + + `AdditionalPluginDirectories` + pin consigné à la main (`S3-INT.md`, README). Coût : une junction créée puis + défaite, et un pin que rien n'automatise (4 bumps à la main : `4de1f0b`, `f810264`, `c1fdd29`, `f09c42e`). +- **Push.** S3-T1 a créé le remote sans pousser (consigne) ; premier push en S3-T4. S7-T2 a laissé `f09c42e` local et + le pin pointait un commit absent du remote jusqu'à la fin de S7. Constat S8 : `origin/main` = `dae6206` (contient + `f09c42e`), **l'addon est à jour sur GitHub**. Côté jeu, en revanche, **aucun commit depuis le début du lot** : le + dernier commit touchant `LifePlay.uproject` est antérieur (`b11f7e8`) ; `21`, `etat/sandbox/**`, + `Config/LPSandbox.ini` sont non suivis, `LifePlay.uproject` et les deux `Build.cs` modifiés non commités. Le pin + documenté n'est donc consommable par personne d'autre. +- **Identité git.** Les deux premiers commits de l'addon (`4938abb` S3-T1, `d18243e` S3-T2) portent + `LifePlay ` : identité non configurée dans le nouveau dépôt. Les suivants portent + `edujiro5972 `, alors que le dépôt jeu utilise + `336072401+edujiro5972@users.noreply.github.com`. Trois identités pour un seul auteur ; l'historique publié n'a pas + été réécrit (il faudrait un push forcé). + +--- + +## 7. Coût / bénéfice vs mono-agent mono-modèle + +**Coût mesurable.** +- 27 fiches (S0–S7) et 29 rapports, ~200 Ko de Markdown sous `etat/sandbox/` (S5 seul : 52 Ko) pour un changement net de + quelques milliers de lignes déplacées et −336 lignes dupliquées. +- Chaque agent relit `21` et les fiches amont : le contexte est rechargé ~28 fois. +- Trous de transmission que la continuité d'un seul agent aurait évités : S1 non compilé, junction S3 défaite, + `S1-CART` jamais mis à jour, commit jeu jamais fait. +- Traçabilité incomplète : 11 fiches sur 27 (S0–S7) indiquent le modèle réel. + +**Bénéfice mesurable.** +- 0 build cassé à l'étape IMPL-H, 0 retour arrière, 0 régression (parité de la boîte S5 = S7 = S8). +- Des VAL indépendantes qui ont refusé de sur-vendre : `BLOCKED-RUNTIME` en S1, `PASS-STATIC` en S5, réserve + `StreetMap` en S3. Un agent unique valide plus volontiers son propre travail. +- Les paliers bas (M0, flash) ont porté 5 fiches de VAL / CART sans défaut (S0-T1, S0-T3, S1-T3, S4-T4a, S6) ; un + mono-modèle haut de gamme aurait payé ces fiches au prix fort. + +**Verdict.** Pour une migration à risque (déplacement inter-dépôts, refonte de géométrie partagée), le surcoût +documentaire est justifié par l'absence de régression et la qualité des preuves. Pour des lots simples (S0, S6, S7-T2) +le découpage en 3–4 rôles est du surcoût pur : un seul agent M0/M2 avec une VAL séparée suffisait. + +--- + +## 8. Décision + +**GO conditionnel** pour réutiliser le mode multi-agents + multi-modèles sur les prochaines migrations de même nature +(extraction de code, refonte partagée jeu ↔ addon). Les trois conditions sont les recommandations ci-dessous ; sans +elles, les mêmes trous (build sans propriétaire, git non clos, traçabilité partielle) se reproduiront. +**NO-GO** pour les lots simples (≤ 1 fichier de config / doc, pas de C++) : un agent unique + une VAL M0. + +--- + +## 9. Recommandations (3) + +1. **Politique modèles, tracée et vérifiée.** M0 (`composer-2.5-fast`) par défaut pour les VAL à checklist + smoke et + les IMPL-L mécaniques non destructives ; `gemini-3.8-flash-high` pour les CART de type grep ; M2 + (`claude-sonnet-5-5-high`) pour les CART analytiques, les VAL qui doivent juger une parité ou un PASS-STATIC, **et + toute opération destructive ou inter-dépôts** (suppression de sources, `.uproject`) ; M3 + (`claude-opus-5-thinking-high`) réservé aux IMPL-H qui changent un comportement (type S5-T2), pas aux déplacements + (type S2-T2 → M2) ; M4 pour la synthèse transverse. Chaque fiche porte « Modèle prévu | utilisé » ; une VAL sans ce + champ dans les fiches amont est en `FAIL-TRACE`. +2. **Clôture git par lot, avec un propriétaire.** Toute fiche IMPL finit par un build vert (la compilation appartient à + IMPL, pas à VAL). Le lot ne passe au suivant qu'après un « gate » A-INT : commit + push de l'addon, bump du pin, + **commit du jeu** (fichiers non suivis inclus), identité git vérifiée (`git config user.email` identique dans les + deux dépôts). Prochaine action concrète : commiter côté jeu `LifePlay.uproject`, `Config/LPSandbox.ini`, les + `Build.cs`, `LPShowCoupleSubsystem.*`, `21`, `22` et `etat/sandbox/`. +3. **Séparer VAL et DOC, et tenir un registre des reports.** La VAL est en lecture seule sur les docs d'acceptation ; + un rôle DOC distinct coche `21`. Les décisions d'architecture passent par une SPEC avant l'INT. Chaque « reste à + faire » d'une fiche entre dans une liste unique de `21` avec un lot propriétaire, pour qu'aucun report ne reste + orphelin (aujourd'hui : `S1-CART`, override `WorldFile`, Show en jeu, plancher, commandlets, Shipping compilé). diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index eaa7621..eb304dc 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -4,24 +4,28 @@ > retour arrière. Les empreintes complètes sont dans `D:\Game\LifePlay_Backup_5.5\\manifest.csv` > et dans l'historique git. -| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | -|---|---|---|---|---|---|---| -| CHG-001 | 2026-09-29 | L0 | `.gitignore` | ignorer les fichiers générés (`Build/*/FileOpenOrder`, `Content/Collections`, `Content/Developers`, `.claude/settings.local.json`) | 31 → 38 lignes | retirer le bloc « CHG-001 » | -| CHG-002 | 2026-09-29 | L0 | `.gitattributes` (nouveau) | aucune conversion de fin de ligne : restauration git identique à l'octet | — → créé | supprimer le fichier | -| CHG-003 | 2026-09-29 | L0 | `.git/` (nouveau) | dépôt local, identité locale, `core.autocrlf=false`, `core.longpaths=true` ; commit `de64933`, tag `lot-L0-base` | — → créé | supprimer `.git/` | -| CHG-004 | 2026-09-29 | L0 | `Tools/Snapshot.ps1`, `Restore.ps1`, `AssetHeaders.ps1`, `CheckIntegrity.ps1`, `LogStats.ps1` (nouveaux) | outillage de traçabilité et de test | — → créés | `git rm` | -| CHG-005 | 2026-09-29 | L0 | `Docs/Migration/**` (nouveau) | documentation 01 à 04, journal, registre, empreintes `etat/L0_*` | — → créés | `git rm` | -| CHG-006 | 2026-09-29 | L0 | `D:\Game\LifePlay_Backup_5.5\L0\` (hors projet) | sauvegarde de `.claude`, `.mcp.json`, `LifePlay.uproject`, `Config`, `Saved/Backups`, `Saved/SaveGames`, `Content/BPs` (53 fichiers) | — → créé | sans objet | + +| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | +| ------- | ---------- | --- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------- | --------------------------- | +| CHG-001 | 2026-09-29 | L0 | `.gitignore` | ignorer les fichiers générés (`Build/*/FileOpenOrder`, `Content/Collections`, `Content/Developers`, `.claude/settings.local.json`) | 31 → 38 lignes | retirer le bloc « CHG-001 » | +| CHG-002 | 2026-09-29 | L0 | `.gitattributes` (nouveau) | aucune conversion de fin de ligne : restauration git identique à l'octet | — → créé | supprimer le fichier | +| CHG-003 | 2026-09-29 | L0 | `.git/` (nouveau) | dépôt local, identité locale, `core.autocrlf=false`, `core.longpaths=true` ; commit `de64933`, tag `lot-L0-base` | — → créé | supprimer `.git/` | +| CHG-004 | 2026-09-29 | L0 | `Tools/Snapshot.ps1`, `Restore.ps1`, `AssetHeaders.ps1`, `CheckIntegrity.ps1`, `LogStats.ps1` (nouveaux) | outillage de traçabilité et de test | — → créés | `git rm` | +| CHG-005 | 2026-09-29 | L0 | `Docs/Migration/**` (nouveau) | documentation 01 à 04, journal, registre, empreintes `etat/L0_*` | — → créés | `git rm` | +| CHG-006 | 2026-09-29 | L0 | `D:\Game\LifePlay_Backup_5.5\L0\` (hors projet) | sauvegarde de `.claude`, `.mcp.json`, `LifePlay.uproject`, `Config`, `Saved/Backups`, `Saved/SaveGames`, `Content/BPs` (53 fichiers) | — → créé | sans objet | + ## Vérifications du lot L0 -| Test | Résultat | -|---|---| -| Aller-retour `Snapshot` → modification → `Restore` (`Config/DefaultEditorSettings.ini` + fichier créé) | ✅ MD5 `85D82E7F…` retrouvé ; fichier créé retiré vers `_retires/` | -| T-STAT-01 | ⚠️ 1 package UE4 (`Content/BPs/GI.uasset`) : défaut connu, traité en L1 | -| T-STAT-02 Modules = 4.27 | ✅ 2 051 fichiers | -| T-STAT-03 environnement = L0 | ✅ 20 fichiers | -| T-STAT-04 arbre 4.27 inchangé | ✅ 144 fichiers (empreinte créée : `etat/L0_ref427.csv`) | + +| Test | Résultat | +| ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | +| Aller-retour `Snapshot` → modification → `Restore` (`Config/DefaultEditorSettings.ini` + fichier créé) | ✅ MD5 `85D82E7F…` retrouvé ; fichier créé retiré vers `_retires/` | +| T-STAT-01 | ⚠️ 1 package UE4 (`Content/BPs/GI.uasset`) : défaut connu, traité en L1 | +| T-STAT-02 Modules = 4.27 | ✅ 2 051 fichiers | +| T-STAT-03 environnement = L0 | ✅ 20 fichiers | +| T-STAT-04 arbre 4.27 inchangé | ✅ 144 fichiers (empreinte créée : `etat/L0_ref427.csv`) | + Aucun fichier du projet n'a été modifié en L0 en dehors de `.gitignore` et des ajouts ci-dessus. @@ -32,31 +36,37 @@ Aucun fichier du projet n'a été modifié en L0 en dehors de `.gitignore` et de Décisions utilisateur D-01 et D-02 : `04_Cahier_Des_Charges.md` §4. Tag d'entrée `lot-L1-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L1\` (387 fichiers). -| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | -|---|---|---|---|---|---|---| -| CHG-007 | 2026-09-29 | L1 | `Content/{BPs,UI,Structs,Scenes,Render,Characters}` (385 fichiers, 293 packages) | retour à la base 4.27 : retire tous les correctifs de contenu non suivis (dont R-03, R-04) | contenu 5.5 (empreintes `etat/L0_md5_*.csv`) → identique à 4.27 (MD5 vérifiés, 0 écart) | `Tools\Restore.ps1 -Lot L1` ou `git checkout lot-L1-avant -- Content` | -| CHG-008 | 2026-09-29 | L1 | `Tools/RunCommandlet.ps1` (nouveau) | lancement de commandlet avec journal archivé et résumé dédoublonné | — → créé | `git rm` | -| CHG-009 | 2026-09-29 | L1 | `Docs/Migration/05_Correctifs_Isoles.md`, `correctifs/P01…P06.patch` (nouveaux) | isolement des correctifs : catalogue, protocole de réimport, patchs de code 4.27 → 5.5 | — → créés | `git rm` | -| CHG-010 | 2026-09-29 | L1 | `Config/DefaultEngine.ini` | essai de retrait des sections `AndroidFileServer`/`CookerSettings` **annulé** : l'éditeur recrée la section à chaque lancement (nouveau `SecurityToken`) | inchangé (restauré depuis `lot-L1-avant`) | sans objet ; traité en L2 | + +| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | +| ------- | ---------- | --- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| CHG-007 | 2026-09-29 | L1 | `Content/{BPs,UI,Structs,Scenes,Render,Characters}` (385 fichiers, 293 packages) | retour à la base 4.27 : retire tous les correctifs de contenu non suivis (dont R-03, R-04) | contenu 5.5 (empreintes `etat/L0_md5_*.csv`) → identique à 4.27 (MD5 vérifiés, 0 écart) | `Tools\Restore.ps1 -Lot L1` ou `git checkout lot-L1-avant -- Content` | +| CHG-008 | 2026-09-29 | L1 | `Tools/RunCommandlet.ps1` (nouveau) | lancement de commandlet avec journal archivé et résumé dédoublonné | — → créé | `git rm` | +| CHG-009 | 2026-09-29 | L1 | `Docs/Migration/05_Correctifs_Isoles.md`, `correctifs/P01…P06.patch` (nouveaux) | isolement des correctifs : catalogue, protocole de réimport, patchs de code 4.27 → 5.5 | — → créés | `git rm` | +| CHG-010 | 2026-09-29 | L1 | `Config/DefaultEngine.ini` | essai de retrait des sections `AndroidFileServer`/`CookerSettings` **annulé** : l'éditeur recrée la section à chaque lancement (nouveau `SecurityToken`) | inchangé (restauré depuis `lot-L1-avant`) | sans objet ; traité en L2 | + ### Relevé de base : moteur 5.5 sur le contenu 4.27, sans aucun correctif -| Commandlet | Résultat | Résumé | -|---|---|---| + +| Commandlet | Résultat | Résumé | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `LPCheckMeshAssignment` | 5 nœuds à broches orphelines dans `GI` : 3 `GetAssetsByClass` (classe perdue : `SkeletalMesh`, `StaticMesh`, `StreetMap`), 2 `Break AssetData` (`ObjectPath`) ; 6 nœuds `EditableTextBoxStyle` (`StatMenu`, `CharGen`) | `etat/L1_LPCheckMeshAssignment_20260929-010821.txt` | -| `CompileAllBlueprints` | **6 erreurs, 100 avertissements, 0 Blueprint en échec de chargement** ; échecs : `UI/StatMenu` (3), `UI/CharGen` (3) — broche `Font` d'`EditableTextBoxStyle` | `etat/L1_CompileAllBlueprints_20260929-010931.txt` | +| `CompileAllBlueprints` | **6 erreurs, 100 avertissements, 0 Blueprint en échec de chargement** ; échecs : `UI/StatMenu` (3), `UI/CharGen` (3) — broche `Font` d'`EditableTextBoxStyle` | `etat/L1_CompileAllBlueprints_20260929-010931.txt` | + Les commandlets de contrôle n'ont rien écrit : contenu toujours identique à 4.27 après passage (385/385). ### Vérifications de sortie L1 -| Test | Résultat | -|---|---| -| Contenu de gameplay = 4.27 | ✅ 385/385 MD5 | -| T-STAT-01 | ⏳ 293 packages UE4 (attendu : base 4.27) — resave contrôlé en `L4-CX-B07` | -| T-STAT-02, 03, 04 | ✅ | -| T-LOAD-01 | ❌ attendu : 6 erreurs (`CX-B02`), à corriger en L4 | -| T-LOAD-02 | ❌ attendu : 5 broches (`CX-B01`), à corriger en L4 | + +| Test | Résultat | +| -------------------------- | ------------------------------------------------------------------------- | +| Contenu de gameplay = 4.27 | ✅ 385/385 MD5 | +| T-STAT-01 | ⏳ 293 packages UE4 (attendu : base 4.27) — resave contrôlé en `L4-CX-B07` | +| T-STAT-02, 03, 04 | ✅ | +| T-LOAD-01 | ❌ attendu : 6 erreurs (`CX-B02`), à corriger en L4 | +| T-LOAD-02 | ❌ attendu : 5 broches (`CX-B01`), à corriger en L4 | + --- @@ -65,22 +75,25 @@ Les commandlets de contrôle n'ont rien écrit : contenu toujours identique à 4 Tag d'entrée `lot-L2-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L2\` (`.uproject`, `DefaultEngine.ini`) ; dossiers mis de côté dans `L2\_purge\` (manifeste `L2\purge_manifest.csv`). -| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | -|---|---|---|---|---|---|---| -| CHG-011 | 2026-09-29 | L2 | `LifePlay.uproject` | `TargetPlatforms` `WindowsNoEditor` → `Windows` ; section `Plugins` explicite : `ModdingExtra`, `StreetMap`, `EnhancedInput` activés ; `McpAutomationBridge` limité à la cible `Editor` (`TargetAllowList`) ; `AndroidFileServer` désactivé | MD5 `DA10325E…` → nouveau | `Tools\Restore.ps1 -Lot L2 -Only LifePlay.uproject` | -| CHG-012 | 2026-09-29 | L2 | `Config/DefaultEngine.ini` | retrait de la section `AndroidFileServerRuntimeSettings` (et de son `SecurityToken`) ; le plugin étant désactivé, l'éditeur ne la recrée plus (vérifié après un lancement) | 14 lignes retirées | `Tools\Restore.ps1 -Lot L2 -Only Config\DefaultEngine.ini` | -| CHG-013 | 2026-09-29 | L2 | `Binaries`, `Intermediate`, `Saved/Cooked`, `Saved/StagedBuilds`, `Plugins/*/Binaries`, `Plugins/*/Intermediate`, `Build/WindowsNoEditor`, `Build/LinuxNoEditor` (≈ 13,9 Go) | reconstruction à partir de zéro ; **déplacés**, pas supprimés | → `D:\Game\LifePlay_Backup_5.5\L2\_purge\` | `Move-Item` inverse (voir `purge_manifest.csv`) ; suppression définitive sur décision utilisateur | + +| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | +| ------- | ---------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| CHG-011 | 2026-09-29 | L2 | `LifePlay.uproject` | `TargetPlatforms` `WindowsNoEditor` → `Windows` ; section `Plugins` explicite : `ModdingExtra`, `StreetMap`, `EnhancedInput` activés ; `McpAutomationBridge` limité à la cible `Editor` (`TargetAllowList`) ; `AndroidFileServer` désactivé | MD5 `DA10325E…` → nouveau | `Tools\Restore.ps1 -Lot L2 -Only LifePlay.uproject` | +| CHG-012 | 2026-09-29 | L2 | `Config/DefaultEngine.ini` | retrait de la section `AndroidFileServerRuntimeSettings` (et de son `SecurityToken`) ; le plugin étant désactivé, l'éditeur ne la recrée plus (vérifié après un lancement) | 14 lignes retirées | `Tools\Restore.ps1 -Lot L2 -Only Config\DefaultEngine.ini` | +| CHG-013 | 2026-09-29 | L2 | `Binaries`, `Intermediate`, `Saved/Cooked`, `Saved/StagedBuilds`, `Plugins/*/Binaries`, `Plugins/*/Intermediate`, `Build/WindowsNoEditor`, `Build/LinuxNoEditor` (≈ 13,9 Go) | reconstruction à partir de zéro ; **déplacés**, pas supprimés | → `D:\Game\LifePlay_Backup_5.5\L2\_purge\` | `Move-Item` inverse (voir `purge_manifest.csv`) ; suppression définitive sur décision utilisateur | + ### Vérifications de sortie L2 -| Test | Résultat | -|---|---| + +| Test | Résultat | +| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | T-BUILD-01 `LifePlayEditor Win64 Development` | ✅ code 0, 233 s, **0 erreur, 0 avertissement** dans le code du projet et de ses plugins (2 messages de configuration UBT propres à `McpAutomationBridge`) — `Saved/Logs/portage/L2_Build_LifePlayEditor_20260929-011800.log` | -| T-BUILD-02 `LifePlay Win64 Development` | ✅ code 0, 97 s, 0 erreur, 0 avertissement ; exécutable 300 Mo (328 Mo avant : `McpAutomationBridge` exclu du jeu) | -| T-LOAD-04 démarrage éditeur | ✅ 0 `LogClass: Error`, 0 échec de chargement de package ou de plugin | -| Non-régression `LPCheckMeshAssignment` | ✅ relevé identique à L1 (10 erreurs, 285 avertissements, mêmes signatures) | -| ERR-023 (`AndroidFileServer`) | ✅ `DefaultEngine.ini` non réécrit après lancement | -| T-STAT-02, 03, 04 | ✅ | +| T-BUILD-02 `LifePlay Win64 Development` | ✅ code 0, 97 s, 0 erreur, 0 avertissement ; exécutable 300 Mo (328 Mo avant : `McpAutomationBridge` exclu du jeu) | +| T-LOAD-04 démarrage éditeur | ✅ 0 `LogClass: Error`, 0 échec de chargement de package ou de plugin | +| Non-régression `LPCheckMeshAssignment` | ✅ relevé identique à L1 (10 erreurs, 285 avertissements, mêmes signatures) | +| ERR-023 (`AndroidFileServer`) | ✅ `DefaultEngine.ini` non réécrit après lancement | +| T-STAT-02, 03, 04 | ✅ | --- @@ -90,12 +103,14 @@ Tag d'entrée `lot-L2-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L2\` (`. Tag d'entrée `lot-L3-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L3\` (6 entrées, dont 2 fichiers créés que `Restore.ps1` retirerait). -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-014 | 2026-09-29 | L3 | `Source/LifePlay/LifePlay.h`, `LifePlay.cpp` | catégorie `LogLifePlay` | `Tools\Restore.ps1 -Lot L3` | -| CHG-015 | 2026-09-29 | L3 | `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` (nouveaux), `LifePlay.Build.cs` (+ `EngineSettings`) | sous-système de `GameInstance`, **sans modification de Blueprint** : résumé de démarrage (versions, modules, fichiers par type) ; bilan de session à la fermeture ou via la commande console `LifePlay.LogSummary` (messages par catégorie, 25 signatures d'avertissement/erreur les plus fréquentes, chiffres normalisés en `#`, pile d'appel Blueprint incluse, plafond de 4 000 signatures). Les messages du moteur ne sont **pas** modifiés (comparabilité A/B avec 4.27) | idem | -| CHG-016 | 2026-09-29 | L3 | `Plugins/ModdingExtra/.../ModdingExtraBPLibrary.cpp` | instrumentation `LogLPModdingTrace` (Warning) → `LogModdingExtra` (Verbose, même format de ligne) ; échec de `LoadTxt` signalé une seule fois par fichier ; échec de `SaveTxt` désormais signalé (il était silencieux) | idem | -| CHG-017 | 2026-09-29 | L3 | `Tools/LogStats.ps1`, `Docs/Migration/03_Plan_Migration_Tests.md` | prise en compte de `LogModdingExtra` ; procédure A/B (`-LogCmds="LogModdingExtra Verbose"`) | `git revert` | + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| CHG-014 | 2026-09-29 | L3 | `Source/LifePlay/LifePlay.h`, `LifePlay.cpp` | catégorie `LogLifePlay` | `Tools\Restore.ps1 -Lot L3` | +| CHG-015 | 2026-09-29 | L3 | `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` (nouveaux), `LifePlay.Build.cs` (+ `EngineSettings`) | sous-système de `GameInstance`, **sans modification de Blueprint** : résumé de démarrage (versions, modules, fichiers par type) ; bilan de session à la fermeture ou via la commande console `LifePlay.LogSummary` (messages par catégorie, 25 signatures d'avertissement/erreur les plus fréquentes, chiffres normalisés en `#`, pile d'appel Blueprint incluse, plafond de 4 000 signatures). Les messages du moteur ne sont **pas** modifiés (comparabilité A/B avec 4.27) | idem | +| CHG-016 | 2026-09-29 | L3 | `Plugins/ModdingExtra/.../ModdingExtraBPLibrary.cpp` | instrumentation `LogLPModdingTrace` (Warning) → `LogModdingExtra` (Verbose, même format de ligne) ; échec de `LoadTxt` signalé une seule fois par fichier ; échec de `SaveTxt` désormais signalé (il était silencieux) | idem | +| CHG-017 | 2026-09-29 | L3 | `Tools/LogStats.ps1`, `Docs/Migration/03_Plan_Migration_Tests.md` | prise en compte de `LogModdingExtra` ; procédure A/B (`-LogCmds="LogModdingExtra Verbose"`) | `git revert` | + ### Vérifications de sortie L3 @@ -103,20 +118,21 @@ Sessions `UnrealEditor -game` (valables pour le journal, pas pour l'affichage de règle §2 du document 03) : `Saved/Logs/portage/L3_game_20260929-012748.log` (niveau par défaut) et `L3_game_verbose_20260929-013032.log` (trace détaillée). -| Test | Résultat | -|---|---| -| T-BUILD-01, T-BUILD-02 | ✅ 0 erreur, 0 avertissement | -| T-LOG-01 résumé de démarrage | ✅ 20 lignes `LogLifePlay` ; 16 modules ; comptes par type identiques à l'inventaire 4.27 (`vin_Base` : 752 `.lpscene`, 286 `.lpaction`…) | -| T-LOG-01 bilan de session | ✅ écrit à la fermeture ; signatures dédoublonnées avec leur pile d'appel | -| Bruit d'instrumentation | ✅ `LogLPModdingTrace` : 0 ligne au niveau par défaut (1 300 à 5 400 avant) ; 58 lignes `LogLifePlay` pour toute la session | -| Trace détaillée | ✅ 1 284 lignes `LogModdingExtra` avec `-LogCmds` ; `ModLauncher.config` absent signalé **une** fois | + +| Test | Résultat | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| T-BUILD-01, T-BUILD-02 | ✅ 0 erreur, 0 avertissement | +| T-LOG-01 résumé de démarrage | ✅ 20 lignes `LogLifePlay` ; 16 modules ; comptes par type identiques à l'inventaire 4.27 (`vin_Base` : 752 `.lpscene`, 286 `.lpaction`…) | +| T-LOG-01 bilan de session | ✅ écrit à la fermeture ; signatures dédoublonnées avec leur pile d'appel | +| Bruit d'instrumentation | ✅ `LogLPModdingTrace` : 0 ligne au niveau par défaut (1 300 à 5 400 avant) ; 58 lignes `LogLifePlay` pour toute la session | +| Trace détaillée | ✅ 1 284 lignes `LogModdingExtra` avec `-LogCmds` ; `ModLauncher.config` absent signalé **une** fois | | A/B de format avec la build 4.27 | ✅ `LogStats -Compare` lit les deux journaux ; toutes les énumérations communes aux deux sessions sont identiques (écarts limités aux dossiers que seule l'une des sessions a atteints) | -| T-STAT-02, 03, 04 | ✅ | +| T-STAT-02, 03, 04 | ✅ | + Découverte au passage : les erreurs `ParseIntoArray` (ERR-004) viennent de `GI.populateClothes4`, appelée par `Person.ReceiveBeginPlay`. - --- ## Lot L4 — Réimport contrôlé des correctifs (2026-09-29) @@ -125,42 +141,50 @@ appelée par `Person.ReceiveBeginPlay`. Tag d'entrée `L4-CX-B01-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L4-CX-B01\` (`GI.uasset`, MD5 `F4550DA5…`). -| Étape du protocole | Résultat | Preuve (`Docs/Migration/etat/`) | -|---|---|---| -| 2. Photographie avant | `GI::EventGraph` : 6 199 nœuds, 23 662 lignes | `L4-CX-B01_dump_avant.txt` | -| 3. Simulation `-DryRun` | 3 `[enum]` (`StreetMap`, `StaticMesh`, `SkeletalMesh`), 2 `[path]` (1 consommateur chacun), 0 non traité — **conforme à l'attendu** | `L4-CX-B01_dryrun.txt` | -| 4. Application | 3 énumérations, 2 chemins, 1 Blueprint enregistré, 0 cas non traité, 0 échec | `L4-CX-B01_application.txt` | -| 5. Photographie après + diff | 6 201 nœuds ; **35 lignes différentes, toutes attendues** : 3 nœuds `AssetRegistry::GetAssetsByClass` → `LPAssetRegistryHelpers::GetAssetsByClassName` avec leur classe d'origine ; 2 nœuds `GetAssetObjectPath` insérés, reprenant les consommateurs des sorties `ObjectPath` orphelines. Aucune autre ligne du graphe ne change | `L4-CX-B01_dump_apres.txt`, `L4-CX-B01_diff.txt` | -| 6. `LPCheckMeshAssignment` | 0 broche orpheline dans `GI` (restent les 6 nœuds de CX-B02) | `L4-CX-B01_LPCheckMeshAssignment_*.txt` | -| 6. `CompileAllBlueprints` | identique au relevé de base : 6 erreurs (CX-B02), 100 avertissements, 0 échec de chargement ; **aucune erreur nouvelle** | `L4-CX-B01_CompileAllBlueprints_*.txt` | -| 7. A/B d'exécution | **en attente** : la cuisson exige CX-B02 ; sera faite sur la build Development qui suit CX-B02 | — | -| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | -|---|---|---|---|---|---|---| +| Étape du protocole | Résultat | Preuve (`Docs/Migration/etat/`) | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| 2. Photographie avant | `GI::EventGraph` : 6 199 nœuds, 23 662 lignes | `L4-CX-B01_dump_avant.txt` | +| 3. Simulation `-DryRun` | 3 `[enum]` (`StreetMap`, `StaticMesh`, `SkeletalMesh`), 2 `[path]` (1 consommateur chacun), 0 non traité — **conforme à l'attendu** | `L4-CX-B01_dryrun.txt` | +| 4. Application | 3 énumérations, 2 chemins, 1 Blueprint enregistré, 0 cas non traité, 0 échec | `L4-CX-B01_application.txt` | +| 5. Photographie après + diff | 6 201 nœuds ; **35 lignes différentes, toutes attendues** : 3 nœuds `AssetRegistry::GetAssetsByClass` → `LPAssetRegistryHelpers::GetAssetsByClassName` avec leur classe d'origine ; 2 nœuds `GetAssetObjectPath` insérés, reprenant les consommateurs des sorties `ObjectPath` orphelines. Aucune autre ligne du graphe ne change | `L4-CX-B01_dump_apres.txt`, `L4-CX-B01_diff.txt` | +| 6. `LPCheckMeshAssignment` | 0 broche orpheline dans `GI` (restent les 6 nœuds de CX-B02) | `L4-CX-B01_LPCheckMeshAssignment_*.txt` | +| 6. `CompileAllBlueprints` | identique au relevé de base : 6 erreurs (CX-B02), 100 avertissements, 0 échec de chargement ; **aucune erreur nouvelle** | `L4-CX-B01_CompileAllBlueprints_*.txt` | +| 7. A/B d'exécution | **en attente** : la cuisson exige CX-B02 ; sera faite sur la build Development qui suit CX-B02 | — | + + + +| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | +| ------- | ---------- | --------- | ----------------------- | --------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | CHG-018 | 2026-09-29 | L4-CX-B01 | `Content/BPs/GI.uasset` | réimport de CX-B01 (`LPFixAssetRegistryPins`) | MD5 `F4550DA5…` (UE4) → `0AA14EDB…` (UE5, réenregistré par la commandlet) | `Tools\Restore.ps1 -Lot L4-CX-B01` ou `git checkout L4-CX-B01-avant -- Content/BPs/GI.uasset` | + ### Sous-lot L4-CX-B02 — `EditableTextBoxStyle` (ERR-021) : essai 1 **rejeté** Tag d'entrée `L4-CX-B02-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L4-CX-B02\`. Preuves : `Docs/Migration/etat/L4-CX-B02_essai1_rejete/`. -| Étape | Résultat | -|---|---| -| Simulation | 6 nœuds sur 6 attendus | -| Application | « 6 traités, 0 liaison non gérée, 0 échec » | -| Compilation | 0 erreur (6 avant), 98 avertissements, 0 broche orpheline | + +| Étape | Résultat | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Simulation | 6 nœuds sur 6 attendus | +| Application | « 6 traités, 0 liaison non gérée, 0 échec » | +| Compilation | 0 erreur (6 avant), 98 avertissements, 0 broche orpheline | | **Relecture du diff** | ❌ `CharGen::Make_64` a perdu sa liaison `Font` **sans** recevoir la liaison `TextStyle` : sa police retombe sur la valeur par défaut (aucune police, couleur magenta). Défaut **silencieux** : compilation et rapport de la commandlet au vert | + Cause : `LPFixEditableTextBoxFont` coupait la broche `Font` de chaque nœud au fil du parcours. Dans `CharGen`, `Break_62` est parcouru avant `Make_64` : la coupure effaçait aussi la liaison vue depuis `Make_64`, qui n'était plus rebranché. Dépend de l'ordre des nœuds (sans effet dans `StatMenu`). La campagne du 2026-09-07 avait utilisé cette commandlet : le défaut y était très probablement présent. -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-019 | 2026-09-29 | L4-CX-B02 | `Content/UI/StatMenu.uasset`, `CharGen.uasset` | essai 1 appliqué puis **annulé** par `Tools\Restore.ps1 -Lot L4-CX-B02` (MD5 `34807BE0…`, `63CA2A78…` retrouvés) | sans objet | -| CHG-020 | 2026-09-29 | L4-CX-B02 | `Source/LifePlayEditorTools/LPFixEditableTextBoxFontCommandlet.cpp` | les broches `Font` ne sont coupées qu'après le relevé complet de la topologie | `git revert` | -| CHG-021 | 2026-09-29 | L4 | `Tools/ReimportCorrectif.ps1` (nouveau) | automatisation des étapes 1 à 6 du protocole | `git rm` | + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------- | +| CHG-019 | 2026-09-29 | L4-CX-B02 | `Content/UI/StatMenu.uasset`, `CharGen.uasset` | essai 1 appliqué puis **annulé** par `Tools\Restore.ps1 -Lot L4-CX-B02` (MD5 `34807BE0…`, `63CA2A78…` retrouvés) | sans objet | +| CHG-020 | 2026-09-29 | L4-CX-B02 | `Source/LifePlayEditorTools/LPFixEditableTextBoxFontCommandlet.cpp` | les broches `Font` ne sont coupées qu'après le relevé complet de la topologie | `git revert` | +| CHG-021 | 2026-09-29 | L4 | `Tools/ReimportCorrectif.ps1` (nouveau) | automatisation des étapes 1 à 6 du protocole | `git rm` | + Enseignement pour le protocole : l'étape 5 (relecture du diff nœud par nœud) est indispensable ; ni la compilation ni le rapport de la commandlet n'ont détecté ce défaut. @@ -170,18 +194,22 @@ compilation ni le rapport de la commandlet n'ont détecté ce défaut. Tag d'entrée `L4-CX-B02b-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L4-CX-B02b\`. Exécuté avec `Tools\ReimportCorrectif.ps1` et la commandlet corrigée (CHG-020). -| Étape | Résultat | Preuve (`etat/`) | -|---|---|---| -| 3. Simulation | 6 nœuds sur 6 attendus | `L4-CX-B02b_dryrun.txt` | -| 4. Application | 6 traités, 2 Blueprints, 0 liaison non gérée, 0 échec ; **4 rebranchements `TextStyle`** (2 par Blueprint) | `L4-CX-B02b_application.txt` | -| 5. Diff | `StatMenu` 425 → 422 lignes, `CharGen` 4 458 → 4 455 ; 9 lignes chacun, symétriques ; les 4 `Make` ont `TextStyle ← Break_62.TextStyle` ; plus aucune broche `Font` orpheline | `L4-CX-B02b_diff_*.txt`, `L4-CX-B02b_dump_*` | -| 6. `LPCheckMeshAssignment` | **0 broche orpheline dans tout le projet** | `L4-CX-B02b_LPCheckMeshAssignment_*.txt` | -| 6. `CompileAllBlueprints` | **0 erreur** (6 avant), 98 avertissements (100 avant : les 2 « Font obsolète » disparaissent), 0 échec de chargement | `L4-CX-B02b_CompileAllBlueprints_*.txt` | -| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | -|---|---|---|---|---|---|---| +| Étape | Résultat | Preuve (`etat/`) | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| 3. Simulation | 6 nœuds sur 6 attendus | `L4-CX-B02b_dryrun.txt` | +| 4. Application | 6 traités, 2 Blueprints, 0 liaison non gérée, 0 échec ; **4 rebranchements** `TextStyle` (2 par Blueprint) | `L4-CX-B02b_application.txt` | +| 5. Diff | `StatMenu` 425 → 422 lignes, `CharGen` 4 458 → 4 455 ; 9 lignes chacun, symétriques ; les 4 `Make` ont `TextStyle ← Break_62.TextStyle` ; plus aucune broche `Font` orpheline | `L4-CX-B02b_diff_*.txt`, `L4-CX-B02b_dump_*` | +| 6. `LPCheckMeshAssignment` | **0 broche orpheline dans tout le projet** | `L4-CX-B02b_LPCheckMeshAssignment_*.txt` | +| 6. `CompileAllBlueprints` | **0 erreur** (6 avant), 98 avertissements (100 avant : les 2 « Font obsolète » disparaissent), 0 échec de chargement | `L4-CX-B02b_CompileAllBlueprints_*.txt` | + + + +| ID | Date | Lot | Fichiers | Raison | Avant → après | Retour arrière | +| ------- | ---------- | ---------- | --------------------------------------------------------- | ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------ | | CHG-022 | 2026-09-29 | L4-CX-B02b | `Content/UI/StatMenu.uasset`, `Content/UI/CharGen.uasset` | réimport de CX-B02 | `34807BE0…` → `131E4B96…` ; `63CA2A78…` → `8D4E4200…` | `Tools\Restore.ps1 -Lot L4-CX-B02b` ou `git checkout L4-CX-B02b-avant -- Content/UI` | + **Effet sur les tests** : T-LOAD-01 (0 erreur de compilation) et T-LOAD-02 (0 broche orpheline) passent pour la première fois depuis le retour à la base saine. @@ -191,70 +219,83 @@ Tag d'entrée `L4-CX-B03-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L4-CX MD5 `C063EE72…` = 4.27, `CentralLondon_BuiltData.uasset`). Correctif sur une propriété d'acteur : le contrôle se fait par rapport de la commandlet avant/après (pas de graphe). -| Étape | Résultat | Preuve (`etat/`) | -|---|---|---| -| Rapport avant | 1 composant : `Wall.StaticMeshComponent0`, échelle X=0 Y=12,56 Z=5,75, cube moteur visible, `BlockAll` | `L4-CX-B03_rapport_avant.txt` | -| Application `-Fix -Scale=0.1` | X 0 → 0,1 (panneau de 10 cm : corps physique valide, aspect de mur plat conservé) ; 1 carte enregistrée, 0 échec | `L4-CX-B03_application.txt` | -| Rapport après | 0 composant à échelle nulle | `L4-CX-B03_rapport_apres.txt` | -| Non-régression | `LPCheckMeshAssignment` 0 broche orpheline ; `CompileAllBlueprints` 0 erreur, 0 échec de chargement ; seule la carte a changé (`_BuiltData` inchangé) | résumés `L4-CX-B03_*` | -| ID | Date | Lot | Fichiers | Avant → après | Retour arrière | -|---|---|---|---|---|---| +| Étape | Résultat | Preuve (`etat/`) | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | +| Rapport avant | 1 composant : `Wall.StaticMeshComponent0`, échelle X=0 Y=12,56 Z=5,75, cube moteur visible, `BlockAll` | `L4-CX-B03_rapport_avant.txt` | +| Application `-Fix -Scale=0.1` | X 0 → 0,1 (panneau de 10 cm : corps physique valide, aspect de mur plat conservé) ; 1 carte enregistrée, 0 échec | `L4-CX-B03_application.txt` | +| Rapport après | 0 composant à échelle nulle | `L4-CX-B03_rapport_apres.txt` | +| Non-régression | `LPCheckMeshAssignment` 0 broche orpheline ; `CompileAllBlueprints` 0 erreur, 0 échec de chargement ; seule la carte a changé (`_BuiltData` inchangé) | résumés `L4-CX-B03_*` | + + + +| ID | Date | Lot | Fichiers | Avant → après | Retour arrière | +| ------- | ---------- | --------- | ----------------------------------- | ------------------------- | -------------------------------------------------------------------------------------- | | CHG-023 | 2026-09-29 | L4-CX-B03 | `Content/Scenes/CentralLondon.umap` | `C063EE72…` → `21912547…` | `Tools\Restore.ps1 -Lot L4-CX-B03` ou `git checkout L4-CX-B03-avant -- Content/Scenes` | + ### Sous-lot L4-CX-B04 — événements `On Clicked` orphelins (validé) Tag d'entrée `L4-CX-B04-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L4-CX-B04\`. `Tools\ReimportCorrectif.ps1`. -| Étape | Résultat | Preuve (`etat/`) | -|---|---|---| -| Simulation | 3 sur 3 attendus | `L4-CX-B04_dryrun.txt` | -| Application | `TopRight` : `StatButtonL` (1 nœud en aval) ; `CharGen` : `ButtonEyelashColor` (5), `ButtonPreviewErect` (1) ; 3 retirés, 0 échec | `L4-CX-B04_application.txt` | -| Diff | `TopRight` 907 → 906 nœuds, `CharGen` 1 320 → 1 318 : **seuls les 3 événements disparaissent**. Les nœuds en aval sont conservés (déjà inatteignables ; la photographie n'inclut pas les broches d'exécution) | `L4-CX-B04_diff_*.txt` | -| Contrôles | 0 broche orpheline ; `CompileAllBlueprints` 0 erreur, **95** avertissements (98 avant), 0 échec de chargement | résumés `L4-CX-B04_*` | -| ID | Date | Lot | Fichiers | Avant → après | Retour arrière | -|---|---|---|---|---|---| +| Étape | Résultat | Preuve (`etat/`) | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| Simulation | 3 sur 3 attendus | `L4-CX-B04_dryrun.txt` | +| Application | `TopRight` : `StatButtonL` (1 nœud en aval) ; `CharGen` : `ButtonEyelashColor` (5), `ButtonPreviewErect` (1) ; 3 retirés, 0 échec | `L4-CX-B04_application.txt` | +| Diff | `TopRight` 907 → 906 nœuds, `CharGen` 1 320 → 1 318 : **seuls les 3 événements disparaissent**. Les nœuds en aval sont conservés (déjà inatteignables ; la photographie n'inclut pas les broches d'exécution) | `L4-CX-B04_diff_*.txt` | +| Contrôles | 0 broche orpheline ; `CompileAllBlueprints` 0 erreur, **95** avertissements (98 avant), 0 échec de chargement | résumés `L4-CX-B04_`* | + + + +| ID | Date | Lot | Fichiers | Avant → après | Retour arrière | +| ------- | ---------- | --------- | --------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------- | | CHG-024 | 2026-09-29 | L4-CX-B04 | `Content/UI/TopRight.uasset`, `Content/UI/CharGen.uasset` | `C34F7BB0…` → `B3B0ED10…` ; `8D4E4200…` → `A7C718C6…` | `Tools\Restore.ps1 -Lot L4-CX-B04` ou `git checkout L4-CX-B04-avant -- Content/UI` | + ### Build Development empaquetée `D:\Game\LifePlay_Build_5.5_L4` (2026-09-29) -État du code : `L4-CX-B04-apres`. Commande : `RunUAT BuildCookRun -platform=Win64 -clientconfig=Development --build -cook -stage -archive` (journal `Saved/Logs/portage/L4_BuildCookRun_20260929-030445.log`). +État du code : `L4-CX-B04-apres`. Commande : `RunUAT BuildCookRun -platform=Win64 -clientconfig=Development -build -cook -stage -archive` (journal `Saved/Logs/portage/L4_BuildCookRun_20260929-030445.log`). + + +| Test | Résultat | +| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| T-LOAD-05 cuisson | ✅ `Success - 0 error(s), 94 warning(s)` ; `BUILD SUCCESSFUL`, `ExitCode=0` | +| Données livrées en fichiers nus | ✅ `Windows/LifePlay/Content/Modules` présent ; résumé de démarrage : 16 modules, comptes identiques à 4.27 | +| Test de fumée (lancement automatisé) | ✅ 185 s sans plantage, fermeture propre, bilan de session écrit ; menu principal atteint (journal `Saved/Logs/portage/L4_build_smoke_20260929.log`) | +| T-RUN-02 à T-RUN-05 (génération de personnages, scène, sauvegarde, 30 min) | ⏳ **nécessite une partie jouée** : scénario `03_Plan_Migration_Tests.md` §4, sur cette build **et** sur la référence 4.27 | -| Test | Résultat | -|---|---| -| T-LOAD-05 cuisson | ✅ `Success - 0 error(s), 94 warning(s)` ; `BUILD SUCCESSFUL`, `ExitCode=0` | -| Données livrées en fichiers nus | ✅ `Windows/LifePlay/Content/Modules` présent ; résumé de démarrage : 16 modules, comptes identiques à 4.27 | -| Test de fumée (lancement automatisé) | ✅ 185 s sans plantage, fermeture propre, bilan de session écrit ; menu principal atteint (journal `Saved/Logs/portage/L4_build_smoke_20260929.log`) | -| T-RUN-02 à T-RUN-05 (génération de personnages, scène, sauvegarde, 30 min) | ⏳ **nécessite une partie jouée** : scénario `03_Plan_Migration_Tests.md` §4, sur cette build **et** sur la référence 4.27 | Note : le `LifePlay.exe` à la racine de la build est un lanceur ; le processus du jeu est `Windows/LifePlay/Binaries/Win64/LifePlay.exe`. ### Correction de l'empaquetage — ERR-026 (2026-09-29) -**Retour utilisateur sur `LifePlay_Build_5.5_L4`** : « le jeu ne fonctionne pas, la plupart des assets ne sont +**Retour utilisateur sur** `LifePlay_Build_5.5_L4` : « le jeu ne fonctionne pas, la plupart des assets ne sont pas intégrés ». Confirmé : **3 257 packages cuits au lieu de 8 561**. LifePlay charge maillages, vêtements, coiffures et textures par chemin à l'exécution (`GetAssetsByClass`, `GetAssetByObjectPath`) ; le cuiseur, qui ne suit que les références statiques, ne les voit pas. Les builds du 28/09 utilisaient `-CookAll` ; ce paramètre n'avait pas été repris. **Erreur de ma part, non détectée faute de test du volume cuit** : le test T-PKG-01 est ajouté au plan de tests. -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-025 | 2026-09-29 | L4 | `Config/DefaultGame.ini` | `+DirectoriesToAlwaysCook=(Path="/Game")` (exclusion `fBlah` conservée) : tout le contenu est cuit quelle que soit la méthode d'empaquetage, y compris le bouton « Package » de l'éditeur | `Tools\Restore.ps1 -Lot L4-cook` | -| CHG-026 | 2026-09-29 | L4 | `Docs/Migration/03_Plan_Migration_Tests.md`, `Registre_Erreurs.md` | test T-PKG-01 ; ERR-026 | `git revert` | + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | +| CHG-025 | 2026-09-29 | L4 | `Config/DefaultGame.ini` | `+DirectoriesToAlwaysCook=(Path="/Game")` (exclusion `fBlah` conservée) : tout le contenu est cuit quelle que soit la méthode d'empaquetage, y compris le bouton « Package » de l'éditeur | `Tools\Restore.ps1 -Lot L4-cook` | +| CHG-026 | 2026-09-29 | L4 | `Docs/Migration/03_Plan_Migration_Tests.md`, `Registre_Erreurs.md` | test T-PKG-01 ; ERR-026 | `git revert` | + Build `D:\Game\LifePlay_Build_5.5_L4b` (`BuildCookRun` Development, journal `L4b_BuildCookRun_20260929-081054.log`) : -| Test | Résultat | -|---|---| -| T-LOAD-05 cuisson | ✅ `Success - 0 error(s), 95 warning(s)` | -| T-PKG-01 contenu cuit | ✅ **8 561** `.uasset`, 2,6 Go (= référence 4.27) | -| Test de fumée (150 s, sans intervention) | ✅ aucun plantage, fermeture propre ; une partie a démarré et 4 personnages ont été générés | + +| Test | Résultat | +| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| T-LOAD-05 cuisson | ✅ `Success - 0 error(s), 95 warning(s)` | +| T-PKG-01 contenu cuit | ✅ **8 561** `.uasset`, 2,6 Go (= référence 4.27) | +| Test de fumée (150 s, sans intervention) | ✅ aucun plantage, fermeture propre ; une partie a démarré et 4 personnages ont été générés | | Premier A/B (`LogStats -Compare`, échantillon faible) | `Accessed None` maillage/perso **0,50** (4.27 : 0,27 ; 5.5 du 28/09 : 16,6) ; **4 acteurs distincts sur 4** (5.5 du 28/09 : tous `Person_C_0`) → CX-B01 corrige très probablement ERR-002 ; **à confirmer visuellement** (T-RUN-02 à 04) | + `LifePlay_Build_5.5_L4` (incomplète) est conservée comme preuve et peut être supprimée. --- @@ -267,34 +308,40 @@ Partie jouée par l'utilisateur sur `LifePlay_Build_5.5_L4b` (journal archivé `Saved/Logs/portage/L4b_partie_utilisateur_20260929.log`) : PNJ aléatoires corrects ; personnages créés par preset invisibles en dialogue, dévêtus dans le mini-jeu. -| Étape | Constat | Preuve | -|---|---|---| -| Indicateur | `Accessed None` maillage : 26,5 / personnage (1 219 / 46) contre 0,27 en 4.27 (4 / 15) | `Tools\LogStats.ps1` | -| Pile d'appel | `GeneratePerson → LoadPreset → GenerateAndApplyStrings/Bools` lit `MeshSexRace.Mesh` et `SMBool.SkeletalMesh` vides ; 0 occurrence en 4.27 | journal utilisateur | -| Pistes écartées (identiques en 4.27) | `clearVariable` (ERR-028, 211 contre ≈ même nombre en 4.27) ; `populateClothes4` / `ParseIntoArray` (631 contre 630) | `Grep` sur les deux journaux | -| Structures | `Mesh_2…` ∈ `Structs/MeshSexRace` ; `SkeletalMesh_2…` ∈ `Structs/SMBool` | recherche binaire | -| Diff de graphes 4.27 / 5.5 (`LPDumpFunction`) | `GenerateAndApplyStrings`, `GenerateAndApplyBools`, `LoadMeshIfNeeded`, `getSkeletalMeshFromPath`, `SMBoolToSkeletalMesh` : **logique identique** (seuls écarts : `SetSkeletalMesh` → `SetSkinnedAssetAndUpdate`, redirection moteur ; broche `Key` du `PrintString` UE5) | `etat/L5-diag_dump427_*`, `etat/L5-diag_dump55_*` | -| Mécanisme | `MeshSexRace` est chargé à la demande : `LoadMeshIfNeeded(Index)` → `getSkeletalMeshFromPath(AssetPath)` → `LPAssetRegistryHelpers::GetAssetByObjectPath(…, OnlyOnDisk=true)` → `GetAsset` → `Cast` | dumps | -| Réglages | `LegacyPercentage` absent des deux `GameSettings.sav` : valeur par défaut identique | lecture binaire | + +| Étape | Constat | Preuve | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| Indicateur | `Accessed None` maillage : 26,5 / personnage (1 219 / 46) contre 0,27 en 4.27 (4 / 15) | `Tools\LogStats.ps1` | +| Pile d'appel | `GeneratePerson → LoadPreset → GenerateAndApplyStrings/Bools` lit `MeshSexRace.Mesh` et `SMBool.SkeletalMesh` vides ; 0 occurrence en 4.27 | journal utilisateur | +| Pistes écartées (identiques en 4.27) | `clearVariable` (ERR-028, 211 contre ≈ même nombre en 4.27) ; `populateClothes4` / `ParseIntoArray` (631 contre 630) | `Grep` sur les deux journaux | +| Structures | `Mesh_2…` ∈ `Structs/MeshSexRace` ; `SkeletalMesh_2…` ∈ `Structs/SMBool` | recherche binaire | +| Diff de graphes 4.27 / 5.5 (`LPDumpFunction`) | `GenerateAndApplyStrings`, `GenerateAndApplyBools`, `LoadMeshIfNeeded`, `getSkeletalMeshFromPath`, `SMBoolToSkeletalMesh` : **logique identique** (seuls écarts : `SetSkeletalMesh` → `SetSkinnedAssetAndUpdate`, redirection moteur ; broche `Key` du `PrintString` UE5) | `etat/L5-diag_dump427_`*, `etat/L5-diag_dump55_*` | +| Mécanisme | `MeshSexRace` est chargé à la demande : `LoadMeshIfNeeded(Index)` → `getSkeletalMeshFromPath(AssetPath)` → `LPAssetRegistryHelpers::GetAssetByObjectPath(…, OnlyOnDisk=true)` → `GetAsset` → `Cast` | dumps | +| Réglages | `LegacyPercentage` absent des deux `GameSettings.sav` : valeur par défaut identique | lecture binaire | + Conclusion provisoire : code et réglages identiques ; le seul maillon qui change de moteur est la résolution chemin → asset (`GetAssetByObjectPath`, porté de `FName` vers `FSoftObjectPath`). -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-027 | 2026-09-29 | L5 | `Source/LifePlay/LPAssetRegistryHelpers.cpp` | instrumentation permanente : tout chemin non résolu est signalé une fois (`LogLifePlay` Warning, chemin reçu et chemin interprété), réussites en `Verbose` | `Tools\Restore.ps1 -Lot L5-instr` | + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| CHG-027 | 2026-09-29 | L5 | `Source/LifePlay/LPAssetRegistryHelpers.cpp` | instrumentation permanente : tout chemin non résolu est signalé une fois (`LogLifePlay` Warning, chemin reçu et chemin interprété), réussites en `Verbose` | `Tools\Restore.ps1 -Lot L5-instr` | + Build `D:\Game\LifePlay_Build_5.5_L5a` (instrumentée) : éditeur et jeu compilés sans erreur ; cuisson `0 error(s), 0 warning(s)` ; T-PKG-01 : 8 561 packages ; test de fumée 180 s sans plantage (aucune partie démarrée : pas de sauvegarde dans cette build). En attente d'une scène de quête jouée. ### Relevé `MeshSexRace` pendant une partie (build L5b, `-LPDumpVars`, 2026-09-29) -| Constat | Preuve | -|---|---| -| Fin de partie : `MeshSexRace[3]` = `bridget_gp`, `MeshSexRace[4]` = `michael` **chargés** ; les 22 autres corps vides (non utilisés ; 6 corps legacy jamais chargés car `LegacyPercentage = 0`) | `[dump fin]`, `Saved/Logs/portage/L5b_partie_utilisateur_20260929.log` | -| 4 PNJ aléatoires : 0 erreur. Chaque personnage de quête : ≈ 38 lectures de `Mesh` vide | chronologie du journal | -| Pile : `LoadPreset → GenerateAndApplyStrings → PlayerChangeRace → UpdatePlayerMeshWithMorphs → UpdatePlayerMesh` (boucle sur `MeshSexRace`, lecture de `Mesh.Materials`) | idem | -| Hypothèse « résolution de chemin » : **écartée** (seuls `Pubes05/06`, faux en 4.27 aussi) | instrumentation CHG-027 | -| **Base de comparaison invalide** : la session 4.27 de référence n'a chargé **aucun** preset de quête (`GenerateAndApplyStrings` ×2). Le « 0 occurrence en 4.27 » ne prouve rien (piège §5.3 du rapport du 28/09). Journal archivé : `Saved/Logs/portage/REF427_session_20260928_sans_quete.log` | `Grep` | + +| Constat | Preuve | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| Fin de partie : `MeshSexRace[3]` = `bridget_gp`, `MeshSexRace[4]` = `michael` **chargés** ; les 22 autres corps vides (non utilisés ; 6 corps legacy jamais chargés car `LegacyPercentage = 0`) | `[dump fin]`, `Saved/Logs/portage/L5b_partie_utilisateur_20260929.log` | +| 4 PNJ aléatoires : 0 erreur. Chaque personnage de quête : ≈ 38 lectures de `Mesh` vide | chronologie du journal | +| Pile : `LoadPreset → GenerateAndApplyStrings → PlayerChangeRace → UpdatePlayerMeshWithMorphs → UpdatePlayerMesh` (boucle sur `MeshSexRace`, lecture de `Mesh.Materials`) | idem | +| Hypothèse « résolution de chemin » : **écartée** (seuls `Pubes05/06`, faux en 4.27 aussi) | instrumentation CHG-027 | +| **Base de comparaison invalide** : la session 4.27 de référence n'a chargé **aucun** preset de quête (`GenerateAndApplyStrings` ×2). Le « 0 occurrence en 4.27 » ne prouve rien (piège §5.3 du rapport du 28/09). Journal archivé : `Saved/Logs/portage/REF427_session_20260928_sans_quete.log` | `Grep` | + Suite : session 4.27 jouant les mêmes scènes de quête, puis comparaison normalisée. @@ -307,16 +354,16 @@ Note de traçabilité : la sauvegarde `L5-instr` de `LPSessionLogSubsystem.*` a Chaîne causale, établie par le capteur L5c et le code source des deux moteurs : 1. La scène `vin_Pregnancy/Scenes/pregnant_trap.lpscene` appelle `generatePersonTemporary()` **sans argument** - (personnage aléatoire). + (personnage aléatoire). 2. Les arguments `()` sont découpés par `ParseIntoArray` (non culling). Sur une **chaîne vide** : - - UE 4.27 (`String.cpp:947`, garde `*Start != '\0'`) → **0 élément** ; - - UE 5.5 (`String.cpp.inl:1306`, réécrit sur `UE::String::ParseTokens`, qui visite toujours le dernier - jeton) → **1 élément vide `[""]`**. + - UE 4.27 (`String.cpp:947`, garde `*Start != '\0'`) → **0 élément** ; + - UE 5.5 (`String.cpp.inl:1306`, réécrit sur `UE::String::ParseTokens`, qui visite toujours le dernier + jeton) → **1 élément vide** `[""]`. 3. `GeneratePerson` teste `Length(Presets) > 0` : en 5.5, il prend le chemin « presets » au lieu du chemin aléatoire. 4. Il charge chaque `.lpcharacter` dont le nom se termine par `"" + ".lpcharacter"` : **les 141 presets**, - appliqués successivement (journal L5c : stéréotypes, fantasy, âges, silhouettes… puis `phenotypes/*`). + appliqués successivement (journal L5c : stéréotypes, fantasy, âges, silhouettes… puis `phenotypes/*`). 5. Un preset de phénotype impose `INFO Race: African/Asian/Caucasian` → `PlayerChangeRace` → corps legacy - `MeshSexRace[0..2|5..7]`, jamais chargé (`LegacyPercentage = 0`) → **corps vide : invisible ; vêtements + `MeshSexRace[0..2|5..7]`, jamais chargé (`LegacyPercentage = 0`) → **corps vide : invisible ; vêtements Daz sans support : dévêtu**. Le rapport du 2026-09-28 (§4) avait écarté `ParseIntoArray` (« implémentation identique ») : c'est exact pour @@ -331,30 +378,36 @@ Preuves : `Saved/Logs/portage/L5c_partie_utilisateur2_20260929.log`, `etat/L5-di Tag d'entrée `L4-CX-B08-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L4-CX-B08\` (17 Blueprints ; 4 fichiers source créés, retirés par `Restore.ps1` en cas de retour). -| Étape | Résultat | Preuve (`etat/`) | -|---|---|---| -| Fonction de remplacement | `ULPStringCompat::ParseIntoArray` : transcription de l'algorithme 4.27 (chaîne vide → `[]` ; séparateur sensible à la casse), même signature que `UKismetStringLibrary::ParseIntoArray` | `Source/LifePlay/LPStringCompat.*` | -| Simulation | 114 nœuds, 17 Blueprints (GI 73, Person 10, Room 7, StreetMap_Static 4, 13 widgets) ; rien écrit | `L4-CX-B08_dryrun.txt` | -| Application | 114 rebranchés, 0 écart de liaison (contrôle intégré : liaisons et valeurs par défaut de chaque broche identiques avant/après), 0 échec | `L4-CX-B08_application.txt` | -| Contrôles | 0 broche orpheline ; `CompileAllBlueprints` 0 erreur, 95 avertissements (inchangé), 0 échec de chargement | résumés `L4-CX-B08_*` | -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| +| Étape | Résultat | Preuve (`etat/`) | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | +| Fonction de remplacement | `ULPStringCompat::ParseIntoArray` : transcription de l'algorithme 4.27 (chaîne vide → `[]` ; séparateur sensible à la casse), même signature que `UKismetStringLibrary::ParseIntoArray` | `Source/LifePlay/LPStringCompat.*` | +| Simulation | 114 nœuds, 17 Blueprints (GI 73, Person 10, Room 7, StreetMap_Static 4, 13 widgets) ; rien écrit | `L4-CX-B08_dryrun.txt` | +| Application | 114 rebranchés, 0 écart de liaison (contrôle intégré : liaisons et valeurs par défaut de chaque broche identiques avant/après), 0 échec | `L4-CX-B08_application.txt` | +| Contrôles | 0 broche orpheline ; `CompileAllBlueprints` 0 erreur, 95 avertissements (inchangé), 0 échec de chargement | résumés `L4-CX-B08_*` | + + + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | CHG-029 | 2026-09-29 | L4-CX-B08 | `Source/LifePlay/LPStringCompat.h/.cpp`, `Source/LifePlayEditorTools/LPFixParseIntoArrayCommandlet.h/.cpp` (nouveaux) ; 17 Blueprints | `ParseIntoArray` UE5 renvoie `[""]` pour une chaîne vide (UE4 : `[]`) : `generatePerson()` sans argument chargeait les 141 presets | `Tools\Restore.ps1 -Lot L4-CX-B08` ou `git checkout L4-CX-B08-avant -- Content Source` | -| CHG-030 | 2026-09-29 | L5 | `Source/LifePlayEditorTools/LPDumpFunctionCommandlet.cpp` | option `-Exec` (broches d'exécution) | `Tools\Restore.ps1 -Lot L5-dumpexec` | +| CHG-030 | 2026-09-29 | L5 | `Source/LifePlayEditorTools/LPDumpFunctionCommandlet.cpp` | option `-Exec` (broches d'exécution) | `Tools\Restore.ps1 -Lot L5-dumpexec` | + ### Validation d'exécution de CX-B08 — build `LifePlay_Build_5.5_L5d` (2026-09-30) Partie jouée par l'utilisateur : **personnages de quête et personnages aléatoires visibles et habillés**. Journal : `Saved/Logs/portage/L5d_partie_utilisateur_20260930.log`. -| Mesure | 5.5 avant (L5b/L5c) | 5.5 L5d | 4.27 (quête jouée) | -|---|---:|---:|---:| -| Personnages générés | 16 | 40 | 16 | -| Presets de phénotype/stéréotype chargés à tort | tous | **0** | 0 | -| Lectures `Mesh_2` / `SkeletalMesh_2` vides | 378+ | **0** | 0 | -| `Accessed None` maillage / personnage | 43,7 | **0,38** | 0,06 | -| Plantages | 0 | 0 | 0 | + +| Mesure | 5.5 avant (L5b/L5c) | 5.5 L5d | 4.27 (quête jouée) | +| ---------------------------------------------- | ------------------- | -------- | ------------------ | +| Personnages générés | 16 | 40 | 16 | +| Presets de phénotype/stéréotype chargés à tort | tous | **0** | 0 | +| Lectures `Mesh_2` / `SkeletalMesh_2` vides | 378+ | **0** | 0 | +| `Accessed None` maillage / personnage | 43,7 | **0,38** | 0,06 | +| Plantages | 0 | 0 | 0 | + Résidu : 15 lectures `GetSkeletalMesh_DEPRECATED` vides (composants proxy ; dette R-03, lot L7a) ; `ModLauncher.config` (ERR-014) et `OldPlayer.lpcharacter` absents (build sans sauvegarde antérieure). Cuisson L5d : 0 erreur, @@ -364,14 +417,16 @@ Résidu : 15 lectures `GetSkeletalMesh_DEPRECATED` vides (composants proxy ; det ## Lot L6 — Validation V1 (2026-09-30) -Partie utilisateur sur `LifePlay_Build_5.5_L5d` : **42 min, 271 personnages, aucun plantage ; sauvegarde (`1.sav`, +Partie utilisateur sur `LifePlay_Build_5.5_L5d` : **42 min, 271 personnages, aucun plantage ; sauvegarde (**`1.sav`**, le jeu numérote les emplacements) et rechargement fonctionnels**. Journaux : `Saved/Logs/portage/V1_partie_20260930*.log`. Anomalies signalées : ERR-030 à ERR-034 (registre). -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | CHG-031 | 2026-09-30 | L6a | `Config/DefaultEngine.ini` | `r.DefaultFeature.AntiAliasing=2` (clé UE4, ignorée par UE5 : « dummy variable ») → `r.AntiAliasingMethod=2` (TAA, comme en 4.27) ; le jeu tournait en TSR (défaut UE5). Autres réglages vérifiés identiques à 4.27 : GI dynamique 0, reflets SSR, ombres virtuelles 0 | `Tools\Restore.ps1 -Lot L6a` | + Validation L6a (build `LifePlay_Build_5.5_L6a`, partie utilisateur 2026-09-30) : **scintillement disparu** (ERR-032 clos). Capteur « read property Person » : relevé dans `GI::ReturnStatIfNeeded` (variable de scène `Player`, classe `Person`, tableau `Person` vide). Comparaison par personnage : `Dress` 0 sur cette partie (situationnel) ; variables `Person` vides @@ -385,100 +440,114 @@ aucune marge n'est stockée dans l'asset. Source moteur : défaut 4.27 `FCoreSty PressedPadding (2,3,2,1) ; défaut 5.5 `FUMGCoreStyle "Button"` (12,1.5,12,1.5) pour les deux. `FMargin` est sérialisé champ par champ par rapport au défaut : les assets 4.27 prennent donc la marge UE5 au chargement (+20 px par bouton). -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-032 | 2026-09-30 | L6e | `Source/LifePlayEditorTools/LPDumpWidgetTreeCommandlet.h/.cpp`, `LPFixButtonPaddingCommandlet.h/.cpp` (nouveaux) | outils : photographie des arbres UMG (`-BP=` ou `*`), correctif des marges | `git checkout L6e-avant -- Source` | -| CHG-033 | 2026-09-30 | L6e-CX-B09 | 46 Widget Blueprints de `Content/UI` (liste : `etat/L6e-CX-B09_assets.txt`) | 429 boutons (passe 1, marges égales au défaut UE5) + 1 bouton (passe 2, `TopRight::Button_8` Pressed (12,2,12,2) : marge 4.27 partielle, Left/Right hérités d'UE5) remis au défaut UE4 ; 3 boutons à marge 0 choisie par l'auteur conservés | `Tools\Restore.ps1 -Lot L6e-CX-B09` | -| Contrôle | Résultat | Preuve | -|---|---|---| -| Simulation | 432 boutons vus, 429 à corriger, 46 paquets | `etat/L6e-CX-B09_dryrun.txt` | -| Diff des 73 arbres UMG avant/après | hors lignes `WidgetStyle` : identiques ; dans `WidgetStyle`, seules `NormalPadding`/`PressedPadding` changent | `etat/L6e-CX-B09_widgets_{avant,apres,diff}.txt` | -| Idempotence | 3e simulation après passe 2 : 432 boutons vus, 0 à corriger | `etat/L6e-CX-B09c_LPFixButtonPadding_*.txt` | -| `CompileAllBlueprints` | 0 erreur, 190 avertissements (identique à L4-CX-B08) | `etat/L6e-CX-B09_CompileAllBlueprints_*.txt` | -| Build empaquetée `LifePlay_Build_5.5_L6e` | cuisson 0 erreur, 95 avertissements (inchangé) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L6e_BuildCookRun.log` | - -### Lot L6f — ERR-031, portrait vide lors d'un appel (2026-09-30) : diagnostic, sans correctif - -Capture utilisateur 5.5 : l'encadré d'appel montre un dégradé de ciel. Chaîne établie : `GI::secondScreen` (contact lu dans -la variable DSL, `Person.secondScreen = true`, `Scene.FaceRender` visible) → `Render/SecondScreenMat` ← `SceneCapture2D` -`SecondScreen` de `CentralLondon` (rend seulement `Wall` et `PlayerCharacter`). La carte 4.27 d'origine (copie MD5 identique, -chargée temporairement sous `/Game/_Diag`, déplacée ensuite dans `D:\Game\LifePlay_Backup_5.5\L6f\_diag`) contient les -mêmes données : caméra sans racine, en position nulle. **L'utilisateur confirme le même défaut en 4.27** : ERR-031 est -préexistant, hors périmètre de la migration ; aucun asset modifié. - -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-034 | 2026-09-30 | L6f | `Source/LifePlayEditorTools/LPDumpComponentsCommandlet.h/.cpp` (nouveau) | outil de diagnostic : composants d'un Blueprint (`-BP=`) ou acteurs d'une carte (`-Map= -Filter=`), propriétés différentes du défaut | `git checkout L6f-avant -- Source` | - -Note d'outillage : `L6f_dump_GI.txt` a d'abord été écrasé par le vidage de `Person` (le script prenait le journal le plus -récent) ; il a été régénéré depuis `Saved/Logs/portage/L6f_LPDumpFunction_20260930-102202.log` (378 graphes). - -Validation L6e (build `LifePlay_Build_5.5_L6e`, partie utilisateur 2026-09-30) : **barre de temps conforme à la 4.27** -(ERR-034 clos, CX-B09 validé en jeu). - -ERR-033 (double clic) : **non reproduit** depuis le build L6e (constat utilisateur, 2026-09-30). Aucun correctif ciblé ; -laissé sous surveillance. - -### Lot L4-CX-B07 — réenregistrement UE5 des derniers paquets UE4 (2026-09-30) - -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-035 | 2026-09-30 | L4-CX-B07 | `Source/LifePlayEditorTools/LPAssetListUtils`, `LPDumpAssetsCommandlet`, `LPResavePackagesCommandlet` (nouveaux) ; `LifePlayEditorTools.Build.cs` (`bUseUnity = false` : les fonctions utilitaires de chaque commandlet sont dans un espace de noms anonyme, la compilation unity les faisait entrer en collision) ; 239 paquets (liste : `etat/L4-CX-B07_assets.txt`) | EX-03 : derniers paquets au format UE4 (ERR-022) | `Tools\Restore.ps1 -Lot L4-CX-B07` ; `git checkout L4-CX-B07-avant -- Source` | - -| Contrôle | Résultat | Preuve | -|---|---|---| -| Photographie `LPDumpAssets` (292 paquets : les 239 + `BPs`, `UI`, `Structs`, `Render`, y compris GI et Person) | 1,28 million de lignes, 0 échec de chargement | `Saved/Logs/portage/L4-CX-B07_assets_{avant,avant2,apres}.txt` | -| Déterminisme | 2 photographies sans modification : seuls `NodeGuid`/`GraphGuid` diffèrent (régénérés au chargement des nœuds UE4) ; exclus du diff → 0 écart | idem | -| Simulation puis application | 239/239 sauvegardés, 0 échec | `etat/L4-CX-B07_dryrun.txt`, `etat/L4-CX-B07_application.txt` | -| T-STAT-01 (en-têtes) | **8563/8563 paquets UE5** | `etat/L4-CX-B07_headers_apres.csv` | -| Diff avant/après (409 lignes) | toutes neutres, voir ci-dessous | `etat/L4-CX-B07_diff_filtre.txt` | - -Familles d'écarts du diff : (1) textures : stockage de la source PNG → `UEDELTA` (mêmes dimensions, format et mips ; données -éditeur, la cuisson en dérive) ; (2) 31 structures : `DefaultValue`/`CurrentDefaultValue` explicités avec les valeurs zéro -du type (identiques au défaut implicite) ; (3) 5 matériaux : copie UE4 dépréciée d'`EmissiveColor`/`Opacity` retirée de -`UMaterial`, les entrées restent identiques dans `MaterialEditorOnlyData` ; (4) 8 broches `Transform` : `-0` → `0` ; -(5) 5 signatures de délégué héritées (`SignatureClass/Name`) effacées sur des nœuds d'entrée ; (6) 2 avertissements de -dépréciation enregistrés sur des nœuds (`AI` R-04, `CharGenSlider` R-03, dette connue) ; (7) `PC` : broche -`TransformScaleMethod` du `SpawnActor` de `TestBall` (acteur de test) `SelectDefaultAtRuntime` → `MultiplyWithRoot`, -valeur déjà portée par les nœuds UE5 de `GI` ; (8) objets `PLACEHOLDER-CLASS` du chargeur (ordre de chargement). +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| CHG-032 | 2026-09-30 | L6e | `Source/LifePlayEditorTools/LPDumpWidgetTreeCommandlet.h/.cpp`, `LPFixButtonPaddingCommandlet.h/.cpp` (nouveaux) | outils : photographie des arbres UMG (`-BP=` ou `*`), correctif des marges | `git checkout L6e-avant -- Source` | +| CHG-033 | 2026-09-30 | L6e-CX-B09 | 46 Widget Blueprints de `Content/UI` (liste : `etat/L6e-CX-B09_assets.txt`) | 429 boutons (passe 1, marges égales au défaut UE5) + 1 bouton (passe 2, `TopRight::Button_8` Pressed (12,2,12,2) : marge 4.27 partielle, Left/Right hérités d'UE5) remis au défaut UE4 ; 3 boutons à marge 0 choisie par l'auteur conservés | `Tools\Restore.ps1 -Lot L6e-CX-B09` | + + + +| Contrôle | Résultat | Preuve | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| Simulation | 432 boutons vus, 429 à corriger, 46 paquets | `etat/L6e-CX-B09_dryrun.txt` | +| Diff des 73 arbres UMG avant/après | hors lignes `WidgetStyle` : identiques ; dans `WidgetStyle`, seules `NormalPadding`/`PressedPadding` changent | `etat/L6e-CX-B09_widgets_{avant,apres,diff}.txt` | +| Idempotence | 3e simulation après passe 2 : 432 boutons vus, 0 à corriger | `etat/L6e-CX-B09c_LPFixButtonPadding_*.txt` | +| `CompileAllBlueprints` | 0 erreur, 190 avertissements (identique à L4-CX-B08) | `etat/L6e-CX-B09_CompileAllBlueprints_*.txt` | +| Build empaquetée `LifePlay_Build_5.5_L6e` | cuisson 0 erreur, 95 avertissements (inchangé) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L6e_BuildCookRun.log` | + + +### Lot L6f — ERR-031, portrait vide lors d'un appel (2026-09-30) : diagnostic, sans correctif + +Capture utilisateur 5.5 : l'encadré d'appel montre un dégradé de ciel. Chaîne établie : `GI::secondScreen` (contact lu dans +la variable DSL, `Person.secondScreen = true`, `Scene.FaceRender` visible) → `Render/SecondScreenMat` ← `SceneCapture2D` +`SecondScreen` de `CentralLondon` (rend seulement `Wall` et `PlayerCharacter`). La carte 4.27 d'origine (copie MD5 identique, +chargée temporairement sous `/Game/_Diag`, déplacée ensuite dans `D:\Game\LifePlay_Backup_5.5\L6f\_diag`) contient les +mêmes données : caméra sans racine, en position nulle. **L'utilisateur confirme le même défaut en 4.27** : ERR-031 est +préexistant, hors périmètre de la migration ; aucun asset modifié. + + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | +| CHG-034 | 2026-09-30 | L6f | `Source/LifePlayEditorTools/LPDumpComponentsCommandlet.h/.cpp` (nouveau) | outil de diagnostic : composants d'un Blueprint (`-BP=`) ou acteurs d'une carte (`-Map= -Filter=`), propriétés différentes du défaut | `git checkout L6f-avant -- Source` | + + +Note d'outillage : `L6f_dump_GI.txt` a d'abord été écrasé par le vidage de `Person` (le script prenait le journal le plus +récent) ; il a été régénéré depuis `Saved/Logs/portage/L6f_LPDumpFunction_20260930-102202.log` (378 graphes). + +Validation L6e (build `LifePlay_Build_5.5_L6e`, partie utilisateur 2026-09-30) : **barre de temps conforme à la 4.27** +(ERR-034 clos, CX-B09 validé en jeu). + +ERR-033 (double clic) : **non reproduit** depuis le build L6e (constat utilisateur, 2026-09-30). Aucun correctif ciblé ; +laissé sous surveillance. + +### Lot L4-CX-B07 — réenregistrement UE5 des derniers paquets UE4 (2026-09-30) + + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------- | +| CHG-035 | 2026-09-30 | L4-CX-B07 | `Source/LifePlayEditorTools/LPAssetListUtils`, `LPDumpAssetsCommandlet`, `LPResavePackagesCommandlet` (nouveaux) ; `LifePlayEditorTools.Build.cs` (`bUseUnity = false` : les fonctions utilitaires de chaque commandlet sont dans un espace de noms anonyme, la compilation unity les faisait entrer en collision) ; 239 paquets (liste : `etat/L4-CX-B07_assets.txt`) | EX-03 : derniers paquets au format UE4 (ERR-022) | `Tools\Restore.ps1 -Lot L4-CX-B07` ; `git checkout L4-CX-B07-avant -- Source` | + + + +| Contrôle | Résultat | Preuve | +| -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| Photographie `LPDumpAssets` (292 paquets : les 239 + `BPs`, `UI`, `Structs`, `Render`, y compris GI et Person) | 1,28 million de lignes, 0 échec de chargement | `Saved/Logs/portage/L4-CX-B07_assets_{avant,avant2,apres}.txt` | +| Déterminisme | 2 photographies sans modification : seuls `NodeGuid`/`GraphGuid` diffèrent (régénérés au chargement des nœuds UE4) ; exclus du diff → 0 écart | idem | +| Simulation puis application | 239/239 sauvegardés, 0 échec | `etat/L4-CX-B07_dryrun.txt`, `etat/L4-CX-B07_application.txt` | +| T-STAT-01 (en-têtes) | **8563/8563 paquets UE5** | `etat/L4-CX-B07_headers_apres.csv` | +| Diff avant/après (409 lignes) | toutes neutres, voir ci-dessous | `etat/L4-CX-B07_diff_filtre.txt` | + + +Familles d'écarts du diff : (1) textures : stockage de la source PNG → `UEDELTA` (mêmes dimensions, format et mips ; données +éditeur, la cuisson en dérive) ; (2) 31 structures : `DefaultValue`/`CurrentDefaultValue` explicités avec les valeurs zéro +du type (identiques au défaut implicite) ; (3) 5 matériaux : copie UE4 dépréciée d'`EmissiveColor`/`Opacity` retirée de +`UMaterial`, les entrées restent identiques dans `MaterialEditorOnlyData` ; (4) 8 broches `Transform` : `-0` → `0` ; +(5) 5 signatures de délégué héritées (`SignatureClass/Name`) effacées sur des nœuds d'entrée ; (6) 2 avertissements de +dépréciation enregistrés sur des nœuds (`AI` R-04, `CharGenSlider` R-03, dette connue) ; (7) `PC` : broche +`TransformScaleMethod` du `SpawnActor` de `TestBall` (acteur de test) `SelectDefaultAtRuntime` → `MultiplyWithRoot`, +valeur déjà portée par les nœuds UE5 de `GI` ; (8) objets `PLACEHOLDER-CLASS` du chargeur (ordre de chargement). | `CompileAllBlueprints` | 0 erreur, 190 avertissements (identique à L6e) | `etat/L4-CX-B07_CompileAllBlueprints_*.txt` | -| Build empaquetée `LifePlay_Build_5.5_V1rc` | cuisson 0 erreur, 95 avertissements (inchangé) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L4-CX-B07_BuildCookRun.log` | - ---- - -## V1 — validation finale (2026-09-30), étiquette `v1.0` - -Partie utilisateur sur `LifePlay_Build_5.5_V1rc` (contenu = `L4-CX-B07-apres`) : environ 2 h, 339 personnages, sauvegardes -`1.sav` et `2.sav`, sortie normale, aucun dossier `Crashes` ; retour utilisateur : « pas de souci majeur constaté ». -Journaux : `Saved/Logs/portage/V1rc_partie_20260930*.log`. - -| Exigence | Résultat | -|---|---| -| EX-01 compilation | `LifePlayEditor` compile sans erreur (L2, recompilé à chaque lot) | -| EX-02 Blueprints | `CompileAllBlueprints` 0 erreur (190 avertissements de dépréciation, dette R-03/R-04) | -| EX-03 format UE5 | 8563/8563 paquets (CX-B07) | -| EX-04 `Modules` | chargés comme en 4.27 (L3, T-STAT-02) | -| EX-05 build jouée | personnages distincts, visibles et habillés, scènes, sauvegarde et rechargement | -| EX-06 sans plantage | 42 min (L5d), environ 2 h (V1rc) : 0 plantage | -| EX-07 journalisation | `LogLifePlay`, résumé de démarrage et de session, traces de portage en `Verbose` | -| EX-08 traçabilité | CHG-001 à CHG-035, tags avant/après par lot, `Restore.ps1` testé (L0) | -| EX-09 environnement | `.claude/`, `.mcp.json`, `Saved/Backups/`, arbre 4.27 non modifiés | - -Comparaison par personnage V1 (L5d) → V1rc des `Accessed None … Person` par fonction : `getActorVar` 4,6 → 4,0, -`ReturnStatIfNeeded` 3,6 → 3,8, `setActorVar` 0,8 → 0,8 ; seul `GI::Dress` varie (74,5 → 283,6), par rafales liées aux -lieux (ERR-030, lot L7i). Anomalies ouvertes après V1 : voir `Registre_Erreurs.md` et lots L7 de `04_Cahier_Des_Charges.md`. +| Build empaquetée `LifePlay_Build_5.5_V1rc` | cuisson 0 erreur, 95 avertissements (inchangé) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L4-CX-B07_BuildCookRun.log` | + +--- + +## V1 — validation finale (2026-09-30), étiquette `v1.0` + +Partie utilisateur sur `LifePlay_Build_5.5_V1rc` (contenu = `L4-CX-B07-apres`) : environ 2 h, 339 personnages, sauvegardes +`1.sav` et `2.sav`, sortie normale, aucun dossier `Crashes` ; retour utilisateur : « pas de souci majeur constaté ». +Journaux : `Saved/Logs/portage/V1rc_partie_20260930*.log`. + + +| Exigence | Résultat | +| -------------------- | ------------------------------------------------------------------------------------- | +| EX-01 compilation | `LifePlayEditor` compile sans erreur (L2, recompilé à chaque lot) | +| EX-02 Blueprints | `CompileAllBlueprints` 0 erreur (190 avertissements de dépréciation, dette R-03/R-04) | +| EX-03 format UE5 | 8563/8563 paquets (CX-B07) | +| EX-04 `Modules` | chargés comme en 4.27 (L3, T-STAT-02) | +| EX-05 build jouée | personnages distincts, visibles et habillés, scènes, sauvegarde et rechargement | +| EX-06 sans plantage | 42 min (L5d), environ 2 h (V1rc) : 0 plantage | +| EX-07 journalisation | `LogLifePlay`, résumé de démarrage et de session, traces de portage en `Verbose` | +| EX-08 traçabilité | CHG-001 à CHG-035, tags avant/après par lot, `Restore.ps1` testé (L0) | +| EX-09 environnement | `.claude/`, `.mcp.json`, `Saved/Backups/`, arbre 4.27 non modifiés | + + +Comparaison par personnage V1 (L5d) → V1rc des `Accessed None … Person` par fonction : `getActorVar` 4,6 → 4,0, +`ReturnStatIfNeeded` 3,6 → 3,8, `setActorVar` 0,8 → 0,8 ; seul `GI::Dress` varie (74,5 → 283,6), par rafales liées aux +lieux (ERR-030, lot L7i). Anomalies ouvertes après V1 : voir `Registre_Erreurs.md` et lots L7 de `04_Cahier_Des_Charges.md`. ## Publication GitHub (2026-09-30) -Dépôt : https://github.com/edujiro5972/LifePlay-Revival (fork de `vinfamy/LifePlay`, AGPL-3.0), compte `edujiro5972`. +Dépôt : [https://github.com/edujiro5972/LifePlay-Revival](https://github.com/edujiro5972/LifePlay-Revival) (fork de `vinfamy/LifePlay`, AGPL-3.0), compte `edujiro5972`. Préparation dans `D:\Game\LifePlay_Publication` (copies ; ni ce dépôt ni l'arbre 4.27 modifiés). -| Branche | Commit | Contenu | -|---|---|---| -| `archive/original` | 42dc72b | `master` d'origine (V3.6) | -| `archive/ue4.27` | 9e61ec5 | arbre 4.27 filtré selon le `.gitignore` d'origine (sans `Content/Heavy`, `Content/Saves`, binaires, `*.png`, `*.7z`, `*.mp4`), 2 773 fichiers | -| `ue5.5-v1` = `main` (branche par défaut) | a55a43f | V1 ; 34 commits et 40 tags, dont `v1.0` | + +| Branche | Commit | Contenu | +| ---------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `archive/original` | 42dc72b | `master` d'origine (V3.6) | +| `archive/ue4.27` | 9e61ec5 | arbre 4.27 filtré selon le `.gitignore` d'origine (sans `Content/Heavy`, `Content/Saves`, binaires, `*.png`, `*.7z`, `*.mp4`), 2 773 fichiers | +| `ue5.5-v1` = `main` (branche par défaut) | a55a43f | V1 ; 34 commits et 40 tags, dont `v1.0` | + Historique V1 réécrit pour la publication (`git filter-branch`, script `D:\Game\LifePlay_Publication\scrub.sh`) : auteur, committer et signataire de `v1.0` = `edujiro5972 <336072401+edujiro5972@users.noreply.github.com>` ; valeurs @@ -496,38 +565,42 @@ Rattachement du dépôt local à l'historique publié (2026-09-30) : `.git` sauv `LifePlay_Revival_V1_UE5.5_Full_Source_Code.7z` (4 851 560 856 octets, SHA-256 `38ee16640f960317348a044195a23b55ac8eda34aec085e661034dc9c26cc594`), déposée par l'utilisateur sur Mega : -. Principe de l'archive de l'auteur +[https://mega.nz/file/zloFkL7T#3P5GDfcfF5U352T0s472vCtyNh131RLgNWn__PSNL2I](https://mega.nz/file/zloFkL7T#3P5GDfcfF5U352T0s472vCtyNh131RLgNWn__PSNL2I). Principe de l'archive de l'auteur (`LifePlay_5_9_UE4_Full_Source_Code.7z`) : dossier projet à la racine, 7z `LZMA2:24 BCJ` solide. Contenu : `Content` complet (24 912 fichiers, dont `Heavy` 8 350, au format UE5), `Binaries`, plugins `StreetMap` et `ModdingExtra`, `.git` d'un clone propre du dépôt publié. Exclus : `Saved` (comme l'auteur), `Intermediate`, `McpAutomationBridge`, `.mcp.json`, `.claude`, captures d'écran, `.git` local. Test d'intégrité 7-Zip réussi. Script : `D:\Game\LifePlay_Publication\make_archive.cmd`. `README.md` réécrit en anglais avec le lien de l'archive. - ---- - -## Lot L7a — R-03 refait : nœuds `Get SkeletalMesh` dépréciés (2026-09-30) - -Analyse (source UE 5.5) : la lecture de `USkinnedMeshComponent::SkeletalMesh` passe par `GetSkeletalMesh_DEPRECATED()`, -qui renvoie `Cast(GetSkinnedAsset())`. CX-B05 avait pris `USkeletalMeshComponent::GetSkeletalMeshAsset()`, -qui n'existe pas sur les proxies (`USkinnedMeshComponent`) : d'où ERR-002 aggravé. Remplacement exact retenu : -`ULPMeshCompat::GetSkeletalMesh(Component)`, même calcul, pour tout `USkinnedMeshComponent`. Inventaire structurel -(instantané CX-B07) : 56 nœuds sources (les appels `GetSkeletalMesh_DEPRECATED` relevés ne sont que les copies compilées ; -la variable `SkeletalMesh` de `GenitalSlider` est une variable Blueprint, non concernée). - -| ID | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-036 | 2026-09-30 | L7a (CX-B10) | `Source/LifePlay/LPMeshCompat.h/.cpp`, `Source/LifePlayEditorTools/LPFixSkeletalMeshGetCommandlet.h/.cpp` (nouveaux), `Tools/DiffRemplacementNoeuds.py` ; 8 Blueprints (`etat/L7a_assets.txt`) | ERR-012 / R-03 : 56 nœuds dépréciés | `Tools\Restore.ps1 -Lot L7a` ; `git checkout L7a-avant -- Source Tools` | - -| Contrôle | Résultat | Preuve | -|---|---|---| -| Simulation | 56 nœuds, 0 cas non pris en charge (cible `self`, nœud impur) | `etat/L7a_dryrun.txt` | -| Application (contrôle intégré des liaisons) | 56/56 remplacés, 8 Blueprints sauvegardés, 0 écart | `etat/L7a_application.txt` | -| Diff indépendant `LPDumpAssets` avant/après (`Tools/DiffRemplacementNoeuds.py`) | 56 paires (même graphe, même position) ; liaisons `self`→`Component` et `SkeletalMesh`→`ReturnValue` identiques pour 56/56 ; **0 autre objet modifié** (hors copies compilées, graphes anonymes régénérés, GUID, positions des graphes-souches, messages de compilation) | `Saved/Logs/portage/L7a_assets_{avant,apres}.txt` | -| Chargement des 8 Blueprints | avertissements 112 → 6 | journaux `L7a_LPDumpAssets_*` | - -Effet de mesure : le nœud d'origine émettait `Accessed None … SkeletalMesh` sur un composant None ; la nouvelle fonction -renvoie None sans message (trace `LogLifePlay` en `Verbose`). Les compteurs `AccessedNoneMaillage` de `LogStats.ps1` -baissent donc mécaniquement : comparer les sessions sur les autres signatures. + +--- + +## Lot L7a — R-03 refait : nœuds `Get SkeletalMesh` dépréciés (2026-09-30) + +Analyse (source UE 5.5) : la lecture de `USkinnedMeshComponent::SkeletalMesh` passe par `GetSkeletalMesh_DEPRECATED()`, +qui renvoie `Cast(GetSkinnedAsset())`. CX-B05 avait pris `USkeletalMeshComponent::GetSkeletalMeshAsset()`, +qui n'existe pas sur les proxies (`USkinnedMeshComponent`) : d'où ERR-002 aggravé. Remplacement exact retenu : +`ULPMeshCompat::GetSkeletalMesh(Component)`, même calcul, pour tout `USkinnedMeshComponent`. Inventaire structurel +(instantané CX-B07) : 56 nœuds sources (les appels `GetSkeletalMesh_DEPRECATED` relevés ne sont que les copies compilées ; +la variable `SkeletalMesh` de `GenitalSlider` est une variable Blueprint, non concernée). + + +| ID | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------- | +| CHG-036 | 2026-09-30 | L7a (CX-B10) | `Source/LifePlay/LPMeshCompat.h/.cpp`, `Source/LifePlayEditorTools/LPFixSkeletalMeshGetCommandlet.h/.cpp` (nouveaux), `Tools/DiffRemplacementNoeuds.py` ; 8 Blueprints (`etat/L7a_assets.txt`) | ERR-012 / R-03 : 56 nœuds dépréciés | `Tools\Restore.ps1 -Lot L7a` ; `git checkout L7a-avant -- Source Tools` | + + + +| Contrôle | Résultat | Preuve | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | +| Simulation | 56 nœuds, 0 cas non pris en charge (cible `self`, nœud impur) | `etat/L7a_dryrun.txt` | +| Application (contrôle intégré des liaisons) | 56/56 remplacés, 8 Blueprints sauvegardés, 0 écart | `etat/L7a_application.txt` | +| Diff indépendant `LPDumpAssets` avant/après (`Tools/DiffRemplacementNoeuds.py`) | 56 paires (même graphe, même position) ; liaisons `self`→`Component` et `SkeletalMesh`→`ReturnValue` identiques pour 56/56 ; **0 autre objet modifié** (hors copies compilées, graphes anonymes régénérés, GUID, positions des graphes-souches, messages de compilation) | `Saved/Logs/portage/L7a_assets_{avant,apres}.txt` | +| Chargement des 8 Blueprints | avertissements 112 → 6 | journaux `L7a_LPDumpAssets_*` | + + +Effet de mesure : le nœud d'origine émettait `Accessed None … SkeletalMesh` sur un composant None ; la nouvelle fonction +renvoie None sans message (trace `LogLifePlay` en `Verbose`). Les compteurs `AccessedNoneMaillage` de `LogStats.ps1` +baissent donc mécaniquement : comparer les sessions sur les autres signatures. | `CompileAllBlueprints` | 0 erreur, **6 avertissements** (190 avant) : restent seulement les 6 nœuds R-04 `GetRandomPointInNavigableRadius` (`AI`, `Person`, `Room`) | `etat/L7a_CompileAllBlueprints_*.txt` | | Build empaquetée `LifePlay_Build_5.5_L7a` | cuisson 0 erreur, **3 avertissements** (95 avant) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L7a_BuildCookRun.log` | @@ -545,416 +618,581 @@ sortie normale, sauvegardes `1.sav`/`2.sav` ; tests utilisateur effectués sans (situationnel, ERR-030). Messages moteur `GetSocketInfoByName … No SkeletalMesh for Component(CharacterMesh0)` : même signature et mêmes 5 sockets qu'en V1rc, volume lié aux scènes jouées (ERR-011, existe en 4.27) : non imputable à L7a. Fonctions de variables DSL (`getActorVar`, `clearVariable`, `CallFunction`) non touchées par L7a, en hausse par personnage -sur cette partie : famille ERR-003, à suivre en L7i. - -### L7b — R-04 : derniers nœuds dépréciés (2026-09-30) - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-037 | 2026-09-30 | L7b (CX-B11) | `Source/LifePlayEditorTools/LPFixNavRandomPointCommandlet.h/.cpp` (nouveau) ; `Tools/DiffRemplacementNoeuds.py` (profils par lot : `L7a`, `L7b`) ; `Content/BPs/AI`, `Person`, `Room` (`etat/L7b_assets.txt`) | ERR-035 / R-04 : 6 nœuds `GetRandomPointInNavigableRadius` dépréciés | `Tools\Restore.ps1 -Lot L7b` ; `git checkout L7b-avant -- Source Tools` | - -Choix : le remplaçant proposé par le moteur (`K2_GetRandomLocationInNavigableRadius`) est `BlueprintCallable`, alors que -l'ancien nœud est pur ; le brancher imposerait de recâbler le flux d'exécution. On réutilise `LPNavigationHelpers` -(enrobage pur déjà présent, CX-C09) : mêmes broches, et même corps que la fonction dépréciée en 4.27 comme en 5.5 -(simple appel de `K2_GetRandomLocationInNavigableRadius`). `LPFixDeprecatedNodes` (CX-B06) n'est pas réutilisée : elle -ne contrôle rien et embarque aussi la variante R-03 rejetée. - -| Contrôle | Résultat | Preuve | -|---|---|---| -| Simulation | 6 nœuds (`Room::moveToBedIfPossible`, `Room::getStandSpots`, `Person::standIdle` ×2, `Person::jumpOffFnt`, `AI::EventGraph`), 0 cas non pris en charge | `etat/L7b_LPFixNavRandomPoint_20260930-183406.txt` | -| Application (contrôle intégré : liaisons et valeurs par défaut de chaque broche) | 6/6 remplacés, 3 Blueprints sauvegardés, 0 écart | `etat/L7b_LPFixNavRandomPoint_20260930-183707.txt` | -| Diff indépendant `LPDumpAssets` (`DiffRemplacementNoeuds.py … L7b`) | 6 paires ; 7 broches identiques (liaisons et défauts, ex. `Radius` 100 / 200 / 1000 / 2000 / 100000) ; **0 autre objet modifié** ; le profil `L7a` rejoué sur les données L7a donne toujours 56/56 et 0 écart | `Saved/Logs/portage/L7b_assets_{avant,apres}.txt` | -| `CompileAllBlueprints` | 0 erreur, **0 avertissement** (6 avant) | `etat/L7b_CompileAllBlueprints_20260930-183909.txt` | -| Build empaquetée `LifePlay_Build_5.5_L7b` | cuisson 0 erreur, **0 avertissement** (3 avant) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L7b_BuildCookRun.log` | - -Validation en jeu en attente (déplacements des personnages : `standIdle`, `jumpOffFnt`, `moveToBedIfPossible`, IA). - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-038 | 2026-09-30 | L7b | `Content/BPs/AI`, `Person`, `Room` restaurés (`Tools\Restore.ps1 -Lot L7b`, MD5 = `L7b-avant`) ; versions L7b conservées dans `LifePlay_Backup_5.5\L7b\_avant-restauration‚60930-193820` | test en jeu du build L7b (utilisateur) : les personnages se téléportent dans la pièce au lieu de marcher, pieds parfois masqués par un objet ; « la build précédente était meilleure » | réappliquer : `Restore.ps1` depuis `_avant-restauration`, ou relancer `LPFixNavRandomPoint` | - -Analyse : aucune cause trouvée dans le code. Ancienne et nouvelle fonction exécutent le même appel moteur -(`K2_GetRandomLocationInNavigableRadius`), mêmes broches et valeurs par défaut (diff 6/6), et le compilateur remplit la -broche cachée `WorldContextObject` de la même façon pour les deux (`FKCHandler_CallFunction::RegisterNets`, métadonnée -`WorldContext`). Le journal de la partie ne contient aucun message de navigation. Prochaine étape : A/B au même endroit -avec le build L7a (identique au contenu actuel), et vérifier le comportement en 4.27, avant toute nouvelle tentative. - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-039 | 2026-09-30 | L7b | `Content/BPs/AI`, `Person`, `Room` remis à `L7b-apres` (`git checkout L7b-apres`, MD5 identiques aux versions conservées dans `_avant-restauration‚60930-193820`) | retour utilisateur : la téléportation venait de l'accélération du temps (en vitesse normale les personnages marchent) ; les personnages enfoncés dans le sol en extérieur existent aussi en 4.27 et en L7a/V1 (ERR-036, hors périmètre). Le retrait CHG-038 n'était donc pas justifié | `Tools\Restore.ps1 -Lot L7b` | - -L7b validé en jeu : le build testé `LifePlay_Build_5.5_L7b` contient exactement ces 3 paquets. Leçon : avant de retirer -un lot sur un symptôme visuel, vérifier la vitesse de jeu et demander si le défaut existe en 4.27 et dans le build précédent. - -### L7i — variables DSL `Person` vides (ERR-003 / ERR-030), 2026-09-30 - -Mesure (journaux `L7a_partie`, `L7b_partie`, 4.27 `LifePlay_Build_427_Dev\...\LifePlay.log`, par minute) : - -| Signature | 4.27 (23 min) | L7a (62 min) | L7b (45 min) | -|---|---|---|---| -| `Accessed None … Person` dans `GI::Dress` | 376 | 326 | 641 | -| `index 0 … array 'Person'` (variable DSL vide) | 9 | 179 | 56 | -| `index … array 'Floats'` | 2,4 | 31 | 64 | -| `Accessed None … Person` dans `getActorVar` | 0 | 45 | 7,6 | -| `Accessed None … Person` dans `ReturnStatIfNeeded` | 0,35 | 33 | 5,6 | - -Conclusion : **ERR-030 (`Dress`) existe en 4.27** (même signature `Dress:0E35`, même ordre de grandeur) : préexistant, -hors périmètre. L'écart propre à 5.5 porte sur les variables DSL vides (ERR-003). Lecture du graphe -(`GI::GetVariable`) : la variable `Player` est construite avec `Person = [Special]`, `Special = GI.PlayerCharacter` -(`SwitchString_55`, branche `Player`) ; le capteur L6a montre `Special = None`. Les erreurs surviennent tout au long de -la partie, pas seulement au chargement. - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-040 | 2026-09-30 | L7i | `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` : options `-LPTraceFunctions`, `-LPTraceDepth` (variables locales des cadres appelants), `-LPTraceGIVars` (variables de l'instance de jeu) ; inactives sans `-LPTraceScriptEx` | capteur ciblé ERR-003 : quelle scène et quelle variable restent vides | `git checkout L7i-avant -- Source` | - -Build de diagnostic `LifePlay_Build_5.5_L7i` (contenu identique à L7b, 8561 `.uasset`), lancement par -`LifePlay_diagnostic_L7i.bat` (300 relevés, 4 cadres appelants, `-ScriptStackOnWarnings`). - -Session capteur n° 1 (`L7i_partie2_20261001.log`, 53 min, 300 relevés) : les variables vides viennent surtout des -scripts d'IA des PNJ (`.lpai` : `meal`, `church`, `grocery`, `entertainment`…), via `UpdateAI → PlayScene → -ReadSceneLine → CallFunction → getActorVar/setActorVar`. La variable vide est **`AI`**, alimentée par `GI.AIDummy` -(personnage factice du pool `DummyPersons`, attribué par `SpawnDummy`). `GI.PlayerCharacter` n'est pas None. Dans -`GetVariable`, toutes les variables spéciales (`Player`, `Ptr`, `Interacting`, `CurrentCompanion`, `A1`…`A5`) -passent par le même `MakeStruct` dont le nom est figé à « Player » : l'étiquette relevée en L6a était trompeuse. - -Nouveau défaut signalé (utilisateur, 2026-10-01) : **Quick dress laisse souvent le personnage principal nu ; correct -en 4.27** → ERR-037, régression UE5. Chemin : `Action` (bouton « Quick dress ») → `GI::Dress(PersonVariable="Player", -ClothVariables=[], QuickDress=true)` → pour chaque emplacement obligatoire (`Top`, `Bottom`, `Foot`), filtre de -l'inventaire du joueur (sexe, emplacement, `Overlaps`, `Daz == (Race == "Daz")`, exclusion `Wedding/Nurse/Doctor`). -Inventaire de la sauvegarde `1.sav` cohérent (80 vêtements, `Daz=True`). - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-041 | 2026-10-01 | L7i | `Source/LifePlayEditorTools/LPDumpSaveCommandlet.h/.cpp` (nouveau : contenu d'un `.sav` en texte, lecture seule) ; `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` : relevé déclenché par un motif du journal (`-LPDumpOnLog`, `-LPDumpPaths`, `-LPDumpMax`), inactif sans l'option | ERR-037 : relever l'état du joueur au moment du Quick dress ; ERR-003 : état d'`AIDummy` | `git checkout L7i-capteur -- Source` | - -Build `LifePlay_Build_5.5_L7i` reconstruite (contenu identique), lancement `LifePlay_diagnostic_L7i_2.bat`. - -Session capteur n° 2 (`L7i_partie3_20261001.log`, 5 min, 8 relevés Quick dress, 60 relevés IA) : - -- **Quick dress (ERR-037)** : à la maison, 4 essais sur 4 corrects (haut, bas, chaussures, accessoires). Au travail, - 3 essais sur 3 ne mettent que `Trench-Coat-Outfit-Top`. État relevé : `Race = "Daz"`, `IsMale = False`, - inventaire de 80 vêtements. Au travail (`CurrentAmenity = work`), `GI::Dress` n'accepte que les vêtements - `Bool3` (étiquette « Formal ») ; l'inventaire n'en contient que 2 (`HC-Business-Jacket` en `Outerwear`, - `Trench-Coat-Outfit-Top` en `Top`), aucun bas ni chaussure formels. Quand aucun vêtement ne convient à un - emplacement, la sortie `Completed` de la boucle n'est pas câblée : l'emplacement reste vide. Graphe identique à - 4.27 → comportement d'origine, à confirmer par un essai 4.27 au travail. -- **Variable `AI` vide** : `GI.AIDummy` est valide au moment de l'erreur ; les erreurs portent toujours sur les - emplacements de PNJ `CheckingAiIndex` 5 et 8. `UpdateAI` charge le PNJ dans `AIDummy` (`LoadPreset`, - `StatOnly`) puis `AddVariable("AI", [AIDummy])` ; à l'erreur, `GetVariable("AI")` renvoie une variable vide - (non trouvée). Piste suivante : ce qui retire ou masque `AI` pour ces deux PNJ. - -Suite L7i (2026-10-01) : - -- ERR-037 (Quick dress) **préexistant** : même comportement en 4.27 au travail (essai utilisateur). Journal 4.27 de - l'essai archivé : `Saved/Logs/portage/ref427_quickdress_20261001.log` ; la session 4.27 de référence (23 min) est - archivée sous `ref427_partie_20260930.log` (le `LifePlay.log` 4.27 avait été remplacé par l'essai). -- Variable `AI` introuvable : au moment de l'erreur, la boucle de vérification des scènes du joueur (`CHECKING:`) - s'exécute entre les lignes du script d'IA ; les deux boucles avancent par `Delay(0)` (macro `GI:Async`) et - partagent la table des variables DSL. Moteur : `FDelayAction`, `UKismetSystemLibrary::Delay` et - `FLatentActionManager::ProcessLatentActions` équivalents 4.27/5.5 ; images par seconde équivalentes (≈ 55). - Écart mesuré : par cycle de vérification, ≈ 140 scènes en 4.27 contre ≈ 430 en 5.5 dont 100 vérifiées 2 à 3 fois - (redémarrages : `CheckingSceneIndex` remis à -1 et liste remélangée à chaque changement de `CurrentAmenity`). - Les sessions comparées n'ont pas suivi le même parcours (déplacements, accélération du temps) : une comparaison - A/B sur un parcours identique est nécessaire avant de conclure à une régression. - -A/B à parcours identique (2026-10-01, nouvelle partie, vitesse normale, maison puis travail ; 4.27 47 min, -`AB427_20261001.log` ; 5.5 33 min, `AB55_20261001.log`) : - -| Par minute | 4.27 | 5.5 | -|---|---|---| -| Vérifications de scènes | 626 | 614 | -| `AI` introuvable (`getActorVar`…) | 0 | 0 | -| Variables `Person` vides | 124 | 123 | -| `getBeautyValue` (`Floats` vide) | 1,3 | 52 | - -Les erreurs `AI` dépendaient du rythme de jeu (accélération du temps) : absentes à parcours égal, classées comme -préexistantes (même mécanisme en 4.27). Seul écart réel : `getBeautyValue`. - -Cause (ERR-005) : `BeautyStandards` est indexé par forme (`round`, `oval`…, fichier -`Modules/beauty_standards_default.txt` identique dans les deux versions) ; la première recherche par nom de morph -échoue normalement (62 avertissements en 4.27 aussi). L'écart vient du nombre d'appels : `filterMorphList` est -appelé sans étiquette et teste `Contains(tags, "")`. Moteur : `FString::Find` 4.27 → `FCString::Stristr`, qui -cherche le caractère nul et ne trouve rien ; 5.5 → `TCString::Strnistr`, qui renvoie le début de la chaîne. Les -autres modes (sensible à la casse, depuis la fin) et `StartsWith`, `EndsWith`, `Replace` sont identiques. - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-042 | 2026-10-01 | L7i (CX-B12) | `Source/LifePlay/LPStringCompat.h/.cpp` (`FindUE4`, `Contains`, `FindSubstring`, `Split`) ; `Source/LifePlayEditorTools/LPFixStringSearchCommandlet`, `LPTestStringCompatCommandlet`, `LPDumpCDOCommandlet` (nouveaux) ; `Tools/DiffRemplacementNoeuds.py` (profil `CX-B12`, broches absentes des deux côtés) ; 28 Blueprints (`etat/L7i_CX-B12_assets.txt`) | ERR-005 : recherche de texte avec motif vide | `Tools\Restore.ps1 -Lot L7i-CX-B12` ; `git checkout L7i-CX-B12-avant -- Source Tools` | - -| Contrôle | Résultat | Preuve | -|---|---|---| -| Test `LPTestStringCompat` : transcription littérale de `FString::Find` 4.27 contre `LPStringCompat` et contre le moteur 5.5 | 3 120 cas : compat = 4.27 dans 100 % des cas ; moteur 5.5 ≠ 4.27 dans 60 cas, tous « sous-chaîne vide, insensible à la casse, depuis le début » | `Saved/Logs/portage/L7i_LPTestStringCompat_*.log` | -| Simulation | 385 nœuds (`Contains` 160, `Split` 222, `FindSubstring` 3), 28 Blueprints, 0 cas non pris en charge | `etat/L7i_LPFixStringSearch_20261001-191027.txt` | -| Application (contrôle intégré) | 385/385 remplacés, 28 Blueprints sauvegardés, 0 écart | `etat/L7i_LPFixStringSearch_20261001-191311.txt` | -| Diff indépendant (`DiffRemplacementNoeuds.py … CX-B12`) | 385 paires, liaisons et valeurs par défaut identiques ; 0 autre objet modifié ; profils L7a (56/56) et L7b (6/6) inchangés | `Saved/Logs/portage/L7i_CX-B12_assets_{avant,apres}.txt` | -| `CompileAllBlueprints` | 0 erreur, 0 avertissement | `etat/L7i_CompileAllBlueprints_20261001-191653.txt` | -| Build empaquetée `LifePlay_Build_5.5_L7i_B12` | cuisson 0 erreur, 0 avertissement (164 paquets recuits) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L7i_CX-B12_BuildCookRun.log` | - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-043 | 2026-10-01 | L7i (CX-B12) | — (validation) | partie utilisateur de 46 min sur `LifePlay_Build_5.5_L7i_B12`, même parcours que l'A/B : « pas de soucis en vue » | — | - -| Par personnage (`LogStats.ps1`) | 4.27 (`AB427`) | 5.5 avant (`AB55`) | 5.5 + CX-B12 (`B12_validation_20261001.log`) | -|---|---|---|---| -| Personnages | 788 | 562 | 1 044 | -| `Accessed None` | 16,5 | 17,8 | 18,3 | -| Index hors bornes | 8,7 | 12,4 | 9,7 | -| Erreurs (session) | 6 | 30 | 7 | -| `getBeautyValue` (par minute) | 1,3 | 52 | 2,3 | -| Plantage | 0 | 0 | 0 | - -**Lot L7i clos.** Leçon : comparer 4.27 et 5.5 sur un parcours identique (nouvelle partie, vitesse normale) ; -les sessions libres (accélération du temps, lieux différents) faussent les taux et ont conduit à deux fausses pistes -(variable `AI`, Quick dress). - -### L7h — vérification StreetMap (ERR-018), 2026-10-01 - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-044 | 2026-10-01 | L7h | `Source/LifePlayEditorTools/LPCheckStreetMapCommandlet` (option `-Path`), `LPCheckMeshesCommandlet` (nouveau), `LifePlayEditorTools.Build.cs` (module `MeshDescription`) | ERR-018 : tailles de fichiers StreetMap divisées par 3 à 15 | `git checkout L7h-avant -- Source` | - -Méthode : copie temporaire de `Heavy/StreetMap` 4.27 (560 fichiers, 276 Mo, lecture seule de la source) sous -`Content/_Temp427`, mesurée par l'éditeur 5.5 avec les mêmes commandlets que les assets 5.5, puis supprimée. - -| Contrôle | 4.27 | 5.5 | Preuve | -|---|---|---|---| -| 280 villes `UStreetMap` : routes, nœuds, bâtiments, eaux, parcs, bâtiments nommés, aménités | — | **0 ligne différente** (ex. `CentralLondon` : 18 460 routes, 49 425 bâtiments, 5 191 aménités) | `etat/L7h_streetmap_{427,55}.csv` | -| 280 maillages `Static` : LOD, sommets et triangles sources et de rendu, sections, matériaux, dimensions | — | **0 ligne différente** (ex. `CentralLondon` : 1 084 265 triangles) | `etat/L7h_meshes_{427,55}.csv` | - -Conclusion : aucune perte de données. La réduction de taille vient du format UE5, qui ne stocke plus les données de -rendu construites dans le fichier de l'éditeur (reconstruites depuis les données sources et mises en cache). Le -niveau `Scenes/CentralLondon.umap` (1,0 → 0,4 Mo) est joué dans toutes les sessions de validation sans défaut de -carte signalé. - -### L7k — erreurs d'exécution fréquentes, 2026-10-01 - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-045 | 2026-10-01 | L7k | — (analyse des journaux `AB427`, `AB55`, `B12_validation`) | ERR-006, ERR-007 et signatures résiduelles | — | - -| Par personnage | 4.27 | 5.5 avant CX-B12 | 5.5 + CX-B12 | -|---|---|---|---| -| `ContactMenu` dans `Person::UpdateDailyPose` (ERR-007) | 0,12 | 0,62 | 0,20 | -| `Near` dans `Person::standIdle` (ERR-006) | 0,05 | 0,38 | 0,09 | -| `currentMatchRace` dans `GI::determinePersonVars` | 0,04 | 0,33 | 0,09 | -| `OccupyingFnt`, `HeadtrackStatic` (`Person`) | 0,03 ; 0,04 | 0,22 ; 0,21 | 0,07 ; 0,07 | -| `clearVariable` (vérifications « qui » des scènes) | 7,2 | 6,7 | 15,3 | -| … rapporté aux vérifications de scènes | 0,37 | 0,36 | 0,39 | - -ERR-006 et ERR-007 étaient des effets secondaires de la recherche de texte vide (ERR-005) : ils reviennent au niveau -4.27 sans autre correctif. `clearVariable` suit le volume de vérifications de scènes (même chaîne d'appel et mêmes -proportions `GetSpecific` / `getRelative` / `getPerson` dans les trois sessions) : la session de validation a -vérifié davantage de scènes par personnage (843/min contre 626). **Lot L7k clos sans modification.** - -### L7g — build Shipping, 2026-10-01 - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-046 | 2026-10-01 | L7g | `Source/LifePlay/LPSessionLogSubsystem.cpp` : comparaison au nom `LogLifePlay` (`FName`) au lieu de `LogLifePlay.GetCategoryName()` | la configuration Shipping ne compilait pas : `NO_LOGGING` y remplace les catégories de journal par des objets vides (`FNoLoggingCategory`, sans `GetCategoryName`) | `git checkout L7g-avant -- Source` | - -Choix de structure : la version publiée par l'auteur (`LifePlay_Build_2026.9`, UE4) est une build Shipping **en -fichiers séparés** (pas de `.pak`), conforme à `UsePakFile=False` du projet : conservé tel quel (modding). Commande : -`RunUAT BuildCookRun -platform=Win64 -clientconfig=Shipping -build -cook -iterate -stage -archive --archivedirectory=D:\Game\LifePlay_Build_5.5_Shipping`. `McpAutomationBridge` est déjà exclu des builds de jeu -(`TargetAllowList: Editor` dans le `.uproject`). - -| Contrôle | Auteur (UE4) | 5.5 Shipping | Preuve | -|---|---|---|---| -| Compilation `LifePlay Win64 Shipping` et `LifePlayEditor Win64 Development` | — | 0 erreur | `Build.bat` | -| Paquets `.uasset` | 8 561 | 8 561 | comptage | -| Fichiers `Content/Modules` (hors paquets) | 2 051 | 2 051 | comptage | -| Taille totale | 5,2 Go | 3,3 Go | format UE5 (voir L7h) | -| Cuisson | — | réussie, sans avertissement | `Saved/Logs/portage/L7g_BuildCookRun_Shipping.log` | - -En Shipping, le jeu n'écrit pas de journal (comme la version de l'auteur) : la validation se fait par une partie. - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-047 | 2026-10-01 | L7g | — (validation) | partie utilisateur sur `LifePlay_Build_5.5_Shipping` (nouvelle partie, scènes, sauvegarde et rechargement) : « pas de soucis constaté » | — | - -**Lot L7g clos.** La build Shipping est la version distribuable de référence. - -### Publication V1.1 (2026-10-01) - -Tag annoté `v1.1` (`0d6dc55`, signataire edujiro5972), branche `ue5.5-v1.1`, README anglais mis à jour. -Archive `D:\Game\LifePlay_Publication\LifePlay_Revival_V1.1_UE5.5_Full_Source_Code.7z` (script -`make_archive_v1_1.cmd`, mêmes exclusions que V1) : 4,94 Go, 25 644 fichiers, 8,74 Go décompressés, test 7-Zip OK, -SHA-256 `6dbfb18e55e51558708239856b54f757b6044307d2d1cf261e88c94baaf617ae`. Les deux fichiers `Config` modifiés par -L7c pendant l'archivage ont été vérifiés dans l'archive : identiques à `v1.1`. Lien Mega à ajouter au README après dépôt. - -### L7c — configuration, 2026-10-01 - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-049 | 2026-10-01 | L7c | `Config/DefaultEngine.ini` : `r.DynamicGlobalIlluminationMethod=0`, `r.ReflectionMethod=2`, `r.Shadow.Virtual.Enable=0`, `DefaultGraphicsRHI=DefaultGraphicsRHI_DX12` ; `Config/DefaultGame.ini` : 5 clés de nativisation retirées | rendre explicites les valeurs jouées et validées (valeurs par défaut du code moteur 5.5 : `IndirectLightRendering.cpp`, `VirtualShadowMapArray.cpp` ; RHI D3D12 relevé dans les journaux) ; nativisation supprimée depuis UE 5.0 (`UE_DEPRECATED(5.0)`) | `git checkout L7c-avant -- Config` | - -`PhysXTreeRebuildRate` est conservé : propriété toujours valide de `UPhysicsSettings` en 5.5. - -### L7d — visuel, 2026-10-01 - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-050 | 2026-10-01 | L7d | `Source/LifePlayEditorTools/LPConvertAtmosphericFogCommandlet` (nouveau) ; `Content/Scenes/CentralLondon.umap` | `AAtmosphericFog` déprécié depuis 4.26 : remplacé par un `ASkyAtmosphere` équivalent | `Tools\Restore.ps1 -Lot L7d` | - -Constats : en 5.5, `UAtmosphericFogComponent` dérive de `USkyAtmosphereComponent` et se convertit au chargement -(`SetPositionToMatchDeprecatedAtmosphericFog` : planète posée à Z = −100 000, mode `PlanetTopAtComponentTransform`) ; -le rendu passait donc déjà par SkyAtmosphere. Éclairage précalculé : `CentralLondon_BuiltData` 40 Ko (4.27) / -43 Ko (5.5), aucun message de reconstruction : rien à reconstruire (la ville est éclairée dynamiquement). - -| Contrôle | Résultat | Preuve | -|---|---|---| -| Simulation | 1 acteur, 26 propriétés SkyAtmosphere relevées (valeurs effectives) | `etat/L7d_LPConvertAtmosphericFog_20261001-215150.txt` | -| 1er essai | écart de transformation détecté (transformation monde non recalculée hors monde enregistré) : **carte non sauvegardée** ; corrigé (`UpdateComponentToWorld`) | `etat/L7d_LPConvertAtmosphericFog_20261001-215247.txt` | -| Application | 26/26 propriétés et transformations relative et monde identiques ; carte sauvegardée | `etat/L7d_LPConvertAtmosphericFog_20261001-215445.txt` | -| Diff `LPDumpAssets` de la carte | 387 objets avant et après ; seuls changements : `AtmosphericFog_1` (+3 composants) → `SkyAtmosphere_0` (+3 composants), même étiquette, `RelativeLocation` Z = −100 000 et `TransformMode` explicités (valeurs effectives de l'ancien composant) | `Saved/Logs/portage/L7d_map_{avant,apres}.txt` | - -### L7e, L7f — évaluation, 2026-10-01 - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-051 | 2026-10-01 | L7e, L7f | — | évaluation | — | - -- **L7e** : 4 systèmes Cascade (`Heavy/New3/Particles` : `ParticleBurst`, `ParticleGravity`, `ParticleLiquid`, - `ParticleSquirt`), toujours pris en charge par UE 5.5 ; une conversion vers Niagara changerait l'aspect des effets - par rapport à 4.27 : reportée au jour où le moteur retirera Cascade. Aucun actif ne contient de vêtement physique - (`ClothingAssetCommon` absent en 4.27 comme en 5.5) : rien à migrer de NvCloth vers Chaos. -- **L7f** : `DefaultPlayerInputClass=EnhancedPlayerInput` et `DefaultInputComponentClass=EnhancedInputComponent` - sont déjà actifs ; ils exécutent les 34 actions et axes hérités de `DefaultInput.ini`. Une migration complète - (Input Actions, Mapping Contexts, réécriture des événements d'entrée des Blueprints) n'apporterait rien au joueur - et demanderait un test manuel des 34 commandes : reportée. - -### L7j — image du contact pendant un appel (ERR-031, amélioration), 2026-10-01 - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-052 | 2026-10-01 | L7j | `Source/LifePlayEditorTools/LPFixSceneCaptureCommandlet` (nouveau) ; `Content/Scenes/CentralLondon.umap` | ERR-031 (présent en 4.27) : la caméra `SecondScreen` filme le ciel | `Tools\Restore.ps1 -Lot L7j` | - -Chaîne (L6f et L7j) : `GI::secondScreen` marque le contact (`Person.secondScreen`) ; `GI::Show` le place sur la position -de son emplacement de scène (`ActorTransforms[i]`) **décalée de −10 000 000 en Y**, sur la copie de studio -`SecondScreenFloor` (−3 131 543 ; −24 967 212, soit le studio des scènes − 10 000 000 Y) ; l'encadré affiche -`Render/SecondScreen`, rendu par la caméra `SecondScreen`. Aucun Blueprint ne déplace cette caméra ; dans la carte -(4.27 comme 5.5) elle n'a pas de racine (`RootComponent = None`) et ses composants sont à l'origine. - -Correctif : racine = son `SceneComponent` existant, capture rattachée, position = `CameraScene` (cadrage des scènes du -studio : −3 131 132 ; −14 967 241 ; 50, lacet 180°) − 10 000 000 Y. Contrôle intégré (racine, rattachement, position -monde = cible) puis diff `LPDumpAssets` de la carte : **3 lignes modifiées**, toutes sur `SecondScreen` -(`Saved/Logs/portage/L7d_map_apres.txt` → `L7j_map_apres.txt`). Le cadrage (scène entière plutôt que portrait) est à -ajuster d'après le test en jeu si nécessaire (`-From=CameraPortrait`). - -### L7l — personnages enfoncés dans le sol en extérieur (ERR-036, amélioration), 2026-10-01 : diagnostic - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-053 | 2026-10-01 | L7l | `Source/LifePlayEditorTools/LPCheckMeshesCommandlet` (colonnes `MinZ`, `MaxZ`) | diagnostic | — | - -Le lieu `Modules/vin_Base/World/forest___vin01.lpworld` (donnée de module, identique en 4.27) pose comme sol le meuble -`fnt_new208` (dalle 230 × 99 × 0,6) en Z = −31,2 avec une échelle (0,713 ; 1,481 ; **100**) ; ailleurs (`bathroom`, -`shower`, `sports_centre`) le même meuble est un petit objet (Z = 60, échelle 5). Les meubles sont rattachés à un point -d'ancrage en Z = 60 (`Room::addObject`). La hauteur exacte de la surface de neige par rapport au sol de marche dépend -de la chaîne de composants du meuble : une mesure en jeu est nécessaire avant de corriger le fichier (ou d'ajuster la -dalle avec l'éditeur de lieu intégré au jeu, « Edit building »). - -Test utilisateur (build `LifePlay_Build_5.5_L7cdj`) : le contact apparaît désormais dans l'encadré d'appel (en pied, -avec le sol de `SecondScreenFloor`) ; demande : cadrer le visage. - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-054 | 2026-10-01 | L7j | `Source/LifePlay/LPSecondScreenSubsystem.h/.cpp` (nouveau sous-système d'instance de jeu) | cadrer le visage du contact quel que soit son emplacement de scène : à chaque image, la caméra `SecondScreen` est placée à 80 cm devant la tête de `GI.SecondScreenPerson` (os `head`, sinon haut de la capsule), dans l'axe du personnage, champ 30° ; composant rendu mobile au premier usage. Aucun asset ni Blueprint modifié | `git checkout L7j2-avant -- Source` | - -Compilation `LifePlayEditor` Development et `LifePlay` Shipping : 0 erreur. La position fixe posée par CHG-052 reste -la position de repli (avant tout appel). - -Suite L7l (2026-10-01) : inspection en direct dans l'éditeur (serveur MCP, lecture seule) : la racine de l'acteur -`Room` porte l'échelle de la première ligne du `.lpworld` (studio : 8 ; 8 ; 0,2 ; forêt : 6 ; 16 ; **0,01**) ; positions -et échelles des meubles sont donc multipliées par 0,01 en hauteur : la dalle `fnt_new208` affleure le sol (≈ +0,3 cm -contre ≈ +0,5 cm). **La dalle n'est pas surélevée** : l'écart vient de la hauteur à laquelle les personnages sont -posés. Mesure en jeu préparée : - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-055 | 2026-10-01 | L7l | `Source/LifePlay/LPSessionLogSubsystem.cpp` : option `-LPDumpDelay` (délai du relevé sur motif) | relever, 4 s après chaque changement de lieu (`test0706 amenity`), le sol du lieu (`Room`), la capsule du joueur et du personnage en interaction ; script `LifePlay_releve_L7l.bat` du build `LifePlay_Build_5.5_L7cdj` | `git checkout L7j2-apres -- Source` | - -Mesures L7l (2026-10-01, `L7l_releve2_20261001.log`) : **tous les lieux sont construits dans le studio** -(acteur unique `Room_C_0` en −3 131 543 ; −14 967 213 ; −115), la ville étant masquée (`StreetMapStatic.bHidden`). Au -centre sportif (échelle Z 0,2) : sol à −105, pieds du joueur à −100 (capsule −12, demi-hauteur 88) : correct. -Échelles verticales déclarées : 81 lieux à 0,2, 5 à 0,3, 1 à 0,4 et les **7 lieux extérieurs à 0,01** (beach, forest, -mountain, park, pool, river, street). Hypothèse : le plafond `WallT` (cube blanc, Z relatif 2 040, sans collision) -suit l'échelle : 4,08 m en intérieur mais **20 cm au-dessus du sol en extérieur**, où il masque tout ce qui est plus bas -(pieds, pieds des meubles) et apparaît comme le « sol » blanc ; les personnages marchent sur le vrai sol, dessous. - -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-056 | 2026-10-01 | L7l | `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` : option `-LPDumpEvery=N` (relevé périodique) | confirmer l'hypothèse du plafond par une mesure en extérieur (`LifePlay_releve_L7l.bat`) | `git checkout L7j2-apres -- Source` | +sur cette partie : famille ERR-003, à suivre en L7i. + +### L7b — R-04 : derniers nœuds dépréciés (2026-09-30) + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| CHG-037 | 2026-09-30 | L7b (CX-B11) | `Source/LifePlayEditorTools/LPFixNavRandomPointCommandlet.h/.cpp` (nouveau) ; `Tools/DiffRemplacementNoeuds.py` (profils par lot : `L7a`, `L7b`) ; `Content/BPs/AI`, `Person`, `Room` (`etat/L7b_assets.txt`) | ERR-035 / R-04 : 6 nœuds `GetRandomPointInNavigableRadius` dépréciés | `Tools\Restore.ps1 -Lot L7b` ; `git checkout L7b-avant -- Source Tools` | + + +Choix : le remplaçant proposé par le moteur (`K2_GetRandomLocationInNavigableRadius`) est `BlueprintCallable`, alors que +l'ancien nœud est pur ; le brancher imposerait de recâbler le flux d'exécution. On réutilise `LPNavigationHelpers` +(enrobage pur déjà présent, CX-C09) : mêmes broches, et même corps que la fonction dépréciée en 4.27 comme en 5.5 +(simple appel de `K2_GetRandomLocationInNavigableRadius`). `LPFixDeprecatedNodes` (CX-B06) n'est pas réutilisée : elle +ne contrôle rien et embarque aussi la variante R-03 rejetée. + + +| Contrôle | Résultat | Preuve | +| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| Simulation | 6 nœuds (`Room::moveToBedIfPossible`, `Room::getStandSpots`, `Person::standIdle` ×2, `Person::jumpOffFnt`, `AI::EventGraph`), 0 cas non pris en charge | `etat/L7b_LPFixNavRandomPoint_20260930-183406.txt` | +| Application (contrôle intégré : liaisons et valeurs par défaut de chaque broche) | 6/6 remplacés, 3 Blueprints sauvegardés, 0 écart | `etat/L7b_LPFixNavRandomPoint_20260930-183707.txt` | +| Diff indépendant `LPDumpAssets` (`DiffRemplacementNoeuds.py … L7b`) | 6 paires ; 7 broches identiques (liaisons et défauts, ex. `Radius` 100 / 200 / 1000 / 2000 / 100000) ; **0 autre objet modifié** ; le profil `L7a` rejoué sur les données L7a donne toujours 56/56 et 0 écart | `Saved/Logs/portage/L7b_assets_{avant,apres}.txt` | +| `CompileAllBlueprints` | 0 erreur, **0 avertissement** (6 avant) | `etat/L7b_CompileAllBlueprints_20260930-183909.txt` | +| Build empaquetée `LifePlay_Build_5.5_L7b` | cuisson 0 erreur, **0 avertissement** (3 avant) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L7b_BuildCookRun.log` | + + +Validation en jeu en attente (déplacements des personnages : `standIdle`, `jumpOffFnt`, `moveToBedIfPossible`, IA). + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| CHG-038 | 2026-09-30 | L7b | `Content/BPs/AI`, `Person`, `Room` restaurés (`Tools\Restore.ps1 -Lot L7b`, MD5 = `L7b-avant`) ; versions L7b conservées dans `LifePlay_Backup_5.5\L7b\_avant-restauration‚60930-193820` | test en jeu du build L7b (utilisateur) : les personnages se téléportent dans la pièce au lieu de marcher, pieds parfois masqués par un objet ; « la build précédente était meilleure » | réappliquer : `Restore.ps1` depuis `_avant-restauration`, ou relancer `LPFixNavRandomPoint` | + + +Analyse : aucune cause trouvée dans le code. Ancienne et nouvelle fonction exécutent le même appel moteur +(`K2_GetRandomLocationInNavigableRadius`), mêmes broches et valeurs par défaut (diff 6/6), et le compilateur remplit la +broche cachée `WorldContextObject` de la même façon pour les deux (`FKCHandler_CallFunction::RegisterNets`, métadonnée +`WorldContext`). Le journal de la partie ne contient aucun message de navigation. Prochaine étape : A/B au même endroit +avec le build L7a (identique au contenu actuel), et vérifier le comportement en 4.27, avant toute nouvelle tentative. + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| CHG-039 | 2026-09-30 | L7b | `Content/BPs/AI`, `Person`, `Room` remis à `L7b-apres` (`git checkout L7b-apres`, MD5 identiques aux versions conservées dans `_avant-restauration‚60930-193820`) | retour utilisateur : la téléportation venait de l'accélération du temps (en vitesse normale les personnages marchent) ; les personnages enfoncés dans le sol en extérieur existent aussi en 4.27 et en L7a/V1 (ERR-036, hors périmètre). Le retrait CHG-038 n'était donc pas justifié | `Tools\Restore.ps1 -Lot L7b` | + + +L7b validé en jeu : le build testé `LifePlay_Build_5.5_L7b` contient exactement ces 3 paquets. Leçon : avant de retirer +un lot sur un symptôme visuel, vérifier la vitesse de jeu et demander si le défaut existe en 4.27 et dans le build précédent. + +### L7i — variables DSL `Person` vides (ERR-003 / ERR-030), 2026-09-30 + +Mesure (journaux `L7a_partie`, `L7b_partie`, 4.27 `LifePlay_Build_427_Dev\...\LifePlay.log`, par minute) : + + +| Signature | 4.27 (23 min) | L7a (62 min) | L7b (45 min) | +| -------------------------------------------------- | ------------- | ------------ | ------------ | +| `Accessed None … Person` dans `GI::Dress` | 376 | 326 | 641 | +| `index 0 … array 'Person'` (variable DSL vide) | 9 | 179 | 56 | +| `index … array 'Floats'` | 2,4 | 31 | 64 | +| `Accessed None … Person` dans `getActorVar` | 0 | 45 | 7,6 | +| `Accessed None … Person` dans `ReturnStatIfNeeded` | 0,35 | 33 | 5,6 | + + +Conclusion : **ERR-030 (**`Dress`**) existe en 4.27** (même signature `Dress:0E35`, même ordre de grandeur) : préexistant, +hors périmètre. L'écart propre à 5.5 porte sur les variables DSL vides (ERR-003). Lecture du graphe +(`GI::GetVariable`) : la variable `Player` est construite avec `Person = [Special]`, `Special = GI.PlayerCharacter` +(`SwitchString_55`, branche `Player`) ; le capteur L6a montre `Special = None`. Les erreurs surviennent tout au long de +la partie, pas seulement au chargement. + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------- | +| CHG-040 | 2026-09-30 | L7i | `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` : options `-LPTraceFunctions`, `-LPTraceDepth` (variables locales des cadres appelants), `-LPTraceGIVars` (variables de l'instance de jeu) ; inactives sans `-LPTraceScriptEx` | capteur ciblé ERR-003 : quelle scène et quelle variable restent vides | `git checkout L7i-avant -- Source` | + + +Build de diagnostic `LifePlay_Build_5.5_L7i` (contenu identique à L7b, 8561 `.uasset`), lancement par +`LifePlay_diagnostic_L7i.bat` (300 relevés, 4 cadres appelants, `-ScriptStackOnWarnings`). + +Session capteur n° 1 (`L7i_partie2_20261001.log`, 53 min, 300 relevés) : les variables vides viennent surtout des +scripts d'IA des PNJ (`.lpai` : `meal`, `church`, `grocery`, `entertainment`…), via `UpdateAI → PlayScene → ReadSceneLine → CallFunction → getActorVar/setActorVar`. La variable vide est `AI`, alimentée par `GI.AIDummy` +(personnage factice du pool `DummyPersons`, attribué par `SpawnDummy`). `GI.PlayerCharacter` n'est pas None. Dans +`GetVariable`, toutes les variables spéciales (`Player`, `Ptr`, `Interacting`, `CurrentCompanion`, `A1`…`A5`) +passent par le même `MakeStruct` dont le nom est figé à « Player » : l'étiquette relevée en L6a était trompeuse. + +Nouveau défaut signalé (utilisateur, 2026-10-01) : **Quick dress laisse souvent le personnage principal nu ; correct +en 4.27** → ERR-037, régression UE5. Chemin : `Action` (bouton « Quick dress ») → `GI::Dress(PersonVariable="Player", ClothVariables=[], QuickDress=true)` → pour chaque emplacement obligatoire (`Top`, `Bottom`, `Foot`), filtre de +l'inventaire du joueur (sexe, emplacement, `Overlaps`, `Daz == (Race == "Daz")`, exclusion `Wedding/Nurse/Doctor`). +Inventaire de la sauvegarde `1.sav` cohérent (80 vêtements, `Daz=True`). + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------ | +| CHG-041 | 2026-10-01 | L7i | `Source/LifePlayEditorTools/LPDumpSaveCommandlet.h/.cpp` (nouveau : contenu d'un `.sav` en texte, lecture seule) ; `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` : relevé déclenché par un motif du journal (`-LPDumpOnLog`, `-LPDumpPaths`, `-LPDumpMax`), inactif sans l'option | ERR-037 : relever l'état du joueur au moment du Quick dress ; ERR-003 : état d'`AIDummy` | `git checkout L7i-capteur -- Source` | + + +Build `LifePlay_Build_5.5_L7i` reconstruite (contenu identique), lancement `LifePlay_diagnostic_L7i_2.bat`. + +Session capteur n° 2 (`L7i_partie3_20261001.log`, 5 min, 8 relevés Quick dress, 60 relevés IA) : + +- **Quick dress (ERR-037)** : à la maison, 4 essais sur 4 corrects (haut, bas, chaussures, accessoires). Au travail, +3 essais sur 3 ne mettent que `Trench-Coat-Outfit-Top`. État relevé : `Race = "Daz"`, `IsMale = False`, +inventaire de 80 vêtements. Au travail (`CurrentAmenity = work`), `GI::Dress` n'accepte que les vêtements +`Bool3` (étiquette « Formal ») ; l'inventaire n'en contient que 2 (`HC-Business-Jacket` en `Outerwear`, +`Trench-Coat-Outfit-Top` en `Top`), aucun bas ni chaussure formels. Quand aucun vêtement ne convient à un +emplacement, la sortie `Completed` de la boucle n'est pas câblée : l'emplacement reste vide. Graphe identique à +4.27 → comportement d'origine, à confirmer par un essai 4.27 au travail. +- **Variable** `AI` **vide** : `GI.AIDummy` est valide au moment de l'erreur ; les erreurs portent toujours sur les +emplacements de PNJ `CheckingAiIndex` 5 et 8. `UpdateAI` charge le PNJ dans `AIDummy` (`LoadPreset`, +`StatOnly`) puis `AddVariable("AI", [AIDummy])` ; à l'erreur, `GetVariable("AI")` renvoie une variable vide +(non trouvée). Piste suivante : ce qui retire ou masque `AI` pour ces deux PNJ. + +Suite L7i (2026-10-01) : + +- ERR-037 (Quick dress) **préexistant** : même comportement en 4.27 au travail (essai utilisateur). Journal 4.27 de +l'essai archivé : `Saved/Logs/portage/ref427_quickdress_20261001.log` ; la session 4.27 de référence (23 min) est +archivée sous `ref427_partie_20260930.log` (le `LifePlay.log` 4.27 avait été remplacé par l'essai). +- Variable `AI` introuvable : au moment de l'erreur, la boucle de vérification des scènes du joueur (`CHECKING:`) +s'exécute entre les lignes du script d'IA ; les deux boucles avancent par `Delay(0)` (macro `GI:Async`) et +partagent la table des variables DSL. Moteur : `FDelayAction`, `UKismetSystemLibrary::Delay` et +`FLatentActionManager::ProcessLatentActions` équivalents 4.27/5.5 ; images par seconde équivalentes (≈ 55). +Écart mesuré : par cycle de vérification, ≈ 140 scènes en 4.27 contre ≈ 430 en 5.5 dont 100 vérifiées 2 à 3 fois +(redémarrages : `CheckingSceneIndex` remis à -1 et liste remélangée à chaque changement de `CurrentAmenity`). +Les sessions comparées n'ont pas suivi le même parcours (déplacements, accélération du temps) : une comparaison +A/B sur un parcours identique est nécessaire avant de conclure à une régression. + +A/B à parcours identique (2026-10-01, nouvelle partie, vitesse normale, maison puis travail ; 4.27 47 min, +`AB427_20261001.log` ; 5.5 33 min, `AB55_20261001.log`) : + + +| Par minute | 4.27 | 5.5 | +| --------------------------------- | ---- | --- | +| Vérifications de scènes | 626 | 614 | +| `AI` introuvable (`getActorVar`…) | 0 | 0 | +| Variables `Person` vides | 124 | 123 | +| `getBeautyValue` (`Floats` vide) | 1,3 | 52 | + + +Les erreurs `AI` dépendaient du rythme de jeu (accélération du temps) : absentes à parcours égal, classées comme +préexistantes (même mécanisme en 4.27). Seul écart réel : `getBeautyValue`. + +Cause (ERR-005) : `BeautyStandards` est indexé par forme (`round`, `oval`…, fichier +`Modules/beauty_standards_default.txt` identique dans les deux versions) ; la première recherche par nom de morph +échoue normalement (62 avertissements en 4.27 aussi). L'écart vient du nombre d'appels : `filterMorphList` est +appelé sans étiquette et teste `Contains(tags, "")`. Moteur : `FString::Find` 4.27 → `FCString::Stristr`, qui +cherche le caractère nul et ne trouve rien ; 5.5 → `TCString::Strnistr`, qui renvoie le début de la chaîne. Les +autres modes (sensible à la casse, depuis la fin) et `StartsWith`, `EndsWith`, `Replace` sont identiques. + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------- | +| CHG-042 | 2026-10-01 | L7i (CX-B12) | `Source/LifePlay/LPStringCompat.h/.cpp` (`FindUE4`, `Contains`, `FindSubstring`, `Split`) ; `Source/LifePlayEditorTools/LPFixStringSearchCommandlet`, `LPTestStringCompatCommandlet`, `LPDumpCDOCommandlet` (nouveaux) ; `Tools/DiffRemplacementNoeuds.py` (profil `CX-B12`, broches absentes des deux côtés) ; 28 Blueprints (`etat/L7i_CX-B12_assets.txt`) | ERR-005 : recherche de texte avec motif vide | `Tools\Restore.ps1 -Lot L7i-CX-B12` ; `git checkout L7i-CX-B12-avant -- Source Tools` | + + + +| Contrôle | Résultat | Preuve | +| --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| Test `LPTestStringCompat` : transcription littérale de `FString::Find` 4.27 contre `LPStringCompat` et contre le moteur 5.5 | 3 120 cas : compat = 4.27 dans 100 % des cas ; moteur 5.5 ≠ 4.27 dans 60 cas, tous « sous-chaîne vide, insensible à la casse, depuis le début » | `Saved/Logs/portage/L7i_LPTestStringCompat_*.log` | +| Simulation | 385 nœuds (`Contains` 160, `Split` 222, `FindSubstring` 3), 28 Blueprints, 0 cas non pris en charge | `etat/L7i_LPFixStringSearch_20261001-191027.txt` | +| Application (contrôle intégré) | 385/385 remplacés, 28 Blueprints sauvegardés, 0 écart | `etat/L7i_LPFixStringSearch_20261001-191311.txt` | +| Diff indépendant (`DiffRemplacementNoeuds.py … CX-B12`) | 385 paires, liaisons et valeurs par défaut identiques ; 0 autre objet modifié ; profils L7a (56/56) et L7b (6/6) inchangés | `Saved/Logs/portage/L7i_CX-B12_assets_{avant,apres}.txt` | +| `CompileAllBlueprints` | 0 erreur, 0 avertissement | `etat/L7i_CompileAllBlueprints_20261001-191653.txt` | +| Build empaquetée `LifePlay_Build_5.5_L7i_B12` | cuisson 0 erreur, 0 avertissement (164 paquets recuits) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L7i_CX-B12_BuildCookRun.log` | + + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------- | -------------- | +| CHG-043 | 2026-10-01 | L7i (CX-B12) | — (validation) | partie utilisateur de 46 min sur `LifePlay_Build_5.5_L7i_B12`, même parcours que l'A/B : « pas de soucis en vue » | — | + + + +| Par personnage (`LogStats.ps1`) | 4.27 (`AB427`) | 5.5 avant (`AB55`) | 5.5 + CX-B12 (`B12_validation_20261001.log`) | +| ------------------------------- | -------------- | ------------------ | -------------------------------------------- | +| Personnages | 788 | 562 | 1 044 | +| `Accessed None` | 16,5 | 17,8 | 18,3 | +| Index hors bornes | 8,7 | 12,4 | 9,7 | +| Erreurs (session) | 6 | 30 | 7 | +| `getBeautyValue` (par minute) | 1,3 | 52 | 2,3 | +| Plantage | 0 | 0 | 0 | + + +**Lot L7i clos.** Leçon : comparer 4.27 et 5.5 sur un parcours identique (nouvelle partie, vitesse normale) ; +les sessions libres (accélération du temps, lieux différents) faussent les taux et ont conduit à deux fausses pistes +(variable `AI`, Quick dress). + +### L7h — vérification StreetMap (ERR-018), 2026-10-01 + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | ---------------------------------- | +| CHG-044 | 2026-10-01 | L7h | `Source/LifePlayEditorTools/LPCheckStreetMapCommandlet` (option `-Path`), `LPCheckMeshesCommandlet` (nouveau), `LifePlayEditorTools.Build.cs` (module `MeshDescription`) | ERR-018 : tailles de fichiers StreetMap divisées par 3 à 15 | `git checkout L7h-avant -- Source` | + + +Méthode : copie temporaire de `Heavy/StreetMap` 4.27 (560 fichiers, 276 Mo, lecture seule de la source) sous +`Content/_Temp427`, mesurée par l'éditeur 5.5 avec les mêmes commandlets que les assets 5.5, puis supprimée. + + +| Contrôle | 4.27 | 5.5 | Preuve | +| ------------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------- | --------------------------------- | +| 280 villes `UStreetMap` : routes, nœuds, bâtiments, eaux, parcs, bâtiments nommés, aménités | — | **0 ligne différente** (ex. `CentralLondon` : 18 460 routes, 49 425 bâtiments, 5 191 aménités) | `etat/L7h_streetmap_{427,55}.csv` | +| 280 maillages `Static` : LOD, sommets et triangles sources et de rendu, sections, matériaux, dimensions | — | **0 ligne différente** (ex. `CentralLondon` : 1 084 265 triangles) | `etat/L7h_meshes_{427,55}.csv` | + + +Conclusion : aucune perte de données. La réduction de taille vient du format UE5, qui ne stocke plus les données de +rendu construites dans le fichier de l'éditeur (reconstruites depuis les données sources et mises en cache). Le +niveau `Scenes/CentralLondon.umap` (1,0 → 0,4 Mo) est joué dans toutes les sessions de validation sans défaut de +carte signalé. + +### L7k — erreurs d'exécution fréquentes, 2026-10-01 + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ---------------------------------------------------------- | ------------------------------------------ | -------------- | +| CHG-045 | 2026-10-01 | L7k | — (analyse des journaux `AB427`, `AB55`, `B12_validation`) | ERR-006, ERR-007 et signatures résiduelles | — | + + + +| Par personnage | 4.27 | 5.5 avant CX-B12 | 5.5 + CX-B12 | +| ------------------------------------------------------ | ----------- | ---------------- | ------------ | +| `ContactMenu` dans `Person::UpdateDailyPose` (ERR-007) | 0,12 | 0,62 | 0,20 | +| `Near` dans `Person::standIdle` (ERR-006) | 0,05 | 0,38 | 0,09 | +| `currentMatchRace` dans `GI::determinePersonVars` | 0,04 | 0,33 | 0,09 | +| `OccupyingFnt`, `HeadtrackStatic` (`Person`) | 0,03 ; 0,04 | 0,22 ; 0,21 | 0,07 ; 0,07 | +| `clearVariable` (vérifications « qui » des scènes) | 7,2 | 6,7 | 15,3 | +| … rapporté aux vérifications de scènes | 0,37 | 0,36 | 0,39 | + + +ERR-006 et ERR-007 étaient des effets secondaires de la recherche de texte vide (ERR-005) : ils reviennent au niveau +4.27 sans autre correctif. `clearVariable` suit le volume de vérifications de scènes (même chaîne d'appel et mêmes +proportions `GetSpecific` / `getRelative` / `getPerson` dans les trois sessions) : la session de validation a +vérifié davantage de scènes par personnage (843/min contre 626). **Lot L7k clos sans modification.** + +### L7g — build Shipping, 2026-10-01 + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | +| CHG-046 | 2026-10-01 | L7g | `Source/LifePlay/LPSessionLogSubsystem.cpp` : comparaison au nom `LogLifePlay` (`FName`) au lieu de `LogLifePlay.GetCategoryName()` | la configuration Shipping ne compilait pas : `NO_LOGGING` y remplace les catégories de journal par des objets vides (`FNoLoggingCategory`, sans `GetCategoryName`) | `git checkout L7g-avant -- Source` | + + +Choix de structure : la version publiée par l'auteur (`LifePlay_Build_2026.9`, UE4) est une build Shipping **en +fichiers séparés** (pas de `.pak`), conforme à `UsePakFile=False` du projet : conservé tel quel (modding). Commande : +`RunUAT BuildCookRun -platform=Win64 -clientconfig=Shipping -build -cook -iterate -stage -archive -archivedirectory=D:\Game\LifePlay_Build_5.5_Shipping`. `McpAutomationBridge` est déjà exclu des builds de jeu +(`TargetAllowList: Editor` dans le `.uproject`). + + +| Contrôle | Auteur (UE4) | 5.5 Shipping | Preuve | +| --------------------------------------------------------------------------- | ------------ | --------------------------- | -------------------------------------------------- | +| Compilation `LifePlay Win64 Shipping` et `LifePlayEditor Win64 Development` | — | 0 erreur | `Build.bat` | +| Paquets `.uasset` | 8 561 | 8 561 | comptage | +| Fichiers `Content/Modules` (hors paquets) | 2 051 | 2 051 | comptage | +| Taille totale | 5,2 Go | 3,3 Go | format UE5 (voir L7h) | +| Cuisson | — | réussie, sans avertissement | `Saved/Logs/portage/L7g_BuildCookRun_Shipping.log` | + + +En Shipping, le jeu n'écrit pas de journal (comme la version de l'auteur) : la validation se fait par une partie. + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| CHG-047 | 2026-10-01 | L7g | — (validation) | partie utilisateur sur `LifePlay_Build_5.5_Shipping` (nouvelle partie, scènes, sauvegarde et rechargement) : « pas de soucis constaté » | — | + + +**Lot L7g clos.** La build Shipping est la version distribuable de référence. + +### Publication V1.1 (2026-10-01) + +Tag annoté `v1.1` (`0d6dc55`, signataire edujiro5972), branche `ue5.5-v1.1`, README anglais mis à jour. +Archive `D:\Game\LifePlay_Publication\LifePlay_Revival_V1.1_UE5.5_Full_Source_Code.7z` (script +`make_archive_v1_1.cmd`, mêmes exclusions que V1) : 4,94 Go, 25 644 fichiers, 8,74 Go décompressés, test 7-Zip OK, +SHA-256 `6dbfb18e55e51558708239856b54f757b6044307d2d1cf261e88c94baaf617ae`. Les deux fichiers `Config` modifiés par +L7c pendant l'archivage ont été vérifiés dans l'archive : identiques à `v1.1`. Lien Mega à ajouter au README après dépôt. + +### L7c — configuration, 2026-10-01 + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | +| CHG-049 | 2026-10-01 | L7c | `Config/DefaultEngine.ini` : `r.DynamicGlobalIlluminationMethod=0`, `r.ReflectionMethod=2`, `r.Shadow.Virtual.Enable=0`, `DefaultGraphicsRHI=DefaultGraphicsRHI_DX12` ; `Config/DefaultGame.ini` : 5 clés de nativisation retirées | rendre explicites les valeurs jouées et validées (valeurs par défaut du code moteur 5.5 : `IndirectLightRendering.cpp`, `VirtualShadowMapArray.cpp` ; RHI D3D12 relevé dans les journaux) ; nativisation supprimée depuis UE 5.0 (`UE_DEPRECATED(5.0)`) | `git checkout L7c-avant -- Config` | + + +`PhysXTreeRebuildRate` est conservé : propriété toujours valide de `UPhysicsSettings` en 5.5. + +### L7d — visuel, 2026-10-01 + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------- | +| CHG-050 | 2026-10-01 | L7d | `Source/LifePlayEditorTools/LPConvertAtmosphericFogCommandlet` (nouveau) ; `Content/Scenes/CentralLondon.umap` | `AAtmosphericFog` déprécié depuis 4.26 : remplacé par un `ASkyAtmosphere` équivalent | `Tools\Restore.ps1 -Lot L7d` | + + +Constats : en 5.5, `UAtmosphericFogComponent` dérive de `USkyAtmosphereComponent` et se convertit au chargement +(`SetPositionToMatchDeprecatedAtmosphericFog` : planète posée à Z = −100 000, mode `PlanetTopAtComponentTransform`) ; +le rendu passait donc déjà par SkyAtmosphere. Éclairage précalculé : `CentralLondon_BuiltData` 40 Ko (4.27) / +43 Ko (5.5), aucun message de reconstruction : rien à reconstruire (la ville est éclairée dynamiquement). + + +| Contrôle | Résultat | Preuve | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | +| Simulation | 1 acteur, 26 propriétés SkyAtmosphere relevées (valeurs effectives) | `etat/L7d_LPConvertAtmosphericFog_20261001-215150.txt` | +| 1er essai | écart de transformation détecté (transformation monde non recalculée hors monde enregistré) : **carte non sauvegardée** ; corrigé (`UpdateComponentToWorld`) | `etat/L7d_LPConvertAtmosphericFog_20261001-215247.txt` | +| Application | 26/26 propriétés et transformations relative et monde identiques ; carte sauvegardée | `etat/L7d_LPConvertAtmosphericFog_20261001-215445.txt` | +| Diff `LPDumpAssets` de la carte | 387 objets avant et après ; seuls changements : `AtmosphericFog_1` (+3 composants) → `SkyAtmosphere_0` (+3 composants), même étiquette, `RelativeLocation` Z = −100 000 et `TransformMode` explicités (valeurs effectives de l'ancien composant) | `Saved/Logs/portage/L7d_map_{avant,apres}.txt` | + + +### L7e, L7f — évaluation, 2026-10-01 + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | -------- | -------- | ---------- | -------------- | +| CHG-051 | 2026-10-01 | L7e, L7f | — | évaluation | — | + + +- **L7e** : 4 systèmes Cascade (`Heavy/New3/Particles` : `ParticleBurst`, `ParticleGravity`, `ParticleLiquid`, +`ParticleSquirt`), toujours pris en charge par UE 5.5 ; une conversion vers Niagara changerait l'aspect des effets +par rapport à 4.27 : reportée au jour où le moteur retirera Cascade. Aucun actif ne contient de vêtement physique +(`ClothingAssetCommon` absent en 4.27 comme en 5.5) : rien à migrer de NvCloth vers Chaos. +- **L7f** : `DefaultPlayerInputClass=EnhancedPlayerInput` et `DefaultInputComponentClass=EnhancedInputComponent` +sont déjà actifs ; ils exécutent les 34 actions et axes hérités de `DefaultInput.ini`. Une migration complète +(Input Actions, Mapping Contexts, réécriture des événements d'entrée des Blueprints) n'apporterait rien au joueur +et demanderait un test manuel des 34 commandes : reportée. + +### L7j — image du contact pendant un appel (ERR-031, amélioration), 2026-10-01 + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------- | +| CHG-052 | 2026-10-01 | L7j | `Source/LifePlayEditorTools/LPFixSceneCaptureCommandlet` (nouveau) ; `Content/Scenes/CentralLondon.umap` | ERR-031 (présent en 4.27) : la caméra `SecondScreen` filme le ciel | `Tools\Restore.ps1 -Lot L7j` | + + +Chaîne (L6f et L7j) : `GI::secondScreen` marque le contact (`Person.secondScreen`) ; `GI::Show` le place sur la position +de son emplacement de scène (`ActorTransforms[i]`) **décalée de −10 000 000 en Y**, sur la copie de studio +`SecondScreenFloor` (−3 131 543 ; −24 967 212, soit le studio des scènes − 10 000 000 Y) ; l'encadré affiche +`Render/SecondScreen`, rendu par la caméra `SecondScreen`. Aucun Blueprint ne déplace cette caméra ; dans la carte +(4.27 comme 5.5) elle n'a pas de racine (`RootComponent = None`) et ses composants sont à l'origine. + +Correctif : racine = son `SceneComponent` existant, capture rattachée, position = `CameraScene` (cadrage des scènes du +studio : −3 131 132 ; −14 967 241 ; 50, lacet 180°) − 10 000 000 Y. Contrôle intégré (racine, rattachement, position +monde = cible) puis diff `LPDumpAssets` de la carte : **3 lignes modifiées**, toutes sur `SecondScreen` +(`Saved/Logs/portage/L7d_map_apres.txt` → `L7j_map_apres.txt`). Le cadrage (scène entière plutôt que portrait) est à +ajuster d'après le test en jeu si nécessaire (`-From=CameraPortrait`). + +### L7l — personnages enfoncés dans le sol en extérieur (ERR-036, amélioration), 2026-10-01 : diagnostic + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ------------------------------------------------------------------------------ | ---------- | -------------- | +| CHG-053 | 2026-10-01 | L7l | `Source/LifePlayEditorTools/LPCheckMeshesCommandlet` (colonnes `MinZ`, `MaxZ`) | diagnostic | — | + + +Le lieu `Modules/vin_Base/World/forest___vin01.lpworld` (donnée de module, identique en 4.27) pose comme sol le meuble +`fnt_new208` (dalle 230 × 99 × 0,6) en Z = −31,2 avec une échelle (0,713 ; 1,481 ; **100**) ; ailleurs (`bathroom`, +`shower`, `sports_centre`) le même meuble est un petit objet (Z = 60, échelle 5). Les meubles sont rattachés à un point +d'ancrage en Z = 60 (`Room::addObject`). La hauteur exacte de la surface de neige par rapport au sol de marche dépend +de la chaîne de composants du meuble : une mesure en jeu est nécessaire avant de corriger le fichier (ou d'ajuster la +dalle avec l'éditeur de lieu intégré au jeu, « Edit building »). + +Test utilisateur (build `LifePlay_Build_5.5_L7cdj`) : le contact apparaît désormais dans l'encadré d'appel (en pied, +avec le sol de `SecondScreenFloor`) ; demande : cadrer le visage. + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| CHG-054 | 2026-10-01 | L7j | `Source/LifePlay/LPSecondScreenSubsystem.h/.cpp` (nouveau sous-système d'instance de jeu) | cadrer le visage du contact quel que soit son emplacement de scène : à chaque image, la caméra `SecondScreen` est placée à 80 cm devant la tête de `GI.SecondScreenPerson` (os `head`, sinon haut de la capsule), dans l'axe du personnage, champ 30° ; composant rendu mobile au premier usage. Aucun asset ni Blueprint modifié | `git checkout L7j2-avant -- Source` | + + +Compilation `LifePlayEditor` Development et `LifePlay` Shipping : 0 erreur. La position fixe posée par CHG-052 reste +la position de repli (avant tout appel). + +Suite L7l (2026-10-01) : inspection en direct dans l'éditeur (serveur MCP, lecture seule) : la racine de l'acteur +`Room` porte l'échelle de la première ligne du `.lpworld` (studio : 8 ; 8 ; 0,2 ; forêt : 6 ; 16 ; **0,01**) ; positions +et échelles des meubles sont donc multipliées par 0,01 en hauteur : la dalle `fnt_new208` affleure le sol (≈ +0,3 cm +contre ≈ +0,5 cm). **La dalle n'est pas surélevée** : l'écart vient de la hauteur à laquelle les personnages sont +posés. Mesure en jeu préparée : + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| CHG-055 | 2026-10-01 | L7l | `Source/LifePlay/LPSessionLogSubsystem.cpp` : option `-LPDumpDelay` (délai du relevé sur motif) | relever, 4 s après chaque changement de lieu (`test0706 amenity`), le sol du lieu (`Room`), la capsule du joueur et du personnage en interaction ; script `LifePlay_releve_L7l.bat` du build `LifePlay_Build_5.5_L7cdj` | `git checkout L7j2-apres -- Source` | + + +Mesures L7l (2026-10-01, `L7l_releve2_20261001.log`) : **tous les lieux sont construits dans le studio** +(acteur unique `Room_C_0` en −3 131 543 ; −14 967 213 ; −115), la ville étant masquée (`StreetMapStatic.bHidden`). Au +centre sportif (échelle Z 0,2) : sol à −105, pieds du joueur à −100 (capsule −12, demi-hauteur 88) : correct. +Échelles verticales déclarées : 81 lieux à 0,2, 5 à 0,3, 1 à 0,4 et les **7 lieux extérieurs à 0,01** (beach, forest, +mountain, park, pool, river, street). Hypothèse : le plafond `WallT` (cube blanc, Z relatif 2 040, sans collision) +suit l'échelle : 4,08 m en intérieur mais **20 cm au-dessus du sol en extérieur**, où il masque tout ce qui est plus bas +(pieds, pieds des meubles) et apparaît comme le « sol » blanc ; les personnages marchent sur le vrai sol, dessous. + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------- | +| CHG-056 | 2026-10-01 | L7l | `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` : option `-LPDumpEvery=N` (relevé périodique) | confirmer l'hypothèse du plafond par une mesure en extérieur (`LifePlay_releve_L7l.bat`) | `git checkout L7j2-apres -- Source` | + Mesure L7l (2026-10-01, `L7l_releve3_20261001.log`, relevé toutes les 5 s) : lieu extérieur « rue » (échelle racine 3,5 ; 18 ; **0,01**) : sol à −114,5, pieds du joueur à −112 (capsule −24,2, demi-hauteur 88) : posés ; `WallT` visible, Z relatif 2 040 → **−94,6 en monde, 18 cm au-dessus des pieds** : hypothèse confirmée. En intérieur (café, police, échelle 0,2) : `WallT` à +293, pieds à −100 : correct (capture utilisateur sans défaut). -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-057 | 2026-10-01 | L7l | `Source/LifePlay/LPOutdoorRoomSubsystem.h/.cpp` (nouveau sous-système) | masquer le plafond `WallT` de `GI.Room` tant que l'échelle Z de sa racine est < 0,05 (lieux extérieurs), le rendre visible quand la pièce redevient intérieure (seulement s'il l'a masqué) ; aucun Blueprint ni asset modifié ; build `LifePlay_Build_5.5_L7cdj` | supprimer les 2 fichiers (`git checkout fa3d98e -- Source`) | -Validation en jeu (2026-10-01, build `LifePlay_Build_5.5_L7cdj`) : en forêt, pieds des personnages et sol texturé visibles (ERR-036 corrigé) ; pendant un appel, visage du contact cadré (ERR-031 corrigé) ; ciel de jour correct (L7c/L7d), nuit non vérifiée. Tags `L7j-valide`, `L7l-valide`. +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | --- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| CHG-057 | 2026-10-01 | L7l | `Source/LifePlay/LPOutdoorRoomSubsystem.h/.cpp` (nouveau sous-système) | masquer le plafond `WallT` de `GI.Room` tant que l'échelle Z de sa racine est < 0,05 (lieux extérieurs), le rendre visible quand la pièce redevient intérieure (seulement s'il l'a masqué) ; aucun Blueprint ni asset modifié ; build `LifePlay_Build_5.5_L7cdj` | supprimer les 2 fichiers (`git checkout fa3d98e -- Source`) | + + +Validation en jeu (2026-10-01, build `LifePlay_Build_5.5_L7cdj`) : en forêt, pieds des personnages et sol texturé visibles (ERR-036 corrigé) ; pendant un appel, visage du contact cadré (ERR-031 corrigé) ; ciel de jour correct (L7c/L7d), nuit non vérifiée. Tags `L7j-valide`, `L7l-valide`. + +Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (soleil fixe) **en 4.27, en L7i_B12 et en L7cdj** : comportement d'origine. Le grand disque blanc à l'horizon est présent **en L7i_B12 comme en L7cdj** : il ne vient pas de L7d (déjà présent en V1, origine non étudiée). Un retour arrière de L7d, lancé à tort avant cette comparaison, a été annulé avant tout commit ou build (`Tools\Restore.ps1 -Lot L7d-retour`, empreinte de la carte revenue à `8BD9C4A9…`). Tags `L7c-valide`, `L7d-valide`. + + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +| ------- | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CHG-058 | 2026-10-02 | Doc | nouveaux `07_Architecture_5.5.md`, `08_Guide_Developpeur.md`, `09_Commandes_Utiles.md`, `10_Protocole_Operations.md` ; mis à jour `README.md` (migration et racine), `01`, `03`, `04` (bilan §5), `05` (CX-C14 à C18, statuts), `06` (résultat V1), registre (ERR-020) | finalisation de la documentation de migration : architecture finale, guide développeur, commandes, protocole et leçons | `git revert` du commit | +| CHG-059 | 2026-10-02 | O0 | nouveaux `Tools/AuditAssets.py`, `Docs/Migration/11_Audit_Optimisation.md` ; lanceur `LifePlay_profil_O0.bat` (build L7cdj, hors dépôt) | audit d'optimisation en lecture seule : inventaire des assets, build livré, leviers et lots proposés ; aucune modification du projet | `git revert` du commit | +| CHG-060 | 2026-10-02 | O0 | `Docs/Migration/11_Audit_Optimisation.md` §9-10 ; lanceur `LifePlay_profil_O0.bat` corrigé (capture par `-ExecCmds`) | résultats de la mesure en jeu (60 images/s tenues, à-coups du thread de jeu dus aux minuteries Blueprint, 463 Mo de physique) ; produits utiles de la bibliothèque Fab ; lots révisés | `git revert` du commit | +| CHG-061 | 2026-10-02 | O1 | `LifePlay.uproject` (20 plugins limités à l'éditeur), `Config/DefaultGame.ini` (16 `DirectoriesToNeverCook`), nouveau `Tools/AuditScriptDeps.py` ; `README.md` : lien Mega de la V1.1 | build Shipping allégé de 387 Mo sans changement de contenu ; build `LifePlay_Build_5.5_O1` | `Tools\Restore.ps1 -Lot O1` ou `git checkout O1-avant -- LifePlay.uproject Config` | +| CHG-062 | 2026-10-02 | O9 | nouvelle commandlet `LPSetComponentCollision` ; `Content/BPs/MapMarkers.uasset` : composants `Tree` et `Car` en `NoCollision` (diff avant/après : 2 lignes) | chargement de partie de 71 s (identique en 4.27) dû aux ≈ 160 000 corps physiques des arbres et voitures décoratifs de la carte (décision utilisateur : option 1) ; build `LifePlay_Build_5.5_O9` | `Tools\Restore.ps1 -Lot O9` | +| CHG-063 | 2026-10-02 | O9b | `LPSetComponentCollision` (modèles des nœuds « Add Component ») ; `Content/BPs/MapMarkers.uasset` : modèles `NODE_AddHISM-0` et `-1` (`HISM_C`, marqueurs de bâtiments) en `NoCollision` (diff : 2 lignes) | les clics se font sur les icônes, pas sur les bâtiments (utilisateur) ; ≈ 49 500 corps physiques restants au chargement ; build `LifePlay_Build_5.5_O9b` | `Tools\Restore.ps1 -Lot O9b` | +| CHG-064 | 2026-10-02 | O2 | `Config/DefaultGame.ini` : `UsePakFile=True`, `bUseIoStore=True`, `bCompressed=True` (réglages par défaut du moteur : Oodle Kraken) ; `Modules` reste en fichiers nus | build Development 3 367 → 2 002 Mo (−40 %), 23 928 → 2 104 fichiers ; build `LifePlay_Build_5.5_O2` ; validé en jeu le 2026-10-02 (7 contrôles) | `Tools\Restore.ps1 -Lot O2` ou `git checkout O2-avant -- Config` | +| CHG-065 | 2026-10-02 | O4 | nouveau `Tools/FixTextureMips.py` ; 29 textures (`Clothes4` 27, route, trottoir) : `MipGenSettings` = groupe de texture, `NeverStream` = faux (diff : ces 2 propriétés + `LightingGuid` régénéré) | textures de couleur 3D sans mipmaps : scintillement à distance, pleine résolution toujours en mémoire ; exclus : cheveux et masques de transparence (29), interface et textures sans matériau (44), dimensions non puissance de 2 (733) ; build `LifePlay_Build_5.5_O4` | `Tools\Restore.ps1 -Lot O4` | +| CHG-066 | 2026-10-02 | O | build Shipping de distribution `LifePlay_Build_5.5_Shipping_V1.2` (O1, O2, O4, O9, O9b) | 3 246 → 1 494 Mo (−54 %), 24 249 → 2 084 fichiers ; premier essai sans ville (corrigé par CHG-067), reconstruit et validé en jeu le 2026-10-02 | — (build seulement) | +| CHG-067 | 2026-10-02 | O2b | `Plugins/ModdingExtra` : `GetAllFilesFromFolder` reconstruit la liste des `.uasset`/`.umap` depuis le registre d'assets quand le disque n'en renvoie aucun (contenu compressé IoStore) ; dépendance `AssetRegistry` | régression d'O2 constatée sur le build Shipping : aucune ville proposée (`UI/CityMenu` liste les fichiers `Heavy/StreetMap/*.uasset` du disque, `UI/City` vérifie chaque ville par suffixe `/.uasset`) ; seul appel concerné, les autres lisent des fichiers texte de `Modules` ; build `LifePlay_Build_5.5_Shipping_V1.2` reconstruit | `git checkout O2b-avant -- Plugins/ModdingExtra` | +| CHG-068 | 2026-10-02 | V1.2 | `README.md` (section V1.2, branche `ue5.5-v1.2`) ; tag annoté `v1.2` ; archive `D:\Game\LifePlay_Publication\LifePlay_Revival_V1.2_UE5.5_Full_Source_Code.7z` (4 937 914 101 octets, 25 681 fichiers, SHA-256 `fb298583743497799dd274cc62b8cf80fbef3445766be5898201924c64674f1a`, script `make_archive_v1_2.cmd`) | publication de la V1.2 (optimisation) ; contrôles : test 7-Zip, aucun `.mcp.json`/`.claude`/`McpAutomationBridge`/`Saved`/`Intermediate`, `Config`, `.uproject` et `ModdingExtra` identiques au tag | lien Mega à ajouter après envoi | +| CHG-069 | 2026-10-02 | Réconciliation | fusion `-s ours` de `vinfamy/master` dans `main` (commit `82a64d9`, arbre inchangé) ; `Docs/Migration/12_Reconciliation_Vinfamy.md` | historique de l'auteur rattaché ; aucun fichier récupérable ; les 33 scènes `corruption` vides de `vin_Base` existent dans `vin_Corruption` (non restaurées : doublons) | `git reset --hard reconcil-avant` (avant toute autre publication) | +| CHG-070 | 2026-10-02 | ERR-038 | nouveau `Source/LifePlay/LPRoomTransitionSubsystem.h/.cpp` | relevé (`ERR038_releve_20261002.log`) : la nouvelle pièce est construite ≈ 2 s avant que `destroyDummy` rende les personnages de l'ancienne à la réserve ; le sous-système cache aussitôt, au changement de pièce, les personnages actifs juste avant (hors compagnon et scène en cours), cesse de les suivre dès que le jeu les rend ou les réaffiche, et les réaffiche après 10 s par sécurité ; aucun Blueprint modifié ; build `LifePlay_Build_5.5_ERR038` ; test en jeu en attente | supprimer les 2 fichiers (`git checkout ERR038-avant -- Source`) | +| CHG-071 | 2026-10-02 | Ménage | `README.md` : V1.2 en téléchargement principal (lien Mega), V1.1 et V1 en versions précédentes ; suppression de 13 builds 5.5 obsolètes (V1rc, L7a, L7b, L7i, L7i_B12, L7cdj, O1, O1_essai1, O2, O4, O9, O9b, Shipping_V1.2_essai1, ≈ 43 Go) après copie vérifiée de `Saved`, `Content\Saves` et des lanceurs dans `D:\Game\LifePlay_Backup_5.5\builds_supprimees\` ; `08_Guide_Developpeur.md` §4 | lien V1.2 reçu ; place disque | builds : reconstruction depuis les tags git | +| CHG-072 | 2026-10-02 | ERR-038 | `LPRoomTransitionSubsystem` : détection par la seule échelle de la racine de `Room`, délai de réaffichage 10 s → 3 s | premier test en jeu concluant (anciens personnages cachés, compagnon présent, nouveaux personnages apparus), mais 2 faux positifs au relevé : nom de lieu vidé un instant sans changement de pièce ; arrivée au restaurant peuplée 3 s avant la construction de la pièce (personnages apparus 10 s en retard) ; build `LifePlay_Build_5.5_ERR038` reconstruit | `git checkout ERR038-apres -- Source/LifePlay/LPRoomTransitionSubsystem.*` | +| CHG-073 | 2026-10-02 | ERR-038 | `Source/LifePlay/LPRoomTransitionSubsystem.h/.cpp` supprimés (copie dans `D:\Game\LifePlay_Backup_5.5\ERR038_retrait`) ; build `LifePlay_Build_5.5_ERR038` reconstruit sans le sous-système (vérifié dans l'exécutable) | second test « plus problématique » : relevé `ERR038_apres2_20261002` = 2 déclenchements, tous deux à tort (clients du lieu d'arrivée cachés 3 s) ; le critère temporel ne distingue pas les personnages créés pendant le trajet puis retirés de ceux qui sont conservés ; retrait proposé et accepté par l'utilisateur ; ERR-038 reste ouvert | `git checkout ERR038-apres -- Source/LifePlay` | +| CHG-074 | 2026-10-02 | O7 | O7a : `Content/Saves` (14 125 fichiers, parties de test) → `D:\Game\LifePlay_Backup_5.5\O7\Content_Saves` (vérifié), `Content/Developers` et `Content/Collections` (dossiers vides) retirés, 3 captures de la racine rangées ; O7b : `08_Guide_Developpeur.md` §2.1 (carte de `Content/Heavy`, dossiers à ne pas déplacer) | ménage sans risque et documentation, à la place d'un renommage des dossiers jugé trop risqué (préfixes de chemins en dur, contrat de modding) | déplacer en retour le contenu de `O7\Content_Saves` | +| CHG-075 | 2026-10-02 | DATA1 | `Plugins/ModdingExtra/.../ModdingExtraBPLibrary.cpp` : statistiques de `LoadTxt` par type de fichier (bilan à la fermeture, commande `ModdingExtra.LoadTxtStats`) ; `11_Audit_Optimisation.md` §17 (stratégie de couche de données) | étape 1 de la couche de données : mesurer avant de construire ; `PopulateRaws` = 12,6 s par chargement de partie (trace O2) ; build `LifePlay_Build_5.5_DATA1` | `git checkout DATA1-avant -- Plugins/ModdingExtra` | +| CHG-076 | 2026-10-02 | DATA23 | nouveaux `Plugins/ModdingExtra/.../LPDataCache.h/.cpp` ; `ModdingExtra.cpp` (démarrage en arrière-plan), `ModdingExtraBPLibrary.cpp` (`LoadTxt` depuis la mémoire, `SaveTxt`, vérification, statistiques cache/disque), `ModdingExtra.Build.cs` (`Json`) | couche de données étapes 2 et 3 : `Modules` chargé une fois en arrière-plan, cache compilé JSON par type avec champs et calque ; 2 031 fichiers en 0,14 s depuis le cache ; build `LifePlay_Build_5.5_DATA23` ; validation en jeu en attente | `git checkout DATA23-avant -- Plugins/ModdingExtra` ; ou option `-LPNoDataCache` | +| CHG-077 | 2026-10-02 | O1b / DATA23 | `Config/DefaultGame.ini` : `+DirectoriesToNeverCook=(Path="/HairStrands")` retiré ; validation de la couche de données (CHG-076) : 2 658 textes servis par le cache, 0 écart, lecture 0,074 s pour la session | ERR-039 : erreurs `GroomComponent` au démarrage depuis O1 (module conservé par dépendance, contenu non cuit) ; vérifié : 0 erreur après reconstruction | remettre la ligne dans `DefaultGame.ini` | +| CHG-078 | 2026-10-02 | DATA4b | nouveaux `Source/LifePlay/LPDataCompat.h/.cpp` (`WhatWhereToArray` en C++, vérification `-LPVerifyWhatWhere` contre la fonction Blueprint d'origine) et `Source/LifePlayEditorTools/LPFixWhatWhereCommandlet.h/.cpp` ; `Content/BPs/GI.uasset` : 2 appels de l'EventGraph rebranchés (diff : 22 lignes, uniquement les 2 nœuds et les liaisons de leurs voisins) | `WhatWhereToArray` = 94 % de `PopulateScenes` (3,4 s par passage) ; premier essai refusé par le contrôle intégré (broche `Target` non exclue), rien sauvegardé, commandlet corrigée ; build `LifePlay_Build_5.5_DATA4b` ; validé en jeu le 2026-10-02 : plus de 3 000 appels vérifiés, 0 écart ; `PopulateRaws` 12,6 s → 1,26 s | `Tools\Restore.ps1 -Lot DATA4b` | +| CHG-079 | 2026-10-02 | V1.3 | `README.md` (section V1.3, branche `ue5.5-v1.3`) ; tag annoté `v1.3` ; build `LifePlay_Build_5.5_Shipping_V1.3` validé en jeu ; archive `LifePlay_Revival_V1.3_UE5.5_Full_Source_Code.7z` (4 945 770 158 octets, 11 583 fichiers, SHA-256 `24e7d58f759ca8998fe0d7ddc8c6a4c30262978806903e6d3deb68b37c46512a`, contrôles : test 7-Zip, aucun secret ni outil local, 14 fichiers identiques au tag) | publication de la V1.3 (couche de données, `WhatWhereToArray` en C++, ERR-039) | — | +| CHG-080 | 2026-10-02 | V1.3 | `README.md` : V1.3 en téléchargement principal (lien Mega), V1.2 et V1.1 en versions précédentes ; lien de la V1 retiré (archive supprimée de Mega faute d'espace, état git conservé par le tag `v1.0`) | lien V1.3 reçu | — | +| CHG-081 | 2026-10-02 | O7d | nouveau `Tools/BuildCollections.py` ; 13 collections statiques `Content/Collections/LifePlay*.collection` (hors dépôt, régénérables) ; `11_Audit_Optimisation.md` §16.5, `08_Guide_Developpeur.md` §2.1, `09_Commandes_Utiles.md` §3 | suite d'O7 (choix utilisateur) : regrouper les 8 561 assets par thème dans l'éditeur sans renommer de dossier ; collection transversale `LifePlay_Chemins_Fixes` (4 984 assets à ne pas déplacer) ; 13 545 chemins, 0 inconnu, chargées par l'éditeur ; aucun asset modifié, pas de build | supprimer `Content/Collections/LifePlay*.collection` (`git checkout O7d-avant` pour le reste) | +| CHG-082 | 2026-10-02 | O7d | `Tools/BuildCollections.py` : collections écrites dans `Saved/Collections` (locales) au lieu de `Content/Collections` (partagées) ; 13 fichiers déplacés vers `D:\Game\LifePlay_Backup_5.5\O7d\Content_Collections` puis régénérés (identiques) ; `11_Audit_Optimisation.md` §16.5, `08_Guide_Developpeur.md` §2.1 | avertissement à l'ouverture de l'éditeur (utilisateur) : une collection partagée exige un fournisseur de contrôle de révision, désactivé dans ce projet ; une collection locale n'en utilise pas ; validé par l'utilisateur le 2026-10-02 (plus d'avertissement) | `git checkout O7d-apres -- Tools/BuildCollections.py` et remettre les fichiers sauvegardés | +| CHG-083 | 2026-10-02 | O7e | `11_Audit_Optimisation.md` §16.6 | étude en lecture seule de l'intégration d'une coiffure Fab (demande utilisateur) : liste des coiffures = valeur par défaut `GI.HairstyleTable` (126 entrées, 35 absentes déjà en 4.27), suivi par noms d'os Genesis 8, morphs de forme facultatifs, sauvegardes par nom ; 34 coiffures Fab, ≈ 12 maillages utilisables | — (documentation) | +| CHG-084 | 2026-10-02 | H1 | nouveaux `Source/LifePlay/LPHairstyleSubsystem.h/.cpp`, module `Content/Modules/lp_Revival` (`lp_Revival.lpmod`, `Hairstyles/h1_testhair.lphair`) ; asset de test `Content/Heavy/New5/Hairs/H1_TestHair` (copie de `FloraHair_New`, hors dépôt) ; `11_Audit_Optimisation.md` §16.7 | liste des coiffures ouverte aux mods (demande utilisateur) : fichiers `.lphair` ajoutés en fin de `GI.HairstyleTable` sans modifier de Blueprint ; vérifié au journal : doublons refusés, 1 coiffure ajoutée (table 126 → 127) ; build `LifePlay_Build_5.5_H1` ; validé en jeu le 2026-10-02 (nouvelle partie et liste des villes, coiffure ajoutée en fin de liste, suivi des animations, module désactivé puis réactivé, Continue) | supprimer le module `lp_Revival` (ou le désactiver) ; `git checkout H1-avant -- Source` | +| CHG-085 | 2026-10-02 | O7e | nouvelle commandlet `Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.h/.cpp` (+ `SkeletalMeshUtilitiesCommon`, `StaticMeshDescription` dans `LifePlayEditorTools.Build.cs`) ; assets `Content/Heavy/New5/Hairs/Pocolov17*` (hors dépôt) ; `Content/Modules/lp_Revival/Hairstyles/pocolov17.lphair` (remplace `h1_testhair.lphair`, `H1_TestHair` retiré) ; `Licenses/Free_3D_Models_Credits.txt` ; `11_Audit_Optimisation.md` §16.8 | première coiffure Fab (Pocolov Hair 17, CC BY 4.0, téléchargée avec l'accord de l'utilisateur) : maillage statique rattaché au squelette Genesis 8, ajusté à la tête de Bridget, matériaux `DazHair` ; build `LifePlay_Build_5.5_O7e` : coiffure ajoutée (index 126) ; test en jeu en attente | retirer `pocolov17.lphair` ; sauvegardes dans `D:\Game\LifePlay_Backup_5.5\O7e` ; `git checkout O7e-avant -- Source Content/Modules Licenses` | +| CHG-086 | 2026-10-02 | O7e | `LPHairFromStaticMeshCommandlet` : ajustement sur le haut du crâne (`-Cranium`, `-Margin`) ; `Pocolov17` et `_Skeleton` régénérés (premier essai dans `O7e\essai1`), `Pocolov17_Strands` : `Thicken` 10 → 1 ; `11_Audit_Optimisation.md` §16.8 | premier essai en jeu (capture utilisateur) : coiffure trop étroite enfoncée dans le crâne, mèches opaques ; échelle 0,74 → 1,19 en largeur ; build `LifePlay_Build_5.5_O7e` reconstruit ; test en jeu en attente | `Tools`/assets : remettre `O7e\essai1` ; `git checkout O7e-apres -- Source/LifePlayEditorTools` | +| CHG-087 | 2026-10-02 | O7e | `Pocolov17_Cap` (hors dépôt) : `MapOpacity` = texture blanche du moteur ; `11_Audit_Optimisation.md` §16.8 | second essai en jeu correct (crâne couvert, couleur appliquée, profil bien placé) ; trous au-dessus de l'oreille dus à la lisière découpée de la calotte ; retouche choisie par l'utilisateur ; build `LifePlay_Build_5.5_O7e` reconstruit | remettre `Pocolov17_CapOpacity` dans `MapOpacity` | +| CHG-088 | 2026-10-02 | O7e | — | O7e validé en jeu par l'utilisateur (profil sans trou, mouvement suivi) ; défaut mineur accepté : bas de la calotte visible sous le lobe de l'oreille | — | +| CHG-089 | 2026-10-02 | H2 | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (mode base, plusieurs coiffures par fichier, `NONE`, `MORPHABLE`, vérification `-LPVerifyHairstyles`) ; nouveau `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (126 coiffures, 89 morphs) ; `11_Audit_Optimisation.md` §16.9 | liste de base des coiffures sortie du Blueprint (demande utilisateur) ; vérifié : table et morphs reconstruits identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; validé en jeu le 2026-10-02 (nouvelle partie, liste du menu inchangée avec Pocolov17 en dernier, personnages générés, Continue) | supprimer `vin_Base.lphair` (retour à la table du Blueprint) ; `git checkout H2-avant -- Source/LifePlay` | +| CHG-090 | 2026-10-02 | H2b | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (clés `FACIAL_HAIR`, `BODY_HAIR_M`, `BODY_HAIR_F`) ; `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (+ 44 noms) ; `11_Audit_Optimisation.md` §16.10 | pilosité sortie du Blueprint (demande utilisateur) : listes `GI.FHairstyles`, `BHairstyles_M`, `BHairstyles` ; `GI.OtherTable` laissée (maillages absents depuis 4.27) ; vérifié : 3 listes identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; validé en jeu le 2026-10-02 (28 barbes + sans barbe dans le menu de création) | retirer les lignes `*_HAIR` de `vin_Base.lphair` ; `git checkout H2b-avant -- Source/LifePlay` | +| CHG-091 | 2026-10-02 | Doc | nouveaux `Docs/Migration/13_Protocole_Coiffure.md`, `Tools/HairImportFbx.py`, `Tools/HairMaterials.py` ; `README.md` (migration), `08_Guide_Developpeur.md`, `09_Commandes_Utiles.md` | protocole de conversion d'une coiffure (demande utilisateur) ; scripts ponctuels d'O7e rendus paramétrables ; protocole rejoué de bout en bout sur Pocolov 17 sous le nom `Pocolov17P` : bornes identiques au millimètre, dépendances OK ; essai retiré du projet (`D:\Game\LifePlay_Backup_5.5\O7e\proto_essai`) | `git revert` du commit | +| CHG-092 | 2026-10-02 | HAIRS1 | nouveaux `Tools/HairPipeline.ps1`, `Tools/FbxMaterialMap.py`, `Tools/hairs/HAIRS1.json` ; `Tools/HairImportFbx.py` (rapport JSON), `Tools/HairMaterials.py` (calotte seulement si utilisée, contrôle des dépendances) ; assets `Content/Heavy/New5/Hairs/Pocolov03/06/11/15/16*` (hors dépôt) ; `Content/Modules/lp_Revival/Hairstyles/pocolov03/06/11/15/16.lphair` ; `Licenses/Free_3D_Models_Credits.txt` ; `13_Protocole_Coiffure.md` §7 | industrialisation de la conversion (demande utilisateur) et test sur 5 coiffures Pocolov (CC BY 4.0, téléchargées avec l'accord de l'utilisateur) ; 5 cas d'archives différents intégrés aux règles ; build `LifePlay_Build_5.5_HAIRS1` : 6 coiffures ajoutées (index 126 à 131) ; contrôle visuel en jeu en attente | retirer les `.lphair` ; assets et sources dans `D:\Game\LifePlay_Backup_5.5\HAIRS1` ; `git checkout HAIRS1-avant -- Tools Licenses Content/Modules` | +| CHG-093 | 2026-10-02 | SKIN | `LPHairFromStaticMeshCommandlet` : liaison par transfert de poids depuis le corps (`-Skin=transfer` par défaut, 6 voisins, bras et jambes exclus) et sortie de la peau (`-Gap` 0,3 cm) ; `LifePlayEditorTools.Build.cs` (`SkeletalMeshDescription`, `AnimationCore`) ; `HairPipeline.ps1` (`-Rebuild`, options `skin`, `gap`) ; 6 coiffures Pocolov reconstruites (versions précédentes dans `HAIRS1\\precedente_*`), Pocolov 17 ajoutée au lot ; `.lphair` et crédits : description des modifications corrigée ; `13_Protocole_Coiffure.md` | vidéo utilisateur : mèches longues liées à la seule tête, qui traversent épaules et poitrine quand la tête tourne ; poids obtenus : tête 47 à 62 %, cou, clavicules, poitrine ; 254 à 950 sommets sortis de la peau par coiffure ; build `LifePlay_Build_5.5_SKIN` ; validé en jeu le 2026-10-02 (les mèches ne traversent plus la peau quand la tête tourne) | `-Skin=rigid` ; `git checkout SKIN-avant -- Source Tools` et remettre les versions précédentes | +| CHG-094 | 2026-10-02 | UI14 | nouveaux `Source/LifePlay/LPVersionSubsystem.h/.cpp`, `Source/LifePlayEditorTools/LPEditWidgetCommandlet.h/.cpp` (+ `Json` dans le `.Build.cs`), `Tools/ui/MainMenu_V1.4.json`, `Docs/Migration/14_Interface_V1.4.md` ; `Content/UI/MainMenu.uasset` (6 modifications, diff conforme) ; `Content/Modules/donors.txt` (→ `Vinfamy`) ; `Config/DefaultGame.ini` (`ProjectVersion=2026.10.4`) | demande utilisateur avant la V1.4 : version année.mois.publication, crédits vidés avec Vinfamy auteur original, Donate → Patreon edujiro5972, Discord → discord.gg/q4689pNSy, Commission masqué ; build `LifePlay_Build_5.5_UI14` : « LifePlay 2026.10.4 » ; validé en jeu le 2026-10-02 (version, crédits, bouton Commission masqué, liens Donate et Discord) | `Tools\Restore.ps1 -Lot UI14` ; `git checkout UI14-avant -- Config Source Content/Modules/donors.txt` | +| CHG-095 | 2026-10-02 | MODS14 | nouveaux `Tools/SplitModules.ps1`, `Tools/modules/V1.4.json`, `Docs/Migration/15_Modules_Optionnels.md` ; build `LifePlay_Build_5.5_Shipping_V1.4` (dossier neuf, contrôlé : pas de fichier périmé) ; archive `D:\Game\LifePlay_Publication\LifePlay_Revival_V1.4_Optional_Modules.zip` (875 812 octets, 626 fichiers, SHA-256 `cd8b4ffa9f676d3703fce9d2b42a0f61682c6c04bda30d033630c8f1be8fd236`) | demande utilisateur : build Shipping réduite à l'essentiel (`vin_Base`, `lp_Revival`, `vin_VanillaRealistic`), 14 modules optionnels à part ; vérifié dans les Blueprints : aucun fichier de module optionnel lu en dur, `GI.removeDeletedMods` retire les modules absents ; build allégé démarré sans plantage ; validé en jeu le 2026-10-02 (build sans les modules optionnels, puis avec l'archive extraite) ; modules de test retirés du build vers `LifePlay_Build_5.5_Shipping_V1.4_test_extraction` | remettre les dossiers de `LifePlay_Build_5.5_Shipping_V1.4_modules_optionnels` dans le build | +| CHG-096 | 2026-10-02 | V1.4 | `README.md` (section V1.4, branche `ue5.5-v1.4`) ; `Donate.url` (racine) → Patreon edujiro5972, comme le bouton Donate ; tag annoté `v1.4` | publication de la V1.4 (LifePlay 2026.10.4) : coiffures en fichiers `.lphair`, 6 coiffures Pocolov, chaîne d'import, menu principal, modules optionnels séparés | — | +| CHG-097 | 2026-10-02 | V1.4 | archives de publication (`D:\Game\LifePlay_Publication`) : sources `LifePlay_Revival_V1.4_UE5.5_Full_Source_Code.7z` (4 942 392 569 octets, 12 766 entrées, SHA-256 `bda44e52e8f45775e09fdea51b613bb64fc5e0c7fd0ec76108804e0bb39b3426`, script `make_archive_v1_4.cmd`) ; jeu `LifePlay_Revival_V1.4_Windows_Game.7z` (1 301 813 923 octets, racine `LifePlay_Revival_V1.4`, SHA-256 `d4a36542550ae2bcc7f3f634255125fc6c7520f5cf29df4b74ae3bbc0ac55a9f`, script `make_game_v1_4.cmd`) ; modules optionnels `LifePlay_Revival_V1.4_Optional_Modules.zip` (875 812 octets, SHA-256 `cd8b4ffa9f676d3703fce9d2b42a0f61682c6c04bda30d033630c8f1be8fd236`) | contrôles : tests 7-Zip ; sources : aucun `.mcp.json`/`.claude`/`McpAutomationBridge`/`Saved`/`Intermediate`/jeton, 17 modules, 10 fichiers identiques au tag `v1.4`, `.git` = `a83ea56` ; jeu : 3 modules (`vin_Base`, `lp_Revival`, `vin_VanillaRealistic`), ni `Saved` ni `.pdb` | liens Mega à ajouter après envoi | +| CHG-098 | 2026-10-02 | V1.4 | `README.md` : archive des sources V1.4 en téléchargement principal (lien Mega), V1.3 ajoutée aux versions précédentes | lien Mega de l'archive développeur reçu ; liens du jeu et des modules optionnels à venir | — | +| CHG-099 | 2026-10-02 | V1.4 | `README.md` : section V1.4, point des modules optionnels reformulé (plus de nom d'archive ni de mode d'installation) | décision utilisateur : les archives du jeu et des modules optionnels ne sont pas annoncées sur GitHub (la documentation technique garde les détails de l'outil) | `git revert` du commit | +| CHG-100 | 2026-10-03 | H3 | nouveau `Docs/Migration/16_H3_Mods_Paquets.md` | préparation de H3 (demande utilisateur) : étude en lecture seule du moteur 5.5 (dossier `Mods/` lu par le gestionnaire de plugins en jeu empaqueté, plugins Mod activés par défaut, paquets de plugins montés au démarrage, registre `AssetRegistry.bin` des plugins chargé) et de la configuration (paquets non signés, shaders intégrés aux matériaux, IoStore) ; architecture, risques, lots H3a à H3c | — (documentation) | +| CHG-101 | 2026-10-03 | H3a | `Releases/2026.10.4` (non versionné, 1,3 Go) ; `ModSources/LPHairSample/` (`LPHairSample.uplugin`, `Module/LPHairSample.lpmod`, `Module/Hairstyles/pocolov05.lphair`, `Module/CREDITS.txt` ; assets `Content/Hairs/Pocolov05*` non versionnés) ; `Tools/mods/LPHairSample.json` ; `Tools/HairPipeline.ps1` (chemins de plugin, `credits`) ; `Tools/HairMaterials.py` (contrôle des dépendances par chemin de paquet) ; `.gitignore` (`/Releases/`, `/Mods/`, produits et assets de `ModSources`) ; builds `LifePlay_Build_5.5_Ref_2026.10.4`, `LifePlay_Build_5.5_H3a` | prototype H3a : version de référence (paquets identiques au V1.4 publié), mod d'essai Pocolov 05 (Fab, CC BY 4.0, téléchargement accordé) cuit en DLC contre 2026.10.4 (6 paquets, 0 erreur) et installé dans une copie du V1.4 ; version `2026` mal nommée (argument coupé par PowerShell) déplacée vers `LifePlay_Backup_5.5\H3a` ; **validé en jeu le 2026-10-03** (16_H3_Mods_Paquets.md §5) | supprimer `ModSources/LPHairSample`, `Tools/mods`, `Releases/2026.10.4` ; `git revert` du commit | +| CHG-102 | 2026-10-03 | H3a | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` ; `Content/Modules/lp_Revival/lp_Revival.lpmod` (`MODULE_DEFAULT_ENABLED: true`) | retour utilisateur : coiffure du mod présente alors que le mod est décoché. Le sous-système (H1) tenait pour actif un module absent de `GI.ModEnableds`, alors que le jeu le tient pour inactif (défaut présent en V1.4 : `lp_Revival` affiché décoché mais actif). Même règle que le jeu ; un module à clé `MODULE_DEFAULT_ENABLED: true` absent de la liste y est ajouté coché (au démarrage et après `LoadSettings`). Validé en jeu le 2026-10-03 | `git revert` du commit, recompiler | +| CHG-103 | 2026-10-03 | UI15 | `Content/UI/MainMenu.uasset`, `Content/UI/ModManager.uasset` (snapshot `LifePlay_Backup_5.5\UI15`) ; nouveaux `Source/LifePlayEditorTools/LPTitleModManagerCommandlet.h/.cpp`, `Source/LifePlay/LPModSettingsLibrary.h/.cpp`, `Docs/Migration/17_Mods_Ecran_Titre.md` ; build `LifePlay_Build_5.5_UI15b` | demande utilisateur : gestionnaire de mods dès l'écran titre, puis choix entre nouvelle partie et partie sauvegardée. Entrée dédiée sur l'écran titre (copie de « Load »), fermeture vers le menu titre avec relecture des données et enregistrement du choix (`SaveModSelection`, sans toucher à la dernière sauvegarde) ; entrée d'origine inchangée en partie. Première version (entrée d'origine rendue visible) remplacée après retour (alignement). **Validé en jeu le 2026-10-03** | `.\Tools\Restore.ps1 -Lot UI15` ; `git revert` du commit | +| CHG-104 | 2026-10-03 | H3b | nouveaux `Tools/BuildMod.ps1`, `Tools/ModSource.ps1` ; `Tools/HairPipeline.ps1` (mode `mod` : montage temporaire, `.lphair` et crédits dans `ModSources//Module`, `-Rebuild` déplace aussi les textures) ; `Tools/HairMaterials.py` (analyse synchrone du dossier, réutilisation des textures) ; `Tools/mods/LPHairSample.json` (`mod`) ; `Source/LifePlay/LPHairstyleSubsystem.cpp` (repli `DoesPackageExist`) ; Pocolov 05 reconstruite (précédente dans `LifePlay_Backup_5.5\H3a\Pocolov05\precedente_*`) ; build `LifePlay_Build_5.5_H3b` ; archive `D:\Game\LifePlay_Publication\Mods\LPHairSample-for-LifePlay-2026.10.4.zip` (334 473 octets, SHA-256 `8F6A45ED…CBC8`) | lot H3b : un mod d'assets en une commande ; plantage de `HairMaterials.py` dans un plugin expliqué (registre de la commandlet sans les assets du plugin) et corrigé ; build de base sans rien du mod par construction. **Validé en jeu le 2026-10-03** (16_H3_Mods_Paquets.md §6) | `git revert` du commit, recompiler | +| CHG-105 | 2026-10-03 | H3c | nouveaux `Docs/Modding/FullGuides/Creating_Asset_Mods.md`, `Docs/Modding/ShortGuides/explanation.lphair`, `Tools/NewMod.ps1`, `Tools/EditMod.ps1` ; `Docs/Modding/ShortGuides/explanation.lpmod` (`MODULE_DEFAULT_ENABLED`) ; `Docs/Migration/10_Protocole_Operations.md` §6, `09_Commandes_Utiles.md` §7 (liste de contrôle de publication), `16_H3_Mods_Paquets.md` §7 | lot H3c : guide des moddeurs en anglais, outils de création et d'édition d'un mod, version de référence intégrée à la procédure de publication (livrée dans l'archive des sources ; `Mods/` vide au build) | `git revert` du commit | +| CHG-106 | 2026-10-03 | H3d | nouveaux `Source/LifePlay/LPModInfoSubsystem.h/.cpp` ; `Source/LifePlay/LifePlay.Build.cs` (`Slate`, `SlateCore`) ; `Tools/NewMod.ps1` (métadonnées), `Tools/BuildMod.ps1` (ordre des lignes 1 à 3, `MODULE_PACKAGED`, `MODULE_GAME_VERSION`, `.uplugin` livré, contrôle prix et licences, nom d'archive) ; `Docs/Modding/FullGuides/Creating_Asset_Mods.md` §4b, `explanation.lpmod` | demande utilisateur : métadonnées extensibles (dates, licence, lien de l'auteur, prix) et avertissement en jeu quand un mod a été construit pour une autre version. Le jeu ne lit que les 3 premières lignes d'un `.lpmod`, par position ; fenêtre Slate C++, sans Blueprint. **Validé en jeu le 2026-10-03** | `git revert` du commit, recompiler | +| CHG-107 | 2026-10-03 | H3d | nouveau mod `ModSources/lpr_hair_pocolov` (`.uplugin`, `Module/` : `.lpmod`, 12 `.lphair`, `CREDITS.txt` ; assets non versionnés), `Tools/mods/lpr_hair_pocolov.json` ; retirés : `Content/Modules/lp_Revival` (6 `.lphair`, `.lpmod`), assets `Content/Heavy/New5/Hairs/Pocolov*` (non versionnés), `ModSources/LPHairSample`, `Tools/mods/LPHairSample.json` (tous vers `LifePlay_Backup_5.5\H3d\retires`) ; `Licenses/Free_3D_Models_Credits.txt` (lignes Pocolov remplacées par un renvoi ; snapshot `H3d`) ; nouveau `Tools/modules/next.json` ; builds `LifePlay_Build_5.5_H3d` ; archive `D:\Game\LifePlay_Publication\Mods\lpr_hair_pocolov-1.0-for-LifePlay-2026.10.4.zip` (3 807 460 octets, SHA-256 `4E9420E5…2B8F`) | demande utilisateur : module `lpr_hair_pocolov` regroupant les assets Pocolov ; choix utilisateur : les 12 coiffures CC BY, les 6 du jeu de base déplacées dans le paquet, mod d'essai retiré ; 5 archives téléchargées (accord). **Validé en jeu le 2026-10-03** | remettre les dossiers de `LifePlay_Backup_5.5\H3d\retires` ; `.\Tools\Restore.ps1 -Lot H3d` ; `git revert` du commit | +| CHG-108 | 2026-10-03 | H3c | `Docs/Modding/FullGuides/Creating_Asset_Mods.md` §5 | essai utilisateur de `Tools/EditMod.ps1` : mod monté et chargé (« Mounting Mod plugin lpr_hair_pocolov »), 12 coiffures visibles une fois « Afficher le contenu des plugins » coché dans le navigateur de contenu (masqué par défaut ; la recherche porte sur les noms d'assets) : précision ajoutée au guide. **H3c validé le 2026-10-03** | `git revert` du commit | +| CHG-109 | 2026-10-03 | PUB | `Tools/SplitModules.ps1` (mode `per_module` : une archive par module, notice tirée du `.lpmod`, `INDEX.txt` ; refus d'un build contenant un mod d'assets installé) ; `Tools/modules/next.json` ; `Docs/Migration/15_Modules_Optionnels.md` §5, `09_Commandes_Utiles.md` §7 | décision utilisateur pour la prochaine publication : chaque mod livré dans un zip indépendant, sauf les modules de base (texte) gardés dans le build Shipping ; diffusion des modules sur Discord dans un premier temps. Essai sur une copie (15 archives, 640 fichiers, garde-fou vérifié) | `git revert` du commit | +| CHG-110 | 2026-10-03 | H3c | sources texte de `ModSources/lpr_hair_pocolov` (`.uplugin`, `.lpmod`, 12 `.lphair`, `CREDITS.txt`) | incident : le commit `54f6604` (tag `H3c-valide`) a été fait pendant que le mod était monté dans `Mods/` par `EditMod.ps1` (éditeur ouvert) ; `git add -A` a enregistré la suppression de ses sources. Aucune perte sur disque (mod remis dans `ModSources/` à la fermeture de l'éditeur, 70 assets présents) ; sources réenregistrées par `bf9bb58`. Règle ajoutée (10_Protocole_Operations.md §6) : ne jamais commiter ni lancer un build tant que `Mods/` n'est pas vide. Le tag `H3c-valide` reste sur `54f6604` (déplacement non fait sans accord) | — | +| CHG-111 | 2026-10-03 | V1.5 | `Config/DefaultGame.ini` (`ProjectVersion=2026.10.1.5`), format de version (`14_Interface_V1.4.md`, `LPVersionSubsystem.h`), `README.md` (section V1.5, branche), `Tools/modules/next.json` (groupe `vin_*`), `Tools/SplitModules.ps1` (groupes), `Tools/BuildMod.ps1` (`INDEX.txt`), `15_Modules_Optionnels.md` ; build `LifePlay_Build_5.5_Shipping_V1.5` (version de référence `Releases/2026.10.1.5`, paquets identiques ; 2 modules de base) ; `Releases/2026.10.4` déplacé vers `LifePlay_Backup_5.5\Releases` ; modules `D:\Game\LifePlay_Publication\Modules_2026.10.1.5` (`vin_Modules`, `lpr_hair_pocolov-1.0`, `INDEX.txt`) | publication V1.5 (décisions utilisateur : version `année.mois.majeur.mineur` ; modules `vin_*` en un seul zip, autres mods chacun le leur ; diffusion des modules sur Discord, rien sur GitHub). Validé en jeu le 2026-10-03 ; les mods extraits dans le build pour le test et les fichiers de session ont été déplacés vers `LifePlay_Build_5.5_Shipping_V1.5_test_extraction` (build contrôlé contre le manifeste UAT : 0 fichier en trop, 1 459 fichiers) | `git revert` des commits V1.5 | +| CHG-112 | 2026-10-03 | V1.5 | archives `D:\Game\LifePlay_Publication` : `LifePlay_Revival_V1.5_UE5.5_Full_Source_Code.7z` (6 195 829 571 octets, SHA-256 `a71e77ca98c9a86399e4e8685f2b7ea74d6e9358694094968648f98acf7bc9d3` ; avec `Releases/2026.10.1.5` et `ModSources/`, `.git` du clone propre sur `v1.5`) ; `LifePlay_Revival_V1.5_Windows_Game.7z` (1 300 541 638 octets, SHA-256 `f5e34ecaf8de90659853a9ae2393b2571ae45894808f4f2b59ba1b4e857a7fe1`, racine `LifePlay_Revival_V1.5`) ; scripts `make_archive_v1_5.cmd`, `make_game_v1_5.cmd` | archives de publication V1.5 : tests 7-Zip OK ; sources sans `.mcp.json`, `.claude`, `Saved`, `Mods`, `Intermediate`, McpAutomationBridge, produits de cuisson des mods, ni `SecurityToken` dans `Config` ; jeu sans aucun mod. Clone propre mis à jour depuis le dépôt local (récupération GitHub bloquée, arrêtée) | — | +| CHG-113 | 2026-10-03 | V1.5 | `README.md` (téléchargement : archive des sources V1.5 en principal, lien Mega, SHA-256, raison de la taille ; V1.4 dans les versions précédentes) | lien Mega de l'archive des sources V1.5 fourni par l'utilisateur ; le jeu et les modules (Discord) ne sont pas mentionnés sur GitHub | `git revert` du commit | +| CHG-114 | 2026-10-04 | Dbg | nouveaux `Tools/BuildDebug.ps1`, `Tools/CollectDebugArtifacts.ps1`, `Tools/SummarizeDebugCsv.ps1`, `Tools/debug/*.bat`, `Source/LifePlay/LPDebugSubsystem.h/.cpp`, `Docs/Migration/18_Build_Debug.md` ; événements nommés `LoadTxt` / `LPDataCache` ; `LPModInfoSubsystem` (`ModInfoCheckInterval`) et `LPModSettingsLibrary` (`SettingsModEnabledsName`) pour le build unifié ; `08`, `09`, README migration, skill `lifeplay-debug-build` | mesure reproductible (Insights, mémoire, CSV) sans coût Shipping : `#if !UE_BUILD_SHIPPING`, `IncludeDebugFiles` inchangé, pas de `-nodebuginfo` | `git revert` du commit | +| CHG-115 | 2026-10-04 | G6a | `Source/LifePlay/LPDataCompat.h/.cpp` (`FindCenterOfPolygon`) ; nouveaux `Source/LifePlayEditorTools/LPFixFindCenterCommandlet.h/.cpp` ; `Content/BPs/GI.uasset` (4 appels rebranchés : 3 dans l'EventGraph, 1 dans `MoveTo` ; fonction Blueprint d'origine laissée en place) ; snapshot `LifePlay_Backup_5.5\G6a` | feuille CPU de l'image Continuer, trace Development `G6Dev.utrace` (464 Mo, canaux `cpu,frame,bookmark,loadtime`, sans `memory`) : `FindCenterOfPolygon` 0,350 s exclusif / 0,742 s inclus, 49 425 appels. Centre de boîte englobante, coordonnées ramenées en float, aire ` | dx | +| CHG-116 | 2026-10-04 | G6b | `ULPDataCompat::PreloadCityPackages` ; appel dans `LPModInfoSubsystem::Initialize` | lot I/O séparé de CHG-115. Le canal `loadtime` de `G6Dev.utrace` nomme les paquets lus pendant l'image longue : `/Game/Heavy/StreetMap/Static/CentralLondon` (maillage, vrac cuit de l'ordre des 164 Mo du CSV I6) et `/Game/Heavy/StreetMap/CentralLondon` (données StreetMap). Lecture async prioritaire dès l'instance de jeu, pas dans l'image du clic Continuer. Disque chaud : l'attente synchrone de cette trace était 0,325 s (`GetAsset` × 2) ; l'image I6 à froid faisait 6,2 s. Partie du 2026-10-04 : `Preload ville : demande envoyee` à 14:31:06, image Continuer à 14:31:15 | retirer l'appel dans `LPModInfoSubsystem::Initialize` | +| CHG-117 | 2026-10-04 | Dbg | `LPDebugSubsystem` (`-LPCollisionLog`) ; `LifePlay.Build.cs` (`Chaos`) ; lanceurs `LifePlay_debug_sessionB_mem.bat` et `LifePlay_debug_collision.bat` ; `09`, `18` §10, skill `lifeplay-debug-build` | le dump du `ExecCmds` de démarrage (I5, 23 primitives, 1 trimesh) ne nomme pas l'acteur des 93 Mo de trimesh. Le switch échantillonne toutes les 2 s et écrit le porteur des triangles. Rapport : les capsules PNJ sont les 0,11 Mo de convexes, pas le trimesh | `git revert` du commit | +| CHG-118 | 2026-10-04 | Dbg | `Tools/debug/LifePlay_debug_ui.bat` et `.ps1` ; `BuildDebug.ps1` copie aussi le `.ps1` ; `18` §3 | choisir les options de mesure dans une fenêtre, au lieu de lancer `LifePlay.exe` sans switch. Le canal `memory` est décoché par défaut. `-csvCaptureFrames` n'est pas proposé. Copié dans l'archive Development déjà cuite | supprimer les deux fichiers | +| CHG-119 | 2026-10-04 | PairCap | nouveaux `Source/LifePlay/LPPairCapsuleSubsystem.h/.cpp` (snapshot `LifePlay_Backup_5.5\PairCap` : sources absentes, journal sauvegardé) | essai demandé : après la coupure des capsules d'une scène à deux (`GI.SexPersons`, sinon `AP1` et `AP2`, les deux en `NoCollision` et à moins de 4 m), une capsule unique orientée de l'un vers l'autre, canal `Pawn`, rayon du corps, longueur qui les couvre. Les capsules d'origine restent coupées. Les meubles qui ignorent `Pawn` et le sol ne l'arrêtent pas. Si le balayage touche un mur, le couple est repoussé ensemble. Journal : `Capsule couple : posee` / `retiree` | `.\Tools\Restore.ps1 -Lot PairCap` | +| CHG-120 | 2026-10-04 | PairCap2 | `LPPairCapsuleSubsystem.h/.cpp` (snapshot `LifePlay_Backup_5.5\PairCap2`) | retour de la partie 16:25 : capsule posée rayon 20, barrière sur le lit, murs toujours traversés, capsule du joueur absente à la fin. Inversion : la capsule du couple est posée après la coupure, droite, calée sur les deux maillages, canal `WorldDynamic` (les meubles ignorent seulement `Pawn`). Le lit et le sol ne repoussent pas. À la fin de la scène elle est détruite, les capsules du couple reviennent en `QueryAndPhysics`, gravité du joueur à 1 si `GI::Show` l'avait mise à 0 | `.\Tools\Restore.ps1 -Lot PairCap2` | +| CHG-121 | 2026-10-04 | PairCap | retrait de `Source/LifePlay/LPPairCapsuleSubsystem.h/.cpp` (`Restore.ps1 -Lot PairCap -Only` ces deux fichiers ; copies dans `LifePlay_Backup_5.5\PairCap\_retires\20261004-170801`) | demande utilisateur : annuler toutes les modifications de capsules (CHG-119 et CHG-120). La scène à deux était devenue injouable. Aucun Blueprint touché. Cuisson Development ensuite pour retirer le sous-système de l'exécutable joué | remettre les deux fichiers de `_retires\20261004-170801` | +| CHG-122 | 2026-10-04 | PairCol | nouvelle commandlet éditeur `Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.h/.cpp` | essai hors partie (demande utilisateur) : `work___vin01` reconstruit dans une scène d'éditeur (54 meubles, échelle monde ~1, aucun Blueprint modifié, pas de cuisson). Capsules copiées du CDO `Person` (rayon 20, demi-hauteur 88, profil Custom : Pawn Ignore, WorldStatic Ignore, WorldDynamic Block). Le balayage est celui de la marche, l'animation n'est pas jouée. Résultat (`Saved/Logs/portage/LPPairCollisionTest.txt`, code 0) : meuble qui ignore Pawn traversé (371 cm) ; meuble qui bloque Pawn arrête une capsule active à 50 cm (`fnt_new183`) et laisse passer une capsule `NoCollision` ; deux capsules au profil Person se traversent déjà ; avec le profil Pawn du moteur, `IgnoreActorWhenMoving` les laisse se traverser (80 cm) pendant que le meuble les arrête encore | supprimer les deux fichiers de la commandlet | +| CHG-123 | 2026-10-04 | PairCol | nouveaux `Source/LifePlay/LPPairSandbox.h/.cpp`, `Tools/debug/LifePlay_pair_sandbox.bat` | fenêtre jouable à part de la partie (demande utilisateur) pour voir les deux réglages. Chambre `work___vin01` (54 meubles), capsules du CDO `Person` dessinées (verte joueur, cyan autre, rouge si coupée). Touche 1 : meubles bloquent `Pawn`. Touche 2 : capsules `QueryAndPhysics` ou `NoCollision`. L'instance de jeu Blueprint n'est pas chargée. Ouverture vérifiée : mode `LPPairSandboxGameMode`, 54 meubles, 0 absent, capsule 20 / 88, départ (−700, −700, 108) | supprimer les trois fichiers | +| CHG-124 | 2026-10-04 | PairCol | `LPPairSandbox` : soleil, contre-jour, ciel, lampe au-dessus du couple ; maillages `bridget_gp` (joueur) et `michael` (l'autre) sur les capsules | la fenêtre montrait les capsules sans corps et restait sombre. La collision reste celle de la capsule : le maillage ne bloque pas | retirer les lumières et les `SetSkinnedAssetAndUpdate` | +| CHG-125 | 2026-10-04 | PairCol | `LPPairSandbox` : soleil, contre-jour, ciel et correction d'exposition retirés ; un spot au plafond (cône vers le bas, au-dessus du couple) et deux lampes latérales | la fenêtre aveuglait. L'éclairage de la pièce est celui d'un bureau : plafonnier et deux lampes sur les côtés | remettre le soleil | +| CHG-126 | 2026-10-04 | PairCol | `LPPairSandbox` : plafonnier et lampes créés mobiles avant l'enregistrement ; exposition manuelle, `r.EyeAdaptationQuality` à 0 | la lumière brillait une seconde puis s'éteignait : lampes stationnaires sans précalcul, et l'œil automatique assombrissait la pièce | remettre les lumières stationnaires | +| CHG-127 | 2026-10-04 | PairCol | `LPPairSandbox` : plafonnier 80 000, lampes latérales 25 000, cône plus large, chute non quadratique, exposition manuelle +6, ombres des lampes coupées | avec l'exposition du projet (calée sur un soleil), 5 000 candelas restaient noires. Les trois lampes éclairent le couple sans soleil | revenir aux intensités de CHG-126 | +| CHG-128 | 2026-10-04 | PairCol | `LPPairSandbox` : plafonnier, lampes latérales et exposition forcée retirés. Éclairage copié de `CentralLondon` : `FullBodyLight` intensité 1,5 (canal 0), `Sun` intensité 5 température 10 000 K (canal 1 seulement), `SkyLight` cubemap `urban_street_03_1k` intensité 0,6, `SkyAtmosphere` à Z −100 000, `BP_Sky_Sphere` branché sur le soleil. Les trois lumières sont mobiles | la pièce restait noire. Le jeu n'éclaire pas le bureau avec un plafonnier : le relevé de la carte donne ces intensités, et `SkylightHours` va de 0,2 (nuit) à 0,7 (midi). 0,6 est l'heure 9 et l'heure 15. L'exposition redevient celle du projet | remettre le plafonnier de CHG-127 | +| CHG-129 | 2026-10-04 | PairCol | `LPPairSandbox` : sol en `BlockAllDynamic` (la capsule Person ignore `WorldStatic`), départ cherché sur le canal `Pawn`, apparition au point libre, vitesse remise à zéro, physique du maillage Daz coupée. Si le corps passe sous le sol, retour au point de départ | Bridget traversait la pièce puis tombait sans fin : le cube `BlockAll` ne porte pas cette capsule, et un départ dans un meuble l'éjecte | remettre le sol `BlockAll` | +| CHG-130 | 2026-10-04 | PairCol | `LPPairSandbox` : quatre murs, sol et plafond de `Room` (cubes `FntWall`, positions du Blueprint, enfants de la racine à l'échelle du `.lpworld`). Les murs bloquent en `WorldDynamic`. Plafond sans collision, comme `WallT` | la pièce n'avait pas d'enveloppe : Bridget sortait. Les meubles bloquent et les deux capsules se traversent, c'est le réglage déjà mesuré | retirer les six cubes | +| CHG-131 | 2026-10-04 | PairCol | `LPPairSandbox` : touches 1 et 2 lues par front montant (`IsInputKeyDown`, aussi pavé numérique). Scène `AgMakeout` (`AgMakeout_F` sur Bridget, `AgMakeout_M` sur Michael), les deux au même point, en boucle | 1 et 2 ne changeaient rien : `WasInputKeyJustPressed` est déjà effacé quand le personnage est mis à jour. La scène est une paire Daz du show | retirer `PlayAnimation` | +| CHG-132 | 2026-10-04 | PairCol | `LPPairSandbox` : menu déroulant des paires Daz (F sur Bridget, M sur Michael). Départ à 1 m des murs ouest et sud. Caméra à 2,5 m, bras sans collision, réinitialisée à chaque animation. Clic droit pour tourner | le couple était à ~60 cm du coin et la caméra à 3,8 m. Le menu liste les scènes du show | retirer le menu et remettre le bras à 380 | +| CHG-133 | 2026-10-04 | PairCol | `LPPairSandbox` : départ décalé de 1 m vers le centre (`X=-508,7` `Y=-499,3`) pour sortir de la plante `fnt_new155` du coin sud-ouest | le couple était posé dans le pot | revenir à `+100` cm des murs | +| CHG-134 | 2026-10-04 | PairCol | `LPPairSandbox` : le partenaire est soudé au joueur (même position, même rotation, marche coupée, capsule sans collision). Seule la capsule du joueur déplace le couple | en jeu les deux corps d'une scène partagent le point et se déplacent ensemble ; ici Michael restait sur place | retirer `Weld` | +| CHG-135 | 2026-10-04 | PairCol | `LPPairSandbox` : la capsule du couple est recalculée sur les os des deux animations (6 instants), rayon et hauteur, bas maintenu au sol | la capsule Person (rayon 20, demi-hauteur 88) ne couvre pas les deux corps | retirer `FitCoupleCapsule` | +| CHG-136 | 2026-10-04 | PairCol | `LPPairSandbox` : capsule recalculée à chaque animation, sur la pose des deux clips, et recentrée sur le couple. Les corps restent en place | la mesure précédente lisait la pose de repos et déplaçait la capsule | revenir à la capsule Person | +| CHG-137 | 2026-10-04 | PairCol | `LPPairSandbox` : la capsule du couple est remplacée par une boîte recalculée sur les os à chaque animation. Le déplacement balaie cette boîte | essai demandé : une boîte épouse mieux une pose allongée qu'une capsule verticale | revenir à `FitCoupleCapsule` | +| CHG-138 | 2026-10-04 | PairCol | `LPPairSandbox` : la boîte suit les os affichés de la pose en cours, marge 4 cm. Plus de boîte sur toute la durée du clip, plus de décalage pour garder le bas au-dessus du sol | la boîte était trop basse et trop grande par rapport aux corps | revenir à la mesure sur 8 instants | +| CHG-139 | 2026-10-04 | PairCol | `LPPairSandbox` : retour à la boîte mesurée sur l'animation (8 instants). La mise à jour sur les os affichés est retirée | la boîte suivait la pose affichée en traversant le bureau, décalée des corps | — | +| CHG-140 | 2026-10-04 | ShowCouple | nouveau `LPShowCoupleSubsystem` : pendant un show à deux (`SexPersons`, sinon `AP1` et `AP2`, capsules coupées, moins de 4 m), le partenaire suit le joueur, une boîte mesurée sur l'animation arrête le couple, les meubles de `Room` bloquent `Pawn` le temps de la scène. `lp.ShowCouple 0` rend la main au Blueprint. L'ancien chemin n'est pas modifié | essai du show réel à partir de la boîte de la sandbox, réversible | retirer les deux fichiers | +| CHG-141 | 2026-10-04 | ShowCouple | `LPShowCoupleSubsystem` : après le tick des acteurs, le partenaire est soudé au joueur (même point, maillages yaw −90 et Z = −demi-hauteur, comme la sandbox). La boîte des clips `_F` et `_M` est recalculée à chaque changement d'animation, pas une seule fois à l'entrée | la boîte restait sur la première mesure et les deux corps ne partageaient pas le point | revenir au `FitBox` de CHG-140 | +| CHG-142 | 2026-10-04 | ShowCouple | `LPShowCoupleSubsystem` : la boîte est mesurée sur les séquences de `GI.LastAnimation` et sur tous les maillages du couple, pas seulement un clip `_F`/`_M` du maillage principal. Même changement d'animation, même soudure | partie 21:16 : `branche` sans aucune ligne `boite`, la boîte verte absente | revenir au filtre `_F`/`_M` de CHG-141 | +| CHG-143 | 2026-10-04 | ShowCouple | `LPShowCoupleSubsystem` : les primitifs de `Room`, de ses acteurs attachés et de ses références proches qui ignorent `Pawn` passent en `Block` tant que le show dure (pas seulement les composants directs). La boîte ignore les clips `idle` et prend la paire voisine (`_0`/`_1` ou `_F`/`_M`) dans le même dossier | partie 21:31 : boîte `idle_Daz+SF_ft07_1` (centre X=-54, étendue 99×46×93), seulement 12 composants. Le repos debout gonflait la boîte, et `SF_ft07_0` n'était pas chargé | retirer `BlockRoomPawn` et le filtre `idle` | +| CHG-144 | 2026-10-04 | PawnGlobal | `Room::addObject` : `SetCollisionResponseToChannel(Pawn)` passe de `ECR_Ignore` à `ECR_Block`. Snapshot `LifePlay_Backup_5.5\PawnGlobal`. Commandlet `LPFixPawnResponse` | le canal Pawn est le même pour toutes les chambres ; essai demandé : `Block` dès la pose du meuble, plus seulement pendant le show | `.\Tools\Restore.ps1 -Lot PawnGlobal` | +| CHG-145 | 2026-10-04 | PawnDyn | `Room::addObject` enchaîne `SetCollisionObjectType(WorldDynamic)` après le `Pawn Block`. Murs `Wall1`–`Wall4` en `WorldDynamic` et `Pawn Block`. `KeepTogether` balaie comme la sandbox : le sol ne masque plus le mur. Snapshot `LifePlay_Backup_5.5\PawnDyn` | options 1 et 2 de la sandbox sur Oui : la capsule Person ignore `WorldStatic`, donc `Pawn Block` seul la laisse traverser ; la boîte du show s'arrêtait au lit | `.\Tools\Restore.ps1 -Lot PawnDyn` | +| CHG-146 | 2026-10-04 | ShowCouple | `LPShowCoupleSubsystem` : un meuble qui a une action `sit`, `lie`, `use` ou `work` dans `Heavy/New2/Daily` ne bloque plus `Pawn`. Pendant cette animation, la capsule ignore `WorldDynamic`, puis la réponse d'origine revient | les PNJ ne pouvaient plus s'asseoir ni se coucher : la capsule était repoussée hors de la chaise et du lit | retirer `OpenUseFurniture` et `LetSittersPass` | +| CHG-147 | 2026-10-04 | ShowCouple | Pendant le show, les meubles utilisables rebloquent `Pawn` (restaurés à la sortie). La boîte a la collision de la sandbox et son balayage s'arrête sur un objet qui bloque, sans accepter le déplacement s'il est déjà dedans. Le sol ne compte pas | la boîte traversait le lit, les murs et les meubles : le lit était forcé en Ignore, et un contact déjà engagé laissait passer tout le mouvement | revenir au `KeepTogether` de CHG-145 | +| CHG-148 | 2026-10-04 | ShowCouple | `KeepTogether` : un contact déjà engagé (la boîte repose dans le lit) ne fige plus le déplacement. Un obstacle rencontré devant arrête et laisse glisser, y compris en hauteur | les contrôles ne déplaçaient plus la boîte : chaque mouvement commençait dedans et était refusé | revenir au refus de CHG-147 | +| CHG-149 | 2026-10-04 | ShowCouple | `KeepTogether` : le sol arrête la boîte comme un mur. Un volume déjà traversé (mur ou sol) repousse la boîte dehors au lieu d'accepter le déplacement | la boîte passait dans le mur et dans le sol : ces contacts étaient ignorés | revenir au filtre de CHG-148 | +| CHG-150 | 2026-10-04 | ShowCouple | `KeepTogether` ignore toutes les capsules de personnages. Le balayage ne s'arrête plus que sur les murs, le sol et les meubles | des capsules vides, le corps étant posé à côté, bloquaient le déplacement de la boîte | retirer la boucle `AddIgnoredActor` | +| CHG-151 | 2026-10-05 | ShowCouple | `FitToBodies` : la boîte suit les os affichés des deux corps, marge 4 cm. La mesure sur toute l'animation reste le repli si les os ne sont pas prêts | la boîte couvrait tout le clip plus 12 cm et entrait dans les objets autour du couple | retirer `FitToBodies` et rappeler `FitBox` à chaque clip | +| CHG-152 | 2026-10-05 | ShowCouple | La boîte ignore mains, pieds et os annexes. Seul le contact latéral le plus profond annule la direction qui y entre ; le sol et le lit ne coupent plus le déplacement horizontal | la boîte faisait 2,18 m et un contact sur le côté bloquait la gauche | revenir au `FitToBodies` et au balayage de CHG-151 | +| CHG-153 | 2026-10-05 | ShowCouple | Un contact de profondeur 0 ne bloque plus. Le flanc d'un lit ne repousse plus la boîte : elle peut y revenir, et un recouvrement la soulève vers le matelas | `Wall1`, `Wall4` et `Wall` coupaient la gauche sans pénétration, et quitter le lit empêchait d'y retourner | retirer `IsBedMesh` et le seuil de profondeur | +| CHG-154 | 2026-10-05 | ShowCouple | La boîte ignore l'acteur `Wall` de CentralLondon. Le lit ne la pousse plus vers le plafond ; seul le dessus du matelas la porte | la coque `Wall` coupait la gauche dans le vide, et la poussée du lit collait le couple au plafond | retirer l'ignorance de `Wall` et la sortie du flanc de lit | +| CHG-155 | 2026-10-05 | ShowCouple | Un lit est reconnu par son nom. Il ne bloque un déplacement que vers le bas, pour se poser ; le retour horizontal n'est plus arrêté et le contact ne renvoie plus la boîte | après avoir quitté le lit, la boîte ne pouvait plus y revenir | revenir au test `DoesPackageExist` et au filtre du flanc seul | +| CHG-156 | 2026-10-05 | Clavier | `LPKeyboardSubsystem` : bouton « Clavier » ajouté à l'exécution dans `MainMenu.SettingsScrollBox` ; panneau pour changer, ajouter ou retirer une touche (actions et axes clavier). Écrit dans `Saved/Config/Windows`, pas dans `GameSettings.sav` | pouvoir réaffecter les touches, dont Q, sans modifier les Blueprints | retirer `LPKeyboardSubsystem.h/.cpp` et la dépendance `UMG` | +| CHG-158 | 2026-10-05 | Réglages | `LPSettingsSubsystem` : onglets Audio, Video, Lewd, General, Keyboard, Misc au-dessus de `SettingsScrollBox`. Les lignes du Blueprint restent en place et sont montrées selon leur texte ; le reste va dans Misc. Keyboard affiche la liste des touches | la liste unique ne montrait pas le clavier | retirer `LPSettingsSubsystem.h/.cpp` | +| CHG-220 | 2026-10-06 | S8 | `Docs/Migration/22_Eval_Agents_Sandbox.md` (nouveau) ; `Docs/Migration/etat/sandbox/S8/S8-VAL.md` (nouveau) ; `Docs/Migration/21_Sandbox_Addon.md` (§ Acceptation : S8 coché, push addon coché, deux cases ouvertes ajoutées `BuildMod -Target` et commit côté jeu ; ligne S8 du tableau des lots) ; `Saved/Logs/S8-VAL-runtime-game.log` (copie locale, non versionnée) | lot S8-T1/T2 : bilan du mode multi-agents + multi-modèles sur S0–S7 (lots tenus / coupés, consignes et reprises, adéquation modèle ↔ fiche : 11 fiches sur 27 tracent le modèle réel, S3-T3 sous-classé, S2-T2 sur-classé ; erreurs de rôle ; friction git : submodule abandonné, jeu jamais commité, trois identités git) ; décision **GO conditionnel** et 3 recommandations (politique modèles tracée, clôture git par lot, séparation VAL / DOC + registre des reports) ; validation S8-VAL **PASS** : `Tools/debug/LifePlay_pair_sandbox.bat` lancé tel quel, plugin monté, `LPPairSandboxGameMode`, 16 lignes `LogLPSandbox`, boîte `AgMakeout` identique à S5 / S7-VAL, 0 crash ; constat : `origin/main` addon = `dae6206` (pin `f09c42e` poussé) | aucun changement de code C++ ni de configuration | retirer `22_Eval_Agents_Sandbox.md`, `etat/sandbox/S8/`, cette entrée ; rétablir § Acceptation et ligne S8 de doc 21 (état S7) | +| CHG-219 | 2026-10-06 | S7 | `LifePlay.uproject` (plugin `LPSandbox` : `Optional: true`) ; repo addon `LifePlay-Sandbox` `Plugins/LPSandbox/LPSandbox.uplugin` (`SupportedTargetPlatforms: ["Win64"]`, module `LPSandbox` : `TargetConfigurationDenyList: ["Shipping"]`, description ; commit local `f09c42e`, non poussé) ; `Docs/Migration/21_Sandbox_Addon.md` (§ Acceptation, ligne S7, pin), `Docs/Migration/etat/sandbox/S3/S3-INT.md` (pin) ; `Docs/Migration/etat/sandbox/S7/S7-SPEC.md`, `S7-IMPL-L.md`, `S7-CART.md`, `S7-VAL.md` (nouveaux) ; `Saved/Logs/S7-VAL-*.log` (copies locales, non versionnées) | lot S7-T1..T4 : contrat d'isolation du plugin (aucun include / `LIFEPLAY_API` / module `LifePlay` : 0 résultat ; `Person` en lecture de CDO seule, aucun `GI` / `Room` ; chemins par config) ; `LPSandbox` exclu du Shipping (`TargetAllowList: ["Editor"]` rejeté : le sandbox tourne en `-game`), `LPAnimMeasure` conservé car requis par `LifePlay.Build.cs` ; validation S7-VAL **PASS** : build `LifePlayEditor Development` à jour, plan UBT Game Development (6 occurrences `LPSandbox`) / Shipping (0), smoke Pair ~1 min 45 s (boîte AgMakeout identique à S5-VAL, 0 crash) ; limites : pas de compilation Shipping complète, commit addon non poussé, `Optional` n'évite pas l'échec du build sans clone sibling | aucun changement de code C++ | `git revert f09c42e` dans l'addon, retirer `"Optional": true` du `.uproject`, remettre le pin `c1fdd29`, retirer `S7/`, § Acceptation et l'entrée journal | +| CHG-218 | 2026-10-06 | S6 | `Docs/Migration/20_Chaine_Animations.md`, `Docs/Migration/09_Commandes_Utiles.md`, `Docs/Migration/21_Sandbox_Addon.md` (lot S6), `Docs/Migration/etat/sandbox/S6/S6-VAL.md` (nouveau) ; `Saved/AnimPipeline/S6-VAL/`, `Saved/Logs/S6-VAL-LPAnimEval.log` (copies locales, non versionnées) | lot S6-T1…T3 : confirmation que `LifePlayEditorTools.Build.cs` dépend déjà de `LPAnimMeasure` (module addon, aucun include cassé, 0 fichier `Source/LPAnimMeasure/`) ; build `LifePlayEditor Win64 Development` à jour ; doc chaîne animations et commandes alignées (plugin `LPSandbox`, bats `Tools/debug`, sorties `Saved/Measurement`, commandlets `LPAnimEval` / `LPAnimTwist` / `LPAnimCatalog`) ; validation S6-VAL **PASS** : `-run=LPAnimEval -Clips=Idle_Loop -Instants=1` (~1,3 s, code 0, `rapport.json` valide, `LogLPAnimMeasure` actif) ; bat Mesure non rejoué (déjà PASS en S2-VAL) | aucun changement de code C++ | retirer `S6-VAL.md`, entrée journal, rétablir les trois docs | +| CHG-217 | 2026-10-06 | S5 | repo addon `LifePlay-Sandbox` (`Plugins/LPSandbox/Source/LPAnimMeasure/LPCoupleSim.h` nouveau 215 l., `LPCoupleSimCore.cpp` nouveau 474 l., `LPSandbox/LPCoupleSim.cpp` 477 → 384 l., commit `c1fdd29` ; `README.md` : table du pin, commit `bed7901`, doc seule, le pin reste `c1fdd29`) ; `Source/LifePlay/LPShowCoupleSubsystem.h/.cpp` (1048 → 805 l., `FSavedMove` imbriquée supprimée, fichiers non suivis par git) ; `Docs/Migration/etat/sandbox/S5/S5-CART.md`, `S5-IMPL-H.md`, `S5-VAL.md` (nouveaux) ; `Docs/Migration/21_Sandbox_Addon.md` (section « CoupleBox », lot S5, pin) ; `Docs/Migration/etat/sandbox/S3/S3-INT.md` (pin) ; `Saved/Logs/S5-VAL-runtime-game.log`, `S5-VAL-build.log` (copies locales, non versionnées) | lot S5-T1…T4 : fin de la dérive de fork entre la sandbox Pair et `LPShowCoupleSubsystem` ; une seule source de vérité `namespace LPCoupleSim` dans le module `LPAnimMeasure` (niveau pur, lecture d'assets, effets de bord, `ResolveCoupleMove` avec `FSweepOptions::Show()` / `Sandbox()`), −336 lignes dupliquées ; écarts conservés par options (plancher −90 / −88, 2 / 3 itérations) ; builds `LPSandboxEditor` (non-unity) et `LifePlayEditor` (unity) 0 erreur / 0 avertissement. Validation S5-VAL : PASS : bat Pair depuis le jeu (~90 s) : plugin monté, `LPPairSandboxGameMode`, 16 lignes d'initialisation `LogLPSandbox`, ligne `Sandbox boite AgMakeout` (fond −90,000 cm), 0 erreur de module, aucun crash ; ShowCouple : PASS-STATIC (compilation, 34 références `LPCoupleSim`, copies locales supprimées), parcours show en jeu non exercé ; pin addon `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` | `LifePlay-Sandbox` au pin `f8102647ccdebad37e4977ccbf2c51e583823a80` (`LPCoupleSim` absent du module partagé) ; `LPShowCoupleSubsystem` et `LPCoupleSim.cpp` (sandbox) avec leurs copies locales de la géométrie | addon : `git checkout f8102647ccdebad37e4977ccbf2c51e583823a80` ; jeu : restaurer `LPShowCoupleSubsystem.h/.cpp` à la version d'avant S5-T2 (fichiers non suivis : pas d'instantané git, défaire à partir de `S5-IMPL-H.md` §3) ; retirer `S5-CART.md`, `S5-IMPL-H.md`, `S5-VAL.md`, cette entrée ; rétablir le pin `f8102647…` dans doc 21 / `S3-INT.md` | +| CHG-216 | 2026-10-06 | S4 | repo addon `LifePlay-Sandbox` (`LPInteractionSandbox.h/.cpp` supprimés, `LPSandbox.Build.cs`, `README.md`, `Docs/S3-move.md`, commit `f810264`) ; `Docs/Migration/etat/sandbox/S4/S4-VAL.md` (nouveau) ; `Docs/Migration/21_Sandbox_Addon.md` (lot S4/S6, pin addon) ; `Docs/Migration/etat/sandbox/S3/S3-INT.md` (pin) ; `Saved/Logs/S4-VAL-runtime-game.log` (copie locale, non versionnée) | lot S4-T3/T4a : retrait du GameMode Interaction legacy dans l'addon (~543 LOC, doublon chat/vues) ; grep addon sans `LPInteractionSandbox` / `ALPInteractionGameMode` ; mode Interaction via menu `LPPairSandbox` ; build `LifePlayEditor Win64 Development` OK ; essai Pair ~45 s depuis le jeu : plugin monté, `LPPairSandboxGameMode`, 16 lignes `LogLPSandbox`, 0 erreur module ; pin addon mis à jour `f8102647ccdebad37e4977ccbf2c51e583823a80` | addon : restaurer `LPInteractionSandbox.*` depuis commit `32ad09c` ; jeu : retirer `S4-VAL.md`, entrée journal, rétablir pin `4de1f0b` dans doc 21 / `S3-INT.md` | +| CHG-215 | 2026-10-06 | S3 | `LifePlay.uproject` (`AdditionalPluginDirectories`, plugin `LPSandbox`) ; `Source/` (retrait de `LPAnimMeasure/`, `LPPairSandbox*`, `LPInteractionSandbox*`, `LPSandboxConfig*`) ; `Config/LPSandbox.ini` (copie dans l'addon) ; `Source/LifePlay/LifePlay.Build.cs`, `Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs` ; `Docs/Migration/21_Sandbox_Addon.md` (section stratégie git, lot S3) ; `Docs/Migration/etat/sandbox/S3/S3-INT-scaffold.md`, `S3-IMPL-L.md`, `S3-IMPL-H.md`, `S3-INT.md`, `S3-VAL.md` (nouveaux) ; repo addon `LifePlay-Sandbox` (nouveau : `README.md`, `Docs/S3-move.md`, plugin `LPSandbox`) | lot S3 sandbox→addon : code sandbox (`LPSandbox` + `LPAnimMeasure`) transféré vers le dépôt séparé `LifePlay-Sandbox` (https://github.com/edujiro5972/LifePlay-Sandbox.git, `main`) ; stratégie git : deux clones siblings (`D:\Game\LifePlay-master 5.5`, `D:\Game\LifePlay-Sandbox`) + `AdditionalPluginDirectories = ["../LifePlay-Sandbox/Plugins"]`, **pas de submodule** (plugin imbriqué `Plugins/LPSandbox/Plugins/LPSandbox` ou doublon `External/`) ; pin manuel `4de1f0b6764ed3bc575bb687630d3f070e1be221` (S3-T4 : `FLPSandboxModule::StartupModule` injecte `QuaterniusUALRoot` dans `LPAnimRedirect::SetUALRoot`) ; Content monté dans l'addon par junction NTFS (`AdditionalAssetDirectories` absent d'UE 5.5) ; build `LifePlayEditor Win64 Development` OK (S3-INT). Validation S3-VAL : PASS : bat Pair depuis le jeu (~85 s) : plugin monté, `Game class is 'LPPairSandboxGameMode'`, 16 lignes `LogLPSandbox`, 0 erreur de module ; addon mince `LPSandbox.uproject -game` : GameMode et sandbox OK, réserve : 2 458 erreurs de Blueprints du jeu (`StreetMapBuilding`, plugin `StreetMap` non activé dans l'addon), sans effet sur le sandbox ; avertissement hors lot : `/Game/Quaternius/UAL/M_Joints` absent | `LifePlay` : code sandbox et `LPAnimMeasure` dans `Source/` ; aucun `AdditionalPluginDirectories` ni plugin `LPSandbox` dans `LifePlay.uproject` | retirer `AdditionalPluginDirectories` et l'entrée `LPSandbox` de `LifePlay.uproject` ; recopier les sources depuis le dépôt addon (commit `d18243e` pour le code transféré, `4de1f0b` pour le pin) vers `Source/LifePlay/` et `Source/LPAnimMeasure/` (`LPSANDBOX_API` -> `LIFEPLAY_API`) ; rétablir `game=/Script/LifePlay.LPPairSandboxGameMode` dans les trois bats `Tools/debug/` ; supprimer le clone sibling si inutile | +| CHG-214 | 2026-10-06 | S2 | `Source/LPAnimMeasure/` (nouveau : `LPAnimMeasure.Build.cs`, `LPAnimMeasure.h/.cpp`) ; `LPPoseMeasure.h/.cpp`, `LPSkeletonProfile.h/.cpp`, `LPPoseOrchestrator.h/.cpp`, `LPAnimRedirect.h/.cpp` (déplacés de `Source/LifePlay/`) ; `LifePlay.uproject`, `Source/LifePlay.Target.cs`, `Source/LifePlayEditor.Target.cs`, `Source/LifePlay/LifePlay.Build.cs`, `Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs`, `Source/LifePlay/LifePlay.cpp`, `Source/LifePlay/LPInteractionSandbox.cpp`, `Tools/LogStats.ps1` (modifiés) ; `Docs/Migration/21_Sandbox_Addon.md` (section module + graphe de dépendances) ; `Docs/Migration/etat/sandbox/S2/S2-CART.md`, `S2-IMPL-H.md`, `S2-VAL.md` (nouveaux) | lot S2 sandboxaddon : chaîne de mesure de poses extraite de `LifePlay` vers le module Runtime `LPAnimMeasure` (8 fichiers déplacés, `LPANIMMEASURE_API`, catégorie `LogLPAnimMeasure`, 6 exports ajoutés pour `LPPairSandbox.cpp`) ; couplage à `LPSandboxConfig` rompu par `LPAnimRedirect::SetUALRoot` appelé depuis `FLifePlayModule::StartupModule` ; module acyclique (aucune dépendance vers `LifePlay`) ; `#include "Engine/TextureCube.h"` ajouté à `LPInteractionSandbox.cpp` (défaut IWYU révélé par la recomposition unity) ; compteur `LogLPAnimMeasure` dans `LogStats.ps1`. Validation S2-VAL : PASS (11 fichiers dans le module, 0 reliquat dans `LifePlay`, `.uproject` et deux `Build.cs` conformes, DLL `UnrealEditor-LPAnimMeasure` du 18:34 postérieure aux sources, bat Mesure lancé : 269 lignes `LogLPAnimMeasure`, profils chargés, mesure et orchestrateur en marche, 0 erreur de module ou de lien). Reste : `S1-CART.md` à mettre à jour, commandlets anim non relancés, contrôle visuel non fait | `LifePlay` : les 8 fichiers étaient dans `Source/LifePlay/` (`LPANIMMEASURE_API` -> `LIFEPLAY_API`, `LogLPAnimMeasure` -> `LogLifePlay`, `LifePlay.h` inclus) ; `LifePlay.cpp` en `FDefaultGameModuleImpl` ; aucun module `LPAnimMeasure` | remettre les 8 fichiers dans `Source/LifePlay/` avec leurs ancres d'origine (fichiers non suivis par git, aucun instantané enregistré : défaire les modifications listées dans `S2-IMPL-H.md` §2) ; supprimer `Source/LPAnimMeasure/` ; retirer l'entrée du `.uproject`, des deux `*.Target.cs` et des deux `Build.cs` ; rétablir `FDefaultGameModuleImpl` dans `LifePlay.cpp` | +| CHG-213 | 2026-10-06 | S1 | `Config/LPSandbox.ini` (nouveau) ; `Source/LifePlay/LPSandboxConfig.h/.cpp` (nouveaux) ; `Source/LifePlay/LPPairSandbox.cpp`, `LPAnimRedirect.cpp` (modifiés) ; `Docs/Migration/21_Sandbox_Addon.md` (§ Contrat) ; `Docs/Migration/etat/sandbox/S1/S1-SPEC.md`, `S1-IMPL.md`, `S1-CART.md`, `S1-VAL.md` (nouveaux) | lot S1 sandbox→addon : chemins du sandbox Pair externalisés dans `Config/LPSandbox.ini` (19 clés, 6 sections) lu par `FLPSandboxConfig::Get()` ; defaults = anciens littéraux, donc comportement inchangé sans ini ; 0 littéral `/Game/...` restant dans `LPPairSandbox.cpp` / `LPAnimRedirect.cpp` (S1-CART : 0 OUBLI) ; contrat résumé dans doc 21 ; validation S1-VAL : PASS statique, compilation UBT et runtime non exécutés (DLL `UnrealEditor-LifePlay` plus ancienne que les sources), test override `WorldFile` à faire à la main | `LPPairSandbox.cpp` et `LPAnimRedirect.cpp` : littéraux remplacés par `FLPSandboxConfig::Get().` ; `Mode` lu si `Saved/sandbox_mode.txt` absent | `git checkout` de `LPPairSandbox.cpp` / `LPAnimRedirect.cpp` (ou rétablir les littéraux de S1-SPEC § 2) ; supprimer `LPSandboxConfig.*`, `Config/LPSandbox.ini`, fichiers S1 ; rétablir le placeholder du § Contrat de doc 21 | +| CHG-212 | 2026-10-06 | S0 | `.gitignore` ; `Docs/Migration/21_Sandbox_Addon.md` (nouveau) ; `Docs/Migration/etat/sandbox/S0/S0-CART.md`, `S0-VAL.md` (nouveaux) | lot S0 sandbox→addon : motif `.gitignore` `Debug` → `/Debug/` pour que `Tools/debug/*.bat` ne soient plus ignorés sous Windows (`core.ignorecase`) ; ébauche doc 21 et inventaire gelé du périmètre (~7,5 k LOC, commandlets, lanceurs) ; validation S0-T2 (S0-VAL PASS : `git check-ignore` vide sur les 3 bats, sources témoin présentes) | ligne 5 : `Debug` → `/Debug/` | remettre `Debug` ; `git rm` des fichiers S0 et de `21_Sandbox_Addon.md` | +| CHG-211 | 2026-10-06 | AP6 | Planches d'images animées du catalogue des animations (mouvement visible au survol de la vignette). Demande de l'utilisateur après validation du catalogue ; un GIF a été écarté (256 couleurs, lourd, aucun encodeur dans Unreal ni dans le Python standard) au profit d'une planche d'images JPEG animée par la page. Modifications : (1) Sandbox (`Source/LifePlay/LPPairSandbox.h/.cpp`, **hors dépôt**) : mode « planches » de la tournée (`Saved/sandbox_tour.txt` contenant `planches`) : par clip, 12 images par seconde de clip (de 4 à 24), à intervalles réguliers (fraction i/N), profil droit, sans calque ni repères ; un même carré pour toutes les images du clip (union des os projetés, marge 15 %), réduit à 256 px ; images collées côte à côte, JPEG qualité 85 (`IImageWrapper`), `Saved/Measurement/planches/.jpg` + `.json` (`images`, `duree_s`, `taille`). Résultat : 43 planches, 4,3 Mo au total (ex. `Walk_Loop` 16 images, 101 Ko), tournée de 16 min 33 s, sans erreur (compilation : `etat/AP6_Build_20261006-160922.txt`). (2) `Tools/anim/Catalogue.py` : option `--planches` ; contrôle bloquant (planche `.jpg` et `.json` présents, `images` >= 2, `taille` 256 ; refus vérifiés pour planche absente et `images` = 1) ; bloc `planche` dans chaque fiche (`fichier`, `images`, `taille`, `ips` = images / durée) ; copie dans `Module/Animations/planches/.jpg`. Page : vignette fixe ; au survol (souris) la planche défile à la cadence du clip et la vignette revient à la sortie ; au toucher, lance / arrête ; la planche n'est chargée qu'au premier survol ; avec la préférence système « réduire les animations », seulement au clic. Correction après essai dans le navigateur : l'image et l'animation masquées restaient affichées (règle CSS `display` plus forte que l'attribut `hidden`), corrigé par `.card img[hidden], .card .anim[hidden] { display: none }`. Résultat : catalogue régénéré dans `ModSources/lpr_anim_quaternius/Module/Animations/` (42 fiches, 42 vignettes, 42 planches), `CATALOGUE OK`. Vérifié dans le navigateur : la planche se charge, l'animation remplace la vignette et avance à la cadence attendue (3 images en 250 ms pour 12,3 i/s) | lot AP6 de `20_Chaine_Animations.md` : planches du catalogue, animation au survol vérifiée dans le navigateur | `.\Tools\Restore.ps1 -Lot AP6` (même instantané qu'AP6 : sandbox, `Catalogue.py`) ; rien n'est commité | +| CHG-210 | 2026-10-06 | AP6 | Catalogue des animations (fiches calculées, `pack.json`, vignettes, `catalogue.json` et `catalogue.html`). Modifications : (1) `AnimSources/quaternius/pack.json` (nouveau) : déclaration du pack (source Quaternius Universal Animation Library Standard, CC0 1.0, fichier `.glb`, crédit, préfixe `qua`, exclusion `A_TPose`) et, pour les 42 clés, nom FR/EN, catégorie (les 8 du § 5 plus chute, magie, nage, conduite), accessoire, style, usage FR/EN ; champ facultatif `posture` qui remplace la posture calculée (`"dans l'eau"` pour les deux clips de nage). (2) `Source/LifePlayEditorTools/LPAnimCatalogCommandlet.h/.cpp` (nouveau), `-run=LPAnimCatalog` : pour chaque clé de `livraison.json`, mesure le clip féminin source image par image et contrôle que F et M ont même durée et même nombre d'images ; écrit `Saved/AnimPipeline/quaternius/fiches_calculees.json`. Mesures : durée, images, cadence ; boucle (dernière image comparée à la première, extrémités relatives à la hanche, en % de la taille ; boucle si ≤ 1 %) ; déplacement horizontal de la hanche (cm, cm/s, sens avant / arrière / côté, sur place sous 5 cm) ; posture par image (allongé : tronc > 60° ; à genoux : genou < 12 cm et hanche < 72 % ; assis : hanche < 72 % et plus de 20 cm en arrière des chevilles ; accroupi : hanche < 72 % ; debout sinon), posture du clip si ≥ 60 % des images, sinon « variable », posture de début et de fin ; hauteur d'assise (articulation de hanche, médiane des images assises) ; part des images pied au sol (< 3 cm de la hauteur de repos) ; mains au repos / en mouvement / levées. Deux corrections après la première relecture des fiches : axe avant du corps inversé (Roll sortait « arrière », Sitting_Enter « avant », la position assise sortait « accroupi ») et seuil debout abaissé de 80 % à 72 % (gardes de combat et poussée classées accroupi / assis). Résultat : 42 fiches, 0 échec (`etat/AP6_LPAnimCatalog_20261006-155043.txt`, puis le passage final du même jour). (3) Sandbox (`Source/LifePlay/LPPairSandbox.h/.cpp`, **hors dépôt**) : mode « vignettes » de la tournée (`Saved/sandbox_tour.txt` contenant le mot `vignettes`) : une vue par clip, profil droit à 1/3 de la durée, sans calque, squelettes ni repères de caméra ; image recadrée en carré autour des os du corps de Bridget projetés à l'écran (marge 15 %), réduite à 512 px et écrite par le délégué `UGameViewportClient::OnScreenshotCaptured` dans `Saved/Measurement/vignettes/.png` ; le `tour.csv` de la tournée complète n'est pas remplacé. 43 vignettes. La tournée complète (387 captures) a aussi été refaite après les corrections de torsion d'AP4. (4) `Tools/anim/Catalogue.py` (nouveau) : assemble `catalogue.json`, `catalogue.html` (page autonome hors ligne, filtres catégorie / posture / durée, recherche, clé copiable, thème clair et sombre, lisible sur téléphone) et `vignettes/.png` dans `ModSources//Module/Animations/`. Champs du § 5 ; `taille_ko` = taille des `.uasset` **non cuits** (`taille_cuite: false`, la taille cuite viendra en AP7) ; qualité = « accepté » ou « à revoir » avec motifs (rapport AP4), pire écart brut et pire torsion des deux corps ; empreinte SHA-256 des deux clips. Contrôles bloquants (rien n'est écrit si un échoue) : rapport AP4 valide ; `fiches_calculees.json` sans échec et plus récent que `livraison.json` ; fiche calculée et entrée `pack.json` pour chaque clé ; clés uniques ; aucune collision avec les 17 clés du jeu, les clés `ANIMATION:` / `animate()` des modules texte (hors module d'essai) et les autres catalogues de `ModSources` ; champs obligatoires ; vignette présente. Essais : un succès (42 fiches), trois refus (vignette manquante, collision avec `dance`, fiches plus anciennes que la livraison). Corrections après relecture : hauteur d'assise affichée seulement pour une posture assise ; écart de boucle arrondi ; cartes filtrées réellement masquées ; cadre des vignettes carré. (5) `Tools/anim/DeliverAnims.ps1` : commutateurs `-Catalogue` (après la livraison) et `-CatalogueOnly` (régénère sans relancer Unreal, après le contrôle du rapport AP4). Résultat : `ModSources/lpr_anim_quaternius/Module/Animations/` : `catalogue.json`, `catalogue.html`, 42 vignettes ; `Saved/AnimPipeline/quaternius/catalogue_rapport.json` (`ok: true`, 42 fiches). Relecture des 5 témoins (marche, assis, pistolet, danse, chute) : conforme après corrections (marche sur place, boucle, pieds au sol 68 % ; assis, assise 45 cm ; chute variable, debout puis allongé, 53 cm en arrière, pas de boucle). Compilations `etat/AP6_Build_20261006-*.txt`, 0 erreur. Reste ouvert : taille cuite (AP7) ; validation visuelle de la page par l'utilisateur | lot AP6 de `20_Chaine_Animations.md` : une fiche par clip, champs calculés relus sur 5 clips témoins (marche, assis, pistolet, danse, chute) | `.\Tools\Restore.ps1 -Lot AP6` (retire le commandlet, `pack.json` et `Catalogue.py` ; remet `DeliverAnims.ps1`, la sandbox et les documents) ; le catalogue généré dans `ModSources` et les vignettes dans `Saved` ne sont pas dans l'instantané (sorties régénérables) ; rien n'est commité | +| CHG-209 | 2026-10-06 | AP5 | Livraison en mod et appel par `animate()`. Mécanisme du jeu relevé (exports `etat/L6f_dump_Person.txt`, `etat/AP5_LPDumpFunction_20261006-140513.txt`, journal `Saved/Logs/portage/AP5_LPDumpFunction_20261006-140513.log`, `AP5_Room_Dailies_CDO.txt`) : `.lpaction` `ANIMATION:` (`GI::EventGraph`) et `.lpscene` `Actor.animate()` (`GI::CallFunction`, aiguillage `SwitchString_22`, cas `animate` : `AnimateForced` vrai, `Person::PlayDaily(Parameters[0])`, `AnimateForced` faux) appellent `Person::PlayDaily()`. `PlayDaily` : aiguillage `SwitchString_7` de clés écrites en dur (martialart, sneak, dance, na_sleep, write…), sortie `Default` vide (une clé inconnue ne faisait rien) ; chemin générique (cas martialart, sneak, dance, fightlost, knockedout) : debout puis `PlayDailyAnim()`. `PlayDailyAnim` parcourt `Room.Dailies` (tableau fixe de 34 clips, 17 clés × F/M, `/Game/Heavy/New3/Anims/Daily/`, valeur par défaut de Room, aucun graphe de Room ne le modifie), coupe le nom au premier `_`, compare la partie gauche à la clé, prend `_M` si `IsMale`, sinon un suffixe ≠ `M` ; le clip trouvé va dans `currentDaily`, joué par les Anim Blueprints `Daily` (michael_Skeleton) et `Daily_F` (bridget_gp_Skeleton). Aucun chemin construit, aucun registre d'assets : la ligne 14 de `16_H3_Mods_Paquets.md` (animations trouvées par `GetAssetsByClass`) ne vaut donc pas pour ces animations. Squelettes des clips convertis `B_*` / `M_*` : bridget_gp_Skeleton / michael_Skeleton, identiques à ceux de `Daily_F` / `Daily`. Modifications : (1) `Source/LifePlay/LPModDailies.h/.cpp` (nouveaux) : `ULPModDailiesSubsystem` (GameInstance) relève au démarrage les `UAnimSequence` des plugins de type Mod sous `//Anims` nommés `_F` / `_M` (en éditeur, exploration synchrone du dossier, le registre étant asynchrone) ; dès que `GI.Room` existe (et à chaque nouvel acteur Room) les ajoute à la fin de `Room.Dailies` (une clé du jeu reste servie par le clip du jeu ; un clip de mod dont la clé existe déjà dans le jeu est ignoré avec avertissement) ; `ULPModDailiesLibrary::IsModDaily()` (BlueprintPure) ; journalise `PlayDaily() -> cle de mod` ou `cle inconnue`. `LifePlay.Build.cs` : dépendance `Projects`. (2) `Source/LifePlayEditorTools/LPPatchDailiesCommandlet.h/.cpp` (nouveau), `-run=LPPatchDailies [-DryRun]`, appliqué à `Content/BPs/Person.uasset` : (a) sortie `Default` de `SwitchString_7` vers un Branch sur `IsModDaily(clé)` puis le chemin générique (`Knot_13`) ; une clé inconnue du jeu et des mods ne fait toujours rien ; (b) le Split `_` de `PlayDailyAnim` cherche depuis la fin (`SearchDir FromEnd`) : une clé peut contenir `_`, sans effet sur les 17 clés du jeu. Contrôles intégrés (nœuds trouvés une seule fois, Default libre, relecture des liaisons, compilation sans erreur, sinon rien n'est sauvegardé) ; rejouable. Résultats : essai à blanc `etat/AP5_LPPatchDailies_20261006-143052.txt`, application `…-143149.txt`, contrôle `…-143248.txt` (« deja fait »). (3) `Tools/anim/DeliverAnims.ps1` et `Tools/anim/DeliverAnims.py` (nouveaux) : contrôle bloquant de `Saved/AnimPipeline/quaternius/rapport.json` (`valide`), affichage des « à revoir » ; crée `ModSources/lpr_anim_quaternius` (uplugin, `.lpmod` CC0, CREDITS) s'il manque ; monte le mod, copie (sans toucher aux sources) les 42 paires `B_` / `M_` (hors `A_TPose`) vers `/lpr_anim_quaternius/Anims/Female/qua__F` et `/Male/qua__M`, contrôle squelette, nombre d'images et durée, écrit `Saved/AnimPipeline/quaternius/livraison.json`, démonte. Contrôle de sortie `LIVRAISON copies 84 erreurs 0` ; `-DryRun`. Deux corrections pendant le lot : `-FullStdOutLogOutput` (les messages Python de niveau Log n'apparaissaient pas dans la sortie console) et exploration du dossier du mod avant copie (sinon une deuxième passe échouait : copies existantes invisibles du registre). Journal final `etat/AP5_DeliverAnims_20261006-143739.txt`. (4) `ModSources/lpr_anim_quaternius/` (nouveau) : mod d'assets, 42 clés × 2 clips. (5) `Content/Modules/lpr_anim_essai/` (nouveau, **module d'essai, jamais livré**) : action « Essai animations AP5 » (maison) lançant une scène : femme puis homme temporaires, `animate(qua_dance_loop)`, `animate(qua_sitting_idle_loop)`, `animate(qua_pistol_idle_loop)`, témoin `dance`, clé inexistante `qua_cle_inexistante`. Essai (jeu lancé en éditeur `-game`, mod monté dans `Mods/`, journal `Saved/Logs/LifePlay.log` du 2026-10-06 13:14–13:20) : `84 clip(s) ajoute(s) a Room_C_0.Dailies (118 au total)` ; les trois clés `-> cle de mod` sur la femme et l'homme (scène jouée deux fois) ; `qua_cle_inexistante -> cle inconnue`. Contrôle visuel par l'utilisateur, validé sur 5 points : bons clips sur la femme puis l'homme, corps bien posés, deux places distinctes, témoin `dance` normal, enchaînement des trois animations visible. Compilations : `etat/AP5_Build_20261006-143013.txt`, `…-143912.txt`, `…-150926.txt` (0 erreur) ; une première compilation avait échoué (collision de nom dans la compilation unitaire, corrigée par un espace de noms nommé). Reste à faire (hors AP5) : essai en jeu empaqueté avec le mod cuit en DLC (`BuildMod -Name lpr_anim_quaternius`), qui exige une nouvelle version de référence du jeu (`Person` et le C++ ont changé), à faire avec la prochaine publication ou en AP7 ; catalogue (AP6) ; retirer le module d'essai avant toute publication | lot AP5 de `20_Chaine_Animations.md` : livraison en mod, clés appelables depuis un scénario | `.\Tools\Restore.ps1 -Lot AP5` (remet `Person`, `LifePlay.Build.cs` et les documents ; retire les fichiers et dossiers créés : sources C++, outils, `ModSources/lpr_anim_quaternius`, `Content/Modules/lpr_anim_essai`) ; rien n'est commité | +| CHG-208 | 2026-10-06 | AP4 | Contrôles indépendants de la formule : nouveau module `LifePlayEditorTools/LPAnimChecks`, calculé à chaque image du clip converti et comparé au mouvement source mis à l'échelle (hauteur de hanche). Six contrôles : sol (pied ou orteil plus bas que la source, cm), glissement (pied qui bouge quand le pied source est posé, cm par image), genou (hyperextension derrière la ligne hanche-cheville au-delà de la source, cm), flexion (écart de flexion coude / genou avec la source, °), saut (rotation d'un os d'une image à l'autre au-delà de 1,5 × la source + 2°), pénétration (main ou coude dans les capsules du tronc, cm). Pas d'actif physique sur `bridget_gp` ni `michael` (recherche par sous-agent) : capsules du tronc construites depuis la peau du maillage (sommets dont l'os dominant est du tronc, une capsule par segment de colonne, rayon au 30e centile des distances à l'axe) ; Bridget : `abdomenLower` 10,1 cm, `abdomenUpper` 10,2, `chestLower` 11,7, `chestUpper` 6,0 ; Michael : 12,0 / 12,7 / 14,1 / 8,1 ; auto-test (articulation de l'os porteur à une profondeur égale au rayon) : écart 0,0000 cm. `LPAnimEval` : contrôles branchés (`controles_.csv`, verdict accepte / a revoir / rejete ; `a_revoir` dans `rapport.json`), seuils étalonnés sur les 43 clips : sol 3 cm, glissement 3 cm/image, genou 1 cm, flexion 5°, saut 10°, pénétration 1 cm ; « rejeté » au-delà de 3 × le seuil (`-RejetFacteur`), seul le rejet bloque ; options `-Meshes`, `-SansControles`, `-Seuil*`. Correction : `-Clips`, `-Bodies` et `-Meshes` s'arrêtaient à la première virgule (`FParse::Value` sans `bShouldStopOnSeparator=false`), aussi dans `LPAnimTwist`. Défaut trouvé par le contrôle de saut et corrigé : la décomposition flexion / torsion d'AP3, faite dans le repère du corps autour de l'axe en pose T, devenait instable quand le segment s'écartait d'environ 180° de cet axe (`PickUp_Table` 147,7° sur `lForearmBend`, `Roll` 44,9° sur `rThighBend`, `Sword_Attack` 26°, `Pistol_Reload` 26°, `Punch_Jab` 14°). Décomposition désormais dans le repère du parent, dans la formule (`LPPoseMeasure::TransferPose`) et dans `LPAnimTwist` (qui reprend la rotation complète portée par l'os Twist : relançable sur des clips déjà traités). Clips retraités (43 + 43) : clip converti 0,51 % → 0,19 % au pire, torsion 0,97° → 0,49° ; sauts : seul `Jump_Start` reste à 23,1° (`lForearmBend`). Relevé final sur 86 clips : 0 rejeté, 8 à revoir (4 par corps : `Death01`, `Fixing_Kneeling`, `Roll` pour le sol, entre 4,4 et 5,6 cm — poses au sol, hauteur de cheville différente entre squelettes ; `Jump_Start` pour le saut). Essai de refus (`-Clips=Jump_Start -SeuilSaut=5`) : code 1. Rapport HTML : `Tools/anim/RapportControles.py` (écrit par un sous-agent, libellé d'unité de la pose T corrigé) produit `/rapport.html` ; vérifié : 43 lignes de contrôles et 129 lignes d'écarts par corps, HTML bien formé, rendu contrôlé | lot AP4 de `20_Chaine_Animations.md` : contrôles indépendants et rapport | `.\Tools\Restore.ps1 -Lot AP4` (remet aussi la formule, `LPAnimTwist` et les clips d'avant AP4 ; `Tools/anim/RapportControles.py`, créé après l'instantané, est à retirer à la main) | +| CHG-207 | 2026-10-06 | AP3 | Commandlet `LPAnimTwist` : répartition de la torsion sur les clips convertis. Pour chaque clé et chaque paire Bend / Twist du profil (épaule, avant-bras, cuisse, gauche et droite), la rotation de l'os Bend depuis la pose T ajustée est décomposée autour de l'axe du segment : Bend garde la flexion, l'enfant Twist reçoit la rotation complète ; les autres os gardent leur transformation (contrôle à chaque clé, tolérance 0,01° et 0,01 cm, sinon le clip n'est pas sauvé). `-DryRun`, rapport `rapport_torsion.json`. `LPAnimEval` mesure désormais la torsion (écart de rotation des os Bend et Twist avec la formule, colonnes `torsion_deg`, `torsion_os`, seuil bloquant `-SeuilTorsion` facultatif) ; `TPoses` exporté. Résultat : 43 + 43 clips traités et sauvés, conservation 0,00000° / 0,00000 cm, torsion déplacée jusqu'à 179,9° ; écart de torsion avec la formule 178,9° → 0,97° ; directions : 0,00 % aux instants sur une clé, 0,51 % au pire entre deux clés (`Roll` à 1/3, interpolation séparée de Bend et Twist), 108 instants sur 129 à 0,00 % ; seconde passe : 0,001° encore déplaçable (idempotent). Une recuisson (`RetargetQuaternius.py`) défait ce traitement : à relancer après chaque recuisson jusqu'au pilote AP7 | lot AP3 de `20_Chaine_Animations.md` | `.\Tools\Restore.ps1 -Lot AP3` (remet les clips d'avant) | +| CHG-206 | 2026-10-06 | AP2 | Commandlet `LPAnimEval` (`LifePlayEditorTools`, dépendance `Json`) : étapes 3 et 5 de la chaîne sans fenêtre. Pose T de chaque corps (écrit `retarget_pose_.txt`), contrôles : segments ajustés ≤ `SeuilPoseT` (1 %), symétrie gauche / droite ≤ `SeuilSymetrie` (0,5°), réglages ≤ `SeuilReglage` (30°) ; évaluation de la formule et du clip converti sur chaque clip à `Instants` instants, ≤ `SeuilEval` (1 %). Rapports `Saved/AnimPipeline//rapport.json`, `rapport_poseT_.json`, `eval_.csv` ; code de sortie 0 / 1. `LPPoseMeasure` : relevé structuré de l'ajustement (`FLPTFitResult`, `TFitResult`) et fonctions exportées (`LIFEPLAY_API`), `LPSkeletonProfile` exporté. Contrôle : 22 s pour 43 clips × 3 instants × 2 corps (tournée de la sandbox : ~7 min, Bridget seule) ; fichiers de pose identiques à l'octet ; `eval_Bridget.csv` identique ligne à ligne à la tournée ; Michael mesuré pour la première fois : 129 instants, 0,00 % (pire réel 0,0026 %) ; symétrie 0,000°. Essai de refus (seuils impossibles) : code 1, 24 causes listées | lot AP2 de `20_Chaine_Animations.md` | `.\Tools\Restore.ps1 -Lot AP2` (retire le commandlet) | +| CHG-205 | 2026-10-06 | AP1 | Profils de squelettes : `Tools/anim/profiles/ue4_mannequin.json` et `genesis8.json` (rôle de chaque os, réglages de pose T Daz). `LPSkeletonProfile` (nouveau, dépendance `Json` dans `LifePlay.Build.cs`) les lit et identifie un squelette (≥ 95 % des os du profil). `LPPoseMeasure` : toutes les tables en dur (segments, segments de pose T, paumes, correspondances, chaînes colonne et cou, contrôles du tronc, repère, réglages A → T) sont dérivées des rôles pour tout couple de profils ; journaux propres à Genesis retirés (attaches, tronc en pose T). Sandbox : points clés, tête et bassin par rôle. `RetargetQuaternius.py` : chaînes et racine dérivées des mêmes profils, identification des maillages. Contrôle : 3 squelettes identifiés à 100 % (C++ et Python) ; 19 segments, 39 segments de pose T, 47 os transférés, 26 chaînes par rig (inchangés) ; `retarget_pose_Bridget.txt` et `_Michael.txt` identiques à l'octet ; tournée complète identique ligne à ligne (129 instants, 0,00 %) ; recuisson 43 + 43 clips, tournée courte sur 5 clips à 0,00 %. Incident : une première version du script de refonte a coupé le mauvais bloc (nom de structure présent deux fois) ; fichier remis depuis l'instantané `AP1` avant de recommencer | lot AP1 de `20_Chaine_Animations.md` : une seule table de correspondance | `.\Tools\Restore.ps1 -Lot AP1` (retire `Tools/anim`, `LPSkeletonProfile.*` à retirer à la main : créés après l'instantané) | +| CHG-204 | 2026-10-06 | PoseMeasure | Phase C : `Tools/RetargetQuaternius.py` réécrit. La pose de retarget de `RTG_Bridget` et `RTG_Michael` est la pose T ajustée, lue dans `Saved/Measurement/retarget_pose_.txt` (écrit par la sandbox à l'entrée du mode Mesure, `LPPoseMeasure::WriteRetargetPose`, 34 os : épaule 45,6°, cuisse 8,3°, avant-bras 14,2°… ; Michael calculé sur son propre squelette) ; plus d'alignement par le maillage, de réglages Daz séparés ni d'essais `pole` / `roll180`. Chaîne colonne `abdomenLower → chestUpper` (le pelvis Genesis n'est pas son parent) et cou en *Interpolated* ; 10 chaînes de doigts, chaînes d'orteils. Recuisson : 43 clips `B_` et 43 `M_`. Tournée complète : clip recuit 0,00 % sur les 129 instants (ancien clip : médiane 29,2 %, pire 58,7 %), même résultat que la formule. Non mesuré : répartition de la torsion entre os Bend et Twist (le retargeter met toute la rotation dans l'os Bend), et Michael (sandbox sur Bridget seule). `BuildMod.ps1 -Retarget` échoue depuis l'environnement de l'agent (l'éditeur lancé par `Start-Process` s'arrête aussitôt sans journal) : éditeur lancé par `cmd /c start` avec les mêmes arguments | phase C du plan | `.\Tools\Restore.ps1 -Lot PoseRetarget` | +| CHG-203 | 2026-10-06 | PoseMeasure | Calque de pose : le calque orange n'est plus le squelette du mannequin mis à l'échelle (proportions et clavicule du mannequin), mais le squelette de Bridget posé par la formule (directions du mannequin, longueurs de Bridget), calé sur le bassin affiché, sans les os du visage, dessiné au premier plan. En vue pose T, c'est la pose T ajustée. Le contrôle caméra (`MESURE vues`) compare Bridget à ce calque (`CalqueKeys`) : 20 % auparavant (écart de proportions), 0,00 % en vue formule. Les traits depuis l'os racine (sol → hanche) ne sont plus dessinés | le calque mélangeait pose et proportions (question de l'utilisateur) | `.\Tools\Restore.ps1 -Lot PoseCalque` | +| CHG-202 | 2026-10-06 | PoseMeasure | La formule devient la vue par défaut du mode Mesure (la boucle de l'orchestrateur ne démarre plus seule, bouton Auto conservé). Navigation : N / P change de clip parmi les 43 clips Quaternius qui ont une copie `B_`, PgUp / PgDn change d'instant (`LPPoseOrchestrator::SampleAt`). Bouton Tournée, ou fichier `Saved/sandbox_tour.txt` posé avant le lancement (noms de clips facultatifs) : chaque clip à 0, 1/3 et 2/3 de sa durée, vues face, profil droit et dos, captures `Saved/Measurement/tour/*.png` (387 images, ~540 Mo) et relevé `tour.csv`. Contrôle du tronc par rotation depuis la pose T (bassin, poitrine, tête) au lieu des segments de colonne. Colonne de la formule : `spine_01` ↔ `abdomenLower` et `spine_03` ↔ `chestUpper` exacts, os intermédiaires interpolés (la poitrine s'écartait de 1 à 6,6 %). Relevé sur 129 instants : formule 0,00 % partout (contrôle de cohérence, pas une validation) ; clip converti médiane 29 %, pire 58,7 % (`Pistol_Aim_Down`, main gauche). Planche visuelle : épée, course, genou à terre, visée cohérents avec le mannequin | étapes 1 et 2 du plan après validation de la formule | `.\Tools\Restore.ps1 -Lot PoseTour` | +| CHG-201 | 2026-10-06 | PoseMeasure | Phase B : `LPPoseMeasure::TransferPose`, pose de Bridget calculée directement depuis le mannequin (rotation de chaque os depuis la pose T ajustée, portée par le changement de repère M). Correspondance : bassin → `hip`, clavicules, bras, avant-bras, mains, cuisses, tibias, pieds, orteils, 30 phalanges ; décomposition flexion / torsion (`ToSwingTwist`) entre os Bend et Twist (épaule, avant-bras, cuisse) ; colonne (3 → 4 os) et cou (2 → 3) interpolés à la même fraction de hauteur ; déplacement du bassin mis à l'échelle par la hauteur de hanche ; os non associés en rotation locale de pose T. Touche F ; contrôle `Bridget-formule` à chaque passage ; capture `formule_NNNN_.png` à l'arrêt de la boucle. Relevé : membres, mains, doigts et clavicules ≤ 1 % ; colonne en Idle `abdomenUpper` 6,5 %, `chestUpper` 1,8 % (interpolation de chaîne contre mesure os par os). Capture : poings fermés comme le mannequin, épaules symétriques | méthode : équation de transformation avant l'ajustement fin | `.\Tools\Restore.ps1 -Lot PoseTransfer` | +| CHG-200 | 2026-10-06 | PoseMeasure | Journal `POSET tronc` : position des articulations du tronc en pose T (avant / côté / haut depuis la hanche gauche, en % de la taille) pour les deux squelettes. Constat : le pelvis Genesis est au même point qu'`abdomenLower` (segment de longueur nulle, d'où les 60 %) ; la colonne n'a pas les mêmes jonctions (3 os mannequin, 4 Genesis avec `chestLower`) ; la clavicule du mannequin part de l'arrière du tronc (4,8 % derrière la hanche), celle de Genesis du sternum (2,9 % devant) | question de l'utilisateur sur les écarts non ajustés | `.\Tools\Restore.ps1 -Lot PoseTFit` | +| CHG-199 | 2026-10-06 | PoseMeasure | Phase A (équivalence en pose T) : `FittedBodyT` ajuste la pose T Daz de Bridget sur la pose T du mannequin (jambes, bras, mains, paumes, 28 phalanges), une fois par session ; tout l'étalonnage passe sur cette pose T ajustée. Touche T : les deux poses T côte à côte et le relevé dans le panneau. Journal `POSET` : écarts avant → après (pouce 17 % → 0, pied 9,4 % → 0, avant-bras 8,2 % → 0, tous les segments ajustés à 0,0 %) et rotations locales à reporter dans `RTG_Bridget` (`POSET reglage`, symétriques gauche/droite : pied 15°, avant-bras 14,2°, main 10,4°, pouce 24 et 22°, autres doigts 0,5 à 8,6°, cuisse, bras 2,3°). Bassin, colonne, cou et clavicules relevés sans ajustement (définition des os). Modèles `metric links4`. En Idle, la main reste tournée d'environ 180° : à confirmer visuellement | méthode : établir l'équivalence des deux poses T avant toute transformation | `.\Tools\Restore.ps1 -Lot PoseTFit` | +| CHG-198 | 2026-10-06 | PoseMeasure | `LPPoseMeasure` : tous les segments sont étalonnés sur une pose T commune (`FLPPose.TPose`) : le repos du mannequin ; pour Genesis, le repos plus les réglages Daz A → T de `RetargetQuaternius.py` (`DazToT`, pose de retarget de `RTG_Bridget`). On compare la rotation de chaque os depuis sa pose T. L'épaule redevient le segment clavicule ; paume mesurée par index → auriculaire (avec le pouce, 70° d'écart dès la pose T). Le clip brut est contrôlé à chaque passage (`MESURE Bridget-brut`). Touche M : maillage du mannequin. Relevés : écart entre poses T faible (épaule 1,3 %, cuisse 1,3 %, main 3 %, paume 2,4 %) sauf avant-bras 8,2 %, pied 9,4 %, clavicule 19 %, pelvis 60 % ; clip brut hors seuil (avant-bras 24 à 27 %, main 25 %, pied 12 %, tibia 7,7 %, haut du dos et cou 13 % en Idle) ; corrigé 0,01 %. La main reste tournée de 177° : non validé. `LPPairSandbox.h` (bShowMannequin) est hors instantané `PoseT` | les comparaisons mélangeaient pose, morphologie et définition des os | `.\Tools\Restore.ps1 -Lot PoseT` | +| CHG-197 | 2026-10-06 | PoseMeasure | `LPPoseMeasure` : l'épaule n'est plus mesurée par la clavicule étalonnée, mais par la direction base du cou → articulation de l'épaule (`neck_01→upperarm` / `neckLower→ShldrBend`), visée en tournant la clavicule (point de la sphère du mobile sur la demi-droite visée). Champs `Mover` et `Label` (`lShoulder`, `rShoulder`) ajoutés à `FLink`. Modèles `metric links2`. Rotation des clavicules : 24 à 26° (4° avant), bras 17 à 24° ; contrôle 0,29 % au pire, round 854, seuil atteint | contrôle visuel (R) : en brut les épaules sont rejetées en arrière et les bras traversent le corps ; l'étalonnage au repos recopiait l'erreur du retargeting (repos mannequin en T, Genesis en A) | `.\Tools\Restore.ps1 -Lot PoseShoulder` | +| CHG-196 | 2026-10-06 | PoseMeasure | Touche R du mode Mesure : Bridget brute (clip `B_` au même instant, sans correction) ou corrigée, au même cadrage, sans lancer de passage (`FLPOrchestratorRound.BridgetRaw`). Le contrôle porte sur la pose affichée. Les passages automatiques repassent en vue corrigée. Corrections relevées dans les modèles : clavicules ~4°, bras ~5°, avant-bras ~48°, mains 106 à 117° | l'utilisateur voit un défaut d'épaule ; il faut savoir s'il vient du clip converti ou de la correction avant d'agir | `.\Tools\Restore.ps1 -Lot PoseRaw` | +| CHG-195 | 2026-10-06 | PoseMeasure | Caméra libre du mode Mesure (`LPPairSandbox`) : touche 0, départ au cadrage de la caméra fixe en cours, ZQSD/WASD, E/Espace et C/Ctrl pour monter et descendre, Maj ×3, clic droit + souris pour orienter ; 1 à 9 reviennent aux caméras fixes. Elle survit aux passages automatiques (vue nommée « Libre » dans les captures) et est détruite à la sortie du mode. Aide ajoutée au panneau. Démarrage vérifié (round 840, seuil atteint) ; déplacement non testé | contrôle visuel par l'utilisateur | `.\Tools\Restore.ps1 -Lot PoseFreeCam` | +| CHG-194 | 2026-10-06 | PoseMeasure | `LPPoseMeasure` : score, correction et contrôle sur la même table `Links` (19 segments : colonne, cou, pieds, mains, clavicules), au lieu des 10 segments visés. Pelvis et clavicules étalonnés sur la pose de repos (`FLPPose.Rest`) : la rotation de l'os depuis son repos porte la direction de repos du mannequin. Écarts au repos relevés (`ETALON`) : pelvis 60 %, clavicules 19,1 % (= l'écart mesuré en jeu, donc différence de définition), bras 24 à 27 % (pose A contre T, non étalonnés). Modèles `metric links1`, colonne `links`, anciens modèles et `best_Face.png` retirés. Résultat (rounds 829 à 832) : contrôle 0,29 % au pire (lFoot), aucun os au-dessus de 1 %, boucle arrêtée sur « seuil atteint ». Le contrôle n'est plus indépendant de la correction : l'image `shots/` sert de vérification. Le texte du panneau (`LPPairSandbox.cpp`) a aussi changé, hors instantané `PoseLinks` | le contrôle signalait pelvis 50 % et clavicules 19 % alors que le score de l'orchestrateur valait 0 | `.\Tools\Restore.ps1 -Lot PoseLinks` | +| CHG-193 | 2026-10-06 | PoseMeasure | Boucle automatique du mode Mesure (`LPPairSandbox`, `LPPoseOrchestrator`) : démarre à l'entrée du mode, un passage toutes les 2 s, capture sans interface 0,6 s après chaque passage (`Saved/Measurement/shots/round_NNNN_.png`), référence `best_.png` à la première capture ou quand le modèle est remplacé. Arrêt au seuil, sur plateau (5 passages identiques) ou après 200 passages ; bouton Auto / Arrêter ; journal `AUTO`. `FLPOrchestratorRound.bKept` ajouté. Vérifié : rounds 804 à 828, arrêt sur plateau, images nettes | l'utilisateur n'apporte rien en lançant chaque passage | `.\Tools\Restore.ps1 -Lot PoseAuto` | +| CHG-192 | 2026-10-06 | PoseMeasure | Sandbox Mesure : la sphère de la caméra active n'est plus dessinée (bande jaune), figures à ±90 cm, caméras décalées de 55 cm hors du panneau ; contrôle indépendant `LPPoseMeasure::Compare` sur Bridget après chaque clip (ligne `controle` du panneau, `MESURE os` au journal). Premier relevé : pelvis 49,5 % / 51,7 %, lCollar et rCollar 19,1 %, abdomenLower 9 à 11 % | Bridget était sous le panneau ; le score de l'orchestrateur vaut 0 par construction | `.\Tools\Restore.ps1 -Lot PoseView` | +| CHG-191 | 2026-10-06 | PoseMeasure | `LPPoseMeasure.cpp` : score et visée en direction des segments (angle en % de 180°, repère du corps), plus en positions mises à l'échelle. Modèles marqués `metric direction1`, colonne `metric` = `direction` ; anciens `best_*.txt` retirés (copies dans l'instantané). Manches 697 à 712 : 10,76 → 0 (Idle_Loop) et 6,13 → 0 (A_TPose) dès la première manche, puis 0 stable. Ce zéro est attendu par construction (le score mesure ce que la visée règle). Contrôle caméra (positions, calque) : pire vue 20,5 % (rFoot) contre 16,5 % avant. Validation visuelle en attente | le score en positions butait sur les proportions (tronc 7,6 %, jambes 8 %) | `.\Tools\Restore.ps1 -Lot PoseMeasure3` | +| CHG-190 | 2026-10-06 | PoseMeasure | `LPPoseOrchestrator.cpp` : rotation locale = Parent⁻¹ * Monde (`LocalRot` l. 138, `ApplyDeltas` l. 166), au lieu de Monde * Parent⁻¹. Les `best_*.txt` calculés avec l'ancien ordre sont retirés (copies dans l'instantané). Deux passes (manches 675 à 696) : chaque « avant » égale l'« après » retenu au tour précédent et le score enregistré (Idle_Loop 6,8047, A_TPose 7,9827). Plateau dès la manche 680 : tronc 7,7 %, jambes 8,2 %, bras 2,5 % (Idle_Loop) | la boucle restait bloquée (passes identiques, kept=0) : le modèle relu donnait 11,66 % au lieu des 6,85 % enregistrés | `.\Tools\Restore.ps1 -Lot PoseMeasure2` | +| CHG-189 | 2026-10-05 | PoseMeasure | `LPPoseMeasure`, `LPPoseOrchestrator`, panneau Mesure : score pondéré (tronc 50 %, jambes 30 %, bras 20 %) et la liste de chaque os. Le tronc est verrouillé avant les membres. Un passage qui améliore un genou mais empire le tronc ne remplace pas le modèle. Les anciens `best_*.txt` sans `metric weighted3` ne sont plus rechargés | un seul os (le genou) donnait l'impression que la pose était corrigée | retirer `FLPPoseScore` et revenir au libellé du pire os | +| CHG-188 | 2026-10-05 | PoseMeasure | Orchestrateur : 8 échantillons sur la durée, synchro sur le calque du mannequin, archive `Saved/Measurement/history.csv` et `round_XXXX_*.csv`. Le calque jaune est le squelette du mannequin posé sur le bassin. Round 1 : Bridget et Michael restent à 60 % (os `pelvis`), avant = après | comparer les écarts d’un passage à l’autre, et sur toute la durée du clip | `.\Tools\Restore.ps1 -Lot PoseMeasure` | +| CHG-187 | 2026-10-05 | PoseMeasure | Chambre `measurement` dans la sandbox (menu Mesure, `LifePlay_measure_sandbox.bat`). Neuf caméras autour des trois corps, squelettes dessinés, écart de chaque vue en pourcentage de la taille. Pire vue mesurée : Droite 14,5 % (pied gauche de Michael) | une seule vue de face ne voyait pas le bras derrière le dos | `.\Tools\Restore.ps1 -Lot PoseMeasure` | +| CHG-186 | 2026-10-05 | AnimToGenesis | Pose `pole` : demi-tour des épaules seulement (`lShldrBend`, `rShldrBend`). L'avant-bras et la main gardent l'alignement de ~10°. Idle mesuré : Bridget 2,1 / 6,4 cm devant le buste, Michael −0,8 / 4,0 (mannequin −1,4 / 3,5). 43 clips `B_` et 43 `M_` | la main passait derrière le dos ; un demi-tour de l'avant-bras ne déplace pas le poignet | retirer le mode `pole` dans `Tools/RetargetQuaternius.py` | +| CHG-185 | 2026-10-05 | AnimToGenesis | `LPAnimRedirect` lit `Saved/Config/Windows/LPAnimTarget.ini`. `BuildMod.ps1 -Target sandbox -Open` ouvre la sandbox ; `-Target game -Open` ouvre la partie (carte par défaut, sans forcer la sandbox). `game` fait jouer `B_Walk_Loop` / `M_Walk_Loop` par la marche du jeu. `sandbox` laisse cette marche au Blueprint. La sandbox ne subit pas la marche du jeu | choisir la cible des clips convertis sans les déplacer | `.\Tools\Restore.ps1 -Lot AnimToGenesis` | +| CHG-184 | 2026-10-05 | AnimToGenesis | `BuildMod.ps1 -Retarget bake` lance `RetargetQuaternius.py` dans UnrealEditor avant la cuisson : les clips Quaternius sont recopiés sur Bridget et Michael. `-Retarget rigs` ne refait que les rigs. `-RetargetOnly` ne cuit pas. Sans `-Retarget`, un mod se cuit comme avant. Les copies effacées sont seulement `B_` / `M_` des clips source | la conversion vers les deux corps du jeu fait partie du build de mod | `.\Tools\Restore.ps1 -Lot AnimToGenesis` | +| CHG-183 | 2026-10-05 | Sandbox | Pose Genesis : épaule 43,5°, clavicule 2,5°, cuisse 6°, sens mesuré vers le T | l'alignement par le maillage mettait ~40° sur la clavicule et ~11° sur l'épaule | retirer `apply_daz_adjustments` | +| CHG-182 | 2026-10-05 | Sandbox | Cou et tête Genesis exclus de l'alignement par le maillage | le centre du crâne basculait la tête en arrière | réinclure la chaîne Neck dans `auto_align_bones` | +| CHG-181 | 2026-10-05 | Sandbox | Pose de retarget Genesis 8 alignée par le maillage (MeshToMesh), pas par la chaîne | ChainToChain ignore un os isolé, donc l'avant-bras et la main restaient en pose A | revenir à `auto_align_all_bones` | +| CHG-180 | 2026-10-05 | Sandbox | Recopie Genesis 8 : une chaîne par segment, jamais à travers un os Twist (col+épaule, avant-bras, main, cuisse, tibia, pied) | la chaîne clavicule→main faisait passer la rotation de l'avant-bras dans l'os de torsion | revenir aux chaînes LeftArm / LeftLeg de CHG-179 | +| CHG-179 | 2026-10-05 | Sandbox | Recopie Bridget/Michael : os de torsion ignorés, pose cible alignée sur le T du mannequin, rotation un pour un, pas de translation | les bras Daz se repliaient dans le bassin ; le mannequin, lui, était correct | relancer l'ancienne copie sans `auto_align_all_bones` | +| CHG-178 | 2026-10-05 | Sandbox | En Interaction, MC et PNJ jouent le même clip Quaternius. Bridget et Michael reçoivent la copie retargetée (`B_` / `M_`) | seul le mannequin jouait ces clips | retirer `Tools/RetargetQuaternius.py`, `Content/Quaternius/Retarget` et les clips `B_` / `M_` | +| CHG-177 | 2026-10-05 | Sandbox | Menu toujours visible : MC et PNJ choisissent Mannequin, Bridget ou Michael. Le mannequin joue les clips Quaternius ; Bridget et Michael gardent l'idle Daz ou la paire `_F` / `_M` | le mode Interaction forçait le mannequin sur le MC | retirer `SelectBody` et les listes MC / PNJ | +| CHG-176 | 2026-10-05 | Sandbox | Mode Interaction : le MC devient le mannequin Quaternius (`Content/Quaternius/UAL`, CC0, sans root motion). Le menu Marche liste ses 43 clips. Une touche de direction joue le clip choisi, l'arrêt revient à `Idle_Loop`. Pair garde Bridget | les cycles Daz ne pliaient que le pied | retirer `Content/Quaternius`, `LoadQuaternius` et `UseBody` | +| CHG-175 | 2026-10-05 | Sandbox | La touche reste collée à la tête du PNJ, du côté de la caméra, et reste dans l'image | la lettre E flottait au milieu de la pièce | remettre le décalage de 42 cm | +| CHG-174 | 2026-10-05 | Sandbox | À moins de 1,5 m, la touche `Interact` lue dans `Saved/Config/Windows/LPKeyboard.ini` s'affiche à côté du PNJ. Cette touche ouvre la liste | la lettre était fixe sur E | retirer la pastille et relire E | +| CHG-173 | 2026-10-05 | Sandbox | La marche ne joue qu'avec une touche de direction, le repos revient à l'arrêt. En vue Yeux, 5 s sans marche repasse en vue Interaction ; reprendre la marche rend la vue Yeux | le cycle tournait aussi à l'arrêt | retirer le minuteur et rejouer le cycle en continu | +| CHG-172 | 2026-10-05 | Sandbox | Le menu Interaction est un panneau opaque, sans texte moteur par-dessus. WHO1 et WHO2 ont une race (Human, Elf, Orc, Vampire) et des cases : temporaire, créature, mariage, relation, collègue, employé, parent, fréquentation, compagnon | les listes étaient couvertes, et le filtre WHO ignorait race et relation | retirer les cases et le panneau | +| CHG-171 | 2026-10-05 | Sandbox | Mode Interaction : menu Marche (`Repos`, `walk_F`, `walk_M`, `HighHeelsWalk`, `HighHeels`) joué sur Bridget. Les `.lpaction` de `vin_Base` sont filtrés par WHO1, WHO2, WHERE et WHEN. E, près du PNJ, affiche cette liste | essayer les cycles de marche et voir quelles actions restent pour Bridget et Michael | retirer les menus et `CollectActions` | +| CHG-170 | 2026-10-05 | Sandbox | Vue Dessus : bras 2 m, pitch −80°, champ 95°. L'ancienne distance de 8 m passait au-dessus du plafond, vers 4 m | la vue de dessus montrait l'extérieur de la chambre | remettre le bras à 8 m | +| CHG-169 | 2026-10-05 | Sandbox | La chambre `work___vin01` sert aux deux essais. Menu en haut à gauche : Pair (collision, animations) ou Interaction (marche, vues, E ne charge que `chat.lpaction`). Les deux .bat ouvrent cette chambre | la vue Interaction séparée restait vide | retirer le menu de mode et `BeginInteraction` | +| CHG-168 | 2026-10-05 | Sandbox | `LPInteractionSandbox` et `Tools/debug/LifePlay_interaction_sandbox.bat` : MC (Bridget, idle Daz) et un PNJ (Michael), sol, trois vues (1 Interaction, 2 Dessus, 3 Yeux, C enchaine). E à moins de 150 cm n'ouvre que `chat.lpaction`. Le cycle `walk_F` et la caméra suiveuse ne sont plus appliqués dans la partie : le clip ne pliait que le pied et la vue ne suivait pas | garder la partie intacte pendant l'essai des mouvements et des caméras | supprimer les deux fichiers, le .bat, et rappeler `WalkAnim` / `FollowCamera` | +| CHG-167 | 2026-10-05 | Proximité | Marche : `walk_F` ou `walk_M` selon `IsMale`, rythme selon la vitesse, l'animation d'origine revient à l'arrêt. Caméra troisième personne derrière le personnage (bras 3,4 m, pitch entre −55° et −12°) tant que la vue active est `Birdview` ; C rend la main au jeu | le MC glissait sans cycle, et la vue de dessus ne le suivait pas | retirer `FollowCamera`, `WalkAnim` et `RestoreAnim` | +| CHG-166 | 2026-10-05 | Proximité | La liste E reste cachée tant que E n'est pas pressé ; un second E ou un clic lance la ligne. Les boutons ne prennent pas le clavier. `PlayerCharacter` n'est pas possédé : `bRunPhysicsWithNoController` pour que `AddMovementInput` le déplace. Les touches digitales sont lues avec `IsPressed` | en ville la liste s'ouvrait seule et le MC ne bougeait pas : aucune ligne `Marche` dans le journal de 13:07, le mouvement sans contrôleur est ignoré | retirer `bPromptOpen` et `bRunPhysicsWithNoController` | +| CHG-165 | 2026-10-05 | Clavier | Les touches sont dans `Saved/Config/Windows/LPKeyboard.ini`. `Input.ini` est le cache généré du moteur : à la sortie il l'efface quand le texte à écrire est vide | partie 12:44 : 55 actions écrites dans `Input.ini`, fichier absent après la fermeture de 12:58 | revenir au chemin `Input.ini` de CHG-164 | +| CHG-164 | 2026-10-05 | Clavier | `LPKeyboardSubsystem` écrit et relit `Saved/Config/Windows/Input.ini` (`+ActionMappings` / `+AxisMappings`). Le fichier est créé au lancement. `SaveKeyMappings` ne l'écrit pas : `UInputSettings` est `defaultconfig` | le jeu packagé n'avait pas de `Input.ini`, donc les touches n'étaient pas restaurées | retirer `LoadSaved` / `SaveSaved` et revenir à `SaveKeyMappings` | +| CHG-163 | 2026-10-05 | Réglages | Curseurs couleur et page Keyboard : même police à contour que les réglages existants, curseur copié sur `SettingSlider`. Couleur relue et écrite dans `Saved/Config/Windows/LPColorGrade.ini`. Les touches restent dans `Saved/Config/Windows/Input.ini` | les derniers réglages et le clavier n'avaient pas le style du menu, et la relecture devait être explicite | retirer le style partagé et revenir à `GConfig` pour la couleur | +| CHG-162 | 2026-10-05 | Réglages | Page Keyboard : sections Movement, Camera, Interaction, Menus, Favorites, Time, World, Save, Scene, Other. Le texte d'aide passe à la ligne | la liste alphabétique mélangeait favoris, déplacement et menus | retirer `GroupFor` et le tri par section dans `LPKeyboardSubsystem.cppzzzzzzzzzzzzzzzzzzzzzzzzzzz` | +| CHG-161 | 2026-10-05 | Réglages | `LPColorGradeSubsystem` : dans Video, Brightness (compensation d'exposition), Saturation, Contrast, White Balance et Tint. Volume de post-traitement non borné, valeurs neutres du moteur au départ. Fichier `Saved/Config/Windows/LPColorGrade.ini` | l'image trop blanche se règle par l'exposition et le Color Grading d'Unreal, pas par un nouveau pipeline couleur | retirer `LPColorGradeSubsystem.h/.cpp` et le cas `LPColor` dans `CategoryOf` | +| CHG-160 | 2026-10-05 | Réglages | Onglets à la largeur du nom, ordre General, Audio, Video, Keyboard, Lewd, Misc. Page clavier en anglais. `Volume - Sex` affiché `Volume - Dialogue`, `Volume - Sex Moans` affiché `Volume - SFX` | les onglets se chevauchaient, le clavier était en français, et les deux volumes gardaient l'ancien libellé | retirer le renommage et les libellés anglais dans `LPSettingsSubsystem.cpp` et `LPKeyboardSubsystem.cpp` | +| CHG-159 | 2026-10-05 | Réglages | `LPSettingsSubsystem` : entrée `LPSettingsTitleButton` sur l'écran titre, juste après `ModManagerTitleButton` (donc avant Donate). Le `SettingsButton` d'origine reste masqué par `ColapsedForTitleScreen` ; le clic de la nouvelle entrée rejoue son `OnClicked`. La liste est reconstruite : `InsertChildAt` laissait le bouton en dernier dans la boîte affichée, sous Exit, et le contenu centré le décalait vers la droite | Settings absent du menu principal, puis affiché sous Exit | retirer l'entrée titre dans `PlaceSettingsButton` | +| CHG-157 | 2026-10-05 | Proximité | `LPDirectControlSubsystem` : marche ZQSD et WASD sur `MoveForward` / `MoveRight` (les flèches gardent la vue de dessus), journal seulement sur J, action `Interact` sur E. Index des `.lpaction` dont `WHERE` contient `interaction`, et des `.lpnear` (`MESH` / `PROMPT` / `ACTION`). L'invite Slate suit la personne ou le meuble le plus proche (150 cm) ; E appelle `TakeModActionWithBuilding` avec l'`ActionDetail` déjà chargé. `Content/Modules/vin_Base/Proximity/furniture.lpnear` | déplacer le personnage et lancer une action de module sans modifier les Blueprints | retirer `LPDirectControlSubsystem.h/.cpp`, `furniture.lpnear`, et les lignes `MoveForward`, `MoveRight`, `Interact` de `DefaultInput.ini` ; remettre Journal sur Q et WASD sur `LeftRight` / `DownUp` | -Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (soleil fixe) **en 4.27, en L7i_B12 et en L7cdj** : comportement d'origine. Le grand disque blanc à l'horizon est présent **en L7i_B12 comme en L7cdj** : il ne vient pas de L7d (déjà présent en V1, origine non étudiée). Un retour arrière de L7d, lancé à tort avant cette comparaison, a été annulé avant tout commit ou build (`Tools\Restore.ps1 -Lot L7d-retour`, empreinte de la carte revenue à `8BD9C4A9…`). Tags `L7c-valide`, `L7d-valide`. -| CHG | Date | Lot | Fichiers | Raison | Retour arrière | -|---|---|---|---|---|---| -| CHG-058 | 2026-10-02 | Doc | nouveaux `07_Architecture_5.5.md`, `08_Guide_Developpeur.md`, `09_Commandes_Utiles.md`, `10_Protocole_Operations.md` ; mis à jour `README.md` (migration et racine), `01`, `03`, `04` (bilan §5), `05` (CX-C14 à C18, statuts), `06` (résultat V1), registre (ERR-020) | finalisation de la documentation de migration : architecture finale, guide développeur, commandes, protocole et leçons | `git revert` du commit | -| CHG-059 | 2026-10-02 | O0 | nouveaux `Tools/AuditAssets.py`, `Docs/Migration/11_Audit_Optimisation.md` ; lanceur `LifePlay_profil_O0.bat` (build L7cdj, hors dépôt) | audit d'optimisation en lecture seule : inventaire des assets, build livré, leviers et lots proposés ; aucune modification du projet | `git revert` du commit | -| CHG-060 | 2026-10-02 | O0 | `Docs/Migration/11_Audit_Optimisation.md` §9-10 ; lanceur `LifePlay_profil_O0.bat` corrigé (capture par `-ExecCmds`) | résultats de la mesure en jeu (60 images/s tenues, à-coups du thread de jeu dus aux minuteries Blueprint, 463 Mo de physique) ; produits utiles de la bibliothèque Fab ; lots révisés | `git revert` du commit | -| CHG-061 | 2026-10-02 | O1 | `LifePlay.uproject` (20 plugins limités à l'éditeur), `Config/DefaultGame.ini` (16 `DirectoriesToNeverCook`), nouveau `Tools/AuditScriptDeps.py` ; `README.md` : lien Mega de la V1.1 | build Shipping allégé de 387 Mo sans changement de contenu ; build `LifePlay_Build_5.5_O1` | `Tools\Restore.ps1 -Lot O1` ou `git checkout O1-avant -- LifePlay.uproject Config` | -| CHG-062 | 2026-10-02 | O9 | nouvelle commandlet `LPSetComponentCollision` ; `Content/BPs/MapMarkers.uasset` : composants `Tree` et `Car` en `NoCollision` (diff avant/après : 2 lignes) | chargement de partie de 71 s (identique en 4.27) dû aux ≈ 160 000 corps physiques des arbres et voitures décoratifs de la carte (décision utilisateur : option 1) ; build `LifePlay_Build_5.5_O9` | `Tools\Restore.ps1 -Lot O9` | -| CHG-063 | 2026-10-02 | O9b | `LPSetComponentCollision` (modèles des nœuds « Add Component ») ; `Content/BPs/MapMarkers.uasset` : modèles `NODE_AddHISM-0` et `-1` (`HISM_C`, marqueurs de bâtiments) en `NoCollision` (diff : 2 lignes) | les clics se font sur les icônes, pas sur les bâtiments (utilisateur) ; ≈ 49 500 corps physiques restants au chargement ; build `LifePlay_Build_5.5_O9b` | `Tools\Restore.ps1 -Lot O9b` | -| CHG-064 | 2026-10-02 | O2 | `Config/DefaultGame.ini` : `UsePakFile=True`, `bUseIoStore=True`, `bCompressed=True` (réglages par défaut du moteur : Oodle Kraken) ; `Modules` reste en fichiers nus | build Development 3 367 → 2 002 Mo (−40 %), 23 928 → 2 104 fichiers ; build `LifePlay_Build_5.5_O2` ; validé en jeu le 2026-10-02 (7 contrôles) | `Tools\Restore.ps1 -Lot O2` ou `git checkout O2-avant -- Config` | -| CHG-065 | 2026-10-02 | O4 | nouveau `Tools/FixTextureMips.py` ; 29 textures (`Clothes4` 27, route, trottoir) : `MipGenSettings` = groupe de texture, `NeverStream` = faux (diff : ces 2 propriétés + `LightingGuid` régénéré) | textures de couleur 3D sans mipmaps : scintillement à distance, pleine résolution toujours en mémoire ; exclus : cheveux et masques de transparence (29), interface et textures sans matériau (44), dimensions non puissance de 2 (733) ; build `LifePlay_Build_5.5_O4` | `Tools\Restore.ps1 -Lot O4` | -| CHG-066 | 2026-10-02 | O | build Shipping de distribution `LifePlay_Build_5.5_Shipping_V1.2` (O1, O2, O4, O9, O9b) | 3 246 → 1 494 Mo (−54 %), 24 249 → 2 084 fichiers ; premier essai sans ville (corrigé par CHG-067), reconstruit et validé en jeu le 2026-10-02 | — (build seulement) | -| CHG-067 | 2026-10-02 | O2b | `Plugins/ModdingExtra` : `GetAllFilesFromFolder` reconstruit la liste des `.uasset`/`.umap` depuis le registre d'assets quand le disque n'en renvoie aucun (contenu compressé IoStore) ; dépendance `AssetRegistry` | régression d'O2 constatée sur le build Shipping : aucune ville proposée (`UI/CityMenu` liste les fichiers `Heavy/StreetMap/*.uasset` du disque, `UI/City` vérifie chaque ville par suffixe `/.uasset`) ; seul appel concerné, les autres lisent des fichiers texte de `Modules` ; build `LifePlay_Build_5.5_Shipping_V1.2` reconstruit | `git checkout O2b-avant -- Plugins/ModdingExtra` | -| CHG-068 | 2026-10-02 | V1.2 | `README.md` (section V1.2, branche `ue5.5-v1.2`) ; tag annoté `v1.2` ; archive `D:\Game\LifePlay_Publication\LifePlay_Revival_V1.2_UE5.5_Full_Source_Code.7z` (4 937 914 101 octets, 25 681 fichiers, SHA-256 `fb298583743497799dd274cc62b8cf80fbef3445766be5898201924c64674f1a`, script `make_archive_v1_2.cmd`) | publication de la V1.2 (optimisation) ; contrôles : test 7-Zip, aucun `.mcp.json`/`.claude`/`McpAutomationBridge`/`Saved`/`Intermediate`, `Config`, `.uproject` et `ModdingExtra` identiques au tag | lien Mega à ajouter après envoi | -| CHG-069 | 2026-10-02 | Réconciliation | fusion `-s ours` de `vinfamy/master` dans `main` (commit `82a64d9`, arbre inchangé) ; `Docs/Migration/12_Reconciliation_Vinfamy.md` | historique de l'auteur rattaché ; aucun fichier récupérable ; les 33 scènes `corruption` vides de `vin_Base` existent dans `vin_Corruption` (non restaurées : doublons) | `git reset --hard reconcil-avant` (avant toute autre publication) | -| CHG-070 | 2026-10-02 | ERR-038 | nouveau `Source/LifePlay/LPRoomTransitionSubsystem.h/.cpp` | relevé (`ERR038_releve_20261002.log`) : la nouvelle pièce est construite ≈ 2 s avant que `destroyDummy` rende les personnages de l'ancienne à la réserve ; le sous-système cache aussitôt, au changement de pièce, les personnages actifs juste avant (hors compagnon et scène en cours), cesse de les suivre dès que le jeu les rend ou les réaffiche, et les réaffiche après 10 s par sécurité ; aucun Blueprint modifié ; build `LifePlay_Build_5.5_ERR038` ; test en jeu en attente | supprimer les 2 fichiers (`git checkout ERR038-avant -- Source`) | -| CHG-071 | 2026-10-02 | Ménage | `README.md` : V1.2 en téléchargement principal (lien Mega), V1.1 et V1 en versions précédentes ; suppression de 13 builds 5.5 obsolètes (V1rc, L7a, L7b, L7i, L7i_B12, L7cdj, O1, O1_essai1, O2, O4, O9, O9b, Shipping_V1.2_essai1, ≈ 43 Go) après copie vérifiée de `Saved`, `Content\Saves` et des lanceurs dans `D:\Game\LifePlay_Backup_5.5\builds_supprimees\` ; `08_Guide_Developpeur.md` §4 | lien V1.2 reçu ; place disque | builds : reconstruction depuis les tags git | -| CHG-072 | 2026-10-02 | ERR-038 | `LPRoomTransitionSubsystem` : détection par la seule échelle de la racine de `Room`, délai de réaffichage 10 s → 3 s | premier test en jeu concluant (anciens personnages cachés, compagnon présent, nouveaux personnages apparus), mais 2 faux positifs au relevé : nom de lieu vidé un instant sans changement de pièce ; arrivée au restaurant peuplée 3 s avant la construction de la pièce (personnages apparus 10 s en retard) ; build `LifePlay_Build_5.5_ERR038` reconstruit | `git checkout ERR038-apres -- Source/LifePlay/LPRoomTransitionSubsystem.*` | -| CHG-073 | 2026-10-02 | ERR-038 | `Source/LifePlay/LPRoomTransitionSubsystem.h/.cpp` supprimés (copie dans `D:\Game\LifePlay_Backup_5.5\ERR038_retrait`) ; build `LifePlay_Build_5.5_ERR038` reconstruit sans le sous-système (vérifié dans l'exécutable) | second test « plus problématique » : relevé `ERR038_apres2_20261002` = 2 déclenchements, tous deux à tort (clients du lieu d'arrivée cachés 3 s) ; le critère temporel ne distingue pas les personnages créés pendant le trajet puis retirés de ceux qui sont conservés ; retrait proposé et accepté par l'utilisateur ; ERR-038 reste ouvert | `git checkout ERR038-apres -- Source/LifePlay` | -| CHG-074 | 2026-10-02 | O7 | O7a : `Content/Saves` (14 125 fichiers, parties de test) → `D:\Game\LifePlay_Backup_5.5\O7\Content_Saves` (vérifié), `Content/Developers` et `Content/Collections` (dossiers vides) retirés, 3 captures de la racine rangées ; O7b : `08_Guide_Developpeur.md` §2.1 (carte de `Content/Heavy`, dossiers à ne pas déplacer) | ménage sans risque et documentation, à la place d'un renommage des dossiers jugé trop risqué (préfixes de chemins en dur, contrat de modding) | déplacer en retour le contenu de `O7\Content_Saves` | -| CHG-075 | 2026-10-02 | DATA1 | `Plugins/ModdingExtra/.../ModdingExtraBPLibrary.cpp` : statistiques de `LoadTxt` par type de fichier (bilan à la fermeture, commande `ModdingExtra.LoadTxtStats`) ; `11_Audit_Optimisation.md` §17 (stratégie de couche de données) | étape 1 de la couche de données : mesurer avant de construire ; `PopulateRaws` = 12,6 s par chargement de partie (trace O2) ; build `LifePlay_Build_5.5_DATA1` | `git checkout DATA1-avant -- Plugins/ModdingExtra` | -| CHG-076 | 2026-10-02 | DATA23 | nouveaux `Plugins/ModdingExtra/.../LPDataCache.h/.cpp` ; `ModdingExtra.cpp` (démarrage en arrière-plan), `ModdingExtraBPLibrary.cpp` (`LoadTxt` depuis la mémoire, `SaveTxt`, vérification, statistiques cache/disque), `ModdingExtra.Build.cs` (`Json`) | couche de données étapes 2 et 3 : `Modules` chargé une fois en arrière-plan, cache compilé JSON par type avec champs et calque ; 2 031 fichiers en 0,14 s depuis le cache ; build `LifePlay_Build_5.5_DATA23` ; validation en jeu en attente | `git checkout DATA23-avant -- Plugins/ModdingExtra` ; ou option `-LPNoDataCache` | -| CHG-077 | 2026-10-02 | O1b / DATA23 | `Config/DefaultGame.ini` : `+DirectoriesToNeverCook=(Path="/HairStrands")` retiré ; validation de la couche de données (CHG-076) : 2 658 textes servis par le cache, 0 écart, lecture 0,074 s pour la session | ERR-039 : erreurs `GroomComponent` au démarrage depuis O1 (module conservé par dépendance, contenu non cuit) ; vérifié : 0 erreur après reconstruction | remettre la ligne dans `DefaultGame.ini` | -| CHG-078 | 2026-10-02 | DATA4b | nouveaux `Source/LifePlay/LPDataCompat.h/.cpp` (`WhatWhereToArray` en C++, vérification `-LPVerifyWhatWhere` contre la fonction Blueprint d'origine) et `Source/LifePlayEditorTools/LPFixWhatWhereCommandlet.h/.cpp` ; `Content/BPs/GI.uasset` : 2 appels de l'EventGraph rebranchés (diff : 22 lignes, uniquement les 2 nœuds et les liaisons de leurs voisins) | `WhatWhereToArray` = 94 % de `PopulateScenes` (3,4 s par passage) ; premier essai refusé par le contrôle intégré (broche `Target` non exclue), rien sauvegardé, commandlet corrigée ; build `LifePlay_Build_5.5_DATA4b` ; validé en jeu le 2026-10-02 : plus de 3 000 appels vérifiés, 0 écart ; `PopulateRaws` 12,6 s → 1,26 s | `Tools\Restore.ps1 -Lot DATA4b` | -| CHG-079 | 2026-10-02 | V1.3 | `README.md` (section V1.3, branche `ue5.5-v1.3`) ; tag annoté `v1.3` ; build `LifePlay_Build_5.5_Shipping_V1.3` validé en jeu ; archive `LifePlay_Revival_V1.3_UE5.5_Full_Source_Code.7z` (4 945 770 158 octets, 11 583 fichiers, SHA-256 `24e7d58f759ca8998fe0d7ddc8c6a4c30262978806903e6d3deb68b37c46512a`, contrôles : test 7-Zip, aucun secret ni outil local, 14 fichiers identiques au tag) | publication de la V1.3 (couche de données, `WhatWhereToArray` en C++, ERR-039) | — | -| CHG-080 | 2026-10-02 | V1.3 | `README.md` : V1.3 en téléchargement principal (lien Mega), V1.2 et V1.1 en versions précédentes ; lien de la V1 retiré (archive supprimée de Mega faute d'espace, état git conservé par le tag `v1.0`) | lien V1.3 reçu | — | -| CHG-081 | 2026-10-02 | O7d | nouveau `Tools/BuildCollections.py` ; 13 collections statiques `Content/Collections/LifePlay*.collection` (hors dépôt, régénérables) ; `11_Audit_Optimisation.md` §16.5, `08_Guide_Developpeur.md` §2.1, `09_Commandes_Utiles.md` §3 | suite d'O7 (choix utilisateur) : regrouper les 8 561 assets par thème dans l'éditeur sans renommer de dossier ; collection transversale `LifePlay_Chemins_Fixes` (4 984 assets à ne pas déplacer) ; 13 545 chemins, 0 inconnu, chargées par l'éditeur ; aucun asset modifié, pas de build | supprimer `Content/Collections/LifePlay*.collection` (`git checkout O7d-avant` pour le reste) | -| CHG-082 | 2026-10-02 | O7d | `Tools/BuildCollections.py` : collections écrites dans `Saved/Collections` (locales) au lieu de `Content/Collections` (partagées) ; 13 fichiers déplacés vers `D:\Game\LifePlay_Backup_5.5\O7d\Content_Collections` puis régénérés (identiques) ; `11_Audit_Optimisation.md` §16.5, `08_Guide_Developpeur.md` §2.1 | avertissement à l'ouverture de l'éditeur (utilisateur) : une collection partagée exige un fournisseur de contrôle de révision, désactivé dans ce projet ; une collection locale n'en utilise pas ; validé par l'utilisateur le 2026-10-02 (plus d'avertissement) | `git checkout O7d-apres -- Tools/BuildCollections.py` et remettre les fichiers sauvegardés | -| CHG-083 | 2026-10-02 | O7e | `11_Audit_Optimisation.md` §16.6 | étude en lecture seule de l'intégration d'une coiffure Fab (demande utilisateur) : liste des coiffures = valeur par défaut `GI.HairstyleTable` (126 entrées, 35 absentes déjà en 4.27), suivi par noms d'os Genesis 8, morphs de forme facultatifs, sauvegardes par nom ; 34 coiffures Fab, ≈ 12 maillages utilisables | — (documentation) | -| CHG-084 | 2026-10-02 | H1 | nouveaux `Source/LifePlay/LPHairstyleSubsystem.h/.cpp`, module `Content/Modules/lp_Revival` (`lp_Revival.lpmod`, `Hairstyles/h1_testhair.lphair`) ; asset de test `Content/Heavy/New5/Hairs/H1_TestHair` (copie de `FloraHair_New`, hors dépôt) ; `11_Audit_Optimisation.md` §16.7 | liste des coiffures ouverte aux mods (demande utilisateur) : fichiers `.lphair` ajoutés en fin de `GI.HairstyleTable` sans modifier de Blueprint ; vérifié au journal : doublons refusés, 1 coiffure ajoutée (table 126 → 127) ; build `LifePlay_Build_5.5_H1` ; validé en jeu le 2026-10-02 (nouvelle partie et liste des villes, coiffure ajoutée en fin de liste, suivi des animations, module désactivé puis réactivé, Continue) | supprimer le module `lp_Revival` (ou le désactiver) ; `git checkout H1-avant -- Source` | -| CHG-085 | 2026-10-02 | O7e | nouvelle commandlet `Source/LifePlayEditorTools/LPHairFromStaticMeshCommandlet.h/.cpp` (+ `SkeletalMeshUtilitiesCommon`, `StaticMeshDescription` dans `LifePlayEditorTools.Build.cs`) ; assets `Content/Heavy/New5/Hairs/Pocolov17*` (hors dépôt) ; `Content/Modules/lp_Revival/Hairstyles/pocolov17.lphair` (remplace `h1_testhair.lphair`, `H1_TestHair` retiré) ; `Licenses/Free_3D_Models_Credits.txt` ; `11_Audit_Optimisation.md` §16.8 | première coiffure Fab (Pocolov Hair 17, CC BY 4.0, téléchargée avec l'accord de l'utilisateur) : maillage statique rattaché au squelette Genesis 8, ajusté à la tête de Bridget, matériaux `DazHair` ; build `LifePlay_Build_5.5_O7e` : coiffure ajoutée (index 126) ; test en jeu en attente | retirer `pocolov17.lphair` ; sauvegardes dans `D:\Game\LifePlay_Backup_5.5\O7e` ; `git checkout O7e-avant -- Source Content/Modules Licenses` | -| CHG-086 | 2026-10-02 | O7e | `LPHairFromStaticMeshCommandlet` : ajustement sur le haut du crâne (`-Cranium`, `-Margin`) ; `Pocolov17` et `_Skeleton` régénérés (premier essai dans `O7e\essai1`), `Pocolov17_Strands` : `Thicken` 10 → 1 ; `11_Audit_Optimisation.md` §16.8 | premier essai en jeu (capture utilisateur) : coiffure trop étroite enfoncée dans le crâne, mèches opaques ; échelle 0,74 → 1,19 en largeur ; build `LifePlay_Build_5.5_O7e` reconstruit ; test en jeu en attente | `Tools`/assets : remettre `O7e\essai1` ; `git checkout O7e-apres -- Source/LifePlayEditorTools` | -| CHG-087 | 2026-10-02 | O7e | `Pocolov17_Cap` (hors dépôt) : `MapOpacity` = texture blanche du moteur ; `11_Audit_Optimisation.md` §16.8 | second essai en jeu correct (crâne couvert, couleur appliquée, profil bien placé) ; trous au-dessus de l'oreille dus à la lisière découpée de la calotte ; retouche choisie par l'utilisateur ; build `LifePlay_Build_5.5_O7e` reconstruit | remettre `Pocolov17_CapOpacity` dans `MapOpacity` | -| CHG-088 | 2026-10-02 | O7e | — | O7e validé en jeu par l'utilisateur (profil sans trou, mouvement suivi) ; défaut mineur accepté : bas de la calotte visible sous le lobe de l'oreille | — | -| CHG-089 | 2026-10-02 | H2 | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (mode base, plusieurs coiffures par fichier, `NONE`, `MORPHABLE`, vérification `-LPVerifyHairstyles`) ; nouveau `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (126 coiffures, 89 morphs) ; `11_Audit_Optimisation.md` §16.9 | liste de base des coiffures sortie du Blueprint (demande utilisateur) ; vérifié : table et morphs reconstruits identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; validé en jeu le 2026-10-02 (nouvelle partie, liste du menu inchangée avec Pocolov17 en dernier, personnages générés, Continue) | supprimer `vin_Base.lphair` (retour à la table du Blueprint) ; `git checkout H2-avant -- Source/LifePlay` | -| CHG-090 | 2026-10-02 | H2b | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (clés `FACIAL_HAIR`, `BODY_HAIR_M`, `BODY_HAIR_F`) ; `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (+ 44 noms) ; `11_Audit_Optimisation.md` §16.10 | pilosité sortie du Blueprint (demande utilisateur) : listes `GI.FHairstyles`, `BHairstyles_M`, `BHairstyles` ; `GI.OtherTable` laissée (maillages absents depuis 4.27) ; vérifié : 3 listes identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; validé en jeu le 2026-10-02 (28 barbes + sans barbe dans le menu de création) | retirer les lignes `*_HAIR` de `vin_Base.lphair` ; `git checkout H2b-avant -- Source/LifePlay` | -| CHG-091 | 2026-10-02 | Doc | nouveaux `Docs/Migration/13_Protocole_Coiffure.md`, `Tools/HairImportFbx.py`, `Tools/HairMaterials.py` ; `README.md` (migration), `08_Guide_Developpeur.md`, `09_Commandes_Utiles.md` | protocole de conversion d'une coiffure (demande utilisateur) ; scripts ponctuels d'O7e rendus paramétrables ; protocole rejoué de bout en bout sur Pocolov 17 sous le nom `Pocolov17P` : bornes identiques au millimètre, dépendances OK ; essai retiré du projet (`D:\Game\LifePlay_Backup_5.5\O7e\proto_essai`) | `git revert` du commit | -| CHG-092 | 2026-10-02 | HAIRS1 | nouveaux `Tools/HairPipeline.ps1`, `Tools/FbxMaterialMap.py`, `Tools/hairs/HAIRS1.json` ; `Tools/HairImportFbx.py` (rapport JSON), `Tools/HairMaterials.py` (calotte seulement si utilisée, contrôle des dépendances) ; assets `Content/Heavy/New5/Hairs/Pocolov03/06/11/15/16*` (hors dépôt) ; `Content/Modules/lp_Revival/Hairstyles/pocolov03/06/11/15/16.lphair` ; `Licenses/Free_3D_Models_Credits.txt` ; `13_Protocole_Coiffure.md` §7 | industrialisation de la conversion (demande utilisateur) et test sur 5 coiffures Pocolov (CC BY 4.0, téléchargées avec l'accord de l'utilisateur) ; 5 cas d'archives différents intégrés aux règles ; build `LifePlay_Build_5.5_HAIRS1` : 6 coiffures ajoutées (index 126 à 131) ; contrôle visuel en jeu en attente | retirer les `.lphair` ; assets et sources dans `D:\Game\LifePlay_Backup_5.5\HAIRS1` ; `git checkout HAIRS1-avant -- Tools Licenses Content/Modules` | -| CHG-093 | 2026-10-02 | SKIN | `LPHairFromStaticMeshCommandlet` : liaison par transfert de poids depuis le corps (`-Skin=transfer` par défaut, 6 voisins, bras et jambes exclus) et sortie de la peau (`-Gap` 0,3 cm) ; `LifePlayEditorTools.Build.cs` (`SkeletalMeshDescription`, `AnimationCore`) ; `HairPipeline.ps1` (`-Rebuild`, options `skin`, `gap`) ; 6 coiffures Pocolov reconstruites (versions précédentes dans `HAIRS1\\precedente_*`), Pocolov 17 ajoutée au lot ; `.lphair` et crédits : description des modifications corrigée ; `13_Protocole_Coiffure.md` | vidéo utilisateur : mèches longues liées à la seule tête, qui traversent épaules et poitrine quand la tête tourne ; poids obtenus : tête 47 à 62 %, cou, clavicules, poitrine ; 254 à 950 sommets sortis de la peau par coiffure ; build `LifePlay_Build_5.5_SKIN` ; validé en jeu le 2026-10-02 (les mèches ne traversent plus la peau quand la tête tourne) | `-Skin=rigid` ; `git checkout SKIN-avant -- Source Tools` et remettre les versions précédentes | -| CHG-094 | 2026-10-02 | UI14 | nouveaux `Source/LifePlay/LPVersionSubsystem.h/.cpp`, `Source/LifePlayEditorTools/LPEditWidgetCommandlet.h/.cpp` (+ `Json` dans le `.Build.cs`), `Tools/ui/MainMenu_V1.4.json`, `Docs/Migration/14_Interface_V1.4.md` ; `Content/UI/MainMenu.uasset` (6 modifications, diff conforme) ; `Content/Modules/donors.txt` (→ `Vinfamy`) ; `Config/DefaultGame.ini` (`ProjectVersion=2026.10.4`) | demande utilisateur avant la V1.4 : version année.mois.publication, crédits vidés avec Vinfamy auteur original, Donate → Patreon edujiro5972, Discord → discord.gg/q4689pNSy, Commission masqué ; build `LifePlay_Build_5.5_UI14` : « LifePlay 2026.10.4 » ; validé en jeu le 2026-10-02 (version, crédits, bouton Commission masqué, liens Donate et Discord) | `Tools\Restore.ps1 -Lot UI14` ; `git checkout UI14-avant -- Config Source Content/Modules/donors.txt` | -| CHG-095 | 2026-10-02 | MODS14 | nouveaux `Tools/SplitModules.ps1`, `Tools/modules/V1.4.json`, `Docs/Migration/15_Modules_Optionnels.md` ; build `LifePlay_Build_5.5_Shipping_V1.4` (dossier neuf, contrôlé : pas de fichier périmé) ; archive `D:\Game\LifePlay_Publication\LifePlay_Revival_V1.4_Optional_Modules.zip` (875 812 octets, 626 fichiers, SHA-256 `cd8b4ffa9f676d3703fce9d2b42a0f61682c6c04bda30d033630c8f1be8fd236`) | demande utilisateur : build Shipping réduite à l'essentiel (`vin_Base`, `lp_Revival`, `vin_VanillaRealistic`), 14 modules optionnels à part ; vérifié dans les Blueprints : aucun fichier de module optionnel lu en dur, `GI.removeDeletedMods` retire les modules absents ; build allégé démarré sans plantage ; validé en jeu le 2026-10-02 (build sans les modules optionnels, puis avec l'archive extraite) ; modules de test retirés du build vers `LifePlay_Build_5.5_Shipping_V1.4_test_extraction` | remettre les dossiers de `LifePlay_Build_5.5_Shipping_V1.4_modules_optionnels` dans le build | -| CHG-096 | 2026-10-02 | V1.4 | `README.md` (section V1.4, branche `ue5.5-v1.4`) ; `Donate.url` (racine) → Patreon edujiro5972, comme le bouton Donate ; tag annoté `v1.4` | publication de la V1.4 (LifePlay 2026.10.4) : coiffures en fichiers `.lphair`, 6 coiffures Pocolov, chaîne d'import, menu principal, modules optionnels séparés | — | -| CHG-097 | 2026-10-02 | V1.4 | archives de publication (`D:\Game\LifePlay_Publication`) : sources `LifePlay_Revival_V1.4_UE5.5_Full_Source_Code.7z` (4 942 392 569 octets, 12 766 entrées, SHA-256 `bda44e52e8f45775e09fdea51b613bb64fc5e0c7fd0ec76108804e0bb39b3426`, script `make_archive_v1_4.cmd`) ; jeu `LifePlay_Revival_V1.4_Windows_Game.7z` (1 301 813 923 octets, racine `LifePlay_Revival_V1.4`, SHA-256 `d4a36542550ae2bcc7f3f634255125fc6c7520f5cf29df4b74ae3bbc0ac55a9f`, script `make_game_v1_4.cmd`) ; modules optionnels `LifePlay_Revival_V1.4_Optional_Modules.zip` (875 812 octets, SHA-256 `cd8b4ffa9f676d3703fce9d2b42a0f61682c6c04bda30d033630c8f1be8fd236`) | contrôles : tests 7-Zip ; sources : aucun `.mcp.json`/`.claude`/`McpAutomationBridge`/`Saved`/`Intermediate`/jeton, 17 modules, 10 fichiers identiques au tag `v1.4`, `.git` = `a83ea56` ; jeu : 3 modules (`vin_Base`, `lp_Revival`, `vin_VanillaRealistic`), ni `Saved` ni `.pdb` | liens Mega à ajouter après envoi | -| CHG-098 | 2026-10-02 | V1.4 | `README.md` : archive des sources V1.4 en téléchargement principal (lien Mega), V1.3 ajoutée aux versions précédentes | lien Mega de l'archive développeur reçu ; liens du jeu et des modules optionnels à venir | — | -| CHG-099 | 2026-10-02 | V1.4 | `README.md` : section V1.4, point des modules optionnels reformulé (plus de nom d'archive ni de mode d'installation) | décision utilisateur : les archives du jeu et des modules optionnels ne sont pas annoncées sur GitHub (la documentation technique garde les détails de l'outil) | `git revert` du commit | -| CHG-100 | 2026-10-03 | H3 | nouveau `Docs/Migration/16_H3_Mods_Paquets.md` | préparation de H3 (demande utilisateur) : étude en lecture seule du moteur 5.5 (dossier `Mods/` lu par le gestionnaire de plugins en jeu empaqueté, plugins Mod activés par défaut, paquets de plugins montés au démarrage, registre `AssetRegistry.bin` des plugins chargé) et de la configuration (paquets non signés, shaders intégrés aux matériaux, IoStore) ; architecture, risques, lots H3a à H3c | — (documentation) | -| CHG-101 | 2026-10-03 | H3a | `Releases/2026.10.4` (non versionné, 1,3 Go) ; `ModSources/LPHairSample/` (`LPHairSample.uplugin`, `Module/LPHairSample.lpmod`, `Module/Hairstyles/pocolov05.lphair`, `Module/CREDITS.txt` ; assets `Content/Hairs/Pocolov05*` non versionnés) ; `Tools/mods/LPHairSample.json` ; `Tools/HairPipeline.ps1` (chemins de plugin, `credits`) ; `Tools/HairMaterials.py` (contrôle des dépendances par chemin de paquet) ; `.gitignore` (`/Releases/`, `/Mods/`, produits et assets de `ModSources`) ; builds `LifePlay_Build_5.5_Ref_2026.10.4`, `LifePlay_Build_5.5_H3a` | prototype H3a : version de référence (paquets identiques au V1.4 publié), mod d'essai Pocolov 05 (Fab, CC BY 4.0, téléchargement accordé) cuit en DLC contre 2026.10.4 (6 paquets, 0 erreur) et installé dans une copie du V1.4 ; version `2026` mal nommée (argument coupé par PowerShell) déplacée vers `LifePlay_Backup_5.5\H3a` ; **validé en jeu le 2026-10-03** (16_H3_Mods_Paquets.md §5) | supprimer `ModSources/LPHairSample`, `Tools/mods`, `Releases/2026.10.4` ; `git revert` du commit | -| CHG-102 | 2026-10-03 | H3a | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` ; `Content/Modules/lp_Revival/lp_Revival.lpmod` (`MODULE_DEFAULT_ENABLED: true`) | retour utilisateur : coiffure du mod présente alors que le mod est décoché. Le sous-système (H1) tenait pour actif un module absent de `GI.ModEnableds`, alors que le jeu le tient pour inactif (défaut présent en V1.4 : `lp_Revival` affiché décoché mais actif). Même règle que le jeu ; un module à clé `MODULE_DEFAULT_ENABLED: true` absent de la liste y est ajouté coché (au démarrage et après `LoadSettings`). Validé en jeu le 2026-10-03 | `git revert` du commit, recompiler | -| CHG-103 | 2026-10-03 | UI15 | `Content/UI/MainMenu.uasset`, `Content/UI/ModManager.uasset` (snapshot `LifePlay_Backup_5.5\UI15`) ; nouveaux `Source/LifePlayEditorTools/LPTitleModManagerCommandlet.h/.cpp`, `Source/LifePlay/LPModSettingsLibrary.h/.cpp`, `Docs/Migration/17_Mods_Ecran_Titre.md` ; build `LifePlay_Build_5.5_UI15b` | demande utilisateur : gestionnaire de mods dès l'écran titre, puis choix entre nouvelle partie et partie sauvegardée. Entrée dédiée sur l'écran titre (copie de « Load »), fermeture vers le menu titre avec relecture des données et enregistrement du choix (`SaveModSelection`, sans toucher à la dernière sauvegarde) ; entrée d'origine inchangée en partie. Première version (entrée d'origine rendue visible) remplacée après retour (alignement). **Validé en jeu le 2026-10-03** | `.\Tools\Restore.ps1 -Lot UI15` ; `git revert` du commit | -| CHG-104 | 2026-10-03 | H3b | nouveaux `Tools/BuildMod.ps1`, `Tools/ModSource.ps1` ; `Tools/HairPipeline.ps1` (mode `mod` : montage temporaire, `.lphair` et crédits dans `ModSources//Module`, `-Rebuild` déplace aussi les textures) ; `Tools/HairMaterials.py` (analyse synchrone du dossier, réutilisation des textures) ; `Tools/mods/LPHairSample.json` (`mod`) ; `Source/LifePlay/LPHairstyleSubsystem.cpp` (repli `DoesPackageExist`) ; Pocolov 05 reconstruite (précédente dans `LifePlay_Backup_5.5\H3a\Pocolov05\precedente_*`) ; build `LifePlay_Build_5.5_H3b` ; archive `D:\Game\LifePlay_Publication\Mods\LPHairSample-for-LifePlay-2026.10.4.zip` (334 473 octets, SHA-256 `8F6A45ED…CBC8`) | lot H3b : un mod d'assets en une commande ; plantage de `HairMaterials.py` dans un plugin expliqué (registre de la commandlet sans les assets du plugin) et corrigé ; build de base sans rien du mod par construction. **Validé en jeu le 2026-10-03** (16_H3_Mods_Paquets.md §6) | `git revert` du commit, recompiler | -| CHG-105 | 2026-10-03 | H3c | nouveaux `Docs/Modding/FullGuides/Creating_Asset_Mods.md`, `Docs/Modding/ShortGuides/explanation.lphair`, `Tools/NewMod.ps1`, `Tools/EditMod.ps1` ; `Docs/Modding/ShortGuides/explanation.lpmod` (`MODULE_DEFAULT_ENABLED`) ; `Docs/Migration/10_Protocole_Operations.md` §6, `09_Commandes_Utiles.md` §7 (liste de contrôle de publication), `16_H3_Mods_Paquets.md` §7 | lot H3c : guide des moddeurs en anglais, outils de création et d'édition d'un mod, version de référence intégrée à la procédure de publication (livrée dans l'archive des sources ; `Mods/` vide au build) | `git revert` du commit | -| CHG-106 | 2026-10-03 | H3d | nouveaux `Source/LifePlay/LPModInfoSubsystem.h/.cpp` ; `Source/LifePlay/LifePlay.Build.cs` (`Slate`, `SlateCore`) ; `Tools/NewMod.ps1` (métadonnées), `Tools/BuildMod.ps1` (ordre des lignes 1 à 3, `MODULE_PACKAGED`, `MODULE_GAME_VERSION`, `.uplugin` livré, contrôle prix et licences, nom d'archive) ; `Docs/Modding/FullGuides/Creating_Asset_Mods.md` §4b, `explanation.lpmod` | demande utilisateur : métadonnées extensibles (dates, licence, lien de l'auteur, prix) et avertissement en jeu quand un mod a été construit pour une autre version. Le jeu ne lit que les 3 premières lignes d'un `.lpmod`, par position ; fenêtre Slate C++, sans Blueprint. **Validé en jeu le 2026-10-03** | `git revert` du commit, recompiler | -| CHG-107 | 2026-10-03 | H3d | nouveau mod `ModSources/lpr_hair_pocolov` (`.uplugin`, `Module/` : `.lpmod`, 12 `.lphair`, `CREDITS.txt` ; assets non versionnés), `Tools/mods/lpr_hair_pocolov.json` ; retirés : `Content/Modules/lp_Revival` (6 `.lphair`, `.lpmod`), assets `Content/Heavy/New5/Hairs/Pocolov*` (non versionnés), `ModSources/LPHairSample`, `Tools/mods/LPHairSample.json` (tous vers `LifePlay_Backup_5.5\H3d\retires`) ; `Licenses/Free_3D_Models_Credits.txt` (lignes Pocolov remplacées par un renvoi ; snapshot `H3d`) ; nouveau `Tools/modules/next.json` ; builds `LifePlay_Build_5.5_H3d` ; archive `D:\Game\LifePlay_Publication\Mods\lpr_hair_pocolov-1.0-for-LifePlay-2026.10.4.zip` (3 807 460 octets, SHA-256 `4E9420E5…2B8F`) | demande utilisateur : module `lpr_hair_pocolov` regroupant les assets Pocolov ; choix utilisateur : les 12 coiffures CC BY, les 6 du jeu de base déplacées dans le paquet, mod d'essai retiré ; 5 archives téléchargées (accord). **Validé en jeu le 2026-10-03** | remettre les dossiers de `LifePlay_Backup_5.5\H3d\retires` ; `.\Tools\Restore.ps1 -Lot H3d` ; `git revert` du commit | -| CHG-108 | 2026-10-03 | H3c | `Docs/Modding/FullGuides/Creating_Asset_Mods.md` §5 | essai utilisateur de `Tools/EditMod.ps1` : mod monté et chargé (« Mounting Mod plugin lpr_hair_pocolov »), 12 coiffures visibles une fois « Afficher le contenu des plugins » coché dans le navigateur de contenu (masqué par défaut ; la recherche porte sur les noms d'assets) : précision ajoutée au guide. **H3c validé le 2026-10-03** | `git revert` du commit | -| CHG-109 | 2026-10-03 | PUB | `Tools/SplitModules.ps1` (mode `per_module` : une archive par module, notice tirée du `.lpmod`, `INDEX.txt` ; refus d'un build contenant un mod d'assets installé) ; `Tools/modules/next.json` ; `Docs/Migration/15_Modules_Optionnels.md` §5, `09_Commandes_Utiles.md` §7 | décision utilisateur pour la prochaine publication : chaque mod livré dans un zip indépendant, sauf les modules de base (texte) gardés dans le build Shipping ; diffusion des modules sur Discord dans un premier temps. Essai sur une copie (15 archives, 640 fichiers, garde-fou vérifié) | `git revert` du commit | -| CHG-110 | 2026-10-03 | H3c | sources texte de `ModSources/lpr_hair_pocolov` (`.uplugin`, `.lpmod`, 12 `.lphair`, `CREDITS.txt`) | incident : le commit `54f6604` (tag `H3c-valide`) a été fait pendant que le mod était monté dans `Mods/` par `EditMod.ps1` (éditeur ouvert) ; `git add -A` a enregistré la suppression de ses sources. Aucune perte sur disque (mod remis dans `ModSources/` à la fermeture de l'éditeur, 70 assets présents) ; sources réenregistrées par `bf9bb58`. Règle ajoutée (10_Protocole_Operations.md §6) : ne jamais commiter ni lancer un build tant que `Mods/` n'est pas vide. Le tag `H3c-valide` reste sur `54f6604` (déplacement non fait sans accord) | — | -| CHG-111 | 2026-10-03 | V1.5 | `Config/DefaultGame.ini` (`ProjectVersion=2026.10.1.5`), format de version (`14_Interface_V1.4.md`, `LPVersionSubsystem.h`), `README.md` (section V1.5, branche), `Tools/modules/next.json` (groupe `vin_*`), `Tools/SplitModules.ps1` (groupes), `Tools/BuildMod.ps1` (`INDEX.txt`), `15_Modules_Optionnels.md` ; build `LifePlay_Build_5.5_Shipping_V1.5` (version de référence `Releases/2026.10.1.5`, paquets identiques ; 2 modules de base) ; `Releases/2026.10.4` déplacé vers `LifePlay_Backup_5.5\Releases` ; modules `D:\Game\LifePlay_Publication\Modules_2026.10.1.5` (`vin_Modules`, `lpr_hair_pocolov-1.0`, `INDEX.txt`) | publication V1.5 (décisions utilisateur : version `année.mois.majeur.mineur` ; modules `vin_*` en un seul zip, autres mods chacun le leur ; diffusion des modules sur Discord, rien sur GitHub). Validé en jeu le 2026-10-03 ; les mods extraits dans le build pour le test et les fichiers de session ont été déplacés vers `LifePlay_Build_5.5_Shipping_V1.5_test_extraction` (build contrôlé contre le manifeste UAT : 0 fichier en trop, 1 459 fichiers) | `git revert` des commits V1.5 | -| CHG-112 | 2026-10-03 | V1.5 | archives `D:\Game\LifePlay_Publication` : `LifePlay_Revival_V1.5_UE5.5_Full_Source_Code.7z` (6 195 829 571 octets, SHA-256 `a71e77ca98c9a86399e4e8685f2b7ea74d6e9358694094968648f98acf7bc9d3` ; avec `Releases/2026.10.1.5` et `ModSources/`, `.git` du clone propre sur `v1.5`) ; `LifePlay_Revival_V1.5_Windows_Game.7z` (1 300 541 638 octets, SHA-256 `f5e34ecaf8de90659853a9ae2393b2571ae45894808f4f2b59ba1b4e857a7fe1`, racine `LifePlay_Revival_V1.5`) ; scripts `make_archive_v1_5.cmd`, `make_game_v1_5.cmd` | archives de publication V1.5 : tests 7-Zip OK ; sources sans `.mcp.json`, `.claude`, `Saved`, `Mods`, `Intermediate`, McpAutomationBridge, produits de cuisson des mods, ni `SecurityToken` dans `Config` ; jeu sans aucun mod. Clone propre mis à jour depuis le dépôt local (récupération GitHub bloquée, arrêtée) | — | -| CHG-113 | 2026-10-03 | V1.5 | `README.md` (téléchargement : archive des sources V1.5 en principal, lien Mega, SHA-256, raison de la taille ; V1.4 dans les versions précédentes) | lien Mega de l'archive des sources V1.5 fourni par l'utilisateur ; le jeu et les modules (Discord) ne sont pas mentionnés sur GitHub | `git revert` du commit | diff --git a/Docs/Migration/etat/sandbox/S0/S0-CART.md b/Docs/Migration/etat/sandbox/S0/S0-CART.md new file mode 100644 index 0000000..9f5c55f --- /dev/null +++ b/Docs/Migration/etat/sandbox/S0/S0-CART.md @@ -0,0 +1,47 @@ +# S0-CART — Inventaire périmètre sandbox + +- **Date :** 2026-10-06 +- **Fiche :** S0-T1 | **Agent :** A-CART | **Modèle prévu :** `gemini-3.8-flash-high` | **Utilisé :** `gemini-3.8-flash-high` + +## Diagnostic `.gitignore` + +| Élément | Détail | +|---|---| +| Motif | Ligne 5 : `Debug` (sans `/`) | +| Effet | Ignore `Tools/debug/` sous Windows (`core.ignorecase=true`) | +| Preuve | `git check-ignore -v` → `.gitignore:5:Debug` sur les 3 bats | + +Correction attendue S0-T2 : remplacer par `/Debug/`. + +## Inventaire + +| Chemin | LOC approx | Statut Git | Notes | +|---|---:|---|---| +| `Source/LifePlay/LPPairSandbox.h` | 322 | `??` | Stage/Character/GameMode Pair | +| `Source/LifePlay/LPPairSandbox.cpp` | 4261 | `??` | Monolith Pair/Interaction/Mesure | +| `Source/LifePlay/LPInteractionSandbox.h` | 102 | `??` | Legacy Interaction GM | +| `Source/LifePlay/LPInteractionSandbox.cpp` | 440 | `??` | Legacy (bats n'utilisent plus) | +| `Source/LifePlay/LPPoseMeasure.h` | 100 | `??` | API mesure | +| `Source/LifePlay/LPPoseMeasure.cpp` | 1558 | `??` | Sample/Transfer/T-fit | +| `Source/LifePlay/LPSkeletonProfile.h` | 38 | `??` | Profils JSON | +| `Source/LifePlay/LPSkeletonProfile.cpp` | 157 | `??` | | +| `Source/LifePlay/LPPoseOrchestrator.h` | 30 | `??` | | +| `Source/LifePlay/LPPoseOrchestrator.cpp` | 487 | `??` | | +| `Source/LifePlay/LPAnimRedirect.h` | 15 | `??` | sandbox\|game target | +| `Source/LifePlay/LPAnimRedirect.cpp` | 24 | `??` | | +| `Source/LifePlay/LPShowCoupleSubsystem.h` | 77 | `??` | Port jeu (reste dans LifePlay) | +| `Source/LifePlay/LPShowCoupleSubsystem.cpp` | 1048 | `??` | | +| `Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.*` | ~546 | `??` | PairCol headless | +| `Source/LifePlayEditorTools/LPAnimEvalCommandlet.*` | ~489 | `??` | Mesure headless | +| `Source/LifePlayEditorTools/LPAnimCatalogCommandlet.*` | ~388 | `??` | | +| `Source/LifePlayEditorTools/LPAnimChecks.*` | ~435 | `??` | | +| `Source/LifePlayEditorTools/LPAnimTwistCommandlet.*` | ~384 | `??` | | +| `Tools/debug/LifePlay_pair_sandbox.bat` | 7 | ignored (`Debug`) | Lanceur Pair | +| `Tools/debug/LifePlay_interaction_sandbox.bat` | 7 | ignored (`Debug`) | Lanceur Interaction | +| `Tools/debug/LifePlay_measure_sandbox.bat` | 9 | ignored (`Debug`) | Lanceur Mesure | + +## Synthèse + +- Runtime sandbox+measure : ~7.5k LOC dans `Source/LifePlay/` +- Bats : 3 fichiers masqués par `.gitignore:5:Debug` +- ShowCouple reste côté jeu (sync Lot S5) diff --git a/Docs/Migration/etat/sandbox/S0/S0-VAL.md b/Docs/Migration/etat/sandbox/S0/S0-VAL.md new file mode 100644 index 0000000..cbfa82d --- /dev/null +++ b/Docs/Migration/etat/sandbox/S0/S0-VAL.md @@ -0,0 +1,25 @@ +# S0-VAL — Validation S0-T2 + +- **Date :** 2026-10-06 +- **Fiche :** S0-T3 | **Agent :** A-VAL | **Rôle :** validation S0-T2 +- **Modèle prévu :** `composer-2.5-fast` | **Modèle utilisé :** `composer-2.5-fast` + +## Verdict + +**PASS** + +## Checks + +| # | Critère | Résultat | Preuve | +|---:|---|---|---| +| 1 | `.gitignore` ligne Debug = `/Debug/` (pas `Debug` nu) | OK | Ligne 5 : `/Debug/` | +| 2 | `git check-ignore -v` sur les 3 bats ne matche pas | OK | Exit code `1`, sortie vide | +| 3 | Les 3 `.bat` existent sur disque | OK | `Tools/debug/LifePlay_pair_sandbox.bat`, `LifePlay_interaction_sandbox.bat`, `LifePlay_measure_sandbox.bat` | +| 4 | Sources inventoriées présentes | OK | `Source/LifePlay/LPPairSandbox.cpp`, `LPPoseMeasure.cpp`, `LPShowCoupleSubsystem.cpp` | + +## Commandes de preuve + +```text +git check-ignore -v Tools/debug/LifePlay_pair_sandbox.bat Tools/debug/LifePlay_interaction_sandbox.bat Tools/debug/LifePlay_measure_sandbox.bat +→ (aucune sortie), exit 1 +``` diff --git a/Docs/Migration/etat/sandbox/S1/S1-CART.md b/Docs/Migration/etat/sandbox/S1/S1-CART.md new file mode 100644 index 0000000..cef1e4a --- /dev/null +++ b/Docs/Migration/etat/sandbox/S1/S1-CART.md @@ -0,0 +1,193 @@ +# S1-CART — Cartographie et classification des chemins Sandbox + +- **Date :** 2026-10-06 +- **Fiche :** S1-T3 | **Agent :** A-CART | **Rôle :** Cartographie & Audit de conformité S1 +- **Modèle prévu :** `gemini-3.8-flash-high` | **Modèle utilisé :** `gemini-3.8-flash-high` +- **Référence spécification :** `Docs/Migration/etat/sandbox/S1/S1-SPEC.md` +- **Référence implémentation :** `Docs/Migration/etat/sandbox/S1/S1-IMPL.md` + +--- + +## 1. Périmètre et méthode + +### Fichiers audités +- `Source/LifePlay/LPPairSandbox.cpp` +- `Source/LifePlay/LPAnimRedirect.cpp` +- `Source/LifePlay/LPSandboxConfig.h` +- `Source/LifePlay/LPSandboxConfig.cpp` +- *(En-têtes associés pour traçabilité : `LPPairSandbox.h`, `LPAnimRedirect.h`)* + +### Motifs recherchés +1. `/Game/Heavy` +2. `/Game/BPs/Person` +3. `vin_Base` +4. `Quaternius` + +### Grille de classification +- **OK default** : Hit localisé dans `LPSandboxConfig.*` (valeur par défaut conforme au contrat `S1-SPEC.md` ou lecture de clé ini), ou appel dynamique via `FLPSandboxConfig::Get().` dans le code exécutable. +- **DETTE (hors contrat volontaire)** : Identifiant C++ interne (méthode, membre, classe), chaîne de log, texte informatif UI Slate ou commentaire explicitement exclu du contrat d'externalisation ini par `S1-SPEC.md`. +- **OUBLI** : Littéral de chemin restant en dur dans le code d'exécution qui aurait dû passer par la configuration `FLPSandboxConfig`. + +--- + +## 2. Inventaire et classification des hits + +### 2.1. Motif `/Game/Heavy` + +| Fichier | Ligne | Extrait / Contexte | Classification | Justification | +|---|---:|---|---|---| +| `LPSandboxConfig.h` | 16 | `FString BridgetMesh = TEXT("/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp");` | **OK default** | Default contractuel clé `BridgetMesh` (`[Sandbox.Assets]`) | +| `LPSandboxConfig.h` | 17 | `FString MichaelMesh = TEXT("/Game/Heavy/Daz/Michael/michael.michael");` | **OK default** | Default contractuel clé `MichaelMesh` (`[Sandbox.Assets]`) | +| `LPSandboxConfig.h` | 19 | `FString DazIdleFemale = TEXT("/Game/Heavy/Daz/Anims/Idle_F_Daz.Idle_F_Daz");` | **OK default** | Default contractuel clé `DazIdleFemale` (`[Sandbox.Assets]`) | +| `LPSandboxConfig.h` | 20 | `FString DazIdleMale = TEXT("/Game/Heavy/Daz/Anims/idle_Daz.idle_Daz");` | **OK default** | Default contractuel clé `DazIdleMale` (`[Sandbox.Assets]`) | +| `LPSandboxConfig.h` | 21 | `FString FurnitureWallMaterial = TEXT("/Game/Heavy/New2/Furniture/Materials/FntWall.FntWall");` | **OK default** | Default contractuel clé `FurnitureWallMaterial` (`[Sandbox.Assets]`) | +| `LPSandboxConfig.h` | 24 | `FString FurnitureRoot = TEXT("/Game/Heavy/New2/Furniture");` | **OK default** | Default contractuel clé `FurnitureRoot` (`[Sandbox.Roots]`) | +| `LPSandboxConfig.h` | 25 | `FString DazAnimRoot = TEXT("/Game/Heavy/Daz/Anims");` | **OK default** | Default contractuel clé `DazAnimRoot` (`[Sandbox.Roots]`) | +| `LPSandboxConfig.h` | 36 | `FString SkyCubemap = TEXT("/Game/Heavy/New2/Buildings/Mats/urban_street_03_1k.urban_street_03_1k");` | **OK default** | Default contractuel clé `SkyCubemap` (`[Sandbox.Sky]`) | +| `LPSandboxConfig.cpp` | - | *Aucun littéral brut* (lecture des clés ini par nom de clé) | **OK default** | Respecte le contrat | +| `LPPairSandbox.cpp` | 858 | `*FLPSandboxConfig::Get().FurnitureRoot` | **OK default** | Consommateur config (ancien l.857 `/Game/Heavy/New2/Furniture`) | +| `LPPairSandbox.cpp` | 878 | `*FLPSandboxConfig::Get().FurnitureWallMaterial` | **OK default** | Consommateur config (ancien l.877 `.../FntWall.FntWall`) | +| `LPPairSandbox.cpp` | 1002 | `*FLPSandboxConfig::Get().SkyCubemap` | **OK default** | Consommateur config (ancien l.1001 `urban_street_03_1k`) | +| `LPPairSandbox.cpp` | 1079 | `FLPSandboxConfig::Get().DazIdleFemale` | **OK default** | Consommateur config (ancien l.1078 `Idle_F_Daz`) | +| `LPPairSandbox.cpp` | 1080 | `FLPSandboxConfig::Get().DazIdleMale` | **OK default** | Consommateur config (ancien l.1079 `idle_Daz`) | +| `LPPairSandbox.cpp` | 1530 | `*FLPSandboxConfig::Get().BridgetMesh` | **OK default** | Consommateur config (ancien l.1529 `bridget_gp`) | +| `LPPairSandbox.cpp` | 2611 | `*FLPSandboxConfig::Get().DazAnimRoot` | **OK default** | Consommateur config (ancien l.2610 `/Game/Heavy/Daz/Anims`) | +| `LPPairSandbox.cpp` | 3028 | `FLPSandboxConfig::Get().BridgetMesh` | **OK default** | Consommateur config (ancien l.3026 `bridget_gp`) | +| `LPPairSandbox.cpp` | 3031 | `FLPSandboxConfig::Get().MichaelMesh` | **OK default** | Consommateur config (ancien l.3030 `michael`) | +| `LPPairSandbox.cpp` | 3108 | `*FLPSandboxConfig::Get().DazAnimRoot` | **OK default** | Consommateur config (ancien l.3106 `/Game/Heavy/Daz/Anims`) | +| `LPAnimRedirect.cpp` | - | *Aucun hit* | - | Conforme | + +**Bilan `/Game/Heavy` :** 8 `OK default` (définitions `LPSandboxConfig.h`), 10 `OK default` (consommations `LPPairSandbox.cpp`), 0 DETTE, 0 OUBLI. + +--- + +### 2.2. Motif `/Game/BPs/Person` + +| Fichier | Ligne | Extrait / Contexte | Classification | Justification | +|---|---:|---|---|---| +| `LPSandboxConfig.h` | 15 | `FString PersonBP = TEXT("/Game/BPs/Person.Person_C");` | **OK default** | Default contractuel clé `PersonBP` (`[Sandbox.Assets]`) | +| `LPSandboxConfig.cpp` | 46 | `SandboxCfgReadString(File, TEXT("Sandbox.Assets"), TEXT("PersonBP"), PersonBP);` | **OK default** | Lecture ini de la clé `PersonBP` | +| `LPPairSandbox.cpp` | 845 | `LoadObject(nullptr, *FLPSandboxConfig::Get().PersonBP);` | **OK default** | Consommateur config dans `Build` (ancien l.844) | +| `LPPairSandbox.cpp` | 1090 | `LoadObject(nullptr, *FLPSandboxConfig::Get().PersonBP);` | **OK default** | Consommateur config dans `ConfigureCapsule` (ancien l.1089) | +| `LPAnimRedirect.cpp` | - | *Aucun hit* | - | Conforme | + +*(Note : les occurrences de `PersonClass`, `PersonCap`, `struct FPerson` dans `LPPairSandbox.cpp` sont des symboles C++ / AST interne de script, sans aucun littéral de chemin).* + +**Bilan `/Game/BPs/Person` :** 2 `OK default` (`LPSandboxConfig.*`), 2 `OK default` (`LPPairSandbox.cpp`), 0 DETTE, 0 OUBLI. + +--- + +### 2.3. Motif `vin_Base` + +| Fichier | Ligne | Extrait / Contexte | Classification | Justification | +|---|---:|---|---|---| +| `LPSandboxConfig.h` | 30 | `FString ModuleRoot = TEXT("Modules/vin_Base");` | **OK default** | Default contractuel clé `ModuleRoot` (`[Sandbox.Content]`) | +| `LPSandboxConfig.h` | 31 | `FString WorldFile = TEXT("Modules/vin_Base/World/work___vin01.lpworld");` | **OK default** | Default contractuel clé `WorldFile` (`[Sandbox.Content]`) | +| `LPSandboxConfig.h` | 32 | `FString ChatAction = TEXT("Modules/vin_Base/Interactions/chat.lpaction");` | **OK default** | Default contractuel clé `ChatAction` (`[Sandbox.Content]`) | +| `LPSandboxConfig.cpp` | 59-61 | *Lectures `ModuleRoot`, `WorldFile`, `ChatAction`* | **OK default** | Lectures ini des clés | +| `LPPairSandbox.cpp` | 623 | `FPaths::Combine(..., FLPSandboxConfig::Get().ModuleRoot)` | **OK default** | Consommateur config dans `LoadActions` (ancien l.622) | +| `LPPairSandbox.cpp` | 833 | `FPaths::Combine(..., FLPSandboxConfig::Get().WorldFile)` | **OK default** | Consommateur config dans `Build` (ancien l.832) | +| `LPPairSandbox.cpp` | 1046 | `FPaths::Combine(..., FLPSandboxConfig::Get().ChatAction)` | **OK default** | Consommateur config dans `LoadChat` (ancien l.1045) | +| `LPAnimRedirect.cpp` | - | *Aucun hit* | - | Conforme | + +**Bilan `vin_Base` :** 3 `OK default` (`LPSandboxConfig.h`), 3 `OK default` (`LPPairSandbox.cpp`), 0 DETTE, 0 OUBLI. + +--- + +### 2.4. Motif `Quaternius` + +| Fichier | Ligne | Extrait / Contexte | Classification | Justification | +|---|---:|---|---|---| +| `LPSandboxConfig.h` | 18 | `FString QuaterniusMannequinMesh = TEXT("/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard");` | **OK default** | Default contractuel clé `QuaterniusMannequinMesh` | +| `LPSandboxConfig.h` | 26 | `FString QuaterniusRoot = TEXT("/Game/Quaternius");` | **OK default** | Default contractuel clé `QuaterniusRoot` | +| `LPSandboxConfig.h` | 27 | `FString QuaterniusUALRoot = TEXT("/Game/Quaternius/UAL");` | **OK default** | Default contractuel clé `QuaterniusUALRoot` | +| `LPSandboxConfig.h` | 33 | `FString QuaterniusUALDisk = TEXT("Quaternius/UAL");` | **OK default** | Default contractuel clé `QuaterniusUALDisk` | +| `LPSandboxConfig.cpp` | 49 | `SandboxCfgReadString(..., TEXT("QuaterniusMannequinMesh"), ...);` | **OK default** | Lecture ini clé `QuaterniusMannequinMesh` | +| `LPSandboxConfig.cpp` | 56 | `SandboxCfgReadString(..., TEXT("QuaterniusRoot"), ...);` | **OK default** | Lecture ini clé `QuaterniusRoot` | +| `LPSandboxConfig.cpp` | 57 | `SandboxCfgReadString(..., TEXT("QuaterniusUALRoot"), ...);` | **OK default** | Lecture ini clé `QuaterniusUALRoot` | +| `LPSandboxConfig.cpp` | 62 | `SandboxCfgReadString(..., TEXT("QuaterniusUALDisk"), ...);` | **OK default** | Lecture ini clé `QuaterniusUALDisk` | +| `LPAnimRedirect.cpp` | 23 | `*FLPSandboxConfig::Get().QuaterniusUALRoot` | **OK default** | Formatage dynamique `ClipPath` via config | +| `LPAnimRedirect.h` | 4 | `// Les fichiers restent dans /Game/Quaternius/UAL.` | **DETTE** | Commentaire d'en-tête informatif (aucun impact runtime) | +| `LPPairSandbox.h` | 240 | `void LoadQuaternius();` | **DETTE** | Déclaration méthode C++ interne | +| `LPPairSandbox.h` | 261 | `FString QuaterniusMesh;` | **DETTE** | Déclaration membre C++ interne de cache | +| `LPPairSandbox.h` | 262 | `TMap QuaterniusClips;` | **DETTE** | Déclaration membre C++ interne de cache | +| `LPPairSandbox.cpp` | 1529 | `*FLPSandboxConfig::Get().QuaterniusMannequinMesh` | **OK default** | Consommateur config dans `BeginMeasure` | +| `LPPairSandbox.cpp` | 1890 | `// Clips du mannequin ... les 43 clips Quaternius.` | **DETTE** | Commentaire de documentation interne | +| `LPPairSandbox.cpp` | 1895 | `FLPSandboxConfig::Get().QuaterniusUALDisk` | **OK default** | Consommateur config dans `GetMeasureClips` | +| `LPPairSandbox.cpp` | 2773 | `LoadQuaternius();` | **DETTE** | Appel méthode C++ interne | +| `LPPairSandbox.cpp` | 2958 | `void ALPPairSandboxCharacter::LoadQuaternius()` | **DETTE** | Définition méthode C++ interne | +| `LPPairSandbox.cpp` | 2960 | `QuaterniusMesh.Reset();` | **DETTE** | Utilisation membre C++ interne | +| `LPPairSandbox.cpp` | 2961 | `QuaterniusClips.Reset();` | **DETTE** | Utilisation membre C++ interne | +| `LPPairSandbox.cpp` | 2963 | `FLPSandboxConfig::Get().QuaterniusRoot` | **OK default** | Scan synchronous via clé config | +| `LPPairSandbox.cpp` | 2965 | `*FLPSandboxConfig::Get().QuaterniusUALRoot` | **OK default** | Recherche assets via clé config | +| `LPPairSandbox.cpp` | 2977 | `QuaterniusMesh = Data.GetSoftObjectPath().ToString();` | **DETTE** | Assignation membre C++ interne | +| `LPPairSandbox.cpp` | 2986 | `QuaterniusClips.Add(Name, Data.GetSoftObjectPath().ToString());` | **DETTE** | Assignation membre C++ interne | +| `LPPairSandbox.cpp` | 2990 | `if (!QuaterniusClips.Contains(TEXT("Walk_Loop")))` | **DETTE** | Consultation membre C++ interne | +| `LPPairSandbox.cpp` | 2994 | `UE_LOG(LogLifePlay, Warning, TEXT("Sandbox Quaternius : ..."), ...);` | **DETTE** | Tag de log console informatif | +| `LPPairSandbox.cpp` | 3024 | `FString Path = QuaterniusMesh;` | **DETTE** | Lecture membre C++ interne | +| `LPPairSandbox.cpp` | 3035 | `LoadQuaternius();` | **DETTE** | Appel méthode C++ interne | +| `LPPairSandbox.cpp` | 3036 | `Path = QuaterniusMesh;` | **DETTE** | Lecture membre C++ interne | +| `LPPairSandbox.cpp` | 3479 | `LoadQuaternius();` | **DETTE** | Appel méthode C++ interne | +| `LPPairSandbox.cpp` | 3482 | `QuaterniusClips.GetKeys(ClipNames);` | **DETTE** | Lecture membre C++ interne | +| `LPPairSandbox.cpp` | 3883 | `TEXT("MC et PNJ jouent le meme clip Quaternius...")` | **DETTE** | Texte UI Slate explicatif (hors contrat ini) | + +**Bilan `Quaternius` :** 13 `OK default` (4 en `.h`, 4 en `.cpp`, 5 consommateurs), 19 `DETTE` (identifiants C++, logs, Slate UI, commentaires), 0 OUBLI. + +--- + +## 3. Matrice de couverture contractuelle `S1-SPEC.md` + +Vérification des 19 clés définies dans `Docs/Migration/etat/sandbox/S1/S1-SPEC.md` : + +| Clé | Section | Valeur Default contractuelle | Statut Implémentation | Statut Littéral Résiduel | +|---|---|---|---|---| +| `PersonBP` | `Sandbox.Assets` | `/Game/BPs/Person.Person_C` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `BridgetMesh` | `Sandbox.Assets` | `/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `MichaelMesh` | `Sandbox.Assets` | `/Game/Heavy/Daz/Michael/michael.michael` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `QuaterniusMannequinMesh` | `Sandbox.Assets` | `/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `DazIdleFemale` | `Sandbox.Assets` | `/Game/Heavy/Daz/Anims/Idle_F_Daz.Idle_F_Daz` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `DazIdleMale` | `Sandbox.Assets` | `/Game/Heavy/Daz/Anims/idle_Daz.idle_Daz` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `FurnitureWallMaterial` | `Sandbox.Assets` | `/Game/Heavy/New2/Furniture/Materials/FntWall.FntWall` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `FurnitureRoot` | `Sandbox.Roots` | `/Game/Heavy/New2/Furniture` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `DazAnimRoot` | `Sandbox.Roots` | `/Game/Heavy/Daz/Anims` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `QuaterniusRoot` | `Sandbox.Roots` | `/Game/Quaternius` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `QuaterniusUALRoot` | `Sandbox.Roots` | `/Game/Quaternius/UAL` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `ModuleRoot` | `Sandbox.Content` | `Modules/vin_Base` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `WorldFile` | `Sandbox.Content` | `Modules/vin_Base/World/work___vin01.lpworld` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `ChatAction` | `Sandbox.Content` | `Modules/vin_Base/Interactions/chat.lpaction` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `QuaterniusUALDisk` | `Sandbox.Content` | `Quaternius/UAL` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `SkyCubemap` | `Sandbox.Sky` | `/Game/Heavy/New2/Buildings/Mats/urban_street_03_1k.urban_street_03_1k` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `SkyIntensity` | `Sandbox.Sky` | `0.6` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `MeasureOutputRoot` | `Sandbox.Output` | `Measurement` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | +| `Mode` | `Sandbox.Mode` | `Pair` | Implémenté (`FLPSandboxConfig`) | Aucun littéral durci restant | + +--- + +## 4. Analyse des DETTES et éléments hors contrat + +Conformément à la section 1 de `S1-SPEC.md`, les éléments suivants ont été laissés hors contrat de façon volontaire et justifiée : + +1. **Symboles C++ internes :** Les identifiants `LoadQuaternius()`, `QuaterniusMesh`, `QuaterniusClips` sont des noms de symboles internes au monolithe `ALPPairSandboxCharacter`. Leur renommage relève du refactoring / renommage de pipeline et non de la configuration de chemins. +2. **Textes d'interface utilisateur (UI Slate) :** La ligne 3883 de `LPPairSandbox.cpp` contient une aide contextuelle pour les développeurs (`MC et PNJ jouent le meme clip Quaternius...`). Elle ne référence aucun asset UE et n'impacte pas le pipeline d'import/redirection. +3. **Logs d'information console :** Les chaînes de logging (`UE_LOG(LogLifePlay, Warning, TEXT("Sandbox Quaternius : corps %s..."))`) servent au diagnostic développeur. +4. **Littéraux Engine & Poses conventionnelles :** Les chemins `/Engine/BasicShapes/Cube.Cube` (l.877), `/Engine/EngineSky/BP_Sky_Sphere.BP_Sky_Sphere_C` (l.1025) ainsi que les noms de poses conventionnels (`Idle_Loop`, `Walk_Loop`, `A_TPose`, préfixes `B_`/`M_`) restent en dur comme stipulé au contrat. +5. **Observation périphérique hors périmètre :** `Source/LifePlay/LPInteractionSandbox.cpp` contient encore des chemins en dur (`/Game/Heavy/...`, `Modules/vin_Base/...`). Ce fichier correspond à l'ancienne sandbox d'interaction legacy (marquée comme obsolète dans `S0-CART.md`) et ne fait pas partie du périmètre S1 défini par `S1-SPEC.md` (qui se concentre sur `LPPairSandbox` et `LPAnimRedirect`). + +--- + +## 5. Synthèse et critère Done + +### Synthèse quantitative + +- **Hits totaux audités sur les 4 motifs :** 47 + - **OK default :** 28 (initialiseurs dans `LPSandboxConfig.h`, parsers dans `LPSandboxConfig.cpp`, consommations via `FLPSandboxConfig::Get()` dans `LPPairSandbox.cpp` et `LPAnimRedirect.cpp`) + - **DETTE (hors contrat volontaire) :** 19 (symboles C++, logs, aide Slate UI, commentaires) + - **OUBLI :** 0 + +### Critère Done + +> **Critère Done : aucun OUBLI bloquant non justifié.** + +- **Résultat :** **ATTEINT (100% CONFORME)**. +- Aucun littéral de chemin d'asset ou de fichier disque correspondant aux motifs recherchés n'est resté en dur dans `Source/LifePlay/LPPairSandbox.cpp` ou `Source/LifePlay/LPAnimRedirect.cpp`. +- Tous les chemins passent sans exception par la configuration centralisée `FLPSandboxConfig::Get()`. diff --git a/Docs/Migration/etat/sandbox/S1/S1-IMPL.md b/Docs/Migration/etat/sandbox/S1/S1-IMPL.md new file mode 100644 index 0000000..4c94d88 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S1/S1-IMPL.md @@ -0,0 +1,62 @@ +# S1-T2 — Implémentation du lecteur `Config/LPSandbox.ini` + +Rôle : A-IMPL-L. Référence : `S1-SPEC.md`. Pas de split du monolith ; `ShowCouple` non modifié. Non compilé / non committé (voir « Reste à faire »). + +## Fichiers + +| Fichier | Statut | +|---|---| +| `Source/LifePlay/LPSandboxConfig.h` | créé | +| `Source/LifePlay/LPSandboxConfig.cpp` | créé | +| `Config/LPSandbox.ini` | créé (contenu exemple du SPEC, valeurs = littéraux historiques) | +| `Source/LifePlay/LPPairSandbox.cpp` | modifié (`#include "LPSandboxConfig.h"` + remplacements ci-dessous) | +| `Source/LifePlay/LPAnimRedirect.cpp` | modifié (`ClipPath`) | + +## Symboles créés + +- `struct LIFEPLAY_API FLPSandboxConfig` : champs `Mode, PersonBP, BridgetMesh, MichaelMesh, QuaterniusMannequinMesh, DazIdleFemale, DazIdleMale, FurnitureWallMaterial, FurnitureRoot, DazAnimRoot, QuaterniusRoot, QuaterniusUALRoot, ModuleRoot, WorldFile, ChatAction, QuaterniusUALDisk, SkyCubemap, SkyIntensity (float), MeasureOutputRoot`. Initialiseurs de membres = littéraux historiques. +- `static const FLPSandboxConfig& FLPSandboxConfig::Get()` : singleton (static locale thread-safe), chargé une fois. +- `void FLPSandboxConfig::Load()` (privé) : `FPaths::FileExists` puis `FConfigFile::Read(ProjectConfigDir()/LPSandbox.ini)` ; lecture directe non hiérarchique. Fichier absent, clé absente ou valeur vide => default conservé. `SkyIntensity` : ignoré si non numérique. +- Helper anonyme `SandboxCfgReadString` (`LPSandboxConfig.cpp`). + +## Symboles touchés dans `LPPairSandbox.cpp` + +| Fonction | Littéral remplacé | Clé | +|---|---|---| +| namespace `PairSandbox` (catalogue d'actions, scan `*.lpaction`, ~l.622) | `"Modules/vin_Base"` | `ModuleRoot` | +| `ALPPairSandboxStage::Build` | `.../World/work___vin01.lpworld` | `WorldFile` | +| `ALPPairSandboxStage::Build` | `/Game/BPs/Person.Person_C` | `PersonBP` | +| `ALPPairSandboxStage::Build` | `/Game/Heavy/New2/Furniture/%s.%s` | `FurnitureRoot` (`"%s/%s.%s"`) | +| `ALPPairSandboxStage::Build` | `.../Materials/FntWall.FntWall` | `FurnitureWallMaterial` | +| `ALPPairSandboxStage::Build` (SkyLight) | `SetIntensity(0.6f)` | `SkyIntensity` | +| `ALPPairSandboxStage::Build` (SkyLight) | `urban_street_03_1k` | `SkyCubemap` | +| `ALPPairSandboxStage::LoadChat` | `.../Interactions/chat.lpaction` | `ChatAction` | +| `ALPPairSandboxStage::PlayIdle` | `Idle_F_Daz` / `idle_Daz` (`const TCHAR*` -> `const FString&`) | `DazIdleFemale` / `DazIdleMale` | +| `ALPPairSandboxStage::ConfigureCapsule` | `/Game/BPs/Person.Person_C` | `PersonBP` | +| `ALPPairSandboxStage::BeginMeasure` (appels `Place` Mannequin/Bridget) | `UAL1_Standard`, `bridget_gp` | `QuaterniusMannequinMesh`, `BridgetMesh` | +| `PairSandboxAuto::ShotDir`, `PairSandboxTour::TourDir/VignetteDir/PlancheDir` | `TEXT("Measurement")` | `MeasureOutputRoot` | +| `ALPPairSandboxStage::ShowFormulaDefault` (`WriteRetargetPose`) | `TEXT("Measurement")` | `MeasureOutputRoot` | +| `ALPPairSandboxStage::GetMeasureClips` (scan disque clips `B_`) | `Quaternius`,`UAL` | `QuaterniusUALDisk` | +| `ALPPairSandboxStage::FitCoupleBox` (lambda `Accumulate`) et `ALPPairSandboxCharacter::RefreshPose` | `/Game/Heavy/Daz/Anims/%s%s.%s%s` | `DazAnimRoot` (`"%s/%s%s.%s%s"`) | +| `ALPPairSandboxCharacter::LoadQuaternius` | `/Game/Quaternius`, `/Game/Quaternius/UAL` | `QuaterniusRoot`, `QuaterniusUALRoot` | +| `ALPPairSandboxCharacter::ApplyBody` | `bridget_gp`, `michael` | `BridgetMesh`, `MichaelMesh` | +| `ALPPairSandboxCharacter::ShowAnimMenu` (fin, après lecture de `sandbox_mode.txt`) | nouveau : branche `else` si `Saved/sandbox_mode.txt` absent | `Mode` | + +`Mode` : le `.txt` éphémère garde la priorité (comportement inchangé s'il existe). S'il est absent et que `Mode != Pair`, `SelectMode` est appelé sur le choix correspondant (`Pair` = comportement actuel, aucun appel). + +## Symbole touché dans `LPAnimRedirect.cpp` + +- `LPAnimRedirect::ClipPath` : `"/Game/Quaternius/UAL/%s.%s"` -> `"%s/%s.%s"` avec `QuaterniusUALRoot`. + +## Defaults + +Tous les defaults (champs de `FLPSandboxConfig` et `Config/LPSandbox.ini`) sont identiques caractère par caractère aux littéraux remplacés ; sans ini ou avec l'ini exemple, chemins et valeurs résultants sont identiques (les `FString::Printf` reconstituent exactement les mêmes chaînes). Les logs d'erreur (`LogLifePlay`) sont inchangés. + +## Hors périmètre / non touchés + +`ShowCouple`, `Saved/sandbox_mode.txt`/`sandbox_tour.txt`, `LPKeyboard.ini`, littéraux `/Engine/...`, noms de clips/préfixes, liste `Names[]`. + +## Reste à faire + +- Compilation UBT non exécutée dans cette fiche (nouveaux fichiers pris par le glob du module `LifePlay`). +- Packaging (staging de `Config/LPSandbox.ini`) : voir note du SPEC, non traité. diff --git a/Docs/Migration/etat/sandbox/S1/S1-SPEC.md b/Docs/Migration/etat/sandbox/S1/S1-SPEC.md new file mode 100644 index 0000000..822d104 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S1/S1-SPEC.md @@ -0,0 +1,107 @@ +# S1-T1 — Contrat `Config/LPSandbox.ini` + +Rôle : A-ARCH. Périmètre : **spécification uniquement** (aucun changement runtime dans cette fiche). +Source analysée : `Source/LifePlay/LPPairSandbox.cpp` (et `.h`, qui ne contient aucun littéral de chemin : seulement `QuaterniusMesh` / `QuaterniusClips` en `FString`, lignes 261-262) + `Source/LifePlay/LPAnimRedirect.cpp:22` (helper `ClipPath` utilisé par le sandbox). + +## 1. Décisions de contrat + +| Décision | Choix | +|---|---| +| Emplacement | `Config/LPSandbox.ini` (projet, versionné). **Pas** `Saved/`. | +| Lecture (S2, hors périmètre ici) | `FConfigFile::Read(FPaths::Combine(FPaths::ProjectConfigDir(), TEXT("LPSandbox.ini")))` — lecture directe, non hiérarchique (pas de `DefaultLPSandbox.ini`). | +| Fichier absent / clé absente | Repli sur le **default = littéral actuel du code** (colonne « default »). Le comportement actuel doit rester identique sans ini. | +| Section | Une section par thème : `[Sandbox.Mode]`, `[Sandbox.Assets]`, `[Sandbox.Roots]`, `[Sandbox.Content]`, `[Sandbox.Sky]`, `[Sandbox.Output]`. | +| Convention de chemin asset | Chemin UE complet `/Game/...Nom.Nom` (format `LoadObject`), sauf mention « racine » (sans `.Nom`). | +| Convention de chemin disque | Relatif à `FPaths::ProjectContentDir()` / `ProjectSavedDir()` selon la clé (le préfixe est noté dans l'usage). | +| Hors contrat (volontairement) | `Saved/Config/Windows/LPKeyboard.ini` (config clavier utilisateur, reste en Saved), `Saved/sandbox_mode.txt` et `Saved/sandbox_tour.txt` (déclencheurs éphémères consommés au lancement), littéraux `/Engine/...` (Cube, `BP_Sky_Sphere`), noms de clips/poses (`Idle_Loop`, `Walk_Loop`, `A_TPose`, préfixes `B_`/`M_`), liste des 43 poses `Names[]` (l. ~3365-3376). | + +## 2. Tableau des clés + +Les clés notées `*` sont critiques (demandées par la fiche). Numéros de ligne = `LPPairSandbox.cpp` au moment de l'analyse. + +| clé ini | section | valeur default | usage dans code | fichier:ligne | +|---|---|---|---|---| +| `PersonBP` * | `Sandbox.Assets` | `/Game/BPs/Person.Person_C` | `LoadObject` : lecture des défauts de capsule (rayon, demi-hauteur) dans `ALPPairSandboxStage::Build` | `LPPairSandbox.cpp:844` | +| `PersonBP` (même clé, 2e usage) | `Sandbox.Assets` | idem | `LoadObject` : copie du profil/réponses de collision de la capsule dans `ConfigureCapsule` | `LPPairSandbox.cpp:1089` | +| `WorldFile` * | `Sandbox.Content` | `Modules/vin_Base/World/work___vin01.lpworld` | Fichier de la chambre (props + échelle) ; chemin = `ProjectContentDir()` + valeur ; passé à `PairSandbox::LoadWorldFile` | `LPPairSandbox.cpp:832` | +| `ModuleRoot` * | `Sandbox.Content` | `Modules/vin_Base` | Racine du scan récursif `*.lpaction` (catalogue d'actions Interaction) ; `ProjectContentDir()` + valeur | `LPPairSandbox.cpp:622` | +| `ChatAction` | `Sandbox.Content` | `Modules/vin_Base/Interactions/chat.lpaction` | Lecture de `ACTION_NAME` pour `ChatLabel` ; `ProjectContentDir()` + valeur | `LPPairSandbox.cpp:1045` | +| `FurnitureRoot` * | `Sandbox.Roots` | `/Game/Heavy/New2/Furniture` | Racine des meshes de meubles : `"/%s.%s"` avec `Prop.Name` issu du `.lpworld` | `LPPairSandbox.cpp:857` | +| `FurnitureWallMaterial` | `Sandbox.Assets` | `/Game/Heavy/New2/Furniture/Materials/FntWall.FntWall` | `LoadObject` : matériau des murs/coque de la chambre | `LPPairSandbox.cpp:877` | +| `DazAnimRoot` * | `Sandbox.Roots` | `/Game/Heavy/Daz/Anims` | Racine des clips Daz : paires `"/%s%s.%s%s"` (Pair + `_F`/`_M`) ; idles Daz | `LPPairSandbox.cpp:2610`, `3106`, `1078-1079` | +| `DazIdleFemale` | `Sandbox.Assets` | `/Game/Heavy/Daz/Anims/Idle_F_Daz.Idle_F_Daz` | `PlayIdle(bFemale = true)` | `LPPairSandbox.cpp:1078` | +| `DazIdleMale` | `Sandbox.Assets` | `/Game/Heavy/Daz/Anims/idle_Daz.idle_Daz` | `PlayIdle(bFemale = false)` | `LPPairSandbox.cpp:1079` | +| `BridgetMesh` * | `Sandbox.Assets` | `/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp` | Corps Bridget : `ApplyBody("Bridget")` et mode Mesure (`Place("Bridget", …)`) | `LPPairSandbox.cpp:3026`, `1529` | +| `MichaelMesh` * | `Sandbox.Assets` | `/Game/Heavy/Daz/Michael/michael.michael` | Corps Michael : `ApplyBody("Michael")` | `LPPairSandbox.cpp:3030` | +| `QuaterniusRoot` * | `Sandbox.Roots` | `/Game/Quaternius` | `ScanPathsSynchronous` de l'Asset Registry avant détection du corps/clips | `LPPairSandbox.cpp:2962` | +| `QuaterniusUALRoot` * | `Sandbox.Roots` | `/Game/Quaternius/UAL` | `GetAssetsByPath` (détection auto du mesh le plus riche en bones + clips non `B_`/`M_`) ; sert aussi de base à `LPAnimRedirect::ClipPath` (`"/%s.%s"`) | `LPPairSandbox.cpp:2964`, `LPAnimRedirect.cpp:22` | +| `QuaterniusUALDisk` | `Sandbox.Content` | `Quaternius/UAL` | Dossier disque scanné (`*.uasset`) pour lister les clips ayant une copie `B_` (43 clips) ; `ProjectContentDir()` + valeur | `LPPairSandbox.cpp:1894` | +| `QuaterniusMannequinMesh` | `Sandbox.Assets` | `/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard` | Mannequin de référence en mode Mesure (`Place("Mannequin", …)`) | `LPPairSandbox.cpp:1528` | +| `Mode` * | `Sandbox.Mode` | `Pair` | Mode initial du menu. Valeurs : `Pair`, `Interaction`, `Mesure` (liste `Modes`, `Modes[0]` = `Pair`). Aujourd'hui seul `Mesure` est activable, via `Saved/sandbox_mode.txt` ; l'ini fournirait le mode persistant. Le `.txt` éphémère garde la priorité (compat) | `LPPairSandbox.cpp:3396-3399`, `3907-3921` | +| `SkyCubemap` * (opt.) | `Sandbox.Sky` | `/Game/Heavy/New2/Buildings/Mats/urban_street_03_1k.urban_street_03_1k` | `LoadObject` pour `SkyLight->SetCubemap` (SkyLight `SLS_SpecifiedCubemap`) | `LPPairSandbox.cpp:1001` | +| `SkyIntensity` (opt.) | `Sandbox.Sky` | `0.6` | `SkyLight->SetIntensity(0.6f)` | `LPPairSandbox.cpp:1000` | +| `MeasureOutputRoot` | `Sandbox.Output` | `Measurement` | Sous-dossier de `ProjectSavedDir()` pour `shots`, `tour`, `vignettes`, `planches`, `retarget_pose_*.txt` (les sous-noms restent en dur) | `LPPairSandbox.cpp:1687`, `1862`, `1873`, `1885`, `1930-1931` | + +### Notes + +- `PersonBP` n'a qu'une clé pour deux sites d'appel (844 et 1089) : en S2 un seul accesseur partagé. +- `DazAnimRoot` : les deux idles (`DazIdleFemale/Male`) sont des clés distinctes car leurs noms ne suivent pas le motif `%s%s.%s%s` (casse différente `Idle_F_Daz` / `idle_Daz`) ; les valeurs par défaut sont donc les littéraux exacts, y compris la casse. +- `Walk_Loop`, `Idle_Loop`, `A_TPose`, préfixes `B_`/`M_` : conventions de nommage Quaternius/retarget, non exposées (hors contrat) ; seul leur dossier (`QuaterniusUALRoot`) l'est. +- Les préfixes disque vs chemin UE (`Quaternius/UAL` vs `/Game/Quaternius/UAL`) sont deux clés séparées pour ne pas dériver un chemin de l'autre (l'actuel code les écrit indépendamment). +- Packaging : un `Config/LPSandbox.ini` custom n'est staged que s'il est listé (UFS/`DirectoriesToAlwaysStageAsUFS` ou `+DirectoriesToAlwaysStageAsNonUFS`) ; le sandbox est un outil éditeur/dev, donc à traiter en S2 uniquement si le sandbox doit tourner packagé. + +## 3. Contenu proposé de `Config/LPSandbox.ini` + +Les valeurs ci-dessous sont **identiques** aux littéraux actuels : charger ce fichier ne doit rien changer au comportement. + +```ini +; Config/LPSandbox.ini +; Contrat S1-T1 — chemins et options du bac a sable LPPairSandbox. +; Toute cle absente => valeur par defaut du code (identique a celle-ci). +; Chemins "/Game/..." : format LoadObject (/Package/Nom.Nom ou /Package/BP.BP_C). +; Chemins relatifs (sans "/") : relatifs a Content/ sauf [Sandbox.Output] (relatif a Saved/). + +[Sandbox.Mode] +; Mode initial du menu : Pair | Interaction | Mesure +; (Saved/sandbox_mode.txt, s'il existe, reste prioritaire et est consomme.) +Mode=Pair + +[Sandbox.Assets] +PersonBP=/Game/BPs/Person.Person_C +BridgetMesh=/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp +MichaelMesh=/Game/Heavy/Daz/Michael/michael.michael +QuaterniusMannequinMesh=/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard +DazIdleFemale=/Game/Heavy/Daz/Anims/Idle_F_Daz.Idle_F_Daz +DazIdleMale=/Game/Heavy/Daz/Anims/idle_Daz.idle_Daz +FurnitureWallMaterial=/Game/Heavy/New2/Furniture/Materials/FntWall.FntWall + +[Sandbox.Roots] +; Racines de packages UE (sans nom d'asset final) +FurnitureRoot=/Game/Heavy/New2/Furniture +DazAnimRoot=/Game/Heavy/Daz/Anims +QuaterniusRoot=/Game/Quaternius +QuaterniusUALRoot=/Game/Quaternius/UAL + +[Sandbox.Content] +; Chemins disque, relatifs a Content/ +ModuleRoot=Modules/vin_Base +WorldFile=Modules/vin_Base/World/work___vin01.lpworld +ChatAction=Modules/vin_Base/Interactions/chat.lpaction +QuaterniusUALDisk=Quaternius/UAL + +[Sandbox.Sky] +; Optionnel : cubemap du SkyLight du sandbox +SkyCubemap=/Game/Heavy/New2/Buildings/Mats/urban_street_03_1k.urban_street_03_1k +SkyIntensity=0.6 + +[Sandbox.Output] +; Relatif a Saved/ +MeasureOutputRoot=Measurement +``` + +## 4. Critères d'acceptation pour S2 (implémentation, hors de cette fiche) + +1. Sans `Config/LPSandbox.ini`, comportement strictement identique à aujourd'hui (defaults = littéraux de la colonne « default »). +2. Avec le fichier d'exemple ci-dessus, comportement strictement identique. +3. Un seul point de lecture (struct de config chargée une fois, p. ex. `FLPSandboxConfig::Get()`), plus de `TEXT("/Game/...")` en dur dans `LPPairSandbox.cpp`/`LPAnimRedirect.cpp` pour les clés ci-dessus. +4. Clé présente mais asset introuvable : même journal d'erreur qu'aujourd'hui (`LogLifePlay`), sans repli silencieux sur une autre valeur. diff --git a/Docs/Migration/etat/sandbox/S1/S1-VAL.md b/Docs/Migration/etat/sandbox/S1/S1-VAL.md new file mode 100644 index 0000000..9afc997 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S1/S1-VAL.md @@ -0,0 +1,59 @@ +# S1-VAL — Validation du lot S1 (`Config/LPSandbox.ini`) + +- **Date :** 2026-10-06 +- **Fiche :** S1-T4 | **Rôle :** A-VAL +- **Modèle prévu :** `claude-sonnet-5-5-high` (M2) | **Modèle utilisé :** `claude-sonnet-5-5-high` (M2) +- **Références :** `S1-SPEC.md`, `S1-IMPL.md`, `S1-CART.md` + +## Verdict : **PASS** (preuve statique complète ; runtime UE non exécuté, voir « Limite ») + +| # | Contrôle | Résultat | +|---|---|---| +| 1 | `Config/LPSandbox.ini` existe, 19 clés du SPEC, 6 sections | **PASS** | +| 2 | `FLPSandboxConfig` compile-ready (`Get`/`Load`, defaults = SPEC) | **PASS** (revue statique, UBT non lancé) | +| 3 | `LPPairSandbox.cpp` sans `TEXT("/Game/...)` pour les clés SPEC (0 oubli S1-CART) | **PASS** | +| 4 | Lancement court du bat Pair | **BLOCKED-RUNTIME** (preuve statique PASS) | +| 5 | Test override `WorldFile` invalide | **Test manuel requis** (procédure ci-dessous) | + +## 1. `Config/LPSandbox.ini` + +Fichier présent. Sections : `Sandbox.Mode`, `Sandbox.Assets`, `Sandbox.Roots`, `Sandbox.Content`, `Sandbox.Sky`, `Sandbox.Output`. Les 19 clés du § 3 du SPEC sont toutes présentes avec une valeur identique au SPEC et au default de `LPSandboxConfig.h` (comparaison ligne à ligne) : +`Mode` ; `PersonBP`, `BridgetMesh`, `MichaelMesh`, `QuaterniusMannequinMesh`, `DazIdleFemale`, `DazIdleMale`, `FurnitureWallMaterial` ; `FurnitureRoot`, `DazAnimRoot`, `QuaterniusRoot`, `QuaterniusUALRoot` ; `ModuleRoot`, `WorldFile`, `ChatAction`, `QuaterniusUALDisk` ; `SkyCubemap`, `SkyIntensity` ; `MeasureOutputRoot`. + +## 2. `FLPSandboxConfig` (`LPSandboxConfig.h/.cpp`) + +- `static const FLPSandboxConfig& Get()` : singleton par static locale initialisée par lambda (appelle `Load()` privé, accès légal depuis un membre). +- `void Load()` : `FPaths::FileExists` puis `FConfigFile::Read(ProjectConfigDir()/LPSandbox.ini)`, lecture directe non hiérarchique ; fichier/clé absent ou valeur vide => default conservé ; `SkyIntensity` ignoré si non numérique. +- 19 champs, 19 appels `SandboxCfgReadString` (dont `SkyIntensity` via texte) : aucune clé oubliée, noms de section/clé identiques à l'ini. +- Defaults des initialiseurs de membres = littéraux du SPEC (`Mode="Pair"`, `SkyIntensity=0.6f`, etc.). +- Includes présents (`CoreMinimal.h`, `Misc/ConfigCacheIni.h`, `Misc/Paths.h`) ; `LIFEPLAY_API` sur le struct ; `#include "LPSandboxConfig.h"` dans `LPPairSandbox.cpp` (l. 8) et `LPAnimRedirect.cpp` (l. 2). Aucune erreur de syntaxe/API relevée à la lecture. + +## 3. Grep littéraux (preuve d'absence d'oubli) + +| Recherche | Fichier | Résultat | +|---|---|---| +| `/Game/` | `LPPairSandbox.cpp` | 0 littéral de chemin (1 seule occurrence de texte : message de log `chat.lpaction illisible`, l. 1050, non-chemin) | +| `/Game/`, `Quaternius`, `Measurement` | `LPAnimRedirect.cpp` | uniquement `FLPSandboxConfig::Get().QuaterniusUALRoot` (l. 23) | +| `vin_Base`, `work___vin01`, `Daz/Anims`, `Quaternius/UAL`, `"Measurement"`, `TEXT("Modules` | `LPPairSandbox.cpp` | 0 | +| `FLPSandboxConfig::Get().` | `LPPairSandbox.cpp` / `LPAnimRedirect.cpp` | 27 / 1 consommations | + +Câblage vérifié pour les clés « discrètes » : `SkyIntensity` (l. 1001), `MeasureOutputRoot` (l. 1688, 1863, 1874, 1886, 1932), `Mode` (l. 3926-3931, branche `else` après `sandbox_mode.txt`, `Pair` = comportement inchangé). Reste hors contrat (voulu) : `*.lpaction` (extension), noms `Bridget`/`Michael`/préfixes `B_`/`M_`, `/Engine/...`. `LPInteractionSandbox.cpp` (legacy) hors périmètre S1 (noté dans S1-CART). Concorde avec `S1-CART.md` : 0 OUBLI. + +## 4. Runtime — BLOCKED-RUNTIME + +- `UnrealEditor.exe` **trouvé** : `D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe` (chemin déjà utilisé par `Tools/debug/LifePlay_pair_sandbox.bat`) ; aucun `UnrealEditor` en cours d'exécution. +- Lancement **non tenté** : `Binaries/Win64/UnrealEditor-LifePlay.dll` date du 2026-10-06 16:09 alors que `LPSandboxConfig.cpp` date de 18:18. Les sources S1 n'ont donc jamais été compilées (S1-IMPL : « UBT non exécuté ») ; un lancement chargerait l'ancien binaire (ou la boîte de dialogue de recompilation), sans rien prouver sur S1. La compilation (UBT/`BuildMod`) n'est pas dans le périmètre de cette fiche. +- Conséquence : compilation, chargement de l'ini et comportement runtime **non démontrés**. Prochaine étape : build `LifePlayEditor` puis lancer `Tools/debug/LifePlay_pair_sandbox.bat` (attendu : chambre `work___vin01` chargée, aucun `Error` `LogLifePlay` nouveau). + +## 5. Test override (manuel requis) + +Procédure, après compilation : +1. Sauvegarder `Config/LPSandbox.ini`. Dans `[Sandbox.Content]`, mettre `WorldFile=Modules/vin_Base/World/INEXISTANT.lpworld`. +2. Lancer `Tools/debug/LifePlay_pair_sandbox.bat`. +3. Attendu : log `LogLifePlay: Error: Sandbox collision : chambre illisible (…/Content/Modules/vin_Base/World/INEXISTANT.lpworld)` (`LPPairSandbox.cpp` l. 838) ; pièce sans meubles ; pas de repli silencieux sur le fichier par défaut (critère SPEC § 4.4). +4. Restaurer l'ini (`WorldFile=Modules/vin_Base/World/work___vin01.lpworld`), relancer : chambre normale, 54 meubles. +5. Variante sans ini : renommer temporairement le fichier ; comportement identique à la valeur par défaut (critère SPEC § 4.1). + +## Limite + +PASS fondé **uniquement** sur la preuve statique (ini + code branché + 0 oubli). Compilation et runtime non vérifiés ; tests 4 et 5 à exécuter manuellement après build. diff --git a/Docs/Migration/etat/sandbox/S2/S2-CART.md b/Docs/Migration/etat/sandbox/S2/S2-CART.md new file mode 100644 index 0000000..d04244f --- /dev/null +++ b/Docs/Migration/etat/sandbox/S2/S2-CART.md @@ -0,0 +1,271 @@ +# S2-CART — Cartographie d'extraction du module `LPAnimMeasure` + +- **Date :** 2026-10-06 +- **Fiche :** S2-T1 | **Agent :** A-CART | **Rôle :** Cartographie (lecture seule, aucun code modifié) +- **Modèle prévu :** `claude-sonnet-5-5-high` (M2) +- **Références :** `S1/S1-SPEC.md`, `S1/S1-CART.md` + +--- + +## 1. Décision principale : `LPAnimRedirect` + +`LPAnimRedirect.cpp` appelle `FLPSandboxConfig::Get().QuaterniusUALRoot` (seule dépendance, ligne 23 de `ClipPath`). `LPPoseOrchestrator.cpp` appelle `LPAnimRedirect::ClipPath` 8 fois (l. 223, 233, 237, 255, 262, 303, 345, 404). `LPDirectControlSubsystem.cpp` (runtime jeu, module `LifePlay`) appelle `IsGameTarget()` (l. 880) et `ClipPath()` (l. 960). + +Garder Redirect dans `LifePlay` n'est **pas possible** : `LPPoseOrchestrator` va dans `LPAnimMeasure`, qui ne peut pas dépendre de `LifePlay` (cycle `LifePlay` → `LPAnimMeasure` → `LifePlay`). + +### Choix retenu : **(b) Redirect déménage dans `LPAnimMeasure`, racine UAL injectée** + +- `LPAnimRedirect` ne dépend plus de `LPSandboxConfig` (le `#include "LPSandboxConfig.h"` disparaît du `.cpp`). +- Ajout d'un point d'injection dans `LPAnimRedirect.h` : + - `LPANIMMEASURE_API void SetUALRoot(const FString& Root);` + - `LPANIMMEASURE_API const FString& GetUALRoot();` + - Valeur par défaut interne : `TEXT("/Game/Quaternius/UAL")` (identique au défaut de `FLPSandboxConfig::QuaterniusUALRoot`, donc comportement inchangé sans ini). +- `LPANIMMEASURE_API` ajouté à `IsGameTarget()` et `ClipPath()`. +- **Câblage côté `LifePlay`** (propriétaire de `LPSandboxConfig`, qui reste dans `LifePlay` pour S3) : + `LifePlay.cpp` passe de `FDefaultGameModuleImpl` à une classe `FLifePlayModule : FDefaultGameModuleImpl` dont `StartupModule()` appelle `LPAnimRedirect::SetUALRoot(FLPSandboxConfig::Get().QuaterniusUALRoot)`. Le singleton lit `Config/LPSandbox.ini` ; l'ini est donc pris en compte pour tous les consommateurs (sandbox, `LPDirectControlSubsystem`, commandlets), sans exiger que la sandbox soit lancée d'abord. + Alternative plus légère, moins sûre : appeler `SetUALRoot` au début de `ALPPairSandboxCharacter::BeginPlay`. À éviter : `LPDirectControlSubsystem` et les commandlets s'exécuteraient avec la valeur par défaut seulement. +- **Couplage à documenter** : le contrat `S1-SPEC.md` (clé `Sandbox.Roots/QuaterniusUALRoot`) reste valide, mais sa consommation passe par `SetUALRoot` au lieu d'un appel direct. `S1-CART.md` doit mettre à jour la ligne `LPAnimRedirect.cpp:23` (consommateur indirect). +- Pourquoi (b) plutôt que laisser Redirect hors module : `LPPoseOrchestrator` (dans `LPAnimMeasure`) en dépend directement, donc l'un des deux doit bouger ou être injecté. Déplacer Redirect + une seule fonction d'injection est le plus petit changement (2 fichiers, 1 setter). +- Alternative écartée (c) : injecter un resolver `TFunction` dans l'Orchestrator. Plus invasif (signatures `Run`/`Restore`/`SampleAt`), et `IsGameTarget()` devrait rester séparé. + +--- + +## 2. Liste exhaustive des fichiers à déplacer + +Source : `Source/LifePlay/` → destination : `Source/LPAnimMeasure/` (structure plate comme `LifePlay`, pas de `Public/`/`Private/`). + +| # | Fichier | Destination | +|---|---|---| +| 1 | `LPPoseMeasure.h` | `Source/LPAnimMeasure/LPPoseMeasure.h` | +| 2 | `LPPoseMeasure.cpp` | `Source/LPAnimMeasure/LPPoseMeasure.cpp` | +| 3 | `LPSkeletonProfile.h` | `Source/LPAnimMeasure/LPSkeletonProfile.h` | +| 4 | `LPSkeletonProfile.cpp` | `Source/LPAnimMeasure/LPSkeletonProfile.cpp` | +| 5 | `LPPoseOrchestrator.h` | `Source/LPAnimMeasure/LPPoseOrchestrator.h` | +| 6 | `LPPoseOrchestrator.cpp` | `Source/LPAnimMeasure/LPPoseOrchestrator.cpp` | +| 7 | `LPAnimRedirect.h` | `Source/LPAnimMeasure/LPAnimRedirect.h` | +| 8 | `LPAnimRedirect.cpp` | `Source/LPAnimMeasure/LPAnimRedirect.cpp` | + +Nouveaux fichiers du module : + +| Fichier | Contenu | +|---|---| +| `Source/LPAnimMeasure/LPAnimMeasure.Build.cs` | règles du module (section 5) | +| `Source/LPAnimMeasure/LPAnimMeasure.h` | `LPANIMMEASURE_API DECLARE_LOG_CATEGORY_EXTERN(LogLPAnimMeasure, Log, All);` | +| `Source/LPAnimMeasure/LPAnimMeasure.cpp` | `IMPLEMENT_MODULE(FDefaultModuleImpl, LPAnimMeasure)` + `DEFINE_LOG_CATEGORY(LogLPAnimMeasure)` | + +**Ne bougent PAS** : `LPSandboxConfig.h/.cpp` (reste dans `LifePlay`, S3), `LPPairSandbox.*`, `LPDirectControlSubsystem.*`, `LifePlay.h/.cpp`. + +--- + +## 3. Dépendances sortantes de chaque fichier déplacé + +| Fichier | `#include` projet (hors Engine) | Autres dépendances à traiter | +|---|---|---| +| `LPPoseMeasure.h` | `CoreMinimal.h` | 6 symboles `LIFEPLAY_API` → `LPANIMMEASURE_API` ; commentaire l.~74 (« LifePlayEditorTools ») | +| `LPPoseMeasure.cpp` | `LPPoseMeasure.h`, `LPSkeletonProfile.h`, **`LifePlay.h`** (l. 6) | 12 `UE_LOG(LogLifePlay…)` (l. 298, 862, 878…) ; Engine : `Animation/AnimSequence.h`, `Animation/Skeleton.h`, `Misc/FileHelper.h` | +| `LPSkeletonProfile.h` | `CoreMinimal.h` | `FLPSkeletonProfile` et 2 fonctions `LIFEPLAY_API` → `LPANIMMEASURE_API` | +| `LPSkeletonProfile.cpp` | `LPSkeletonProfile.h`, **`LifePlay.h`** (l. 5) | 4 `UE_LOG(LogLifePlay…)` ; Engine : `Dom/JsonObject.h`, `Serialization/Json*`, `HAL/FileManager.h`, `Misc/Paths.h` ; lit `Tools/anim/profiles/*.json` via `FPaths::ProjectDir()` (aucun changement) | +| `LPPoseOrchestrator.h` | `LPPoseMeasure.h` | aucune API exportée aujourd'hui (voir §4) | +| `LPPoseOrchestrator.cpp` | `LPPoseOrchestrator.h`, `LPAnimRedirect.h`, **`LifePlay.h`** (l. 5) | 4 `UE_LOG(LogLifePlay…)` ; Engine : `Animation/AnimSequence.h`, `HAL/FileManager.h`, `Misc/FileHelper.h`, `Misc/Paths.h` | +| `LPAnimRedirect.h` | `CoreMinimal.h` | ajouter `LPANIMMEASURE_API` + `SetUALRoot/GetUALRoot` | +| `LPAnimRedirect.cpp` | `LPAnimRedirect.h`, **`LPSandboxConfig.h`** (l. 2, à supprimer) | `GConfig` (`Misc/ConfigCacheIni.h`), `FPaths` | + +### Couplages à casser (sinon cycle de modules) + +1. **`LifePlay.h` / `LogLifePlay`** : 3 `.cpp` (20 appels `UE_LOG` au total : Measure 12, Orchestrator 4, SkeletonProfile 4) incluent `LifePlay.h`. `LPAnimMeasure` ne peut pas dépendre de `LifePlay`. Solution : catégorie propre `LogLPAnimMeasure` (`LPAnimMeasure.h/.cpp`), remplacement mécanique `LogLifePlay` → `LogLPAnimMeasure` et `#include "LifePlay.h"` → `#include "LPAnimMeasure.h"`. + - **Effet de bord** : `Tools/LogStats.ps1` ligne 51 compte les lignes `LogLifePlay` (statistique de session). Les lignes `POSET`/`PROFIL` migrent vers `LogLPAnimMeasure` ; ajouter une entrée `LogLPAnimMeasure` dans ce script (ou accepter la baisse du compteur). Aucune occurrence de `LogLifePlay` dans `Source/LifePlayEditorTools`. +2. **`LPSandboxConfig.h`** dans `LPAnimRedirect.cpp` : voir §1. +3. Aucune autre référence à des types `ALP*`/`ULP*`/`FLPSandbox*`/`FLPPair*`/`GEngine`/`UWorld` dans `LPPoseMeasure.cpp`, `LPSkeletonProfile.cpp`, `LPPoseOrchestrator.cpp` (vérifié par grep, 0 occurrence) : l'extraction est propre hors les deux points ci-dessus. + +--- + +## 4. Consommateurs : `#include` et usages (hors fichiers déplacés) + +### 4.1. `Source/LifePlay` + +| Consommateur | `#include` | Symboles utilisés | Action | +|---|---|---|---| +| `LPPairSandbox.cpp` l. 5-7 | `LPAnimRedirect.h`, `LPPoseMeasure.h`, `LPPoseOrchestrator.h` | `LPAnimRedirect::ClipPath` (l. 1923, 1929, 2011, 3089, 3111) ; `LPPoseMeasure::Sample/Compare/KeyBones/LogReport/TFitReport/TransferPose/WriteRetargetPose/TPoses/TFitResult` (l. 1419-1520, 1923-1932) ; `LPPoseOrchestrator::Run/Restore/SampleAt` (l. 1450-1451) | includes inchangés (include path public du module), mais **exports à ajouter** (§4.3) | +| `LPPairSandbox.cpp` l. 8 | `LPSandboxConfig.h` | — | inchangé (reste dans `LifePlay`) | +| `LPDirectControlSubsystem.cpp` l. 10 | `LPAnimRedirect.h` | `IsGameTarget()` l. 880, `ClipPath()` l. 960 | include inchangé ; dépendance `LifePlay` → `LPAnimMeasure` en `Build.cs` | +| `LifePlay.cpp` | (nouveau) `LPAnimRedirect.h`, `LPSandboxConfig.h` | `SetUALRoot` dans `StartupModule` | à créer | + +### 4.2. `Source/LifePlayEditorTools` + +| Consommateur | `#include` | Symboles utilisés (tous déjà exportés) | +|---|---|---| +| `LPAnimTwistCommandlet.cpp` l. 10-11 | `LPPoseMeasure.h`, `LPSkeletonProfile.h` | `LPSkeletonProfile::Find` (l. 59), `LPPoseMeasure::Sample`, `TPoses` (l. 265-295) | +| `LPAnimEvalCommandlet.cpp` l. 9-10 | `LPPoseMeasure.h`, `LPSkeletonProfile.h` | `LPSkeletonProfile::Find` (l. 86, 171), `Sample`, `TransferPose`, `Compare`, `TPoses`, `WriteRetargetPose`, `TFitResult` | +| `LPAnimCatalogCommandlet.cpp` l. 8 | `LPPoseMeasure.h` | `RoleBone` (l. 31), `Sample` (l. 144) | +| `LPAnimChecks.h` l. 15 | `LPPoseMeasure.h` | `FLPPose` (type en signature publique) ; `LPAnimChecks.cpp` : `RoleBone` (l. 26), `Sample` (l. 270) | +| `LPAnimTwistCommandlet.h` l. 7 | — (commentaire) | mention `LPPoseMeasure::TransferPose` | + +`LifePlayEditorTools` n'inclut ni `LPPoseOrchestrator.h`, ni `LPAnimRedirect.h`, ni `LPSandboxConfig.h`. +`LPAnimEvalCommandlet` a son propre `ClipPath(ClipDir, ...)` (fonction locale) et n'utilise pas `LPAnimRedirect`. + +### 4.3. Exports requis (la frontière de module les rend obligatoires) + +Aujourd'hui exportés par `LIFEPLAY_API` (→ `LPANIMMEASURE_API`) : `LPPoseMeasure::{Sample, TPoses, TFitResult, TransferPose, WriteRetargetPose, Compare, RoleBone}`, `FLPSkeletonProfile` (struct), `LPSkeletonProfile::{Find, Identify}`. + +**À ajouter** (utilisés aujourd'hui par `LPPairSandbox.cpp` qui reste dans `LifePlay`, donc cross-module après extraction) : + +| Symbole | Utilisé par | +|---|---| +| `LPPoseMeasure::KeyBones` | `LPPairSandbox.cpp` l. 1419 | +| `LPPoseMeasure::LogReport` | `LPPairSandbox.cpp` l. 1477, 1483, 1493 | +| `LPPoseMeasure::TFitReport` | `LPPairSandbox.cpp` l. 1520 | +| `LPPoseOrchestrator::Run` | `LPPairSandbox.cpp` l. 1451 | +| `LPPoseOrchestrator::Restore` | `LPPairSandbox.cpp` l. 1451 | +| `LPPoseOrchestrator::SampleAt` | `LPPairSandbox.cpp` l. 1450 | +| `LPAnimRedirect::IsGameTarget`, `ClipPath`, `SetUALRoot`, `GetUALRoot` | `LPPairSandbox.cpp`, `LPDirectControlSubsystem.cpp`, `LifePlay.cpp` | + +Restent internes (non exportés, usage intra-module uniquement) : `LPPoseMeasure::{AlignToReference, ScorePose, SyncSample, FormatPassage, FormatKept, FormatSnapshot}` et `LPSkeletonProfile::All`. +Les structs de données (`FLPPose`, `FLPPoseMeasureReport`, `FLPPoseScore`, `FLPJointScore`, `FLPTFitSegment`, `FLPTFitAdjust`, `FLPTFitResult`, `FLPOrchestratorRound`, `FLPTPoseAdjust`) sont des structs sans UCLASS/USTRUCT ; elles n'ont besoin d'aucun export (types header-only, membres `TArray`/`FString` inline). `FLPSkeletonProfile` conserve son `API` car elle a des méthodes `.cpp` (`Bone`, `Chain`). + +--- + +## 5. Frontière de module proposée + +### 5.1. `Source/LPAnimMeasure/LPAnimMeasure.Build.cs` + +```csharp +using UnrealBuildTool; + +public class LPAnimMeasure : ModuleRules +{ + public LPAnimMeasure(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + // Structure plate (comme LifePlay) : exposer le dossier du module aux dependants. + PublicIncludePaths.Add(ModuleDirectory); + + PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" }); + + // LPSkeletonProfile : lecture des profils Tools/anim/profiles/*.json + PrivateDependencyModuleNames.Add("Json"); + } +} +``` + +Justification des dépendances (vérifiées par les `#include` Engine des 8 fichiers) : +- `Core`, `CoreUObject`, `Engine` : `UAnimSequence`, `USkeleton`, `FTransform`, `GConfig`, `FPaths`, `IFileManager`, `FFileHelper`. `Engine` public car `FLPPose` n'expose que des types Core (`FName`, `FTransform`, `FQuat`, `FString`), mais les en-têtes sont consommés par des modules qui ont déjà `Engine` ; le garder public est inoffensif. +- `Json` (+ `JsonUtilities` non requis) : `Dom/JsonObject.h`, `Serialization/JsonReader.h`, `JsonSerializer.h` dans `LPSkeletonProfile.cpp`. +- **Aucune** dépendance vers `LifePlay` ni `LifePlayEditorTools`. +- `Json` reste aussi dans `LifePlay.Build.cs` : il est ajouté en AP1 pour `LPSkeletonProfile` ; une fois le fichier déplacé, vérifier qu'aucun autre fichier de `LifePlay` n'utilise `Json` avant de le retirer (non traité ici, laisser en l'état est sans risque). + +### 5.2. Export API + +`LPANIMMEASURE_API` (généré par UBT pour le module `LPAnimMeasure`) remplace `LIFEPLAY_API` dans les 4 headers déplacés (pas de `LIFEPLAY_API` résiduel dans `LPPoseMeasure.h`, `LPSkeletonProfile.h`, `LPAnimRedirect.h`, `LPPoseOrchestrator.h`). + +### 5.3. `Source/LPAnimMeasure/LPAnimMeasure.cpp` + +```cpp +#include "LPAnimMeasure.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_MODULE(FDefaultModuleImpl, LPAnimMeasure); +DEFINE_LOG_CATEGORY(LogLPAnimMeasure); +``` + +(`IMPLEMENT_PRIMARY_GAME_MODULE` reste uniquement dans `LifePlay.cpp`.) + +### 5.4. `LifePlay.Build.cs` + +Ajouter : + +```csharp +PublicDependencyModuleNames.Add("LPAnimMeasure"); +``` + +Public (et non Private) car `LifePlayEditorTools` dépend de `LifePlay` publiquement et les headers `LPPoseMeasure.h` etc. sont inclus par ses commandlets ; une dépendance explicite dans `LifePlayEditorTools.Build.cs` est quand même ajoutée (5.5) pour ne pas dépendre de la transitivité. `PublicIncludePaths.Add(ModuleDirectory)` de `LifePlay` n'est plus nécessaire pour ces 4 headers mais reste requis pour `LPAssetRegistryHelpers.h` et autres. + +### 5.5. `LifePlayEditorTools.Build.cs` + +Ajouter dans `PublicDependencyModuleNames` (à côté de `"LifePlay"`) : `"LPAnimMeasure"` — public car `LPAnimChecks.h` expose `FLPPose` dans sa signature. + +### 5.6. `LifePlay.uproject` — entrée `Modules` + +Insérer **avant** `LifePlay` (même phase de chargement `Default`, ordre explicite) : + +```json +{ + "Name": "LPAnimMeasure", + "Type": "Runtime", + "LoadingPhase": "Default" +} +``` + +`Type: Runtime` car `LifePlay` (module runtime du jeu packagé) en dépend (`LPDirectControlSubsystem` utilise `LPAnimRedirect`). Les dépendances ne sont pas déclarées dans le `.uproject` (c'est `Build.cs` qui gère ; `AdditionalDependencies` n'est pas requis). + +### 5.7. Targets + +- `Source/LifePlay.Target.cs` : `ExtraModuleNames` → `{ "LifePlay", "LPAnimMeasure" }`. +- `Source/LifePlayEditor.Target.cs` : `ExtraModuleNames` → `{ "LifePlay", "LPAnimMeasure", "LifePlayEditorTools" }`. + +--- + +## 6. Graphe de dépendances + +### Avant + +``` +LifePlayEditorTools ──► LifePlay + ├─ LPPoseMeasure ──► LPSkeletonProfile + ├─ LPPoseOrchestrator ──► LPPoseMeasure, LPAnimRedirect + ├─ LPAnimRedirect ──► LPSandboxConfig + ├─ LPPairSandbox ──► Redirect, Measure, Orchestrator, SandboxConfig + └─ LPDirectControlSubsystem ──► LPAnimRedirect +``` + +### Après + +``` + ┌────────────────────────────────────────────┐ +LifePlayEditorTools ──┤ ▼ + │ │ LPAnimMeasure (Runtime, nouveau) + ▼ │ ├─ LPPoseMeasure ──► LPSkeletonProfile + LifePlay ────────┘ ├─ LPPoseOrchestrator ──► LPPoseMeasure + │ │ └─► LPAnimRedirect + ├─ LPSandboxConfig (reste, S3) ├─ LPAnimRedirect (UALRoot injectee) + ├─ LPPairSandbox ───► LPAnimMeasure (Redirect, Measure, Orchestrator) + LPSandboxConfig + ├─ LPDirectControlSubsystem ───► LPAnimMeasure (Redirect) + └─ LifePlay.cpp StartupModule ─► LPAnimRedirect::SetUALRoot(FLPSandboxConfig::Get().QuaterniusUALRoot) +``` + +Sens des flèches module : + +| De | Vers | Type | +|---|---|---| +| `LifePlay` | `LPAnimMeasure` | Public | +| `LifePlayEditorTools` | `LifePlay` | Public (existant) | +| `LifePlayEditorTools` | `LPAnimMeasure` | Public (nouveau) | +| `LPAnimMeasure` | `LifePlay` | **aucune** (acyclique) | +| `LPAnimMeasure` | `LifePlayEditorTools` | **aucune** | + +Couplage résiduel documenté : `LPAnimRedirect` ne lit plus `LPSandboxConfig` ; le flux va dans le sens `LifePlay` (config) → `LPAnimMeasure` (setter). Ordre de chargement : `LPAnimMeasure` avant `LifePlay` (garanti par la dépendance `Build.cs` et par l'ordre dans `.uproject`). + +--- + +## 7. Risques et points de vigilance + +| # | Risque | Mitigation | +|---|---|---| +| R1 | `LogLifePlay` → `LogLPAnimMeasure` : la catégorie change dans les logs `POSET`/`PROFIL` et le compteur `Tools/LogStats.ps1:51` | ajouter `LogLPAnimMeasure` au script | +| R2 | `SetUALRoot` non appelé avant premier `ClipPath` (commandlet sans module `LifePlay` initialisé) | `LifePlay` est toujours chargé (module primaire) avant les commandlets de `LifePlayEditorTools` ; défaut interne identique au défaut de la config | +| R3 | Symbole non exporté oublié → erreur de lien `LNK2019` dans `LPPairSandbox.cpp` | liste §4.3 (8 symboles existants à exporter + 2 nouveaux) | +| R4 | `LifePlayEditorTools` en `bUseUnity = false` : aucune modification | — | +| R5 | `LPAnimMeasure` en compilation unity : aucun namespace anonyme avec noms identiques entre les 3 `.cpp` ne doit entrer en collision (ces fichiers étaient déjà dans l'unity de `LifePlay`) | si collision, ajouter `bUseUnity = false` au `Build.cs` | +| R6 | Les `.uasset` ne référencent pas ces classes C++ (aucune `UCLASS`/`USTRUCT` dans les 8 fichiers) : pas de redirecteur de classe `CoreRedirects` à écrire | — | +| R7 | `Tools/RetargetQuaternius.py` cite `LPPoseMeasure::WriteRetargetPose`/`LPSkeletonProfile::Identify` uniquement en commentaires | aucune action | + +--- + +## 8. Ordre d'exécution suggéré (pour S2-T2) + +1. Créer `Source/LPAnimMeasure/` + `LPAnimMeasure.Build.cs`, `LPAnimMeasure.h/.cpp`. +2. `git mv` des 8 fichiers (conserve l'historique). +3. Remplacer `LIFEPLAY_API` → `LPANIMMEASURE_API`, `LifePlay.h` → `LPAnimMeasure.h`, `LogLifePlay` → `LogLPAnimMeasure` dans les fichiers déplacés ; ajouter les exports manquants (�4.3) ; retirer l'include de `LPSandboxConfig.h` dans `LPAnimRedirect.cpp` et ajouter `SetUALRoot/GetUALRoot`. +4. `LifePlay.cpp` : `FLifePlayModule::StartupModule` → `SetUALRoot(...)`. +5. Mettre à jour `LifePlay.Build.cs`, `LifePlayEditorTools.Build.cs`, `LifePlay.uproject`, les deux `*.Target.cs`. +6. Regénérer le projet (`GenerateProjectFiles`) et compiler Editor + Game ; lancer la validation (commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist`) pour contrôler l'absence de régression. diff --git a/Docs/Migration/etat/sandbox/S2/S2-IMPL-H.md b/Docs/Migration/etat/sandbox/S2/S2-IMPL-H.md new file mode 100644 index 0000000..b677a03 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S2/S2-IMPL-H.md @@ -0,0 +1,176 @@ +# S2-IMPL-H — Extraction du module `LPAnimMeasure` + +- **Date :** 2026-10-06 +- **Fiche :** S2-T2 | **Agent :** A-IMPL-H | **Rôle :** Implémentation +- **Modèle prévu :** `claude-opus-5-thinking-high` (M3) +- **Référence suivie :** `S2/S2-CART.md` (décision §1 **(b)**, listes §2 à §5, ordre §8) +- **Statut :** compilé (`LifePlayEditor` **et** `LifePlay`, Win64 Development), **non committé** +- **Hors périmètre respecté :** aucune UI Slate de la sandbox touchée, `LPSandboxConfig.*` et `LPPairSandbox.*` restent dans `LifePlay`. + +--- + +## 1. Fichiers créés + +| Fichier | Contenu | +|---|---| +| `Source/LPAnimMeasure/LPAnimMeasure.Build.cs` | `PublicIncludePaths.Add(ModuleDirectory)` (structure plate) ; `PublicDependencyModuleNames = Core, CoreUObject, Engine` ; `PrivateDependencyModuleNames = Json` ; aucune dépendance vers `LifePlay` / `LifePlayEditorTools` | +| `Source/LPAnimMeasure/LPAnimMeasure.h` | `LPANIMMEASURE_API DECLARE_LOG_CATEGORY_EXTERN(LogLPAnimMeasure, Log, All)` | +| `Source/LPAnimMeasure/LPAnimMeasure.cpp` | `IMPLEMENT_MODULE(FDefaultModuleImpl, LPAnimMeasure)` + `DEFINE_LOG_CATEGORY(LogLPAnimMeasure)` | +| `Docs/Migration/etat/sandbox/S2/S2-IMPL-H.md` | cette fiche | + +## 2. Fichiers déplacés (`Source/LifePlay/` → `Source/LPAnimMeasure/`) + +Les 8 fichiers de `S2-CART.md` §2, déplacés tels quels (pas de `Public/`/`Private/`) : +`LPPoseMeasure.h/.cpp`, `LPSkeletonProfile.h/.cpp`, `LPPoseOrchestrator.h/.cpp`, `LPAnimRedirect.h/.cpp`. + +**`git mv` non utilisable** : les 8 fichiers n'étaient pas encore suivis par git (`fatal: not under version control`) — ils apparaissent en `??` dans le statut, comme la plupart des fichiers de la sandbox. Le déplacement est un `Move-Item` ; il n'y a aucun historique git à préserver pour ces fichiers. Les anciens chemins n'existent plus sous `Source/LifePlay/`. + +### Modifications appliquées aux fichiers déplacés + +| Fichier | Modification | +|---|---| +| `LPPoseMeasure.h` | 6 `LIFEPLAY_API` → `LPANIMMEASURE_API` ; **3 exports ajoutés** (`TFitReport`, `LogReport`, `KeyBones`) ; commentaire d'en-tête du namespace actualisé (`LPANIMMEASURE_API`, mention de la sandbox) | +| `LPPoseMeasure.cpp` | `#include "LifePlay.h"` → `#include "LPAnimMeasure.h"` ; **12** `UE_LOG(LogLifePlay…)` → `LogLPAnimMeasure` | +| `LPSkeletonProfile.h` | 3 `LIFEPLAY_API` → `LPANIMMEASURE_API` (struct `FLPSkeletonProfile`, `Find`, `Identify`) | +| `LPSkeletonProfile.cpp` | `#include "LifePlay.h"` → `#include "LPAnimMeasure.h"` ; **4** `LogLifePlay` → `LogLPAnimMeasure` | +| `LPPoseOrchestrator.h` | **3 exports ajoutés** (`Run`, `Restore`, `SampleAt`) | +| `LPPoseOrchestrator.cpp` | `#include "LifePlay.h"` → `#include "LPAnimMeasure.h"` ; **4** `LogLifePlay` → `LogLPAnimMeasure` | +| `LPAnimRedirect.h` | `LPANIMMEASURE_API` sur `IsGameTarget`/`ClipPath` ; **2 fonctions ajoutées** : `SetUALRoot`, `GetUALRoot` | +| `LPAnimRedirect.cpp` | `#include "LPSandboxConfig.h"` **retiré** ; racine interne `static FString UALRoot()` initialisée à `TEXT("/Game/Quaternius/UAL")` ; `ClipPath` utilise `GetUALRoot()` | + +Vérification : plus aucune occurrence de `LIFEPLAY_API`, `LogLifePlay`, `#include "LifePlay.h"` ni `#include "LPSandboxConfig.h"` dans `Source/LPAnimMeasure/` (seules subsistent 3 mentions en commentaire, qui documentent la rupture de couplage). + +## 3. Fichiers modifiés hors module + +| Fichier | Modification | +|---|---| +| `LifePlay.uproject` | entrée `{ "Name": "LPAnimMeasure", "Type": "Runtime", "LoadingPhase": "Default" }` insérée **avant** `LifePlay` | +| `Source/LifePlay.Target.cs` | `ExtraModuleNames` → `{ "LifePlay", "LPAnimMeasure" }` | +| `Source/LifePlayEditor.Target.cs` | `ExtraModuleNames` → `{ "LifePlay", "LPAnimMeasure", "LifePlayEditorTools" }` | +| `Source/LifePlay/LifePlay.Build.cs` | `PublicDependencyModuleNames.Add("LPAnimMeasure")` | +| `Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs` | `"LPAnimMeasure"` ajouté à `PublicDependencyModuleNames`, à côté de `"LifePlay"` | +| `Source/LifePlay/LifePlay.cpp` | `FDefaultGameModuleImpl` → `FLifePlayModule` (§4) | +| `Source/LifePlay/LPInteractionSandbox.cpp` | `#include "Engine/TextureCube.h"` ajouté — effet de bord de la recomposition des blobs unity (§7) | +| `Tools/LogStats.ps1` | compteur `LogLPAnimMeasure` ajouté à côté de `LogLifePlay` (mitigation **R1** de `S2-CART.md` §7) | + +Non touchés : `LPPairSandbox.h/.cpp`, `LPDirectControlSubsystem.h/.cpp`, `LPSandboxConfig.h/.cpp`, les 4 fichiers de `LifePlayEditorTools` consommateurs. Leurs `#include "LPPoseMeasure.h"` etc. restent valides grâce à `PublicIncludePaths.Add(ModuleDirectory)` du nouveau module. `Json` reste déclaré dans `LifePlay.Build.cs` (retrait non traité, sans risque — cf. `S2-CART.md` §5.1). + +## 4. Câblage de la racine UAL (`LifePlay.cpp` existe) + +`Source/LifePlay/LifePlay.cpp` existait bien (`IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, LifePlay, "LifePlay")`). La solution préférée de `S2-CART.md` §1 a donc été appliquée, sans recourir à l'alternative `BeginPlay` : + +```cpp +class FLifePlayModule : public FDefaultGameModuleImpl +{ +public: + virtual void StartupModule() override + { + FDefaultGameModuleImpl::StartupModule(); + LPAnimRedirect::SetUALRoot(FLPSandboxConfig::Get().QuaterniusUALRoot); + } +}; + +IMPLEMENT_PRIMARY_GAME_MODULE( FLifePlayModule, LifePlay, "LifePlay" ); +``` + +Conséquences : + +- Le flux de configuration va dans le sens `LifePlay` (propriétaire de `LPSandboxConfig`) → `LPAnimMeasure` (setter). Aucun cycle de modules. +- `Config/LPSandbox.ini` (clé `Sandbox.Roots/QuaterniusUALRoot`, contrat `S1-SPEC.md`) est pris en compte pour **tous** les consommateurs — sandbox, `ULPDirectControlSubsystem`, commandlets — sans exiger que la sandbox soit lancée d'abord. +- `SetUALRoot` ignore une chaîne vide : un ini incomplet ne peut pas effacer la racine. +- Défaut interne de `LPAnimRedirect` identique au défaut de `FLPSandboxConfig::QuaterniusUALRoot` (`/Game/Quaternius/UAL`) : si `SetUALRoot` n'est jamais appelé (test unitaire, outil chargeant `LPAnimMeasure` seul), le comportement reste celui d'avant l'extraction. +- `S1-CART.md` doit être mis à jour : `LPAnimRedirect.cpp` n'est plus un consommateur direct de la clé ; `LifePlay.cpp` le devient. + +## 5. API publique du module `LPAnimMeasure` + +Les structures de données sont des `struct` C++ simples (aucune `UCLASS`/`USTRUCT`, aucune `UFUNCTION`) : header-only, elles ne nécessitent pas d'export, sauf `FLPSkeletonProfile` qui a des méthodes définies en `.cpp`. + +### 5.1. `LPAnimMeasure.h` + +| Symbole | Export | +|---|---| +| `LogLPAnimMeasure` (catégorie de log) | `LPANIMMEASURE_API` | + +### 5.2. `LPAnimRedirect.h` + +| Symbole | Export | Statut | +|---|---|---| +| `bool LPAnimRedirect::IsGameTarget()` | `LPANIMMEASURE_API` | export ajouté | +| `FString LPAnimRedirect::ClipPath(const FString& AssetName)` | `LPANIMMEASURE_API` | export ajouté | +| `void LPAnimRedirect::SetUALRoot(const FString& Root)` | `LPANIMMEASURE_API` | **nouveau** | +| `const FString& LPAnimRedirect::GetUALRoot()` | `LPANIMMEASURE_API` | **nouveau** | + +### 5.3. `LPPoseMeasure.h` + +Types publics non exportés (header-only) : `FLPPose`, `FLPPoseMeasureReport`, `FLPJointScore`, `FLPPoseScore`, `FLPTFitSegment`, `FLPTFitAdjust`, `FLPTFitResult`. + +| Symbole | Export | Statut | +|---|---|---| +| `bool Sample(const FString& AnimPath, float Time, FLPPose& Out)` | `LPANIMMEASURE_API` | existant | +| `bool TPoses(const FLPPose& Reference, const FLPPose& Target, FLPPose& OutRefT, FLPPose& OutBodyT)` | `LPANIMMEASURE_API` | existant | +| `const TArray& TFitReport()` | `LPANIMMEASURE_API` | **export ajouté** (`LPPairSandbox.cpp`) | +| `bool TFitResult(const FLPPose& Reference, const FLPPose& Target, FLPTFitResult& Out)` | `LPANIMMEASURE_API` | existant | +| `bool TransferPose(const FLPPose& Reference, const FLPPose& Template, FLPPose& Out)` | `LPANIMMEASURE_API` | existant | +| `bool WriteRetargetPose(const FLPPose& Reference, const FLPPose& Template, const FString& Path)` | `LPANIMMEASURE_API` | existant | +| `FLPPoseMeasureReport Compare(const TCHAR* Body, const TCHAR* Clip, const FLPPose& Reference, const FLPPose& Target)` | `LPANIMMEASURE_API` | existant | +| `void LogReport(const FLPPoseMeasureReport& Report)` | `LPANIMMEASURE_API` | **export ajouté** (`LPPairSandbox.cpp`) | +| `FName RoleBone(const FLPPose& Pose, const TCHAR* Role, int32 Index = 0)` | `LPANIMMEASURE_API` | existant | +| `TArray KeyBones(const FLPPose& Pose)` | `LPANIMMEASURE_API` | **export ajouté** (`LPPairSandbox.cpp`) | + +Restent internes au module (non exportés) : `AlignToReference`, `ScorePose`, `SyncSample`, `FormatPassage`, `FormatKept`, `FormatSnapshot`. + +### 5.4. `LPSkeletonProfile.h` + +| Symbole | Export | Statut | +|---|---|---| +| `struct FLPTPoseAdjust` | — (header-only) | existant | +| `struct FLPSkeletonProfile` (+ `Bone`, `Chain`, `Has`) | `LPANIMMEASURE_API` | existant | +| `const FLPSkeletonProfile* Find(const FString& Id)` | `LPANIMMEASURE_API` | existant | +| `const FLPSkeletonProfile* Identify(const TArray& BoneNames, float* OutScore = nullptr)` | `LPANIMMEASURE_API` | existant | + +Reste interne : `LPSkeletonProfile::All()`. + +### 5.5. `LPPoseOrchestrator.h` + +Type public non exporté (header-only) : `FLPOrchestratorRound`. + +| Symbole | Export | Statut | +|---|---|---| +| `FLPOrchestratorRound Run(const TCHAR* Clip, bool bOneSubject = false)` | `LPANIMMEASURE_API` | **export ajouté** (`LPPairSandbox.cpp`) | +| `FLPOrchestratorRound Restore(const TCHAR* Clip)` | `LPANIMMEASURE_API` | **export ajouté** (`LPPairSandbox.cpp`) | +| `FLPOrchestratorRound SampleAt(const TCHAR* Clip, float Fraction)` | `LPANIMMEASURE_API` | **export ajouté** (`LPPairSandbox.cpp`) | + +### 5.6. Contrôle de couverture + +Tous les symboles `LPPoseMeasure::`, `LPSkeletonProfile::`, `LPPoseOrchestrator::`, `LPAnimRedirect::` référencés depuis `Source/LifePlay/` et `Source/LifePlayEditorTools/` sont exportés (relevé exhaustif : `ClipPath`, `Compare`, `Find`, `IsGameTarget`, `KeyBones`, `LogReport`, `Restore`, `RoleBone`, `Run`, `Sample`, `SampleAt`, `SetUALRoot`, `TFitReport`, `TFitResult`, `TPoses`, `TransferPose`, `WriteRetargetPose`). Mitigation **R3** de `S2-CART.md` §7 couverte. + +## 6. Graphe de dépendances obtenu + +``` +LifePlayEditorTools ──► LifePlay ──► LPAnimMeasure + └──────────────────────────────┘ (public, pour FLPPose dans LPAnimChecks.h) + +LPAnimMeasure ──► Core, CoreUObject, Engine (public), Json (privé) +LPAnimMeasure ──► LifePlay : AUCUNE (acyclique) +``` + +Ordre de chargement `LPAnimMeasure` avant `LifePlay` garanti deux fois : dépendance `Build.cs` et position dans `LifePlay.uproject`. + +## 7. Validation + +| Target | Résultat | Journal | +|---|---|---| +| `LifePlayEditor Win64 Development` | **succès** — `UnrealEditor-LPAnimMeasure.lib/.dll` produits, `LifePlay` et `LifePlayEditorTools` relinkés sans `LNK2019` | `Saved/Logs/portage/S2-T2-build.log` | +| `LifePlay Win64 Development` (Game) | **succès** après le correctif ci-dessous | `Saved/Logs/portage/S2-T2-build-game.log` | + +### Effet de bord rencontré (famille du risque **R5**) + +Première passe du target Game : `error C2027: utilisation du type non défini 'UTextureCube'` dans `LPInteractionSandbox.cpp:123` (`LoadObject`). Ce fichier n'a jamais inclus `Engine/TextureCube.h` ; il dépendait de l'include transitif apporté par un voisin du **même blob unity** de `LifePlay` — très probablement `LPPairSandbox.cpp`, qui inclut `Engine/TextureCube.h` explicitement. La sortie des 8 fichiers du module a changé la composition des blobs et découvert ce défaut IWYU préexistant (le target Editor, dont le découpage unity diffère, ne le voyait pas). + +Correctif : ajout de `#include "Engine/TextureCube.h"` dans `LPInteractionSandbox.cpp`. Aucune collision de namespace anonyme n'est survenue dans `LPAnimMeasure` ; `bUseUnity = false` n'a **pas** été nécessaire. + +## 8. Reste à faire + +- Validation fonctionnelle (commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist`, sandbox) : fiche S2-T3 / `S2-VAL.md`. +- Mise à jour de `S1-CART.md` (ligne `LPAnimRedirect.cpp:23` → consommation via `LifePlay.cpp` / `SetUALRoot`). +- Aucun commit effectué (consigne de la fiche). diff --git a/Docs/Migration/etat/sandbox/S2/S2-VAL.md b/Docs/Migration/etat/sandbox/S2/S2-VAL.md new file mode 100644 index 0000000..3438a6d --- /dev/null +++ b/Docs/Migration/etat/sandbox/S2/S2-VAL.md @@ -0,0 +1,79 @@ +# S2-VAL - Validation du lot S2 (module `LPAnimMeasure`) + +- **Date :** 2026-10-06 +- **Fiche :** S2-T4a / S2-T4b | **Rôle :** A-VAL puis prep DOC +- **Modèle prévu :** `claude-sonnet-5-5-high` (M2) | **Modèle utilisé :** `claude-sonnet-5-5-high` (M2) +- **Références :** `S2-CART.md`, `S2-IMPL-H.md` + +## Verdict : **PASS** (statique et runtime Mesure) + +| # | Contrôle | Résultat | +|---|---|---| +| 1 | `Source/LPAnimMeasure/` : 8 fichiers déplacés + `Build.cs` + module (`.h`/`.cpp`) | **PASS** (11 fichiers) | +| 2 | `Source/LifePlay/` sans `LPPoseMeasure*`, `LPSkeletonProfile*`, `LPPoseOrchestrator*`, `LPAnimRedirect*` | **PASS** (0 fichier, recherche récursive dans `Source/`) | +| 3 | `LifePlay.uproject` liste `LPAnimMeasure` | **PASS** (Runtime, `Default`, avant `LifePlay`) | +| 4 | `LifePlay.Build.cs` et `LifePlayEditorTools.Build.cs` dépendent de `LPAnimMeasure` | **PASS** (les deux en `PublicDependencyModuleNames`) | +| 5 | Binaires récents (DLL `LPAnimMeasure`) après compilation S2-T2 | **PASS** | +| 6 | Runtime du bat Mesure | **PASS** (lancé, 75 s, journal `LogLPAnimMeasure` actif, 0 erreur de module ou de lien) | + +## 1. Contenu de `Source/LPAnimMeasure/` + +| Fichier | Rôle | +|---|---| +| `LPAnimMeasure.Build.cs` | règles du module (`Core`, `CoreUObject`, `Engine` publics ; `Json` privé ; aucune dépendance vers `LifePlay`) | +| `LPAnimMeasure.h` / `.cpp` | `IMPLEMENT_MODULE(FDefaultModuleImpl, LPAnimMeasure)` ; catégorie `LogLPAnimMeasure` (`LPANIMMEASURE_API`) | +| `LPPoseMeasure.h` / `.cpp` | 8 fichiers déplacés (CART §2) | +| `LPSkeletonProfile.h` / `.cpp` | | +| `LPPoseOrchestrator.h` / `.cpp` | | +| `LPAnimRedirect.h` / `.cpp` | `SetUALRoot` / `GetUALRoot` ajoutés | + +Aucune occurrence de `LIFEPLAY_API`, de `#include "LifePlay.h"` ou de `LPSandboxConfig` dans ces sources (hors commentaires) : le module ne dépend pas de `LifePlay`. + +## 2. Reliquats dans `Source/LifePlay/` + +Recherche des noms `LPPoseMeasure*`, `LPSkeletonProfile*`, `LPPoseOrchestrator*`, `LPAnimRedirect*` sur tous les fichiers de `Source/` : seuls ceux de `Source/LPAnimMeasure/` existent. Les consommateurs restants (`LPPairSandbox.cpp`, `LPDirectControlSubsystem.cpp`, `LifePlay.cpp`, 4 fichiers de `LifePlayEditorTools`) incluent les en-têtes par `PublicIncludePaths` du nouveau module. + +## 3. Déclarations de module + +| Fichier | Ligne | Contenu | +|---|---:|---| +| `LifePlay.uproject` | `Modules[0]` | `{ "Name": "LPAnimMeasure", "Type": "Runtime", "LoadingPhase": "Default" }`, avant `LifePlay` et `LifePlayEditorTools` | +| `Source/LifePlay/LifePlay.Build.cs` | 46 | `PublicDependencyModuleNames.Add("LPAnimMeasure")` | +| `Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs` | 22 | `"LPAnimMeasure"` dans `PublicDependencyModuleNames` | +| `Source/LifePlay.Target.cs` | 15 | `ExtraModuleNames` : `LifePlay`, `LPAnimMeasure` | +| `Source/LifePlayEditor.Target.cs` | 16 | `ExtraModuleNames` : `LifePlay`, `LPAnimMeasure`, `LifePlayEditorTools` | + +## 4. Binaires (Win64, Development) + +| Fichier | Date | +|---|---| +| dernière source modifiée de `Source/LPAnimMeasure/` (`LPAnimRedirect.cpp`) | 2026-10-06 18:31:38 | +| `Binaries/Win64/UnrealEditor-LPAnimMeasure.dll` (520 192 o) / `.pdb` | 2026-10-06 18:34:00 | +| `UnrealEditor-LifePlay.dll` / `UnrealEditor-LifePlayEditorTools.dll` | 2026-10-06 18:34:26 | +| `UnrealEditor.modules` (entrée `LPAnimMeasure`) | 2026-10-06 18:34:27 | +| `LifePlay.exe` (cible Game, module lié en monolithique) | 2026-10-06 18:37:17 | + +La DLL est postérieure à la dernière source du module : binaire à jour. Différence avec S1-VAL : la limite « DLL plus ancienne que les sources » n'existe plus. + +## 5. Runtime : bat Mesure + +Méthode : équivalent de `Tools/debug/LifePlay_measure_sandbox.bat` (même ligne de commande `UnrealEditor.exe LifePlay.uproject /Engine/Maps/Entry?game=/Script/LifePlay.LPPairSandboxGameMode -game -windowed ...`), avec `Saved/sandbox_mode.txt` = `Mesure` pendant l'essai, remis à `Pair` ensuite. Le processus a été arrêté après environ 90 s. Journal conservé : `Saved/Logs/S2-VAL-runtime.log`. + +| Constat | Journal | +|---|---| +| Module chargé, aucun `Unable to load module`, `LNK2`, `Fatal error` ni `Assertion failed` | 0 occurrence | +| Catégorie `LogLPAnimMeasure` active | 269 lignes (profils, `POSET`, `MESURE`, `ORCHESTRE`) | +| Profils chargés | `genesis8` (37 rôles), `ue4_mannequin` (31 rôles) ; squelettes `UAL1_Standard_Skeleton` -> `ue4_mannequin` et `bridget_gp_Skeleton` -> `genesis8` à 100 % | +| Chaîne de mesure (Sample / Compare / Transfer / orchestrateur) | `MESURE Bridget Idle_Loop pose 0.00% (bassin) proportion 15.2% (rCollar) OK` ; `ORCHESTRE Idle_Loop pondere 8 ...` (94 lignes `MESURE`/`ORCHESTRE`) | +| Racine UAL injectée (`SetUALRoot` depuis `FLifePlayModule::StartupModule`) | `Sandbox Quaternius : corps /Game/Quaternius/UAL/UAL1_Standard...`, 43 animations | +| Sorties produites | `Saved/Measurement/history.csv`, `round_0929_Idle_Loop.csv`, `round_0930_A_TPose.csv`, `retarget_pose_*.txt` | +| `LogLifePlay: Error` | 1 : `Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable sur GameInstance_0` (déjà présent dans un journal antérieur au lot S2, sans lien avec `LPAnimMeasure`) | + +Effets de bord de l'essai : 2 manches ajoutées à `Saved/Measurement` (hors dépôt) ; `Saved/Logs/LifePlay.log` renouvelé. La capture de la boucle automatique et le contrôle visuel de la scène n'ont pas été faits. + +## Limites + +- Contrôle visuel de la sandbox Mesure (vues, captures `shots/`) non fait : seul le journal sert de preuve. +- Les commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist` n'ont pas été relancés (prévus par `S2-IMPL-H.md` §8 ; hors de cette fiche). +- `S1-CART.md` n'a pas encore reçu la mise à jour demandée par `S2-IMPL-H.md` (consommation de `QuaterniusUALRoot` via `LifePlay.cpp` / `SetUALRoot`) : reste à faire. +- Aucun commit effectué. diff --git a/Docs/Migration/etat/sandbox/S3/S3-IMPL-H.md b/Docs/Migration/etat/sandbox/S3/S3-IMPL-H.md new file mode 100644 index 0000000..4c809a6 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S3/S3-IMPL-H.md @@ -0,0 +1,87 @@ +# S3-IMPL-H — Transfert du code sandbox vers le repo addon + +- **Date :** 2026-10-06 +- **Fiche :** S3-T2 | **Agent :** A-IMPL-H | **Rôle :** Implémentation +- **Modèle prévu :** `claude-opus-5-thinking-high` (M3) +- **Amont :** `S3/S3-INT-scaffold.md` (S3-T1, commit addon `4938abb`) +- **Statut :** **compilé** — cibles `LPSandboxEditor` **et** `LPSandbox` (Win64 Development), 0 erreur / 0 avertissement ; commit dans le repo addon uniquement +- **Fiche détaillée (côté addon) :** `D:\Game\LifePlay-Sandbox\Docs\S3-move.md` + +> **Le repo jeu n'est pas modifié par cette fiche, hors ce document.** Les fichiers transférés sont toujours en place sous `Source/` — leur suppression est le lot **S3-T3**. Aucun submodule n'est branché. + +--- + +## 1. Ce qui a été copié (jeu → addon) + +| Source (repo jeu) | Destination (repo addon) | Modifications | +|---|---|---| +| `Source/LPAnimMeasure/` (10 fichiers) | `Plugins/LPSandbox/Source/LPAnimMeasure/` | **aucune** — identiques à l'octet près | +| `Source/LifePlay/LPPairSandbox.h` / `.cpp` | `Plugins/LPSandbox/Source/LPSandbox/` | renommages §2 | +| `Source/LifePlay/LPInteractionSandbox.h` / `.cpp` | `Plugins/LPSandbox/Source/LPSandbox/` | renommages §2 | +| `Source/LifePlay/LPSandboxConfig.h` / `.cpp` | `Plugins/LPSandbox/Source/LPSandbox/` | renommages §2 | +| `Config/LPSandbox.ini` | `Config/LPSandbox.ini` | **aucune** | + +`LPAnimMeasure.Build.cs` du scaffold a été conservé (déjà conforme : `PublicIncludePaths.Add(ModuleDirectory)`, `Public = Core, CoreUObject, Engine`, `Private = Json`), seul son commentaire d'en-tête a été actualisé. Les deux autres stubs (`LPAnimMeasure.h/.cpp`) ont été remplacés par les versions du jeu. + +## 2. Renommages appliqués (module `LPSandbox` seulement) + +| Renommage | Occurrences | +|---|---| +| `LIFEPLAY_API` → `LPSANDBOX_API` | 7 | +| `#include "LifePlay.h"` → `#include "LPSandbox.h"` | 2 | +| `LogLifePlay` → `LogLPSandbox` | 49 (`LPPairSandbox.cpp` 43, `LPInteractionSandbox.cpp` 6) | + +La catégorie `LogLPSandbox` existait déjà dans le scaffold (`LPSandbox.h` / `.cpp`). `LPAnimMeasure` n'a eu besoin d'aucun renommage : le lot S2 avait déjà coupé son couplage. + +**Fidélité vérifiée** : en réappliquant ces trois renommages en sens inverse, les 6 fichiers de `LPSandbox` redonnent exactement les octets des originaux du jeu (fins de ligne et caractères non-ASCII des commentaires inclus). Aucune divergence fonctionnelle n'a été introduite : toute dérive future entre les deux copies sera donc imputable à S3-T3 ou au-delà. + +## 3. Couplage résiduel vers `LifePlay` : nul + +`Plugins/LPSandbox/Source/` ne contient plus aucun `LIFEPLAY_API`, `LogLifePlay` ni `#include "LifePlay.h"`. Il reste 2 mentions du mot `LifePlay`, dans des commentaires citant les lanceurs `Tools/debug/LifePlay_*_sandbox.bat`. + +Le relevé des identifiants `ULP*` / `ALP*` / `FLP*` référencés par les fichiers transférés ne donne que les classes de la sandbox elle-même et trois types de `LPAnimMeasure` (`FLPPose`, `FLPPoseMeasureReport`, `FLPOrchestratorRound`). Le `Person` du jeu est résolu par chemin d'asset (`/Game/BPs/Person.Person_C`), jamais par include : rien de C++ ne reste à casser. + +## 4. Dépendances déclarées côté addon + +``` +LPSandbox Public : Core, CoreUObject, Engine, InputCore, SlateCore, LPAnimMeasure + Private : Slate, ImageWrapper, AssetRegistry, Json +LPAnimMeasure Public : Core, CoreUObject, Engine Private : Json +``` + +`InputCore` et `SlateCore` sont publics parce que les headers exposent respectivement un `FKey` et des `TSharedPtr`. `LPAnimMeasure` est public par parité avec `LifePlay.Build.cs`. Non reprises de `LifePlay.Build.cs`, inutiles à la sandbox : `NavigationSystem`, `EngineSettings`, `Projects`, `UMG`, `Chaos`, `ApplicationCore`. + +## 5. Cibles du projet conteneur + +Le scaffold S3-T1 n'avait pas de `Source/`, donc aucun build CLI possible (point d'attention §5 de `S3-INT-scaffold.md`). Ajouté côté addon : `Source/LPSandboxEditor.Target.cs`, `Source/LPSandbox.Target.cs` et un module de jeu primaire vide `Source/LPSandboxApp/`. + +Mesure à l'appui de ce dernier choix : la cible **Editor** avec `ExtraModuleNames = []` compile et lie correctement, mais la cible **Game** échoue en `LNK1120: 9 externes non résolus` (`FMemory_Malloc`, `FMemory_Realloc`, `FMemory_Free`, `GNameBlocksDebug`, `GObjectArrayForDebugVisualizers`, `GDebuggingState`) faute de module de jeu primaire dans le binaire monolithique. `LPSandboxApp` (`IMPLEMENT_PRIMARY_GAME_MODULE`) est donc le module factice que la fiche autorisait ; les deux cibles le référencent. + +## 6. Validation + +| Cible | Résultat | +|---|---| +| `LPSandboxEditor Win64 Development` | succès — `UnrealEditor-LPSandbox.dll`, `UnrealEditor-LPAnimMeasure.dll`, `UnrealEditor-LPSandboxApp.dll` | +| `LPSandbox Win64 Development` (Game) | succès — `Binaries/Win64/LPSandbox.exe` | + +L'effet de bord unity rencontré en S2 (`UTextureCube` dans `LPInteractionSandbox.cpp`) ne s'est pas reproduit : le correctif fait partie des sources transférées. + +Le jeu n'a pas été recompilé : il est inchangé, donc son état de build reste celui de S2-T2. + +## 7. Conséquences pour S3-T3 (suppression côté jeu) + +Points à traiter lors de la suppression, repérés pendant le transfert : + +- `Source/LifePlay/LifePlay.cpp` — `FLifePlayModule::StartupModule()` appelle `LPAnimRedirect::SetUALRoot(FLPSandboxConfig::Get().QuaterniusUALRoot)` : dépend **à la fois** de `LPAnimMeasure` et de `LPSandboxConfig`. Retirer les deux casse ce câblage (ajouté en S2-T2, cf. `S2-IMPL-H.md` §4). +- `Source/LifePlayEditorTools/` — les commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist` et `LPAnimChecks.h` consomment `LPAnimMeasure` (`PublicDependencyModuleNames`). `LPAnimMeasure` ne peut donc pas disparaître du jeu sans décision sur ces commandlets. +- `Source/LifePlay/LPDirectControlSubsystem.cpp:879` — détecte la sandbox par **nom de classe en chaîne** : `Mode->GetClass()->GetName().Contains(TEXT("LPPairSandbox"))`. Le test survit au changement de module (le nom de classe est inchangé), mais il devient du code mort dans le jeu une fois la sandbox retirée. Ce fichier est aussi consommateur de `LPAnimRedirect` (relevé S2). +- Entrées à nettoyer si suppression : `LifePlay.uproject` (module `LPAnimMeasure`), `Source/LifePlay.Target.cs` et `Source/LifePlayEditor.Target.cs` (`ExtraModuleNames`), `Source/LifePlay/LifePlay.Build.cs` (`PublicDependencyModuleNames.Add("LPAnimMeasure")`), `Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs`. +- `Tools/debug/LifePlay_*_sandbox.bat` — les lanceurs passent le `GameMode` par URL : `?game=/Script/LifePlay.LPPairSandboxGameMode`. Côté addon, le chemin devient **`/Script/LPSandbox.LPPairSandboxGameMode`** (idem `LPInteractionGameMode`) et le `.uproject` change. Il n'y a aucun autre point d'entrée : rien dans `Config/*.ini` ni dans le code du jeu n'instancie ces `GameMode`. +- `Config/LPSandbox.ini` existe désormais en double ; le contrat `S1-SPEC.md` reste la référence des deux copies. + +## 8. Commits + +| Repo | Action | +|---|---| +| `LifePlay-Sandbox` (addon) | 1 commit (`S3-T2: transfert du code sandbox depuis le jeu`) | +| `LifePlay` (jeu) | **aucun** — seul ce document est ajouté, non committé (consigne de la fiche) | diff --git a/Docs/Migration/etat/sandbox/S3/S3-IMPL-L.md b/Docs/Migration/etat/sandbox/S3/S3-IMPL-L.md new file mode 100644 index 0000000..27f67fe --- /dev/null +++ b/Docs/Migration/etat/sandbox/S3/S3-IMPL-L.md @@ -0,0 +1,68 @@ +# S3-IMPL-L — Purge sandbox côté jeu + branchement plugin LPSandbox + +- **Date :** 2026-10-06 +- **Fiche :** S3-T3 | **Agent :** A-IMPL-L | **Modèle :** composer-2.5-fast (M0) +- **Amont :** `S3/S3-IMPL-H.md` (transfert addon S3-T2) +- **Statut :** implémenté — compilation UBT non relancée dans cette fiche + +--- + +## 1. Lanceurs sandbox (`Tools/debug/*_sandbox.bat`) + +| Fichier | Changement | +|---|---| +| `LifePlay_pair_sandbox.bat` | `?game=/Script/LPSandbox.LPPairSandboxGameMode` | +| `LifePlay_interaction_sandbox.bat` | idem | +| `LifePlay_measure_sandbox.bat` | idem | + +Entry map (`/Engine/Maps/Entry`), stub `GameInstanceClass=/Script/Engine.GameInstance` et écriture `Saved/sandbox_mode.txt` (Mesure) inchangés. + +`Tools/BuildMod.ps1 -Target sandbox -Open` délègue toujours à `LifePlay_interaction_sandbox.bat` : aucune modification du script PowerShell. + +## 2. Jonctions (temporaires, avant submodule T4) + +| Jonction | Cible | Résultat | +|---|---|---| +| `D:\Game\LifePlay-Sandbox\Content` → `D:\Game\LifePlay-master 5.5\Content` | **créée** | addon lit le Content du jeu | +| `d:\Game\LifePlay-master 5.5\Plugins\LPSandbox` → `D:\Game\LifePlay-Sandbox\Plugins\LPSandbox` | **créée** | jeu charge le plugin depuis l’addon | + +Si une jonction existait déjà au chemin, `mklink /J` échoue (fichier/répertoire existant) — documenter et ne pas écraser. + +## 3. Fichiers supprimés du repo jeu + +- `Source/LifePlay/LPPairSandbox.h` / `.cpp` +- `Source/LifePlay/LPInteractionSandbox.h` / `.cpp` +- `Source/LifePlay/LPSandboxConfig.h` / `.cpp` +- `Source/LPAnimMeasure/` (module entier, 11 fichiers) + +**Conservé :** `Config/LPSandbox.ini` (contrat S1, lu par le plugin sur le projet hôte). + +## 4. `LifePlay.uproject` + +- Retiré : module `LPAnimMeasure` dans `Modules[]`. +- Ajouté : plugin `{ "Name": "LPSandbox", "Enabled": true }` (chemin physique = jonction §2 ; T4 = submodule). + +## 5. Cibles et modules + +| Fichier | Action | +|---|---| +| `Source/LifePlay.Target.cs` | `ExtraModuleNames` : `LifePlay` seul | +| `Source/LifePlayEditor.Target.cs` | retrait de `LPAnimMeasure` | +| `Source/LifePlay/LifePlay.Build.cs` | **`PublicDependencyModuleNames.Add("LPAnimMeasure")` conservé** (module fourni par le plugin) | +| `Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs` | inchangé (`LPAnimMeasure` via plugin) | + +## 6. Injection `SetUALRoot` + +- **`Source/LifePlay/LifePlay.cpp`** : retour à `FDefaultGameModuleImpl` (plus d’appel `SetUALRoot` / `LPSandboxConfig`). +- **`Plugins/LPSandbox/Source/LPSandbox/LPSandbox.cpp`** (addon) : `FLPSandboxModule::StartupModule()` appelle `LPAnimRedirect::SetUALRoot(FLPSandboxConfig::Get().QuaterniusUALRoot)` — commit addon séparé. + +## 7. Code jeu laissé tel quel (volontaire) + +- `LPDirectControlSubsystem.cpp` : détection sandbox par `GetName().Contains("LPPairSandbox")` — nom de classe inchangé, compatible module `LPSandbox`. +- Pas de submodule git (lot T4). + +## 8. Prochaine validation recommandée + +1. `Build.bat LifePlayEditor Win64 Development` (jeu + plugin jonction). +2. `Tools/debug/LifePlay_pair_sandbox.bat` — chambre `work___vin01`, menu Pair. +3. Vérifier `LogLPAnimMeasure` / clips sous `/Game/Quaternius/UAL` (ini chargée). diff --git a/Docs/Migration/etat/sandbox/S3/S3-INT-scaffold.md b/Docs/Migration/etat/sandbox/S3/S3-INT-scaffold.md new file mode 100644 index 0000000..f0a6a33 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S3/S3-INT-scaffold.md @@ -0,0 +1,48 @@ +# S3-INT-scaffold - Scaffold du repo addon `LifePlay-Sandbox` + +- **Date :** 2026-10-06 +- **Fiche :** S3-T1 | **Agent :** A-INT | **Rôle :** Intégration +- **Modèle prévu :** `claude-sonnet-5-5-high` (M2) +- **Statut :** scaffold créé, commit local `4938abb` (branche `main`), **non compilé** (aucun build UE lancé) ; remote GitHub créé, **non poussé** +- **Hors périmètre respecté :** aucun code sandbox déplacé depuis LifePlay ; aucun commit dans le repo jeu. + +## 1. Emplacement + +`D:\Game\LifePlay-Sandbox\` (sibling de `D:\Game\LifePlay-master 5.5\`). + +## 2. Fichiers créés + +| Chemin (relatif à `LifePlay-Sandbox/`) | Contenu | +|---|---| +| `.gitignore` | UE standard (Binaries, Build, Intermediate, Saved, DerivedDataCache, .vs, .vscode, .idea, `*.sln`...), `Plugins/*/Binaries|Intermediate`, `/Content/` (junction) | +| `README.md` | clone sibling, layout, montage du Content, lancement | +| `LPSandbox.uproject` | `EngineAssociation` 5.5, plugin `LPSandbox` activé, pas de module projet | +| `Config/DefaultEngine.ini` | minimal | +| `Config/DefaultGame.ini` | minimal + note sur `AdditionalAssetDirectories` (voir §3) | +| `Plugins/LPSandbox/LPSandbox.uplugin` | Runtime, `CanContainContent: true`, modules `LPSandbox` + `LPAnimMeasure` | +| `Plugins/LPSandbox/Source/LPSandbox/LPSandbox.Build.cs`, `.h`, `.cpp` | module vide (`IMPLEMENT_MODULE(FDefaultModuleImpl, LPSandbox)`, `LogLPSandbox`) | +| `Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimMeasure.Build.cs`, `.h`, `.cpp` | stub (mêmes dépendances que le module du jeu ; `LogLPAnimMeasure`) ; rempli en S3-T2 | +| `Tools/debug/.gitkeep`, `Docs/.gitkeep` | placeholders | + +## 3. Montage du Content du jeu + +`AdditionalAssetDirectories` : **non supportée** par UE 5.5 (recherche dans `Engine/Source` et `Engine/Config` : 0 occurrence). Non utilisée. +Solution documentée dans le README : **junction NTFS** `LifePlay-Sandbox\Content` -> `LifePlay-master 5.5\Content` (`mklink /J`), dossier ignoré par git. +Alternative : lier `Plugins\LPSandbox` dans le projet jeu. Limite : écritures `/Game` depuis l'addon atterrissent dans le repo jeu. +La junction n'a **pas** été créée par cette fiche. + +## 4. Remote + +| Étape | Résultat | +|---|---| +| `gh auth status` | OK (compte `edujiro5972`, scopes `repo`) | +| `gh repo create LifePlay-Sandbox --private --source=. --remote=origin` | **OK** : https://github.com/edujiro5972/LifePlay-Sandbox (privé), `origin` configuré | +| `git push -u origin main` | **non exécuté** (push non autorisé dans cette étape ; à lancer sur validation) | + +Statut : **REMOTE-CREATED, PUSH-PENDING** (pas de BLOCKED-REMOTE). + +## 5. Points d'attention pour S3-T2 + +- Projet sans `Source/` : première ouverture UE = compilation du plugin proposée par l'éditeur ; pour un build CLI il faudra ajouter `Source/LPSandbox*.Target.cs` + un module projet (à décider). +- `LPAnimMeasure` existe en double (jeu + addon) tant que S3-T2 / S8 n'ont pas tranché ; pas de conflit tant que les deux projets ne chargent pas le même plugin. +- Lanceurs `Tools/debug/*.bat` : à adapter (chemins `.uproject` addon) lors de leur transfert. diff --git a/Docs/Migration/etat/sandbox/S3/S3-INT.md b/Docs/Migration/etat/sandbox/S3/S3-INT.md new file mode 100644 index 0000000..52839bc --- /dev/null +++ b/Docs/Migration/etat/sandbox/S3/S3-INT.md @@ -0,0 +1,64 @@ +# S3-INT — Integration jeu <-> addon LifePlay-Sandbox (fiche S3-T4) + +Date : 2026-10-06 — Role A-INT + +## Pin addon + +| Champ | Valeur | +|---|---| +| Repo addon | https://github.com/edujiro5972/LifePlay-Sandbox.git | +| Branche | `main` | +| **Commit pin** | `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (`f09c42e`, **local, non pousse** au 2026-10-06) | +| Contenu du commit | S5-T2 : `LPCoupleSim` (géométrie du couple partagée jeu/addon) ; inclut S4-T3 (retrait GM Interaction legacy), S4-T2 (découpe `LPPairSandbox.cpp`) et S3-T4 (`StartupModule` → `SetUALRoot`) | +| Pins précédents | `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (S5-T2), `f8102647ccdebad37e4977ccbf2c51e583823a80` (S4-T3), `4de1f0b6764ed3bc575bb687630d3f070e1be221` (S3-T4) | + +## Structure + +- Addon = uproject mince + plugin nested : `LifePlay-Sandbox/Plugins/LPSandbox/LPSandbox.uplugin`. +- Un submodule monte sur `Plugins/LPSandbox` du jeu donnerait `Plugins/LPSandbox/Plugins/LPSandbox` : **incorrect**, ecarte. + +## Strategie retenue : repos siblings + AdditionalPluginDirectories + +1. Deux repos git independants, cote a cote : + ``` + D:\Game\LifePlay-master 5.5\ (jeu) + D:\Game\LifePlay-Sandbox\ (addon) + ``` +2. `LifePlay.uproject` : + ```json + "AdditionalPluginDirectories": [ "../LifePlay-Sandbox/Plugins" ], + "Plugins": [ { "Name": "LPSandbox", "Enabled": true }, ... ] + ``` + Chemin relatif au dossier du `.uproject` ; UBT/UE scannent `../LifePlay-Sandbox/Plugins/**/*.uplugin`. +3. La junction `Plugins\LPSandbox` du jeu a ete **retiree** (`rmdir Plugins\LPSandbox`, cible intacte). +4. Les modules plugin (LPSandbox, LPAnimMeasure, ...) se compilent dans + `Intermediate/External/...` du jeu (plugin hors arbre projet). +5. `Source/LifePlay/LifePlay.Build.cs` depend de `LPAnimMeasure` (inchange, noms OK) ; + `LPDirectControlSubsystem` n'a pas necessite d'adaptation. + +## Submodule optionnel `External/LifePlay-Sandbox` : NON retenu + +Un submodule dans le jeu dupliquerait l'addon (sibling + copie) et preterait a confusion +sur la copie effectivement chargee. Pin manuel a la place (ci-dessous). + +## Pin manuel (reproduire l'environnement) + +```powershell +cd D:\Game +git clone https://github.com/edujiro5972/LifePlay-Sandbox.git +git -C LifePlay-Sandbox checkout f09c42e5a2bc613cf269cce76c878461ca41c8b7 +# Le jeu doit etre clone a cote : D:\Game\LifePlay-master 5.5 +``` + +Mettre a jour ce document (hash) a chaque bump de l'addon qui doit etre consomme par le jeu. + +## Validation + +- `Build.bat LifePlayEditor Win64 Development -Project="...\LifePlay.uproject"` : + **OK** (91/91, 0 erreur) ; `UnrealEditor-LPSandbox.dll` lie depuis `AdditionalPluginDirectories`. +- Journal : `%TEMP%\s3t4_build.log`. + +## Etat des repos + +- Addon : `main` pousse sur `origin` (premier push, `-u`), arbre propre. +- Jeu : `LifePlay.uproject` modifie + ce document ; non commites (a faire par A-INT/orchestrateur). diff --git a/Docs/Migration/etat/sandbox/S3/S3-VAL.md b/Docs/Migration/etat/sandbox/S3/S3-VAL.md new file mode 100644 index 0000000..97fc479 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S3/S3-VAL.md @@ -0,0 +1,59 @@ +# S3-VAL - Validation du lot S3 (addon `LifePlay-Sandbox` branché sur le jeu) + +- **Date :** 2026-10-06 +- **Fiche :** S3-T5 | **Rôle :** A-VAL +- **Modèle prévu :** `claude-sonnet-5-5-high` | **Modèle utilisé :** `claude-sonnet-5-5-high` +- **Pin addon :** `4de1f0b6764ed3bc575bb687630d3f070e1be221` (`HEAD` de `D:\Game\LifePlay-Sandbox`, arbre propre) +- **Références :** `S3-INT.md`, `S3-IMPL-H.md`, `Docs/S3-move.md` (côté addon) + +## Verdict : **PASS** (jeu : PASS complet ; addon mince : PASS avec réserve, voir §3) + +| # | Contrôle | Résultat | +|---|---|---| +| 1 | Bat Pair lancé depuis le **jeu** (`Tools/debug/LifePlay_pair_sandbox.bat`) : plugin LPSandbox monté, `LPPairSandboxGameMode` trouvé | **PASS** | +| 2 | Aucune erreur de module (`Unable to load module`, `LNK`, `Fatal error`, `Assertion failed`) ni de GameMode introuvable | **PASS** (0 occurrence) | +| 3 | Sandbox opérationnel (mur, collision, éclairage, interaction, Quaternius) | **PASS** (16 lignes `LogLPSandbox`) | +| 4 | Addon mince `LPSandbox.uproject -game`, même GameMode | **PASS avec réserve** (GameMode et sandbox OK ; erreurs de chargement de Blueprints du jeu, voir §3) | +| 5 | Processus tués après essai | **PASS** (aucun `UnrealEditor` résiduel) | + +## 1. Jeu : bat Pair + +Commande du bat (inchangée) : `UnrealEditor.exe LifePlay.uproject "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 -ini:Engine:[...]GameInstanceClass=/Script/Engine.GameInstance`. `Saved/sandbox_mode.txt` = `Pair`. Processus resté vivant (`Responding = True`) environ 85 s, puis tué. Journal : `Saved/Logs/S3-VAL-runtime-game.log` (copie de `LifePlay.log`). + +| Constat | Journal | +|---|---| +| Plugin monté depuis `AdditionalPluginDirectories` | `LogPluginManager: Mounting External plugin LPSandbox` | +| GameMode résolu | `LogLoad: Game class is 'LPPairSandboxGameMode'` (aucun « GameMode introuvable ») | +| Sandbox en marche | `Sandbox mur`, `Sandbox collision : 54 meubles, 0 absents, capsule 20 / 88, profil Person lu`, `Sandbox eclairage`, `Sandbox interaction : 286 actions, 160 lieux, 12 horaires` | +| Racine UAL injectée par l'addon (S3-T4) | `Sandbox Quaternius : corps /Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard, 43 animation(s)` | +| Erreurs de module / lien | 0 | +| `: Error:` (total) | 1 : `LogLifePlay: Error: Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable` (déjà présent avant le lot S2, sans lien avec l'addon) | + +`LogLPAnimMeasure` : 0 ligne, attendu en mode Pair (la mesure n'est active qu'en mode Mesure ; `S2-VAL.md` avait validé les 269 lignes en Mesure avant le transfert). Le module `LPAnimMeasure` (DLL `Binaries/Win64/UnrealEditor-LPAnimMeasure.dll` du 19:20:01) est chargé comme dépendance de `LPSandbox`. + +Avertissement non bloquant : `LoadErrors: ... a dependent package /Game/Quaternius/UAL/M_Joints was not available` (le fichier `Content/Quaternius/UAL/M_Joints.uasset` n'existe pas dans le jeu ; l'asset `UAL1_Standard` se charge malgré tout, aucun effet sur le sandbox). À traiter hors S3. + +## 2. Binaires du jeu + +`UnrealEditor-LifePlay.dll` 19:20:19, `UnrealEditor-LifePlayEditorTools.dll` 19:20:23, `UnrealEditor-LPAnimMeasure.dll` 19:20:01, `UnrealEditor-LPSandbox.dll` 19:20:27 (compilés dans l'arbre du jeu depuis le plugin sibling). `Source/` du jeu ne contient plus ni `LPAnimMeasure/` ni aucun fichier `LP*Sandbox*` (lot S3-T3 effectif). `LifePlay.uproject` : `AdditionalPluginDirectories = ["../LifePlay-Sandbox/Plugins"]` et `LPSandbox` activé. + +## 3. Addon mince : `LPSandbox.uproject -game` + +Commande : `UnrealEditor.exe D:\Game\LifePlay-Sandbox\LPSandbox.uproject "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 -ini:...GameInstanceClass=/Script/Engine.GameInstance -abslog=...\S3-VAL-thin.log`. Content : junction `D:\Game\LifePlay-Sandbox\Content` -> `D:\Game\LifePlay-master 5.5\Content` (présente). Processus vivant environ 3 min (la compression des animations `/Game/Heavy/...` en cache DDC a ralenti la fin de l'essai), puis tué. Journal : `Saved/Logs/S3-VAL-runtime-thin.log` (copie de `LifePlay-Sandbox/Saved/Logs/S3-VAL-thin.log`). + +| Constat | Journal | +|---|---| +| Plugin monté | `LogPluginManager: Mounting Project plugin LPSandbox` | +| GameMode résolu | `LogLoad: Game class is 'LPPairSandboxGameMode'` | +| Sandbox en marche | mêmes lignes `LogLPSandbox` que dans le jeu (mur, collision 54 meubles / 0 absents, éclairage, interaction 286 actions) | +| Erreur de module / lien | 0 | +| `: Error:` | 2 458, tous dus au **contenu du jeu** : 217 `Unknown structure` + erreurs de compilation Blueprint (`CityExpansion`, `Save`, `GI`, `UI/*`) sur des structures `StreetMapBuilding` (`/Script/StreetMapRuntime`) | + +**Réserve (cause identifiée) :** `LPSandbox.uproject` n'active pas le plugin `StreetMap` (plugin du jeu, absent du dossier addon) ; les Blueprints du jeu qui l'utilisent se chargent donc avec des types de repli. Ils ne servent pas au sandbox (le monde est créé par code), qui démarre quand même. Conséquences : (1) l'addon mince valide le chargement du module, du GameMode et du Content monté, pas le contenu complet du jeu ; (2) l'essai complet (BP `Person`, `GI`, etc.) reste à faire depuis le jeu (§1). Piste si besoin : ajouter `AdditionalPluginDirectories = ["../LifePlay-master 5.5/Plugins"]` au `LPSandbox.uproject` (non fait : hors périmètre S3). `Config/LPSandbox.ini` de l'addon est lu côté addon, copie identique à celle du jeu. + +## 4. Effets de bord et limites + +- Aucun Blueprint ni asset du jeu modifié par l'essai (`git status` : seuls `GI`, `Person`, `Room` figuraient déjà modifiés avant). +- 2 processus `UnrealEditor` tués (jeu, puis addon) ; `CrashReportClient` éventuel nettoyé. +- Contrôle visuel non fait (preuve par journal uniquement). Mode Mesure et commandlets non relancés après transfert (hors fiche). +- Aucun commit effectué par cette fiche. diff --git a/Docs/Migration/etat/sandbox/S4/S4-CART.md b/Docs/Migration/etat/sandbox/S4/S4-CART.md new file mode 100644 index 0000000..4ac4fd7 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S4/S4-CART.md @@ -0,0 +1,201 @@ +# S4-T1 : cartographie de LPPairSandbox.cpp + +Role A-CART. Lecture seule : aucun fichier du plugin n'a ete modifie. + +Source : `D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\Source\LPSandbox\LPPairSandbox.cpp` +Taille mesuree : **4274 lignes** (`LPPairSandbox.h` : 322 lignes, `LPInteractionSandbox.cpp` : 441, `LPSandboxConfig.cpp` : 73). +Objectif : aucun fichier > ~800 LOC. + +## 1. Constat + +- Le .cpp contient 4 classes (UCLASS dans le .h) : `ALPPairSandboxStage`, `ALPPairSandboxCharacter`, `ALPPairSandboxGameMode`, plus les structs `FPairSandboxMenu` (l. 57-77) et `PairSandbox::*` (l. 79-823, machine virtuelle `lpaction`, catalogue, lecture `.lpworld`). +- Les UCLASS restent dans `LPPairSandbox.h` : seul le .cpp se decoupe, sans toucher a UHT. Les membres prives restent accessibles car chaque nouveau .cpp n'implemente que des methodes des memes classes. +- Quatre fonctions depassent 150 lignes et doivent etre decoupees en sous-methodes, sinon l'objectif ~800 LOC reste tenable mais la lisibilite non : + +| Fonction | Lignes | Taille | +|---|---|---| +| `ALPPairSandboxCharacter::ShowAnimMenu` | 3363-3939 | 577 | +| `ALPPairSandboxStage::BeginMeasure` | 1290-1664 | 375 | +| `ALPPairSandboxCharacter::Tick` | 3940-4247 | 308 | +| `ALPPairSandboxStage::Build` | 831-1042 | 213 | +| `ALPPairSandboxStage::TickTour` | 2068-2196 | ~130 | + +## 2. Carte par unite (lignes actuelles) + +### 2.1 RoomBuilder (lpworld, murs, mobilier, eclairage) + +| Bloc | Lignes | LOC | Contenu | +|---|---|---|---| +| `FProp`, `Token`, `LoadWorldFile` | 748-823 | 76 | lecture `work___vin01.lpworld` (echelle, props) | +| `Stage::Stage()` (ctor) | 825-829 | 5 | RootComponent | +| `Build` partie piece | 831-953 | 123 | meubles `StaticMeshComponent`, lambda `Shell` (4 murs + plafond + sol), capsule Person, recherche du point de depart libre | +| `Build` partie eclairage | 954-1040 | 87 | 2 directionnelles, SkyLight, SkyAtmosphere, BP_Sky_Sphere | +| `Build` fin | 1041-1042 | 2 | appel `LoadChat`, `LoadActions` (reste dans l'orchestrateur) | + +Total source : ~290 LOC. + +### 2.2 CoupleSim (box, weld, capsule, animation de paire) + +| Bloc | Lignes | LOC | Contenu | +|---|---|---|---| +| `PlayIdle`, `ConfigureCapsule`, `Weld`, `Apply`, `ToggleFurniture`, `ToggleCapsules` | 1072-1165 | 94 | idle Daz, capsule copiee de Person, soudure partenaire, collisions | +| `PlayPair`, `MoveCouple` | 2528-2599 | 72 | animation de paire, deplacement avec blocage par la box | +| `FitCoupleBox` | 2600-2712 | 113 | boite englobante du couple (lambda `Accumulate` 2609-2661) | +| `Character::LoadQuaternius`, `SelectBody`, `ApplyBody` | 2958-3060 | 103 | corps Quaternius / Bridget / Michael / mannequin | +| `Character::RefreshPose`, `PlayWalk` | 3063-3135 | 73 | pose d'interaction, marche | + +Total source : ~455 LOC. + +### 2.3 InteractionFilters (VM lpaction, catalogue, chat) + +| Bloc | Lignes | LOC | Contenu | +|---|---|---|---| +| Types `FAction`, `FPerson`, `FVal`, `FEnv`, `EKind`, `FTok`, globales `Catalog`, `PlaceChoices`, `WhenChoices`, `MakePerson` | 79-153 | ~75 | modele de donnees | +| `Tokenize` + `FWhoEval` (`Eat`, `Resolve`, `Call`, `ReadArgs`, `Primary`, `Unary`, `Add`, `Compare`, `And`, `Expr`) | 155-548 | 394 | analyseur et evaluateur des conditions WHO | +| `WhoAllows`, `WhereHas`, `LoadActions`, `CollectActions` | 549-685 et 727-747 | ~158 | filtrage du catalogue `.lpaction` | +| `ReadInteractKey` | 686-726 | 41 | touche d'interaction depuis l'ini d'entree | +| `Stage::LoadChat` | 1044-1070 | 27 | `chat.lpaction` | +| `Stage::BeginInteraction` | 1167-1208 | 42 | entree dans le mode Interaction | +| `Character::ApplyInteractionView` | 3136-3178 | 43 | vues interaction / dessus / yeux | +| `Character::ApplyFilters` | 2950-2957 | 8 | rafraichit le chat | +| `ShowChat`, `HideChat` | 3179-3274 | 96 | liste des actions filtrees (Slate) | +| `RefreshInteractKey`, `ShowKeyMark`, `HideKeyMark`, `PlaceKeyMark` | 3275-3362 | 88 | pastille de touche | +| Panneau filtres (flags WHO1 / WHO2, combos lieu / heure / race) | 3560-3641, 3642-3676, 3845-3890 | ~170 | partie de `ShowAnimMenu` | +| Bloc interaction de `Tick` | 4017-4119 | 103 | marche, distance, touche E | + +Total source : ~1290 LOC. + +### 2.4 MeasureUI (mesure, tournee, vignettes, planches, panneau Slate) + +| Bloc | Lignes | LOC | Contenu | +|---|---|---|---| +| `EndMeasure` | 1209-1289 | 81 | nettoyage de la scene de mesure | +| `BeginMeasure` | 1290-1664 | 375 | scene, lambda `Place` (1370-1445), lambda `RunClip` (1446-1593), anneau de 6 cameras (1601-1642), score des vues | +| `ReplayMeasure`, `ShowBestModel` | 1665-1678 | 14 | | +| Namespace `PairSandboxAuto` | 1679-1698 | 20 | constantes et chemins des captures auto | +| `ToggleRaw`, `ToggleFormula`, `ToggleT`, `SetAutoRun`, `TickAuto` | 1700-1851 | 152 | boucle automatique | +| Namespace `PairSandboxTour` | 1853-1888 | 36 | constantes tournee, vignettes, planches | +| `GetMeasureClips`, `ShowFormulaDefault`, `BrowseClip`, `BrowseTime` | 1890-1988 | 99 | clips, vue par defaut | +| `StartTour`, `TickTour` | 1989-2196 | 208 | tournee des clips | +| `SaveVignette`, `SavePlancheFrame`, `WritePlanche` | 2197-2307 | 111 | PNG 512 px, JPEG 256 px | +| `GetMeasureCameraName`, `UseMeasureCamera`, `SetFreeView`, `MoveFreeView` | 2309-2399 | 91 | cameras fixes et libre | +| `DrawMeasureFigures`, `ScoreMeasureViews` | 2400-2527 | 128 | calques, squelettes, ecart par camera | +| Panneau Mesure (`MeasureHolder`, boutons, Auto, Tournee) | 3706-3832 | 127 | partie de `ShowAnimMenu` | +| Branche Mesure de `SelectMode` | 2877-2906 | ~30 | | +| Entrees clavier Mesure de `Tick` (0-9, R, M, T, F, N, P, PgUp, PgDn, camera libre) | 4125-4211 | 87 | | +| Libelles flottants Mesure de `Tick` | 4212-4240 | ~29 | | + +Total source : ~1590 LOC. + +### 2.5 Stage / GameMode / Character (reste) + +| Bloc | Lignes | LOC | Contenu | +|---|---|---|---| +| Includes + `FPairSandboxMenu` | 1-77 | 77 | | +| `Stage::Possess` | 2713-2745 | 33 | | +| `Character` ctor | 2746-2768 | 23 | spring arm, camera | +| `SetStage`, `EndPlay`, `ResetView` | 2769-2824 | 56 | | +| `SelectAnim`, `SelectMode` (hors Mesure), `SelectView`, `SelectWalk` | 2825-2949 | ~100 | | +| `ShowAnimMenu` : preparation, combos, ossature Slate, fin (mode persistant) | 3363-3705, 3833-3844, 3876-3939 | ~420 | | +| `Tick` commun (souris, ZQSD, chute, Echap) | 3940-4016, 4120-4124 | ~82 | | +| `GameMode` | 4240-4274 | ~35 | | + +Total source : ~830 LOC. + +Controle : 290 + 455 + 1290 + 1590 + 830 = 4455, soit un ecart de ~180 lignes avec 4274 : les chevauchements (`ShowAnimMenu` et `Tick` sont comptes dans plusieurs unites par tranche, bornes approximatives). + +## 3. Plan de fichiers propose + +Dossier plat conserve (`PublicIncludePaths.Add(ModuleDirectory)` dans `LPSandbox.Build.cs`) : aucun changement de `.Build.cs` necessaire, UBT ramasse les nouveaux `.cpp` du dossier. Les LOC estimes incluent ~20-35 lignes d'includes par fichier (le .cpp d'origine en porte 55). + +### 3.1 Fichiers conserves + +| Fichier | LOC estime | Role | +|---|---|---| +| `LPPairSandbox.h` | ~340 | UCLASS inchangees ; +4 declarations privees (`BuildRoom`, `BuildLighting`, `RunMeasureClip`, `BuildMenuPanels`) ; `FLPMeasureFigure` y reste | +| `LPPairSandbox.cpp` | ~150 | ctor Stage, `Build` (orchestrateur), `Possess`, `GameMode` | +| `LPPairSandboxCharacter.cpp` (nouveau) | ~330 | ctor, `SetStage`, `EndPlay`, `ResetView`, `Select*`, `Tick` commun | + +### 3.2 Unite RoomBuilder + +| Fichier | LOC estime | Contenu | +|---|---|---| +| `LPSandboxWorldFile.h` | ~30 | `FProp`, `LoadWorldFile` | +| `LPSandboxWorldFile.cpp` | ~100 | `Token`, `LoadWorldFile` (748-823) | +| `LPSandboxRoomBuilder.cpp` | ~160 | `Stage::BuildRoom` (831-953) | +| `LPSandboxLighting.cpp` | ~120 | `Stage::BuildLighting` (954-1040) | + +### 3.3 Unite CoupleSim + +| Fichier | LOC estime | Contenu | +|---|---|---| +| `LPCoupleSim.cpp` | ~200 | 1072-1165 + 2528-2599 | +| `LPCoupleBox.cpp` | ~150 | `FitCoupleBox` (2600-2712) | +| `LPSandboxBodies.cpp` | ~215 | 2958-3135 (corps, pose, marche) | + +### 3.4 Unite InteractionFilters + +| Fichier | LOC estime | Contenu | +|---|---|---| +| `LPSandboxActionVM.h` | ~110 | types `PairSandbox::*` (79-153) + declarations (`MakePerson`, `WhoAllows`, `CollectActions`, `ReadInteractKey`, `LoadActions`) + `extern` des globales | +| `LPSandboxActionVM.cpp` | ~430 | `Tokenize` + `FWhoEval` (155-548) | +| `LPSandboxActionCatalog.cpp` | ~260 | 549-747 + `Stage::LoadChat` | +| `LPInteractionMode.cpp` | ~230 | `BeginInteraction`, `ApplyInteractionView`, `ApplyFilters`, bloc interaction du `Tick` (extrait en `TickInteraction`) | +| `LPChatUI.cpp` | ~210 | `ShowChat`, `HideChat`, pastille de touche (3179-3362) | +| `LPFilterPanel.cpp` | ~190 | panneau WHO1 / WHO2 / lieu / heure (extrait de `ShowAnimMenu`) | + +### 3.5 Unite MeasureUI + +| Fichier | LOC estime | Contenu | +|---|---|---| +| `LPMeasureShared.h` | ~70 | constantes et chemins de `PairSandboxAuto` et `PairSandboxTour` (en `inline constexpr` / `inline` pour partage) | +| `LPMeasureScene.cpp` | ~350 | `EndMeasure`, `BeginMeasure` (hors RunClip et cameras), `ReplayMeasure`, `ShowBestModel` | +| `LPMeasureClip.cpp` | ~190 | `RunMeasureClip` (lambdas `Place` + `RunClip`, 1370-1593) | +| `LPMeasureAuto.cpp` | ~185 | 1700-1851 | +| `LPMeasureTour.cpp` | ~340 | 1890-2196 | +| `LPMeasureCapture.cpp` | ~150 | vignettes, planches (2197-2307) | +| `LPMeasureCameras.cpp` | ~170 | 2309-2399 + anneau de cameras (1601-1642, extrait en `SpawnMeasureCameras`) | +| `LPMeasureOverlay.cpp` | ~165 | `DrawMeasureFigures`, `ScoreMeasureViews` | +| `LPMeasurePanel.cpp` | ~300 | panneau Slate (3706-3832), touches (4125-4211), libelles (4212-4240), branche Mesure de `SelectMode` | + +### 3.6 Menu (partage entre unites) + +| Fichier | LOC estime | Contenu | +|---|---|---| +| `LPPairSandboxMenu.h` | ~30 | `FPairSandboxMenu` (57-77) | +| `LPSandboxMenu.cpp` | ~470 | `ShowAnimMenu` : liste des animations, Modes / Vues / Marche / Corps, ossature Slate ; appelle `BuildMeasurePanel` et `BuildFilterPanel` | + +## 4. Totaux et verification de l'objectif + +| Unite | Fichiers | LOC estime | Plus gros fichier | +|---|---|---|---| +| RoomBuilder | 4 | ~410 | 160 | +| CoupleSim | 3 | ~565 | 215 | +| InteractionFilters | 6 | ~1430 | 430 | +| MeasureUI | 9 | ~1920 | 350 | +| Stage / GameMode / Character | 3 + 2 menu | ~1020 | 470 | +| **Total** | **27** | **~5350** | **470** | + +- Plus gros fichier : `LPSandboxMenu.cpp` (~470). Marge d'environ 40 % sous 800 LOC. +- Le total passe de 4274 a ~5350 : surcout de ~1000 lignes, imputable aux includes repetes (~25 fichiers x ~25 lignes), aux en-tetes internes et aux signatures des fonctions extraites. +- Les UCLASS ne bougent pas : aucun risque de regeneration UHT incoherente. + +## 5. Risques et points d'attention + +1. **Namespaces anonymes partages** : `PairSandboxAuto` et `PairSandboxTour` sont utilises par Auto, Tour, Capture et Scene. A deplacer dans `LPMeasureShared.h` en `inline constexpr` / fonctions `inline`, sinon duplication ODR entre .cpp. +2. **Globales `PairSandbox::Catalog / PlaceChoices / WhenChoices`** : definies dans `LPSandboxActionCatalog.cpp`, `extern` dans `LPSandboxActionVM.h`. +3. **Lambdas a pointeur de membre** : le lambda `Flag` (`bool ALPPairSandboxCharacter::*`) exige l'acces prive : `BuildFilterPanel` doit etre une methode membre, pas une fonction libre. +4. **`static bool bPoseWritten`** (`ShowFormulaDefault`, 1918) : reste local au .cpp Tour, ne pas dupliquer. +5. **Unity build** : le module a `PCHUsage = UseExplicitOrSharedPCHs` ; verifier avec et sans unity (`bUseUnity=false`) que les includes de chaque nouveau .cpp suffisent. +6. **Doublon connu** : `LPInteractionSandbox.cpp` (441 LOC) reimplemente `LoadChat`, `ShowChat`, `HideChat`. A ne pas fusionner dans S4 (hors perimetre), mais `LPChatUI.cpp` pourra servir de base a un dedoublonnage ulterieur. +7. **Tick** : l'ordre des blocs (souris, deplacement, interaction, Echap, mesure, libelles) doit etre conserve exactement lors de l'extraction en `TickInteraction` / `TickMeasureInput` / `TickMeasureLabels`. + +## 6. Ordre de migration recommande (un commit par etape, build + capture de reference entre chaque) + +1. Extraire `LPSandboxActionVM.h/.cpp` et `LPSandboxActionCatalog.cpp` (code libre, sans UObject : risque minimal). +2. Extraire `LPSandboxWorldFile` puis `BuildRoom` / `BuildLighting` (RoomBuilder). +3. Extraire CoupleSim (`LPCoupleSim`, `LPCoupleBox`, `LPSandboxBodies`). +4. Extraire `LPChatUI`, `LPInteractionMode`. +5. Extraire `LPMeasureShared.h` puis Capture, Tour, Auto, Overlay, Cameras. +6. Decouper `BeginMeasure` (`RunMeasureClip`, `SpawnMeasureCameras`) et `ShowAnimMenu` (`BuildMeasurePanel`, `BuildFilterPanel`), puis `Tick`. +7. Verifier : build Win64 Development, lancement des 3 modes (Pair, Interaction, Mesure), comparaison de `Saved/Measurement/tour.csv` et d'une vignette avant / apres. diff --git a/Docs/Migration/etat/sandbox/S4/S4-IMPL-H.md b/Docs/Migration/etat/sandbox/S4/S4-IMPL-H.md new file mode 100644 index 0000000..70cdf18 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S4/S4-IMPL-H.md @@ -0,0 +1,150 @@ +# S4-IMPL-H — Découpe de `LPPairSandbox.cpp` (fiche S4-T2) + +- **Date :** 2026-10-06 +- **Fiche :** S4-T2 | **Agent :** A-IMPL-H | **Rôle :** Implémentation +- **Modèle prévu :** `claude-opus-5-thinking-high` (M3) +- **Amont :** `S4/S4-CART.md` (S4-T1, lecture seule) +- **Statut :** **compilé** — `LPSandboxEditor` **et** `LPSandbox` Win64 Development, 0 erreur / 0 avertissement +- **Commit addon :** `32ad09c` — `S4-T2: decoupe de LPPairSandbox.cpp en 7 unites de traduction` + +> **Le repo jeu n'est pas modifié par cette fiche, hors ce document.** Tout le code vit dans +> `D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\Source\LPSandbox\`. +> `LPInteractionSandbox.*` (legacy, 441 LOC) **n'est pas touché** : sa suppression est le lot S4-T3. + +--- + +## 1. Résultat : LOC par fichier + +Avant : `LPPairSandbox.cpp` = **4274 lignes**, `LPPairSandbox.h` = 322. + +| Fichier | LOC | Contenu | +|---|---:|---| +| `LPPairSandbox.h` | **332** | UCLASS inchangées + 3 déclarations privées + 4 déclarations anticipées | +| `LPPairSandboxMenu.h` | **73** | `FPairSandboxMenu`, `PairSandboxMenu::FindChoice` / `MakeCombo` | +| `LPSandboxActionVM.h` | **55** | `PairSandbox::FAction` / `FPerson`, `extern` des globales, 4 prototypes | +| `LPSandboxRoomBuilder.cpp` | **316** | `.lpworld` (`Token`, `FProp`, `LoadWorldFile`), `Stage::Build` (mobilier, murs, éclairage) | +| `LPCoupleSim.cpp` | **477** | `PlayIdle`, `ConfigureCapsule`, `Weld`, `Apply`, `Toggle*`, `PlayPair`, `MoveCouple`, `FitCoupleBox`, `LoadQuaternius`, `SelectBody`, `ApplyBody`, `RefreshPose`, `PlayWalk` | +| `LPSandboxActionVM.cpp` | **647** | `Tokenize`, `FWhoEval`, `WhoAllows`, `WhereHas`, `LoadActions`, `ReadInteractKey`, `CollectActions` | +| `LPSandboxInteraction.cpp` | **493** | `LoadChat`, `BeginInteraction`, `ApplyFilters`, `ApplyInteractionView`, `ShowChat` / `HideChat`, pastille de touche, `BuildFilterPanel` | +| `LPSandboxMeasure.cpp` | **837** | `EndMeasure`, `BeginMeasure`, `ReplayMeasure`, `ShowBestModel`, caméras fixes et libre, `DrawMeasureFigures`, `ScoreMeasureViews`, `BuildMeasurePanel` | +| `LPSandboxMeasureTour.cpp` | **743** | `PairSandboxAuto`, `Toggle{Raw,Formula,T}`, `SetAutoRun`, `TickAuto`, `PairSandboxTour`, clips et navigation, `StartTour` / `TickTour`, vignettes, planches, `TickMeasureInput` | +| `LPPairSandbox.cpp` | **807** | constructeurs, `Possess`, `SetStage`, `EndPlay`, `ResetView`, `Select*`, `ShowAnimMenu`, `Tick`, `GameMode` | + +- **Plus gros fichier : 837 LOC** (`LPSandboxMeasure.cpp`), sous l'objectif de ~900. +- Trois fichiers dépassent le seuil souple de 800 (837, 807) ; les ramener sous 800 imposerait de couper + `BeginMeasure` (374 lignes) et `ShowAnimMenu` (277 lignes après extraction) au-delà du découpage « pragmatique » demandé. +- Total du module : 4274 → **4320 lignes de code déplacé + 240 lignes d'en-têtes et d'includes** répétés. + +## 2. Écart assumé avec la fiche : 7 unités au lieu de 5 (+1) + +La fiche demandait 5 unités, avec autorisation de scinder Mesure en deux si elle dépassait 900. +**Deux scissions ont été nécessaires**, pour une raison arithmétique : + +| Unité telle que définie par la fiche | LOC si non scindée | +|---|---:| +| Mesure (`BeginMeasure` … `draw` + panneau + touches) | **~1570** → scindée en 837 + 743 (prévu par la fiche) | +| Interaction (VM `lpaction` + catalogue + chat + mode + filtres) | **~1140** → scindée en 647 (`ActionVM`) + 493 (`Interaction`) | + +La scission d'Interaction suit le fichier que la fiche nommait elle-même comme en-tête optionnel +(`LPSandboxActionVM.h`) : l'en-tête a été créé **et** son `.cpp`, parce que le corps de l'évaluateur +(`Tokenize` + `FWhoEval` + filtrage du catalogue, 592 lignes) n'a aucune raison d'être `inline`. +La frontière est nette : `LPSandboxActionVM.*` ne connaît ni UE Slate, ni les classes de la sandbox — +c'est du code libre, testable isolément. + +Les 5 noms de fichiers demandés existent tous et portent le contenu demandé. + +## 3. Ce qui n'a pas bougé + +- **Aucune UCLASS déplacée.** Les 3 classes restent déclarées dans `LPPairSandbox.h`, donc UHT ne voit + aucun changement de réflexion (`Reflection code generated` : 1 fichier, comme avant). +- **Aucun corps de fonction réécrit.** Le découpage a été fait par extraction de plages de lignes : + un diff ligne à ligne entre l'original et l'union des nouveaux fichiers ne remonte que 13 lignes + retirées et 33 ajoutées, toutes listées au §4. +- `LPSandbox.Build.cs` est inchangé : UBT ramasse les nouveaux `.cpp` du dossier plat. +- `LPInteractionSandbox.*` (doublon connu de `LoadChat` / `ShowChat` / `HideChat`) est intact : lot S4-T3. + +## 4. Les seules modifications de code (liste exhaustive) + +### 4.1. Trois méthodes privées ajoutées à `LPPairSandbox.h` + +```cpp +TSharedRef BuildMeasurePanel(TSharedPtr& OutHolder); +TSharedRef BuildFilterPanel(TSharedPtr& OutHolder); +void TickMeasureInput(APlayerController* PC, ALPPairSandboxStage* Room, float DeltaSeconds, float DX, float DY); +``` + +Elles sortent trois blocs contigus de `ShowAnimMenu` (576 lignes) et de `Tick` (301 lignes), qui à eux +seuls auraient laissé `LPPairSandbox.cpp` à ~1220 LOC. Elles restent **membres** : le panneau des filtres +construit des cases à cocher sur des **pointeurs sur membres privés** +(`&ALPPairSandboxCharacter::bWho1Married`), ce qu'une fonction libre ne peut pas faire (risque n° 3 de la +cartographie). Déclarations anticipées ajoutées : `APlayerController`, `SBox`, `SVerticalBox`. + +Les corps appelants gardent la forme d'origine : + +| Avant (ligne d'origine) | Après | +|---|---| +| `SAssignNew(MeasureHolder, SVerticalBox) …` (3706-3822) | `BuildMeasurePanel(MeasureHolder)` | +| `SAssignNew(FilterHolder, SBox) …` (3845-3878) | `BuildFilterPanel(FilterHolder)` | +| `if (bMeasure && Room) { … }` (4125-4211) | `if (bMeasure && Room) { TickMeasureInput(PC, Room, DeltaSeconds, DX, DY); }` | + +Les holders restent des variables locales de `ShowAnimMenu`, recopiées ensuite dans +`Menu->MeasureRow` / `Menu->FilterRow` comme avant : l'ordre d'évaluation de l'arbre Slate est identique. + +### 4.2. Deux lambdas locales devenues fonctions libres + +`FindChoice` et `MakeCombo` (lignes 3512-3545) servaient à 9 listes déroulantes, dont 7 partent dans +`BuildFilterPanel`. Elles deviennent `PairSandboxMenu::FindChoice` / `MakeCombo` dans +`LPPairSandboxMenu.h`, corps inchangé. `MakeCombo` capturait `Self` sans l'utiliser : la capture +inutile disparaît. Les 9 appels sont simplement qualifiés. + +### 4.3. Globales du catalogue + +`PairSandbox::Catalog`, `PlaceChoices`, `WhenChoices` restent définies une seule fois +(`LPSandboxActionVM.cpp`) et sont déclarées `extern` dans `LPSandboxActionVM.h` (risque n° 2 de la +cartographie). `ShowAnimMenu` les lit toujours directement. + +### 4.4. Namespaces de constantes + +`PairSandboxAuto` et `PairSandboxTour` **n'ont pas eu besoin d'un en-tête partagé** (risque n° 1 de la +cartographie) : après ce découpage, leurs quatre consommateurs (`TickAuto`, `StartTour`, `TickTour`, +`SaveVignette` / `WritePlanche`) vivent tous dans `LPSandboxMeasureTour.cpp`. C'est ce qui a dicté la +frontière Mesure / MesureTour : `LPSandboxMeasure.cpp` ne référence aucune des deux. +Le `static bool bPoseWritten` de `ShowFormulaDefault` reste unique, dans le même fichier (risque n° 4). + +## 5. Compilation + +| Cible | Mode d'unité | Résultat | +|---|---|---| +| `LPSandboxEditor Win64 Development` | **non-unity** (7 `.cpp` exclus du blob, car dans le working set git) | succès, 0 avertissement | +| `LPSandbox Win64 Development` (Game) | non-unity | succès, `Binaries/Win64/LPSandbox.exe` | +| `LPSandboxEditor Win64 Development` | **unity** (`-Rebuild` après commit) | succès, 0 avertissement | + +Les deux modes ont été vérifiés (risque n° 5 de la cartographie) : la première compilation, faite avant +le commit, a traité chaque nouveau `.cpp` **isolément**, ce qui prouve que les blocs `#include` de chaque +unité se suffisent à eux-mêmes ; le `-Rebuild` après commit valide le chemin unity. +Aucun include n'a dû être corrigé : la compilation est passée du premier coup. + +## 6. Vérification « zéro changement de comportement » + +Contrôle automatisé : comparaison du multiensemble des lignes de code (hors `#include`, commentaires, +accolades et crochets seuls) entre l'original et l'union des 9 fichiers produits. + +- **13 lignes de l'original absentes** : les 2 déclarations de lambdas `FindChoice` / `MakeCombo`, + les 9 lignes d'appel à ces lambdas (requalifiées), les 2 `SAssignNew(…Holder, …)` (devenus + `SAssignNew(OutHolder, …)`). +- **33 lignes nouvelles** : les 9 appels requalifiés, 2 `TWeakObjectPtr Self(this)`, les 3 signatures + des méthodes extraites et leurs 3 appels, 2 `#pragma once`, 2 ouvertures de `namespace`, + et les 12 lignes de déclaration de `LPSandboxActionVM.h` / `LPPairSandboxMenu.h`. + +Tout le reste du fichier d'origine — y compris l'ordre des blocs de `Tick` (souris, déplacement, +interaction, Échap, mesure, libellés), qui était le risque n° 7 — est recopié à l'octet près. + +## 7. Reste à faire + +- **S4-T3** : supprimer `LPInteractionSandbox.*` (441 LOC), doublon de `LoadChat` / `ShowChat` / + `HideChat` désormais isolés dans `LPSandboxInteraction.cpp`. Penser au `GameMode` + `/Script/LPSandbox.LPInteractionGameMode` référencé par `Tools/debug/LifePlay_interaction_sandbox.bat`. +- **Validation d'exécution (S4-VAL)** : non faite ici. Lancer les 3 modes (Pair, Interaction, Mesure) et + comparer `Saved/Measurement/tour.csv` et une vignette avant / après, comme le recommande `S4-CART.md` §6.7. +- Si le seuil souple de 800 LOC devient contraignant : découper `BeginMeasure` (`RunMeasureClip`, + `SpawnMeasureCameras`) et `ShowAnimMenu`, ce qui ferait passer Mesure et l'orchestrateur sous 700. diff --git a/Docs/Migration/etat/sandbox/S4/S4-VAL.md b/Docs/Migration/etat/sandbox/S4/S4-VAL.md new file mode 100644 index 0000000..7fd3697 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S4/S4-VAL.md @@ -0,0 +1,54 @@ +# S4-VAL — Validation du lot S4 (retrait GM Interaction legacy + build Pair) + +- **Date :** 2026-10-06 +- **Fiche :** S4-T4a | **Rôle :** A-VAL +- **Modèle prévu :** `composer-2.5-fast` | **Modèle utilisé :** `composer-2.5-fast` +- **Pin addon :** `f8102647ccdebad37e4977ccbf2c51e583823a80` (`HEAD` de `D:\Game\LifePlay-Sandbox`, commits `32ad09c` S4-T2 + `f810264` S4-T3) +- **Références :** `S4-IMPL-H.md`, `S4-CART.md`, README addon (note S4-T3) + +## Verdict : **PASS** + +| # | Contrôle | Résultat | +|---|---|---| +| 1 | Addon : plus de symboles `LPInteractionSandbox` / `ALPInteractionGameMode` (grep repo) | **PASS** | +| 2 | Compilation `LifePlayEditor Win64 Development` après retrait des sources Interaction GM | **PASS** (UBT : `UnrealEditor-LPSandbox.dll`, 0 erreur) | +| 3 | Bat Pair depuis le jeu (~45 s, `Saved/sandbox_mode.txt` = `Pair`) : plugin monté, GameMode Pair | **PASS** | +| 4 | Journal sandbox (`LogLPSandbox`) | **PASS** (16 lignes : mur, collision, éclairage, interaction, Quaternius, corps, pose) | +| 5 | Erreurs module / lien / GameMode introuvable | **PASS** (0) | +| 6 | Processus tué après essai | **PASS** | + +## 1. S4-T3 (addon) + +- Fichiers supprimés : `Plugins/LPSandbox/Source/LPSandbox/LPInteractionSandbox.h` / `.cpp` (~543 LOC). +- `LPSandbox.Build.cs`, `README.md`, `Docs/S3-move.md` mis à jour ; mode **Interaction** documenté via menu `LPPairSandbox`. +- Commit addon : `f810264` (`S4-T3: retirer le GameMode Interaction legacy`). + +## 2. Compilation + +```powershell +& 'D:\Game\Launcher\Engine\UE_5.5\Engine\Build\BatchFiles\Build.bat' LifePlayEditor Win64 Development ` + -Project='D:\Game\LifePlay-master 5.5\LifePlay.uproject' -WaitMutex +``` + +- UHT : 1 fichier généré (retrait des classes Interaction GM). +- `UnrealEditor-LPSandbox.dll` : 2026-10-06 20:15:08 (postérieur aux sources). + +## 3. Runtime : bat Pair (jeu) + +Équivalent du bat `Tools/debug/LifePlay_pair_sandbox.bat` (même URL `game=/Script/LPSandbox.LPPairSandboxGameMode`), processus laissé ~45 s puis arrêté. Journal copié : `Saved/Logs/S4-VAL-runtime-game.log` (source `LifePlay.log` de la session). + +| Constat | Journal | +|---|---| +| Plugin monté | `LogPluginManager: Mounting External plugin LPSandbox` | +| GameMode résolu | `LogLoad: Game class is 'LPPairSandboxGameMode'` | +| Sandbox Pair | 16 × `LogLPSandbox` (mur ×4, collision 54 meubles, éclairage, interaction 286 actions, Quaternius 43 anims, corps Bridget/Michael, pose AgMakeout) | +| Erreurs module | 0 | +| `: Error:` hors lot | 1 × `LogLifePlay: Error: Coiffures : GI.HairstyleTable...` (déjà connu, S3-VAL) ; avertissements `M_Joints` / `M_Main` absents (S3-VAL) | + +Initialisation carte ~32 s ; fenêtre de 45 s suffisante pour voir les lignes `LogLPSandbox` (init moteur total ~51 s si laissé ouvert plus longtemps). + +## 4. Limites + +- Mode **Interaction** via menu non exercé visuellement (hors fiche ; le retrait du second GameMode ne change pas l'entrée bat Pair). +- Contrôle visuel non fait (preuve journal + build). +- Aucun commit jeu par cette fiche (doc + journal `CHG-216` uniquement). diff --git a/Docs/Migration/etat/sandbox/S5/S5-CART.md b/Docs/Migration/etat/sandbox/S5/S5-CART.md new file mode 100644 index 0000000..3f1d982 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S5/S5-CART.md @@ -0,0 +1,355 @@ +# 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. diff --git a/Docs/Migration/etat/sandbox/S5/S5-IMPL-H.md b/Docs/Migration/etat/sandbox/S5/S5-IMPL-H.md new file mode 100644 index 0000000..a4195bc --- /dev/null +++ b/Docs/Migration/etat/sandbox/S5/S5-IMPL-H.md @@ -0,0 +1,257 @@ +# 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& Parents, const TArray& Local, TArray& OutComponent)` | locales + parents → espace composant | +| `void AccumulatePoints(const TArray& 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 Move; TEnumAsByte 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 IsWalkableSupport; // jeu : IsBedMesh + TFunction IsIgnoredShell; // jeu : Wall / Wall_* + TFunction 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. diff --git a/Docs/Migration/etat/sandbox/S5/S5-VAL.md b/Docs/Migration/etat/sandbox/S5/S5-VAL.md new file mode 100644 index 0000000..69e5245 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S5/S5-VAL.md @@ -0,0 +1,73 @@ +# S5-VAL — Validation du lot S5 (`LPCoupleSim` partagé jeu ↔ addon) + +- **Date :** 2026-10-06 +- **Fiche :** S5-T3 | **Rôle :** A-VAL +- **Modèle prévu :** `claude-sonnet-5-5-high` | **Modèle utilisé :** `claude-sonnet-5-5-high` +- **Pin addon :** `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (`HEAD` = `origin/main` de `D:\Game\LifePlay-Sandbox`, arbre propre ; `S5-T2`) +- **Références :** `S5-CART.md` (S5-T1), `S5-IMPL-H.md` (S5-T2), `S4-VAL.md` + +## Verdict : **PASS** (PASS runtime Pair, PASS-STATIC ShowCouple) + +| # | Contrôle | Résultat | +|---|---|---| +| 1 | Bat Pair depuis le jeu (~45 s demandées, session laissée ~90 s) : plugin monté, GameMode Pair | **PASS** | +| 2 | `LogLPSandbox` : chemin `FitCoupleBox` (maintenant `LPCoupleSim`) exécuté, pas de crash couple box | **PASS** | +| 3 | Aucun `Fatal`, assertion, `ensure`, `Access violation`, aucun nouveau dossier `Saved/Crashes` | **PASS** | +| 4 | `LifePlayEditor Win64 Development` à jour (`Target is up to date`), DLL postérieures aux sources S5 | **PASS** | +| 5 | `LPShowCoupleSubsystem` : compile + branché sur `LPCoupleSim` (34 références), copies locales supprimées | **PASS-STATIC** | +| 6 | Parcours show en jeu (sélection du couple, `Engage` → `KeepTogether` → `Release`) | **non exercé** (voir §4) | +| 7 | Processus tué après essai | **PASS** | + +## 1. Runtime : bat Pair (jeu) + +Équivalent du bat `Tools/debug/LifePlay_pair_sandbox.bat` (même URL `game=/Script/LPSandbox.LPPairSandboxGameMode`, `Saved/sandbox_mode.txt` = `Pair`), lancé à 20:42:10. Le processus a répondu à partir de ~20:43:07 (initialisation de la carte ~50 s), puis a été laissé jusqu'à ~20:43:50 avant d'être arrêté. Journal copié : `Saved/Logs/S5-VAL-runtime-game.log` (source `LifePlay.log`, 1 709 lignes ; copie locale non versionnée). + +Binaires utilisés : `UnrealEditor-LPAnimMeasure.dll` 20:36:29, `UnrealEditor-LPSandbox.dll` 20:36:36, `UnrealEditor-LifePlay.dll` 20:37:15, tous postérieurs aux sources S5 (`LPCoupleSim.h` 20:28, `LPCoupleSimCore.cpp` 20:32, `LPShowCoupleSubsystem.cpp` 20:35). L'essai a donc bien exercé le code S5-T2. + +| Constat | Journal | +|---|---| +| Plugin monté | `LogPluginManager: Mounting External plugin LPSandbox` | +| GameMode résolu | `LogLoad: Game class is 'LPPairSandboxGameMode'` | +| Initialisation sandbox | 16 lignes `LogLPSandbox` : mur ×4, collision (54 meubles, 0 absent), éclairage, interaction (286 actions), Quaternius 43 anims, corps `bridget_gp` / `michael`, boîte, pose | +| **Boîte du couple** | `Sandbox boite AgMakeout : centre X=20.831 Y=1.400 Z=-0.534, etendue X=41.180 Y=50.958 Z=89.466` | +| Poses | `AgMakeout_F` sur Bridget, `AgMakeout_M` sur Michael, puis `B_/M_Idle_Loop` | +| Suite de session | séquence automatique de mesure (9 caméras, `AUTO arret`), sans erreur ; session restée active (`Responding = True`) jusqu'à l'arrêt manuel | +| Erreurs module / lien / GameMode | 0 | +| `: Error:` hors lot | 1 × `LogLifePlay: Error: Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable` (déjà connu, S3-VAL / S4-VAL) | + +**Lecture de la boîte.** La ligne `Sandbox boite` est produite par le chemin refactoré (`MeshToActor` → `AccumulateAnimBounds` → `FitFromAnimBounds` → `EnsureBox` → `ApplyBoxFit`). Fond de boîte : `Center.Z - Extent.Z = -0.534 - 89.466 = -90.000`, soit exactement `18 - PlayerSpot.Z` (= −90) : l'écart de plancher sandbox / jeu de 2 cm (jeu = −88), **conservé volontairement** par S5-T2 (§4 de `S5-IMPL-H.md`), est donc inchangé. Pas de NaN, pas de rejet (`bValid`), extension sous le plafond de 800 cm. + +Limite de la comparaison avant / après : aucune capture antérieure de cette paire (`AgMakeout`) n'a été archivée ; le contrôle de parité numérique stricte (écart attendu : aucun) n'est pas fait. Le fond à −90,000 cm et des valeurs plausibles pour une paire debout sont la seule preuve de parité disponible ici. + +## 2. Compilation + +```powershell +& 'D:\Game\Launcher\Engine\UE_5.5\Engine\Build\BatchFiles\Build.bat' LifePlayEditor Win64 Development ` + -Project='D:\Game\LifePlay-master 5.5\LifePlay.uproject' -WaitMutex +``` + +Résultat : `Target is up to date` (journal `Saved/Logs/S5-VAL-build.log`). La compilation effective des deux cibles (`LPSandboxEditor` non-unity, `LifePlayEditor` unity, 0 erreur / 0 avertissement) est documentée par `S5-IMPL-H.md` §6 ; cette fiche confirme qu'aucune source n'a changé depuis (DLL postérieures). + +## 3. ShowCouple : PASS-STATIC + +| Contrôle | Résultat | +|---|---| +| `LPShowCoupleSubsystem.cpp` référence `LPCoupleSim` / `ResolveCoupleMove` / `EnsureBox` | 34 occurrences | +| `AddPose`, `FSavedMove` imbriquée, `SweepMultiByChannel` dans le jeu | 0 (supprimés) | +| Algorithme de balayage / échantillonnage de pose dans l'addon | une seule implémentation : `LPCoupleSimCore.cpp` (+ déclarations de `LPCoupleSim.h`) ; plus de copie dans `LPSandbox/LPCoupleSim.cpp` | +| Dépendances `.Build.cs` | inchangées (`LifePlay` → `LPAnimMeasure` déjà public) | +| Le module `LifePlay` (qui contient le sous-système) charge au runtime Pair | oui (DLL du 20:37 chargée, aucun `LogModuleManager` en erreur) | + +## 4. Limites et décision PASS-STATIC + +- **Pas de parcours show automatique.** `LPShowCoupleSubsystem` ne s'active qu'avec un vrai couple en scène (GI, `Room`, `SexPersons`, `AP1/AP2`, clips du dossier `/Game/Heavy/New2/Daily/...`), donc depuis une sauvegarde et un Blueprint de scène ; le bat Pair utilise `GameInstance` brut (`-ini:...GameInstanceClass=/Script/Engine.GameInstance`) et ne l'exerce pas. Aucune ligne `Show couple` dans le journal Pair. +- **Conséquence :** `Engage` / `KeepTogether` / `Release` du jeu, la dépénétration, les lits (`IsBedMesh`), la coque `Wall*` et la diffusion `OnIgnoredShell` ne sont validés que par compilation et relecture (PASS-STATIC), pas par exécution. +- `LPPairCollisionTest` (commandlet de l'éditeur) **non relancé** dans cette fiche (hors consigne VAL ; recommandé en S5-T5 / S7 si disponible dans le module éditeur). +- Contrôle visuel non fait ; le mode Interaction n'est pas exercé. +- Alignement du plancher (−90 → −88) : **non décidé** ; reste à trancher par une capture avant / après sur une scène du jeu. + +## 5. Reste ouvert + +1. Un essai de show complet en jeu (nouvelle partie ou sauvegarde, scène à deux, comparer `Show couple : boite ...` avant / après `c1fdd29`). +2. Mise sous suivi git de `LPShowCoupleSubsystem.{h,cpp}` dans le repo jeu (non suivis, cf. `S5-IMPL-H.md`). +3. Tests unitaires du niveau pur (`MeshToActor`, `FitFromAnimBounds`, `FitFromLiveBounds`, `SlideWish`). diff --git a/Docs/Migration/etat/sandbox/S6/S6-VAL.md b/Docs/Migration/etat/sandbox/S6/S6-VAL.md new file mode 100644 index 0000000..4d298db --- /dev/null +++ b/Docs/Migration/etat/sandbox/S6/S6-VAL.md @@ -0,0 +1,81 @@ +# S6-VAL — Validation du lot S6 (stack Mesure sur liaison addon) + +- **Date :** 2026-10-06 +- **Fiche :** S6-T3 | **Rôle :** A-VAL +- **Modèle prévu :** `composer-2.5-fast` | **Modèle utilisé :** `composer-2.5-fast` +- **Pin addon :** `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (`S3-INT.md`, inchangé depuis S5) +- **Références :** `S2-VAL.md` (module + bat Mesure), `S3-VAL.md` (plugin monté), `20_Chaine_Animations.md`, `09_Commandes_Utiles.md` + +## Verdict : **PASS** + +| # | Contrôle | Résultat | +|---|---|---| +| 1 | `LifePlayEditorTools.Build.cs` : `LPAnimMeasure` en `PublicDependencyModuleNames` | **PASS** (l. 22, commentaire lot S2) | +| 2 | Includes commandlets anim (`LPPoseMeasure.h`, `LPSkeletonProfile.h`, `LPAnimChecks.h`) sans `Source/LPAnimMeasure/` dans le jeu | **PASS** (module uniquement dans l'addon ; 0 fichier sous `Source/LPAnimMeasure/`) | +| 3 | `LifePlayEditor Win64 Development` | **PASS** (`Target is up to date`, 2026-10-06) | +| 4 | Commandlet court `-run=LPAnimEval` (addon chargé) | **PASS** (1 clip, 1 instant, ~1,3 s, code 0, `rapport.json` `valide: true`) | +| 5 | Bat Mesure sur liaison addon | **PASS** (déjà `S2-VAL.md` : 269 lignes `LogLPAnimMeasure`, sorties `Saved/Measurement/` ; non rejoué ici) | + +## 1. S6-T1 — Build et linkage + +`Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs` : + +```csharp +PublicDependencyModuleNames.AddRange(new string[] +{ + // ... + "LifePlay", + "LPAnimMeasure" +}); +``` + +Consommateurs vérifiés (includes inchangés, résolus via le module plugin) : + +| Fichier | Headers `LPAnimMeasure` | +|---|---| +| `LPAnimEvalCommandlet.cpp` | `LPPoseMeasure.h`, `LPSkeletonProfile.h`, `LPAnimChecks.h` | +| `LPAnimCatalogCommandlet.cpp` | `LPPoseMeasure.h` | +| `LPAnimTwistCommandlet.cpp` | `LPPoseMeasure.h`, `LPSkeletonProfile.h` | +| `LPAnimChecks.cpp` | `LPPoseMeasure.h`, `LPSkeletonProfile.h` | + +`Source/LifePlay/LifePlay.Build.cs` : `PublicDependencyModuleNames.Add("LPAnimMeasure")` (commentaire S2/S3). + +Binaires (jeu + plugin sibling) : + +| DLL | Horodatage | +|---|---| +| `UnrealEditor-LPAnimMeasure.dll` | 2026-10-06 20:37:07 | +| `UnrealEditor-LifePlayEditorTools.dll` | 2026-10-06 19:20:23 | + +Aucune correction de sources nécessaire pour S6-T1. + +## 2. S6-T2 — Documentation + +Mises à jour : `Docs/Migration/20_Chaine_Animations.md` (§ 1 état actuel + écart pose T), `Docs/Migration/09_Commandes_Utiles.md` (§ 3 : bats `Tools/debug`, sorties `Saved/Measurement`, commandlets `LPAnimEval` / `LPAnimTwist` / `LPAnimCatalog`). + +## 3. S6-T3 — Commandlet `LPAnimEval` (court) + +```powershell +$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' +$Proj = 'D:\Game\LifePlay-master 5.5\LifePlay.uproject' +& $UE $Proj -run=LPAnimEval "-Clips=Idle_Loop" "-Instants=1" ` + "-Out=D:\Game\LifePlay-master 5.5\Saved\AnimPipeline\S6-VAL" -unattended -nopause ` + -abslog="D:\Game\LifePlay-master 5.5\Saved\Logs\S6-VAL-LPAnimEval.log" +``` + +| Constat | Valeur | +|---|---| +| Code de sortie | 0 | +| Durée commandlet | ~1,34 s | +| `rapport.json` | `"valide": true`, 1 clip, 2 corps, 0 échec | +| Pose T | `rapport_poseT_Bridget.json`, `rapport_poseT_Michael.json` (profil `genesis8`, symétrie 0,000°) | +| Évaluation | `eval_Bridget.csv`, `eval_Michael.csv` ; pire formule ≈ 10⁻⁷ %, pire clip ≈ 3×10⁻⁵ % | +| Journal | `LogLPAnimMeasure` (POSET) actif — module addon bien lié | + +Artefacts locaux (non versionnés) : `Saved/AnimPipeline/S6-VAL/`, `Saved/Logs/S6-VAL-LPAnimEval.log`. + +## 4. Limites + +- Pas de relecture du bat Mesure ni comparaison `tour.csv` après S3 (report sur preuve S2-VAL + commandlet ci-dessus). +- Pas de relance complète 43 clips × 3 instants (déjà AP4 / CHG-208). +- Contrôle visuel sandbox non refait. diff --git a/Docs/Migration/etat/sandbox/S7/S7-CART.md b/Docs/Migration/etat/sandbox/S7/S7-CART.md new file mode 100644 index 0000000..b52165c --- /dev/null +++ b/Docs/Migration/etat/sandbox/S7/S7-CART.md @@ -0,0 +1,70 @@ +# S7-T3 - Cartographie de conformite du plugin `LPSandbox` (contrat S7-SPEC) + +- **Fiche :** S7-T3 | **Role :** CART | **Date :** 2026-10-06 +- **Cible :** `D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\Source` (modules `LPSandbox`, `LPAnimMeasure`) +- **Outil :** `rg` (ripgrep). Compte hors commentaires precise quand applicable. + +## Resultats des grep exiges + +| # | Motif | Portee | Resultats | Attendu | +|---|---|---|---:|---:| +| 1 | `#include ` / `"LifePlay.h"` | `Source/**` | **0** | 0 | +| 2 | `LIFEPLAY_API` | `Source/**` | **0** | 0 | +| 3 | `"LifePlay"` ou `"LifePlayEditorTools"` (module) | `*.Build.cs` | **0** | 0 | +| 4 | `^\s*#include.*LifePlay` (tout include mentionnant LifePlay) | `Source/**` | **0** | 0 | +| 5 | `LifePlay` (mot, tous contextes) | `Source/**` | 16 | commentaires + 1 nom de section ini (voir ci-dessous) | +| 6 | `SpawnActor.*Person`, `ConstructorHelpers`, `TSubclassOf<`, `TObjectPtrGetString(TEXT("LifePlay.Anim"), TEXT("Target"), ...)` : **nom de section d'un fichier `.ini`** (donnee), pas une dependance de code. + +## Cases du contrat + +### C1 - Aucun lien vers `LifePlay` + +- [x] Aucun `#include "LifePlay.h"` ni header du module jeu (grep #1, #4 : 0). +- [x] Aucun `LIFEPLAY_API` (grep #2 : 0). +- [x] Aucune dependance de module `LifePlay` / `LifePlayEditorTools` dans les `*.Build.cs` (grep #3 : 0 ; `LPSandbox` -> `Core`, `CoreUObject`, `Engine`, `InputCore`, `SlateCore`, `LPAnimMeasure`, `Slate`, `ImageWrapper`, `AssetRegistry`, `Json` ; `LPAnimMeasure` -> `Core`, `CoreUObject`, `Engine`, `Json`). +- [x] Aucun chemin `Source/LifePlay` hors commentaires. +- [x] Graphe acyclique `LPSandbox -> LPAnimMeasure` ; `LPAnimMeasure` sans dependance vers `LPSandbox` (verifie dans `LPAnimMeasure.Build.cs`). + +### C2 - Aucun chargement de graphe `GI` / `Person` / `Room` + +- [x] Aucune reference a `GI` du jeu : `\bGI\b`, `BPs/GI`, `BPs/Room`, `BP_Room` -> **0** sur le code (le motif `\bRoom\b` donne 66 hits : tous des **variables locales** `ALPPairSandboxStage* Room`, type du plugin, aucune relation avec le Blueprint `Room` du jeu). +- [x] `Person` : lecture de CDO uniquement. Deux sites, tous deux `LoadObject(nullptr, *FLPSandboxConfig::Get().PersonBP)` puis `GetDefaultObject()` : + - `LPSandboxRoomBuilder.cpp:119-120` ; + - `LPCoupleSim.cpp:39-40`. + Aucun `SpawnActor` de `Person`, aucun `CallFunctionByName`. Le seul `ProcessEvent` du plugin (`LPSandboxRoomBuilder.cpp:308`) vise `BP_Sky_Sphere` du **moteur**. +- [x] Chemin du Blueprint : `Config/LPSandbox.ini` `[Sandbox.Assets] PersonBP` (defaut `/Game/BPs/Person.Person_C`, `LPSandboxConfig.h:15`). +- [x] Lanceur : `GameInstanceClass=/Script/Engine.GameInstance` (`Tools/debug/LifePlay_pair_sandbox.bat`) ; le journal runtime confirme `Game class is 'LPPairSandboxGameMode'` et « profil Person lu ». L'unique `LogLifePlay: Error: Coiffures : GI.HairstyleTable ...` est emis par le **module jeu** (categorie `LogLifePlay`, pas `LogLPSandbox`) : erreur connue (S3-VAL, S4-VAL, S5-VAL), non liee au plugin. + +### C3 - `SoftObjectPath` / config seulement + +- [x] Aucun `ConstructorHelpers`, `TSubclassOf<>` ni `TObjectPtr<>` vers un type du jeu (grep #6 : 0). +- [x] Un seul litteral `/Game/` hors config : `LPAnimRedirect.cpp:17` (`/Game/Quaternius/UAL`), **defaut interne** documente (S2), surcharge par `LPAnimRedirect::SetUALRoot` (poussee par le module jeu depuis la config) ; asset de contenu tiers, pas de code du jeu. +- [x] `LoadObject` par chaine (config) et `AssetRegistry` + `GetSoftObjectPath().ToString()` (`LPCoupleSim.cpp:227,236`) pour Quaternius. +- [x] Repli au litteral historique si fichier/cle absents (S1) ; journal `LogLPSandbox` sur asset introuvable. + +### C4 - Montage sibling documente + +- [x] Layout sibling + decision « pas de submodule » : `21_Sandbox_Addon.md` (section S3) et `S3-INT.md`. +- [x] `LifePlay.uproject` : `AdditionalPluginDirectories` = `../LifePlay-Sandbox/Plugins` ; `LPSandbox` `Enabled: true`, `Optional: true` (S7-T2). +- [x] Pin consigne : `S3-INT.md` (maj S7-T4 -> `f09c42e`). +- [x] README de l'addon : layout, clone sibling, junction `Content` (S3). + +### C5 - Configurations + +- [x] `SupportedTargetPlatforms: ["Win64"]`. +- [x] `LPSandbox` : `TargetConfigurationDenyList: ["Shipping"]`. +- [x] `LPAnimMeasure` : aucune deny-list. +- [x] Pas de `TargetAllowList: ["Editor"]`. + +## Ecarts / observations + +| # | Observation | Gravite | +|---|---|---| +| 1 | `LoadObject(PersonBP)` charge le **package** `Person` (donnees serialisees) : le contrat C2 interdit l'execution, pas ce chargement ; inevitable pour lire le CDO. | Info | +| 2 | Section ini `[LifePlay.Anim]` lue par `LPAnimRedirect` : couplage de **nom** (donnee), pas de code. | Info | +| 3 | `Optional: true` ne dispense pas du clone sibling pour compiler le jeu (`LifePlay.Build.cs` -> `LPAnimMeasure`). | Limite (documentee S7-IMPL-L) | + +**Verdict CART : conforme (0 ecart bloquant).** diff --git a/Docs/Migration/etat/sandbox/S7/S7-IMPL-L.md b/Docs/Migration/etat/sandbox/S7/S7-IMPL-L.md new file mode 100644 index 0000000..1ba9daa --- /dev/null +++ b/Docs/Migration/etat/sandbox/S7/S7-IMPL-L.md @@ -0,0 +1,31 @@ +# S7-T2 - Implementation : plugin `LPSandbox` hors Shipping + +- **Fiche :** S7-T2 | **Role :** IMPL-L | **Date :** 2026-10-06 +- **Contrat :** `S7-SPEC.md` §C5 +- **Commit addon :** `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (`main`, local, non pousse) : `S7-T2: uplugin SupportedTargetPlatforms Win64, LPSandbox module deny Shipping` + +## Choix + +| Option proposee | Decision | Motif | +|---|---|---| +| `TargetAllowList: ["Editor"]` | **Rejetee** | Le sandbox tourne en `-game` (cible Game Development / DebugGame via `UnrealEditor.exe -game`) : trop strict. | +| `TargetAllowList: ["Editor", "Game"]` (module) | Rejetee | Equivalent a exclure Server / Client / Program ; ne sort pas Shipping (Shipping est une **configuration**, pas un type de cible). | +| `TargetAllowList: ["Editor", "Program"]` | Rejetee | Exclut la cible Game Development, qui monte `LPSandbox` (preuve S7-VAL §3). | +| **`TargetConfigurationDenyList: ["Shipping"]` sur le module `LPSandbox`** | **Retenue** | Supportee par UE 5.5 (`ModuleDescriptor.cpp:220`, `ModuleDescriptor.cs:327`). Exclut `LPSandbox` du Shipping seulement. | +| Meme deny-list sur `LPAnimMeasure` | **Rejetee** | `LifePlay.Build.cs` depend de `LPAnimMeasure` (`LPDirectControlSubsystem`, `LPShowCoupleSubsystem` -> `LPCoupleSim`) : un Shipping du jeu casserait. | +| `Optional: true` sur le plugin dans `LifePlay.uproject` | **Retenue (limitee)** | Evite l'erreur « plugin introuvable » au chargement de l'editeur si l'addon voisin n'est pas clone. Ne remplace pas le build : `LifePlay.Build.cs` exige `LPAnimMeasure` (voir Limites). | + +## Modifications + +1. `D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\LPSandbox.uplugin` + - `"SupportedTargetPlatforms": ["Win64"]` ; + - module `LPSandbox` : `"TargetConfigurationDenyList": ["Shipping"]` ; + - `Description` : precise que `LPSandbox` n'est pas compile en Shipping et que `LPAnimMeasure` l'est toujours. +2. `D:\Game\LifePlay-master 5.5\LifePlay.uproject` : plugin `LPSandbox` -> `"Enabled": true, "Optional": true`. + +## Limites + +- **Pas de jeu empaquete avec sandbox.** En Shipping, le GameMode `LPPairSandboxGameMode` n'existe pas ; les `.bat` `Tools/debug/*_sandbox.bat` ne s'appliquent qu'a Development / DebugGame. +- **`Optional` ne dispense pas du clone sibling pour compiler.** Sans `../LifePlay-Sandbox/Plugins`, `LifePlay.Build.cs` ne trouve pas `LPAnimMeasure` : le build du jeu echoue. `Optional` ne couvre que le chargement du projet sans module a compiler. +- **Commit addon non pousse** et pin (`S3-INT.md`) mis a jour vers `f09c42e` : un `git push` de l'addon est necessaire avant de partager le jeu. +- Aucun changement de code C++ ; `LPAnimMeasure.Build.cs` / `LPSandbox.Build.cs` inchanges. diff --git a/Docs/Migration/etat/sandbox/S7/S7-SPEC.md b/Docs/Migration/etat/sandbox/S7/S7-SPEC.md new file mode 100644 index 0000000..f1a41a9 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S7/S7-SPEC.md @@ -0,0 +1,58 @@ +# S7-T1 - Contrat d'isolation du plugin `LPSandbox` (jeu <-> addon) + +- **Fiche :** S7-T1 | **Role :** A-ARCH | **Date :** 2026-10-06 +- **Perimetre :** specification. Les controles sont rejoues en `S7-CART.md` (S7-T3), l'implementation est en `S7-T2`. +- **Amont :** `21_Sandbox_Addon.md` (S1 contrat de config, S2 module `LPAnimMeasure`, S3 liaison sibling, S5 `LPCoupleSim`). +- **Plugin :** `D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\` (modules `LPSandbox`, `LPAnimMeasure`). + +Le plugin doit rester **sans connaissance compilee du jeu** : le jeu depend du plugin (`LifePlay.Build.cs` -> `LPAnimMeasure`), jamais l'inverse. +Toute information du jeu consommee par le plugin passe par un chemin d'asset (config) ou par la reflexion UE. + +## Checklist de contrat (a verifier a chaque lot touchant l'addon) + +### C1 - Aucun `include` / lien vers `LifePlay` dans le plugin + +- [ ] Aucun `#include "LifePlay.h"` ni header du module `LifePlay` (`LPShowCoupleSubsystem.h`, `LPDirectControlSubsystem.h`, ...). +- [ ] Aucun symbole `LIFEPLAY_API` (ni export, ni usage). +- [ ] Aucune entree `"LifePlay"` / `"LifePlayEditorTools"` dans `PublicDependencyModuleNames`, `PrivateDependencyModuleNames`, `DynamicallyLoadedModuleNames` des `*.Build.cs` du plugin. +- [ ] Aucun chemin `Source/LifePlay` dans le code (hors commentaires de provenance). +- [ ] Graphe de modules : `LPSandbox -> LPAnimMeasure` uniquement ; `LPAnimMeasure` ne depend ni de `LPSandbox` ni du jeu. + +Verification : `rg` sur `Plugins/LPSandbox/Source` (motifs en `S7-CART.md`). Attendu : 0 resultat hors commentaires. + +### C2 - Aucun chargement de graphe Blueprint `GI` / `Person` / `Room` + +- [ ] Pas de reference a `GI`, `Room`, `BP_Room`, `GameInstance` du jeu (nom ou chemin `/Game/BPs/GI`, `/Game/BPs/Room`). +- [ ] Le seul Blueprint du jeu touche est **`Person`**, uniquement pour **lire le CDO** de la classe (rayon / demi-hauteur de capsule, profil et reponses de collision) : + `LoadObject(PersonBP)` puis `GetDefaultObject()`. Aucune instance (`SpawnActor`), aucun appel de fonction Blueprint, aucun `ProcessEvent`. +- [ ] Le chemin du Blueprint vient de `Config/LPSandbox.ini` (`[Sandbox.Assets] PersonBP`), jamais d'un `StaticClass()` / include C++. +- [ ] Le lanceur neutralise l'instance de jeu du Blueprint : `-ini:Engine:[/Script/EngineSettings.GameMapsSettings]:GameInstanceClass=/Script/Engine.GameInstance` (deja dans `Tools/debug/LifePlay_pair_sandbox.bat`). + +Remarque : `LoadObject` sur `Person.Person_C` charge le package du Blueprint (donnees serialisees) ; le contrat interdit l'**execution** de ses graphes, pas la lecture du CDO. + +### C3 - `SoftObjectPath` / config uniquement + +- [ ] Tout asset du jeu (`/Game/...`) est reference par chaine de `Config/LPSandbox.ini` (19 cles, 6 sections, `S1-SPEC.md`) ou par decouverte `AssetRegistry` (`GetSoftObjectPath().ToString()`). +- [ ] Aucun `TSubclassOf<>` / `TObjectPtr<>` / `ConstructorHelpers` vers un type ou un asset du jeu en dur. +- [ ] Defaut en code = litteral historique ; fichier ou cle absents => comportement identique (repli S1). +- [ ] Un asset introuvable journalise (`LogLPSandbox` / `LogLPAnimMeasure`), sans repli silencieux ni crash. + +### C4 - Montage sibling documente + +- [ ] Jeu et addon sont deux depots **voisins** : `D:\Game\LifePlay-master 5.5\` et `D:\Game\LifePlay-Sandbox\` (pas de submodule imbrique). +- [ ] `LifePlay.uproject` : `"AdditionalPluginDirectories": ["../LifePlay-Sandbox/Plugins"]` et plugin `LPSandbox` declare (`Enabled: true`, `Optional: true`, voir S7-T2). +- [ ] Pin d'addon consigne a la main (`S3-INT.md`, README de l'addon) a chaque evolution consommee par le jeu. +- [ ] Documente dans `21_Sandbox_Addon.md` (section S3) et le README de `LifePlay-Sandbox` (layout, clone sibling, pin). +- [ ] Addon seul (`LPSandbox.uproject`) : `Content` du jeu monte par junction NTFS (documente S3). + +### C5 - Configurations de build (S7-T2) + +- [ ] `LPSandbox.uplugin` : `SupportedTargetPlatforms: ["Win64"]`. +- [ ] Module `LPSandbox` : `TargetConfigurationDenyList: ["Shipping"]` (le sandbox tourne en `-game` avec l'editeur en Development / DebugGame, jamais en Shipping). +- [ ] Module `LPAnimMeasure` : **pas** de deny-list (le module jeu `LifePlay` en depend : `LPCoupleSim`, `LPAnimRedirect`). +- [ ] Aucun `TargetAllowList: ["Editor"]` sur `LPSandbox` : trop strict, le sandbox tourne aussi en cible Game Development. + +## Hors perimetre + +- Staging de `LPSandbox.ini` pour un jeu empaquete (reste ouvert depuis S1). +- Show complet en jeu / alignement du plancher (`S5-VAL.md` §4). diff --git a/Docs/Migration/etat/sandbox/S7/S7-VAL.md b/Docs/Migration/etat/sandbox/S7/S7-VAL.md new file mode 100644 index 0000000..a9041ff --- /dev/null +++ b/Docs/Migration/etat/sandbox/S7/S7-VAL.md @@ -0,0 +1,69 @@ +# S7-VAL - Validation du lot S7 (isolation du plugin `LPSandbox`, hors Shipping) + +- **Date :** 2026-10-06 +- **Fiche :** S7-T4 | **Role :** VAL+DOC | **Entree journal :** CHG-219 +- **Pin addon :** `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (commit local `S7-T2`, **non pousse**) +- **References :** `S7-SPEC.md`, `S7-IMPL-L.md`, `S7-CART.md`, `S5-VAL.md` (parite du smoke) + +## Verdict : **PASS** (PASS runtime Pair ; Shipping verifie par plan UBT, pas par compilation complete) + +| # | Controle | Resultat | +|---|---|---| +| 1 | Grep `LifePlay.h`, `LIFEPLAY_API`, module `LifePlay` dans le plugin | **PASS** (0 / 0 / 0, `S7-CART.md`) | +| 2 | Aucune lecture de `GI` / `Room` du jeu ; `Person` = CDO seul | **PASS** (`S7-CART.md` C2) | +| 3 | `LifePlayEditor Win64 Development` apres changement `uplugin` + `uproject` | **PASS** (`Target is up to date`, code 0, `Saved/Logs/S7-VAL-build.log`) | +| 4 | Cible Game **Development** : `LPSandbox` toujours inclus | **PASS** (`LifePlay.uhtmanifest` Development : 6 occurrences `LPSandbox`) | +| 5 | Cible Game **Shipping** : `LPSandbox` exclu | **PASS** (`LifePlay.uhtmanifest` Shipping : 0 occurrence ; plan UBT `-SkipBuild`, code 0, pas d'erreur de module manquant) | +| 6 | Smoke bat Pair (court) | **PASS** (§2) | +| 7 | Aucun nouveau dossier `Saved/Crashes`, processus tue apres essai | **PASS** | +| 8 | 21 §Acceptation | **Partiel** (cases S7 cochees ; reste S8 ouvert, voir `21_Sandbox_Addon.md`) | + +## 1. Build + +```powershell +& 'D:\Game\Launcher\Engine\UE_5.5\Engine\Build\BatchFiles\Build.bat' LifePlayEditor Win64 Development ` + -Project='D:\Game\LifePlay-master 5.5\LifePlay.uproject' -WaitMutex +``` + +Resultat : `Target is up to date`, 6,7 s, code 0 (aucune source C++ modifiee ; un changement d'`uplugin` ne force pas de recompilation). + +## 2. Smoke Pair (bat court) + +Equivalent du bat `Tools/debug/LifePlay_pair_sandbox.bat` (meme URL `game=/Script/LPSandbox.LPPairSandboxGameMode`, `-game -windowed -ResX=1600 -ResY=900`, meme `GameInstanceClass`), `Saved/sandbox_mode.txt` = `Pair`, lance a 20:54:50, observe ~1 min 45 s (`Responding = True`), puis arrete. Journal copie : `Saved/Logs/S7-VAL-runtime-game.log` (1 378 lignes, copie locale non versionnee). + +| Constat | Journal | +|---|---| +| Plugin monte | `LogPluginManager: Mounting External plugin LPSandbox` | +| GameMode resolu | `LogLoad: Game class is 'LPPairSandboxGameMode'` | +| Init sandbox | `LogLPSandbox` : 4 murs, collision (54 meubles, 0 absent, capsule 20 / 88, **profil Person lu**), eclairage, interaction (286 actions), Quaternius (43 anims), corps `bridget_gp` / `michael` | +| Boite du couple | `Sandbox boite AgMakeout : centre X=20.831 Y=1.400 Z=-0.534, etendue X=41.180 Y=50.958 Z=89.466` : **identique a `S5-VAL.md`** (fond -90,000) | +| Poses | `AgMakeout_F` / `_M`, puis `B_/M_Idle_Loop`, `Walk_Loop` (entrees clavier) | +| Fatal / assertion / `ensure` / `Access violation` | 0 | +| Erreurs module / lien / GameMode | 0 | +| `: Error:` | 1 : `LogLifePlay: Error: Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable` (connu : S3 / S4 / S5-VAL ; module jeu) | +| Nouveau dossier `Saved/Crashes` | 0 | + +La parite numerique de la boite avec S5-VAL confirme qu'aucun comportement de sandbox n'a change. + +## 3. Shipping (plan UBT) + +```powershell +Build.bat LifePlay Win64 Shipping -Project=... -WaitMutex -SkipBuild -NoMutex # exit 0 +Build.bat LifePlay Win64 Development -Project=... -WaitMutex -SkipBuild -NoMutex # exit 0 +``` + +| Cible | `LPSandbox` dans `LifePlay.uhtmanifest` | +|---|---:| +| Development | 6 | +| Shipping | **0** | + +Le manifeste n'enumere que les modules avec en-tetes reflechis ; `LPAnimMeasure` (sans `UCLASS`) n'y figure pas dans les deux cas, la deny-list n'y touche de toute facon pas. + +## 4. Limites + +- **Pas de compilation ni de packaging Shipping complets** : seul le plan UBT (resolution des modules) est verifie. +- Smoke : bat equivalent lance a la main (pas le `.bat` lui-meme) ; 1 min 45 s, pas de scenario d'interaction prolonge. +- Parcours Show complet en jeu et alignement du plancher (-90 / -88) : non exerces (inchange depuis S5-VAL). +- **Commit addon `f09c42e` non pousse** : le pin dans `S3-INT.md` / README pointe un commit local. +- `Optional: true` n'evite pas l'echec du build du jeu sans clone sibling (`LifePlay.Build.cs` -> `LPAnimMeasure`). +- Ancien libelle S7 de `21` (« commandlets anim restants dans le module editeur ») **non traite** ici ; reporte (voir §Acceptation). diff --git a/Docs/Migration/etat/sandbox/S8/S8-VAL.md b/Docs/Migration/etat/sandbox/S8/S8-VAL.md new file mode 100644 index 0000000..9f2aa31 --- /dev/null +++ b/Docs/Migration/etat/sandbox/S8/S8-VAL.md @@ -0,0 +1,57 @@ +# S8-VAL - Smoke Pair final (fiche S8-T2) + +- **Date :** 2026-10-06 +- **Fiche :** S8-T2 | **Rôle :** A-DOC+Orch (validation de clôture) | **Entrée journal :** CHG-220 +- **Modèle prévu :** `claude-opus-5-5-medium` (M4) | **Modèle utilisé :** Claude Opus 5.5 (déclaré par l'environnement ; niveau d'effort non vérifiable depuis la session) +- **Pin addon :** `f09c42e5a2bc613cf269cce76c878461ca41c8b7` ; `HEAD` addon `dae6206` (doc seule, S7-T4) ; **`origin/main` = `dae6206`** (`git ls-remote origin main`) : le pin est désormais poussé +- **Références :** `S7-VAL.md` (dernier smoke), `S5-VAL.md` (référence de la boîte), `22_Eval_Agents_Sandbox.md` + +## Verdict : **PASS** + +| # | Contrôle | Résultat | +|---|---|---| +| 1 | Lancement par le **`.bat` lui-même** (`Tools/debug/LifePlay_pair_sandbox.bat`), pas une ligne équivalente | **PASS** (levée de la limite S7-VAL « bat équivalent lancé à la main ») | +| 2 | Plugin monté depuis `AdditionalPluginDirectories`, GameMode Pair résolu | **PASS** | +| 3 | Initialisation sandbox (`LogLPSandbox`) | **PASS** (16 lignes) | +| 4 | Boîte du couple identique à S5-VAL / S7-VAL | **PASS** (valeurs identiques à 10⁻³ cm) | +| 5 | Fatal / assertion / `ensure` / `Access violation` / module introuvable / `LNK2` | **PASS** (0) | +| 6 | Nouveau dossier `Saved/Crashes`, processus résiduel | **PASS** (0 / 0) | +| 7 | `Saved/sandbox_mode.txt` | `Pair` avant et après (inchangé) | + +## 1. Déroulé + +| Heure (locale) | Événement | +|---|---| +| 21:02:35 | `cmd /c Tools\debug\LifePlay_pair_sandbox.bat` (processus `UnrealEditor` 17896) | +| avant 21:02:45 | `LogPluginManager: Mounting External plugin LPSandbox` (ligne non horodatée, au démarrage du moteur) | +| 21:02:45 | erreur coiffures connue (`LogLifePlay`) | +| 21:02:47 | `LogLoad: Game class is 'LPPairSandboxGameMode'` | +| 21:03:20 – 21:03:21 | 16 lignes `LogLPSandbox` (murs, collision, éclairage, interaction, Quaternius, corps, boîte, poses) | +| 21:04:22 | arrêt forcé (`Stop-Process`) ; `Responding = True` jusque-là | + +Fenêtre demandée ~30–45 s : **non tenable à l'identique**, l'initialisation de la carte prend à elle seule ~46 s +(S4-VAL : ~32 s, S5-VAL : ~50 s). Session observée ~1 min 47 s au total, dont ~60 s après la fin de l'initialisation. +Binaires utilisés : `UnrealEditor-LPSandbox.dll` 20:37:08, `UnrealEditor-LPAnimMeasure.dll` 20:37:07 (aucune source +modifiée depuis S7, pas de rebuild). + +## 2. Journal + +Copie : `Saved/Logs/S8-VAL-runtime-game.log` (1 366 lignes, copie locale non versionnée). + +| Constat | Ligne | +|---|---| +| Collision | `Sandbox collision : 54 meubles, 0 absents, capsule 20 / 88, profil Person lu, depart X=-508.667 Y=-499.333 Z=108.000` | +| Éclairage | `Sandbox eclairage : soleil 5 (canal 1), plein corps 1.5, ciel 0.6 (heure 9 ou 15)` | +| Interaction | `Sandbox interaction : 286 actions, 160 lieux, 12 horaires` | +| Quaternius | `corps /Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard, 43 animation(s)` | +| Corps | `bridget_gp`, `michael` | +| **Boîte** | `Sandbox boite AgMakeout : centre X=20.831 Y=1.400 Z=-0.534, etendue X=41.180 Y=50.958 Z=89.466` (fond −90,000 cm) | +| Poses | `AgMakeout_F sur Bridget`, `AgMakeout_M sur Michael` | +| `: Error:` | 1 : `LogLifePlay: Error: Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable` (connu depuis S3-VAL, module jeu, sans lien avec l'addon) | + +## 3. Limites + +- Preuve par journal uniquement ; pas de contrôle visuel, pas d'entrée clavier (Interaction, Mesure non exercés). +- `BuildMod.ps1 -Target sandbox -Open` non rejoué (la ligne S8 de `21` le cite ; reste ouvert). +- Parcours Show complet, Shipping compilé, déplacement des commandlets anim : inchangés (non faits, voir `21` § Acceptation). +- Côté jeu, aucun fichier de la migration sandbox n'est commité (`21`, `etat/sandbox/**`, `LifePlay.uproject`, `Config/LPSandbox.ini`) : voir `22` § 6. diff --git a/LifePlay.uproject b/LifePlay.uproject index ca2e115..1228961 100644 --- a/LifePlay.uproject +++ b/LifePlay.uproject @@ -18,7 +18,15 @@ ] } ], + "AdditionalPluginDirectories": [ + "../LifePlay-Sandbox/Plugins" + ], "Plugins": [ + { + "Name": "LPSandbox", + "Enabled": true, + "Optional": true + }, { "Name": "ModdingExtra", "Enabled": true diff --git a/Source/LifePlay/LPShowCoupleSubsystem.cpp b/Source/LifePlay/LPShowCoupleSubsystem.cpp new file mode 100644 index 0000000..51b5ed1 --- /dev/null +++ b/Source/LifePlay/LPShowCoupleSubsystem.cpp @@ -0,0 +1,805 @@ +// Show a deux - voir LPShowCoupleSubsystem.h + +#include "LPShowCoupleSubsystem.h" + +#include "Animation/AnimMontage.h" +#include "Animation/AnimSequence.h" +#include "Animation/AnimSingleNodeInstance.h" +#include "Components/CapsuleComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Engine/StaticMesh.h" +#include "EngineUtils.h" +#include "Misc/PackageName.h" +#include "Components/BoxComponent.h" +#include "Components/CapsuleComponent.h" +#include "Components/PrimitiveComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "DrawDebugHelpers.h" +#include "Engine/GameInstance.h" +#include "Engine/World.h" +#include "GameFramework/Character.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "GameFramework/PlayerController.h" +#include "HAL/IConsoleManager.h" +#include "LifePlay.h" +#include "LPCoupleSim.h" +#include "UObject/UnrealType.h" + +static TAutoConsoleVariable CVarShowCouple( + TEXT("lp.ShowCouple"), + 1, + TEXT("1 : boite du couple pendant le show. 0 : ancien chemin du Blueprint."), + ECVF_Default); + +namespace +{ + constexpr double ShowReach = 400.0; + + bool IsBedMesh(const UStaticMesh* Mesh) + { + if (!Mesh) + { + return false; + } + static TSet Beds; + static TSet Others; + const FName Name = Mesh->GetFName(); + if (Beds.Contains(Name)) + { + return true; + } + if (Others.Contains(Name)) + { + return false; + } + const FString MeshName = Mesh->GetName(); + const bool bBed = MeshName.Contains(TEXT("bed"), ESearchCase::IgnoreCase) + || MeshName.Contains(TEXT("fnt_ext011")) + || MeshName.Equals(TEXT("fnt_new045")) + || MeshName.Equals(TEXT("fnt_new235")); + (bBed ? Beds : Others).Add(Name); + return bBed; + } + + ACharacter* AsCharacter(UObject* Object) + { + return Cast(Object); + } + + AActor* ObjectActor(const UObject* Container, const TCHAR* Name) + { + const FObjectPropertyBase* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; + return Prop ? Cast(Prop->GetObjectPropertyValue_InContainer(Container)) : nullptr; + } + + ACharacter* ObjectProperty(const UObject* Container, const TCHAR* Name) + { + return Cast(ObjectActor(Container, Name)); + } + + void ArrayProperty(const UObject* Container, const TCHAR* Name, TArray& Out) + { + const FArrayProperty* ArrayProp = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; + const FObjectPropertyBase* Inner = ArrayProp ? CastField(ArrayProp->Inner) : nullptr; + if (!Inner) + { + return; + } + FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Container)); + for (int32 Index = 0; Index < Helper.Num(); ++Index) + { + if (ACharacter* Who = AsCharacter(Inner->GetObjectPropertyValue(Helper.GetRawPtr(Index)))) + { + Out.Add(Who); + } + } + } + + bool CapsuleOff(const ACharacter* Who) + { + const UCapsuleComponent* Cap = Who ? Who->GetCapsuleComponent() : nullptr; + return Cap && Cap->GetCollisionEnabled() == ECollisionEnabled::NoCollision; + } + + void AddUniqueAnim(TArray& Out, UAnimSequence* Anim) + { + if (Anim) + { + Out.AddUnique(Anim); + } + } + + UAnimSequence* SequenceFromMontage(const UAnimMontage* Montage) + { + if (Montage && Montage->SlotAnimTracks.Num() > 0 && Montage->SlotAnimTracks[0].AnimTrack.AnimSegments.Num() > 0) + { + return Cast(Montage->SlotAnimTracks[0].AnimTrack.AnimSegments[0].GetAnimReference()); + } + return nullptr; + } + + UAnimSequence* PlayingSequence(const USkeletalMeshComponent* Mesh) + { + if (!Mesh) + { + return nullptr; + } + if (UAnimSequence* Seq = Cast(Mesh->AnimationData.AnimToPlay)) + { + return Seq; + } + const UAnimInstance* Instance = Mesh->GetAnimInstance(); + if (UAnimSequence* Seq = SequenceFromMontage(Instance ? Instance->GetCurrentActiveMontage() : nullptr)) + { + return Seq; + } + if (UAnimSingleNodeInstance* Single = Cast(const_cast(Instance))) + { + return Cast(Single->GetCurrentAsset()); + } + return nullptr; + } + + bool IsIdleClip(const UAnimSequence* Anim) + { + return Anim && Anim->GetName().Contains(TEXT("idle"), ESearchCase::IgnoreCase); + } + + UAnimSequence* PairSibling(const UAnimSequence* Anim) + { + if (!Anim) + { + return nullptr; + } + const FString Name = Anim->GetName(); + FString Other; + if (Name.EndsWith(TEXT("_F"))) + { + Other = Name.LeftChop(2) + TEXT("_M"); + } + else if (Name.EndsWith(TEXT("_M"))) + { + Other = Name.LeftChop(2) + TEXT("_F"); + } + else if (Name.EndsWith(TEXT("_0"))) + { + Other = Name.LeftChop(2) + TEXT("_1"); + } + else if (Name.EndsWith(TEXT("_1"))) + { + Other = Name.LeftChop(2) + TEXT("_0"); + } + else + { + return nullptr; + } + const FString Folder = FPaths::GetPath(Anim->GetOutermost()->GetName()); + const FString Path = Folder + TEXT("/") + Other + TEXT(".") + Other; + return LoadObject(nullptr, *Path); + } + + void CompletePair(TArray& Out) + { + TArray Extra; + for (const UAnimSequence* Anim : Out) + { + AddUniqueAnim(Extra, PairSibling(Anim)); + } + for (UAnimSequence* Anim : Extra) + { + Out.AddUnique(Anim); + } + } + + void KeepShowClips(TArray& Clips) + { + Clips.RemoveAll([](const UAnimSequence* Anim) { return IsIdleClip(Anim); }); + CompletePair(Clips); + } + + void ReadAnimProperty(const FProperty* Prop, const void* Container, TArray& Out) + { + if (const FArrayProperty* ArrayProp = CastField(Prop)) + { + const FObjectPropertyBase* Inner = CastField(ArrayProp->Inner); + if (!Inner) + { + return; + } + FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Container)); + for (int32 Index = 0; Index < Helper.Num(); ++Index) + { + UObject* Value = Inner->GetObjectPropertyValue(Helper.GetRawPtr(Index)); + AddUniqueAnim(Out, Cast(Value)); + AddUniqueAnim(Out, SequenceFromMontage(Cast(Value))); + } + } + else if (const FObjectPropertyBase* ObjectProp = CastField(Prop)) + { + UObject* Value = ObjectProp->GetObjectPropertyValue_InContainer(Container); + AddUniqueAnim(Out, Cast(Value)); + AddUniqueAnim(Out, SequenceFromMontage(Cast(Value))); + } + } + + void CollectShowAnims(const UObject* GI, const ACharacter* Lead, const ACharacter* Partner, TArray& Out, FString& Label) + { + TArray FromShow; + if (const FStructProperty* Last = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("LastAnimation"))) : nullptr) + { + if (Last->Struct) + { + const void* Data = Last->ContainerPtrToValuePtr(GI); + for (TFieldIterator It(Last->Struct); It; ++It) + { + ReadAnimProperty(*It, Data, FromShow); + } + } + } + KeepShowClips(FromShow); + if (FromShow.Num() == 0) + { + auto FromActor = [&](const ACharacter* Who) + { + if (!Who) + { + return; + } + TInlineComponentArray Meshes; + Who->GetComponents(Meshes); + for (const USkeletalMeshComponent* Mesh : Meshes) + { + AddUniqueAnim(FromShow, PlayingSequence(Mesh)); + } + }; + FromActor(Lead); + FromActor(Partner); + KeepShowClips(FromShow); + } + Out = MoveTemp(FromShow); + for (const UAnimSequence* Anim : Out) + { + if (!Label.IsEmpty()) + { + Label += TEXT("+"); + } + Label += Anim->GetName(); + } + } + +} + +void ULPShowCoupleSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + PostTickHandle = FWorldDelegates::OnWorldPostActorTick.AddUObject(this, &ULPShowCoupleSubsystem::AfterActors); +} + +void ULPShowCoupleSubsystem::Deinitialize() +{ + FWorldDelegates::OnWorldPostActorTick.Remove(PostTickHandle); + Release(); + Super::Deinitialize(); +} + +void ULPShowCoupleSubsystem::AfterActors(UWorld* World, ELevelTick TickType, float DeltaSeconds) +{ + (void)TickType; + (void)DeltaSeconds; + if (!World || World != GetWorld() || !World->IsGameWorld()) + { + return; + } + if (CVarShowCouple.GetValueOnGameThread() == 0) + { + OpenUseFurniture(); + LetSittersPass(World); + Release(); + return; + } + + UGameInstance* GI = GetGameInstance(); + TArray Found; + ArrayProperty(GI, TEXT("SexPersons"), Found); + if (Found.Num() < 2) + { + Found.Reset(); + if (ACharacter* A = ObjectProperty(GI, TEXT("AP1"))) + { + Found.Add(A); + } + if (ACharacter* B = ObjectProperty(GI, TEXT("AP2"))) + { + Found.Add(B); + } + } + ACharacter* Player = nullptr; + if (const APlayerController* PC = World->GetFirstPlayerController()) + { + Player = Cast(PC->GetPawn()); + } + ACharacter* NextLead = nullptr; + ACharacter* NextPartner = nullptr; + for (ACharacter* Who : Found) + { + if (!IsValid(Who) || !CapsuleOff(Who)) + { + continue; + } + if (!NextLead) + { + NextLead = (Player && Found.Contains(Player) && CapsuleOff(Player)) ? Player : Who; + } + } + if (NextLead) + { + double Best = ShowReach * ShowReach; + for (ACharacter* Who : Found) + { + if (!IsValid(Who) || Who == NextLead || !CapsuleOff(Who)) + { + continue; + } + const double Dist = FVector::DistSquared(NextLead->GetActorLocation(), Who->GetActorLocation()); + if (Dist <= Best) + { + Best = Dist; + NextPartner = Who; + } + } + } + if (!NextLead || !NextPartner) + { + OpenUseFurniture(); + LetSittersPass(World); + Release(); + return; + } + if (!bEngaged || Lead.Get() != NextLead || Partner.Get() != NextPartner) + { + Release(); + Engage(NextLead, NextPartner); + } + Weld(); + BlockRoomPawn(); + TArray Anims; + FString Label; + CollectShowAnims(GI, NextLead, NextPartner, Anims, Label); + FString Key; + for (const UAnimSequence* Anim : Anims) + { + if (!Key.IsEmpty()) + { + Key += TEXT("+"); + } + Key += Anim->GetName(); + } + if (!FitToBodies() && !Key.IsEmpty() && Key != FittedPair && Key != RejectedPair) + { + if (FitBox(Anims, Label)) + { + FittedPair = Key; + } + else + { + RejectedPair = Key; + } + } + KeepTogether(); + if (UBoxComponent* Drawn = Box.Get()) + { + DrawDebugBox(World, Drawn->GetComponentLocation(), Drawn->GetScaledBoxExtent(), Drawn->GetComponentQuat(), FColor::Green, false, -1.f, 0, 2.f); + } +} + +void ULPShowCoupleSubsystem::Engage(ACharacter* InLead, ACharacter* InPartner) +{ + Lead = InLead; + Partner = InPartner; + bEngaged = true; + LastSafe = InLead->GetActorLocation(); + SavedPartnerMove = LPCoupleSim::SaveAndFreeze(InPartner); + UE_LOG(LogLifePlay, Warning, TEXT("Show couple : branche (%s + %s)"), *GetNameSafe(InLead), *GetNameSafe(InPartner)); +} + +bool ULPShowCoupleSubsystem::IsUseFurniture(const UStaticMesh* Mesh) +{ + if (!Mesh) + { + return false; + } + const FName Name = Mesh->GetFName(); + if (UseFurniture.Contains(Name)) + { + return true; + } + if (SolidFurniture.Contains(Name)) + { + return false; + } + const TCHAR* Actions[] = { TEXT("sit"), TEXT("lie"), TEXT("use"), TEXT("work") }; + bool bUse = false; + for (const TCHAR* Action : Actions) + { + const FString Path = FString::Printf(TEXT("/Game/Heavy/New2/Daily/%s-f-0-%s"), *Mesh->GetName(), Action); + if (FPackageName::DoesPackageExist(Path)) + { + bUse = true; + break; + } + } + (bUse ? UseFurniture : SolidFurniture).Add(Name); + return bUse; +} + +void ULPShowCoupleSubsystem::OpenUseFurniture() +{ + AActor* Room = ObjectActor(GetGameInstance(), TEXT("Room")); + if (!Room) + { + return; + } + TArray Actors; + Actors.Add(Room); + TArray Attached; + Room->GetAttachedActors(Attached, true, true); + Actors.Append(Attached); + int32 Opened = 0; + for (AActor* Actor : Actors) + { + TInlineComponentArray Meshes; + Actor->GetComponents(Meshes); + for (UStaticMeshComponent* Comp : Meshes) + { + if (!Comp || !IsUseFurniture(Comp->GetStaticMesh())) + { + continue; + } + if (Comp->GetCollisionResponseToChannel(ECC_Pawn) == ECR_Ignore) + { + continue; + } + Comp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); + ++Opened; + } + } + if (Opened > 0) + { + UE_LOG(LogLifePlay, Warning, TEXT("Meubles assis ou couches : %d ouverts au passage"), Opened); + } +} + +void ULPShowCoupleSubsystem::LetSittersPass(UWorld* World) +{ + TSet Sitting; + for (TActorIterator It(World); It; ++It) + { + ACharacter* Who = *It; + UCapsuleComponent* Cap = Who ? Who->GetCapsuleComponent() : nullptr; + if (!Cap || Cap->GetCollisionEnabled() == ECollisionEnabled::NoCollision) + { + continue; + } + const UAnimSequence* Anim = PlayingSequence(Who->GetMesh()); + const FString Name = Anim ? Anim->GetName() : FString(); + const bool bPose = Name.Contains(TEXT("-sit")) || Name.Contains(TEXT("-lie")) || Name.Contains(TEXT("-use")) + || Name.Contains(TEXT("-work")) || Name.Contains(TEXT("sitting"), ESearchCase::IgnoreCase); + if (!bPose) + { + continue; + } + Sitting.Add(Cap); + bool bSeen = false; + for (const FSavedSeat& Saved : SavedSeats) + { + if (Saved.Cap.Get() == Cap) + { + bSeen = true; + break; + } + } + if (bSeen) + { + continue; + } + SavedSeats.Add({ Cap, Cap->GetCollisionResponseToChannel(ECC_WorldDynamic) }); + Cap->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Ignore); + } + for (int32 Index = SavedSeats.Num() - 1; Index >= 0; --Index) + { + UCapsuleComponent* Cap = SavedSeats[Index].Cap.Get(); + if (Cap && Sitting.Contains(Cap)) + { + continue; + } + if (Cap && Cap->GetCollisionEnabled() != ECollisionEnabled::NoCollision) + { + Cap->SetCollisionResponseToChannel(ECC_WorldDynamic, SavedSeats[Index].Response); + } + SavedSeats.RemoveAt(Index); + } +} + +void ULPShowCoupleSubsystem::BlockRoomPawn() +{ + AActor* Room = ObjectActor(GetGameInstance(), TEXT("Room")); + if (!Room) + { + return; + } + TArray Actors; + TSet Seen; + TArray Pending; + Pending.Add(Room); + for (TFieldIterator It(Room->GetClass()); It; ++It) + { + if (const FArrayProperty* ArrayProp = CastField(*It)) + { + const FObjectPropertyBase* Inner = CastField(ArrayProp->Inner); + if (!Inner) + { + continue; + } + FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Room)); + for (int32 Index = 0; Index < Helper.Num(); ++Index) + { + if (AActor* Other = Cast(Inner->GetObjectPropertyValue(Helper.GetRawPtr(Index)))) + { + Pending.Add(Other); + } + } + } + else if (const FObjectPropertyBase* ObjectProp = CastField(*It)) + { + if (AActor* Other = Cast(ObjectProp->GetObjectPropertyValue_InContainer(Room))) + { + Pending.Add(Other); + } + } + } + while (Pending.Num() > 0) + { + AActor* Actor = Pending.Pop(EAllowShrinking::No); + if (!Actor || Seen.Contains(Actor) || Cast(Actor)) + { + continue; + } + bool bInRoom = Actor == Room || Actor->GetOwner() == Room; + for (AActor* Cursor = Actor; Cursor && !bInRoom; Cursor = Cursor->GetAttachParentActor()) + { + bInRoom = Cursor == Room; + } + if (!bInRoom && FVector::DistSquared(Actor->GetActorLocation(), Room->GetActorLocation()) > FMath::Square(4000.0)) + { + continue; + } + Seen.Add(Actor); + Actors.Add(Actor); + TArray Attached; + Actor->GetAttachedActors(Attached, true, true); + Pending.Append(Attached); + } + int32 AlreadyBlock = 0; + int32 Overlap = 0; + int32 Flipped = 0; + FString Names; + for (AActor* Actor : Actors) + { + TInlineComponentArray Prims; + Actor->GetComponents(Prims); + for (UPrimitiveComponent* Prim : Prims) + { + if (!Prim || Prim->GetCollisionEnabled() == ECollisionEnabled::NoCollision || Cast(Prim)) + { + continue; + } + const ECollisionResponse Response = Prim->GetCollisionResponseToChannel(ECC_Pawn); + if (Response == ECR_Block) + { + ++AlreadyBlock; + continue; + } + if (Response == ECR_Overlap) + { + ++Overlap; + } + if (Response != ECR_Ignore) + { + continue; + } + bool bSeen = false; + for (const FSavedPawn& Saved : SavedFurniture) + { + if (Saved.Comp.Get() == Prim) + { + bSeen = true; + break; + } + } + if (bSeen) + { + continue; + } + SavedFurniture.Add({ Prim, ECR_Ignore }); + Prim->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block); + ++Flipped; + if (Flipped <= 6) + { + if (!Names.IsEmpty()) + { + Names += TEXT(", "); + } + Names += FString::Printf(TEXT("%s/%s"), *GetNameSafe(Actor), *Prim->GetName()); + } + } + } + if (!bPawnLogged || Flipped > 0) + { + bPawnLogged = true; + UE_LOG(LogLifePlay, Warning, TEXT("Show couple : Pawn chambre Block=%d Overlap=%d Ignore->Block=%d %s"), + AlreadyBlock, Overlap, Flipped, *Names); + } +} + +void ULPShowCoupleSubsystem::Release() +{ + if (!bEngaged && SavedFurniture.Num() == 0 && !Box.IsValid()) + { + return; + } + LPCoupleSim::Unweld(Partner.Get()); + LPCoupleSim::RestoreMove(SavedPartnerMove); + for (const FSavedPawn& Saved : SavedFurniture) + { + if (UPrimitiveComponent* Prim = Saved.Comp.Get()) + { + Prim->SetCollisionResponseToChannel(ECC_Pawn, Saved.Response); + } + } + SavedFurniture.Reset(); + if (UBoxComponent* Old = Box.Get()) + { + Old->DestroyComponent(); + } + Box.Reset(); + FittedPair.Reset(); + RejectedPair.Reset(); + bPawnLogged = false; + Lead.Reset(); + Partner.Reset(); + if (bEngaged) + { + UE_LOG(LogLifePlay, Warning, TEXT("Show couple : ancien chemin")); + } + bEngaged = false; +} + +void ULPShowCoupleSubsystem::Weld() +{ + ACharacter* Who = Lead.Get(); + ACharacter* Other = Partner.Get(); + if (!Who || !Other) + { + return; + } + LPCoupleSim::AttachPartner(Who, Other); + const double Half = LPCoupleSim::CapsuleHalfOf(Who); + LPCoupleSim::PlaceBodyMesh(Who, Half); + LPCoupleSim::PlaceBodyMesh(Other, Half); +} + +bool ULPShowCoupleSubsystem::FitToBodies() +{ + ACharacter* Who = Lead.Get(); + ACharacter* Other = Partner.Get(); + if (!Who || !Other) + { + return false; + } + // Voie "corps vivants" : os affiches des deux meshes. Geometrie partagee avec la sandbox + // (LPCoupleSim.h) ; cette voie ne recale pas le fond de boite sur la plante des pieds. + const LPCoupleSim::FBoxFitParams Params; + FBox Bounds(ForceInit); + LPCoupleSim::AccumulateLiveBody(Who->GetMesh(), Params, Bounds); + LPCoupleSim::AccumulateLiveBody(Other->GetMesh(), Params, Bounds); + const LPCoupleSim::FBoxFit Fit = LPCoupleSim::FitFromLiveBounds(Bounds, Params); + if (!Fit.bValid) + { + return false; + } + Box = LPCoupleSim::EnsureBox(Who, Box.Get(), TEXT("LPShowCoupleBox"), true); + LPCoupleSim::ApplyBoxFit(Box.Get(), Fit); + if (!LoggedExtent.Equals(Fit.Extent, 5.0)) + { + UE_LOG(LogLifePlay, Warning, TEXT("Show couple : boite pose centre %s etendue %s"), *Fit.Center.ToString(), *Fit.Extent.ToString()); + LoggedExtent = Fit.Extent; + } + return true; +} + +bool ULPShowCoupleSubsystem::FitBox(const TArray& Anims, const FString& Label) +{ + ACharacter* Who = Lead.Get(); + if (!Who || Anims.Num() == 0) + { + return false; + } + // Voie "animations" : meme mesure et meme padding que FitCoupleBox de la sandbox (LPCoupleSim.h). + // Le fond de boite est recale sur la plante des pieds du porteur. + LPCoupleSim::FBoxFitParams Params; + Params.CapsuleHalf = LPCoupleSim::CapsuleHalfOf(Who); + Params.FloorLocalZ = -Params.CapsuleHalf; + const FTransform ToActor = LPCoupleSim::MeshToActor(Params.CapsuleHalf); + FBox Bounds(ForceInit); + for (const UAnimSequence* Anim : Anims) + { + LPCoupleSim::AccumulateAnimBounds(Anim, ToActor, Bounds); + } + const LPCoupleSim::FBoxFit Fit = LPCoupleSim::FitFromAnimBounds(Bounds, Params); + if (!Fit.bValid) + { + UE_LOG(LogLifePlay, Error, TEXT("Show couple : boite non mesuree (%s)"), *Label); + return false; + } + Box = LPCoupleSim::EnsureBox(Who, Box.Get(), TEXT("LPShowCoupleBox"), true); + LPCoupleSim::ApplyBoxFit(Box.Get(), Fit); + UE_LOG(LogLifePlay, Warning, TEXT("Show couple : boite %s centre %s etendue %s"), *Label, *Fit.Center.ToString(), *Fit.Extent.ToString()); + return true; +} + +void ULPShowCoupleSubsystem::KeepTogether() +{ + ACharacter* Who = Lead.Get(); + ACharacter* Other = Partner.Get(); + UBoxComponent* Volume = Box.Get(); + UWorld* World = GetWorld(); + if (!Who || !Other || !Volume || !World) + { + return; + } + const FVector Now = Who->GetActorLocation(); + const FVector Delta = Now - LastSafe; + if (Delta.IsNearlyZero()) + { + return; + } + // Meme resolveur que la sandbox (LPCoupleSim.h), avec les reglages du jeu : balayage multiple, + // 3 passes, depenetration, tous les PNJ ignores, lits franchissables, coque de la chambre ignoree. + LPCoupleSim::FSweepOptions Options = LPCoupleSim::FSweepOptions::Show(); + Options.IsWalkableSupport = [](const UPrimitiveComponent* Comp) + { + const UStaticMeshComponent* MeshComp = Cast(Comp); + return MeshComp && IsBedMesh(MeshComp->GetStaticMesh()); + }; + Options.IsIgnoredShell = [](const AActor* Actor) + { + const FString Name = Actor ? Actor->GetName() : FString(); + return Name == TEXT("Wall") || Name.StartsWith(TEXT("Wall_")); + }; + Options.OnIgnoredShell = [this](const AActor* Actor) + { + if (LastBlocker == TEXT("coque")) + { + return; + } + if (const UStaticMeshComponent* Shell = Actor->FindComponentByClass()) + { + UE_LOG(LogLifePlay, Warning, TEXT("Show couple : coque %s ignoree, maille %s, boite %s"), + *Actor->GetName(), *GetNameSafe(Shell->GetStaticMesh()), *Shell->Bounds.GetBox().ToString()); + } + LastBlocker = TEXT("coque"); + }; + const LPCoupleSim::FSweepResult Move = LPCoupleSim::ResolveCoupleMove( + World, Volume, Who, Other, LastSafe, Delta, Options); + if (!Move.Blocker.IsEmpty() && Move.Blocker != LastBlocker) + { + UE_LOG(LogLifePlay, Warning, TEXT("Show couple : bloque par %s normal %s profondeur %.0f"), + *Move.Blocker, *Move.BlockerNormal.ToString(), Move.BlockerDepth); + LastBlocker = Move.Blocker; + } + if (!Move.Location.Equals(Now, LPCoupleSim::MoveEpsilon)) + { + Who->SetActorLocation(Move.Location, false, nullptr, ETeleportType::TeleportPhysics); + } + LastSafe = Who->GetActorLocation(); +} diff --git a/Source/LifePlay/LPShowCoupleSubsystem.h b/Source/LifePlay/LPShowCoupleSubsystem.h new file mode 100644 index 0000000..fc47883 --- /dev/null +++ b/Source/LifePlay/LPShowCoupleSubsystem.h @@ -0,0 +1,72 @@ +// Show a deux. Le Blueprint (GI::Show) reste l'ancien chemin : capsules coupees, gravite 0. +// lp.ShowCouple 1 (defaut) branche le nouveau, comme la sandbox : les deux corps partagent le point, +// la boite suit les os affiches des deux corps, et les meubles bloquent Pawn le temps de la scene. +// lp.ShowCouple 0 debranche le nouveau et rend la main a l'ancien, sans rien effacer du Blueprint. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/EngineBaseTypes.h" +#include "LPCoupleSim.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "LPShowCoupleSubsystem.generated.h" + +class ACharacter; +class UAnimSequence; +class UBoxComponent; +class UPrimitiveComponent; + +UCLASS() +class LIFEPLAY_API ULPShowCoupleSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + +private: + void AfterActors(UWorld* World, ELevelTick TickType, float DeltaSeconds); + void Engage(ACharacter* InLead, ACharacter* InPartner); + void Release(); + void Weld(); + void BlockRoomPawn(); + void OpenUseFurniture(); + void LetSittersPass(UWorld* World); + bool IsUseFurniture(const class UStaticMesh* Mesh); + bool FitBox(const TArray& Anims, const FString& Label); + bool FitToBodies(); + void KeepTogether(); + + FDelegateHandle PostTickHandle; + TWeakObjectPtr Lead; + TWeakObjectPtr Partner; + TWeakObjectPtr Box; + FVector LastSafe = FVector::ZeroVector; + FString FittedPair; + FString RejectedPair; + FVector LoggedExtent = FVector::ZeroVector; + FString LastBlocker; + bool bEngaged = false; + bool bPawnLogged = false; + + // Mouvement du partenaire gele le temps du show, rendu par Release (LPCoupleSim.h). + LPCoupleSim::FSavedMove SavedPartnerMove; + + struct FSavedPawn + { + TWeakObjectPtr Comp; + ECollisionResponse Response = ECR_Ignore; + }; + TArray SavedFurniture; + + TSet UseFurniture; + TSet SolidFurniture; + + struct FSavedSeat + { + TWeakObjectPtr Cap; + ECollisionResponse Response = ECR_Block; + }; + TArray SavedSeats; +}; diff --git a/Source/LifePlay/LifePlay.Build.cs b/Source/LifePlay/LifePlay.Build.cs index 74d0c38..93247e9 100644 --- a/Source/LifePlay/LifePlay.Build.cs +++ b/Source/LifePlay/LifePlay.Build.cs @@ -27,9 +27,22 @@ public class LifePlay : ModuleRules // Portage 5.5 (L3) : ULPSessionLogSubsystem lit UGeneralProjectSettings (nom et version du jeu) PrivateDependencyModuleNames.AddRange(new string[] { "EngineSettings" }); + // Lot AP1 (sandbox Mesure) : LPSkeletonProfile lit les profils de squelettes Tools/anim/profiles/*.json + PrivateDependencyModuleNames.Add("Json"); + + // Lot AP5 : ULPModDailiesSubsystem liste les plugins de type Mod (IPluginManager). + PrivateDependencyModuleNames.Add("Projects"); + // H3d : ULPModInfoSubsystem affiche un avertissement (fenetre Slate) quand un mod a ete construit pour une - // autre version du jeu - PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); + // autre version du jeu. UMG : le bouton Clavier ajoute dans la liste des reglages. + PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "UMG" }); + + // Releve de collision (-LPCollisionLog) : nombre de triangles des trimesh Chaos. + PrivateDependencyModuleNames.Add("Chaos"); + + // Lot S2 / S3 : LPAnimMeasure (plugin LPSandbox) — LPDirectControlSubsystem et + // LifePlayEditorTools incluent LPAnimRedirect / LPPoseMeasure. + PublicDependencyModuleNames.Add("LPAnimMeasure"); // Uncomment if you are using Slate UI // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); diff --git a/Source/LifePlayEditorTools/LPAnimCatalogCommandlet.cpp b/Source/LifePlayEditorTools/LPAnimCatalogCommandlet.cpp new file mode 100644 index 0000000..30f86e2 --- /dev/null +++ b/Source/LifePlayEditorTools/LPAnimCatalogCommandlet.cpp @@ -0,0 +1,348 @@ +// Lot AP6 - voir LPAnimCatalogCommandlet.h + +#include "LPAnimCatalogCommandlet.h" + +#include "Animation/AnimData/IAnimationDataModel.h" +#include "Animation/AnimSequence.h" +#include "Dom/JsonObject.h" +#include "LPPoseMeasure.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" +#include "Serialization/JsonWriter.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPAnimCatalog, Log, All); + +namespace LPAnimCatalogImpl +{ + // Seuils de classement (definitions : voir l'en-tete). + constexpr float LyingTilt = 60.f; // degres + constexpr float KneelHeight = 12.f; // cm + constexpr float LowHips = 0.72f; // fraction de la hauteur de hanche debout (gardes de combat : debout) + constexpr float SeatBehind = 20.f; // cm + constexpr float Majority = 0.6f; + constexpr float FootContact = 3.f; // cm + constexpr float HandRest = 15.f; // cm/s + constexpr float HandRaised = 0.3f; + + int32 BoneOf(const FLPPose& Pose, const TCHAR* Role, int32 Index = 0) + { + const FName Name = LPPoseMeasure::RoleBone(Pose, Role, Index); + return Name.IsNone() ? INDEX_NONE : Pose.Names.IndexOfByKey(Name); + } + + FVector At(const FLPPose& Pose, int32 Bone) + { + return Pose.Component.IsValidIndex(Bone) ? Pose.Component[Bone].GetLocation() : FVector::ZeroVector; + } + + FVector RestAt(const FLPPose& Pose, int32 Bone) + { + return Pose.Rest.IsValidIndex(Bone) ? Pose.Rest[Bone].GetLocation() : FVector::ZeroVector; + } + + FVector Flat(FVector V) + { + V.Z = 0.f; + return V; + } + + struct FBones + { + int32 Hips, Head, ThighL, ThighR, CalfL, CalfR, FootL, FootR, ToeL, ToeR, HandL, HandR; + bool Bind(const FLPPose& P) + { + Hips = BoneOf(P, TEXT("hips")); Head = BoneOf(P, TEXT("head")); + ThighL = BoneOf(P, TEXT("thigh_l")); ThighR = BoneOf(P, TEXT("thigh_r")); + CalfL = BoneOf(P, TEXT("calf_l")); CalfR = BoneOf(P, TEXT("calf_r")); + FootL = BoneOf(P, TEXT("foot_l")); FootR = BoneOf(P, TEXT("foot_r")); + ToeL = BoneOf(P, TEXT("toe_l")); ToeR = BoneOf(P, TEXT("toe_r")); + HandL = BoneOf(P, TEXT("hand_l")); HandR = BoneOf(P, TEXT("hand_r")); + const int32 All[] = { Hips, Head, ThighL, ThighR, CalfL, CalfR, FootL, FootR, ToeL, ToeR, HandL, HandR }; + for (const int32 B : All) + { + if (B == INDEX_NONE) + { + return false; + } + } + return true; + } + }; + + FVector ForwardOf(const FLPPose& P, const FBones& B) + { + // Haut x gauche = avant (verifie sur Roll, qui avance, et Sitting_Enter, dont le bassin recule). + const FVector Side = Flat(At(P, B.ThighL) - At(P, B.ThighR)); + return FVector::CrossProduct(FVector::UpVector, Side.GetSafeNormal()).GetSafeNormal(); + } + + const TCHAR* PostureOf(const FLPPose& P, const FBones& B, float StandingHips, float Floor) + { + const FVector Trunk = At(P, B.Head) - At(P, B.Hips); + const float Tilt = static_cast(FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(Trunk.GetSafeNormal().Z, -1.0, 1.0)))); + if (Tilt > LyingTilt) + { + return TEXT("allonge"); + } + const float Hips = static_cast(At(P, B.Hips).Z) - Floor; + if (Hips >= LowHips * StandingHips) + { + return TEXT("debout"); + } + const float Knee = static_cast(FMath::Min(At(P, B.CalfL).Z, At(P, B.CalfR).Z)) - Floor; + if (Knee < KneelHeight) + { + return TEXT("a genoux"); + } + const FVector Ankles = (At(P, B.FootL) + At(P, B.FootR)) * 0.5; + const float Behind = static_cast(FVector::DotProduct(Flat(Ankles - At(P, B.Hips)), ForwardOf(P, B))); + return Behind > SeatBehind ? TEXT("assis") : TEXT("accroupi"); + } + + bool ReadJson(const FString& Path, TSharedPtr& Out) + { + FString Text; + if (!FFileHelper::LoadFileToString(Text, *Path)) + { + return false; + } + const TSharedRef> Reader = TJsonReaderFactory<>::Create(Text); + return FJsonSerializer::Deserialize(Reader, Out) && Out.IsValid(); + } + + struct FClipInfo + { + int32 Frames = 0; + double Rate = 0.0; + float Duration = 0.f; + }; + + bool ClipInfo(const FString& Path, FClipInfo& Out) + { + const UAnimSequence* Anim = LoadObject(nullptr, *Path); + const IAnimationDataModel* Model = Anim ? Anim->GetDataModel() : nullptr; + if (!Model) + { + return false; + } + Out.Frames = Model->GetNumberOfKeys(); + Out.Rate = Model->GetFrameRate().AsDecimal(); + Out.Duration = static_cast(Model->GetPlayLength()); + return Out.Frames > 0; + } + + TSharedRef Measure(const FString& Path, const FClipInfo& Info, FString& Error) + { + TSharedRef Card = MakeShared(); + TArray Poses; + Poses.SetNum(Info.Frames); + for (int32 Key = 0; Key < Info.Frames; ++Key) + { + const float Time = FMath::Min(static_cast(Key / Info.Rate), Info.Duration); + if (!LPPoseMeasure::Sample(Path, Time, Poses[Key])) + { + Error = FString::Printf(TEXT("echantillon impossible a %.3f s"), Time); + return Card; + } + } + FBones B; + if (!B.Bind(Poses[0])) + { + Error = FString::Printf(TEXT("roles d'os manquants (profil %s)"), *Poses[0].Profile); + return Card; + } + const FLPPose& First = Poses[0]; + const FLPPose& Last = Poses.Last(); + const float Floor = static_cast(FMath::Min(RestAt(First, B.FootL).Z, RestAt(First, B.FootR).Z)); + const float StandingHips = static_cast(RestAt(First, B.Hips).Z) - Floor; + const float Height = FMath::Max(1.f, static_cast(RestAt(First, B.Head).Z) - Floor); + + Card->SetNumberField(TEXT("images"), Info.Frames); + Card->SetNumberField(TEXT("cadence"), FMath::RoundToFloat(static_cast(Info.Rate) * 100.f) / 100.f); + Card->SetNumberField(TEXT("duree_s"), FMath::RoundToFloat(Info.Duration * 100.f) / 100.f); + + // Boucle : extremites relatives a la hanche, premiere et derniere image. + const int32 Ends[] = { B.HandL, B.HandR, B.FootL, B.FootR, B.Head }; + float LoopGap = 0.f; + for (const int32 E : Ends) + { + const FVector A = At(First, E) - At(First, B.Hips); + const FVector Z = At(Last, E) - At(Last, B.Hips); + LoopGap = FMath::Max(LoopGap, static_cast((A - Z).Size())); + } + const float LoopPct = 100.f * LoopGap / Height; + Card->SetNumberField(TEXT("boucle_ecart_pct"), FMath::RoundToFloat(LoopPct * 100.f) / 100.f); + + // Deplacement horizontal de la hanche. + const FVector Travel = Flat(At(Last, B.Hips) - At(First, B.Hips)); + const float Distance = static_cast(Travel.Size()); + Card->SetNumberField(TEXT("deplacement_cm"), FMath::RoundToFloat(Distance)); + Card->SetNumberField(TEXT("vitesse_cm_s"), Info.Duration > 0.f ? FMath::RoundToFloat(Distance / Info.Duration) : 0.f); + const float Ahead = Distance > 0.f ? static_cast(FVector::DotProduct(Travel / Distance, ForwardOf(First, B))) : 0.f; + Card->SetStringField(TEXT("deplacement_sens"), Ahead > 0.7f ? TEXT("avant") : Ahead < -0.7f ? TEXT("arriere") : TEXT("cote")); + + // Posture image par image. + TMap Count; + TArray SeatHeights; + int32 FeetDown[2] = { 0, 0 }; + int32 Raised[2] = { 0, 0 }; + float HandPath[2] = { 0.f, 0.f }; + const float RestFoot[2] = { static_cast(RestAt(First, B.FootL).Z), static_cast(RestAt(First, B.FootR).Z) }; + const float RestToe[2] = { static_cast(RestAt(First, B.ToeL).Z), static_cast(RestAt(First, B.ToeR).Z) }; + for (int32 Key = 0; Key < Info.Frames; ++Key) + { + const FLPPose& P = Poses[Key]; + const FString Posture = PostureOf(P, B, StandingHips, Floor); + ++Count.FindOrAdd(Posture); + if (Posture == TEXT("assis")) + { + SeatHeights.Add(static_cast((At(P, B.ThighL).Z + At(P, B.ThighR).Z) * 0.5) - Floor); + } + const int32 Feet[2] = { B.FootL, B.FootR }; + const int32 Toes[2] = { B.ToeL, B.ToeR }; + const int32 Hands[2] = { B.HandL, B.HandR }; + for (int32 Side = 0; Side < 2; ++Side) + { + const float Lift = FMath::Min(static_cast(At(P, Feet[Side]).Z) - RestFoot[Side], + static_cast(At(P, Toes[Side]).Z) - RestToe[Side]); + FeetDown[Side] += Lift < FootContact ? 1 : 0; + Raised[Side] += At(P, Hands[Side]).Z > At(P, B.Head).Z ? 1 : 0; + if (Key > 0) + { + const FLPPose& Q = Poses[Key - 1]; + HandPath[Side] += static_cast(((At(P, Hands[Side]) - At(P, B.Hips)) - (At(Q, Hands[Side]) - At(Q, B.Hips))).Size()); + } + } + } + FString Main = TEXT("variable"); + for (const TPair& It : Count) + { + if (It.Value >= Majority * Info.Frames) + { + Main = It.Key; + } + } + Card->SetStringField(TEXT("posture"), Main); + Card->SetStringField(TEXT("posture_debut"), PostureOf(First, B, StandingHips, Floor)); + Card->SetStringField(TEXT("posture_fin"), PostureOf(Last, B, StandingHips, Floor)); + TSharedRef Split = MakeShared(); + for (const TPair& It : Count) + { + Split->SetNumberField(It.Key, FMath::RoundToFloat(100.f * It.Value / Info.Frames)); + } + Card->SetObjectField(TEXT("posture_pct"), Split); + if (SeatHeights.Num() > 0) + { + SeatHeights.Sort(); + Card->SetNumberField(TEXT("hauteur_assise_cm"), FMath::RoundToFloat(SeatHeights[SeatHeights.Num() / 2])); + } + else + { + Card->SetField(TEXT("hauteur_assise_cm"), MakeShared()); + } + const TCHAR* SideNames[2] = { TEXT("gauche"), TEXT("droite") }; + TSharedRef Feet = MakeShared(); + TSharedRef Hands = MakeShared(); + for (int32 Side = 0; Side < 2; ++Side) + { + Feet->SetNumberField(SideNames[Side], FMath::RoundToFloat(100.f * FeetDown[Side] / Info.Frames)); + const float Speed = Info.Duration > 0.f ? HandPath[Side] / Info.Duration : 0.f; + const TCHAR* State = Raised[Side] >= HandRaised * Info.Frames ? TEXT("levee") : Speed < HandRest ? TEXT("repos") : TEXT("mouvement"); + Hands->SetStringField(SideNames[Side], State); + } + Card->SetObjectField(TEXT("pieds_au_sol_pct"), Feet); + Card->SetObjectField(TEXT("mains"), Hands); + return Card; + } +} + +ULPAnimCatalogCommandlet::ULPAnimCatalogCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPAnimCatalogCommandlet::Main(const FString& Params) +{ + using namespace LPAnimCatalogImpl; + FString Livraison = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AnimPipeline/quaternius/livraison.json")); + FParse::Value(*Params, TEXT("Livraison="), Livraison, false); + FString Out = FPaths::GetPath(Livraison); + FParse::Value(*Params, TEXT("Out="), Out, false); + float SeuilBoucle = 1.f; + float SeuilSurPlace = 5.f; + FParse::Value(*Params, TEXT("SeuilBoucle="), SeuilBoucle); + FParse::Value(*Params, TEXT("SeuilSurPlace="), SeuilSurPlace); + + TSharedPtr Root; + if (!ReadJson(Livraison, Root) || Root->Type != EJson::Array) + { + UE_LOG(LogLPAnimCatalog, Error, TEXT("livraison.json illisible : %s"), *Livraison); + return 1; + } + + TSharedRef Cards = MakeShared(); + TArray Failures; + for (const TSharedPtr& Entry : Root->AsArray()) + { + const TSharedPtr Item = Entry->AsObject(); + const FString Key = Item->GetStringField(TEXT("cle")); + const FString PathF = Item->GetStringField(TEXT("source_F")); + const FString PathM = Item->GetStringField(TEXT("source_M")); + FClipInfo F, M; + if (!ClipInfo(PathF, F) || !ClipInfo(PathM, M)) + { + Failures.Add(FString::Printf(TEXT("%s : clip absent ou vide"), *Key)); + continue; + } + if (F.Frames != M.Frames || !FMath::IsNearlyEqual(F.Duration, M.Duration, 0.001f)) + { + Failures.Add(FString::Printf(TEXT("%s : F %d images %.3f s, M %d images %.3f s"), *Key, F.Frames, F.Duration, M.Frames, M.Duration)); + } + FString Error; + TSharedRef Card = Measure(PathF, F, Error); + if (!Error.IsEmpty()) + { + Failures.Add(FString::Printf(TEXT("%s : %s"), *Key, *Error)); + continue; + } + Card->SetBoolField(TEXT("boucle"), Card->GetNumberField(TEXT("boucle_ecart_pct")) <= SeuilBoucle); + if (Card->GetNumberField(TEXT("deplacement_cm")) < SeuilSurPlace) + { + Card->SetStringField(TEXT("deplacement_sens"), TEXT("sur place")); + } + UE_LOG(LogLPAnimCatalog, Display, TEXT("%-28s %5.2f s %3d img boucle %5.2f %% depl %4.0f cm %-9s posture %-9s (%s -> %s)"), + *Key, F.Duration, F.Frames, Card->GetNumberField(TEXT("boucle_ecart_pct")), Card->GetNumberField(TEXT("deplacement_cm")), + *Card->GetStringField(TEXT("deplacement_sens")), *Card->GetStringField(TEXT("posture")), + *Card->GetStringField(TEXT("posture_debut")), *Card->GetStringField(TEXT("posture_fin"))); + Cards->SetObjectField(Key, Card); + } + + TSharedRef Report = MakeShared(); + Report->SetStringField(TEXT("livraison"), Livraison); + Report->SetNumberField(TEXT("seuil_boucle_pct"), SeuilBoucle); + Report->SetNumberField(TEXT("seuil_sur_place_cm"), SeuilSurPlace); + Report->SetObjectField(TEXT("fiches"), Cards); + TArray> Errors; + for (const FString& Failure : Failures) + { + Errors.Add(MakeShared(Failure)); + UE_LOG(LogLPAnimCatalog, Error, TEXT("%s"), *Failure); + } + Report->SetArrayField(TEXT("echecs"), Errors); + const FString OutPath = FPaths::Combine(Out, TEXT("fiches_calculees.json")); + FString Text; + const TSharedRef> Writer = TJsonWriterFactory<>::Create(&Text); + FJsonSerializer::Serialize(Report, Writer); + if (!FFileHelper::SaveStringToFile(Text, *OutPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + UE_LOG(LogLPAnimCatalog, Error, TEXT("ecriture impossible : %s"), *OutPath); + return 1; + } + UE_LOG(LogLPAnimCatalog, Warning, TEXT("CATALOGUE fiches %d echecs %d -> %s"), Cards->Values.Num(), Failures.Num(), *OutPath); + return Failures.Num() > 0 ? 1 : 0; +} diff --git a/Source/LifePlayEditorTools/LPAnimCatalogCommandlet.h b/Source/LifePlayEditorTools/LPAnimCatalogCommandlet.h new file mode 100644 index 0000000..45b8a2c --- /dev/null +++ b/Source/LifePlayEditorTools/LPAnimCatalogCommandlet.h @@ -0,0 +1,40 @@ +// Lot AP6 (Docs/Migration/20_Chaine_Animations.md, section 5) : champs calcules du catalogue des animations. +// +// Pour chaque cle de livraison.json (AP5), mesure le clip feminin source (copie identique de celui du mod) image +// par image, et controle que le clip masculin a la meme duree et le meme nombre d'images. Mesures (corps du clip, +// hauteurs depuis le sol du repos, en cm) : +// duree, images, cadence ; +// boucle : pose de la derniere image comparee a la premiere (mains, pieds, tete, relatives a la hanche), +// ecart maximal en % de la taille (tete au repos) ; boucle si <= SeuilBoucle (1 %) ; +// deplacement : trajet horizontal de la hanche entre premiere et derniere image (cm, cm/s) et sens par rapport a +// l'avant du corps a la premiere image (avant, arriere, cote, sur place si < SeuilSurPlace) ; +// posture : par image, allonge (tronc incline de plus de 60 deg), a genoux (genou a moins de 12 cm du sol, +// hanche sous 72 % de sa hauteur debout), assis (hanche sous 72 %, en arriere des chevilles de plus +// de 20 cm), accroupi (hanche sous 72 %, au-dessus des chevilles), debout sinon ; posture du clip = +// celle d'au moins 60 % des images, sinon "variable" ; posture de debut et de fin ; +// hauteur_assise_cm : hauteur mediane des articulations de hanche sur les images assises (le siege est plus bas +// de l'epaisseur de la cuisse) ; +// pieds : part des images ou chaque pied (cheville ou orteil) est a moins de 3 cm de sa hauteur de repos ; +// mains : par main, repos (vitesse moyenne par rapport a la hanche < 15 cm/s), mouvement, levee (au-dessus +// de la tete sur au moins 30 % des images). +// Ecrit /fiches_calculees.json. Code 1 si un clip manque, est vide, ou si F et M different en duree ou en images. +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPAnimCatalog [-Livraison=] +// [-Out=] [-SeuilBoucle=1] [-SeuilSurPlace=5] + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPAnimCatalogCommandlet.generated.h" + +UCLASS() +class ULPAnimCatalogCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPAnimCatalogCommandlet(); + virtual int32 Main(const FString& Params) override; +}; diff --git a/Source/LifePlayEditorTools/LPAnimChecks.cpp b/Source/LifePlayEditorTools/LPAnimChecks.cpp new file mode 100644 index 0000000..f44d08e --- /dev/null +++ b/Source/LifePlayEditorTools/LPAnimChecks.cpp @@ -0,0 +1,375 @@ +#include "LPAnimChecks.h" + +#include "Animation/AnimData/IAnimationDataModel.h" +#include "Animation/AnimSequence.h" +#include "Engine/SkeletalMesh.h" +#include "PhysicsEngine/BodySetup.h" +#include "PhysicsEngine/PhysicsAsset.h" +#include "PhysicsEngine/SkeletalBodySetup.h" +#include "Rendering/SkeletalMeshLODModel.h" +#include "Rendering/SkeletalMeshModel.h" + +namespace +{ + // Os du tronc dont les capsules forment le volume teste (roles du profil). + const TCHAR* const TrunkRoles[] = { TEXT("hips"), TEXT("pelvis"), TEXT("spine") }; + + // Seuils internes de detection (pas des seuils d'acceptation, fixes dans LPAnimEval). + constexpr float ContactHeight = 3.f; // cm : pied source considere au sol + constexpr float PlantedMove = 1.f; // cm par image : pied source immobile + constexpr float StraightLeg = 15.f; // degres : jambe presque tendue + constexpr float JumpFactor = 1.5f; // une rotation d'image peut depasser la source de 50 % + constexpr float JumpMargin = 2.f; // ... plus 2 degres + + int32 BoneOf(const FLPPose& Pose, const TCHAR* Role, int32 Index = 0) + { + const FName Name = LPPoseMeasure::RoleBone(Pose, Role, Index); + return Name.IsNone() ? INDEX_NONE : Pose.Names.IndexOfByKey(Name); + } + + FVector At(const FLPPose& Pose, int32 Bone) + { + return Pose.Component.IsValidIndex(Bone) ? Pose.Component[Bone].GetLocation() : FVector::ZeroVector; + } + + // Avant du corps : perpendiculaire horizontale a l'axe des hanches. + FVector ForwardOf(const FLPPose& Pose) + { + FVector Side = At(Pose, BoneOf(Pose, TEXT("thigh_l"))) - At(Pose, BoneOf(Pose, TEXT("thigh_r"))); + Side.Z = 0.f; + return FVector::CrossProduct(Side.GetSafeNormal(), FVector::UpVector).GetSafeNormal(); + } + + float Flexion(const FLPPose& Pose, int32 A, int32 B, int32 C) + { + const FVector Upper = (At(Pose, B) - At(Pose, A)).GetSafeNormal(); + const FVector Lower = (At(Pose, C) - At(Pose, B)).GetSafeNormal(); + return FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(FVector::DotProduct(Upper, Lower), -1.f, 1.f))); + } + + // Genou derriere la ligne hanche-cheville (cm, positif = hyperextension), jambe presque tendue seulement. + float KneeBehind(const FLPPose& Pose, int32 Hip, int32 Knee, int32 Ankle) + { + if (Flexion(Pose, Hip, Knee, Ankle) > StraightLeg) + { + return 0.f; + } + const FVector Line = (At(Pose, Ankle) - At(Pose, Hip)).GetSafeNormal(); + const FVector Offset = (At(Pose, Knee) - At(Pose, Hip)) - Line * FVector::DotProduct(At(Pose, Knee) - At(Pose, Hip), Line); + return FMath::Max(0.f, -static_cast(FVector::DotProduct(Offset, ForwardOf(Pose)))); + } + + // Repli sans actif physique : une capsule par segment de la colonne (bassin -> base du cou), rayon pris sur + // la peau du maillage (sommets dont l'os dominant est du tronc), au 30e centile des distances a l'axe : + // prudent, les flancs plus larges que la profondeur ne declenchent pas de faux positifs. + constexpr float SkinPercentile = 0.30f; + + TArray SkinCapsules(const USkeletalMesh* Mesh, const FLPPose& BodyT, const TSet& Trunk) + { + TArray Capsules; + const FSkeletalMeshModel* Imported = Mesh ? Mesh->GetImportedModel() : nullptr; + if (!Imported || Imported->LODModels.Num() == 0 || BodyT.Rest.Num() != BodyT.Names.Num()) + { + return Capsules; + } + const FReferenceSkeleton& MeshRef = Mesh->GetRefSkeleton(); + TArray Nodes; + Nodes.Add(BoneOf(BodyT, TEXT("pelvis"))); + for (int32 Index = 0; ; ++Index) + { + const int32 Bone = BoneOf(BodyT, TEXT("spine"), Index); + if (Bone == INDEX_NONE) + { + break; + } + Nodes.Add(Bone); + } + Nodes.Add(BoneOf(BodyT, TEXT("neck"))); + Nodes.RemoveAll([](int32 Bone) { return Bone == INDEX_NONE; }); + struct FSegment { int32 Bone; FVector A; FVector B; TArray Distances; }; + TArray Segments; + for (int32 Index = 0; Index + 1 < Nodes.Num(); ++Index) + { + const FVector A = BodyT.Rest[Nodes[Index]].GetLocation(); + const FVector B = BodyT.Rest[Nodes[Index + 1]].GetLocation(); + if (FVector::Dist(A, B) > 2.f) + { + Segments.Add({ Nodes[Index], A, B, {} }); + } + } + for (const FSkelMeshSection& Section : Imported->LODModels[0].Sections) + { + for (const FSoftSkinVertex& Vertex : Section.SoftVertices) + { + int32 Best = 0; + for (int32 Influence = 1; Influence < MAX_TOTAL_INFLUENCES; ++Influence) + { + if (Vertex.InfluenceWeights[Influence] > Vertex.InfluenceWeights[Best]) + { + Best = Influence; + } + } + if (!Section.BoneMap.IsValidIndex(Vertex.InfluenceBones[Best])) + { + continue; + } + const int32 Bone = BodyT.Names.IndexOfByKey(MeshRef.GetBoneName(Section.BoneMap[Vertex.InfluenceBones[Best]])); + if (!Trunk.Contains(Bone)) + { + continue; + } + const FVector P(Vertex.Position); + for (FSegment& Segment : Segments) + { + const FVector AB = Segment.B - Segment.A; + const float T = static_cast(FVector::DotProduct(P - Segment.A, AB) / AB.SizeSquared()); + if (T >= 0.f && T <= 1.f) + { + Segment.Distances.Add(static_cast(FVector::Dist(P, Segment.A + AB * T))); + break; + } + } + } + } + for (FSegment& Segment : Segments) + { + if (Segment.Distances.Num() < 20) + { + continue; + } + Segment.Distances.Sort(); + const FTransform& Rest = BodyT.Rest[Segment.Bone]; + FLPCheckCapsule& Capsule = Capsules.AddDefaulted_GetRef(); + Capsule.Bone = Segment.Bone; + Capsule.Center = Rest.InverseTransformPosition((Segment.A + Segment.B) * 0.5f); + Capsule.Axis = Rest.InverseTransformVectorNoScale((Segment.B - Segment.A).GetSafeNormal()); + Capsule.HalfLength = static_cast(FVector::Dist(Segment.A, Segment.B)) * 0.5f; + Capsule.Radius = Segment.Distances[FMath::FloorToInt(Segment.Distances.Num() * SkinPercentile)]; + } + return Capsules; + } + + float SegmentDistance(const FVector& P, const FVector& A, const FVector& B) + { + const FVector AB = B - A; + const float T = AB.IsNearlyZero() ? 0.f : FMath::Clamp(static_cast(FVector::DotProduct(P - A, AB) / AB.SizeSquared()), 0.f, 1.f); + return static_cast(FVector::Dist(P, A + AB * T)); + } +} + +namespace LPAnimChecks +{ + TArray TrunkCapsules(const USkeletalMesh* Mesh, const FLPPose& BodyT) + { + TArray Capsules; + const UPhysicsAsset* Physics = Mesh ? Mesh->GetPhysicsAsset() : nullptr; + TSet Trunk; + for (const TCHAR* Role : TrunkRoles) + { + for (int32 Index = 0; ; ++Index) + { + const int32 Bone = BoneOf(BodyT, Role, Index); + if (Bone == INDEX_NONE) + { + break; + } + Trunk.Add(Bone); + } + } + if (!Physics) + { + return SkinCapsules(Mesh, BodyT, Trunk); + } + for (const TObjectPtr& Setup : Physics->SkeletalBodySetups) + { + const int32 Bone = Setup ? BodyT.Names.IndexOfByKey(Setup->BoneName) : INDEX_NONE; + if (Bone == INDEX_NONE || !Trunk.Contains(Bone)) + { + continue; + } + for (const FKSphylElem& Sphyl : Setup->AggGeom.SphylElems) + { + FLPCheckCapsule& Capsule = Capsules.AddDefaulted_GetRef(); + Capsule.Bone = Bone; + Capsule.Center = Sphyl.Center; + Capsule.Axis = Sphyl.Rotation.Quaternion().GetAxisZ(); + Capsule.HalfLength = Sphyl.Length * 0.5f; + Capsule.Radius = Sphyl.Radius; + } + for (const FKSphereElem& Sphere : Setup->AggGeom.SphereElems) + { + FLPCheckCapsule& Capsule = Capsules.AddDefaulted_GetRef(); + Capsule.Bone = Bone; + Capsule.Center = Sphere.Center; + Capsule.Radius = Sphere.Radius; + } + } + return Capsules.Num() > 0 ? Capsules : SkinCapsules(Mesh, BodyT, Trunk); + } + + float SelfTest(const TArray& Trunk, const FLPPose& BodyT) + { + float Worst = Trunk.Num() > 0 ? 0.f : -1.f; + for (const FLPCheckCapsule& Capsule : Trunk) + { + const FTransform& Bone = BodyT.Component[Capsule.Bone]; + const FVector Center = Bone.TransformPosition(Capsule.Center); + const FVector Axis = Bone.TransformVectorNoScale(Capsule.Axis) * Capsule.HalfLength; + // L'articulation de l'os porteur est une extremite du segment : profondeur egale au rayon si le centre + // et l'axe ont ete bien ramenes dans le repere de l'os. + const float Depth = Capsule.Radius - SegmentDistance(Bone.GetLocation(), Center - Axis, Center + Axis); + Worst = FMath::Max(Worst, FMath::Abs(Depth - Capsule.Radius)); + } + return Worst; + } + + FLPClipChecks CheckClip(const FString& RefPath, const FString& BodyPath, const FLPPose& RefT, const FLPPose& BodyT, + const TArray& Trunk) + { + FLPClipChecks Out; + Out.Clip = FPaths::GetBaseFilename(BodyPath); + const UAnimSequence* Anim = LoadObject(nullptr, *BodyPath); + const IAnimationDataModel* Model = Anim ? Anim->GetDataModel() : nullptr; + if (!Model) + { + Out.Error = TEXT("clip converti absent"); + return Out; + } + const FFrameRate Rate = Model->GetFrameRate(); + Out.Frames = Model->GetNumberOfKeys(); + + // Mise a l'echelle source -> corps : hauteur de hanche des deux poses T. + const int32 RefHips = BoneOf(RefT, TEXT("hips")); + const int32 BodyHips = BoneOf(BodyT, TEXT("hips")); + const float Scale = RefHips != INDEX_NONE && BodyHips != INDEX_NONE && At(RefT, RefHips).Z > 1.f + ? static_cast(At(BodyT, BodyHips).Z / At(RefT, RefHips).Z) : 1.f; + + // Points de contact (pieds, orteils) et articulations (coude, genou), par role. + const TCHAR* const ContactRoles[] = { TEXT("foot_l"), TEXT("toe_l"), TEXT("foot_r"), TEXT("toe_r") }; + struct FJoint { const TCHAR* A; const TCHAR* B; const TCHAR* C; bool bKnee; }; + const FJoint Joints[] = { + { TEXT("thigh_l"), TEXT("calf_l"), TEXT("foot_l"), true }, + { TEXT("thigh_r"), TEXT("calf_r"), TEXT("foot_r"), true }, + { TEXT("upperarm_l"), TEXT("lowerarm_l"), TEXT("hand_l"), false }, + { TEXT("upperarm_r"), TEXT("lowerarm_r"), TEXT("hand_r"), false }, + }; + const TCHAR* const JumpRoles[] = { + TEXT("hips"), TEXT("head"), TEXT("clavicle_l"), TEXT("upperarm_l"), TEXT("lowerarm_l"), TEXT("hand_l"), + TEXT("clavicle_r"), TEXT("upperarm_r"), TEXT("lowerarm_r"), TEXT("hand_r"), + TEXT("thigh_l"), TEXT("calf_l"), TEXT("foot_l"), TEXT("thigh_r"), TEXT("calf_r"), TEXT("foot_r") + }; + const TCHAR* const ProbeRoles[] = { TEXT("hand_l"), TEXT("lowerarm_l"), TEXT("hand_r"), TEXT("lowerarm_r") }; + + FLPPose PrevRef; + FLPPose PrevBody; + for (int32 Key = 0; Key < Out.Frames; ++Key) + { + const float Time = static_cast(Rate.AsSeconds(FFrameTime(Key))); + FLPPose Ref; + FLPPose Body; + if (!LPPoseMeasure::Sample(RefPath, Time, Ref) || !LPPoseMeasure::Sample(BodyPath, Time, Body)) + { + Out.Error = FString::Printf(TEXT("image %d illisible"), Key); + return Out; + } + for (const TCHAR* Role : ContactRoles) + { + const int32 R = BoneOf(Ref, Role); + const int32 B = BoneOf(Body, Role); + if (R == INDEX_NONE || B == INDEX_NONE) + { + continue; + } + // Hauteur au-dessus de la pose T (pieds a plat), source a l'echelle du corps. + const float RefHeight = static_cast(At(Ref, R).Z - At(RefT, R).Z) * Scale; + const float BodyHeight = static_cast(At(Body, B).Z - At(BodyT, B).Z); + const float Below = FMath::Max(0.f, -BodyHeight) - FMath::Max(0.f, -RefHeight); + if (Below > Out.Floor) + { + Out.Floor = Below; + Out.FloorBone = Body.Names[B].ToString(); + } + if (Key > 0) + { + const FVector RefMove = (At(Ref, R) - At(PrevRef, R)) * Scale; + const FVector BodyMove = At(Body, B) - At(PrevBody, B); + const bool bPlanted = RefHeight < ContactHeight && FVector(RefMove.X, RefMove.Y, 0.f).Size() < PlantedMove; + const float Slide = bPlanted ? static_cast(FVector(BodyMove.X - RefMove.X, BodyMove.Y - RefMove.Y, 0.f).Size()) : 0.f; + if (Slide > Out.Slide) + { + Out.Slide = Slide; + Out.SlideBone = Body.Names[B].ToString(); + } + } + } + for (const FJoint& Joint : Joints) + { + const int32 RA = BoneOf(Ref, Joint.A), RB = BoneOf(Ref, Joint.B), RC = BoneOf(Ref, Joint.C); + const int32 BA = BoneOf(Body, Joint.A), BB = BoneOf(Body, Joint.B), BC = BoneOf(Body, Joint.C); + if (RA == INDEX_NONE || RB == INDEX_NONE || RC == INDEX_NONE || BA == INDEX_NONE || BB == INDEX_NONE || BC == INDEX_NONE) + { + continue; + } + const float Gap = FMath::Abs(Flexion(Body, BA, BB, BC) - Flexion(Ref, RA, RB, RC)); + if (Gap > Out.FlexGap) + { + Out.FlexGap = Gap; + Out.FlexJoint = Body.Names[BB].ToString(); + } + if (Joint.bKnee) + { + const float Behind = KneeBehind(Body, BA, BB, BC) - KneeBehind(Ref, RA, RB, RC) * Scale; + if (Behind > Out.KneeBack) + { + Out.KneeBack = Behind; + Out.KneeBone = Body.Names[BB].ToString(); + } + } + } + if (Key > 0) + { + for (const TCHAR* Role : JumpRoles) + { + const int32 R = BoneOf(Ref, Role); + const int32 B = BoneOf(Body, Role); + if (R == INDEX_NONE || B == INDEX_NONE) + { + continue; + } + const float RefTurn = FMath::RadiansToDegrees(Ref.Component[R].GetRotation().AngularDistance(PrevRef.Component[R].GetRotation())); + const float BodyTurn = FMath::RadiansToDegrees(Body.Component[B].GetRotation().AngularDistance(PrevBody.Component[B].GetRotation())); + const float Excess = BodyTurn - (RefTurn * JumpFactor + JumpMargin); + if (Excess > Out.Jump) + { + Out.Jump = Excess; + Out.JumpBone = Body.Names[B].ToString(); + } + } + } + for (const TCHAR* Role : ProbeRoles) + { + const int32 B = BoneOf(Body, Role); + if (B == INDEX_NONE) + { + continue; + } + const FVector Point = At(Body, B); + for (const FLPCheckCapsule& Capsule : Trunk) + { + const FTransform& Bone = Body.Component[Capsule.Bone]; + const FVector Center = Bone.TransformPosition(Capsule.Center); + const FVector Axis = Bone.TransformVectorNoScale(Capsule.Axis) * Capsule.HalfLength; + const float Depth = Capsule.Radius - SegmentDistance(Point, Center - Axis, Center + Axis); + if (Depth > Out.Penetration) + { + Out.Penetration = Depth; + Out.PenetrationBone = Body.Names[B].ToString(); + } + } + } + PrevRef = MoveTemp(Ref); + PrevBody = MoveTemp(Body); + } + return Out; + } +} diff --git a/Source/LifePlayEditorTools/LPAnimChecks.h b/Source/LifePlayEditorTools/LPAnimChecks.h new file mode 100644 index 0000000..a0e3f8c --- /dev/null +++ b/Source/LifePlayEditorTools/LPAnimChecks.h @@ -0,0 +1,60 @@ +// Lot AP4 (Docs/Migration/20_Chaine_Animations.md, section 4) : controles independants de la formule. +// +// Calcules image par image sur le clip converti, compares au mouvement source mis a l'echelle : un defaut deja +// present dans le clip source n'est pas impute a la conversion. Aucun rendu. +// sol : pied ou orteil plus bas que dans la source (cm, au-dela de la source) ; +// glissement : pied qui bouge alors que le pied source est pose (cm par image) ; +// genou : genou en hyperextension, derriere la ligne hanche-cheville, au-dela de la source (cm) ; +// flexion : ecart d'angle de flexion du coude et du genou avec la source (degres) ; +// saut : rotation d'un os d'une image a l'autre au-dela de celle de la source (degres) ; +// penetration : main, poignet ou coude dans les capsules du tronc de l'actif physique du corps (cm). + +#pragma once + +#include "CoreMinimal.h" +#include "LPPoseMeasure.h" + +class USkeletalMesh; + +struct FLPCheckCapsule +{ + int32 Bone = INDEX_NONE; + // Dans l'espace de l'os : centre, axe (unitaire), demi-longueur du segment, rayon. + FVector Center = FVector::ZeroVector; + FVector Axis = FVector::UpVector; + float HalfLength = 0.f; + float Radius = 0.f; +}; + +struct FLPClipChecks +{ + FString Clip; + int32 Frames = 0; + float Floor = 0.f; + FString FloorBone; + float Slide = 0.f; + FString SlideBone; + float KneeBack = 0.f; + FString KneeBone; + float FlexGap = 0.f; + FString FlexJoint; + float Jump = 0.f; + FString JumpBone; + float Penetration = 0.f; + FString PenetrationBone; + FString Error; +}; + +namespace LPAnimChecks +{ + // Capsules du tronc (bassin, colonne, poitrine) de l'actif physique du maillage du corps. + TArray TrunkCapsules(const USkeletalMesh* Mesh, const FLPPose& BodyT); + + // Auto-test du calcul de penetration : l'articulation de l'os porteur de chaque capsule (extremite du segment), + // sur le corps en pose T, doit etre a une profondeur egale au rayon. Rend l'ecart maximal (cm) ; -1 sans capsule. + float SelfTest(const TArray& Trunk, const FLPPose& BodyT); + + // Controle un clip converti (BodyPath) contre son clip source (RefPath), a chaque image du clip converti. + FLPClipChecks CheckClip(const FString& RefPath, const FString& BodyPath, const FLPPose& RefT, const FLPPose& BodyT, + const TArray& Trunk); +} diff --git a/Source/LifePlayEditorTools/LPAnimEvalCommandlet.cpp b/Source/LifePlayEditorTools/LPAnimEvalCommandlet.cpp new file mode 100644 index 0000000..80596b7 --- /dev/null +++ b/Source/LifePlayEditorTools/LPAnimEvalCommandlet.cpp @@ -0,0 +1,459 @@ +#include "LPAnimEvalCommandlet.h" + +#include "Animation/AnimSequence.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Dom/JsonObject.h" +#include "HAL/FileManager.h" +#include "Engine/SkeletalMesh.h" +#include "LPAnimChecks.h" +#include "LPPoseMeasure.h" +#include "LPSkeletonProfile.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Serialization/JsonSerializer.h" +#include "Serialization/JsonWriter.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPAnimEval, Log, All); + +namespace +{ + struct FBody + { + FString Name; + FString Prefix; + }; + + FString ClipPath(const FString& Dir, const FString& Name) + { + return FString::Printf(TEXT("%s/%s.%s"), *Dir, *Name, *Name); + } + + TArray ParseBodies(const FString& Text) + { + TArray Bodies; + TArray Items; + Text.ParseIntoArray(Items, TEXT(","), true); + for (const FString& Item : Items) + { + FString Name; + FString Prefix; + if (Item.Split(TEXT(":"), &Name, &Prefix)) + { + Bodies.Add({ Name.TrimStartAndEnd(), Prefix.TrimStartAndEnd() }); + } + } + return Bodies; + } + + // Clips de reference du dossier qui ont une copie pour chaque corps (prefixe du corps + nom). + TArray ListClips(const FString& Dir, const TArray& Bodies) + { + IAssetRegistry& Registry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); + Registry.ScanPathsSynchronous({ Dir }, true); + TArray Assets; + Registry.GetAssetsByPath(FName(*Dir), Assets, false); + TSet Names; + for (const FAssetData& Asset : Assets) + { + if (Asset.AssetClassPath == UAnimSequence::StaticClass()->GetClassPathName()) + { + Names.Add(Asset.AssetName.ToString()); + } + } + TArray Clips; + for (const FString& Name : Names) + { + bool bCopy = false; + bool bAll = true; + for (const FBody& Body : Bodies) + { + bCopy |= Name.StartsWith(Body.Prefix); + bAll &= Names.Contains(Body.Prefix + Name); + } + if (!bCopy && bAll) + { + Clips.Add(Name); + } + } + Clips.Sort(); + return Clips; + } + + // Torsion : ecart de rotation (degres) des os Bend et Twist du profil entre le clip converti et la formule. + // Les directions de segment ne voient pas la torsion d'un os autour de son axe ; cette mesure si. + float TwistGap(const FLPPose& Raw, const FLPPose& Formula, FString& OutBone) + { + const FLPSkeletonProfile* Profile = LPSkeletonProfile::Find(Raw.Profile); + float Worst = 0.f; + if (!Profile) + { + return Worst; + } + for (const TCHAR* Role : { TEXT("upperarm"), TEXT("lowerarm"), TEXT("thigh") }) + { + for (const TCHAR* Side : { TEXT("_l"), TEXT("_r") }) + { + for (const FString& Name : { FString(Role) + Side, FString(Role) + TEXT("_twist") + Side }) + { + const int32 Bone = Raw.Names.IndexOfByKey(Profile->Bone(Name)); + if (Bone == INDEX_NONE || !Formula.Component.IsValidIndex(Bone)) + { + continue; + } + const float Angle = FMath::RadiansToDegrees( + Raw.Component[Bone].GetRotation().AngularDistance(Formula.Component[Bone].GetRotation())); + if (Angle > Worst) + { + Worst = Angle; + OutBone = Raw.Names[Bone].ToString(); + } + } + } + } + return Worst; + } + + bool SaveJson(const TSharedRef& Object, const FString& Path) + { + FString Text; + const TSharedRef> Writer = TJsonWriterFactory<>::Create(&Text); + FJsonSerializer::Serialize(Object, Writer); + return FFileHelper::SaveStringToFile(Text, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); + } + + // Etape 3 : ajustement de la pose T et ses controles. + bool CheckTPose(const FLPPose& Reference, const FLPPose& Template, const FBody& Body, const FString& PoseDir, const FString& Out, + float SeuilPoseT, float SeuilSymetrie, float SeuilReglage, TArray& Failures) + { + const FString PosePath = FPaths::Combine(PoseDir, FString::Printf(TEXT("retarget_pose_%s.txt"), *Body.Name)); + FLPTFitResult Fit; + if (!LPPoseMeasure::WriteRetargetPose(Reference, Template, PosePath) || !LPPoseMeasure::TFitResult(Reference, Template, Fit)) + { + Failures.Add(FString::Printf(TEXT("%s : pose T non calculee"), *Body.Name)); + return false; + } + const int32 Before = Failures.Num(); + TSharedRef Report = MakeShared(); + Report->SetStringField(TEXT("corps"), Body.Name); + Report->SetStringField(TEXT("profil"), Template.Profile); + Report->SetStringField(TEXT("fichier_pose"), PosePath); + TArray> Segments; + for (const FLPTFitSegment& Segment : Fit.Segments) + { + TSharedRef Item = MakeShared(); + Item->SetStringField(TEXT("segment"), Segment.Name); + Item->SetNumberField(TEXT("avant"), Segment.Before); + Item->SetNumberField(TEXT("apres"), Segment.After); + Item->SetBoolField(TEXT("ajuste"), Segment.bFit); + Segments.Add(MakeShared(Item)); + if (Segment.bFit && Segment.After > SeuilPoseT) + { + Failures.Add(FString::Printf(TEXT("%s : segment %s %.2f %% apres ajustement (seuil %.2f)"), *Body.Name, *Segment.Name, Segment.After, SeuilPoseT)); + } + } + Report->SetArrayField(TEXT("segments"), Segments); + TMap Degrees; + TArray> Adjusts; + for (const FLPTFitAdjust& Adjust : Fit.Adjusts) + { + Degrees.Add(Adjust.Bone, Adjust.Degrees); + TSharedRef Item = MakeShared(); + Item->SetStringField(TEXT("os"), Adjust.Bone.ToString()); + Item->SetNumberField(TEXT("degres"), Adjust.Degrees); + Adjusts.Add(MakeShared(Item)); + if (Adjust.Degrees > SeuilReglage) + { + Failures.Add(FString::Printf(TEXT("%s : reglage %s %.1f deg (borne %.1f)"), *Body.Name, *Adjust.Bone.ToString(), Adjust.Degrees, SeuilReglage)); + } + } + Report->SetArrayField(TEXT("reglages"), Adjusts); + // Symetrie : chaque role _l face a son role _r, os par os. + const FLPSkeletonProfile* Profile = LPSkeletonProfile::Find(Template.Profile); + float WorstGap = 0.f; + if (Profile) + { + for (const TPair>& Role : Profile->Roles) + { + if (!Role.Key.EndsWith(TEXT("_l"))) + { + continue; + } + const TArray& Right = Profile->Chain(Role.Key.LeftChop(2) + TEXT("_r")); + for (int32 Index = 0; Index < FMath::Min(Role.Value.Num(), Right.Num()); ++Index) + { + const float* Left = Degrees.Find(Role.Value[Index]); + const float* Other = Degrees.Find(Right[Index]); + const float Gap = FMath::Abs((Left ? *Left : 0.f) - (Other ? *Other : 0.f)); + WorstGap = FMath::Max(WorstGap, Gap); + if (Gap > SeuilSymetrie) + { + Failures.Add(FString::Printf(TEXT("%s : reglages %s / %s asymetriques de %.2f deg"), *Body.Name, + *Role.Value[Index].ToString(), *Right[Index].ToString(), Gap)); + } + } + } + } + Report->SetNumberField(TEXT("ecart_symetrie_max"), WorstGap); + Report->SetBoolField(TEXT("valide"), Failures.Num() == Before); + SaveJson(Report, FPaths::Combine(Out, FString::Printf(TEXT("rapport_poseT_%s.json"), *Body.Name))); + UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL poseT %s profil %s : %d segments, %d reglages, symetrie %.3f deg, %s"), + *Body.Name, *Template.Profile, Fit.Segments.Num(), Fit.Adjusts.Num(), WorstGap, + Failures.Num() == Before ? TEXT("OK") : TEXT("ECHEC")); + return Failures.Num() == Before; + } +} + +ULPAnimEvalCommandlet::ULPAnimEvalCommandlet() +{ + IsClient = false; + IsEditor = true; + IsServer = false; + LogToConsole = true; +} + +int32 ULPAnimEvalCommandlet::Main(const FString& Params) +{ + FString ClipDir = TEXT("/Game/Quaternius/UAL"); + FString RefClip = TEXT("A_TPose"); + FString BodiesText = TEXT("Bridget:B_,Michael:M_"); + FString PoseDir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Measurement")); + FString Out = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AnimPipeline"), TEXT("quaternius")); + FString ClipsText; + int32 Instants = 3; + float SeuilPoseT = 1.f; + float SeuilSymetrie = 0.5f; + float SeuilReglage = 30.f; + float SeuilEval = 1.f; + float SeuilTorsion = -1.f; + FParse::Value(*Params, TEXT("ClipDir="), ClipDir); + FParse::Value(*Params, TEXT("RefClip="), RefClip); + FParse::Value(*Params, TEXT("Bodies="), BodiesText, false); + FParse::Value(*Params, TEXT("PoseDir="), PoseDir); + FParse::Value(*Params, TEXT("Out="), Out); + FParse::Value(*Params, TEXT("Clips="), ClipsText, false); + FParse::Value(*Params, TEXT("Instants="), Instants); + FParse::Value(*Params, TEXT("SeuilPoseT="), SeuilPoseT); + FParse::Value(*Params, TEXT("SeuilSymetrie="), SeuilSymetrie); + FParse::Value(*Params, TEXT("SeuilReglage="), SeuilReglage); + FParse::Value(*Params, TEXT("SeuilEval="), SeuilEval); + // Torsion Bend / Twist : relevee toujours, bloquante seulement si un seuil (degres) est donne. + FParse::Value(*Params, TEXT("SeuilTorsion="), SeuilTorsion); + // Controles independants (AP4), a chaque image. Seuils etalonnes sur les 43 clips Quaternius (CHG-208) : + // au-dela du seuil, le clip est "a revoir" (liste pour controle visuel) ; au-dela de RejetFacteur fois le + // seuil, il est "rejete" et bloque la livraison. + FString MeshesText = TEXT("Bridget:/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp,Michael:/Game/Heavy/Daz/Michael/michael.michael"); + FParse::Value(*Params, TEXT("Meshes="), MeshesText, false); + const bool bChecks = !FParse::Param(*Params, TEXT("SansControles")); + float SeuilSol = 3.f, SeuilGlissement = 3.f, SeuilGenou = 1.f, SeuilFlexion = 5.f, SeuilSaut = 10.f, SeuilPenetration = 1.f; + float RejetFacteur = 3.f; + FParse::Value(*Params, TEXT("RejetFacteur="), RejetFacteur); + FParse::Value(*Params, TEXT("SeuilSol="), SeuilSol); + FParse::Value(*Params, TEXT("SeuilGlissement="), SeuilGlissement); + FParse::Value(*Params, TEXT("SeuilGenou="), SeuilGenou); + FParse::Value(*Params, TEXT("SeuilFlexion="), SeuilFlexion); + FParse::Value(*Params, TEXT("SeuilSaut="), SeuilSaut); + FParse::Value(*Params, TEXT("SeuilPenetration="), SeuilPenetration); + TMap Meshes; + { + TArray Items; + MeshesText.ParseIntoArray(Items, TEXT(","), true); + for (const FString& Item : Items) + { + FString Name, Path; + if (Item.Split(TEXT(":"), &Name, &Path)) + { + Meshes.Add(Name, Path); + } + } + } + Instants = FMath::Max(Instants, 1); + IFileManager::Get().MakeDirectory(*Out, true); + IFileManager::Get().MakeDirectory(*PoseDir, true); + const TArray Bodies = ParseBodies(BodiesText); + TArray Failures; + TSharedRef Summary = MakeShared(); + Summary->SetStringField(TEXT("dossier_clips"), ClipDir); + Summary->SetNumberField(TEXT("instants"), Instants); + + // Etape 3 : pose T de chaque corps, sur le clip de pose de reference. + FLPPose Reference; + if (!LPPoseMeasure::Sample(ClipPath(ClipDir, RefClip), 0.f, Reference) || Reference.Profile.IsEmpty()) + { + UE_LOG(LogLPAnimEval, Error, TEXT("ANIMEVAL reference absente ou squelette inconnu : %s"), *ClipPath(ClipDir, RefClip)); + return 1; + } + for (const FBody& Body : Bodies) + { + FLPPose Template; + if (!LPPoseMeasure::Sample(ClipPath(ClipDir, Body.Prefix + RefClip), 0.f, Template) || Template.Profile.IsEmpty()) + { + Failures.Add(FString::Printf(TEXT("%s : clip %s%s absent ou squelette inconnu"), *Body.Name, *Body.Prefix, *RefClip)); + continue; + } + CheckTPose(Reference, Template, Body, PoseDir, Out, SeuilPoseT, SeuilSymetrie, SeuilReglage, Failures); + } + + // Etape 5 : formule et clip converti contre la reference, a chaque instant. + TArray Clips = ListClips(ClipDir, Bodies); + if (!ClipsText.IsEmpty()) + { + TArray Wanted; + ClipsText.ParseIntoArray(Wanted, TEXT(","), true); + Clips = Clips.FilterByPredicate([&Wanted](const FString& Clip) { return Wanted.Contains(Clip); }); + } + Summary->SetNumberField(TEXT("clips"), Clips.Num()); + TArray> BodyReports; + TArray> ToReview; + for (const FBody& Body : Bodies) + { + TArray Csv; + Csv.Add(TEXT("clip,instant,formule_pire,formule_os,brut_pire,brut_os,torsion_deg,torsion_os")); + float WorstFormula = 0.f; + float WorstTwist = 0.f; + float WorstRaw = 0.f; + int32 Rejected = 0; + for (const FString& Clip : Clips) + { + const FString RefPath = ClipPath(ClipDir, Clip); + const UAnimSequence* Anim = LoadObject(nullptr, *RefPath); + const float Length = Anim ? Anim->GetPlayLength() : 0.f; + for (int32 Step = 0; Step < Instants; ++Step) + { + // Meme instant que la tournee de la sandbox (k / Instants de la duree). + const float Fraction = static_cast(Step) / static_cast(Instants); + const float Time = FMath::Clamp(Fraction, 0.f, 1.f) * Length; + FLPPose RefPose; + FLPPose Raw; + FLPPose Formula; + if (!LPPoseMeasure::Sample(RefPath, Time, RefPose) || !LPPoseMeasure::Sample(ClipPath(ClipDir, Body.Prefix + Clip), Time, Raw) + || !LPPoseMeasure::TransferPose(RefPose, Raw, Formula)) + { + Failures.Add(FString::Printf(TEXT("%s : %s a %.2f non mesurable"), *Body.Name, *Clip, Fraction)); + continue; + } + const FLPPoseMeasureReport FormulaCheck = LPPoseMeasure::Compare(*Body.Name, *Clip, RefPose, Formula); + const FLPPoseMeasureReport RawCheck = LPPoseMeasure::Compare(*Body.Name, *Clip, RefPose, Raw); + FString TwistBone; + const float Twist = TwistGap(Raw, Formula, TwistBone); + WorstTwist = FMath::Max(WorstTwist, Twist); + Csv.Add(FString::Printf(TEXT("%s,%.2f,%.2f,%s,%.2f,%s,%.2f,%s"), *Clip, Fraction, + FormulaCheck.WorstAnglePercent, *FormulaCheck.WorstAngleBone, RawCheck.WorstAnglePercent, *RawCheck.WorstAngleBone, + Twist, *TwistBone)); + if (SeuilTorsion >= 0.f && Twist > SeuilTorsion) + { + ++Rejected; + Failures.Add(FString::Printf(TEXT("%s : %s a %.2f torsion %.2f deg (%s), seuil %.2f"), *Body.Name, *Clip, Fraction, Twist, *TwistBone, SeuilTorsion)); + } + WorstFormula = FMath::Max(WorstFormula, FormulaCheck.WorstAnglePercent); + WorstRaw = FMath::Max(WorstRaw, RawCheck.WorstAnglePercent); + if (FormulaCheck.WorstAnglePercent > SeuilEval || RawCheck.WorstAnglePercent > SeuilEval) + { + ++Rejected; + Failures.Add(FString::Printf(TEXT("%s : %s a %.2f formule %.2f %% (%s), clip %.2f %% (%s)"), *Body.Name, *Clip, Fraction, + FormulaCheck.WorstAnglePercent, *FormulaCheck.WorstAngleBone, RawCheck.WorstAnglePercent, *RawCheck.WorstAngleBone)); + } + } + } + FFileHelper::SaveStringArrayToFile(Csv, *FPaths::Combine(Out, FString::Printf(TEXT("eval_%s.csv"), *Body.Name))); + + // Controles independants, a chaque image de chaque clip converti. + int32 ChecksFlagged = 0; + if (bChecks) + { + FLPPose Template, RefT, BodyT; + const USkeletalMesh* Mesh = Meshes.Contains(Body.Name) ? LoadObject(nullptr, *Meshes[Body.Name]) : nullptr; + if (LPPoseMeasure::Sample(ClipPath(ClipDir, Body.Prefix + RefClip), 0.f, Template) && LPPoseMeasure::TPoses(Reference, Template, RefT, BodyT)) + { + const TArray Trunk = LPAnimChecks::TrunkCapsules(Mesh, BodyT); + UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL controles %s : maillage %s, %d capsules de tronc"), + *Body.Name, Mesh ? *Mesh->GetName() : TEXT("ABSENT"), Trunk.Num()); + const float SelfGap = LPAnimChecks::SelfTest(Trunk, BodyT); + UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL capsule auto-test %s : ecart %.4f cm"), *Body.Name, SelfGap); + if (SelfGap < 0.f || SelfGap > 0.01f) + { + Failures.Add(FString::Printf(TEXT("%s : auto-test de penetration en echec (%.4f cm)"), *Body.Name, SelfGap)); + } + for (const FLPCheckCapsule& Capsule : Trunk) + { + UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL capsule %s %s : rayon %.1f cm, demi-longueur %.1f cm"), + *Body.Name, *BodyT.Names[Capsule.Bone].ToString(), Capsule.Radius, Capsule.HalfLength); + } + TArray Lines; + Lines.Add(TEXT("clip,images,sol_cm,sol_os,glissement_cm,glissement_os,genou_cm,genou_os,flexion_deg,flexion_os,saut_deg,saut_os,penetration_cm,penetration_os,verdict")); + for (const FString& Clip : Clips) + { + const FLPClipChecks C = LPAnimChecks::CheckClip(ClipPath(ClipDir, Clip), ClipPath(ClipDir, Body.Prefix + Clip), RefT, BodyT, Trunk); + TArray Over; + bool bReject = !C.Error.IsEmpty(); + if (bReject) { Over.Add(C.Error); } + auto Test = [&](float Value, float Seuil, const TCHAR* Label, const FString& Bone) + { + if (Value > Seuil) + { + Over.Add(FString::Printf(TEXT("%s %.1f (%s)"), Label, Value, *Bone)); + bReject |= Value > Seuil * RejetFacteur; + } + }; + Test(C.Floor, SeuilSol, TEXT("sol cm"), C.FloorBone); + Test(C.Slide, SeuilGlissement, TEXT("glissement cm"), C.SlideBone); + Test(C.KneeBack, SeuilGenou, TEXT("genou cm"), C.KneeBone); + Test(C.FlexGap, SeuilFlexion, TEXT("flexion deg"), C.FlexJoint); + Test(C.Jump, SeuilSaut, TEXT("saut deg"), C.JumpBone); + Test(C.Penetration, SeuilPenetration, TEXT("penetration cm"), C.PenetrationBone); + const FString Verdict = Over.Num() == 0 ? TEXT("accepte") : bReject ? TEXT("rejete") : TEXT("a revoir"); + Lines.Add(FString::Printf(TEXT("%s,%d,%.2f,%s,%.2f,%s,%.2f,%s,%.2f,%s,%.2f,%s,%.2f,%s,%s"), *Clip, C.Frames, + C.Floor, *C.FloorBone, C.Slide, *C.SlideBone, C.KneeBack, *C.KneeBone, C.FlexGap, *C.FlexJoint, + C.Jump, *C.JumpBone, C.Penetration, *C.PenetrationBone, *Verdict)); + if (Over.Num() > 0) + { + const FString Detail = FString::Printf(TEXT("%s : %s : %s"), *Body.Name, *Clip, *FString::Join(Over, TEXT(", "))); + if (bReject) + { + Failures.Add(Detail); + } + else + { + ++ChecksFlagged; + ToReview.Add(MakeShared(Detail)); + } + } + } + FFileHelper::SaveStringArrayToFile(Lines, *FPaths::Combine(Out, FString::Printf(TEXT("controles_%s.csv"), *Body.Name))); + } + else + { + Failures.Add(FString::Printf(TEXT("%s : controles impossibles (pose T)"), *Body.Name)); + } + } + TSharedRef Item = MakeShared(); + Item->SetStringField(TEXT("corps"), Body.Name); + Item->SetNumberField(TEXT("instants_mesures"), Csv.Num() - 1); + Item->SetNumberField(TEXT("formule_pire"), WorstFormula); + Item->SetNumberField(TEXT("clip_pire"), WorstRaw); + Item->SetNumberField(TEXT("torsion_pire_deg"), WorstTwist); + Item->SetNumberField(TEXT("instants_hors_seuil"), Rejected); + Item->SetNumberField(TEXT("clips_a_revoir_controles"), ChecksFlagged); + BodyReports.Add(MakeShared(Item)); + UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL eval %s : %d instants, formule pire %.2f %%, clip pire %.2f %%, torsion pire %.2f deg, %d hors seuil"), + *Body.Name, Csv.Num() - 1, WorstFormula, WorstRaw, WorstTwist, Rejected); + } + Summary->SetArrayField(TEXT("corps"), BodyReports); + TArray> FailureList; + for (const FString& Failure : Failures) + { + FailureList.Add(MakeShared(Failure)); + UE_LOG(LogLPAnimEval, Warning, TEXT("ANIMEVAL echec %s"), *Failure); + } + Summary->SetArrayField(TEXT("echecs"), FailureList); + // Clips a revoir : n'empechent pas la livraison, a regarder dans la sandbox (planches de la tournee). + Summary->SetArrayField(TEXT("a_revoir"), ToReview); + for (const TSharedPtr& Item : ToReview) + { + UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL a revoir %s"), *Item->AsString()); + } + Summary->SetBoolField(TEXT("valide"), Failures.Num() == 0); + SaveJson(Summary, FPaths::Combine(Out, TEXT("rapport.json"))); + UE_LOG(LogLPAnimEval, Display, TEXT("ANIMEVAL resultat %s : %d echec(s), rapports dans %s"), + Failures.Num() == 0 ? TEXT("VALIDE") : TEXT("REFUSE"), Failures.Num(), *Out); + return Failures.Num() == 0 ? 0 : 1; +} diff --git a/Source/LifePlayEditorTools/LPAnimEvalCommandlet.h b/Source/LifePlayEditorTools/LPAnimEvalCommandlet.h new file mode 100644 index 0000000..2624f04 --- /dev/null +++ b/Source/LifePlayEditorTools/LPAnimEvalCommandlet.h @@ -0,0 +1,30 @@ +// Lot AP2 (Docs/Migration/20_Chaine_Animations.md) : etapes 3 et 5 de la chaine des animations, sans fenetre. +// +// 1. Pose T : ajuste la pose T de chaque corps sur celle de la reference, ecrit retarget_pose_.txt +// (lu par Tools/RetargetQuaternius.py) et controle l'ajustement : segments ajustes <= SeuilPoseT, +// reglages gauche / droite symetriques a SeuilSymetrie degres pres, aucun reglage > SeuilReglage degres. +// 2. Evaluation : pour chaque clip de reference qui a une copie par corps, a Instants instants, compare la +// formule (phase B) et le clip converti a la reference ; ecart pire <= SeuilEval. +// Rapports : /rapport_poseT_.json, /eval_.csv (colonnes de la tournee de la sandbox), +// /rapport.json. Code de sortie 0 si tous les controles passent, 1 sinon. +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPAnimEval [-ClipDir=/Game/Quaternius/UAL] [-RefClip=A_TPose] +// [-Bodies=Bridget:B_,Michael:M_] [-Instants=3] [-PoseDir=] [-Out=] +// [-SeuilPoseT=1] [-SeuilSymetrie=0.5] [-SeuilReglage=30] [-SeuilEval=1] [-Clips=Idle_Loop,Walk_Loop] + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPAnimEvalCommandlet.generated.h" + +UCLASS() +class ULPAnimEvalCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPAnimEvalCommandlet(); + virtual int32 Main(const FString& Params) override; +}; diff --git a/Source/LifePlayEditorTools/LPAnimTwistCommandlet.cpp b/Source/LifePlayEditorTools/LPAnimTwistCommandlet.cpp new file mode 100644 index 0000000..5902efb --- /dev/null +++ b/Source/LifePlayEditorTools/LPAnimTwistCommandlet.cpp @@ -0,0 +1,355 @@ +#include "LPAnimTwistCommandlet.h" + +#include "Animation/AnimData/IAnimationDataController.h" +#include "Animation/AnimData/IAnimationDataModel.h" +#include "Animation/AnimSequence.h" +#include "Animation/Skeleton.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Dom/JsonObject.h" +#include "HAL/FileManager.h" +#include "LPPoseMeasure.h" +#include "LPSkeletonProfile.h" +#include "Misc/FileHelper.h" +#include "Misc/PackageName.h" +#include "Misc/Paths.h" +#include "Serialization/JsonSerializer.h" +#include "Serialization/JsonWriter.h" +#include "UObject/Package.h" +#include "UObject/SavePackage.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPAnimTwist, Log, All); + +namespace +{ + // Tolerances du controle de conservation : un os hors des paires ne doit pas bouger. + constexpr float KeepDegrees = 0.01f; + constexpr float KeepCentimeters = 0.01f; + + struct FTwistPair + { + int32 Bend = INDEX_NONE; + int32 Twist = INDEX_NONE; + int32 AxisChild = INDEX_NONE; + // Axe du segment dans le repere du parent en pose T ; rotation locale de l'os Bend en pose T ; + // rotation locale de l'os Twist (par rapport a l'os Bend) en pose T. + FVector AxisParent = FVector::ZeroVector; + FQuat BendLocalT = FQuat::Identity; + FQuat TwistLocalT = FQuat::Identity; + }; + + struct FClipResult + { + FString Clip; + int32 Keys = 0; + float TwistMoved = 0.f; + float KeepAngle = 0.f; + float KeepDistance = 0.f; + bool bSaved = false; + FString Error; + }; + + FString ClipPath(const FString& Dir, const FString& Name) + { + return FString::Printf(TEXT("%s/%s.%s"), *Dir, *Name, *Name); + } + + TArray PairsFor(const FLPPose& BodyT) + { + TArray Pairs; + const FLPSkeletonProfile* Profile = LPSkeletonProfile::Find(BodyT.Profile); + if (!Profile) + { + return Pairs; + } + struct FRoles { const TCHAR* Bend; const TCHAR* Twist; const TCHAR* Axis; }; + const FRoles Roles[] = { + { TEXT("upperarm"), TEXT("upperarm_twist"), TEXT("lowerarm") }, + { TEXT("lowerarm"), TEXT("lowerarm_twist"), TEXT("hand") }, + { TEXT("thigh"), TEXT("thigh_twist"), TEXT("calf") }, + }; + for (const TCHAR* Side : { TEXT("_l"), TEXT("_r") }) + { + for (const FRoles& Role : Roles) + { + FTwistPair Pair; + Pair.Bend = BodyT.Names.IndexOfByKey(Profile->Bone(FString(Role.Bend) + Side)); + Pair.Twist = BodyT.Names.IndexOfByKey(Profile->Bone(FString(Role.Twist) + Side)); + Pair.AxisChild = BodyT.Names.IndexOfByKey(Profile->Bone(FString(Role.Axis) + Side)); + if (Pair.Bend == INDEX_NONE || Pair.Twist == INDEX_NONE || Pair.AxisChild == INDEX_NONE) + { + continue; + } + const int32 Parent = BodyT.Parents[Pair.Bend]; + if (Parent == INDEX_NONE) + { + continue; + } + const FQuat ParentT = BodyT.Component[Parent].GetRotation(); + const FVector AxisT = (BodyT.Component[Pair.AxisChild].GetLocation() - BodyT.Component[Pair.Bend].GetLocation()).GetSafeNormal(); + Pair.AxisParent = ParentT.UnrotateVector(AxisT); + Pair.BendLocalT = ParentT.Inverse() * BodyT.Component[Pair.Bend].GetRotation(); + Pair.TwistLocalT = BodyT.Component[Pair.Bend].GetRotation().Inverse() * BodyT.Component[Pair.Twist].GetRotation(); + if (!AxisT.IsNearlyZero()) + { + Pairs.Add(Pair); + } + } + } + return Pairs; + } + + // Traite un clip : cles locales -> composant, swing / twist, nouvelles cles locales des os modifies. + FClipResult ProcessClip(UAnimSequence* Sequence, const FLPPose& BodyT, const TArray& Pairs, bool bDryRun) + { + FClipResult Result; + Result.Clip = Sequence->GetName(); + const USkeleton* Skeleton = Sequence->GetSkeleton(); + const IAnimationDataModel* Model = Sequence->GetDataModel(); + if (!Skeleton || !Model) + { + Result.Error = TEXT("squelette ou donnees absents"); + return Result; + } + const FReferenceSkeleton& Ref = Skeleton->GetReferenceSkeleton(); + const int32 Num = Ref.GetNum(); + if (Num != BodyT.Names.Num()) + { + Result.Error = TEXT("squelette different du corps de reference"); + return Result; + } + const int32 Keys = Model->GetNumberOfKeys(); + Result.Keys = Keys; + TArray> Local; + Local.SetNum(Num); + for (int32 Bone = 0; Bone < Num; ++Bone) + { + const FName Name = Ref.GetBoneName(Bone); + if (Model->IsValidBoneTrackName(Name)) + { + Model->GetBoneTrackTransforms(Name, Local[Bone]); + } + if (Local[Bone].Num() != Keys) + { + Local[Bone].Init(Ref.GetRefBonePose()[Bone], Keys); + } + } + TSet Changed; + TArray> NewLocal = Local; + for (int32 Key = 0; Key < Keys; ++Key) + { + TArray Comp; + Comp.SetNum(Num); + for (int32 Bone = 0; Bone < Num; ++Bone) + { + const int32 Parent = Ref.GetParentIndex(Bone); + Comp[Bone] = Parent == INDEX_NONE ? Local[Bone][Key] : Local[Bone][Key] * Comp[Parent]; + } + for (const FTwistPair& Pair : Pairs) + { + // Rotation complete du membre : portee par l'os Twist (sa rotation locale de pose T retiree), que le + // clip sorte du retargeter ou ait deja ete traite. Decomposition dans le repere du parent (AP4). + const int32 Parent = Ref.GetParentIndex(Pair.Bend); + const FQuat ParentNow = Comp[Parent].GetRotation(); + const FQuat Full = Comp[Pair.Twist].GetRotation() * Pair.TwistLocalT.Inverse(); + const FQuat Delta = ((ParentNow.Inverse() * Full) * Pair.BendLocalT.Inverse()).GetNormalized(); + FQuat Swing; + FQuat Twist; + Delta.ToSwingTwist(Pair.AxisParent, Swing, Twist); + const FQuat Before = Comp[Pair.Bend].GetRotation(); + FTransform BendNew = Comp[Pair.Bend]; + BendNew.SetRotation((ParentNow * Swing * Pair.BendLocalT).GetNormalized()); + Result.TwistMoved = FMath::Max(Result.TwistMoved, FMath::RadiansToDegrees(Before.AngularDistance(BendNew.GetRotation()))); + FTransform& BendLocal = NewLocal[Pair.Bend][Key]; + BendLocal.SetRotation((ParentNow.Inverse() * BendNew.GetRotation()).GetNormalized()); + Changed.Add(Pair.Bend); + // Enfants directs de l'os Bend (dont l'os Twist) : transformation composant conservee. + for (int32 Child = 0; Child < Num; ++Child) + { + if (Ref.GetParentIndex(Child) != Pair.Bend) + { + continue; + } + FTransform& ChildLocal = NewLocal[Child][Key]; + ChildLocal.SetRotation((BendNew.GetRotation().Inverse() * Comp[Child].GetRotation()).GetNormalized()); + ChildLocal.SetTranslation(BendNew.GetRotation().UnrotateVector(Comp[Child].GetLocation() - BendNew.GetLocation())); + Changed.Add(Child); + } + } + // Controle : toute la chaine recomposee avec les nouvelles cles, hors os Bend, doit etre inchangee. + TArray Check; + Check.SetNum(Num); + for (int32 Bone = 0; Bone < Num; ++Bone) + { + const int32 Parent = Ref.GetParentIndex(Bone); + Check[Bone] = Parent == INDEX_NONE ? NewLocal[Bone][Key] : NewLocal[Bone][Key] * Check[Parent]; + bool bBend = false; + for (const FTwistPair& Pair : Pairs) + { + bBend |= Pair.Bend == Bone; + } + if (!bBend) + { + Result.KeepAngle = FMath::Max(Result.KeepAngle, FMath::RadiansToDegrees(Check[Bone].GetRotation().AngularDistance(Comp[Bone].GetRotation()))); + Result.KeepDistance = FMath::Max(Result.KeepDistance, static_cast(FVector::Dist(Check[Bone].GetLocation(), Comp[Bone].GetLocation()))); + } + } + } + if (Result.KeepAngle > KeepDegrees || Result.KeepDistance > KeepCentimeters) + { + Result.Error = FString::Printf(TEXT("conservation hors tolerance : %.4f deg, %.4f cm"), Result.KeepAngle, Result.KeepDistance); + return Result; + } + if (bDryRun) + { + return Result; + } + IAnimationDataController& Controller = Sequence->GetController(); + Controller.OpenBracket(FText::FromString(TEXT("Repartition de la torsion Bend / Twist (AP3)")), false); + for (const int32 Bone : Changed) + { + const FName Name = Ref.GetBoneName(Bone); + if (!Model->IsValidBoneTrackName(Name)) + { + Controller.AddBoneCurve(Name, false); + } + TArray Positions; + TArray Rotations; + TArray Scales; + for (const FTransform& Xform : NewLocal[Bone]) + { + Positions.Add(Xform.GetTranslation()); + Rotations.Add(Xform.GetRotation()); + Scales.Add(Xform.GetScale3D()); + } + Controller.SetBoneTrackKeys(Name, Positions, Rotations, Scales, false); + } + Controller.CloseBracket(false); + UPackage* Package = Sequence->GetPackage(); + Package->MarkPackageDirty(); + FSavePackageArgs Args; + Args.TopLevelFlags = RF_Public | RF_Standalone; + const FString File = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension()); + Result.bSaved = UPackage::SavePackage(Package, Sequence, *File, Args); + if (!Result.bSaved) + { + Result.Error = TEXT("sauvegarde impossible"); + } + return Result; + } +} + +ULPAnimTwistCommandlet::ULPAnimTwistCommandlet() +{ + IsClient = false; + IsEditor = true; + IsServer = false; + LogToConsole = true; +} + +int32 ULPAnimTwistCommandlet::Main(const FString& Params) +{ + FString ClipDir = TEXT("/Game/Quaternius/UAL"); + FString RefClip = TEXT("A_TPose"); + FString BodiesText = TEXT("Bridget:B_,Michael:M_"); + FString ClipsText; + FString Out = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AnimPipeline"), TEXT("quaternius")); + FParse::Value(*Params, TEXT("ClipDir="), ClipDir); + FParse::Value(*Params, TEXT("RefClip="), RefClip); + FParse::Value(*Params, TEXT("Bodies="), BodiesText, false); + FParse::Value(*Params, TEXT("Clips="), ClipsText, false); + FParse::Value(*Params, TEXT("Out="), Out); + const bool bDryRun = FParse::Param(*Params, TEXT("DryRun")); + IFileManager::Get().MakeDirectory(*Out, true); + + FLPPose Reference; + if (!LPPoseMeasure::Sample(ClipPath(ClipDir, RefClip), 0.f, Reference)) + { + UE_LOG(LogLPAnimTwist, Error, TEXT("TORSION reference absente %s"), *ClipPath(ClipDir, RefClip)); + return 1; + } + IAssetRegistry& Registry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); + Registry.ScanPathsSynchronous({ ClipDir }, true); + TArray Assets; + Registry.GetAssetsByPath(FName(*ClipDir), Assets, false); + TArray Wanted; + ClipsText.ParseIntoArray(Wanted, TEXT(","), true); + + TArray BodyItems; + BodiesText.ParseIntoArray(BodyItems, TEXT(","), true); + int32 Failures = 0; + TSharedRef Report = MakeShared(); + Report->SetBoolField(TEXT("essai_a_blanc"), bDryRun); + TArray> BodyReports; + for (const FString& Item : BodyItems) + { + FString BodyName; + FString Prefix; + if (!Item.Split(TEXT(":"), &BodyName, &Prefix)) + { + continue; + } + FLPPose Template; + FLPPose RefT; + FLPPose BodyT; + if (!LPPoseMeasure::Sample(ClipPath(ClipDir, Prefix + RefClip), 0.f, Template) + || !LPPoseMeasure::TPoses(Reference, Template, RefT, BodyT)) + { + UE_LOG(LogLPAnimTwist, Error, TEXT("TORSION %s : pose T impossible"), *BodyName); + ++Failures; + continue; + } + const TArray Pairs = PairsFor(BodyT); + UE_LOG(LogLPAnimTwist, Display, TEXT("TORSION %s profil %s : %d paires Bend / Twist"), *BodyName, *BodyT.Profile, Pairs.Num()); + TArray> ClipReports; + float WorstMoved = 0.f; + for (const FAssetData& Asset : Assets) + { + const FString Name = Asset.AssetName.ToString(); + if (Asset.AssetClassPath != UAnimSequence::StaticClass()->GetClassPathName() || !Name.StartsWith(Prefix) + || (Wanted.Num() > 0 && !Wanted.Contains(Name.RightChop(Prefix.Len())))) + { + continue; + } + UAnimSequence* Sequence = LoadObject(nullptr, *ClipPath(ClipDir, Name)); + if (!Sequence) + { + continue; + } + const FClipResult Result = ProcessClip(Sequence, BodyT, Pairs, bDryRun); + WorstMoved = FMath::Max(WorstMoved, Result.TwistMoved); + TSharedRef Clip = MakeShared(); + Clip->SetStringField(TEXT("clip"), Result.Clip); + Clip->SetNumberField(TEXT("cles"), Result.Keys); + Clip->SetNumberField(TEXT("torsion_deplacee_deg"), Result.TwistMoved); + Clip->SetNumberField(TEXT("conservation_deg"), Result.KeepAngle); + Clip->SetNumberField(TEXT("conservation_cm"), Result.KeepDistance); + Clip->SetBoolField(TEXT("sauve"), Result.bSaved); + Clip->SetStringField(TEXT("erreur"), Result.Error); + ClipReports.Add(MakeShared(Clip)); + if (!Result.Error.IsEmpty()) + { + ++Failures; + UE_LOG(LogLPAnimTwist, Warning, TEXT("TORSION echec %s : %s"), *Result.Clip, *Result.Error); + } + else + { + UE_LOG(LogLPAnimTwist, Display, TEXT("TORSION %s %d cles, torsion deplacee %.1f deg, conservation %.5f deg %.5f cm%s"), + *Result.Clip, Result.Keys, Result.TwistMoved, Result.KeepAngle, Result.KeepDistance, bDryRun ? TEXT(" (essai)") : TEXT("")); + } + } + TSharedRef Body = MakeShared(); + Body->SetStringField(TEXT("corps"), BodyName); + Body->SetNumberField(TEXT("paires"), Pairs.Num()); + Body->SetNumberField(TEXT("torsion_deplacee_max_deg"), WorstMoved); + Body->SetArrayField(TEXT("clips"), ClipReports); + BodyReports.Add(MakeShared(Body)); + } + Report->SetArrayField(TEXT("corps"), BodyReports); + Report->SetNumberField(TEXT("echecs"), Failures); + Report->SetBoolField(TEXT("valide"), Failures == 0); + FString Text; + FJsonSerializer::Serialize(Report, TJsonWriterFactory<>::Create(&Text)); + FFileHelper::SaveStringToFile(Text, *FPaths::Combine(Out, TEXT("rapport_torsion.json")), FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); + UE_LOG(LogLPAnimTwist, Display, TEXT("TORSION resultat %s : %d echec(s)"), Failures == 0 ? TEXT("VALIDE") : TEXT("REFUSE"), Failures); + return Failures == 0 ? 0 : 1; +} diff --git a/Source/LifePlayEditorTools/LPAnimTwistCommandlet.h b/Source/LifePlayEditorTools/LPAnimTwistCommandlet.h new file mode 100644 index 0000000..57b8901 --- /dev/null +++ b/Source/LifePlayEditorTools/LPAnimTwistCommandlet.h @@ -0,0 +1,29 @@ +// Lot AP3 (Docs/Migration/20_Chaine_Animations.md) : repartition de la torsion entre os Bend et Twist. +// +// Le retargeter Unreal met toute la rotation d'un membre dans l'os Bend (les os Twist sont hors chaine) : +// la peau de l'epaule, de l'avant-bras et de la cuisse se vrille. Pour chaque cle des clips convertis, la +// rotation de l'os Bend depuis la pose T ajustee est decomposee autour de l'axe du segment (swing / twist) : +// l'os Bend garde la flexion, l'os Twist recoit la rotation complete. Tous les autres os gardent exactement +// leur transformation (controle a chaque cle) ; c'est la repartition de la formule (LPPoseMeasure::TransferPose). +// Les paires viennent du profil du corps (roles upperarm / upperarm_twist / lowerarm, etc.). +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPAnimTwist [-ClipDir=/Game/Quaternius/UAL] [-RefClip=A_TPose] +// [-Bodies=Bridget:B_,Michael:M_] [-Clips=Idle_Loop,Walk_Loop] [-DryRun] [-Out=] +// Rapport : /rapport_torsion.json. Code de sortie 0 si chaque clip est traite et conserve les autres os. + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPAnimTwistCommandlet.generated.h" + +UCLASS() +class ULPAnimTwistCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPAnimTwistCommandlet(); + virtual int32 Main(const FString& Params) override; +}; diff --git a/Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.cpp b/Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.cpp new file mode 100644 index 0000000..d688206 --- /dev/null +++ b/Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.cpp @@ -0,0 +1,519 @@ +// Portage UE 5.5 - voir LPPairCollisionTestCommandlet.h + +#include "LPPairCollisionTestCommandlet.h" + +#include "AssetRegistry/AssetData.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Components/CapsuleComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Engine/StaticMesh.h" +#include "Engine/World.h" +#include "GameFramework/Character.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "PhysicsEngine/BodySetup.h" +#include "PreviewScene.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPPairCollision, Log, All); + +namespace +{ + struct FProp + { + FString Name; + FVector Location = FVector::ZeroVector; + FRotator Rotation = FRotator::ZeroRotator; + FVector Scale = FVector::OneVector; + }; + + struct FProbe + { + bool bBlocked = false; + bool bStartInside = false; + FString Hit; + float Moved = 0.f; + }; + + float Token(const FString& Part, const TCHAR* Key) + { + const FString Needle = FString::Printf(TEXT("%s="), Key); + const int32 At = Part.Find(Needle); + return At == INDEX_NONE ? 0.f : FCString::Atof(*Part + At + Needle.Len()); + } + + bool LoadWorldFile(const FString& Path, FVector& RoomScale, TArray& Props) + { + TArray Lines; + if (!FFileHelper::LoadFileToStringArray(Lines, *Path)) + { + return false; + } + + bool bWantName = false; + int32 Slash = 0; + FString Name; + for (FString Line : Lines) + { + Line.TrimStartAndEndInline(); + if (Line.IsEmpty()) + { + continue; + } + if (Line == TEXT("*****")) + { + bWantName = true; + Slash = 0; + Name.Reset(); + continue; + } + if (bWantName) + { + Name = Line; + bWantName = false; + continue; + } + if (Line == TEXT("///")) + { + ++Slash; + continue; + } + if (Name.IsEmpty() && Line.StartsWith(TEXT("X="))) + { + RoomScale = FVector(Token(Line, TEXT("X")), Token(Line, TEXT("Y")), Token(Line, TEXT("Z"))); + continue; + } + if (Slash < 2 || !Line.StartsWith(TEXT("X="))) + { + continue; + } + + TArray Parts; + Line.ParseIntoArray(Parts, TEXT(";"), true); + if (Parts.Num() < 3) + { + continue; + } + FProp Prop; + Prop.Name = Name; + Prop.Location = FVector(Token(Parts[0], TEXT("X")), Token(Parts[0], TEXT("Y")), Token(Parts[0], TEXT("Z"))); + Prop.Rotation = FRotator(Token(Parts[1], TEXT("P")), Token(Parts[1], TEXT("Y")), Token(Parts[1], TEXT("R"))); + Prop.Scale = FVector(Token(Parts[2], TEXT("X")), Token(Parts[2], TEXT("Y")), Token(Parts[2], TEXT("Z"))); + Props.Add(Prop); + Slash = 0; + } + return Props.Num() > 0 && !RoomScale.IsNearlyZero(); + } + + int32 BodyElems(UStaticMeshComponent* Comp) + { + const UBodySetup* Body = Comp ? Comp->GetBodySetup() : nullptr; + return Body ? Body->AggGeom.GetElementCount() : 0; + } + + void SetPawnResponse(const TArray& Meshes, const ECollisionResponse Response) + { + for (UStaticMeshComponent* Mesh : Meshes) + { + Mesh->SetCollisionResponseToChannel(ECC_Pawn, Response); + } + } + + void ApplyBodies(ACharacter* A, ACharacter* B, const bool bOn, const bool bIgnoreEachOther) + { + auto Apply = [bOn](ACharacter* Who) + { + UCapsuleComponent* Cap = Who->GetCapsuleComponent(); + Cap->SetCollisionEnabled(bOn ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision); + if (UCharacterMovementComponent* Move = Who->GetCharacterMovement()) + { + Move->GravityScale = bOn ? 1.f : 0.f; + } + Cap->ClearMoveIgnoreActors(); + }; + Apply(A); + Apply(B); + if (bIgnoreEachOther) + { + A->GetCapsuleComponent()->IgnoreActorWhenMoving(B, true); + B->GetCapsuleComponent()->IgnoreActorWhenMoving(A, true); + } + A->GetCapsuleComponent()->RecreatePhysicsState(); + B->GetCapsuleComponent()->RecreatePhysicsState(); + } + + FProbe Probe(ACharacter* Who, const FVector& From, const FVector& Delta) + { + Who->SetActorLocation(From, false, nullptr, ETeleportType::TeleportPhysics); + UCapsuleComponent* Cap = Who->GetCapsuleComponent(); + const FVector Start = Cap->GetComponentLocation(); + FHitResult Hit; + Cap->MoveComponent(Delta, Cap->GetComponentQuat(), true, &Hit); + FProbe Result; + Result.Moved = FVector::Dist(Start, Cap->GetComponentLocation()); + Result.bStartInside = Hit.bStartPenetrating; + Result.bBlocked = Result.Moved < Delta.Size() - 2.f; + if (Hit.bBlockingHit || Hit.bStartPenetrating) + { + Result.Hit = GetNameSafe(Hit.GetActor()); + if (const UPrimitiveComponent* Comp = Hit.GetComponent()) + { + const UStaticMeshComponent* Mesh = Cast(Comp); + const FString MeshName = (Mesh && Mesh->GetStaticMesh()) + ? Mesh->GetStaticMesh()->GetName() + : Comp->GetName(); + Result.Hit += TEXT("/") + MeshName; + } + } + Who->SetActorLocation(From, false, nullptr, ETeleportType::TeleportPhysics); + return Result; + } + + bool FindPairSpot(UWorld* World, float Radius, float HalfHeight, FVector& OutA, FVector& OutB) + { + const FCollisionShape Shape = FCollisionShape::MakeCapsule(Radius, HalfHeight); + FCollisionQueryParams Query(SCENE_QUERY_STAT(LPPairSpot), false); + const float Gap = Radius * 2.f + 40.f; + const float Z = HalfHeight + 20.f; + for (float Y = -700.f; Y <= 700.f; Y += 80.f) + { + for (float X = -700.f; X <= 700.f; X += 80.f) + { + const FVector A(X, Y, Z); + const FVector B(X + Gap, Y, Z); + const bool bA = World->OverlapBlockingTestByChannel(A, FQuat::Identity, ECC_WorldStatic, Shape, Query); + const bool bB = World->OverlapBlockingTestByChannel(B, FQuat::Identity, ECC_WorldStatic, Shape, Query); + if (!bA && !bB) + { + OutA = A; + OutB = B; + return true; + } + } + } + return false; + } +} + +ULPPairCollisionTestCommandlet::ULPPairCollisionTestCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPPairCollisionTestCommandlet::Main(const FString& Params) +{ + TArray Tokens, Switches; + TMap ParamsMap; + ParseCommandLine(*Params, Tokens, Switches, ParamsMap); + + const FString WorldFile = ParamsMap.FindRef(TEXT("World")).IsEmpty() + ? FPaths::Combine(FPaths::ProjectDir(), TEXT("Content/Modules/vin_Base/World/work___vin01.lpworld")) + : ParamsMap.FindRef(TEXT("World")); + const FString OutPath = ParamsMap.FindRef(TEXT("Out")).IsEmpty() + ? FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Logs/portage/LPPairCollisionTest.txt")) + : ParamsMap.FindRef(TEXT("Out")); + + FVector RoomScale = FVector::OneVector; + TArray Props; + if (!LoadWorldFile(WorldFile, RoomScale, Props)) + { + UE_LOG(LogLPPairCollision, Error, TEXT("Chambre illisible : %s"), *WorldFile); + return 1; + } + + FAssetRegistryModule& Assets = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + Assets.Get().SearchAllAssets(true); + TArray MeshAssets; + Assets.Get().GetAssetsByClass(UStaticMesh::StaticClass()->GetClassPathName(), MeshAssets, true); + TMap MeshPath; + for (const FAssetData& Data : MeshAssets) + { + const FString Name = Data.AssetName.ToString(); + const FString Path = FString::Printf(TEXT("%s.%s"), *Data.PackageName.ToString(), *Name); + const bool bFurniture = Path.Contains(TEXT("/Furniture/")) || Path.Contains(TEXT("/Heavy/")); + if (!MeshPath.Contains(Name) || bFurniture) + { + MeshPath.Add(Name, Path); + } + } + + if (!GEngine) + { + UE_LOG(LogLPPairCollision, Error, TEXT("Pas de moteur, scene d'essai impossible.")); + return 1; + } + + FPreviewScene::ConstructionValues SceneValues; + SceneValues.SetCreateDefaultLighting(false).SetEditor(false).SetTransactional(false).SetForceMipsResident(false); + FPreviewScene Scene(SceneValues); + UWorld* World = Scene.GetWorld(); + + FActorSpawnParameters Spawn; + Spawn.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + Spawn.ObjectFlags |= RF_Transient; + + AActor* Room = World->SpawnActor(AActor::StaticClass(), FTransform::Identity, Spawn); + USceneComponent* Root = NewObject(Room, TEXT("RoomRoot")); + Room->SetRootComponent(Root); + Root->RegisterComponent(); + Root->SetWorldScale3D(RoomScale); + + TArray Furniture; + UStaticMeshComponent* Wall = nullptr; + double WallArea = 0.0; + int32 Missing = 0; + int32 WithoutBody = 0; + for (const FProp& Prop : Props) + { + const FString* Path = MeshPath.Find(Prop.Name); + UStaticMesh* Mesh = Path ? LoadObject(nullptr, **Path) : nullptr; + if (!Mesh) + { + ++Missing; + UE_LOG(LogLPPairCollision, Warning, TEXT("Maillage absent : %s"), *Prop.Name); + continue; + } + UStaticMeshComponent* Comp = NewObject(Room); + Comp->SetStaticMesh(Mesh); + Comp->SetupAttachment(Root); + Comp->SetRelativeLocation(Prop.Location); + Comp->SetRelativeRotation(Prop.Rotation); + Comp->SetRelativeScale3D(Prop.Scale); + Comp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + Comp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); + Comp->RegisterComponent(); + Furniture.Add(Comp); + if (BodyElems(Comp) == 0) + { + ++WithoutBody; + } + const FVector Extent = Comp->Bounds.GetBox().GetExtent(); + const double Area = double(Extent.X) * double(Extent.Y); + if ((Prop.Name.StartsWith(TEXT("fnt_new14")) || Prop.Name.StartsWith(TEXT("fnt_new18"))) && Area > WallArea) + { + Wall = Comp; + WallArea = Area; + } + } + if (!Wall && Furniture.Num() > 0) + { + Wall = Furniture[0]; + } + + UStaticMesh* CubeMesh = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); + UStaticMeshComponent* Cube = nullptr; + if (CubeMesh) + { + AActor* CubeActor = World->SpawnActor(AActor::StaticClass(), FTransform::Identity, Spawn); + Cube = NewObject(CubeActor, TEXT("Cube")); + CubeActor->SetRootComponent(Cube); + Cube->SetStaticMesh(CubeMesh); + Cube->SetRelativeLocation(FVector(3000.f, 0.f, 100.f)); + Cube->SetRelativeScale3D(FVector(1.5f)); + Cube->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + Cube->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); + Cube->RegisterComponent(); + Furniture.Add(Cube); + } + + UClass* PersonClass = LoadObject(nullptr, TEXT("/Game/BPs/Person.Person_C")); + const ACharacter* PersonDefaults = PersonClass ? Cast(PersonClass->GetDefaultObject()) : nullptr; + const UCapsuleComponent* PersonCap = PersonDefaults ? PersonDefaults->GetCapsuleComponent() : nullptr; + + auto MakeBody = [&](const FVector& At) -> ACharacter* + { + ACharacter* Who = World->SpawnActor(ACharacter::StaticClass(), At, FRotator::ZeroRotator, Spawn); + if (!Who) + { + return nullptr; + } + UCapsuleComponent* Cap = Who->GetCapsuleComponent(); + if (PersonCap) + { + Cap->SetCapsuleSize(PersonCap->GetUnscaledCapsuleRadius(), PersonCap->GetUnscaledCapsuleHalfHeight()); + Cap->SetCollisionProfileName(PersonCap->GetCollisionProfileName()); + Cap->SetCollisionObjectType(PersonCap->GetCollisionObjectType()); + Cap->SetCollisionResponseToChannels(PersonCap->GetCollisionResponseToChannels()); + } + Cap->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + Cap->RecreatePhysicsState(); + return Who; + }; + + auto ResponseText = [](const UCapsuleComponent* Cap, const ECollisionChannel Channel) -> const TCHAR* + { + switch (Cap->GetCollisionResponseToChannel(Channel)) + { + case ECR_Ignore: return TEXT("Ignore"); + case ECR_Overlap: return TEXT("Overlap"); + case ECR_Block: return TEXT("Block"); + default: return TEXT("?"); + } + }; + + const FVector Park(0.f, 0.f, 8000.f); + ACharacter* A = MakeBody(Park); + ACharacter* B = MakeBody(Park + FVector(200.f, 0.f, 0.f)); + if (!A || !B || !Cube || !Wall) + { + UE_LOG(LogLPPairCollision, Error, TEXT("Scene incomplete (personnes %d, cube %d, mur %d, meubles %d)."), + (A ? 1 : 0) + (B ? 1 : 0), Cube ? 1 : 0, Wall ? 1 : 0, Furniture.Num()); + return 1; + } + + const float Radius = A->GetCapsuleComponent()->GetScaledCapsuleRadius(); + const float Half = A->GetCapsuleComponent()->GetScaledCapsuleHalfHeight(); + FVector SpotA, SpotB; + const bool bInside = FindPairSpot(World, Radius, Half, SpotA, SpotB); + if (!bInside) + { + SpotA = FVector(0.f, 0.f, 4000.f); + SpotB = SpotA + FVector(Radius * 2.f + 40.f, 0.f, 0.f); + } + + const FBox WallBox = Wall->Bounds.GetBox(); + const FVector WallCenter = WallBox.GetCenter(); + FVector WallStart(WallBox.Max.X + Radius + 30.f, WallCenter.Y, FMath::Clamp(Half + 20.f, WallBox.Min.Z, WallBox.Max.Z)); + const FVector WallDelta = FVector(WallCenter.X, WallCenter.Y, WallStart.Z) - WallStart; + + const FBox CubeBox = Cube->Bounds.GetBox(); + const FVector CubeStart(CubeBox.Max.X + Radius + 30.f, CubeBox.GetCenter().Y, CubeBox.GetCenter().Z); + const FVector CubeDelta = FVector(CubeBox.GetCenter().X, CubeStart.Y, CubeStart.Z) - CubeStart; + + const FVector TowardB = (SpotB - SpotA).GetSafeNormal() * 80.f; + + struct FCase + { + const TCHAR* Name; + bool bBlockPawn; + bool bCapsulesOn; + bool bIgnore; + bool bExpectOther; + bool bExpectSolid; + }; + const FCase Cases[] = { + { TEXT("Bureau actuel, capsules allumees"), false, true, false, true, false }, + { TEXT("Scene actuelle, capsules coupees"), false, false, false, false, false }, + { TEXT("Meubles bloquent, capsules encore coupees"), true, false, false, false, false }, + { TEXT("Principes : capsules allumees, ignore mutuel, meubles bloquent"), true, true, true, false, true }, + { TEXT("Meubles bloquent, sans ignorer l'autre"), true, true, false, true, true }, + }; + + TArray Report; + Report.Add(FString::Printf(TEXT("Chambre : %s"), *WorldFile)); + Report.Add(FString::Printf(TEXT("Echelle racine : %s"), *RoomScale.ToString())); + Report.Add(FString::Printf(TEXT("Meubles poses : %d, absents : %d, sans volume simple : %d"), Furniture.Num() - (Cube ? 1 : 0), Missing, WithoutBody)); + if (Furniture.Num() > (Cube ? 1 : 0)) + { + UStaticMeshComponent* Sample = Furniture[0]; + Report.Add(FString::Printf(TEXT("Exemple %s : position monde %s, echelle monde %s, volumes %d"), + *Sample->GetStaticMesh()->GetName(), *Sample->GetComponentLocation().ToString(), + *Sample->GetComponentScale().ToString(), BodyElems(Sample))); + } + Report.Add(FString::Printf(TEXT("Mur d'essai : %s, boite %s"), + Wall->GetStaticMesh() ? *Wall->GetStaticMesh()->GetName() : TEXT("?"), *WallBox.ToString())); + Report.Add(FString::Printf(TEXT("Personnes : capsules moteur, rayon %.1f, demi-hauteur %.1f, profil %s, type %d, corps physique %s. CDO Person : %s"), + Radius, Half, *A->GetCapsuleComponent()->GetCollisionProfileName().ToString(), + int32(A->GetCapsuleComponent()->GetCollisionObjectType()), + A->GetCapsuleComponent()->IsPhysicsStateCreated() ? TEXT("oui") : TEXT("non"), + PersonCap ? TEXT("lu") : TEXT("introuvable, valeurs moteur"))); + Report.Add(FString::Printf(TEXT("Reponses de la capsule Person : Pawn=%s WorldStatic=%s WorldDynamic=%s"), + ResponseText(A->GetCapsuleComponent(), ECC_Pawn), + ResponseText(A->GetCapsuleComponent(), ECC_WorldStatic), + ResponseText(A->GetCapsuleComponent(), ECC_WorldDynamic))); + const bool bPersonBlocksPawn = A->GetCapsuleComponent()->GetCollisionResponseToChannel(ECC_Pawn) == ECR_Block; + Report.Add(bPersonBlocksPawn + ? TEXT("La capsule Person bloque le canal Pawn.") + : TEXT("La capsule Person ne bloque pas le canal Pawn : deux Person se traversent deja.")); + Report.Add(FString::Printf(TEXT("Emplacement du couple : %s -> %s (%s)"), + *SpotA.ToString(), *SpotB.ToString(), bInside ? TEXT("dans la chambre") : TEXT("au-dessus, chambre trop occupee"))); + Report.Add(TEXT("Deplacement = balayage de la capsule (le meme que la marche). L'animation n'est pas jouee.")); + Report.Add(TEXT("")); + + auto ForcePawnProfile = [](ACharacter* Who) + { + UCapsuleComponent* Cap = Who->GetCapsuleComponent(); + Cap->SetCollisionProfileName(TEXT("Pawn")); + Cap->SetCollisionObjectType(ECC_Pawn); + Cap->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + Cap->RecreatePhysicsState(); + }; + + bool bPrinciples = true; + bool bRoom = true; + bool bForced = true; + auto RunCases = [&](const TCHAR* Series, const bool bForcePawn) + { + if (bForcePawn) + { + ForcePawnProfile(A); + ForcePawnProfile(B); + Report.Add(TEXT("")); + Report.Add(TEXT("--- Profil Pawn du moteur (les deux capsules se bloquent, pour isoler IgnoreActorWhenMoving) ---")); + } + for (const FCase& Case : Cases) + { + SetPawnResponse(Furniture, Case.bBlockPawn ? ECR_Block : ECR_Ignore); + ApplyBodies(A, B, Case.bCapsulesOn, Case.bIgnore); + if (bForcePawn && Case.bCapsulesOn) + { + ForcePawnProfile(A); + ForcePawnProfile(B); + if (Case.bIgnore) + { + A->GetCapsuleComponent()->IgnoreActorWhenMoving(B, true); + B->GetCapsuleComponent()->IgnoreActorWhenMoving(A, true); + } + } + + B->SetActorLocation(SpotB, false, nullptr, ETeleportType::TeleportPhysics); + const FProbe Other = Probe(A, SpotA, TowardB); + B->SetActorLocation(Park, false, nullptr, ETeleportType::TeleportPhysics); + const FProbe Solid = Probe(A, CubeStart, CubeDelta); + const FProbe RoomHit = Probe(A, WallStart, WallDelta); + + const bool bExpectOther = (bForcePawn || bPersonBlocksPawn) ? Case.bExpectOther : false; + const bool bOtherOk = Other.bBlocked == bExpectOther && !Other.bStartInside; + const bool bWallOk = RoomHit.bBlocked == Case.bExpectSolid && !RoomHit.bStartInside; + if (!bForcePawn) + { + bPrinciples &= bOtherOk; + bRoom &= bWallOk; + } + else + { + bForced &= bOtherOk && bWallOk; + } + + const FString Line = FString::Printf( + TEXT("%s | %s | autre:%s (attendu %s, %.0f cm, %s) | cube:%s (attendu %s, %.0f cm, %s) | meuble:%s (attendu %s, %.0f cm, %s) | %s"), + Series, Case.Name, + Other.bBlocked ? TEXT("bloque") : TEXT("passe"), bExpectOther ? TEXT("bloque") : TEXT("passe"), Other.Moved, *Other.Hit, + Solid.bBlocked ? TEXT("bloque") : TEXT("passe"), Case.bExpectSolid ? TEXT("bloque") : TEXT("passe"), Solid.Moved, *Solid.Hit, + RoomHit.bBlocked ? TEXT("bloque") : TEXT("passe"), Case.bExpectSolid ? TEXT("bloque") : TEXT("passe"), RoomHit.Moved, *RoomHit.Hit, + (bOtherOk && bWallOk) ? TEXT("OK") : TEXT("ECART")); + Report.Add(Line); + UE_LOG(LogLPPairCollision, Warning, TEXT("%s"), *Line); + } + }; + + RunCases(TEXT("Person"), false); + RunCases(TEXT("Pawn"), true); + + Report.Add(TEXT("")); + Report.Add(FString::Printf(TEXT("Profil Person : %s"), bPrinciples ? TEXT("TIENT") : TEXT("NE TIENT PAS"))); + Report.Add(FString::Printf(TEXT("Profil Pawn force, ignore mutuel : %s"), bForced ? TEXT("TIENT") : TEXT("NE TIENT PAS"))); + Report.Add(FString::Printf(TEXT("Meubles du bureau : %s"), bRoom ? TEXT("TIENT") : TEXT("NE TIENT PAS"))); + Report.Add(TEXT("Le cube du moteur est mesure a part. Le verdict porte sur les meubles de la chambre et sur les deux capsules.")); + UE_LOG(LogLPPairCollision, Warning, TEXT("Person : %s. Pawn force : %s. Meubles : %s."), + bPrinciples ? TEXT("TIENT") : TEXT("NE TIENT PAS"), + bForced ? TEXT("TIENT") : TEXT("NE TIENT PAS"), + bRoom ? TEXT("TIENT") : TEXT("NE TIENT PAS")); + + FFileHelper::SaveStringArrayToFile(Report, *OutPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); + UE_LOG(LogLPPairCollision, Warning, TEXT("Rapport : %s"), *OutPath); + return (bPrinciples && bRoom && bForced) ? 0 : 2; +} diff --git a/Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.h b/Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.h new file mode 100644 index 0000000..b54f246 --- /dev/null +++ b/Source/LifePlayEditorTools/LPPairCollisionTestCommandlet.h @@ -0,0 +1,27 @@ +// Essai isole, editeur seulement. Ne modifie aucun Blueprint et n'entre pas dans la partie. +// +// Reproduit work___vin01 (echelle de la premiere ligne, meubles enfants de cette racine) +// et la reponse de Room::addObject : chaque meuble ignore le canal Pawn. +// Deux capsules copiees sur le CDO de Person marchent l'une vers l'autre, puis vers un mur. +// Les cas reprennent la scene (capsules NoCollision, gravite 0) et le reglage propose +// (capsules QueryAndPhysics, chacun ignore l'autre, meubles bloquent Pawn). +// +// UnrealEditor-Cmd.exe -run=LPPairCollisionTest -unattended -nopause +// -World= (defaut : Content/Modules/vin_Base/World/work___vin01.lpworld) +// -Out= + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPPairCollisionTestCommandlet.generated.h" + +UCLASS() +class ULPPairCollisionTestCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPPairCollisionTestCommandlet(); + virtual int32 Main(const FString& Params) override; +}; diff --git a/Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs b/Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs index 18a8a4f..b99e364 100644 --- a/Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs +++ b/Source/LifePlayEditorTools/LifePlayEditorTools.Build.cs @@ -17,7 +17,9 @@ public class LifePlayEditorTools : ModuleRules "Core", "CoreUObject", "Engine", - "LifePlay" + "LifePlay", + // Lot S2 : LPAnimChecks.h expose FLPPose (LPPoseMeasure.h) dans sa signature. + "LPAnimMeasure" }); PrivateDependencyModuleNames.AddRange(new string[] @@ -38,6 +40,8 @@ public class LifePlayEditorTools : ModuleRules // UWidgetBlueprint au meme titre que les UBlueprint "UMG", "UMGEditor", + // Lot AP2 : LPAnimEvalCommandlet ecrit ses rapports en JSON + "Json", // Portage 5.5 (L7h) : LPCheckMeshesCommandlet lit les donnees sources des UStaticMesh "MeshDescription", // O7e : LPHairFromStaticMeshCommandlet (conversion maillage statique -> SkeletalMesh) diff --git a/Tools/BuildMod.ps1 b/Tools/BuildMod.ps1 index e28e237..f94eab7 100644 --- a/Tools/BuildMod.ps1 +++ b/Tools/BuildMod.ps1 @@ -6,6 +6,10 @@ Sources : ModSources\\ (.uplugin, Content\, Module\ ; voir Tools/ModSource.ps1). 1. controles : plugin et module (MODULE_UNIQUEID = ), version de reference Releases\\Windows, aucun autre plugin dans Mods\ ; + 1b. -Retarget bake|rigs : recopie les clips Quaternius sur Bridget et Michael (Tools/RetargetQuaternius.py + dans UnrealEditor.exe, Slate requis). L'editeur doit etre ferme. -RetargetOnly s'arrete avant la cuisson ; + 1c. -Target sandbox|game ecrit Saved\Config\Windows\LPAnimTarget.ini. -Open lance cette cible + (sandbox : chambre d'essai ; game : partie, carte par defaut) et ne cuit pas ; 2. montage du mod dans Mods\, cuisson DLC contre la version de reference (BuildCookRun -dlcname -basedonreleaseversion, via cmd /c : appelee directement depuis PowerShell, la version est coupee en deux), journal dans Saved\Logs\portage\mods ; remise en place dans ModSources\ dans tous les cas ; @@ -16,13 +20,21 @@ .EXAMPLE .\Tools\BuildMod.ps1 -Name lpr_hair_pocolov + .\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Retarget bake + .\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Retarget bake -RetargetOnly + .\Tools\BuildMod.ps1 -Target sandbox -Open + .\Tools\BuildMod.ps1 -Target game -Open .\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Install D:\Game\LifePlay_Build_5.5_H3d\Windows #> param( - [Parameter(Mandatory)] [string] $Name, + [string] $Name = '', [string] $Version = '', [string] $Out = 'D:\Game\LifePlay_Publication\Mods', - [string] $Install = '' + [string] $Install = '', + [string] $Retarget = '', + [switch] $RetargetOnly, + [string] $Target = '', + [switch] $Open ) $ErrorActionPreference = 'Stop' @@ -31,7 +43,89 @@ $Root = Split-Path $PSScriptRoot -Parent $UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine' $SevenZip = 'C:\Program Files\7-Zip\7z.exe' -# 1. Controles. +function Invoke-AnimToGenesis([string] $ProjectRoot, [string] $Mode) { + $running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue) + if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant la conversion" } + $stepFile = Join-Path $ProjectRoot 'Saved\anim_to_genesis_step.txt' + New-Item -ItemType Directory -Force (Split-Path $stepFile) | Out-Null + [IO.File]::WriteAllText($stepFile, ($Mode.Trim().ToLower() + "`r`n"), (New-Object Text.UTF8Encoding $false)) + $editor = Join-Path $UE 'Binaries\Win64\UnrealEditor.exe' + $project = Join-Path $ProjectRoot 'LifePlay.uproject' + $script = Join-Path $ProjectRoot 'Tools\RetargetQuaternius.py' + $logDir = Join-Path $ProjectRoot 'Saved\Logs' + $success = if ($Mode -eq 'rigs') { 'RETARGET rigs prets' } else { 'RETARGET bridget \d+ michael \d+ deplaces \d+' } + $before = @{} + Get-ChildItem $logDir -Filter 'LifePlay*.log' -ErrorAction SilentlyContinue | ForEach-Object { $before[$_.FullName] = $_.Length } + Write-Host "Conversion des clips Quaternius vers Bridget et Michael ($Mode)" + $proc = Start-Process -FilePath $editor -ArgumentList @($project, "-ExecutePythonScript=$script", '-nosplash', '-unattended') -WorkingDirectory $ProjectRoot -PassThru + $deadline = (Get-Date).AddMinutes(25) + $hit = $null + $failure = $null + while ((Get-Date) -lt $deadline) { + Start-Sleep -Seconds 5 + $logs = @(Get-ChildItem $logDir -Filter 'LifePlay*.log' -ErrorAction SilentlyContinue) + foreach ($file in $logs) { + $offset = 0 + if ($before.ContainsKey($file.FullName)) { $offset = $before[$file.FullName] } + if ($file.Length -le $offset) { continue } + try { + $stream = [IO.File]::Open($file.FullName, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite) + } + catch [System.IO.IOException] { continue } + try { + $stream.Seek($offset, [IO.SeekOrigin]::Begin) | Out-Null + $reader = New-Object IO.StreamReader($stream) + $text = $reader.ReadToEnd() + } + finally { $stream.Close() } + $ok = [regex]::Match($text, $success) + if ($ok.Success) { $hit = $ok.Value } + $bad = [regex]::Match($text, 'chaine impossible[^\r\n]*|RETARGET deplacement rate[^\r\n]*|etape inconnue[^\r\n]*|LogPython: Error:[^\r\n]*') + if ($bad.Success) { $failure = $bad.Value.Trim() } + } + if ($hit) { break } + $editorGone = -not (Get-Process UnrealEditor -ErrorAction SilentlyContinue) + if ($editorGone -and $proc.HasExited) { break } + } + if ($failure -and -not $hit) { throw "Conversion en echec : $failure" } + if (-not $hit) { throw "Conversion sans ligne RETARGET : voir Saved\Logs\LifePlay.log" } + $quiet = (Get-Date).AddMinutes(2) + while ((Get-Date) -lt $quiet -and (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 2 } + $still = @(Get-Process UnrealEditor -ErrorAction SilentlyContinue) + if ($still) { throw "Conversion terminee ($hit) mais UnrealEditor est encore ouvert : le fermer avant la cuisson" } + Write-Host " $hit" +} + +function Write-AnimTarget([string] $ProjectRoot, [string] $Which) { + $dir = Join-Path $ProjectRoot 'Saved\Config\Windows' + New-Item -ItemType Directory -Force $dir | Out-Null + $ini = Join-Path $dir 'LPAnimTarget.ini' + [IO.File]::WriteAllText($ini, "[LifePlay.Anim]`r`nTarget=$Which`r`n", (New-Object Text.UTF8Encoding $false)) + Write-Host "Cible des animations : $Which ($ini)" +} + +function Open-AnimTarget([string] $ProjectRoot, [string] $Which) { + $running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue) + if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant d'ouvrir la cible" } + $editor = Join-Path $UE 'Binaries\Win64\UnrealEditor.exe' + $project = Join-Path $ProjectRoot 'LifePlay.uproject' + if ($Which -eq 'sandbox') { + # Le .bat quote l'argument -ini. Start-Process le coupe sur les deux-points. + & (Join-Path $ProjectRoot 'Tools\debug\LifePlay_interaction_sandbox.bat') + } + else { + Start-Process -FilePath $editor -ArgumentList @($project, '-game', '-windowed', '-ResX=1600', '-ResY=900') -WorkingDirectory $ProjectRoot | Out-Null + } + Write-Host "Ouvert : $Which" +} + +# 1. Controles. Cuisson seulement : -Target -Open peut partir sans mod. +if ($Open -and -not $Target) { throw "-Open demande -Target sandbox ou -Target game" } +if ($Target -and $Target -notin 'sandbox', 'game') { throw "-Target vaut sandbox ou game (recu : $Target)" } +$bCook = -not $RetargetOnly -and -not $Open +if ($bCook -and -not $Name) { throw "Preciser -Name du mod" } +if (-not $bCook -and -not $Target -and -not $Retarget) { throw "Preciser -Name du mod, ou -Target sandbox|game" } +if ($bCook) { if (-not $Version) { $line = Select-String -Path (Join-Path $Root 'Config\DefaultGame.ini') -Pattern '^ProjectVersion=(.+)$' | Select-Object -First 1 if (-not $line) { throw 'ProjectVersion absent de Config\DefaultGame.ini : preciser -Version' } @@ -65,6 +159,21 @@ $release = Join-Path $Root "Releases\$Version\Windows" if (-not (Test-Path (Join-Path $release 'AssetRegistry.bin'))) { throw "Version de reference absente : $release (build de publication avec -createreleaseversion=$Version)" } $stray = Get-StrayMods $Root if ($stray) { throw "Autres plugins dans Mods\ ($($stray -join ', ')) : les remettre dans ModSources avant de cuire" } +} + +# 1b. Conversion, puis redirecteur sandbox ou game. +if ($RetargetOnly -and -not $Retarget) { $Retarget = 'bake' } +if ($Retarget -and $Retarget -notin 'bake', 'rigs') { throw "-Retarget vaut bake ou rigs (recu : $Retarget)" } +if ($Target) { Write-AnimTarget $Root $Target } +if ($Retarget) { Invoke-AnimToGenesis $Root $Retarget } +if ($Open) { + Open-AnimTarget $Root $Target + exit 0 +} +if (-not $bCook) { + Write-Host "Cuisson non lancee" + exit 0 +} # 2. Cuisson DLC. $logDir = Join-Path $Root 'Saved\Logs\portage\mods' diff --git a/Tools/LogStats.ps1 b/Tools/LogStats.ps1 index f3db92a..cdbd333 100644 --- a/Tools/LogStats.ps1 +++ b/Tools/LogStats.ps1 @@ -49,6 +49,7 @@ function Get-LogStats([string]$Path) { Erreurs = @($lines -match ': Error: ').Count Avertissements = @($lines -match ': Warning: ').Count LogLifePlay = @($lines -match 'LogLifePlay').Count + LogLPAnimMeasure = @($lines -match 'LogLPAnimMeasure').Count TraceModding = @($lines -match 'LogLPModdingTrace|LogModdingExtra').Count _Proprietes = $noneProps | Group-Object | Sort-Object Count -Descending | Select-Object -First $Top Count, Name _Fichiers = $files diff --git a/Tools/debug/LifePlay_interaction_sandbox.bat b/Tools/debug/LifePlay_interaction_sandbox.bat new file mode 100644 index 0000000..fd1db6a --- /dev/null +++ b/Tools/debug/LifePlay_interaction_sandbox.bat @@ -0,0 +1,7 @@ +@echo off +rem Meme chambre que la sandbox pair. Le menu en haut a gauche choisit Interaction ou Pair. +rem L'instance de jeu du Blueprint n'est pas chargee. Aucun Blueprint n'est modifie. +setlocal +set "UE=D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe" +set "PROJ=%~dp0..\..\LifePlay.uproject" +start "" "%UE%" "%PROJ%" "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 "-ini:Engine:[/Script/EngineSettings.GameMapsSettings]:GameInstanceClass=/Script/Engine.GameInstance" diff --git a/Tools/debug/LifePlay_measure_sandbox.bat b/Tools/debug/LifePlay_measure_sandbox.bat new file mode 100644 index 0000000..a5d8f38 --- /dev/null +++ b/Tools/debug/LifePlay_measure_sandbox.bat @@ -0,0 +1,9 @@ +@echo off +rem Chambre vide measurement : mannequin, Bridget et Michael, pose figee. +rem L'instance de jeu du Blueprint n'est pas chargee. Aucun Blueprint n'est modifie. +setlocal +set "ROOT=%~dp0..\.." +echo Mesure>"%ROOT%\Saved\sandbox_mode.txt" +set "UE=D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe" +set "PROJ=%~dp0..\..\LifePlay.uproject" +start "" "%UE%" "%PROJ%" "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 "-ini:Engine:[/Script/EngineSettings.GameMapsSettings]:GameInstanceClass=/Script/Engine.GameInstance" diff --git a/Tools/debug/LifePlay_pair_sandbox.bat b/Tools/debug/LifePlay_pair_sandbox.bat new file mode 100644 index 0000000..bb9cb1f --- /dev/null +++ b/Tools/debug/LifePlay_pair_sandbox.bat @@ -0,0 +1,7 @@ +@echo off +rem Fenetre a part de la partie : chambre du bureau. Menu : Pair ou Interaction. +rem L'instance de jeu du Blueprint n'est pas chargee. Aucun Blueprint n'est modifie. +setlocal +set "UE=D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe" +set "PROJ=%~dp0..\..\LifePlay.uproject" +start "" "%UE%" "%PROJ%" "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 "-ini:Engine:[/Script/EngineSettings.GameMapsSettings]:GameInstanceClass=/Script/Engine.GameInstance"