LifePlay-Revival/Docs/Migration/etat/ST/STE/STE-H10-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

58 KiB
Raw Blame History

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_<n>.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 <fichier> 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|<BP>|<Fonction> ; Variables|<Catégorie>|Définir<Var> et lecture …|<Var> : relever le type_id exact par find_node_types {graph, type_id_filter:"<Var>"} ; 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:<Graphe>.K2Node_<Nom>, /Game/BPs/Person.Person:<Graphe>.K2Node_<Nom>, /Game/UI/TopRight.TopRight:<Graphe>.K2Node_<Nom> (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<String,int> 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 '<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
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 '<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).