21 KiB
Fiche U7-T11 — Niveau N3 — Modèle prévu lp-cart (Opus xhigh) | utilisé Opus 5.5 (claude-opus-5-5) — Agent lp-cart — Skills sk-protocol, lp-commandlets, lp-paths, ue-cpp-build
Cartographie, lecture seule : pourquoi Bridget s'affiche chauve dans l'éditeur de personnage quand le mod
lpr_hair_pocolov (MODULE_DEFAULT_ENABLED: true) est présent, en 5.5 (mod chargé) comme en 5.8 (mod refusé).
Je n'ai rien modifié en dehors de ce livrable. Je n'ai lancé ni le jeu, ni l'éditeur, ni aucun commandlet.
Environnement vérifié : moteurs UE_5.5 et UE_5.8 installés (GameInstance.cpp lu dans les deux), copies
5.5 et 5.8 présentes, et les builds LifePlay_Build_5.5_DC4, _DC4_P10, _5.8_U7c et _U7c_GM présents.
1. Réponse courte
- Le défaut ne vient pas de l'ordre d'insertion de
EnsureDefaultEnabledModules(estimé, de façon forte). Cet ordre explique seulement l'affichage : Pocolov apparaît au-dessus de « Base Gameplay Module » (§ 4). - La cause est une reconstruction de
GI.HairstyleTableparULPHairstyleSubsystem::Sync(). Elle intervient après le préchargement des coiffures par le Blueprint et avant l'ouverture de l'éditeur.Sync()vide la table (EmptyValues) puis la recrée sans le champSkeletalMesh, ce qui efface tous les maillages déjà chargés.GI.UpdateDefaultHair(femme, Daz) lit ensuiteHairstyleTable[38].SkeletalMesh, c'est-à-dire ToulouseHR, sans le recharger. Le maillage vautNone, d'où le rendu chauve. Le journal montreAccessed None ... SkeletalMesh_2dansUpdateDefaultHair, en 5.5 comme en 5.8. - Rôle du mod : il n'intervient que comme déclencheur. La reconstruction part dès que la « signature » de
GI.ModEnabledschange ; cette signature dépend de l'ordre. Avec Pocolov, les deux sessions où le défaut apparaît contiennent un tel changement entre l'écran titre et l'éditeur. Sans mod, les sessions archivées n'en contiennent pas. - Prédiction (à vérifier par l'essai du § 7) : sans aucun mod, ouvrir le gestionnaire de mods à l'écran titre d'une installation neuve, puis lancer New Game et cliquer Female, doit aussi donner une Bridget chauve.
2. Chaîne causale
| # | Maillon | Niveau | Preuve |
|---|---|---|---|
| 1 | L'événement Blueprint Event Init de GI (ReceiveInit) s'exécute avant l'initialisation des sous-systèmes. Quand ULPHairstyleSubsystem::Initialize démarre, GI.ModEnableds contient donc la valeur par défaut du Blueprint : GI.LoadSettings ne s'exécute qu'à New Game ou à Continue |
établi (moteur) ; estimé (absence de LoadSettings au démarrage) |
UE_5.5/.../GameInstance.cpp:99 ReceiveInit() puis :128 SubsystemCollection.Initialize ; 5.8 :100/:129. Les avertissements de PopulateRaws (ParseIntoArray_ReturnValue_29) n'apparaissent qu'à 06:36:28 (fermeture du gestionnaire) et à 06:36:32 (New Game), jamais à la frame 0 (P10/5.5/LifePlay.log, grep ParseIntoArray_ReturnValue_29 | cut -c1-32 | uniq -c) |
| 2 | À l'initialisation, EnsureDefaultEnabledModules ajoute Pocolov en fin de ModEnableds (AddValue). LastSignature est alors calculée et un premier Sync() construit la table : tous les SkeletalMesh valent None |
établi | 5.8 LPHairstyleSubsystem.cpp:155-157, :412 ; 5.5 :154-156, :399. Journal 5.5 P10/5.5/LifePlay.log:857-871, journal 5.8 GM LifePlay.log:869-871 |
| 3 | À l'écran titre, le personnage joueur (Person, isPlayer) précharge toutes les coiffures Daz dans GI.HairstyleTable. La chaîne est : affichage d'un widget Loading, minuterie de 0,2 s, GI.LoadMeshIfNeeded, pause de 0,3 s, GI.LoadDefaultTextures, pause de 0,3 s, puis une boucle ForEach HairstyleTable qui appelle GI.LoadHairMeshIfNeeded(i) pour chaque entrée Daz. Le widget Loading est retiré et la musique du titre démarre ensuite. LoadHairMeshIfNeeded charge le maillage par getSkeletalMeshFromPath et le réécrit dans la table (Array_Set) |
estimé (relevé de graphe ; le nom de l'événement d'entrée n'est pas relevé) | L6f_dump_Person.txt:39484 (CustomEvent_99 → CreateWidget), :30568 (minuterie 0,2 s → CustomEvent_15), :31703 (isPlayer), :38980 (LoadDefaultTextures), :30598 (boucle sur HairstyleTable), :30615 (condition Daz), :30620 (LoadHairMeshIfNeeded), :30591 (Loading.RemoveFromParent), :39503 (SpawnSound2D → ThemeSong). L6f_dump_GI.txt:38472-38495 (LoadHairMeshIfNeeded : IsValid, getSkeletalMeshFromPath, Array_Set sur HairstyleTable) |
| 4 | GI.ModEnableds change à l'écran titre. Le cas observé est l'ouverture du gestionnaire de mods : GI.CheckIfModAlreadyFound insère à l'indice 0 chaque module absent de la liste, avec la case décochée. Ensuite, à chaque tick de 2 s, EnsureDefaultEnabledModules s'exécute puis la signature est comparée. Comme elle a changé, le sous-système journalise « liste des modules actifs modifiee, reconstruction » |
établi (code, journaux) ; estimé (origine du changement) | L6f_dump_GI.txt:153-208 (Array_Insert, Index = '0' en :202-206). 5.8 LPHairstyleSubsystem.cpp:167-185, signature dépendante de l'ordre :354-370. 5.5 : interface UIOnly à 06:35:42.287 (frame 310), reconstruction à 06:35:42.689 (P10/5.5/LifePlay.log:6206). 5.8 GM : « gestionnaire, 1 mod(s) refuse(s) marque(s) » à 07:32:12.251 (:6248), reconstruction à 07:32:12.983 (:6250), puis trois autres (:6256, :6263, :6271) |
| 5 | Sync() efface tous les maillages chargés. En mode base, il appelle Table.EmptyValues() puis recrée chaque entrée avec seulement Bool, Daz et SkeletalMeshPath. Sans fichier de base, il réimporte le texte du Blueprint capturé au premier Sync(), où tous les maillages valent None. Aucun maillage n'est reporté |
établi (code) | 5.8 LPHairstyleSubsystem.cpp:446-460 (base) et :486 (Blueprint) ; 5.5 :433-447 et :473. La table de base est « IDENTIQUE au Blueprint » (11_Audit_Optimisation.md:647-649) : la valeur par défaut du Blueprint ne contient donc aucun maillage, qui ne sont chargés qu'à l'exécution |
| 6 | Entre la reconstruction et l'éditeur, plus rien ne recharge l'indice 38 : le préchargement du maillon 3 ne se fait qu'une fois, à l'écran titre | estimé | ordre des journaux : reconstruction (5.5 :6206, 5.8 :6250-6272), New Game, OpenCharGen (5.5 :8307, 5.8 :8568) |
| 7 | Bouton Female → GI.PlayerChangeGender → GI.UpdateDefaultHair (CustomEvent_5). Selon CharacterBeingEdited.IsMale, puis selon Race == 'Daz', l'indice est fixe : femme Daz 38, femme non-Daz 20, homme Daz 50, homme non-Daz 24 (SelectInt). Les branches Daz appellent SetSkinnedAssetAndUpdate(HairSM, HairstyleTable[i].SkeletalMesh) sans LoadHairMeshIfNeeded. Seules les branches non-Daz chargent le maillage |
établi | DOC-MOD_LPDumpFunction_20261007-083554.txt:3811 (CustomEvent_5), :3814-3818 (IfThenElse_104, IsMale), :20464 (IfThenElse_66 : then → CallFunction_661 directement, else → LoadHairMeshIfNeeded), :19484 (SelectInt A=38, B=20), :19497 (Race comparé à 'Daz'), :3831 (SetSkinnedAssetAndUpdate, NewMesh = BreakStruct_13.SkeletalMesh), :19938 (GetArrayItem_6 sur HairstyleTable). Le nœud PrintString 'Update Default Hair' est relié à IfThenElse_104 (:3814-3815) |
| 8 | HairstyleTable[38].SkeletalMesh vaut None : le maillage des cheveux est remplacé par rien, d'où le rendu chauve. La boucle sur les matériaux qui suit lit Materials sur ce None |
établi | 5.5 P10/5.5/LifePlay.log:8377-8385 et 5.8 GM LifePlay.log:8637-8645 : Accessed None trying to read property SkeletalMesh_2_93573D... avec la pile ButtonFemale → PlayerChangeGender → UpdateDefaultHair. Dans le graphe de GI, seul cet événement utilise SMBool.SkeletalMesh comme objet cible (DOC-MOD...083554.txt:4255, :4269) |
| 9 | L'indice 38 correspond bien à « Toulouse » : /Game/Heavy/Daz/Bridget/Hairs/ToulouseHR.ToulouseHR, SEX: F, DAZ: true, morph ToulouseHR__AllExpand 0.45. Les coiffures Pocolov sont ajoutées en fin de table (indices 126 à 137) et ne décalent pas l'indice 38 |
établi | LifePlay-master 5.8/Content/Modules/vin_Base/Hairstyles/vin_Base.lphair:161-164, :516 (fichier identique en 5.5, diff -q vide). Journal 5.5 :859-870 |
| 10 | Les coiffures restent sélectionnables ensuite, parce que CharGen.PopulateHairstyles appelle LoadHairMeshIfNeeded pour chaque entrée de la table |
établi (graphe) | L4-CX-B04_dump_apres_CharGen_EventGraph.txt:708-724, :2034 |
Les lignes set hair morph 0.0 Pocolov17__AllExpand (5.5, :8391) et ... ManeHair__AllExpand (5.8, :8670) arrivent
36 à 38 s après l'ouverture de l'éditeur. J'estime qu'elles correspondent au choix fait ensuite par l'utilisateur dans
l'onglet coiffure (widget Hairstyle_C, non relevé) : elles ne jouent aucun rôle dans le défaut.
3. Pourquoi la reconstruction se produit avec le mod et pas sans (sessions archivées)
Comptage par journal (grep -c sur UpdateDefaultHair, reconstruction, ModManagerButton/« gestionnaire » et
OpenCharGen, ordre des lignes) :
| Session | Mod | Gestionnaire ouvert avant New Game | Reconstruction avant l'éditeur | Accessed None dans UpdateDefaultHair |
|---|---|---|---|---|
P10/5.5/LifePlay.log (5.5 DC4_P10) |
Pocolov chargé | oui (06:35:42) | oui (:6206) |
oui (:8377) |
LifePlay_Build_5.8_U7c_GM/.../LifePlay.log |
Pocolov refusé | oui (07:32:12) | oui (:6250-6272) |
oui (:8637) |
LifePlay_Build_5.5_DC4 (5 journaux) |
aucun | non (gestionnaire ouvert après l'éditeur, ou pas du tout) | non : reconstruction après l'éditeur (backup-...07-18.46.57.log:20078, LifePlay.log:8778) |
non |
LifePlay_Build_5.8_Development (2 journaux) |
aucun | non | non | non |
Dans les sessions sans mod, l'absence d'avertissement ne prouve pas que UpdateDefaultHair s'est exécuté : aucune
pile PlayerChangeGender n'y figure. Ces sessions concordent avec la chaîne, mais ne la démontrent pas.
Contenu de ModEnableds dans GameSettings.sav (lu en octets, commande en § 9) :
| Fichier | Ordre du tableau (indice 0 en premier) |
|---|---|
LifePlay_Build_5.5_DC4 |
vin_Pregnancy(T), vin_VanillaRealistic(T), vin_VanillaPorn(T), vin_NTR(F), vin_Base(T) : 5 entrées seulement |
LifePlay_Build_5.5_DC4_P10 (après la session) |
11 vin_* décochés, puis vin_Pregnancy, vin_VanillaRealistic, vin_VanillaPorn, vin_NTR, vin_Base, lpr_hair_pocolov(T) |
LifePlay_Build_5.8_U7c_GM (après la session) |
11 vin_*, puis vin_Pregnancy, vin_VanillaRealistic, vin_NTR, vin_Base, lpr_hair_pocolov(F), vin_VanillaPorn (ordre modifié par l'utilisateur) |
Ma lecture (estimé, de façon forte) : la liste au démarrage est la liste par défaut du Blueprint, avec les 5 entrées
de DC4. EnsureDefaultEnabledModules y ajoute Pocolov à la fin. À l'ouverture du gestionnaire, les 11 modules absents
sont insérés à l'indice 0. On obtient exactement l'ordre enregistré dans DC4_P10. La signature change, ce qui
déclenche la reconstruction.
Ce mécanisme ne dépend pas de Pocolov : sans mod, l'ouverture du gestionnaire ajouterait aussi les 11 modules (DC4 n'en enregistre que 5). Le même effet est attendu dans deux autres cas :
LoadSettingscharge à New Game une liste enregistrée différente de la liste par défaut augmentée des modules par défaut ;- l'utilisateur coche, décoche ou réordonne un mod à l'écran titre.
Incertain : je n'ai pas vérifié la valeur par défaut de GI.ModEnableds (relevé CDO absent, § 8).
4. Ordre d'insertion de EnsureDefaultEnabledModules : ce qu'il explique
- Le gestionnaire affiche
ModEnabledsà l'envers : indice = longueur − 1 − position affichée (23_Cycle_De_Vie_Mod.md:109).vin_Base, dernier élément du tableau, s'affiche donc en tête. - Le Blueprint insère les nouveaux modules à l'indice 0 (
L6f_dump_GI.txt:202-206), donc en bas de l'affichage. Le C++ ajoute à la fin (LPHairstyleSubsystem.cpp:412), donc au-dessus devin_Base. C'est le symptôme « Pocolov en tête » (établi). Cet emplacement est hors convention. - Conséquences : dans
PopulateRaws, le premier fichier lu l'emporte (23_Cycle_De_Vie_Mod.md:111-116). Pocolov est donc lu aprèsvin_Base, avec la priorité la plus basse. C'est sans effet pour ce mod : il ne contient que.lphair,.lpmodetCREDITS.txt. La garde deSortLoadOrder(ModuleIndex == 1 && ForUp, doc 23 :110) supposevin_Baseen position affichée 0 : avec Pocolov au-dessus, on peut seulement le redescendre. L'effet est mineur. - Aucune lecture par indice fixe de
ModEnabledsn'existe dans les graphes relevés. Les accès sont des boucles,Array_Insert0,Array_Setà l'indice de boucle (ToggleMod),Length+Swap(SortLoadOrder) etRemoveItem:L6f_dump_GI.txt:175,:204,:24547,:41132,:41163,:50828-52633;DOC-MOD...083554.txt:1654-1681,:1738,:3392-3412,:3443.UI/ModuleTickcompare'vin_Base'par nom (DOC-MOD...083338.txt:178,:204,:248). Aucun code ne suppose doncvin_Baseen[0](établi pour les graphes relevés :GI,ModuleTick). - Changer l'emplacement d'insertion (indice 0, juste avant
vin_Base, ou en fin) ne corrige pas le défaut. Le gestionnaire,LoadSettings, une case ou une flèche modifient la liste quel que soit cet emplacement.
Origine : la règle MODULE_DEFAULT_ENABLED vient de CHG-102, lot H3a (et non H3d), commit 3c2da7a
(Journal_Modifications.md:1078, 16_H3_Mods_Paquets.md:101-104). La reconstruction au changement de signature date
de H1 (CHG-084, cad1649), le vidage EmptyValues de H2 (CHG-089, bac557d). Le défaut est donc latent depuis
H1 ; Pocolov ne fait que le rendre visible.
5. Correctifs proposés (C++, sans Blueprint) — décision de l'orchestrateur et de l'utilisateur
| Id | Correctif | Effet | Risques |
|---|---|---|---|
| C1 (cause) | Dans Sync(), conserver les maillages déjà chargés. Avant l'étape 1, relever la correspondance chemin → maillage des entrées dont SkeletalMesh n'est pas nul. Après l'étape 2, réaffecter le maillage aux entrées de même chemin. Les deux branches (base et Blueprint) et les entrées de modules sont couvertes |
la reconstruction devient sans effet pour le chargement paresseux du Blueprint : l'indice 38 garde ToulouseHR | faible : aucun nouveau chargement, objets déjà référencés par GI ; ignorer la clé vide (entrées NONE). Les coiffures d'un mod décoché disparaissent avec leurs entrées. À valider : relecture du chemin par GetFieldText (FName exporté), identique à celui de Known (:495) |
| C2 (déclencheur) | Calculer la signature uniquement sur les modules qui possèdent des coiffures ou une pilosité (Defs, Textures), avec leur état coché, triée pour ne plus dépendre de l'ordre |
ouvrir le gestionnaire, réordonner, cocher un mod texte ou recharger les réglages ne reconstruit plus la table | faible ; ne couvre pas la case d'un mod de coiffures (reconstruction légitime) : à combiner avec C1 |
| C3 (affichage, facultatif) | EnsureDefaultEnabledModules : Helper.InsertValues(0) au lieu de AddValue(), comme le fait CheckIfModAlreadyFound |
Pocolov s'affiche sous vin_Base ; priorité de lecture d'un mod normal |
sans effet sur le défaut ; un module par défaut qui aurait des fichiers texte homonymes de vin_Base les imposerait alors (aucun aujourd'hui : lp_Revival n'existe plus en 5.5) |
| écarté | Recharger en C++ les coiffures Daz après Sync() (LoadObject) |
— | chargement synchrone d'environ 90 maillages, saccade, et duplication de la logique du Blueprint |
Esquisse de C1. Les API ont été vérifiées dans UnrealType.h : 5.5 :2653, :2658 ; 5.8 :2896, :2901.
// Sync(), apres la creation de Table (:439) et avant l'etape 1
const FObjectPropertyBase* MeshProp = CastField<FObjectPropertyBase>(FindField(Struct, TEXT("SkeletalMesh")));
TMap<FString, UObject*> Loaded;
for (int32 i = 0; MeshProp && i < Table.Num(); ++i)
if (UObject* Mesh = MeshProp->GetObjectPropertyValue_InContainer(Table.GetRawPtr(i)))
Loaded.Add(GetFieldText(Struct, Table.GetRawPtr(i), TEXT("SkeletalMeshPath")), Mesh);
// ... etapes 1 et 2 inchangees ...
// apres l'etape 2 (avant le journal :546)
for (int32 i = 0; MeshProp && i < Table.Num(); ++i)
if (UObject* const* Mesh = Loaded.Find(GetFieldText(Struct, Table.GetRawPtr(i), TEXT("SkeletalMeshPath"))))
MeshProp->SetObjectPropertyValue_InContainer(Table.GetRawPtr(i), *Mesh);
Avec -LPVerifyHairstyles, la comparaison au Blueprint se fait avant la réaffectation : elle reste valable.
6. Points (a), (b), (c) de la méthode
- (a) Construction de
ModEnableds: la valeur par défaut du Blueprint au démarrage (estimé) ;LoadSettings(New Game, Continue) la remplace par celle deGameSettings.sav, puisReadModLauncheretremoveDeletedMods(23_Cycle_De_Vie_Mod.md:200-224) ; le gestionnaire appelleCheckIfModAlreadyFound, qui insère à 0 (L6f_dump_GI.txt:153-208) ;ToggleModetSortLoadOrderla modifient ensuite ; le C++ ajoute en fin (:412).IsModEnabledparcourt la liste (L6f_dump_GI.txt:24538-24560). - (b) Lecture par indice : aucune (§ 4).
- (c) Coiffure par défaut : indice fixe dans
UpdateDefaultHair(38 pour une femme Daz = ToulouseHR), lu sans chargement dans la branche Daz. Le maillage reste vide parce queSync()a vidé la table après le préchargement du titre (§ 2, maillons 3, 5 et 7).
7. Essai décisif (utilisateur, 3 étapes, sans mod)
Sur une copie jetable de D:\Game\LifePlay_Build_5.8_U7c. Cette copie n'a aucun mod dans Mods, pas de module
Pocolov et aucun SaveGames : c'est le cas d'un premier lancement.
- Lancer le jeu. À l'écran titre, ouvrir le gestionnaire de mods puis le fermer sans rien changer.
- New Game, puis bouton Female (Bridget).
- Relever deux choses : Bridget est-elle chauve ?
Saved\Logs\LifePlay.logcontient-il « Coiffures : liste des modules actifs modifiee, reconstruction » avantOpenCharGen, puisAccessed None ... SkeletalMesh_2_93573D...avecUpdateDefaultHairdans la pile ?
Verdict :
- chauve avec ces deux lignes : la cause est la reconstruction ; ni Pocolov ni l'ordre d'insertion ne sont en jeu. Correctif C1, éventuellement avec C2.
- ToulouseHR visible et aucune reconstruction avant l'éditeur : la chaîne est réfutée sur le maillon 4. Je reprendrais alors avec le relevé CDO du § 8.
Contre-essai facultatif, sur une nouvelle copie jetable : New Game puis Female sans ouvrir le gestionnaire. ToulouseHR attendue. Contrôle supplémentaire : dans l'état chauve, ouvrir l'onglet coiffure (chargement de toutes les coiffures), cliquer Male puis Female. ToulouseHR attendue, ce qui montre que l'indice est juste et que seul le chargement manquait.
8. Incertitudes et relevés manquants (non lancés)
LPDumpFunctionn'imprime pas le nom des événements personnalisés (LPDumpFunctionCommandlet.cpp:18-30).CustomEvent_5=UpdateDefaultHairest établi par lePrintStringattaché et par la pile d'exécution. L'événement d'entrée du préchargement (CustomEvent_99, Person) reste anonyme. La chaîne n'en a pas besoin.- La valeur par défaut de
GI.ModEnabledset leSkeletalMeshàNonede la valeur par défaut deHairstyleTable[38]sont estimés. Commande pour les établir (copie 5.5, éditeur fermé) :.\Tools\RunCommandlet.ps1 -Lot U7-T11 -Commandlet LPDumpCDO -Args '-Class=/Game/BPs/GI.GI_C -Out=Docs\Migration\etat\U58\U7\U7-T11_GI_CDO.txt -Props=ModEnableds,HairstyleTable' - Lecture exacte d'une sauvegarde, en complément de l'extraction en octets :
.\Tools\RunCommandlet.ps1 -Lot U7-T11 -Commandlet LPDumpSave -Args '-File=D:\Game\LifePlay_Build_5.5_DC4_P10\Windows\LifePlay\Saved\SaveGames\GameSettings.sav -Out=Docs\Migration\etat\U58\U7\U7-T11_GameSettings_P10.txt -Props=ModEnableds' - Le journal 5.8 de la session avec l'éditeur n'existe que dans le build
(
LifePlay_Build_5.8_U7c_GM\Windows\LifePlay\Saved\Logs\LifePlay.log, 9 712 lignes ; le fichier a grossi pendant le relevé, de 660 657 à 666 184 octets, jeu probablement encore ouvert). La copie archivéeU7\T9\LifePlay.logs'arrête à 6 275 lignes, avant l'éditeur. Il faut l'archiver avant le prochain lancement de cette copie.
9. Commandes utilisées (lecture seule)
grep -nsur les journaux cités :Coiffures :,Modules :,reconstruction,UpdateDefaultHair,SkeletalMesh_2_93573D,ButtonFemale,ModManagerButton,OpenCharGen.- Balayage de 41 journaux de builds :
for d in D:/Game/LifePlay_Build_*; do ... grep -c .... - Extraction en octets de
ModEnabledsdansGameSettings.sav:python -I -c "re.finditer(rb'(vin_[A-Za-z]+|lpr?_[A-Za-z_]+)\x00', d) ...". Le drapeau booléen se lit dans l'en-tête de propriété qui suitIsEnabled: octet 0x10 = vrai. - Parcours des relevés :
awkpar nom de nœud dansDOC-MOD_LPDumpFunction_20261007-083554.txt,L6f_dump_Person.txt,L6f_dump_GI.txtetL4-CX-B04_dump_apres_CharGen_EventGraph.txt. git log -S "EnsureDefaultEnabledModules",git log -S "liste des modules actifs modifiee",git log -S "Table.EmptyValues".