58 KiB
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
.uassettouché = HEAD (GI16394D43…, PersonBE21838A…, TopRight3DF4DEC4…) ;git status --short -- Contentvide. - 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_infosde 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 parfind_node_types {graph, type_id_filter:"<Var>"}; fonctions de tableau :find_node_typesfiltreTableau(formeUtilitaires|Tableau|…) ;RandomInteger: filtreRandomInteger. Après connexion, vérifier parget_node_infosque les broches génériques ont pris le bon type. - Variables nouvelles :
BlueprintTools.add_variable(blueprint, name, type_name, graph, container_type)(membre ; locale sigraphest 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_inspect0 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].SkeletalMeshvalide → rien (LH:11-15) ; sinon, chemin ≠ None (LH:43-52) et (Daz ouLegacyPercentage > 0) (LH:63-80) →getSkeletalMeshFromPath→Array_SetdansHairstyleTable(LH:16-29).getSkeletalMeshFromPath:GetAssetByObjectPath→IsAssetLoaded(résultatSmLoadednon lu) →AssetRegistryHelpers::GetAsset→ IsValid → cast (GS:4-80).GetAssetcharge 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 touteGI.HairstyleTable(PEG:2084-2094) → filtreBool == IsMaleETDaz == (Race == "Daz")(PEG:2105-2117, 8664-8678) → maillage valide :Array_Add(Hairs)(CallArrayFunction_11) ; sinonGI.LoadHairMeshIfNeeded(ArrayIndex)(CallFunction_96) puisArray_Addsi 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 dansHairstyleTable) : 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.GenerateAndApplyStringsCallFunction_5/_8; PersonLoadBridgetAndHairs(préchargement de tous les cheveux Daz pour le joueur, PEG:9513-9591, 10670-10688), absent de la trace (GrepLoadBridgetAndHairssur ST : 0 ligne). - Le résultat est utilisé dans la même trame (
SetSkinnedAssetAndUpdatejuste 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; N2Array_Add; N3Set PickedHairIdx; N4 lectureHairIdxs; N5Array_Length; N6RandomInteger; N7GetArrayItem; N8 lecturePickedHairIdx. break_pins(7) : b1CallArrayFunction_10.then/MacroInstance_402.Exec; b2MacroInstance_19.Is Not Valid/CallFunction_96.execute; b3CallFunction_96.then/MacroInstance_11.exec; b4MacroInstance_402.Array Index/CallFunction_96.Index; b5MacroInstance_402.Completed/VariableSet_12.execute; b6VariableGet_52.Hairs/GetArrayItem_3.Array; b7CallFunction_133.ReturnValue/GetArrayItem_3.Dimension 1.connect_pins(18) : c1CallArrayFunction_10.then→ N1.execute ; c2 N1.then →MacroInstance_402.Exec; c3 N4 → N1.TargetArray ; c4MacroInstance_19.Is Not Valid→ N2.execute ; c5CallArrayFunction_11.then→ N2.execute ; c6 N4 → N2.TargetArray ; c7MacroInstance_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) ; c13MacroInstance_402.Completed→ N3.execute ; c14 N3.then →CallFunction_96.execute; c15CallFunction_96.then→VariableSet_12.execute; c16 N8 →CallFunction_96.Index; c17VariableGet_896.HairstyleTable→GetArrayItem_3.Array(même type de structure :Hairsreçoit des éléments deHairstyleTable, PEG:2092, 9254) ; c18 N8 →GetArrayItem_3.Dimension 1.- Inchangés :
IfThenElse_367(filtre),MacroInstance_19(test),CallArrayFunction_11(ajout àHairsdes 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), 7break_pins, 18connect_pins. - Comportement : un seul chargement par
RandomizeHairs; même loi de tirage (uniforme sur les éligibles) ;Hairsne contient plus que les cheveux éligibles déjà chargés (R-A2) ; un cheveu tiré qui ne se charge pas donnePickedHair = None(R-A1 ; avant : exclu du tirage). - GI : aucun graphe modifié (appel existant à
LoadHairMeshIfNeeded, lecture deHairstyleTable).
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) → sinonremoveFromKnownTemp(GEV:25133-25137) →destroyDummy(KeepAlreadyGot = NOT ReuseableInScene)(GEV:23730-23735, 25071-25074).clearScenePersons: pour chaque personne horsRoom.TempNpcs,WithoutDestroying = Interacting OU KnownNpcsHere[Prefix+SaveSlot] == personne OU Companion, puisDestroyPerson(CSP:14-27, 28-98, 200-212).SavePreset(503 nœuds) : sortie immédiate si fichier…Characters\0.lpcharacter(SP:1358-1368) ; siPlayerCharacter != CBE→StripAll(NoUpdateForRevert=false)(SP:1328-1345) → texte : infos, morphs du corps surMorphTargets(SP:155-251, 1426-1432, 1582-1588), pénis, cheveux, peau,BodyParts × Types(SP:2210-2233) →SaveTxt(SP:10-16) → siIncludeStat: lignesSTAT(SP:1294-1300, 1205-1211) →SaveTxt(SP:1278-1284) → siPlayerCharacter != CBE→RevertStripAll(IfThenElse_2957,CallFunction_16039, SP:1346-1357).StatOnlyne saute que l'apparence (SP:1158-1164) :StripAlletRevertStripAllrestent.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).destroyDummyne détruit pas l'acteur : il le cache, appelleResetVars, le téléporte à l'origine,Valid = false(DD:148-165, 249-254).ResetVarsvide le corps (ClearSMCs→ClearMorphTargets→SetSkinnedAssetAndUpdate,STE-H2b-DIAG.md§ 3.2) et videLastStripAll(PEG:11942-11955 ; estimé : appartenance à la chaîne deResetVarsnon suivie maillon par maillon).destroyDummyne fait rien siDummy == AIDummy(DD:282-294) ou siisPlayer(DD:135-142). → Pour un PNJ mis au pool, leRevertStripAllfinal rhabille un corps queResetVarsvide aussitôt (ev_60 : fin deRevertStripAll362,112433 s, début dedestroyDummy362,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) ; D2Set SaveBeforeDestroy(set_pin_valuetrue) ; D3Set SaveBeforeDestroy(false).break_pinsIfThenElse_152.then/CallFunction_104.execute;connect_pinsIfThenElse_152.then→ D1.execute ; D1.then (personne gardée) →CallFunction_104.execute; D1.else (mise au pool) → D2.execute ; D2.then →CallFunction_104.execute.break_pinsCallFunction_104.then/CallFunction_937.execute;connect_pinsCallFunction_104.then→ D3.execute ; D3.then →CallFunction_937.execute.
GI:SavePreset, nœuds nouveaux (2) : S1 Branche ; S2 lectureSaveBeforeDestroy. 3.break_pinsIfThenElse_2957.then/CallFunction_16039.execute;connect_pinsIfThenElse_2957.then→ S1.execute ; S2 → S1.Condition ; S1.else →CallFunction_16039.execute; S1.then non relié (fin de fonction, commeCallFunction_16039.thenvide, SP:1356).- Volume : +1 variable membre ; EventGraph +3 nœuds ; SavePreset 503 → 505 nœuds ; 3
break_pins, 9connect_pins. - Comportement : autres appelants de
SavePreset(GEV:8385CallFunction_16676, GEV:17888CallFunction_8085; appelants hors GEV non inventoriés) : drapeau false → inchangés.WithoutDestroying = true(cas ev_74) → inchangé. Le drapeau est remis à false après chaqueSavePreset, y compris sortie anticipée (SP:1364-1368). - GI touché :
EventGraph(chaîneDestroyPerson:IfThenElse_152,CallFunction_104,CallFunction_937) etSavePreset(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)→StringscontientBottom/Bottom_Under/Top(IG:9-68, 100-108) ;Breaknon relié (IG:64) : tous les enfants sont parcourus.GetSMBoolForSMC_Cloth: boucle sur touteClothTable(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 parFunctionResultau premier trouvé (GC:31-35), parcours complet pour un enfant qui n'est pas un vêtement. 190 079Array_Getpour 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 lectureMeshSuffix.break_pinsFunctionEntry_2157.then/MacroInstance_1872.Exec;connect_pinsFunctionEntry_2157.then→ C1.execute ; C1.then →MacroInstance_1872.Exec;CommutativeAssociativeBinaryOperator_0.ReturnValue→ C1 (valeur).break_pinsCommutativeAssociativeBinaryOperator_0.ReturnValue/CallFunction_2.InSuffix;connect_pinsC2 →CallFunction_2.InSuffix.
- Volume : +1 variable locale, 11 → 13 nœuds, 2
break_pins, 4connect_pins. - Équivalence :
SMCest 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_Clothseulement.
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 lectureProxySuffix.break_pinsDynamicCast_30.then/MacroInstance_301.Exec;connect_pinsDynamicCast_30.then→ P1.execute ; P1.then →MacroInstance_301.Exec;CommutativeAssociativeBinaryOperator_0.ReturnValue→ P1 (valeur).break_pinsCommutativeAssociativeBinaryOperator_0.ReturnValue/CallFunction_2.InSuffix;connect_pinsP2 →CallFunction_2.InSuffix.
- Volume : +1 variable locale, 37 → 39 nœuds, 2
break_pins, 4connect_pins.CallFunction_3(GetSkeletalMesh) litDynamicCast_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) : selonAmenity,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 widgetsAction_Csont créés par le clic simulé sur la carte (StreetMap_Static, non dumpé ; H9 § 5 :ActualLeftMouseInput168 574Array_Get, 295 ms) (estimé).CopyActionsToScrollne crée pas de widget :ClearChildren(Scroll), puis déplace jusqu'à 100 enfants deCursor.ActionBoxversScrollparAddChild(trois libellés retirés), remplitCurrentDailies(CA:4-159, établi).cest une variable locale (lecture sans brocheself, CA:77-78, 89-90 ; estimé).- Appelants de
Currentdans 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 parfind_node_types {graph: TopRight:EventGraph, type_id_filter:"NextTick"}).break_pinsCustomEvent_5.then/SwitchString_1.execute;connect_pinsCustomEvent_5.then→ T1.execute ; sortie d'exécution de T1 →SwitchString_1.execute.
- Volume : 906 → 907 nœuds, 1
break_pins, 2connect_pins. - Comportement : reconstruction une trame plus tard ; un second
Currentdans la même trame, pendant que T1 attend, ne relance pas l'action latente et l'exécution unique lit le dernierAmenity(incertain : à lire dansKismetSystemLibrary.cpp,DelayUntilNextTick, avant IMPL). Les appelants GI qui enchaînent juste aprèsCurrent(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)
- 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) :
Hairschange de contenu ; il est lu ailleurs dans Person (VariableSet_287PEG:2224,VariableGet_3269→MacroInstance_655PEG:2355,VariableGet_198→CallArrayFunction_48PEG: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) :
destroyDummysans effet siPerson == AIDummyouisPlayer(DD:135-142, 282-294) : la personne resterait déshabillée et visible (le joueur ne passe pas parStripAll:PlayerCharacter != CBE, SP:1328-1332). - R-B2 (B1-a) : si
ResetVarsne vide pasLastStripAll(estimé qu'il le vide, PEG:11942-11955), un mannequin réutilisé pourrait rhabiller d'anciens vêtements dansclearScenePersons(CSP:118-134). - R-B3 (B1-c) : apparence perdue ou non sauvegardée.
- R-B4 (B4-a) : un appelant de
Currentlit la liste avant reconstruction ; affichage en retard d'une trame. - R-B5 (B2-b) : index de
ClothTablepé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 gardePerson != AIDummy(comparateur à créer, lp-mcp § 3) ou accepter R-B1 ? - Q-H10-7 (B1-c, B1-d) : étudier
StatOnlyou « 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_Staticpour alléger la génération des actions etShowUI? - 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"(siActualLeftMouseInputest 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 parSavePreset. - 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) ; sinonLoadPackagesynchrone (AssetData.h:540-544), d'où leFlushAsyncLoadingde 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 sigraph; typesbool,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_localsurEngine/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_96exé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îneIfThenElse_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 :
CompileAllBlueprints418 / 418, 0 erreur, 0 avertissement ;LPTestStringCompat3120 / 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 :
LoadHairMeshIfNeededen 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
DestroyPersonsuivi d'undestroyDummy, plus deRevertStripAllsousSavePreset(EV) ;SavePresetmoyen ≈ 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,ClearProxiesinclusif en baisse d'au moins 25 % ; états « exposé » identiques (captures avant / après d'une même scène). - B4-a : au plus un
Currentpar 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
- 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 deHairs(PEG:9256-9260). Cela couvre aussi les cheveux queLoadHairMeshIfNeededne charge pas (non Daz etLegacyPercentage ≤ 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 (GetArrayItemsur unHairsvide). 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). - A1, autres lecteurs de
Hairs(Q-H10-3) : Grepvariable 'Hairs'sur tous les dumps (dumps\*.txt) ne trouve que PEG. Aucun dump de GI ne litHairs.VariableSet_287(PEG:2224-2227) écritHairs = OtherTableaprès la boucleLoadOtherMeshIfNeeded_0(MacroInstance_12, PEG:9478-9484). LeArray_Shuffle(CallArrayFunction_901, PEG:2230-2234) et le seul autre lecteur,MacroInstance_655(PEG:2243-2250, viaVariableGet_3269PEG:2355), suivent cette écriture. Le second passage (SecondTurn = true,VariableSet_2572PEG:2336-2338, aprèsMacroInstance_655.CompletedPEG:2319-2323) relit la même copie d'OtherTable.VariableGet_198→CallArrayFunction_48(PEG:12377-12384) videHairs: c'est une écriture.- Le seul lecteur du résultat de
RandomizeHairsest donc le tirage lui-même (GetArrayItem_3,CallArrayFunction_14). Changer le contenu deHairsn'a pas d'effet (établi pour PEG). Les 133 autres graphes de Person ne sont pas dumpés : estimé sans effet,Hairssert de tableau de travail.
- A1, nœuds devenus orphelins (Q-H10-4) :
MacroInstance_11,CallFunction_133etCallArrayFunction_14restent en place, sans suppression. Des nœuds sans exécution ou purs non reliés ne bloquent pas la compilation (estimé). Sicompile_blueprintles cite en erreur : arrêt, rapport, décision de l'utilisateur. - B1-a, garde du cas
AIDummy(Q-H10-6) : le risque est réel.AIDummyreçoit un vraiSaveSlot(Conv_StringToInt, GEV:24953-24962) et un preset deSaves/_session/Characters/(GEV:24931-24947).destroyDummyne fait rien pour lui (DD:282-294). Une garde par comparaisonPerson == AIDummyest impossible à créer (lp-mcp § 3 : les comparateurs ne sortent pas defind_node_types). La garde retenue est un contrôle a posteriori sans comparateur, fondé sur un fait établi : un mannequin remis au pool aValid = falseen fin dedestroyDummy(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 encoreValidaprèsdestroyDummy, on appellePerson.RevertStripAll. - Le cas
isPlayern'a pas besoin de garde :SavePresetne déshabille pas le joueur (SP:1328-1345). - Nœuds exacts : § C.5, G1 à G4.
- Si le rhabillage a été sauté (
- B4-a, deux appels latents dans la même trame (établi) :
DelayUntilNextTickignore un second appel tant que le premier attend (mêmeCallbackTargetet mêmeUUID, KismetSystemLibrary.cpp:2643-2646). Mais il peut se déclencher dans la même trame moteur :FDelayUntilNextTickAction(DelayAction.h:46-49, 66-70), CVarLatentActions.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 estBlueprintCallableet 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
Currentd'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
Amenityd'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_typesavec 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 partagentGI.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éeDelayUntilNextFrameet fairecreate_node. Ne pas prendreDelayUntilNextTick. - 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:EventGraph906 → 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 ; c5CallArrayFunction_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; c24CallFunction_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 ; c28VariableSet_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) ; D3Set SaveBeforeDestroy(false) ; - G1 Branche ; GR lecture
RevertSkipped; - G2 Branche ; GV lecture
Validde Person (find_node_typesfiltreValidaveccontext_pins:[CustomEvent_3172.Person], commeVariableGet_599GEV:25944-25946) ; - G3 appel
Person.RevertStripAll(filtreRevertStripAll, 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_valuetrue).
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 :
SavePresetsaute le rhabillage et poseRevertSkipped. AprèsdestroyDummy,Valid = falsedonc G2.else : pas de rhabillage (gain § 1.2). AIDummy:Validreste vrai, donc G3 rhabille, comme aujourd'hui mais aprèsdestroyDummy; même état final (estimé).WithoutDestroying = trueet autres appelants deSavePreset:SaveBeforeDestroyreste 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 ← N3seul.-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 :
CompileAllBlueprints418 / 418, 0 erreur, 0 avertissement ;LPTestStringCompat3120 / 0 / 60 ;git status --short -- Content= les 3 assets seulement ;- MD5 des autres
.uassetinchangés ; - comptes de nœuds des autres graphes de GI inchangés (
destroyDummy39,LoadHairMeshIfNeeded17,CopyActionsToScroll34).
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, commandesExportTimerStatistics/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 :
RandomizeHairsmax < 25 ms (38,4) ;- dans
ev_*.csv, au plus ungetSkeletalMeshFromPathréellement chargeant parRandomizeHairs, sauf rejet (chemin non chargeable : 0 attendu) ; - PNJ coiffés (captures).
- B1-a :
- dans chaque
DestroyPersonsuivi d'undestroyDummy, aucunRevertStripAllenfant deSavePreset; - ces
SavePreset≤ 45 ms (73,6 à 89,1 ; § 1.2) ; SavePresetmoyen ≤ 35 ms (53,3) ;- aucun PNJ visible déshabillé après un « Suivant » (captures) ;
- à la scène suivante, un PNJ réutilisé est habillé.
- dans chaque
- B2-a :
GetSMBoolForSMC_Clothmoyen par appel ≤ 1,5 ms (2,17) ; mêmes états « exposé » sur une scène rejouée (captures). - B3-a :
ClearProxiesmoyen par appel ≤ 6,1 ms (8,13). - B4-a :
- au plus un
Currentpar trame (ev_*.csvdes 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).
- au plus un
- 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é (
ResetVarsvideLastStripAll, 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
Currentlisent 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 plusLen(HairIdxs) + 1tours. - 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
.uassetbinaire 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, brancheue5.8).
Tours : 40 / 70
Tours après complément : 51 / 70 (cumul de la fiche ; FicheStats fait foi)
Constats
Établi :
- Nœuds morts :
PrintStringsans entrée d'exécution dansLoadHairMeshIfNeeded(LH:86-96) etgetSkeletalMeshFromPath(GS:53-63) ;SmLoadedécrit mais non lu (GS:64-79) ;BooleanANDsans sortie reliée dansClearProxies(CP:164-168). Delay(0,3)dans la boucle deLoadBridgetAndHairsavecthenvide (PEG:10682-10688) : la boucle n'attend pas (estimé, macro ForEachLoop).- Cas
WithoutDestroying = true(ev_74) :SavePresetrhabille (36,3 ms), puisclearScenePersonsrelance unRevertStripAll(ev_74:462817, 13,5 ms) ou unDress(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) ;
RandomizeHairsgarde ≈ (576,6 − 284,6) / 34 ≈ 8,6 ms par appel pour la boucle seule (copies de structures sur touteHairstyleTable).
Incertain (essai qui tranche) :
- Comportement de
DelayUntilNextTickappelé deux fois dans la même trame (lireKismetSystemLibrary.cppavant 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
Currentpar trame (lireev_*.csvdes 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).