Fiche STE-H10 — Niveau N3 — Modèle prévu (modèle de l'agent lp-cart) | utilisé claude-opus-5-5 — Agent lp-cart (SPEC, lecture seule) — Skills sk-protocol, lp-mcp (lecture), lp-commandlets (lecture), lp-paths, ue-cpp-build # STE-H10-SPEC : correctifs des saccades « cheveux » et « fin de scène » (2026-10-11) Lecture seule : aucun éditeur, commandlet, build ni jeu ; seul fichier écrit : ce livrable. **Aucun choix n'est tranché** : chaque choix est une question Q-H10-n (§ 4). Copie `D:\Game\LifePlay-master 5.8`, HEAD (GI `16394D43…`, Person `BE21838A…`, TopRight `3DF4DEC4…`). Abréviations des sources (dumps dans `etat\ST\STE\dumps\`) : LH `GI__LoadHairMeshIfNeeded_D9.txt` ; GS `GI__getSkeletalMeshFromPath_D9.txt` ; SP `GI__SavePreset_D9.txt` ; GC `GI__GetSMBoolForSMC_Cloth_D9.txt` ; IG `Person__IsGenitalExposed_D9.txt` ; SAG `Person__ShowAppropriateGenital_D9.txt` ; CP `Person__ClearProxies_D9.txt` ; CA `GI__CopyActionsToScroll_D9.txt` ; TR `TopRight__EventGraph_D9.txt` ; GEV `GI__EventGraph_V5.txt` ; PEG `Person__EventGraph_D4.txt` ; CSP `GI__clearScenePersons_V5.txt` ; DD `GI__destroyDummy_D4.txt`. Mesures : ST = export H9 `scratchpad\h9\stats_gt.csv` (GameThread, trace entière : 31 758 trames, 545,58 s, ST:2) ; EV = exports H9 `scratchpad\h9\ev_.csv` (événements complets, colonnes `TimerName,StartTime,EndTime,Depth`) ; dossier `%USERPROFILE%\AppData\Local\Temp\claude\D--Game-LifePlay-master-5-8\37530d8d-c780-4a57-8fbd-bcb010aa6408\scratchpad\h9\` (non suivi). ## 1. Objectif mesurable Réduire les trames ≥ 50 ms mesurées par H9 (120 trames, 18,6 s ; `STE-H9-TRACE.md` § 2) sur deux volets, sans effet visible non accepté par l'utilisateur : (A) chargement synchrone des cheveux à l'apparition des PNJ ; (B) clic « Suivant » de fin de scène. ### 1.1 Mesures de départ (établi) | Fonction | Trace entière (ST) : appels ; inclusif ; moy. / max | 8 trames de fin de scène (H9 § 4) | |---|---|---| | `LoadHairMeshIfNeeded` (GI) | 108 ; 284,6 ms ; 2,6 / 9,1 ms (ST:375) | trames sans clic : 285 ms, 5 à 9 ms par chargement (H9 § 3) | | `getSkeletalMeshFromPath` (GI) | 231 ; 920,5 ms ; 4,0 / 23,1 ms (ST:210) | — | | `RandomizeHairs` (Person) | 34 ; 576,6 ms ; 17,0 / 38,4 ms (ST:261) | — | | `clearScenePersons` / `DestroyPerson` | 15 ; 1 903,2 ms / 30 ; 1 487,2 ms (ST:136, 158) | 1 182 / 934 ms ; 8 / 14 | | `SavePreset` (GI) | 29 ; 1 544,3 ms ; 53,3 / 89,1 ms (ST:155) | 879 ms ; 14 | | `RevertStripAll` / `StripAll` (Person) | 139 ; 2 106,3 ms / 81 ; 618,2 ms (ST:131, 251) | 822 ms ; 44 | | `IsGenitalExposed` (Person) | 312 ; 2 929,3 ms (ST:105) | 594 ms ; 60 | | `GetSMBoolForSMC_Cloth` (GI) | 1 393 ; 3 017,6 ms incl., 726,0 ms excl. (ST:102) | 644 ms ; 309 | | `Dress` / `ClearProxies` | 374 ; 2 825,5 ms / 198 ; 1 610,4 ms incl., 390,5 ms excl. (ST:108, 152) | 390 / 307 ms ; 38 | | `Current` (TopRight) | 50 ; 1 486,9 ms ; 29,7 / 106,4 ms (ST:159) | 616 ms ; 16 | | `ShowUI` / `CopyActionsToScroll` (GI) | 20 ; 644,8 ms / 46 ; 338,7 ms (ST:245, 332) | 380 / 132 ms | Inclusif = appels emboîtés compris : les lignes se recouvrent (`RevertStripAll` contient `IsGenitalExposed`, etc.) ; ne pas les additionner. Le « 285 ms » de H9 § 3 est le total de session de `LoadHairMeshIfNeeded` (ST:375) ; les 89 appels de H9 sont ceux vus sous les actions latentes (estimé). ### 1.2 Décomposition de `SavePreset` appelé par `DestroyPerson` (établi, 6 appels) Commande : Grep `^(DestroyPerson|SavePreset|StripAll|RevertStripAll|SaveTxt|destroyDummy|RestoreCbe|BackupCbe|CheckCbeGenderModel|removeAlreadyGot),` sur `ev_*.csv` (80 lignes affichées). Durées = `EndTime − StartTime`, en ms. | `SavePreset` (EV:ligne) | Durée | `StripAll` initial | `RevertStripAll` final | `destroyDummy` ensuite | |---|---|---|---|---| | ev_74:225959 | 73,6 | 8,4 (ev_74:225962) | 36,3 (ev_74:319281) | non (RestoreCbe ev_74:462553, pas de destroyDummy) | | ev_24:239517 | 78,5 | 12,7 | 52,2 (ev_24:336114) | oui (ev_24:527945) | | ev_60:561096 | 82,5 | 9,2 | 43,6 (ev_60:654602) | oui (ev_60:802612) | | ev_22:228220 | 89,1 | 11,7 | 42,2 (ev_22:331711) | oui (ev_22:496606) | | ev_22:562970 | 84,9 | 14,2 | 50,2 (ev_22:666294) | oui (ev_22:857212) | | ev_98:192243 | 87,9 | 14,3 | 57,8 (ev_98:291929) | oui (ev_98:507720) | | **Total** | **496,5** | **70,5 (14,2 %)** | **282,4 (56,9 %)** | 5 sur 6 | Reste 143,6 ms (28,9 %) : boucles de morphs et deux `SaveTxt` (ex. ev_60:653341 = 6,7 ms, ev_60:654600 = 0,9 ms). Sur les 5 appels suivis d'un `destroyDummy`, le `RevertStripAll` final coûte 246,1 ms, **49,2 ms par personne** (42,2 à 57,8). ### 1.3 Gains attendus par option (estimé, à remesurer par une nouvelle trace) | Option | Trame visée | Gain par occurrence | Gain sur la session (trace H9) | Effet visible | |---|---|---|---|---| | A1 tirer puis charger | apparition de PNJ (trame 1 H9 : 305 ms, 8 chargements) | − 35 à − 63 ms (7 chargements de 5 à 9 ms évités) | ≤ 284,6 ms moins les cheveux tirés | aucun si tout chemin se charge | | A3 asynchrone (sur A1) | idem | − 5 à − 9 ms de plus par PNJ | ≤ 284,6 ms | PNJ sans cheveux quelques trames | | B1-a pas de rhabillage avant mise au pool | Suivant | − 49,2 ms par personne mise au pool | ≈ 29 × 5/6 × 49,2 ≈ 1,2 s ; 8 trames ≈ 575 ms (≈ − 70 ms par trame) | aucun (mannequin caché dans la même trame) | | B2-a suffixe hors boucle (GI) | Suivant, Dress | 30 à 45 % de `GetSMBoolForSMC_Cloth` | ≈ 0,9 à 1,35 s sur 3,02 s ; 8 trames ≈ 190 à 290 ms | aucun | | B3-a suffixe hors boucle (Person) | Suivant, Dress | 25 à 40 % de `ClearProxies` | ≈ 0,4 à 0,65 s sur 1,61 s ; 8 trames ≈ 75 à 120 ms | aucun | | B4-a regrouper `Current` | Suivant | ≈ 38 ms par trame (2 appels → 1) ; ≈ 77 ms reportés à la trame suivante | ≤ 1,49 s × part regroupée (incertain) | liste d'actions à jour une trame plus tard | Les gains de B1-a, B2-a et B3-a **ne s'additionnent pas** : le `RevertStripAll` supprimé par B1-a contient une part des appels de `IsGenitalExposed`, `GetSMBoolForSMC_Cloth`, `Dress` et `ClearProxies`. Raisonnement B2-a / B3-a : 3 des ≈ 7 évaluations de nœud par itération (`GetSkeletalMesh`, `GetObjectName`, `Concat`) sortent de la boucle (GC:36-59, CP:135-154, 176-179). ## 2. Fichiers touchés, ordre ### 2.0 Préalables communs (toute option ; lp-mcp § 1-3) - **P1** : BUILD-GO de Big Boss ; écritures après la porte T11 d'U8 ; pour GI : après C0 (`STE-H4-C0-SPEC.md`, `GI.destroyDummy`) et dans l'ordre fixé avec CAM (Q-H10-12). 0 processus étranger. - **P2** : MD5 de chaque `.uasset` touché = HEAD (GI `16394D43…`, Person `BE21838A…`, TopRight `3DF4DEC4…`) ; `git status --short -- Content` vide. - **P3** : `.\Tools\Snapshot.ps1 -Lot STE-H10 -Path ` pour chaque fichier, MD5 de la copie = P2. - **P4** : éditeur headless (lp-mcp § 2), `load_asset` ; un appel MCP à la fois ; HyperAIStudio lit, outils Epic écrivent. - **P5** : `get_node_infos` de chaque nœud cité : liens = colonne « avant » ; indices des broches relus (lp-mcp § 3) ; tout écart → arrêt, rien écrit. - **type_id** (localisés, lp-mcp § 3) : `Utilitaires|ContrôleDeFlux|Branche` ; `Classe||` ; `Variables||Définir` et lecture `…|` : relever le type_id exact par `find_node_types {graph, type_id_filter:""}` ; fonctions de tableau : `find_node_types` filtre `Tableau` (forme `Utilitaires|Tableau|…`) ; `RandomInteger` : filtre `RandomInteger`. Après connexion, vérifier par `get_node_infos` que les broches génériques ont pris le bon type. - **Variables nouvelles** : `BlueprintTools.add_variable(blueprint, name, type_name, graph, container_type)` (membre ; locale si `graph` est donné ; `MAP` à clé string) — blueprint.py:1154-1185. - **refPath** : `/Game/BPs/GI.GI:.K2Node_`, `/Game/BPs/Person.Person:.K2Node_`, `/Game/UI/TopRight.TopRight:.K2Node_` (Glob : `Content\BPs\GI.uasset`, `Content\BPs\Person.uasset`, `Content\UI\TopRight.uasset`). - **Fin** : `compile_blueprint`, `hyper_blueprint_debug_inspect` 0 diagnostic, `save_assets`, fermeture, `UeState.ps1` = 0, MD5 relevé. - Un commit par option (repli indépendant, § 3). ### 2.1 Volet A — cheveux **Constat** (établi sauf mention) : - `LoadHairMeshIfNeeded(Index)` a déjà un cache : `HairstyleTable[Index].SkeletalMesh` valide → rien (LH:11-15) ; sinon, chemin ≠ None (LH:43-52) et (Daz ou `LegacyPercentage > 0`) (LH:63-80) → `getSkeletalMeshFromPath` → `Array_Set` dans `HairstyleTable` (LH:16-29). - `getSkeletalMeshFromPath` : `GetAssetByObjectPath` → `IsAssetLoaded` (résultat `SmLoaded` non lu) → `AssetRegistryHelpers::GetAsset` → IsValid → cast (GS:4-80). `GetAsset` charge le paquet en synchrone s'il n'est pas en mémoire (§ 6). - Appelant des PNJ : événement Person `RandomizeHairs` (PEG:2057) → `SecondTurn`, `ColorfulHair` (PEG:2342-2346, 2066-2071) → `Array_Clear(Hairs)` (`CallArrayFunction_10`, PEG:9236-9240) → `ForEachLoopWithBreak` (`MacroInstance_402`) sur **toute** `GI.HairstyleTable` (PEG:2084-2094) → filtre `Bool == IsMale` ET `Daz == (Race == "Daz")` (PEG:2105-2117, 8664-8678) → maillage valide : `Array_Add(Hairs)` (`CallArrayFunction_11`) ; sinon `GI.LoadHairMeshIfNeeded(ArrayIndex)` (`CallFunction_96`) puis `Array_Add` si valide (PEG:9244-9260, 10640-10644) → `Completed` : `PickedHair = Hairs[RandomInteger(Len(Hairs))]` (`VariableSet_12`, `GetArrayItem_3`, `CallFunction_133`, `CallArrayFunction_14`, `BreakStruct_3`, PEG:9264-9291) → `SetSkinnedAssetAndUpdate(HairSM, PickedHair)` (`CallFunction_1405`, PEG:2075-2080). → Le premier PNJ d'un sexe / type charge **tous** les cheveux éligibles pour en garder **un** ; d'où les rafales de 5 à 9 ms (H9 § 3). Ensuite le cache GI tient (référence dans `HairstyleTable`) : le coût est surtout en début de session (estimé). - Autres appelants (non visés) : GI.EventGraph `CallFunction_259` / `_287` (index fixes 50/24 et 38/20 du CharGen, GEV:19804-19830, 19377-19382, 19633-19638) ; `GI.GenerateAndApplyStrings` `CallFunction_5` / `_8` ; Person `LoadBridgetAndHairs` (préchargement de tous les cheveux Daz pour le joueur, PEG:9513-9591, 10670-10688), **absent de la trace** (Grep `LoadBridgetAndHairs` sur ST : 0 ligne). - Le résultat est utilisé dans la même trame (`SetSkinnedAssetAndUpdate` juste après) : un chargement asynchrone change l'ordre d'exécution (A3). **Option A1 — « tirer d'abord, charger ensuite »** (Person seul ; GI non modifié) Principe : la boucle ne charge plus ; elle note l'index de chaque cheveu éligible ; un index est tiré, puis `GI.LoadHairMeshIfNeeded(index)` charge ce seul cheveu (le cache GI reste rempli) ; `PickedHair` est lu dans `GI.HairstyleTable`. - Graphe : `Person:EventGraph`. Variables : `add_variable(Person, "HairIdxs", "int", container_type=ARRAY)`, `add_variable(Person, "PickedHairIdx", "int")`. - Nœuds nouveaux (8) : N1 `Array_Clear` ; N2 `Array_Add` ; N3 `Set PickedHairIdx` ; N4 lecture `HairIdxs` ; N5 `Array_Length` ; N6 `RandomInteger` ; N7 `GetArrayItem` ; N8 lecture `PickedHairIdx`. - `break_pins` (7) : b1 `CallArrayFunction_10.then` / `MacroInstance_402.Exec` ; b2 `MacroInstance_19.Is Not Valid` / `CallFunction_96.execute` ; b3 `CallFunction_96.then` / `MacroInstance_11.exec` ; b4 `MacroInstance_402.Array Index` / `CallFunction_96.Index` ; b5 `MacroInstance_402.Completed` / `VariableSet_12.execute` ; b6 `VariableGet_52.Hairs` / `GetArrayItem_3.Array` ; b7 `CallFunction_133.ReturnValue` / `GetArrayItem_3.Dimension 1`. - `connect_pins` (18) : c1 `CallArrayFunction_10.then` → N1.execute ; c2 N1.then → `MacroInstance_402.Exec` ; c3 N4 → N1.TargetArray ; c4 `MacroInstance_19.Is Not Valid` → N2.execute ; c5 `CallArrayFunction_11.then` → N2.execute ; c6 N4 → N2.TargetArray ; c7 `MacroInstance_402.Array Index` → N2.NewItem ; c8 N4 → N5.TargetArray ; c9 N5.ReturnValue → N6.Max ; c10 N4 → N7.Array ; c11 N6.ReturnValue → N7.Dimension 1 ; c12 N7.Output → N3 (valeur) ; c13 `MacroInstance_402.Completed` → N3.execute ; c14 N3.then → `CallFunction_96.execute` ; c15 `CallFunction_96.then` → `VariableSet_12.execute` ; c16 N8 → `CallFunction_96.Index` ; c17 `VariableGet_896.HairstyleTable` → `GetArrayItem_3.Array` (même type de structure : `Hairs` reçoit des éléments de `HairstyleTable`, PEG:2092, 9254) ; c18 N8 → `GetArrayItem_3.Dimension 1`. - Inchangés : `IfThenElse_367` (filtre), `MacroInstance_19` (test), `CallArrayFunction_11` (ajout à `Hairs` des cheveux déjà chargés), `VariableSet_12` → `CallFunction_1405`. Devenus sans exécution : `MacroInstance_11`, `CallFunction_133`, `CallArrayFunction_14` (laissés en place, Q-H10-4). - Volume : +2 variables, +8 nœuds (3 684 → 3 692 dans `Person:EventGraph`), 7 `break_pins`, 18 `connect_pins`. - Comportement : un seul chargement par `RandomizeHairs` ; même loi de tirage (uniforme sur les éligibles) ; `Hairs` ne contient plus que les cheveux éligibles déjà chargés (R-A2) ; un cheveu tiré qui ne se charge pas donne `PickedHair = None` (R-A1 ; avant : exclu du tirage). - GI : aucun graphe modifié (appel existant à `LoadHairMeshIfNeeded`, lecture de `HairstyleTable`). **Option A1b — variante sans tableau d'index** (Person seul) : `MacroInstance_19.Is Not Valid` → `CallArrayFunction_11` (ajout des cheveux non chargés à `Hairs`) ; `Set PickedHairIdx = RandomInteger(Len(Hairs))` avant `VariableSet_12` et `GetArrayItem_3.Dimension 1` ← `PickedHairIdx` ; après `VariableSet_12` : Branche `IsValid(PickedHair)` → sinon `GI.getSkeletalMeshFromPath(BreakStruct_3.SkeletalMeshPath)` → `Set PickedHair` → `CallFunction_1405`. Moins de nœuds (+1 variable, +6 nœuds) mais le cache GI n'est pas rempli (rechargement si le paquet a été libéré, § 6) et `Hairs` contient des maillages vides (R-A2 aggravé). Spécification fine si retenue (Q-H10-1). **Option A2 — préchargement à l'entrée en jeu** (GI.EventGraph) : lancer une boucle du type `LoadBridgetAndHairs` sur les cheveux éligibles des PNJ derrière l'écran de chargement. Déplace ≤ 284,6 ms (et les cheveux pas encore chargés) hors du jeu ; tous les cheveux restent en mémoire (R-A4). **Non spécifiée** : point d'accroche à établir dans GEV (`LoadGame` GEV:7941, `NewGame` → `LoadGame` GEV:9194-9197, `DelayedStart` GEV:17614 ; fin du chargement non suivie). Dump présent (GEV), analyse à faire (fiche suivante). **Option A3 — chargement asynchrone** (sur A1, Person:EventGraph) : remplacer `CallFunction_96` par `Async Load Asset` (nœud latent, graphe d'événements seulement) sur `MakeSoftObjectPath(Conv_NameToString(SkeletalMeshPath))` → `Completed` : cast SkeletalMesh → `Array_Set` dans `GI.HairstyleTable` (cache) → `SetSkinnedAssetAndUpdate`. Effet visible : le PNJ s'affiche **sans cheveux** (ou avec ceux du mannequin précédent) pendant une ou plusieurs trames ; la suite de `RandomizeHairs` (`MacroInstance_6` matériaux PEG:8679-8685) s'exécute avant l'arrivée du maillage et doit être déplacée dans `Completed` (R-A3). Non spécifiée au nœud près (type_id du nœud latent et des conversions à relever) : Q-H10-1. **Option A4 — C++** (chargement asynchrone dans `LPAssetRegistryHelpers`) : hors MCP et hors `Source` pour ce lot ; citée seulement. ### 2.2 Volet B1 — `SavePreset` dans `DestroyPerson` **Constat** (établi sauf mention) : - `DestroyPerson` (GI.EventGraph, `CustomEvent_3172`, GEV:6327-6332) : IsValid (GEV:17765) → `ClearAllExpressions` (GEV:24412) → `SaveSlot > 0` (`IfThenElse_436`, GEV:12351-12360) → [`ReuseableInScene` → `removeAlreadyGot`] (GEV:18724-18733) → `BackupCbe` (GEV:27180) → `CharacterBeingEdited = Person` (GEV:6341-6346) → `CheckCbeGenderModel` (GEV:25928) → `IfThenElse_152` : `IsValid ET Valid ET Correct ET Len(PresetLoadedLite) == 0` (GEV:25937-25963) → **`SavePreset`** (`CallFunction_104`, File = `Saves/_session/Characters/` + Prefix + SaveSlot, Var 0, StatOnly false ; GEV:6333-6339, 6347-6359) → `RestoreCbe` (`CallFunction_937`, GEV:27184-27187) → `IfThenElse_1766` (`WithoutDestroying`, GEV:9488-9492) → sinon `removeFromKnownTemp` (GEV:25133-25137) → `destroyDummy(KeepAlreadyGot = NOT ReuseableInScene)` (GEV:23730-23735, 25071-25074). - `clearScenePersons` : pour chaque personne hors `Room.TempNpcs`, `WithoutDestroying = Interacting OU KnownNpcsHere[Prefix+SaveSlot] == personne OU Companion`, puis `DestroyPerson` (CSP:14-27, 28-98, 200-212). - `SavePreset` (503 nœuds) : sortie immédiate si fichier `…Characters\0.lpcharacter` (SP:1358-1368) ; si `PlayerCharacter != CBE` → `StripAll(NoUpdateForRevert=false)` (SP:1328-1345) → texte : infos, morphs du corps sur `MorphTargets` (SP:155-251, 1426-1432, 1582-1588), pénis, cheveux, peau, `BodyParts × Types` (SP:2210-2233) → `SaveTxt` (SP:10-16) → si `IncludeStat` : lignes `STAT` (SP:1294-1300, 1205-1211) → `SaveTxt` (SP:1278-1284) → si `PlayerCharacter != CBE` → **`RevertStripAll`** (`IfThenElse_2957`, `CallFunction_16039`, SP:1346-1357). `StatOnly` ne saute que l'apparence (SP:1158-1164) : `StripAll` et `RevertStripAll` restent. - `SavePreset` écrit l'état complet de la personne (apparence et statistiques) à **chaque** mise au pool d'un PNJ à `SaveSlot > 0`, pour le retrouver à la prochaine rencontre ; rien ne vérifie si elle a changé (aucune comparaison dans la chaîne ci-dessus). - `destroyDummy` ne détruit pas l'acteur : il le cache, appelle `ResetVars`, le téléporte à l'origine, `Valid = false` (DD:148-165, 249-254). `ResetVars` vide le corps (`ClearSMCs` → `ClearMorphTargets` → `SetSkinnedAssetAndUpdate`, `STE-H2b-DIAG.md` § 3.2) et vide `LastStripAll` (PEG:11942-11955 ; estimé : appartenance à la chaîne de `ResetVars` non suivie maillon par maillon). `destroyDummy` ne fait rien si `Dummy == AIDummy` (DD:282-294) ou si `isPlayer` (DD:135-142). → Pour un PNJ mis au pool, le `RevertStripAll` final rhabille un corps que `ResetVars` vide aussitôt (ev_60 : fin de `RevertStripAll` 362,112433 s, début de `destroyDummy` 362,112485 s) : 49,2 ms par personne (§ 1.2). **Option B1-a — pas de rhabillage avant mise au pool** (GI : EventGraph + SavePreset) - Variable : `add_variable(GI, "SaveBeforeDestroy", "bool")` (membre, défaut false). - `GI:EventGraph`, nœuds nouveaux (3) : D1 Branche (Condition ← `CustomEvent_3172.WithoutDestroying`) ; D2 `Set SaveBeforeDestroy` (`set_pin_value` true) ; D3 `Set SaveBeforeDestroy` (false). 1. `break_pins` `IfThenElse_152.then` / `CallFunction_104.execute` ; `connect_pins` `IfThenElse_152.then` → D1.execute ; D1.then (personne gardée) → `CallFunction_104.execute` ; D1.else (mise au pool) → D2.execute ; D2.then → `CallFunction_104.execute`. 2. `break_pins` `CallFunction_104.then` / `CallFunction_937.execute` ; `connect_pins` `CallFunction_104.then` → D3.execute ; D3.then → `CallFunction_937.execute`. - `GI:SavePreset`, nœuds nouveaux (2) : S1 Branche ; S2 lecture `SaveBeforeDestroy`. 3. `break_pins` `IfThenElse_2957.then` / `CallFunction_16039.execute` ; `connect_pins` `IfThenElse_2957.then` → S1.execute ; S2 → S1.Condition ; S1.else → `CallFunction_16039.execute` ; S1.then non relié (fin de fonction, comme `CallFunction_16039.then` vide, SP:1356). - Volume : +1 variable membre ; EventGraph +3 nœuds ; SavePreset 503 → 505 nœuds ; 3 `break_pins`, 9 `connect_pins`. - Comportement : autres appelants de `SavePreset` (GEV:8385 `CallFunction_16676`, GEV:17888 `CallFunction_8085` ; appelants hors GEV non inventoriés) : drapeau false → inchangés. `WithoutDestroying = true` (cas ev_74) → inchangé. Le drapeau est remis à false après chaque `SavePreset`, y compris sortie anticipée (SP:1364-1368). - GI touché : `EventGraph` (chaîne `DestroyPerson` : `IfThenElse_152`, `CallFunction_104`, `CallFunction_937`) et `SavePreset` (`IfThenElse_2957`, `CallFunction_16039`). **Option B1-b — même effet par un paramètre de `SavePreset`** : écartée ; aucun outil d'ajout de paramètre de fonction (§ 6). **Option B1-c — `StatOnly = true` sur `CallFunction_104`** (1 `set_pin_value`, GI.EventGraph) : saute morphs et première sauvegarde, ≤ 24 ms par appel (143,6 ms / 6, § 1.2, estimé) ; garde `StripAll` et `RevertStripAll`. Risque fort : changements d'apparence de la scène non sauvegardés ; premier enregistrement d'un PNJ (pas de fichier) → `LoadTxt` vide → fichier réduit aux lignes `STAT` (SP:1172, 1205-1211) → apparence perdue (R-B3). Non recommandée ; Q-H10-7. **Option B1-d — ne sauvegarder que si la personne a changé** : demande un drapeau posé par chaque graphe Person qui modifie l'état (`ModifyStat`, `Dress`, `BodyChange`, `CumOn`, …, liste non établie) : non spécifiable sans improvisation ; Q-H10-7. **Option B1-e — différer `SavePreset`** : écartée ; la personne est vidée par `ResetVars` dans la même trame (DD:148-165), une sauvegarde différée lirait un corps vide (estimé). ### 2.3 Volet B2 — `IsGenitalExposed` → `GetSMBoolForSMC_Cloth` **Constat** (établi) : - `ShowAppropriateGenital` → (non créature) `IsGenitalExposed` (SAG:46-55, 77-87). - `IsGenitalExposed` : `GetChildrenComponents(Mesh)` → pour **chaque** enfant : cast SkeletalMeshComponent → `GI.GetSMBoolForSMC_Cloth(SMC)` → `Strings` contient `Bottom` / `Bottom_Under` / `Top` (IG:9-68, 100-108) ; `Break` non relié (IG:64) : tous les enfants sont parcourus. - `GetSMBoolForSMC_Cloth` : boucle sur **toute** `ClothTable` (GC:10-20) ; à chaque itération les nœuds purs sont réévalués : `GetSkeletalMesh(SMC)` → `GetObjectName` → `Concat(nom, ".", nom)` (GC:36-45, 56-59), `Conv_NameToString(SkeletalMeshPath)` → `EndsWith` (GC:46-55) ; sortie par `FunctionResult` au premier trouvé (GC:31-35), parcours complet pour un enfant qui n'est pas un vêtement. 190 079 `Array_Get` pour 309 appels dans les 8 trames (H9 § 4) : ≈ 615 itérations par appel (estimé). **Option B2-a — suffixe calculé une fois** (`GI:GetSMBoolForSMC_Cloth`) - Variable locale : `add_variable(GI, "MeshSuffix", "string", graph=GetSMBoolForSMC_Cloth)`. - Nœuds nouveaux (2) : C1 `Set MeshSuffix` (valeur ← `CommutativeAssociativeBinaryOperator_0.ReturnValue`) ; C2 lecture `MeshSuffix`. 1. `break_pins` `FunctionEntry_2157.then` / `MacroInstance_1872.Exec` ; `connect_pins` `FunctionEntry_2157.then` → C1.execute ; C1.then → `MacroInstance_1872.Exec` ; `CommutativeAssociativeBinaryOperator_0.ReturnValue` → C1 (valeur). 2. `break_pins` `CommutativeAssociativeBinaryOperator_0.ReturnValue` / `CallFunction_2.InSuffix` ; `connect_pins` C2 → `CallFunction_2.InSuffix`. - Volume : +1 variable locale, 11 → 13 nœuds, 2 `break_pins`, 4 `connect_pins`. - Équivalence : `SMC` est une entrée de la fonction, non modifiée dans la boucle : même suffixe à chaque itération (GC:4-6, 56-59). - GI touché : `GetSMBoolForSMC_Cloth` seulement. **Option B2-b — cache `Map` suffixe → index de `ClothTable`** (GI) : `add_variable(GI, "ClothIdxBySuffix", "int", container_type=MAP)` ; `Map_Find` avant la boucle, `Map_Add` au premier trouvé. Gain supérieur (recherche directe après le premier appel) ; risque d'index périmé si `ClothTable` est reconstruite ou réordonnée (R-B5) ; point d'invalidation inconnu ; type_id des nœuds de Map à relever : non spécifiée, Q-H10-8. GI touché : `GetSMBoolForSMC_Cloth` (+ variable membre). **Option B2-c — ignorer les enfants qui ne sont pas des vêtements** (Person.IsGenitalExposed, ex. `HairSM`, `PenisSM`) : demande la liste des composants de Person attachés à `Mesh` (non établie) et des comparateurs d'objets (lp-mcp § 3 : les comparateurs ne sortent pas de `find_node_types`) : non spécifiée, Q-H10-8. GI : aucun. ### 2.4 Volet B3 — `Dress` → `ClearProxies` (Person) **Constat** (établi) : `ClearProxies(Slots)` recopie à chaque appel `TotalTable = GI.OtherTable` + `Bdg_ClothTable` + `ClothTable` (CP:28-33, 112-122, 155-163) ; pour chaque enfant de `Mesh` (CP:10-27), boucle sur `TotalTable` avec le même suffixe recalculé à chaque itération (CP:135-154, 176-179) ; trouvé → slots → `ToDestroy` → `Strip` (CP:57-129). **Option B3-a — suffixe calculé une fois par enfant** (`Person:ClearProxies`) - Variable locale : `add_variable(Person, "ProxySuffix", "string", graph=ClearProxies)`. - Nœuds nouveaux (2) : P1 `Set ProxySuffix` (valeur ← `CommutativeAssociativeBinaryOperator_0.ReturnValue`) ; P2 lecture `ProxySuffix`. 1. `break_pins` `DynamicCast_30.then` / `MacroInstance_301.Exec` ; `connect_pins` `DynamicCast_30.then` → P1.execute ; P1.then → `MacroInstance_301.Exec` ; `CommutativeAssociativeBinaryOperator_0.ReturnValue` → P1 (valeur). 2. `break_pins` `CommutativeAssociativeBinaryOperator_0.ReturnValue` / `CallFunction_2.InSuffix` ; `connect_pins` P2 → `CallFunction_2.InSuffix`. - Volume : +1 variable locale, 37 → 39 nœuds, 2 `break_pins`, 4 `connect_pins`. `CallFunction_3` (`GetSkeletalMesh`) lit `DynamicCast_30.AsSkeletal Mesh Component`, valide quand P1 s'exécute (CP:21-27, 176-179). - La recopie des trois tables à chaque appel (part de l'exclusif 390,5 ms, ST:152) n'est pas traitée : Q-H10-9. GI : aucun. ### 2.5 Volet B4 — interface d'actions (`Current`, `ShowUI`, `CopyActionsToScroll`) **Constat** : - `TopRight.Current(Amenity)` (`CustomEvent_5`, TR:3638-3641) : selon `Amenity`, `StreetMap_Static.ForceHover` → `OpenLeftMouse` → `LeftMouseInput(FromTopRight=true)` → `GI.CopyActionsToScroll(ScrollBox_0, RemoveFirst=3)` → `PC.Exit Action Menu` → `UpdateScrollColor` (TR:3653-3706) ; autres valeurs → `OpenLeftMouse` → … → `CopyActionsToScroll(RemoveFirst=0)` (TR:3759-3795) (établi). Les widgets `Action_C` sont créés par le clic simulé sur la carte (`StreetMap_Static`, non dumpé ; H9 § 5 : `ActualLeftMouseInput` 168 574 `Array_Get`, 295 ms) (estimé). - `CopyActionsToScroll` **ne crée pas** de widget : `ClearChildren(Scroll)`, puis déplace jusqu'à 100 enfants de `Cursor.ActionBox` vers `Scroll` par `AddChild` (trois libellés retirés), remplit `CurrentDailies` (CA:4-159, établi). `c` est une variable locale (lecture sans broche `self`, CA:77-78, 89-90 ; estimé). - Appelants de `Current` dans GI.EventGraph : `CallFunction_400`, `_409` (Amenity = `CurrentAmenity`), `_798`, `_799`, `_812` (GEV:21575-21586, 25474-25486, 25647-25651, établi). 16 appels dans 8 trames (H9 § 4) : 2 par trame en moyenne (répartition par trame non relevée, incertain). - `ShowUI` (`CustomEvent_212`, GEV:729-731) → … → `CopyActionsToScroll` (H9 § 4) : chemin non suivi dans GEV (point à confirmer). **Option B4-a — regrouper les `Current` d'une même trame** (`TopRight:EventGraph` seul) - Nœud nouveau (1) : T1 `DelayUntilNextTick` (latent, § 6 ; type_id par `find_node_types {graph: TopRight:EventGraph, type_id_filter:"NextTick"}`). 1. `break_pins` `CustomEvent_5.then` / `SwitchString_1.execute` ; `connect_pins` `CustomEvent_5.then` → T1.execute ; sortie d'exécution de T1 → `SwitchString_1.execute`. - Volume : 906 → 907 nœuds, 1 `break_pins`, 2 `connect_pins`. - Comportement : reconstruction une trame plus tard ; un second `Current` dans la même trame, pendant que T1 attend, ne relance pas l'action latente et l'exécution unique lit le dernier `Amenity` (incertain : à lire dans `KismetSystemLibrary.cpp`, `DelayUntilNextTick`, avant IMPL). Les appelants GI qui enchaînent juste après `Current` (`CallFunction_696`, `IfThenElse_1302`, `CallArrayFunction_81`, `IfThenElse_153`, `IfThenElse_20`) verraient l'ancienne liste (R-B4). - GI : aucun. **Option B4-b — ne reconstruire la liste que si elle change / alléger sa génération** : demande le graphe qui crée les widgets (`StreetMap_Static`) : **dump manquant D10-1** (§ 4) ; arrêt sur ce point. ### 2.6 Graphes GI touchés, par option (GI partagé avec CAM : sous-lots B1, B4, B5) | Option | Graphes de GI modifiés | Variables GI ajoutées | Autres Blueprints | |---|---|---|---| | A1 | aucun | aucune | `Person:EventGraph` (+2 variables) | | A1b | aucun | aucune | `Person:EventGraph` (+1 variable) | | A2 | `EventGraph` (fin de `LoadGame` / `DelayedStart`, à établir) | aucune (estimé) | — | | A3 | aucun (estimé) | aucune | `Person:EventGraph` | | B1-a | `EventGraph` (chaîne `DestroyPerson`) ; `SavePreset` | `SaveBeforeDestroy` (bool, membre) | — | | B1-c | `EventGraph` (`CallFunction_104`, une valeur) | aucune | — | | B2-a | `GetSMBoolForSMC_Cloth` | `MeshSuffix` (locale à la fonction) | — | | B2-b | `GetSMBoolForSMC_Cloth` | `ClothIdxBySuffix` (Map, membre) | — | | B2-c | aucun | aucune | `Person:IsGenitalExposed` | | B3-a | aucun | aucune | `Person:ClearProxies` (+1 locale) | | B4-a | aucun | aucune | `TopRight:EventGraph` | Grep des noms `SavePreset|GetSMBoolForSMC_Cloth|CopyActionsToScroll|LoadHairMeshIfNeeded|getSkeletalMeshFromPath|DestroyPerson|ShowUI|ClearProxies|RandomizeHairs|IsGenitalExposed` sur `etat\CAM\*.md` : 0 ligne (aucun recouvrement nommé ; la liste des graphes GI de CAM reste à confronter, Q-H10-12). `GI.uasset` est binaire : quel que soit le graphe, une seule écriture de GI à la fois (instantané, commit, puis l'autre lot). ### 2.7 Ordre proposé (si plusieurs options sont retenues) 1. C0 (`STE-H4-C0-SPEC.md`, GI.destroyDummy), déjà décidé. 2. Options sans GI (A1, B3-a, B4-a) : Person puis TopRight, un fichier à la fois. 3. Options GI (B1-a, B2-a) dans une seule session GI, à la place fixée avec CAM. Une trace de mesure après chaque volet ou une seule à la fin : Q-H10-13. ## 3. Repli Avant commit : `.\Tools\Restore.ps1 -Lot STE-H10` éditeur fermé, MD5 = P2. Après commit : `git revert` du commit de l'option (un commit par option). Aucune donnée de partie n'est convertie : `SavePreset` écrit le même texte (B1-a ne touche que le rhabillage après écriture). ## 4. Risques R#, questions Q# ### Risques - **R-A1** (A1, A1b) : un cheveu tiré dont le chemin ne se charge pas donne un PNJ sans cheveux (avant : exclu du tirage). - **R-A2** (A1, A1b) : `Hairs` change de contenu ; il est lu ailleurs dans Person (`VariableSet_287` PEG:2224, `VariableGet_3269` → `MacroInstance_655` PEG:2355, `VariableGet_198` → `CallArrayFunction_48` PEG:12377) : effet à vérifier. - **R-A3** (A3) : cheveux absents quelques trames ; ordre d'exécution changé (matériaux appliqués avant le maillage). - **R-A4** (A2) : tous les cheveux éligibles résidents en mémoire ; chargement d'entrée plus long. - **R-B1** (B1-a) : `destroyDummy` sans effet si `Person == AIDummy` ou `isPlayer` (DD:135-142, 282-294) : la personne resterait déshabillée et visible (le joueur ne passe pas par `StripAll` : `PlayerCharacter != CBE`, SP:1328-1332). - **R-B2** (B1-a) : si `ResetVars` ne vide pas `LastStripAll` (estimé qu'il le vide, PEG:11942-11955), un mannequin réutilisé pourrait rhabiller d'anciens vêtements dans `clearScenePersons` (CSP:118-134). - **R-B3** (B1-c) : apparence perdue ou non sauvegardée. - **R-B4** (B4-a) : un appelant de `Current` lit la liste avant reconstruction ; affichage en retard d'une trame. - **R-B5** (B2-b) : index de `ClothTable` périmé → mauvais vêtement → mauvais état « exposé ». - **R-G** : GI binaire partagé avec CAM : conflit d'écriture si deux lots écrivent GI sans ordre. - **R-M** : gains non additifs (B1-a contient une part de B2 / B3) ; la trace de contrôle fait foi. ### Questions pour l'utilisateur (aucune n'est tranchée ici) - **Q-H10-1** (A) : quelle option pour les cheveux : A1 (tirer puis charger, Person seul), A1b, A2 (préchargement, à spécifier), A3 (asynchrone, cheveux absents quelques trames), ou aucune (coût de session 284,6 ms, concentré au début) ? - **Q-H10-2** (A1) : si le cheveu tiré ne se charge pas : accepter un PNJ sans cheveux, ou ajouter un nouveau tirage (nœuds en plus) ? - **Q-H10-3** (A1) : examiner avant IMPL les autres lecteurs de `Hairs` (PEG:2224, 2355, 12377 ; dump présent) ? - **Q-H10-4** (A1) : nœuds devenus sans exécution (`MacroInstance_11`, `CallFunction_133`, `CallArrayFunction_14`) : laisser ou supprimer ? - **Q-H10-5** (B1) : retenir B1-a (pas de rhabillage d'un PNJ mis au pool juste après sa sauvegarde) ? - **Q-H10-6** (B1-a) : cas `AIDummy` : ajouter une garde `Person != AIDummy` (comparateur à créer, lp-mcp § 3) ou accepter R-B1 ? - **Q-H10-7** (B1-c, B1-d) : étudier `StatOnly` ou « sauvegarder seulement si changé » malgré R-B3 et le nombre de graphes touchés ? - **Q-H10-8** (B2) : B2-a seule (sûre), B2-b (cache, plus de gain, R-B5), B2-c (liste des composants à établir) ? - **Q-H10-9** (B3) : retenir B3-a ; étudier la recopie des trois tables à chaque `ClearProxies` ? - **Q-H10-10** (B4-a) : accepter une liste d'actions mise à jour une trame plus tard ? - **Q-H10-11** (B4-b) : lancer le dump D10-1 de `StreetMap_Static` pour alléger la génération des actions et `ShowUI` ? - **Q-H10-12** (GI / CAM) : ordre des écritures de GI entre STE (B1-a, B2-a) et CAM (B1, B4, B5) ; une seule session GI pour STE ? - **Q-H10-13** (mesure) : une trace Insights après chaque volet, ou une seule après l'ensemble ? ### Dumps manquants (commande exacte ; arrêt sur le point concerné) - **D10-1** (B4-b) : `.\Tools\RunCommandlet.ps1 -Lot STE-D10 -Commandlet LPDumpFunction -Args '-BP=StreetMap_Static -Fn=ActualLeftMouseInput -Pins -Exec' -EtatDir "D:\Game\LifePlay-master 5.8\Docs\Migration\etat\ST\STE"` (si `ActualLeftMouseInput` est un événement et non une fonction : même commande avec `-Fn=EventGraph`). - A2 et le chemin `ShowUI` → `CopyActionsToScroll` : dumps présents (GEV) ; il manque l'analyse, pas un dump. ## 5. Interdits - Rien hors des graphes listés pour l'option retenue ; pas de nouvelle variable hors celles nommées ; pas de modification de `HairstyleTable`, `ClothTable`, `OtherTable`, ni du texte écrit par `SavePreset`. - Pas de macro `IsValid` à exécution ajoutée (convention H3) ; pas de nœud latent hors A3 / B4-a ; pas de C++ (`Source`) dans ce lot. - lp-mcp § 1 : HyperAIStudio lit, outils Epic écrivent ; instantané avant écriture ; preuve en build empaqueté. - Aucun effet visible non accepté (Q-H10-1, Q-H10-10) ; sk-protocol § 2. ## 6. Valeurs moteur lues dans le moteur installé (`D:\Game\Launcher\Engine\UE_5.8`) - `UAssetRegistryHelpers::GetAsset` → `InAssetData.GetAsset()` (`Engine/Source/Runtime/AssetRegistry/Private/AssetRegistryHelpers.cpp:87-89`). - `FAssetData::GetAsset` → `FastGetAsset(true)` (`Engine/Source/Runtime/CoreUObject/Public/AssetRegistry/AssetData.h:573-577`) : paquet en mémoire et chargé → `FindObjectFast` (AssetData.h:532-538, 560-563) ; sinon `LoadPackage` synchrone (AssetData.h:540-544), d'où le `FlushAsyncLoading` de H9 § 3. - `UKismetSystemLibrary::DelayUntilNextTick(WorldContextObject, LatentInfo)`, `UFUNCTION(BlueprintCallable, Category = "Utilities|FlowControl", meta = (Latent, …))` (`Engine/Source/Runtime/Engine/Classes/Kismet/KismetSystemLibrary.h:681-682`). - `BlueprintTools.add_variable(blueprint, name, type_name, graph=None, container_type=None)` : membre, ou locale si `graph` ; types `bool`, `int`, `float`, `byte`, `string`, `name`, `text`… ; `MAP` à clé string (`Engine/Plugins/Experimental/Toolsets/EditorToolset/Content/Python/editor_toolset/toolsets/blueprint.py:1154-1185`). - Aucun outil d'ajout de paramètre de fonction : Grep `def (add|create)_\w*variable|def add_function_(input|parameter)|def add_local` sur `Engine/Plugins/**/*.py` → `add_variable` (1154), `add_struct_variable` (1189), `add_object_variable` (1224) et deux outils ControlRig, aucun ajout de paramètre. ## 7. Critères de VAL (commande → attendu) Dumps après correctif (`.\Tools\RunCommandlet.ps1 -Lot STE-H10V -Commandlet LPDumpFunction -Args '' -EtatDir "D:\Game\LifePlay-master 5.8\Docs\Migration\etat\ST\STE"`) : - A1 : `-BP=Person -Fn=EventGraph -Pins -Exec` → 3 692 nœuds ; liens du § 2.1 (b1-b7 absents, c1-c18 présents) ; `CallFunction_96` exécuté seulement depuis N3. - B1-a : `-BP=GI -Fn=SavePreset -Pins -Exec` → 505 nœuds, `IfThenElse_2957.then → S1`, `S1.else → CallFunction_16039` ; `-BP=GI -Fn=EventGraph -Pins -Exec` → +3 nœuds, chaîne `IfThenElse_152 → D1 → (CallFunction_104 | D2 → CallFunction_104) → D3 → CallFunction_937`. - B2-a : `-BP=GI -Fn=GetSMBoolForSMC_Cloth -Pins -Exec` → 13 nœuds ; `CallFunction_2.InSuffix ← C2`. - B3-a : `-BP=Person -Fn=ClearProxies -Pins -Exec` → 39 nœuds ; `CallFunction_2.InSuffix ← P2`. - B4-a : `-BP=TopRight -Fn=EventGraph -Pins -Exec` → 907 nœuds ; `CustomEvent_5.then → T1 → SwitchString_1`. - Toutes : `CompileAllBlueprints` 418 / 418, 0 erreur, 0 avertissement ; `LPTestStringCompat` 3120 / 0 / 60 (lp-validate) ; `git diff --stat` = fichiers de l'option seulement ; MD5 des fichiers non visés inchangés. Preuve en jeu : nouvelle trace Insights, même lancement que H9 (`-trace=cpu,frame,bookmark,loadtime -statnamedevents -LPHitchLog`), même parcours J5, même analyse (`stats_gt.csv`, `lf_*.csv`, `ev_*.csv`) : - A1 : `LoadHairMeshIfNeeded` en baisse (appels et inclusif, H9 : 108 ; 284,6 ms) ; aucune trame avec plus de 2 chargements de cheveux à la suite ; PNJ coiffés (captures). - B1-a : dans chaque `DestroyPerson` suivi d'un `destroyDummy`, plus de `RevertStripAll` sous `SavePreset` (EV) ; `SavePreset` moyen ≈ 53 → ≈ 25 à 35 ms ; à la scène suivante, un PNJ réutilisé apparaît habillé (preuve visuelle et journal). - B2-a, B3-a : `GetSMBoolForSMC_Cloth`, `ClearProxies` inclusif en baisse d'au moins 25 % ; états « exposé » identiques (captures avant / après d'une même scène). - B4-a : au plus un `Current` par trame de fin de scène ; liste d'actions correcte (captures). - Global : nombre et durée des trames ≥ 50 ms par catégorie (H9 : 120 trames, 18,6 s ; Suivant 18 trames, 4,1 s ; sans clic 78 trames, 10,8 s). ## Complément d'IMPL (décisions du 2026-10-11) Décisions de l'utilisateur, transmises par l'orchestrateur : cheveux = **A1** ; fin de scène = **B1-a, B2-a, B3-a, B4-a** ; preuve = **une seule trace Insights à la fin**, même parcours que STE-H9. Ce complément **remplace**, pour l'IMPL, les étapes MCP de A1 (§ 2.1), B1-a (§ 2.2) et B4-a (§ 2.5), ainsi que les comptes du § 7 pour ces options. Les lignes d'origine restent pour la traçabilité. B2-a (§ 2.3) et B3-a (§ 2.4) sont repris tels quels. Questions closes : Q-H10-1, 5, 8, 9, 10 et 13 par décision ; Q-H10-2, 3, 4 et 6 par les preuves du § C.1. Restent ouvertes : Q-H10-12 (ordre GI avec CAM) et Q-H10-14 (§ C.9). ### C.1 Points tranchés par les preuves 1. **A1, repli (Q-H10-2)** : nouveau tirage parmi les index restants, jusqu'au succès ou à épuisement. Le cheveu qui ne se charge pas est retiré de `HairIdxs`, puis on retire au sort. Résultat (estimé, raisonnement) : le tirage reste uniforme sur les cheveux éligibles **chargeables**, comme aujourd'hui, où les cheveux non chargés sont exclus de `Hairs` (PEG:9256-9260). Cela couvre aussi les cheveux que `LoadHairMeshIfNeeded` ne charge pas (non Daz et `LegacyPercentage ≤ 0`, LH:63-80) : ils sont rejetés au tirage, comme ils sont exclus aujourd'hui. Si aucun cheveu n'est chargeable, `PickedHair = None`, comme aujourd'hui (`GetArrayItem` sur un `Hairs` vide). La boucle est bornée : chaque tour retire un index ou termine (par construction). Le « retour au chemin d'origine » est écarté : il faudrait dupliquer la boucle complète (plus de nœuds pour le même résultat). 2. **A1, autres lecteurs de `Hairs` (Q-H10-3)** : Grep `variable 'Hairs'` sur tous les dumps (`dumps\*.txt`) ne trouve que PEG. Aucun dump de GI ne lit `Hairs`. - `VariableSet_287` (PEG:2224-2227) écrit `Hairs = OtherTable` après la boucle `LoadOtherMeshIfNeeded_0` (`MacroInstance_12`, PEG:9478-9484). Le `Array_Shuffle` (`CallArrayFunction_901`, PEG:2230-2234) et le seul autre lecteur, `MacroInstance_655` (PEG:2243-2250, via `VariableGet_3269` PEG:2355), suivent cette écriture. Le second passage (`SecondTurn = true`, `VariableSet_2572` PEG:2336-2338, après `MacroInstance_655.Completed` PEG:2319-2323) relit la même copie d'`OtherTable`. - `VariableGet_198` → `CallArrayFunction_48` (PEG:12377-12384) vide `Hairs` : c'est une écriture. - Le seul lecteur du résultat de `RandomizeHairs` est donc le tirage lui-même (`GetArrayItem_3`, `CallArrayFunction_14`). **Changer le contenu de `Hairs` n'a pas d'effet** (établi pour PEG). Les 133 autres graphes de Person ne sont pas dumpés : estimé sans effet, `Hairs` sert de tableau de travail. 3. **A1, nœuds devenus orphelins (Q-H10-4)** : `MacroInstance_11`, `CallFunction_133` et `CallArrayFunction_14` restent en place, sans suppression. Des nœuds sans exécution ou purs non reliés ne bloquent pas la compilation (estimé). Si `compile_blueprint` les cite en **erreur** : arrêt, rapport, décision de l'utilisateur. 4. **B1-a, garde du cas `AIDummy` (Q-H10-6)** : le risque est réel. `AIDummy` reçoit un vrai `SaveSlot` (`Conv_StringToInt`, GEV:24953-24962) et un preset de `Saves/_session/Characters/` (GEV:24931-24947). `destroyDummy` ne fait rien pour lui (DD:282-294). Une garde par comparaison `Person == AIDummy` est impossible à créer (lp-mcp § 3 : les comparateurs ne sortent pas de `find_node_types`). La garde retenue est un **contrôle a posteriori** sans comparateur, fondé sur un fait établi : un mannequin remis au pool a `Valid = false` en fin de `destroyDummy` (DD:249-254 ; ordre conservé par C0, `STE-H4-C0-SPEC.md`:15). - Si le rhabillage a été sauté (`RevertSkipped`) et que la personne est encore `Valid` après `destroyDummy`, on appelle `Person.RevertStripAll`. - Le cas `isPlayer` n'a pas besoin de garde : `SavePreset` ne déshabille pas le joueur (SP:1328-1345). - Nœuds exacts : § C.5, G1 à G4. 5. **B4-a, deux appels latents dans la même trame (établi)** : - `DelayUntilNextTick` ignore un second appel tant que le premier attend (même `CallbackTarget` et même `UUID`, KismetSystemLibrary.cpp:2643-2646). Mais il peut se déclencher **dans la même trame moteur** : `FDelayUntilNextTickAction` (DelayAction.h:46-49, 66-70), CVar `LatentActions.GuaranteeNextTickDelay` = 0 par défaut (LatentActionManager.cpp:20-24). Il ne garantit donc ni le report, ni le regroupement de tous les appels de la trame. - **Forme corrigée : `DelayUntilNextFrame`**. Il est `BlueprintCallable` et latent (KismetSystemLibrary.h:689-690). Un second appel en attente est ignoré (KismetSystemLibrary.cpp:2650-2657). L'action ne finit qu'après l'avancée du compteur de trames (`FDelayUntilNextFrameAction`, DelayAction.h:82-94). - Le regroupement marche : tous les `Current` d'une même trame donnent **une** reconstruction à la trame suivante. - Estimé : comme aujourd'hui, l'état final est celui du dernier appel. Le paramètre `Amenity` d'un événement d'ubergraph est conservé dans la trame persistante et relu à la reprise ; contrôle en VAL (§ C.7). ### C.2 Ordre d'écriture - **E0** (une fois) : P1, P4 et un instantané par fichier, MD5 = valeur commitée au moment du GO. Pour GI, c'est la valeur **après C0** (et après CAM si Big Boss place CAM avant). Commandes : - `.\Tools\Snapshot.ps1 -Lot STE-H10 -Path Content\UI\TopRight.uasset` - `.\Tools\Snapshot.ps1 -Lot STE-H10 -Path Content\BPs\Person.uasset` - `.\Tools\Snapshot.ps1 -Lot STE-H10 -Path Content\BPs\GI.uasset` - **E1 TopRight** : B4-a (§ C.3), compile, `save_assets /Game/UI/TopRight`. - **E2 Person** : A1 (§ C.4), puis B3-a (§ 2.4), compile, `save_assets /Game/BPs/Person`. - **E3 GI** : B1-a (§ C.5), puis B2-a (§ 2.3), compile, `save_assets /Game/BPs/GI`. Seulement après C0 et à la place fixée avec CAM (Q-H10-12). - **Contrôle de chaque nœud créé** : avant `create_node`, `find_node_types` avec le filtre indiqué ; après, `get_node_infos` (broches, types) ; tout écart → arrêt. - **Commits** : A1 et B3-a partagent `Person.uasset`, B1-a et B2-a partagent `GI.uasset` (binaires). Le « un commit par option » des § 2.0 et § 3 devient **un commit par asset** (3 commits, ticket au gardien). Le repli se fait par asset (Q-H10-14). ### C.3 E1 — TopRight : B4-a (forme corrigée) - T1 : `find_node_types {graph:{refPath:"/Game/UI/TopRight.TopRight:EventGraph"}, type_id_filter:"NextFrame", context_pins:[]}`, puis choisir l'entrée `DelayUntilNextFrame` et faire `create_node`. **Ne pas** prendre `DelayUntilNextTick`. - 1 `break_pins` : `CustomEvent_5.then` / `SwitchString_1.execute` (TR:3638-3640). - 2 `connect_pins` : `CustomEvent_5.then` → T1.execute ; T1.then → `SwitchString_1.execute`. - Volume : `TopRight:EventGraph` 906 → 907 nœuds ; 1 coupure, 2 liens ; aucune variable. ### C.4 E2 — Person : A1 (forme finale, avec repli) Graphe `/Game/BPs/Person.Person:EventGraph`, événement `RandomizeHairs`. Variables membres : - `add_variable(Person, "HairIdxs", "int", container_type=ARRAY)` ; - `add_variable(Person, "PickedHairIdx", "int")` ; - `add_variable(Person, "HairSearch", "bool")`. Nœuds nouveaux (18) : | Id | Nœud | Filtre `find_node_types` | Réglage | |---|---|---|---| | N1 | `Array_Clear` | `Tableau` | — | | N2 | `Array_Add` | `Tableau` | — | | N4 | lecture `HairIdxs` (une seule, plusieurs sorties) | `HairIdxs` | — | | N5 | `Array_Length` | `Tableau` | — | | N6 | `RandomInteger` | `RandomInteger` | — | | N7 | `GetArrayItem` (Get) | `Tableau` | — | | N3 | `Set PickedHairIdx` | `PickedHairIdx` | — | | N8 | lecture `PickedHairIdx` | `PickedHairIdx` | — | | V1 | `Set PickedHair` | `PickedHair` | valeur non reliée (None) | | V2 | `Set HairSearch` | `HairSearch` | `set_pin_value` true | | GH | lecture `HairSearch` | `HairSearch` | — | | W | macro `WhileLoop` | `WhileLoop` | — | | B1 | Branche | `Branche` | — | | IVI | `IsValidIndex` | `Utilitaires|Tableau|IsValidIndex` (lp-mcp § 3) | Index = 0 | | B2 | Branche | `Branche` | — | | IV | `IsValid` pure (`KismetSystemLibrary::IsValid`, broches `Object` / `ReturnValue` comme GEV:25933-25936) | `IsValid` (pas la macro, convention H3) | — | | V3 | `Set HairSearch` | `HairSearch` | false (une seule instance, deux entrées) | | R | `Array_RemoveItem` | `Tableau` | — | `break_pins` (8) : - b1 `CallArrayFunction_10.then` / `MacroInstance_402.Exec` - b2 `MacroInstance_19.Is Not Valid` / `CallFunction_96.execute` - b3 `CallFunction_96.then` / `MacroInstance_11.exec` - b4 `MacroInstance_402.Array Index` / `CallFunction_96.Index` - b5 `MacroInstance_402.Completed` / `VariableSet_12.execute` - b6 `VariableGet_52.Hairs` / `GetArrayItem_3.Array` - b7 `CallFunction_133.ReturnValue` / `GetArrayItem_3.Dimension 1` - b8 `VariableSet_12.then` / `CallFunction_1405.execute` `connect_pins` (34) — boucle de collecte : - c1 `CallArrayFunction_10.then` → N1.execute ; c2 N1.then → `MacroInstance_402.Exec` ; c3 N4 → N1.TargetArray ; - c4 `MacroInstance_19.Is Not Valid` → N2.execute ; c5 `CallArrayFunction_11.then` → N2.execute ; c6 N4 → N2.TargetArray ; - c7 `MacroInstance_402.Array Index` → N2.NewItem. `connect_pins` — tirage avec repli : - c8 `MacroInstance_402.Completed` → V1.execute ; c9 V1.then → V2.execute ; c10 V2.then → W.execute ; c11 GH → W.Condition ; - c12 W.LoopBody → B1.execute ; c13 N4 → IVI.TargetArray ; c14 IVI.ReturnValue → B1.Condition ; c15 B1.else → V3.execute ; - c16 B1.then → N3.execute ; c17 N4 → N5.TargetArray ; c18 N5.ReturnValue → N6.Max ; c19 N4 → N7.Array ; - c20 N6.ReturnValue → N7.Dimension 1 ; c21 N7.Output → N3 (valeur) ; c22 N3.then → `CallFunction_96.execute` ; - c23 N8 → `CallFunction_96.Index` ; c24 `CallFunction_96.then` → `VariableSet_12.execute` ; - c25 `VariableGet_896.HairstyleTable` → `GetArrayItem_3.Array` ; c26 N8 → `GetArrayItem_3.Dimension 1` ; - c27 `VariableSet_12.Output_Get` → IV.Object ; c28 `VariableSet_12.then` → B2.execute ; c29 IV.ReturnValue → B2.Condition ; - c30 B2.then → V3.execute ; c31 B2.else → R.execute ; c32 N4 → R.TargetArray ; c33 N8 → R.Item ; - c34 W.Completed → `CallFunction_1405.execute`. Résultat : `RandomizeHairs` ne charge plus que le cheveu tiré, plus d'éventuels rejets. `PickedHair` est lu dans `GI.HairstyleTable` à l'index tiré, après chargement (`GetArrayItem_3` est pur et s'évalue au `VariableSet_12`). `CallFunction_1405` et la suite (`MacroInstance_6`, …) sont inchangés. Restent sans exécution : `MacroInstance_11`, `CallFunction_133`, `CallArrayFunction_14` (§ C.1-3). Volume A1 : +3 variables, `Person:EventGraph` 3 684 → 3 702 nœuds, 8 coupures, 34 liens, 2 `set_pin_value`. Puis **B3-a** sur `/Game/BPs/Person.Person:ClearProxies` : étapes du § 2.4 sans changement. Volume : +1 variable locale `ProxySuffix`, 37 → 39 nœuds, 2 coupures, 4 liens. ### C.5 E3 — GI : B1-a (avec garde), puis B2-a Variables membres : - `add_variable(GI, "SaveBeforeDestroy", "bool")` ; - `add_variable(GI, "RevertSkipped", "bool")`. `/Game/BPs/GI.GI:EventGraph`, chaîne `DestroyPerson`. Nœuds nouveaux (9) : - D1 Branche ; - D2 `Set SaveBeforeDestroy` (true) ; D3 `Set SaveBeforeDestroy` (false) ; - G1 Branche ; GR lecture `RevertSkipped` ; - G2 Branche ; GV lecture `Valid` de Person (`find_node_types` filtre `Valid` avec `context_pins:[CustomEvent_3172.Person]`, comme `VariableGet_599` GEV:25944-25946) ; - G3 appel `Person.RevertStripAll` (filtre `RevertStripAll`, contexte Person ; même appel que SP:1354) ; - G4 `Set RevertSkipped` (false). Coupures (3) : - `IfThenElse_152.then` / `CallFunction_104.execute` ; - `CallFunction_104.then` / `CallFunction_937.execute` ; - `CallFunction_166.then` / `CallFunction_731.execute` (GEV:23730-23732, 25075-25076). Liens (18) : - `IfThenElse_152.then` → D1.execute ; `CustomEvent_3172.WithoutDestroying` → D1.Condition ; D1.then → `CallFunction_104.execute` ; - D1.else → D2.execute ; D2.then → `CallFunction_104.execute` ; `CallFunction_104.then` → D3.execute ; D3.then → `CallFunction_937.execute` ; - `CallFunction_166.then` → G1.execute ; GR → G1.Condition ; G1.else → `CallFunction_731.execute` ; G1.then → G2.execute ; - `CustomEvent_3172.Person` → GV.self ; GV → G2.Condition ; G2.then → G3.execute ; `CustomEvent_3172.Person` → G3.self ; - G3.then → G4.execute ; G2.else → G4.execute ; G4.then → `CallFunction_731.execute`. `/Game/BPs/GI.GI:SavePreset`. Nœuds nouveaux (3) : - S1 Branche ; - S2 lecture `SaveBeforeDestroy` ; - S3 `Set RevertSkipped` (`set_pin_value` true). Coupure (1) : `IfThenElse_2957.then` / `CallFunction_16039.execute`. Liens (4) : - `IfThenElse_2957.then` → S1.execute ; S2 → S1.Condition ; - S1.else → `CallFunction_16039.execute` ; - S1.then → S3.execute (S3.then non relié : fin de fonction). Comportement : - PNJ mis au pool : `SavePreset` saute le rhabillage et pose `RevertSkipped`. Après `destroyDummy`, `Valid = false` donc G2.else : pas de rhabillage (gain § 1.2). - `AIDummy` : `Valid` reste vrai, donc G3 rhabille, comme aujourd'hui mais après `destroyDummy` ; même état final (estimé). - `WithoutDestroying = true` et autres appelants de `SavePreset` : `SaveBeforeDestroy` reste false, rien ne change. Volume B1-a : +2 variables membres ; `GI:EventGraph` +9 nœuds (par rapport au compte d'en-tête du dump de référence pris en P5), 3 coupures, 18 liens, 1 `set_pin_value` (D2) ; `GI:SavePreset` 503 → 506 nœuds, 1 coupure, 4 liens, 1 `set_pin_value` (S3). Puis **B2-a** sur `/Game/BPs/GI.GI:GetSMBoolForSMC_Cloth` : étapes du § 2.3 sans changement. Volume : +1 variable locale `MeshSuffix`, 11 → 13 nœuds, 2 coupures, 4 liens. ### C.6 Bilan par asset (liste finale des graphes touchés) | Asset (instantané) | Graphe | Option | Nœuds | Coupures | Liens | Variables | |---|---|---|---|---|---|---| | `Content\UI\TopRight.uasset` (`STE-H10`) | `EventGraph` | B4-a | +1 (906 → 907) | 1 | 2 | — | | `Content\BPs\Person.uasset` (`STE-H10`) | `EventGraph` | A1 | +18 (3 684 → 3 702) | 8 | 34 | `HairIdxs` (int[]), `PickedHairIdx` (int), `HairSearch` (bool) | | idem | `ClearProxies` | B3-a | +2 (37 → 39) | 2 | 4 | `ProxySuffix` (string, locale) | | `Content\BPs\GI.uasset` (`STE-H10`, après C0) | `EventGraph` (chaîne `DestroyPerson`) | B1-a | +9 | 3 | 18 | `SaveBeforeDestroy`, `RevertSkipped` (bool) | | idem | `SavePreset` | B1-a | +3 (503 → 506) | 1 | 4 | — | | idem | `GetSMBoolForSMC_Cloth` | B2-a | +2 (11 → 13) | 2 | 4 | `MeshSuffix` (string, locale) | | **Total** | 6 graphes, 3 assets | 5 options | **+35** | **17** | **66** | 5 membres, 2 locales | GI touché : `EventGraph`, `SavePreset`, `GetSMBoolForSMC_Cloth`, rien d'autre. Ni `destroyDummy` (C0), ni `LoadHairMeshIfNeeded`, ni `getSkeletalMeshFromPath`, ni `CopyActionsToScroll`. ### C.7 VAL (lp-validator ; commande → attendu) Dumps après IMPL, au format `.\Tools\RunCommandlet.ps1 -Lot STE-H10V -Commandlet LPDumpFunction -Args '' -EtatDir "D:\Game\LifePlay-master 5.8\Docs\Migration\etat\ST\STE"` : - `-BP=TopRight -Fn=EventGraph -Pins -Exec` → 907 nœuds ; `CustomEvent_5.then → T1 (DelayUntilNextFrame) → SwitchString_1`. - `-BP=Person -Fn=EventGraph -Pins -Exec` → 3 702 nœuds ; b1-b8 absents, c1-c34 présents ; `CallFunction_96.execute ← N3` seul. - `-BP=Person -Fn=ClearProxies -Pins -Exec` → 39 nœuds ; `CallFunction_2.InSuffix ← P2`. - `-BP=GI -Fn=EventGraph -Pins -Exec` → compte de référence + 9 ; chaîne de § C.5. - `-BP=GI -Fn=SavePreset -Pins -Exec` → 506 nœuds ; `IfThenElse_2957.then → S1`, `S1.else → CallFunction_16039`, `S1.then → S3`. - `-BP=GI -Fn=GetSMBoolForSMC_Cloth -Pins -Exec` → 13 nœuds ; `CallFunction_2.InSuffix ← C2`. - Contrôles généraux : - `CompileAllBlueprints` 418 / 418, 0 erreur, 0 avertissement ; - `LPTestStringCompat` 3120 / 0 / 60 ; - `git status --short -- Content` = les 3 assets seulement ; - MD5 des autres `.uasset` inchangés ; - comptes de nœuds des autres graphes de GI inchangés (`destroyDummy` 39, `LoadHairMeshIfNeeded` 17, `CopyActionsToScroll` 34). ### C.8 Trace finale (unique, même parcours que STE-H9) **Capture et analyse** : - Archive Development du lot (nom fixé par l'orchestrateur), même ligne de lancement que H9 § 1 (`-trace=cpu,frame,bookmark,loadtime -statnamedevents -LPHitchLog -tracefile=…`), parcours J5. - Analyse hors jeu par `UnrealInsights.exe -OpenTraceFile … -ExecOnAnalysisCompleteCmd`, commandes `ExportTimerStatistics` / `ExportTimingEvents` / `ExportThreads` (H9 § 1). - Parcours manuel : on compare **moyennes par appel et pointes par trame**, pas les totaux. **Critères (référence H9 entre parenthèses ; seuils estimés)** : - A1 : - `RandomizeHairs` max < 25 ms (38,4) ; - dans `ev_*.csv`, au plus un `getSkeletalMeshFromPath` réellement chargeant par `RandomizeHairs`, sauf rejet (chemin non chargeable : 0 attendu) ; - PNJ coiffés (captures). - B1-a : - dans chaque `DestroyPerson` suivi d'un `destroyDummy`, aucun `RevertStripAll` enfant de `SavePreset` ; - ces `SavePreset` ≤ 45 ms (73,6 à 89,1 ; § 1.2) ; - `SavePreset` moyen ≤ 35 ms (53,3) ; - aucun PNJ visible déshabillé après un « Suivant » (captures) ; - à la scène suivante, un PNJ réutilisé est habillé. - B2-a : `GetSMBoolForSMC_Cloth` moyen par appel ≤ 1,5 ms (2,17) ; mêmes états « exposé » sur une scène rejouée (captures). - B3-a : `ClearProxies` moyen par appel ≤ 6,1 ms (8,13). - B4-a : - au plus un `Current` par trame (`ev_*.csv` des trames de fin de scène) ; - liste d'actions juste après chaque clic « Suivant » et après un clic sur la carte (captures ; contrôle de la reprise de `Amenity`, § C.1-5). - Global, trames ≥ 50 ms par catégorie : Suivant (18 trames, 4,1 s), sans clic (78 trames, 10,8 s), total (120 trames, 18,6 s). Attendu : baisse de la durée par trame Suivant et des trames sans clic dues aux cheveux. ### C.9 Risques et questions restants - **R-A1, R-A2, R-B1** : traités (§ C.1-1, C.1-2, C.1-4). - **R-B2** reste estimé (`ResetVars` vide `LastStripAll`, PEG:11942-11955) : contrôle visuel en § C.8, PNJ réutilisé habillé. - **R-B4** devient : une trame de retard (≈ 16 ms) sur la liste d'actions, accepté par la décision B4-a. Les appelants GI qui suivent `Current` lisent l'ancienne liste pendant cette trame (estimé sans effet durable : la reconstruction suivante écrase tout). - **Nouveau R-W** (A1) : macro `WhileLoop`, bornée par construction (un index retiré ou fin à chaque tour). La garde moteur de boucle infinie ne peut pas se déclencher : au plus `Len(HairIdxs) + 1` tours. - **Q-H10-12** (ouverte, Big Boss) : place de l'écriture de GI (E3) par rapport aux sous-lots CAM B1, B4 et B5. - **Q-H10-14** (nouvelle, utilisateur) : accepter un commit et un repli **par asset** (TopRight ; Person = A1 + B3-a ; GI = B1-a + B2-a) au lieu d'un par option ? Deux options d'un même `.uasset` binaire ne peuvent pas être commitées séparément sans enregistrement intermédiaire. ## Fichiers à commiter - A `Docs/Migration/etat/ST/STE/STE-H10-SPEC.md` (dépôt du jeu, branche `ue5.8`). Tours : 40 / 70 Tours après complément : 51 / 70 (cumul de la fiche ; `FicheStats` fait foi) ## Constats Établi : - Nœuds morts : `PrintString` sans entrée d'exécution dans `LoadHairMeshIfNeeded` (LH:86-96) et `getSkeletalMeshFromPath` (GS:53-63) ; `SmLoaded` écrit mais non lu (GS:64-79) ; `BooleanAND` sans sortie reliée dans `ClearProxies` (CP:164-168). - `Delay(0,3)` dans la boucle de `LoadBridgetAndHairs` avec `then` vide (PEG:10682-10688) : la boucle n'attend pas (estimé, macro ForEachLoop). - Cas `WithoutDestroying = true` (ev_74) : `SavePreset` rhabille (36,3 ms), puis `clearScenePersons` relance un `RevertStripAll` (ev_74:462817, 13,5 ms) ou un `Dress` (CSP:104-134) : second rhabillage possible, hors des options ci-dessus. - `getSkeletalMeshFromPath` (920,5 ms, ST:210) sert aussi les vêtements (`LoadMeshIfNeeded`, `LoadOtherMeshIfNeeded_0`, PEG:9485-9489) : le volet A ne traite que les cheveux. Estimé : - Le coût des cheveux est surtout celui des premiers PNJ de chaque sexe / type (cache GI ensuite) ; `RandomizeHairs` garde ≈ (576,6 − 284,6) / 34 ≈ 8,6 ms par appel pour la boucle seule (copies de structures sur toute `HairstyleTable`). Incertain (essai qui tranche) : - Comportement de `DelayUntilNextTick` appelé deux fois dans la même trame (lire `KismetSystemLibrary.cpp` avant IMPL de B4-a). - Part des enfants « non vêtements » dans les 1 393 appels de `GetSMBoolForSMC_Cloth` (trace après B2-a, ou liste des composants). - Répartition des 16 `Current` par trame (lire `ev_*.csv` des trames de fin de scène). - Mise à jour du complément : le premier point ci-dessus est tranché (§ C.1-5 : second appel ignoré, mais possible dans la même trame moteur ; d'où `DelayUntilNextFrame`).