LifePlay-Revival/Docs/Migration/etat/REL/GIREV/GIREV-D8-RECH.md

185 lines
31 KiB
Markdown
Raw Normal View History

Fiche GIREV-D8-RECH — Niveau N2 — Modèle prévu non précisé par la fiche | utilisé claude-sonnet-5-5 — Agent lp-cart — Skills sk-protocol, ue-cpp-build, lp-commandlets, lp-paths
# GIREV-D8-RECH : conflits entre morphs opposés, comportement actuel de `GI.ResolveConflictingMorphs` (D8) et bonnes pratiques (2026-10-21)
Hors gabarit : la rubrique 3 est subdivisée (3.1 à 3.6) pour porter le comportement actuel, les sources web, la comparaison et les options demandées par la fiche. Lecture seule : ni MCP, ni éditeur, ni commandlet, aucun téléchargement (les pages web sont lues par l'outil de lecture, rien n'est enregistré sur le disque). Le code source du moteur 5.8 installé est lu en place. Seul ce fichier est écrit. Descriptions neutres (doc 27) : aucun nom de morph, de préréglage ni de section réservée n'est cité ; « morph » désigne une cible de forme. Les questions sont numérotées `DQ1` à `DQ6` pour ne pas les confondre avec les `GQ1` à `GQ9` de `GIREV-PRE.md`. La recherche répond à la consigne de l'utilisateur rapportée par la fiche : « recherche ce qui se fait en best practice sur D8 ». Aucune décision n'est prise ici.
## Résumé (à lire en premier)
1. **Comportement actuel (établi, § 3.1)** : à la fin de `LoadPreset`, pour chaque ligne de la table `MorphTable` dont le côté `Min` est renseigné, si les deux morphs opposés (`Min` et `Max`) sont non nuls sur le composant corps, le **plus faible est remis à 0** ; à égalité stricte, c'est le morph **`Max`** qui est annulé. Aucun journal, aucun paramètre, un seul appelant connu.
2. **Pourquoi seulement le corps : non documenté** (établi comme absence). Aucune source interne ne donne la raison ; R04 range D8 en « information » et CG2-CART-01 en « conception » (`CG2-CART-01.md:51`). Conséquence structurelle (établi) : `Person.setMorphTarget` écrit un nom de morph sur le corps **et** sur les cheveux, la résolution ne corrige que le corps ; un conflit résolu côté corps reste donc entier côté cheveux (effet visuel : estimé).
3. **Pratique observée dans les sources lues (§ 3.2)** : on évite le conflit **par construction** (un seul paramètre signé pilote les deux côtés : MakeHuman, Character Creator), ou on le **borne** (limites Daz, option de bornage Unity, saturation VRM), ou on fixe une **priorité explicite et documentée** (recouvrements VRM `block` / `blend`). Unreal ne normalise pas et le dit dans son code. **Aucune source lue ne fixe de règle d'égalité universelle** : le choix de D8 est une décision de conception, pas l'application d'une norme.
4. **Mesure (§ 3.5)** : dans les 145 fichiers `.lpcharacter` du dossier `Content`, **un seul** contient les deux côtés d'une même paire sous l'hypothèse « mêmes radicaux `_min` / `_max` » (estimé : les paires réelles de `MorphTable` ne sont pas lues). Le conflit vient donc surtout de l'**empilement** de préréglages (`Blend`) ou de tirages, pas des fichiers pris isolément (estimé).
5. **Options (§ 3.6)** : A garder (0 écriture) ; **B règle déterministe documentée** (égalité écrite, une trace au journal, cheveux en variante) ; C groupes exclusifs ou normalisation (net signé, ou exclusivité à l'écriture). **Recommandation non contraignante : B1** (règle actuelle conservée et écrite, une ligne de journal par conflit, aucun changement d'aspect) ; la décision revient à l'utilisateur (DQ1 à DQ6).
## 1. Objet et périmètre
Un domaine : le défaut **D8** de R04 (`04-defauts.md:20`), repris au n° 8 du tableau de `CG2-CART-01.md` (**fichier ligne 51** ; « ligne 8 » de la fiche = n° 8 du tableau, pas la ligne 8 du fichier). Décrire précisément `GI.ResolveConflictingMorphs`, rechercher les bonnes pratiques de résolution de conflits entre morph targets / blend shapes / curseurs, comparer, proposer 2 ou 3 options avec coût, risque et recommandation. Contexte de calendrier : `GI.uasset` n'est révisé qu'une fois, en build 2 (`GIREV-PRE.md:6`) ; D8 est un candidat non retenu par `BOSS-ETAT.md:143`, posé en question `GQ5` (`GIREV-PRE.md:149`, `:246`). Hors périmètre : toute écriture, tout essai, la décision, la mesure des paires réelles de `MorphTable`.
## 2. Commandes et lectures (exactes) avec compte brut
| # | Commande ou lecture | Résultat brut |
|---|---|---|
| 1 | Read `lp-doc/gabarits.md` (CART) ; Read `REL/GIREV/GIREV-PRE.md:1-80` (gabarit d'en-tête) ; Read `CG/CG2/CG2-CART-01.md` (entier) ; Glob `**/*GIREV*` sous `etat` | gabarit lu ; D8 = `CG2-CART-01.md:51` ; Glob : `REL\GIREV\GIREV-PRE.md` seul (le livrable n'existait pas) |
| 2 | Grep `ResolveConflictingMorphs` dans `D:\Game\LifePlay-Sandbox-5.8\Docs\CG` | 12 lignes dans 4 fichiers : `04-defauts.md:20`, `:60`, `:75` ; `03-parcours.md:116`, `:134` ; `06-rapport-audit.md:33`, `:37` ; `archive/CG-REFERENTIEL.md:280`, `:297`, `:305`, `:318`, `:336` (copie ancienne) |
| 3 | `rg --no-ignore -l "ResolveConflictingMorphs" "D:/Game/LifePlay-master 5.8/Docs" "D:/Game/LifePlay-Sandbox-5.8/Docs"` | 16 fichiers : 7 dumps sous `etat` (`portage55/L6f/L6f_dump_GI.txt`, `portage55/L6f/L6f_LPDumpFunction_20260930-102202.txt`, `ST/STA/dumps/GI__graphes.txt`, `ST/STA/STA-T2a_LPDumpFunction_20261009-192922.txt`, `ST/STE/STE-D_LPDumpFunction_20261010-110623.txt`, `ST/STE/dumps/GI__LoadPreset.txt`, `sandbox/DOC-MOD/DOC-MOD_LPDumpFunction_20261007-083441.txt`) ; `Docs/01_Architecture_Initiale.md` ; `CG2-CART-01.md` ; 4 fichiers du référentiel de l'addon (`04-defauts`, `03-parcours`, `06-rapport-audit`, `archive/CG-REFERENTIEL`) ; 3 fichiers `CGREF1-*` de l'addon (`T3-MCP`, `T3b-FICHE`, `T3b-MCP`). Aucun dump d'`EventGraph` (`CAM-B1-PRE`, `STE-ORG4cV`) dans la liste |
| 4 | Read `CGREF1-T3b-MCP.md:1-70` (addon `Docs/etat/CG/CGREF1/`) | lecture MCP 5.8 du 2026-10-16 : A11 `:24`, règle `:44-49`, D8 `:92` |
| 5 | Read `L6f_dump_GI.txt:18210-18300` et `:3505-3530` ; Grep `ResolveConflictingMorphs\|MorphTable` dans ce fichier | fonction à `:18214` (24 nœuds) ; appel `:3518` ; `MorphTable` aussi à `:50274-50316` (construction des curseurs) |
| 6 | Grep `ResolveConflictingMorphs` + `-C 8` dans `ST/STE/dumps/GI__LoadPreset.txt` ; Grep `K2Node_IfThenElse_5` (`-A 8`) ; `rg --no-ignore -m3 "^Commandlet\|^Date\|^Moteur\|Engine"` sur `STE-D_…` | appel `:410-412` ; condition `LoadPresetLite` `:496` ; en-tête STE-D : `LPDumpFunction -BP=GI -Fn=LoadPreset -Pins -Exec`, `2026-10-10T11:07:09 … code de sortie : 0` |
| 7 | Read `04-defauts.md:1-83`, `03-parcours.md:100-197`, `02-format.md:1-40`, `06-rapport-audit.md:29-35`, `:123-178` | cités en § 3 |
| 8 | Read `Engine/Source/Runtime/Engine/Private/Components/SkeletalMeshComponent.cpp:4005-4079` et `SkeletalRender.cpp:20-59` (moteur `D:\Game\Launcher\Engine\UE_5.8`) ; Grep `MaxMorphTargetBlendWeight\|MinMorphTargetBlendWeight\|MorphTarget\.MaxBlendWeight\|MorphTarget\.MinBlendWeight\|define ZERO_ANIMWEIGHT_THRESH` sous `Engine/Source/Runtime` (`*.h`, `*.cpp`) | `SetMorphTarget` `:4011-4035`, `GetMorphTarget` `:4042-4054` ; `MaxMorphTargetBlendWeight = 5.0f` `SkeletalRender.cpp:26` ; `#define ZERO_ANIMWEIGHT_THRESH (0.00001f)` `VectorRegister.h:133` |
| 9 | `rg --no-ignore --files -g "*.lpcharacter" .` (depuis `Content`) puis `\| wc -l` ; idem avec les racines `Modules` et `Saves` | `145` ; `141` ; `4` |
| 10 | `rg --no-ignore --pcre2 -U -l -g "*.lpcharacter" "(?ms)^MORPH ([^:\r\n]+)_min:.*?^MORPH \1_max:" . \| wc -l` ; formulation inverse (`_max` puis `_min`) | `0` ; `1` |
| 11 | Contrôle du motif : `rg --no-ignore -l -g "*.lpcharacter" "^MORPH [^:]*_min:" . \| wc -l` ; idem `_max` ; `rg --no-ignore -c …` sommé par `awk` ; fichiers ayant au moins une ligne `^MORPH ` | `58` ; `59` ; lignes `472` (`_min`) et `523` (`_max`) ; `126` |
| 12 | Recherche web et lecture de pages (13 lignes au § 3.2) | statut de lecture par ligne du tableau § 3.2 |
| 13 | `git log -1 -- Content/BPs/GI.uasset` ; `md5sum` ; `date` | `c4cf6f3 2026-10-14 …` ; `32fc0a970ab194626e8dc07df3a9633d` (égal à `GIREV-PRE.md:57`) ; `2026-10-21 07:41` |
Écart de méthode : une première forme de la commande 9 (`rg --files` suivi de `cut`, `sort`, `uniq -c`, sans `wc -l`) a **affiché les noms** des fichiers dans la sortie de l'outil, dont certains à caractère réservé ; ils ne sont **pas** repris ici. Compter, ne pas lister (à verser).
## 3. Constats `fichier:ligne`
### 3.1 Comportement actuel de `GI.ResolveConflictingMorphs` (établi sauf mention)
Notation : `L6f` = `Docs/Migration/etat/portage55/L6f/L6f_dump_GI.txt` (dump 5.5 du 2026-09-30, 24 nœuds) ; `T3b` = `CGREF1-T3b-MCP.md` (lecture MCP **5.8** du 2026-10-16, 24 nœuds : même nombre, `T3b:24`).
- **Signature** : `FunctionEntry_192` sans broche d'entrée ni de sortie (`L6f:18215`). La fonction n'a donc **aucun paramètre**.
- **Entrées implicites** :
- la variable membre `MorphTable`, tableau de la structure `MorphSlider` (champs `DisplayName`, `Max`, `Min`, `Female`, `Male` : `L6f:18219-18232`, `T3b:44`). La même table construit les curseurs de l'écran du générateur (`L6f:50274-50316`) : **une ligne = un curseur à deux côtés**, `Min` et `Max` étant les noms de deux morphs opposés ;
- le personnage édité `CharacterBeingEdited` (`L6f:18216`, `:18285`) et, via lui, le composant `Mesh` (`L6f:18240-18242`, `:18288-18290`) ;
- les valeurs courantes des deux morphs, lues par `SkeletalMeshComponent::GetMorphTarget` (`L6f:18243-18256`). Dans le moteur 5.8, cette lecture rend la valeur de la liste `MorphTargetCurves` du composant, ou `0.0` si le morph n'y figure pas (`SkeletalMeshComponent.cpp:4042-4054`).
- **Règle**, ligne par ligne de `MorphTable`, indépendamment des autres lignes (`T3b:44-49`, câblage relu dans `L6f:18233-18300`) :
1. si `Min` vaut le nom « None », la ligne est ignorée (`L6f:18233-18238`) ;
2. lecture de `Max` et de `Min` sur le corps (`L6f:18243-18256`) ;
3. si `Max != 0` **et** `Min != 0` (comparaison exacte à `0.0`, `L6f:18257-18273`), sinon rien ;
4. si `Max > Min` (comparaison **stricte** : `L6f:18278-18284`), `SetMorphTarget(nom Min, 0.0, bRemoveZeroWeight = true)` ; sinon `SetMorphTarget(nom Max, 0.0, true)` (`L6f:18291-18300` ; branche vraie → `Min`, branche fausse → `Max` : `T3b:48`).
- **Cas d'égalité** : `Max > Min` est faux, donc le morph **`Max` est annulé** et le `Min` est gardé (`T3b:49`, `04-defauts.md:20`). Le départage est donc **asymétrique** (toujours le même côté), mais déterministe.
- **Effet moteur de l'annulation** : avec `bRemoveZeroWeight = true`, le moteur retire l'entrée du composant quand `|valeur| ≤ 0,00001` (`SkeletalMeshComponent.cpp:4014`, `:4028-4034` ; `VectorRegister.h:133`). Le perdant est donc **supprimé de la liste active**, pas seulement mis à zéro.
- **Propriétés déduites de la structure** (établi pour la structure, estimé pour l'usage) :
- **idempotente** : après un passage, l'un des deux côtés vaut 0 et la condition 3 est fausse ;
- **silencieuse** : aucun nœud de journal ni de message parmi les 24 (`L6f:18214-18300`) ; l'audit R06 range « le silence » parmi ses faiblesses (`06-rapport-audit.md:69`, titre § 3.1) ;
- **destructive** : la valeur perdante n'est pas conservée ;
- **« le plus fort gagne »**, pas « le dernier écrit gagne » : la fonction ne voit que des valeurs finales, pas l'ordre des écritures (estimé : déduit du fait qu'elle ne lit que `GetMorphTarget`).
- **Pourquoi seulement le corps** : aucune source interne ne le dit. Faits établis : la fonction lit et écrit **uniquement** sur `Mesh` (`L6f:18240-18242`, `:18288-18290`) ; le défaut est consigné ainsi : « ne traite que `GetMesh` (ignore `HairSM`) » (`04-defauts.md:20`) ; `Person.setMorphTarget` applique, lui, le **même nom** au composant cheveux puis au corps, chacun deux fois (`T3b:35-41`, D7 `04-defauts.md:19`). Hypothèses (estimé, non départagées) : (a) une fonction écrite autour d'un composant de référence unique ; (b) le maillage de cheveux n'a pas tous ces morphs ; (c) oubli. Essai qui trancherait (b) : lire la liste des morph targets de l'asset de cheveux (lecture MCP ou dump), voir § 5.
- **Appelant** : un seul site connu, `K2Node_CallFunction_6074` dans `LoadPreset`, branche « faux » de `IfThenElse_5` dont la condition est `LoadPresetLite` (`GI__LoadPreset.txt:410-412`, `:496` ; `L6f:3518`), suivi de l'affichage approprié du personnage (`GI__LoadPreset.txt:414`). La fonction s'exécute donc **une fois, après le rejeu de toutes les lignes** du préréglage, et pas à chaque écriture de morph. Elle n'est pas appelée par le curseur de l'écran (le curseur écrit un seul côté : `Min` sous 0,5, `Max` au-dessus, `03-parcours.md:164`). Les corps des `Randomize*` et de `GeneratePerson` ne sont pas lus (`04-defauts.md:55`) : appelants inconnus possibles (estimé : aucun, car aucun dump d'`EventGraph` ne contient l'appel, commande 3). `01_Architecture_Initiale.md:151` place l'appel après `GenerateAndApplyBools` : résumé de l'ancienne architecture, **contredit par les dumps** (l'appel est dans `LoadPreset`).
- **Lien avec la superposition** : `Blend = true` rejoue les lignes du fichier par-dessus l'état courant, sans moyenne (`03-parcours.md:114-116`, `06-rapport-audit.md:31`) ; `BlendPreset` ne fusionne rien, c'est un empilement de fichiers partiels (`06-rapport-audit.md:33`). Le texte de R06 dit que **le seul arbitrage automatique** de ce dispositif est `ResolveConflictingMorphs` (`06-rapport-audit.md:33`). Conséquence (estimé) : si un préréglage posé après un autre fixe le côté opposé à une valeur plus faible que celle déjà en place, **c'est la contribution la plus récente qui est annulée**.
- **Ce qui n'est pas lu** : le contenu de `MorphTable` (nombre de lignes, lesquelles ont `Min` renseigné, drapeaux `Female` / `Male` : la fonction les ignore, `L6f:18231-18232`). La valeur par défaut d'une variable de Blueprint n'est lisible ni dans ces dumps ni dans `OUT1_uasset_rejeu_log_sans_emulation.txt:305` (qui ne donne que le type `Array<struct:MorphSlider>`).
### 3.2 Sources web : ce qui se fait ailleurs
Statut : **lu** = page récupérée et citée ; **résumé** = seulement le résumé du moteur de recherche, page non relue (indicatif) ; **non lu** = page tronquée ou inaccessible.
| # | Source (URL) | Ce qu'elle établit | Statut | Lien avec D8 |
|---|---|---|---|---|
| W1 | https://raw.githubusercontent.com/makehumancommunity/makehuman/master/makehuman/apps/humanmodifier.py | Modificateur à deux cibles : valeur bornée à `[-1, 1]` (ou `[0, 1]` sans cible gauche) ; poids gauche `-min(value, 0.0)`, poids droit `max(0.0, value)` ; cible centrale `1.0 - abs(value)` | lu | **Exclusion par construction** : un seul paramètre signé, l'un des deux côtés vaut toujours 0 ; aucun conflit à résoudre |
| W2 | https://manual.reallusion.com/Character-Creator-4/Content/ENU/4.0/06_Customizing_Morphing_Sliders/User_Interface.htm | « a morphing value can be within a range from -100 to 100 » ; Min et Max règlent les bornes du curseur | lu | Même principe : curseur signé unique ; la page ne traite pas des morphs opposés combinés |
| W3 | https://docs.daz3d.com/public/software/dazstudio/4/userguide/creating_content/assembling/tutorials/creating_joint_controlled_morphs/start | « Change the Min Limit to 0. The JCM only works from 0 to 100 percent » ; « leaving limits on in negative values will cause problems when the shoulder joint is bent in the opposite direction » | lu | **Bornage** comme garde contre l'activation du côté opposé |
| W4 | https://docs.daz3d.com/public/software/dazstudio/4/referenceguide/terms/property/start | Certaines propriétés ont les attributs « Clamped, Mappable, Must Map » ; la page n'explique pas le bornage ni l'ERC | lu | Confirme l'existence d'un attribut de bornage ; rien sur les conflits |
| W5 | https://www.daz3d.com/forums/discussion/comment/381884 ; https://bitbucket.org/Diffeomorphic/import_daz/issues/480/plussing-morphs | Un « contrôleur » créé par ERC Freeze pilote plusieurs propriétés à la fois ; retirer les limites d'un morph peut casser la géométrie (résumé de recherche) | résumé | Pattern « un contrôle, plusieurs morphs » ; prudence sur le retrait des bornes. Pas de règle d'arbitrage trouvée |
| W6 | https://dev.epicgames.com/documentation/unreal-engine/BlueprintAPI/Components/SkeletalMesh/SetMorphTarget | Nœud « Set Morph Target with Name and Value(0-1) » ; « Remove Zero Weight : Used by editor code when it should stay in the active list with zero weight » | lu | Le nœud que D8 appelle ; plage documentée 0-1 |
| W7 | Moteur 5.8 installé : `Engine/Source/Runtime/Engine/Private/Components/SkeletalMeshComponent.cpp:4019-4021` ; `.../Private/SkeletalRender.cpp:26-35` | Commentaire du moteur : « sum up, in the future we might normalize, but for now this just sums up / this won't work well if all of them have full weight - i.e. additive » (le code affecte `=`, `:4021`) ; poids absolu accepté jusqu'à `r.MorphTarget.MaxBlendWeight`, 5,0 par défaut | lu | **Unreal ne normalise ni n'arbitre** : la politique est laissée au contenu. Les valeurs hors `[0, 1]` sont tolérées jusqu'à 5,0 |
| W8 | https://dev.epicgames.com/documentation/metahuman/metahuman-creator-blend-tool-in-unreal-engine | Mélange de un à trois préréglages par points de contrôle ; « As a control point moves closer to a preset, that preset's features blend with your character » ; la page ne donne ni formule de poids, ni normalisation, ni règle de conflit | lu | **Fusion pondérée par proximité**, sans conflit exposé à l'utilisateur ; la page est muette sur la normalisation |
| W9 | https://docs.unity3d.com/ScriptReference/PlayerSettings-legacyClampBlendShapeWeights.html ; https://discussions.unity.com/t/removing-clamping-of-blendshapes-to-the-range-0-100/683047 | Option : « If set to true, the range of BlendShape weights in SkinnedMeshRenderers will be clamped, otherwise BlendShape weights can take any value » ; défaut non précisé par la page. Le fil indique que Unity a retiré le bornage 0-100 et que le code doit borner lui-même (résumé) | lu (page) / résumé (fil) | **Bornage configurable** ; la responsabilité de borner revient au contenu |
| W10 | https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/expressions.md | Recouvrements `overrideBlink` / `overrideLookAt` / `overrideMouth` : `none` (rien), `block` (« Set the target weight to 0 » tant que l'expression dominante a un poids > 0), `blend` (« Attenuates the target weight ») ; la somme des atténuations est saturée (`factor = 1.0 - saturate(value)`) ; valeurs d'expression à borner à `[0, 1]` ; « settings for the same kind are treated as invalid » | lu | **Priorité explicite et documentée**, avec deux modes (coupure ou atténuation) ; aucune règle d'égalité |
| W11 | https://simscommunity.info/2018/07/10/learning-your-tools-the-sims-4-morphmaker/ | Les directions « haut » et « bas » d'un curseur sont deux modificateurs distincts rattachés au même curseur ; l'article ne traite ni des poids, ni des conflits | lu | Non concluant : structure à deux côtés, aucune règle de conflit publiée trouvée |
| W12 | https://www.tech-artists.org/t/blendshape-condition-node/10797 | Un répondant déconseille de « couper des formes quand d'autres s'allument » au-delà des combinaisons de deux ; la voie courante est la forme corrective pour les combinaisons problématiques (résumé) | résumé | Mise en garde contre la logique d'extinction en chaîne |
| W13 | https://docs.blender.org/manual/en/latest/animation/shape_keys/shape_keys_panel.html | Page tronquée à la lecture (215 969 caractères, 100 000 lus, partie utile absente) | non lu | Aucune conclusion tirée (additif par défaut : non vérifié ici) |
Recherches sans résultat utile : « Sims 4 modificateur bidirectionnel » (aucune documentation officielle sur un arbitrage), « Character Creator morphs opposés combinés » (aucune page officielle), « MetaHuman plausibilité » (la page ne donne ni borne ni règle). L'absence de résultat n'est pas une preuve d'absence de règle.
### 3.3 Les patrons de pratique qui se dégagent
| Patron | Sources | Idée |
|---|---|---|
| P1 Exclusion par construction | W1, W2 | un seul paramètre signé pilote les deux côtés ; l'état « les deux côtés actifs » n'existe pas |
| P2 Bornage | W3, W9, W10, W7 | on limite l'amplitude (Daz, Unity, VRM, plafond 5,0 d'Unreal) ; borner n'arbitre pas entre deux côtés |
| P3 Priorité explicite et documentée | W10 | une expression dominante coupe (`block`) ou atténue (`blend`) une autre, règle écrite dans la spécification |
| P4 Fusion pondérée | W8, W10 | des poids continus plutôt qu'une coupure ; la normalisation reste souvent non documentée (W8) |
| P5 Pas d'arbitrage dans le moteur | W7 | le moteur additionne et laisse la politique au contenu ou à l'outil |
| P6 Forme corrective plutôt que chaîne d'extinctions | W12 (indicatif) | traiter la combinaison problématique par une forme dédiée |
### 3.4 Comparaison au comportement actuel
| Critère | `ResolveConflictingMorphs` actuel | Pratique observée |
|---|---|---|
| Où le conflit est-il évité ? | après coup, à la fin du chargement (`GI__LoadPreset.txt:410-412`) ; le curseur de l'écran, lui, est déjà signé (`03-parcours.md:164`, P1) | P1 évite l'état, la fonction le répare |
| Règle de priorité | le plus fort gagne, perdant mis à 0 (`T3b:48-49`) | proche de P3 `block`, **sans atténuation** (P3 `blend`, P4 non utilisés) |
| Égalité | `Max` annulé, asymétrique, non écrit hors R04 | aucune source lue ne fixe de règle d'égalité (W10 : même cas « invalide ») |
| Périmètre | corps seul (`L6f:18240-18242`) | non documenté ; incohérence corps / cheveux possible (estimé) |
| Visibilité | aucun journal | R06 demande des branches d'échec journalisées (`06-rapport-audit.md:166`) |
| Dépendance à l'ordre | aucune (valeurs finales) | P3 documenté par priorité fixe ; ici l'ordre d'empilement est ignoré (estimé) |
| Points solides | déterministe, idempotente, simple, indépendante par ligne, coût d'exécution nul | alignée avec le choix de R06 : superposition plutôt que moteur de fusion (`06-rapport-audit.md:144`) |
### 3.5 Mesure sur le corpus (hypothèse de paire par radical `_min` / `_max`)
- 145 fichiers `.lpcharacter` sous `Content` : 141 sous `Modules`, 4 sous `Saves` (commande 9).
- Fichiers contenant au moins une clé `MORPH` se terminant par `_min` : 58 ; par `_max` : 59 ; lignes : 472 et 523 (commande 11).
- Fichiers contenant **les deux côtés du même radical** : 0 (ordre `_min` puis `_max`) et 1 (ordre inverse), soit **1 sur 145** (commande 10, deux formulations).
- Limites (incertain) : les noms de `MorphTable` ne sont pas lus ; seule une partie des morphs suit la convention de suffixe (`CGREF1-T2-KEYS.csv`, addon `Docs/etat/CG/CGREF1/` : 1 151 lignes dont 101 contiennent `_min` et 108 `_max` d'après `rg -c`, colonnes d'exemples comprises, donc compte indicatif) ; le fichier trouvé peut avoir un des deux côtés à 0 ; les sauvegardes des joueurs hors dépôt ne sont pas mesurées.
- Lecture (estimé) : le conflit survient peu dans les fichiers isolés ; il vient de l'empilement (`Blend`, `generatePersonTemporary` à plusieurs noms : `02-format.md:23`) et des tirages d'intervalles.
### 3.6 Options
Échelle de coût de `CG2-CART-01.md:35-40` (S ≤ 30 min de MCP, M 0,5 à 2 h, L > 2 h). Le nombre d'écritures est une **estimation** par analogie avec `GIREV-PRE.md` (M1 : 8 à 12 écritures ; charge totale de la révision : 50 à 90). Toute écriture dans `GI` entre dans la révision unique de la build 2.
| | **A. Garder tel quel** | **B. Règle déterministe documentée** | **C. Groupes exclusifs / normalisation** |
|---|---|---|---|
| Principe | aucune modification de Blueprint ; la règle actuelle est seulement décrite (guide de moddage, fiche) | la règle « le plus fort gagne » est conservée et **écrite**, avec l'égalité nommée ; trace de journal ; variantes B2 (cheveux) et B3 (égalité neutre) | C1 : valeur nette signée `Max - Min` appliquée à un seul côté (P1 appliqué après coup) ; C2 : exclusivité imposée à **chaque écriture** de morph de `MorphTable` (le dernier écrit gagne) |
| Changement Blueprint | aucun | B1 : 1 nœud de journal + 2 concaténations de noms sur les deux branches de `IfThenElse_643` ; B2 : remplacer les 2 `SetMorphTarget` du corps par `Person.setMorphTarget` ; B3 : un test `Max == Min` annulant les deux | C1 : calcul de la différence, choix du côté, 2 `SetMorphTarget` ; C2 : toucher `Person.setMorphTarget` (chemin chaud, D7 `CG2-CART-01.md:50`), la voie directe de `GenerateAndApplyFloats` (`T3b:42`), et lire des graphes non lus (`Randomize*`, `GeneratePerson`) |
| Coût (échelle CG2) | 0 | B1 **S**, 8 à 15 écritures ; B2 **S-M**, + 6 à 10 ; B3 **S**, + 4 à 6 | C1 **M**, 15 à 25 ; C2 **L**, 40 à 80 + lectures préalables |
| Effet sur les préréglages existants | aucun | B1 aucun (comportement identique) ; B2 : cheveux modifiés quand un conflit existe ; B3 : seules les égalités changent | C1 : **toute** paire en conflit change de valeur (le gagnant n'est plus gardé à sa valeur pleine) ; C2 : tout empilement `Blend` change (le plus récent gagne au lieu du plus fort) |
| Effet sur les sauvegardes | aucun | B1 aucun ; B2 / B3 : un personnage rechargé avec un conflit peut changer d'aspect à l'ouverture (fréquence inconnue) | idem, sur un plus grand nombre de cas ; les fichiers eux-mêmes ne sont pas réécrits (la résolution est faite au chargement), l'aspect rechargé peut différer |
| Risque | nul sur l'aspect ; la dette de silence reste (`06-rapport-audit.md:69`) | B1 très faible (un message journalisé) ; B2 / B3 faible à moyen selon la fréquence des conflits | moyen à élevé ; C2 s'éloigne du choix « superposition sans moteur de fusion » (`06-rapport-audit.md:144`) |
| Bon point | rien à tester ; aucune charge sur la révision de GI | règle lisible pour un moddeur ; la trace mesure enfin la fréquence réelle des conflits ; aucun changement d'aspect en B1 | supprime l'asymétrie et l'état « deux côtés actifs » ; conforme à P1 |
| Mauvais point | égalité asymétrique non écrite, cheveux incohérents, silence | ajoute des écritures à la révision de GI ; B2 / B3 changent l'aspect | redéfinit la sémantique de tous les préréglages et mods ; coût disproportionné pour un défaut de gravité « information » |
**Recommandation (non contraignante) : B1**, avec B2 seulement si un décalage corps / cheveux est constaté en jeu. Raisons : (1) aucune source lue n'impose une règle d'égalité, l'essentiel de la pratique est de **fixer et documenter** une priorité (W10) ; (2) B1 ne change aucun aspect de personnage, donc aucun risque sur les préréglages ni sur les sauvegardes ; (3) la trace donne la mesure qui manque (fréquence des conflits, § 3.5) avant d'envisager C ; (4) coût S, compatible avec la contrainte de révision unique de GI. **A** reste raisonnable si la révision de GI doit rester minimale (D8 est « information », `04-defauts.md:20`). **C** n'est pas recommandé avant la mesure de B1 : il change la sémantique de tout le corpus pour un gain non démontré. Interaction à noter : la décision D9 (morph remis à 0 ou laissé, `CG2-CART-01.md:52`) change les valeurs effectives avant la résolution et donc la fréquence des conflits.
## 4. Établi / estimé / incertain
- **Établi** :
- la règle, l'égalité, le périmètre « corps seul » et l'absence de journal (`L6f:18214-18300`, `T3b:44-49`) ;
- l'appelant unique connu et sa condition (`GI__LoadPreset.txt:410-412`, `:496`) ;
- l'usage de `MorphTable` pour les curseurs (`L6f:50274-50316`) ;
- le comportement moteur de `SetMorphTarget` / `GetMorphTarget` et le plafond 5,0 (`SkeletalMeshComponent.cpp:4011-4054`, `SkeletalRender.cpp:26-35`) ;
- le contenu des pages W1, W2, W3, W4, W6, W8, W9 (page), W10, W11 ;
- les comptes des commandes 9 à 11.
- **Estimé** :
- l'incohérence corps / cheveux (déduite de `T3b:35-41` et `L6f:18240`) ;
- « le plus fort gagne, pas le dernier » (déduit de la lecture des seules valeurs finales) ;
- l'absence d'autre appelant (aucun dump d'`EventGraph` ne l'appelle ; `GeneratePerson`, `Randomize*` non lus) ;
- la rareté des conflits dans les fichiers isolés (hypothèse de paire par radical) ;
- les coûts et nombres d'écritures du § 3.6 ;
- l'identité des dumps 5.5 et 5.8 de la fonction (même nombre de nœuds, règle identique dans les deux lectures, branches non comparées nœud par nœud).
- **Incertain** :
- le contenu et la taille de `MorphTable` ;
- la raison historique du « corps seul » ;
- si les maillages de cheveux possèdent ces morphs ;
- si `SavePerson` écrit les valeurs après résolution ;
- W5, W12 (résumés seuls), W13 (non lu) ;
- la fréquence des conflits en jeu.
## 5. Points à confirmer (repris par la fiche suivante)
1. **Contenu de `MorphTable`** : nombre de lignes, nombre de lignes à `Min` renseigné, noms des paires (à garder hors livrable public). Lecture seule par MCP ou par un dump de valeurs par défaut de `GI` ; fonde la mesure de § 3.5.
2. **Cheveux** : les assets de cheveux portent-ils les morphs de `MorphTable` ? (liste des morph targets). Tranche l'hypothèse (b) du § 3.1 et l'intérêt de B2.
3. **Appelants** : confirmer que `LoadPreset` est l'unique appelant (`Room.LoadPreset` du mode constructeur, `GeneratePerson`, `Randomize*`, `BodyChange`), par `find_nodes` ou dump.
4. **Sauvegardes** : `SavePerson` / `SavePreset` écrivent-ils les valeurs lues sur le composant après résolution ? Détermine si un personnage déjà sauvé porte l'état résolu.
5. **Fréquence en jeu** : n'est mesurable qu'avec la trace de B1 (aucune trace aujourd'hui). Mesure alternative hors jeu : balayer `Content/Saves` et les dossiers de joueurs avec la liste de paires du point 1.
6. **Pages non relues** : W5, W12 (résumés) et W13 (Blender, tronquée) à relire si l'utilisateur veut s'en servir.
## 6. Questions pour la SPEC ou l'utilisateur
- **DQ1** : option A, B (B1 seul, ou avec B2 / B3) ou C ? Recommandation : B1.
- **DQ2** : à égalité, garder « `Max` annulé » (aucun changement d'aspect, à écrire) ou annuler les deux côtés (neutre, change les seuls cas d'égalité) ?
- **DQ3** : étendre la résolution aux cheveux (B2) maintenant, ou seulement après un décalage constaté en jeu ?
- **DQ4** : journaliser chaque conflit ? Niveau : journal seul (recommandé), jamais à l'écran (les textes de débogage à l'écran sont déjà un défaut : D18, D19 `04-defauts.md:35-36`).
- **DQ5** : écrire la règle (le plus fort gagne, égalité) dans le guide de moddage livré avec le jeu, pour les auteurs de préréglages ? C'est une publication : règle doc 27.
- **DQ6** : D8 entre-t-il dans la révision unique de GI (question `GQ5` de `GIREV-PRE.md:246`) ? B1 ajoute environ 8 à 15 écritures à une charge estimée de 50 à 90.
Tours : 31 / 40 (estimé ; `Tools/FicheStats.py` fait foi)
## Constats
- Fichier écrit : `D:\Game\LifePlay-master 5.8\Docs\Migration\etat\REL\GIREV\GIREV-D8-RECH.md`. Aucun autre fichier écrit ; aucune lecture n'a modifié un fichier du projet.
- Fichiers à commiter : `A Docs/Migration/etat/REL/GIREV/GIREV-D8-RECH.md` (dépôt du jeu, branche `ue5.8`, par le gardien sur ticket).
- À verser : `lp-doc` (règle doc 27) — `rg --files -g "*.lpcharacter"` sans `| wc -l` affiche des noms de préréglages à caractère réservé dans la sortie de l'outil : compter, ne pas lister. `lp-commandlets` — `rg --no-ignore --pcre2 -U -l "(?ms)^MORPH ([^:\r\n]+)_min:.*?^MORPH \1_max:"` apparie deux clés d'un même fichier `.lpcharacter` (rétroréférence PCRE2, ripgrep 14.1.1). `lp-cart` — la page Blender du manuel dépasse 200 000 caractères et la partie utile est hors des 100 000 premiers : lire par `offset`.