LifePlay-Revival/Docs/Migration/etat/ST/STE/STE-H3g-SPEC.md

268 lines
38 KiB
Markdown
Raw Normal View History

Fiche STE-H3g — Niveau N3 — Modèle prévu (modèle de l'agent lp-cart) | utilisé claude-sonnet-5-5 — Agent lp-cart — Skills sk-protocol, ue-cpp-build, lp-commandlets, lp-paths (préchargés), lp-mcp (lu : § 1, 3, 4), lp-doc (gabarits)
# STE-H3g-SPEC : gardes G15 (`GI.isSameAs`, variante b) et G16 (`GI:EventGraph`, `UpdateDefaultHair`, F3 sans changement de comportement) (2026-10-13)
Lecture seule stricte (Read, Grep, Glob ; un Bash : `git rev-parse`, `md5sum`, `git status`, `git log`). Aucun éditeur, MCP, commandlet, build ni jeu.
Copie `D:\Game\LifePlay-master 5.8`, branche `ue5.8`, HEAD `0745917`. Gabarit SPEC : `D:\Game\LifePlay-Sandbox\Tools\claude\skills\lp-doc\gabarits.md:36-38`.
Suite de `STE-H3-SPEC.md` (G1-G6), `STE-H3b-SPEC.md` (G7-G11), `STE-H3c-SPEC.md` (G12), `STE-H3d-SPEC.md` (G13) : mêmes conventions (H3S § 2.1),
gardes G15 et G16, risques et questions préfixés `H3g`. Correspondance avec la rubrique 7 de la fiche : § 2.2 = G15, § 2.3 = G16, § 3 = repli,
§ 4 = risques et questions, § 5 = interdits, § 7 = critères de VAL.
**Statut : complet et exécutable** (aucun dump manquant). Deux points à lire avant toute IMPL :
1. **G15 : la variante b ne change pas « un seul cas ».** La fiche (rubrique 3) et la décision Q-H3e-1 supposent que seul « les deux personnes vides » change.
Le corps 5.8 de `GetVariable` (GVP) montre **trois** cas changés, dont (vide, `[None]`) qui est le chemin « pas de compagnon » : § 2.2.4, **Q-H3g-1** (décision de l'utilisateur avant IMPL de G15).
2. **G15 : le DIAG H3e § 2 est réfuté sur un point** : `CurrentCompanion` ne rend **jamais** un tableau vide (GVP:105-112 + 68-70 : `[Companion]`, longueur 1, éventuellement `[None]`).
L'opérande vide des 2 lignes HJ:16827 / 16841 est donc la cible (`Actor` / `Companion`) ou, pour `pimp_out`, le nom mal orthographié (§ 2.2.4). Le résultat actuel de `isSameAs` sans compagnon n'est pas « faux » mais « vrai » quand la cible est elle aussi absente.
Abréviations (chemins absolus) :
- **D** = `D:\Game\LifePlay-master 5.8\Docs\Migration\etat\ST\STE\dumps\` (dumps du jour, GI `C4817B84`) : **ISA** = D`GI__isSameAs_P2.txt` (11 nœuds, 70 lignes) ;
**GVP** = D`GI__GetVariable_P2.txt` (53) ; **GEV** = D`GI__EventGraph_P2.txt` (6 249) ; **ACT** = D`Action__EventGraph_P2.txt` (363) ;
**CRBG** = D`GI__CallFunctionReturnBool_P1.txt` (591 nœuds, avant G13) ; les dumps `-Pins -Exec` ont deux listings (CRBG:1 puis CRBG:2997 ; GEV:1 puis GEV:30177 : le second ne montre que l'exécution) ;
- **HJ** = `D:\Game\LifePlay-master 5.8\Docs\Migration\etat\ST\STE\STE-H3e-J\LifePlay.log` (copie du journal H3e ; ligne de commande `-ScriptStackOnWarnings -LPHitchLog`, HJ:291) ;
- **H3eD** = `STE-H3e-DIAG.md`, **H3fD** = `STE-H3f-DIAG.md`, **H3dS** / **H3dI** = `STE-H3d-SPEC.md` / `STE-H3d-IMPL.md`, **H3S** = `STE-H3-SPEC.md` (dans `etat\ST\STE\`) ;
- **UE** = `D:\Game\Launcher\Engine\UE_5.8\Engine\Source\` (moteur installé, lu).
État relevé (Bash, lecture) : `git rev-parse --short HEAD` → `0745917` ; `md5sum Content/BPs/GI.uasset` → `c4817b847ecf246c859647577647d6e9` (= `C4817B84` des dumps) ;
`git status --short -- Content/BPs/GI.uasset Content/BPs/Person.uasset Content/BPs/Room.uasset` → ` M Content/BPs/Person.uasset`, ` M Content/BPs/Room.uasset` (GI non modifié : conforme à la fiche) ;
`git log --oneline -1 -- Content/BPs/GI.uasset` → `0745917` (G13).
## 1. Objectif mesurable
Supprimer, **sans autre changement de comportement que ceux nommés**, deux familles de lignes du journal H3e, par deux gardes dans `Content\BPs\GI.uasset` :
- **G15** (`isSameAs`) : les 2 lignes `Script Msg: Attempted to access index 0 from array 'Person_24_…' of length 0 in '/Game/Structs/Variable.Variable'!` (HJ:16827, 16841), toutes deux attribuées à `GI.isSameAs`
par leur pile (HJ:16828-16836, 16842-16850 ; H3eD § 1). Variante b décidée (Q-H3e-1) : `Same = false` dès qu'un des deux tableaux `Person` est vide, **avant** la lecture `Person[0]`.
- **G16** (F3) : les 2 lignes `Accessed None trying to read (real) property SkeletalMesh_2_93573D9945CD4136090CCBA1F1188DAC in not an UClass` (HJ:16058, 16086 ; pile
`Contact > OnClickContactButton > GI.LoadPreset > GenerateAndApplyStrings > PlayerChangeRace > UpdateDefaultHair`, HJ:16061-16071), **sans changement de comportement** (décision Q1 de l'utilisateur).
Attendus mesurables (§ 7) : `isSameAs` 11 → **15** nœuds ; `GI:EventGraph` 6 249 → **6 253** ; `GetVariable` (53), `Action:EventGraph` (363), `CallFunctionReturnBool` (G13) inchangés ;
journal de jeu comparable : `Person_24_.*of length 0` 2 → **0**, `SkeletalMesh_2_93573D` 2 → **0**, `isSameAs` 3 → **0**, `UpdateDefaultHair` 4 → **0** (piles ; HJ), `LogScript: Warning: Accessed None` 13 → **≤ 11** (indicatif).
### 1.1 Faits établis sur ISA (`GI.isSameAs`, 11 nœuds)
| Nœud | Liens (ISA) |
|---|---|
| `FunctionEntry_3241` | `then` → `CallFunction_23441.execute` (ISA:3) ; `Person` → `CallFunction_23441.Name`, `Concat_1059.A` (4) ; `Person2` → `CallFunction_23442.Name`, `Concat_1059.C` (5) |
| `CallFunction_23441` (`GetVariable`, **appel 1**) | `execute` ← `CallFunction_5124.then`, `FunctionEntry_3241.then` (7) ; `then` → `CallFunction_23442.execute` (8) ; `Name` ← `Entry.Person` (10) ; `Variable` → `BreakStruct_721.Variable` (11) |
| `CallFunction_23442` (`GetVariable`, **appel 2**) | `execute` ← `CallFunction_23441.then` (13) ; `then` → **`FunctionResult_8.execute`** (14) ; `Name` ← `Entry.Person2` (16) ; `Variable` → `BreakStruct_722.Variable` (17) |
| `BreakStruct_721` / `_722` (`BreakVariable`) | `Person_24_4B712C904750AD4CE1580DBEBD9E7396` → `GetArrayItem_2719.Array` (26) / `GetArrayItem_2720.Array` (36) ; autres sorties sans lien |
| **`GetArrayItem_2719`** / **`_2720`** (lecteurs `Person[0]`) | `Array` ← BreakStruct ; `Dimension 1` = `'0'` ; `Output` → `CallFunction_23443.A` (38-41) / `.B` (42-45) |
| `CallFunction_23443` | `KismetMathLibrary::EqualEqual_ObjectObject` : `A` ← 2719, `B` ← 2720, `ReturnValue` → **`FunctionResult_8.Same`** (46-50) |
| `FunctionResult_8` | `execute` ← `CallFunction_23442.then` (seule source) ; `Same` ← `CallFunction_23443.ReturnValue` (51-53) |
| `Concat_1059` → `PrintString_5124` | `PrintString_5124.execute (vide)` (61) : **inatteignable** (aucune entrée d'exécution) ; son `then` → `CallFunction_23441.execute` (62) |
Conséquences : (i) un **seul** `FunctionResult` (`FunctionResult_8`) : aucun à créer ni supprimer ; (ii) les nœuds purs (BreakStruct, GetArrayItem, EqualEqual) sont évalués à
l'exécution de `FunctionResult_8`, donc **après** `CallFunction_23442.then` ; (iii) le `PrintString` « is Same as » ne s'exécute pas, ce qui explique `is Same as` = 0 dans HJ (H3eD § 1 : « non vérifié sans dump », **maintenant établi**).
### 1.2 Longueur du tableau `Person` rendu par `GetVariable` (GVP) : d'où vient « vide »
- Noms du `Switch` (GVP:39-54) : `Player` → `[PlayerCharacter]` (98-104 ; 57-72) ; **`CurrentCompanion` → `[Companion]`** (105-112 ; `MakeArray_1506.[0]` ← `VariableGet_36005.Special`, 68-70) : longueur 1, **`[None]` sans compagnon** ;
`A1`…`A5` → `[SexPersons[i]]` (145-192) ; `Ptr` (193-200) ; `Interacting` (229-236) ; `choice` et `HoursElapsed` → un tableau `Person` d'un élément vide `[None]` (123-125 ; 211-213).
- Tout autre nom : `Default` → `ForEachLoopWithBreak` sur `Variables` (GVP:8-15, 40) ; trouvé → la `Variable` stockée est rendue (34, 36-38) ; **introuvable → `Completed (vide)` (GVP:15)** : la fonction se termine sans `FunctionResult`, `Variable` n'est pas écrite → tableaux vides (**estimé** : même mécanisme que § 2.2.3 ; cohérent avec « of length 0 in '/Game/Structs/Variable.Variable' », HJ:16827).
- Donc un tableau **vide** ne vient que de ce dernier cas (nom absent de `Variables`, ou variable stockée sans personne). `CurrentCompanion` n'est jamais vide.
### 1.3 Appelants de `isSameAs` (demandé par la fiche, rubrique 4)
- **Site connu, seul dans les dumps 5.8** : `CallFunction_26270` dans `CallFunctionReturnBool`, cas `SwitchString_77.isSameAs` (CRBG:21, 409-426) : `Person` ← `VariableGet_11335.ActualVariable`, `Person2` ← `GetArrayItem_3010.Output` (`ActualParameters[0]`),
`Same` → `VariableSet_7609.BoolToReturn` → `IfThenElse_8994` (CRBG:413-426). Grep `isSameAs` sur CRBG → 3 lignes : 21, 409, 3017 (3017 = pin du second listing du même `SwitchString_77`).
- Grep `isSameAs` → **0** sur GEV, ACT, `GI__CallFunction_V5.txt`, `PC__EventGraph.txt`, `Person__EventGraph_D4.txt`, `Room__EventGraph_V5.txt` ; sur le dump 5.5 complet de GI (`etat\portage55\L6f\L6f_dump_GI.txt`) : 2 lignes seulement, l'appel (21167) et la définition (25963).
- Binaires `.uasset`, fichier nommé (`isSameAs`, mode count) : `Person`, `Room`, `PC`, `StreetMap_Static`, `AI` (`Content\BPs\`), `UI\Action`, `UI\Scene` → **0** ; contrôle positif : `GI.uasset` → 6 ; `UI\Action.uasset` contient `CheckConditionsWho` (1) : la méthode voit un nom de fonction dans un `.uasset`.
- Côté données (non modifié par cette SPEC) : 8 `.lpaction` en `WHO:` (H3eD § 2), dont `approach_bar_i_.lpaction:13` (`If !Actor.isSameAs(CurrentCompanion) && …`) et `pimp_out.lpaction:13` (`Actor.isSameAs(CurrentConpanion)`, nom mal orthographié) ;
`apply_sunscreen.lpscene:79` (`CurrentCompanion.isSameAs(Actor)`) ; le reste = 89 occurrences dans 33 fichiers (H3eD C7, non recomptées ici).
- **Non couvert (incertain)** : les autres Blueprints et widgets non listés ci-dessus. Ce qui trancherait : `find_nodes {graph, title:"isSameAs"}` par MCP sur chaque graphe, ou la VAL (le dump après IMPL ne dit rien des appelants ; seul un appel inconnu à `isSameAs` verrait son résultat changer par b, cas du § 2.2.4).
## 2. Fichiers touchés, ordre
Un seul fichier : `Content\BPs\GI.uasset`, deux graphes : fonction **`isSameAs`** (G15) et **`EventGraph`** (G16, événement personnalisé `UpdateDefaultHair`).
Ni `GetVariable`, ni `CallFunctionReturnBool`, ni `CheckConditionsWho`/`Other`, ni `Action`, ni `Person`, ni le C++, ni les scènes, ni les modules. Aucune garde sur `Person` (STG-B1 y travaille).
Ordre : **G15 puis G16**, dans une seule session MCP, **un seul `save_assets` à la fin** (§ 3). G16 n'exige pas la réponse Q-H3g-1 : si Q-H3g-1 n'est pas rendue, exécuter G16 seule.
### 2.0 Préalables (dans l'ordre, avant tout `create_node`)
- **P1 (dumps)** : aucun manquant. Refaire les deux seulement si le MD5 de `GI.uasset` à l'entrée de l'IMPL diffère de `c4817b847ecf246c859647577647d6e9` (autre IMPL passée entre-temps) :
`.\Tools\RunCommandlet.ps1 -Lot STE-H3g -Commandlet LPDumpFunction -Args '-BP=GI -Fn=isSameAs -Pins -Exec' -EtatDir "D:\Game\LifePlay-master 5.8\Docs\Migration\etat\ST\STE"`
et idem avec `-Fn=EventGraph` (éditeur fermé : `.\Tools\UeState.ps1 -Kind Editeur` = 0).
- **P2** : MD5 de `GI.uasset` relevé juste avant l'IMPL (attendu `c4817b84…`) = valeur de repli.
- **P3** : gel annoncé à toutes les sessions (lp-mcp § 1.7), `.\Tools\Snapshot.ps1 -Lot STE-H3g` (`GI.uasset`), éditeur headless (lp-mcp § 2), `AssetTools.load_asset` `/Game/BPs/GI`.
- **P4** : `get_node_infos {nodes:[{refPath:"/Game/BPs/GI.GI:<Graphe>.K2Node_<Nom>"}]}` pour chaque nœud existant cité ci-dessous (deux appels de dix nœuds au plus : H3dI § 5 « Constats ») ;
les noms `K2Node_*` des dumps sont les noms de `refPath` (H3dI § 1.1 : tous retrouvés) ; liens = colonne « Avant » ; **relever** les indices de broches (attendus : `then` sortie 0, `Variable` sortie 1 des `GetVariable` ;
`Person_24_…` sortie 6 des BreakStruct, H3dI:49-50 ; `FunctionResult_8.execute` entrée 0). **Tout écart : arrêt et rapport.**
- G15 : `graph` = `/Game/BPs/GI.GI:isSameAs` ; nœuds `FunctionEntry_3241`, `CallFunction_23441`, `CallFunction_23442`, `BreakStruct_721`, `BreakStruct_722`, `GetArrayItem_2719`, `GetArrayItem_2720`, `CallFunction_23443`, `FunctionResult_8`, `CallFunction_5124`.
- G16 : `graph` = `/Game/BPs/GI.GI:EventGraph` ; nœuds `CustomEvent_5`, `IfThenElse_104`, `IfThenElse_65`, `IfThenElse_66`, `CallFunction_259`, `CallFunction_287`, `CallFunction_662`, `CallFunction_661`, `MacroInstance_5`, `MacroInstance_4`, `CallFunction_5654`, `CallFunction_237`, `BreakStruct_12`, `BreakStruct_13`, `VariableGet_1147`, `VariableGet_64`.
- **P5** : avant chaque `create_node`, `find_node_types {graph, type_id_filter, context_pins:[]}` : le `type_id` attendu doit figurer dans la réponse (§ 2.1) ; après création, `get_node_infos` du nouveau nœud.
- **P6** : Q-H3g-1 répondue (G15) ; G16 : Q1 déjà répondue.
### 2.1 Conventions (reprises de H3S § 2.1, H3dS § 2.1, lp-mcp § 3-4)
- `type_id` relus en 5.8 dans les IMPL précédentes : Branche = `Utilitaires|ContrôleDeFlux|Branche` (entrées `execute` 0, `Condition` 1 ; sorties `then` 0, `else` 1 ; H3dI:69) ;
IsValidIndex = `Utilitaires|Tableau|IsValidIndex` (`TargetArray` entrée 0, `IndexToTest` entrée 1 = `0` par défaut, `ReturnValue` sortie 0 ; H3dI:68) ;
IsValid = `Utilitaires|IsValid` (fonction **pure**, `Object` entrée 0, `ReturnValue` sortie 0 ; `STE-H5c-IMPL.md:24`, `STE-GARDES-IMPL.md:37-38`).
- Cascades de Branche, jamais AND / OR / Select purs ; une sortie de données peut alimenter plusieurs entrées (les liens existants restent). `delete_node` interdit. Aucun `FunctionResult` créé. Aucun `set_pin_value` (valeurs par défaut relues).
- Après les deux gardes : `compile_blueprint {blueprint:{refPath:"/Game/BPs/GI.GI"}, warnings_as_errors:true}` → `null`, journal de l'éditeur lu (aucune ligne Error/Warning liée à GI) ; **puis** `save_assets {asset_paths:["/Game/BPs/GI"]}` ;
fermeture de l'éditeur, `UeState.ps1` = 0, MD5 relevé (`hyper_blueprint_debug_inspect` absent du serveur, H3dI:156).
- Positions (`pos`) libres, à côté du nœud amont, sans chevauchement ; effet visuel seulement.
### 2.2 G15 — `isSameAs`, lecteurs `Person[0]` (2 lignes HJ) — ISA
#### 2.2.1 Avant → après
**Avant** : § 1.1. Chaîne d'exécution : `FunctionEntry_3241.then` → `CallFunction_23441` → `CallFunction_23442` → `FunctionResult_8` ; `Same` = `EqualEqual_ObjectObject(Person[0], Person2[0])`.
**Après** : 4 nœuds créés (forme équivalente plus courte : aucune, un AND pur de deux `IsValidIndex` coûterait 4 nœuds aussi et contredit la convention) :
`G15-I1` IsValidIndex (`TargetArray` ← `BreakStruct_721`), `G15-B1` Branche, `G15-I2` IsValidIndex (`TargetArray` ← `BreakStruct_722`), `G15-B2` Branche.
`break_pins` (1) : `CallFunction_23442.then` (sortie 0) / `FunctionResult_8.execute` (entrée 0).
`connect_pins` (7) :
1. `CallFunction_23442.then` → `G15-B1.execute` (0) ;
2. `BreakStruct_721.Person_24_4B712C904750AD4CE1580DBEBD9E7396` → `G15-I1.TargetArray` (0) (le lien vers `GetArrayItem_2719.Array` reste ; la broche générique prend le type `Person` au câblage, H3dI:82) ;
3. `G15-I1.ReturnValue` (0) → `G15-B1.Condition` (1) ;
4. `G15-B1.then` (0) → `G15-B2.execute` (0) ;
5. `BreakStruct_722.Person_24_4B712C904750AD4CE1580DBEBD9E7396` → `G15-I2.TargetArray` (0) (le lien vers `GetArrayItem_2720.Array` reste) ;
6. `G15-I2.ReturnValue` (0) → `G15-B2.Condition` (1) ;
7. `G15-B2.then` (0) → `FunctionResult_8.execute` (0).
**`G15-B1.else` et `G15-B2.else` : aucun lien** (le chemin « vide » s'arrête là, § 2.2.3). `IndexToTest` des deux IsValidIndex reste à `0` (relu).
Comptes : `isSameAs` **15** nœuds (11 + 4) ; 1 coupure ; 7 liens. Inchangés à la relecture : `Person` / `Person2` → `CallFunction_23441/2.Name`, `GetArrayItem_2719/2720` (liens `Array` et `Output`), `CallFunction_23443`, `FunctionResult_8.Same`, `Concat_1059`, `PrintString_5124`.
`get_node_infos` final : `G15-B2.then` seul lien de `FunctionResult_8.execute` ; `CallFunction_23442.then` → `G15-B1.execute` seul ; `FunctionResult_8` toujours seul.
#### 2.2.2 Comportement
- Les deux tableaux ont au moins un élément : `then` / `then` → `FunctionResult_8` ; mêmes nœuds, mêmes entrées : **résultat identique** (établi par construction).
- Au moins un tableau vide : l'exécution s'arrête à la Branche (aucune lecture `GetArrayItem` : plus de ligne `Attempted to access index 0 …`) ; `Same` n'est pas écrit : § 2.2.3.
#### 2.2.3 Pourquoi `Same` vaut `false` sur le chemin vide (estimé fort, pas établi par un essai)
`isSameAs` n'a qu'un `FunctionResult`, et créer un second est exclu (convention). Un chemin qui ne l'atteint pas laisse `Same` non écrit. Lecture du moteur :
une sortie d'une fonction script **n'est pas copiée dans le cadre de l'appelé** : l'adresse transmise est celle de la variable de l'appelant (`ScriptCore.cpp:1033-1046`, en particulier `:1045`) ;
cette variable est un temporaire de `CallFunctionReturnBool` (seul appelant, § 1.3), fonction non persistante dont le cadre est mis à zéro à chaque appel (`ScriptCore.cpp:984-994`) ;
`CallFunction_26270` n'est exécuté qu'une fois par appel de `CallFunctionReturnBool` : le temporaire vaut donc `false` à la lecture par `VariableSet_7609`. La fin de fonction sans `FunctionResult` est un chemin normal du compilateur
(`ReturnStatement` empilé en adresse de retour, `KismetCompilerVMBackend.cpp:2338-2349` ; `KCST_EndOfThread` → dépilement, `:2238-2240` ; `KCST_Return` → `EmitReturn`, `:2244-2246`).
Le projet s'appuie déjà sur ce mécanisme : `GetVariable` (nom introuvable, GVP:15) et G3 (`STE-H3-SPEC.md:108-110`). **À confirmer en VAL** : compilation sans diagnostic (§ 7.2) ; repli de conception en § 3.
#### 2.2.4 Cas changés (réponse à la demande « le seul cas changé » de la fiche : **non confirmé, trois cas**)
Avant : `Same = (A0 == B0)` avec `A0` = `Person[0]` (ou `None` si le tableau est vide : `KismetArrayLibrary.cpp:466-480`, défaut ligne 479) et `==` = `A == B` (`KismetMathLibrary.inl:2210-2213`).
| # | `Person` (A) | `Person2` (B) | Avant | Après G15 | Changé |
|---|---|---|---|---|---|
| 1 | ≥ 1, `[0]` valide (X) | ≥ 1, `[0]` valide (Y) | `X == Y` | `X == Y` | non |
| 2 | ≥ 1, `[0]` valide | ≥ 1, `[0]` None (et symétrique) | faux | faux | non |
| 3 | ≥ 1, `[0]` None | ≥ 1, `[0]` None | vrai | vrai | non |
| 4 | vide | ≥ 1, `[0]` valide (et symétrique) | faux | faux | non |
| 5 | **vide** | **vide** | **vrai** (`None == None`) | **faux** | **oui** (cas visé par la fiche) |
| 6 | **vide** | ≥ 1, **`[0]` None** (ex. `CurrentCompanion` sans compagnon, § 1.2) | **vrai** | **faux** | **oui** (non vu par la fiche) |
| 7 | ≥ 1, **`[0]` None** | **vide** | **vrai** | **faux** | **oui** (symétrique de 6) |
Les cas 6 et 7 sont atteignables dès qu'un opérande est `[None]` (`CurrentCompanion` sans compagnon, `choice`, `HoursElapsed`, `A1`…`A5` hors plage, `Interacting` / `Ptr` nuls : § 1.2) et l'autre absent de `Variables`.
Sites de données exposés (non modifiés) : les 8 `.lpaction` (`CurrentCompanion` en `Person2`), `apply_sunscreen.lpscene:79`, `pimp_out.lpaction:13` (`Person2` = nom absent, donc toujours vide : cas 4/5/7, résultat inchangé sauf si la cible est aussi absente).
Exemple lisible : `approach_bar_i_.lpaction:13` (`If !Actor.isSameAs(CurrentCompanion) && …`) avec `Actor` absent et aucun compagnon : `isSameAs` passe de vrai à faux, donc la sous-condition `!…` de **faux à vrai** (les autres sous-conditions de la ligne ne sont pas analysées ici).
Lequel de ces chemins a produit HJ:16827 / 16841 : **incertain** (2 clics sur 32 `310 ACtualLEftMouse` ; 8 actions candidates, aucune ne se lit dans la pile) ; ce qui trancherait : un journal avec la pile complète **et** l'identité de l'action évaluée (instrumentation, hors lecture seule).
### 2.3 G16 — F3, `UpdateDefaultHair`, boucles `Materials` (2 lignes HJ) — GEV
#### 2.3.1 Avant (GEV, dump du jour)
| Nœud | Liens (GEV) |
|---|---|
| `CustomEvent_5` `UpdateDefaultHair` | `then` → `IfThenElse_104.execute` (3701-3703) |
| `IfThenElse_104` | `Condition` ← `VariableGet_3775.IsMale` (3848-3850) ; `then` → `IfThenElse_65` (**homme**) ; `else` → `IfThenElse_66` (**femme**) (3704-3708) |
| `IfThenElse_65` / `_66` | `Condition` ← `CallFunction_235` (`EqualEqual_StriStri(Conv_TextToString(Race), 'Daz')`, 19390-19394) ; `then` → `CallFunction_662` / `_661` (direct) ; `else` → `CallFunction_259` / `_287` (`LoadHairMeshIfNeeded`) (20352-20361) |
| `CallFunction_259` / `_287` | `then` → `CallFunction_662.execute` / `_661.execute` (19804-19808, 19826-19830) |
| `GetArrayItem_4` / `_6` | `HairstyleTable[CallFunction_229 / _254]` (SelectInt 50/24, 38/20) → `BreakStruct_12` / `_13` `.SMBool` (19812-19815, 19831-19834 ; 19633-19638, 19377-19382) |
| `BreakStruct_12` / `_13` | **`SkeletalMesh_2_93573D…`** → `VariableGet_1147.self` + `CallFunction_662.NewMesh` (19818) / `VariableGet_64.self` + `CallFunction_661.NewMesh` (19837) |
| **`CallFunction_662`** / **`_661`** `SkinnedMeshComponent::SetSkinnedAssetAndUpdate` | `self` ← `VariableGet_241.HairSM` ; `NewMesh` ← `BreakStruct_12/13.SkeletalMesh_2` ; `bReinitPose` = `'true'` ; **`then` → `MacroInstance_5.Exec` / `MacroInstance_4.Exec`** (3715-3726) |
| **`MacroInstance_5`** / **`_4`** `ForEachLoop` | `Exec` ← `CallFunction_662.then` / `_661.then` ; `Array` ← `VariableGet_1147` / `_64` (`Materials`, `self` ← `BreakStruct_12/13.SkeletalMesh_2`) ; `LoopBody` → `CallFunction_2918` / `_2917` (`SetMaterial(HairSM, Array Index, Material)`) ; **`Completed` → `CallFunction_5654.execute`** (4238-4251 ; 4143-4145, 4158-4160) |
| `CallFunction_5654` `TurnMaterialDynamic(Mesh = HairSM)` | `execute` ← `MacroInstance_4.Completed`, `MacroInstance_5.Completed` ; `then` → `CallFunction_237.execute` (4138-4142) |
| `CallFunction_237` `Person_C::FitMorphableHair` | `execute` ← `CallFunction_5654.then` (19399-19402) |
Les 8 occurrences de `SkeletalMesh_2_93573D…` dans GEV : 3719, 3725 (`NewMesh`), 4145, 4160 (`self` de `Materials` : **les deux lectures qui lèvent F3**, le message nomme la propriété du contexte, `ScriptCore.cpp:3213-3216`), 19818, 19837 (sorties des BreakStruct),
11237 (valeur par défaut littérale d'une structure d'un autre nœud) et 28727 (`MakeStruct_8`, entrée vide de `ClothTable`) : les deux dernières ne sont pas des lectures. Les trois nœuds `CallFunction_2918`/`_2917`/`_5654`, `CallFunction_237`, `LoadHairMeshIfNeeded` ne sont pas touchés.
#### 2.3.2 Après : deux gardes (G16a homme, G16b femme), `SetSkinnedAssetAndUpdate` **inchangé**
Pour chaque garde : 2 nœuds (`G16x-V` IsValid pure, `G16x-B` Branche), 1 coupure, 5 liens.
- **G16a** (homme) — `break_pins` : `CallFunction_662.then` / `MacroInstance_5.Exec`. `connect_pins` :
1. `BreakStruct_12.SkeletalMesh_2_93573D9945CD4136090CCBA1F1188DAC` → `G16a-V.Object` (0) (les liens vers `VariableGet_1147.self` et `CallFunction_662.NewMesh` restent) ;
2. `CallFunction_662.then` → `G16a-B.execute` (0) ;
3. `G16a-V.ReturnValue` (0) → `G16a-B.Condition` (1) ;
4. `G16a-B.then` (0) → `MacroInstance_5.Exec` ;
5. `G16a-B.else` (1) → `CallFunction_5654.execute` (ce que l'exécution fait aujourd'hui après les boucles, sans le message).
- **G16b** (femme) — `break_pins` : `CallFunction_661.then` / `MacroInstance_4.Exec`. `connect_pins` : `BreakStruct_13.SkeletalMesh_2_…` → `G16b-V.Object` ; `CallFunction_661.then` → `G16b-B.execute` ; `G16b-V.ReturnValue` → `G16b-B.Condition` ; `G16b-B.then` → `MacroInstance_4.Exec` ; `G16b-B.else` → `CallFunction_5654.execute`.
Comptes : `GI:EventGraph` **6 253** nœuds (6 249 + 4) ; 2 coupures ; 10 liens. `CallFunction_5654.execute` : 4 sources (`MacroInstance_4.Completed`, `MacroInstance_5.Completed`, `G16a-B.else`, `G16b-B.else`).
Pourquoi deux gardes : la pile ne dit pas si le maillage None est celui de l'homme ou de la femme (HJ:16058, 16086 ; même pile) ; les deux lectures ont la même forme.
#### 2.3.3 Comportement (identique ; une hypothèse VM estimée)
- Maillage valide : `IsValid` vrai → `then` → boucle : chemin d'origine inchangé (**identique**, établi par construction).
- Maillage None, **avant** : `SetSkinnedAssetAndUpdate(HairSM, None)` ; la boucle lit `Materials` sur un contexte nul : `Accessed None`, expression sautée et valeur rendue effacée (`ScriptCore.cpp:3234-3245`) → tableau vide, **0 itération** (**estimé fort** : le macro `ForEachLoop` n'est pas lisible dans les sources ; un seul message par appel, HJ:16058 / 16086, et non un par itération) → `Completed` → `CallFunction_5654` → `CallFunction_237`.
- Maillage None, **après** : `IsValid` faux → `else` → `CallFunction_5654` → `CallFunction_237` : **même suite d'exécution**, sans lecture ni message. `SetSkinnedAssetAndUpdate(HairSM, None)` est conservé : la coiffure est toujours vidée (décision Q1 : « pas de coiffure, comme aujourd'hui »).
- Écart théorique, sans effet visé : un maillage non nul mais détruit / en attente de destruction (`IsValid` faux, `KismetSystemLibrary.h:126-128`) ne serait plus parcouru ; une entrée de `HairstyleTable` tenue par référence dure n'est pas dans cet état (estimé).
- Hors périmètre : la cause du None n'est pas instruite (H3fD § 4). Piste notée, non vérifiée : sur la branche `Daz` (`CallFunction_235` vrai) le `then` va **directement** à `SetSkinnedAssetAndUpdate` sans `LoadHairMeshIfNeeded` (GEV:20352-20361).
## 3. Repli
- Avant écriture : instantané `.\Tools\Snapshot.ps1 -Lot STE-H3g` (P3), MD5 de départ (P2).
- **Aucun `save_assets` tant que `compile_blueprint` n'est pas à 0 diagnostic pour G15 et G16.** Échec avant enregistrement : fermer l'éditeur sans enregistrer (le `.uasset` reste l'instantané), contrôler le MD5, rapporter. Aucun correctif improvisé.
- Après enregistrement, lien relu différent, `type_id` introuvable, nœud inattendu : arrêt, rapport, puis sur décision de l'orchestrateur éditeur fermé : `.\Tools\Restore.ps1 -Lot STE-H3g -Only Content\BPs\GI.uasset -WhatIf`, puis sans `-WhatIf` ; MD5 = valeur de P2. Un `Restore` de `GI` défait **toute** autre écriture sur GI faite depuis l'instantané (IMPL jamais entrelacées).
- **Repli de conception de G15** (compilation avec diagnostic sur l'`else` non relié, ou VAL montrant `Same` ≠ faux sur tableau vide) : garde **au site d'appel**, dans `CallFunctionReturnBool`, entre `SwitchString_77.isSameAs` et `CallFunction_26270` (patron G13 variante b, H3dI § 1.2-1.3 : `BoolToReturn = false` explicite, `IfThenElse_8994` inchangé) :
2 `GetVariable` (`Scène|GetVariable`) + 2 BreakStruct + 2 IsValidIndex + 2 Branche + 1 Set `BoolToReturn` ≈ 9 nœuds. **Non spécifié ici** (fiche de suite : dump `CallFunctionReturnBool` à jour requis).
- Après commit (gardien) : `git revert` du commit du ticket STE-H3g (indépendant de H3d / H10).
## 4. Risques R#, questions Q#
- **R-H3g-1** : b change trois cas, pas un (§ 2.2.4) ; cas 6/7 = chemin « pas de compagnon » probable. → **Q-H3g-1**.
- **R-H3g-2** : le `false` du chemin vide de G15 repose sur la valeur initiale (zéro) du temporaire de l'appelant, mécanisme lu dans le moteur (§ 2.2.3) mais non rejoué ; un seul site d'appel, appelé une fois par cadre (établi, CRBG:409). Repli : § 3.
- **R-H3g-3** : déclencheur de la F2 `isSameAs` non identifié (quelle action, quelle cible : 2 clics sur 32 en HJ) : un journal à 0 n'est un indice fort que si le parcours rejoue ce contexte ; la preuve principale est statique (dump) et moteur (§ 6).
- **R-H3g-4** : G16, boucle sur tableau effacé = 0 itération, estimée (macro non lisible) ; si l'exécution actuelle itérait, l'`else` omettrait des `SetMaterial` sans maillage : sans objet pour un maillage None (aucun matériau à poser).
- **R-H3g-5** : GI écrit entre-temps par une autre IMPL ou dumps périmés : P1 / P2 (MD5) ; Person et Room sont modifiés dans l'arbre par d'autres lots (` M` à l'état relevé) : ne pas les inclure au ticket.
- **R-H3g-6** : appelants inconnus de `isSameAs` (§ 1.3, partie non couverte) : leur résultat changerait dans les cas 5-7 seulement.
- **R-H3g-7** : F3 masquée, pas guérie : le personnage garde une coiffure vide quand la table donne None (décision Q1) ; la cause reste ouverte.
- **Q-H3g-1 (utilisateur, avant G15)** : confirmer la variante **b** malgré les trois cas changés, ou choisir **c** (aucune garde, bruit : une ligne `Person_24` par évaluation concernée, sans `Accessed None`, résultat actuel conservé). Rappel : une variante « rejeu exact » (a) est impossible dans `isSameAs` (second `FunctionResult` exclu) et ne se réduit pas à une valeur constante (cas 5-7 vrai, cas 4 faux).
Recommandation : b **seulement** si « un opérande absent donne faux » est le sens voulu de `isSameAs` ; sinon c.
- **Q-H3g-2 (orchestrateur)** : lieu de G15 : `isSameAs` (retenu, 4 nœuds, `else` non relié) ou site d'appel (≈ 9 nœuds, `false` explicite). Recommandation : `isSameAs`.
- **Q-H3g-3 (orchestrateur)** : BUILD-GO et ordre : G15 + G16 en une session GI, un seul instantané, un seul `save_assets`.
- **Q-H3g-4 (utilisateur, facultative, hors fiche)** : DIAG sur la cause du None de F3 (piste `Daz`) et sur l'action évaluée à l'origine de la F2 `isSameAs` ; `pimp_out.lpaction:13` (`CurrentConpanion`) reste une donnée à corriger (Q-H3e-2).
## 5. Interdits
- Aucune modification de `GetVariable`, `CallFunctionReturnBool`, `CheckConditionsWho` / `Other`, `Action`, `LoadHairMeshIfNeeded`, `SetSkinnedAssetAndUpdate` (appel conservé tel quel), des boucles `MacroInstance_4` / `_5`, de `Person`, du C++, des scènes et modules.
- Aucune garde sur `Person` (STG-B1). Aucun AND / OR / Select pur, aucun `FunctionResult` créé, aucun `delete_node`, aucun `set_pin_value`. Pas d'IMPL sans P2 à P5. Pas d'écriture HyperAIStudio (lp-mcp § 1.1) ; outils Epic seulement.
- Pas de commit par l'agent : ticket au gardien (`lp-repo`). Pas de `type_id` non relu ; pas de fusion avec une autre IMPL sur GI.
## 6. Valeurs moteur lues dans le moteur installé (UE = `D:\Game\Launcher\Engine\UE_5.8\Engine\Source\`)
- `GenericArray_Get`, index invalide : message `Attempted to access index %d from array '%s' of length %d in '%s'!` (propriété tableau, longueur, chemin du propriétaire = la structure `Variable`), puis élément remis au défaut : `Runtime\Engine\Private\KismetArrayLibrary.cpp:459-482` (message 472-478, `ResetToDefault` 479).
- `GenericArray_IsValidIndex` : test sans message : `Runtime\Engine\Private\KismetArrayLibrary.cpp:583-595`.
- `EqualEqual_ObjectObject` = `A == B` : `Runtime\Engine\Classes\Kismet\KismetMathLibrary.inl:2210-2213`.
- `UKismetSystemLibrary::IsValid(const UObject*)` : « non-null and not pending kill » : `Runtime\Engine\Classes\Kismet\KismetSystemLibrary.h:126-128`.
- `Accessed None trying to read … property X in not an UClass` (propriétaire non classe = structure) : `Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:3213-3216` ; contexte nul : expression sautée, valeur rendue effacée par `ClearValue` : `:3234-3245`.
- Cadre d'une fonction script mis à zéro à chaque appel (non persistant) : `ScriptCore.cpp:984-994` ; paramètre de sortie = adresse de la variable de l'appelant : `ScriptCore.cpp:1033-1046`.
- Fin d'une fonction Blueprint : `ReturnStatement` empilé, `KCST_EndOfThread` dépile, `KCST_Return` émet le retour : `Editor\KismetCompiler\Private\KismetCompilerVMBackend.cpp:2338-2349, 2238-2246` ; signature conservée même si des nœuds de retour sont élagués : `Editor\KismetCompiler\Private\KismetCompiler.cpp:2447-2501`.
- **Non lu** (estimé) : le code du macro `ForEachLoop` (`StandardMacros`, asset) ; la valeur initiale des temporaires de l'appelant (déduite de `ScriptCore.cpp:984-994`).
## 7. Critères de VAL (commande → attendu)
1. **Dumps après IMPL** (éditeur fermé, `UeState.ps1` = 0), dans l'ordre :
`.\Tools\RunCommandlet.ps1 -Lot STE-H3g -Commandlet LPDumpFunction -Args '-BP=GI -Fn=isSameAs -Pins -Exec' -EtatDir "D:\Game\LifePlay-master 5.8\Docs\Migration\etat\ST\STE"`
→ en-tête `=== GI :: isSameAs (15 noeuds) ===` ; `CallFunction_23442.then` → `G15-B1.execute` seul ; `G15-B1.then` → `G15-B2.execute` ; `G15-B2.then` → `FunctionResult_8.execute` seul ; `G15-B1.else` et `G15-B2.else` sans lien ;
`G15-I1.TargetArray` ← `BreakStruct_721.Person_24_…` (qui garde `GetArrayItem_2719.Array`), `G15-I2.TargetArray` ← `BreakStruct_722.Person_24_…` ; `IndexToTest` = `0` ; `FunctionResult_8.Same` ← `CallFunction_23443.ReturnValue` inchangé.
`... -Args '-BP=GI -Fn=EventGraph -Pins -Exec'` → `(6253 noeuds)` ; `CallFunction_662.then` → `G16a-B.execute` seul ; `MacroInstance_5.Exec` ← `G16a-B.then` seul ; `CallFunction_661.then` → `G16b-B.execute` seul ; `MacroInstance_4.Exec` ← `G16b-B.then` seul ;
`CallFunction_5654.execute` ← 4 sources ; `BreakStruct_12.SkeletalMesh_2_…` → `VariableGet_1147.self`, `CallFunction_662.NewMesh`, `G16a-V.Object` (idem `_13`) ; `NewMesh` des deux `SetSkinnedAssetAndUpdate` toujours relié.
`... -Fn=GetVariable` → 53 ; `... -BP=Action -Fn=EventGraph` → 363 ; `... -Fn=CallFunctionReturnBool` → 594 (591 + G13), inchangé par cette fiche.
`python Tools\DiffRemplacementNoeuds.py <avant = D\GI__isSameAs_P2.txt / D\GI__EventGraph_P2.txt> <après> <profil>` : sans autre écart que les nœuds et liens ci-dessus.
2. **Compilation** : `compile_blueprint … warnings_as_errors:true` → `null`, journal de l'éditeur sans Error / Warning pour GI (l'`else` non relié ne doit produire aucun diagnostic : sinon repli § 3) ;
`CompileAllBlueprints` éditeur fermé : BP `/Game/` du projet 0 erreur (critère requalifié par l'utilisateur le 2026-10-13, `CC_Shaders` à part) ; `LPTestStringCompat` **3120 / 0 / 60** (lp-validate).
3. **Empreintes** : `md5sum Content/BPs/GI.uasset` relevé et noté ; `git status --short -- Content` : GI ajouté aux ` M` déjà présents (`Person`, `Room`) sans les modifier (MD5 de ces deux fichiers identiques avant / après).
4. **Journal de jeu** (build Development empaqueté par BUILD, **même ligne de commande que HJ** : `-ScriptStackOnWarnings -LPHitchLog`, HJ:291), Grep en mode `count`, baseline = HJ :
| Motif | HJ | Attendu |
|---|---|---|
| `Person_24_.*of length 0` | 2 | **0** |
| `isSameAs` | 3 | **0** |
| `SkeletalMesh_2_93573D` | 2 | **0** |
| `UpdateDefaultHair` | 4 | **0** |
| `LogScript: Warning: Accessed None` | 13 | **≤ 11** (indicatif : −2 F3 ; les autres familles dépendent du parcours) |
Preuve que le parcours est rejoué (sinon un 0 ne prouve rien) : G16 = au moins 3 `was from AddCreature` (HJ : 3) puis au moins 1 `LOAD ETHNIC` après le troisième (HJ : 6 au total, dont 16085 et 16113) et `CLICK ON THE SAME BUTTON` ≥ 1 (HJ:16114) ;
G15 = clics de carte sur des personnes (HJ : 32 `310 ACtualLEftMouse`, dont les deux HJ:16825 et 16839 suivis d'une F2 en moins de 210 ms) ; **limite R-H3g-3** : le déclencheur n'étant pas identifié, un 0 sur ce motif n'est qu'un indice.
5. **Non-régression** : `fatal|Unhandled|Critical|Assertion` → 0 ; `CHECKING: .*pet/(horse/)?meet_new_person` ≥ 1 (HJ : 6) ; `& .\Tools\LogStats.ps1 -Compare <journal>,<HJ>` : aucune famille nouvelle ; G13 intact (`isCreatureType` → 0).
6. **Cas changés de G15** (R-H3g-1) : si Q-H3g-1 = b, la VAL note qu'un changement de résultat des conditions `isSameAs` sans compagnon est **attendu** (cas 5-7) ; aucun test de jeu ne le contrôle (non mesuré).
Fichiers à commiter (dépôt jeu, branche `ue5.8`, ticket au gardien) :
- A `Docs/Migration/etat/ST/STE/STE-H3g-SPEC.md`
Tours : 27 / 50
## Constats
Commandes (outils Grep / Read sauf mention) et comptes bruts :
- C1 Bash : `git rev-parse --short HEAD` → `0745917` ; `md5sum Content/BPs/GI.uasset` → `c4817b847ecf246c859647577647d6e9` ; `git status --short -- Content/BPs/GI.uasset Content/BPs/Person.uasset Content/BPs/Room.uasset` → ` M Content/BPs/Person.uasset`, ` M Content/BPs/Room.uasset`.
- C2 En-têtes : ISA:1 `(11 noeuds)` ; GVP:1 `(53 noeuds)` ; GEV:1 et GEV:30177 `(6249 noeuds)` ; ACT:1 et ACT:1901 `(363 noeuds)` ; CRBG:1 et CRBG:2997 `(591 noeuds)`.
- C3 Grep `SkeletalMesh_2_93573D9945CD4136090CCBA1F1188DAC` sur GEV (fichier nommé) → lignes 3719, 3725, 4145, 4160, 11237, 19818, 19837, 28727 (8) ; `SetSkinnedAssetAndUpdate` → 997, 3715, 3721, 24129, 24162 (les trois derniers hors `UpdateDefaultHair`).
- C4 Grep `isSameAs` : CRBG → 21, 409, 3017 ; GEV, ACT, `GI__CallFunction_V5.txt`, `PC__EventGraph.txt`, `Person__EventGraph_D4.txt`, `Room__EventGraph_V5.txt` → 0 ; L6f → 21167, 25963 ; `.uasset` : `GI` 6, `Person` / `Room` / `PC` / `UI\Action` / `BPs\AI` / `BPs\StreetMap_Static` / `UI\Scene` 0 ; `UI\Action.uasset` `CheckConditionsWho` → 1.
- C5 `Content\Modules`, Grep `[A-Za-z0-9_]+\.isSameAs\([^)]*\)` (-o) : appels listés (Actor / Companion / Dating / Agg / Host / Guest… ; 8 `.lpaction` : `CurrentCompanion` ×7, `CurrentConpanion` ×1) ; Grep d'un dossier ignoré par git fonctionne ici (`Content\Modules` passé en `path`), contrairement à `etat\…\dumps` (fichier à nommer).
- C6 HJ, Grep count : `310 ACtualLEftMouse` → 32 ; `LOAD ETHNIC` → 6 ; `was from AddCreature` → 3 ; `isSameAs` → 3 ; `UpdateDefaultHair` → 4 ; `LogScript: Warning: Accessed None` → 13 ; `SkeletalMesh_2_93573D` → 2 ; `Person_24_.*of length 0` → 2 (HJ:16827, 16841) ; ligne de commande HJ:291.
- C7 Lectures moteur : voir § 6 (toutes relues le 2026-10-13).
- C8 Écarts de méthode : un `Bash` (git / md5sum) ; deux `Grep` de dossier sans résultat (`Content` en `*.uasset` : binaires arrêtés au premier octet nul, contrôle positif par fichier nommé) ; lecture de `P1` jusqu'à CRBG:3024 pour identifier le second listing.
- Non mesuré : l'action évaluée à l'origine de HJ:16827 / 16841 ; le sexe du personnage de HJ:16058 / 16086 ; le comportement réel de `Same` sur chemin non écrit (§ 2.2.3) ; la valeur de `CurrentCompanion` / de la cible `Actor` dans ces clics ; les autres Blueprints éventuellement appelants.
- Lecture seule respectée ; un seul fichier écrit (ce livrable, non suivi). Aucun nom d'utilisateur ni chemin de profil cité.
## Décisions de l'utilisateur (2026-10-13)
- Q-H3g-1 : **c, pas de garde G15** (la variante b change un cas réel : sans compagnon, `CurrentCompanion` = `[None]`) ; l'avertissement `isSameAs` (1 par clic carte, sans effet visible) est accepté comme bruit ; Q-H3e-1 = b est donc révisée en c.
- G16 : **approuvée** pour l'IMPL (sans changement de comportement) ; IMPL par MCP sous instantané, VAL statique, commit ; preuve en jeu au prochain build.