LifePlay-Revival/Docs/Migration/etat/ST/STE/STE-H3g-SPEC.md
edujiro5972 8084622450 LifePlay UE 5.8 : état de développement au 2026-10-19
Tickets : REP1-T4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:57:42 +02:00

38 KiB
Raw Blame 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 = DGI__isSameAs_P2.txt (11 nœuds, 70 lignes) ; GVP = DGI__GetVariable_P2.txt (53) ; GEV = DGI__EventGraph_P2.txt (6 249) ; ACT = DAction__EventGraph_P2.txt (363) ; CRBG = DGI__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.