LifePlay-Revival/Docs/Migration/etat/U58/U7/U7-T12-IMPL.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

20 KiB

Fiche U7-T12 — Niveau N3 — Modèle prévu lp-impl (Opus xhigh) | utilisé Opus 5.5 (claude-opus-5-5) — Agent lp-impl — Skills ue-cpp-build, sk-protocol (présents aussi dans le contexte : lp-build, lp-ue-compat)

U7-T12 — IMPL : coiffure par défaut perdue à la reconstruction (correctifs C1 + C2)

Copie modifiée : D:\Game\LifePlay-master 5.8 (branche ue5.8), seulement Source\LifePlay\LPHairstyleSubsystem.cpp et .h. La copie 5.5 (main) n'a pas été touchée : git -C "D:/Game/LifePlay-master 5.5" status --short est vide. Je n'ai rien compilé ni lancé, je n'ai fait aucun commit et je n'ai rien supprimé. Je n'ai touché à aucun Blueprint.

1. Résultat

PASS-STATIC (proposé, non compilé). C1 et C2 sont écrits. La garde U7-T7 est intacte : le bloc FLPModGuard::IsRefused et la ligne Coiffures : %s ignore (mod refuse) sont en LPHairstyleSubsystem.cpp:241-251, inchangés. C3 n'a pas été fait. Je n'ai touché ni à LPModGuard ni à LPModInfoSubsystem. Aucun critère d'arrêt n'est atteint (§ 2 et § 6).

2. Vérifications préalables (établi)

Point Constat Source
Fins de ligne et encodage avant modification .cpp en ASCII avec LF, sans BOM ; .h en UTF-8 avec LF, sans BOM (seul caractère non ASCII : §, ligne 1) ; dernière ligne terminée par LF. .gitattributes vaut * -text file, grep -c $'\r' = 0, od -c des 3 premiers octets, git ls-files --eol → i/lf w/lf attr/-text
État de départ HEAD + garde U7-T7 (+15 lignes). MD5 avant T12 : .cpp 78e185d11c9eabe0275a85d0a9799ecf, .h 740e790d11592016d6ea4a0568b3564e. L'instantané D:\Game\LifePlay_Backup_5.5\U7\Source\LifePlay\ contient la version d'origine sans la garde (MD5 db610dcf…, 347de153…, manifest.csv) git diff, md5sum
Champs de la structure SMBool (entrée de HairstyleTable) 8 champs : SkeletalMesh_2, Bool_5, Strings_11, Strings2_15, Bool2_19, Bool3_21, SkeletalMeshPath_25, Daz_27 etat/DOC-MOD_LPDumpFunction_20261007-083554.txt:19944-19951, L6f_dump_GI.txt:38472-38520
Champ objet chargé Seul SkeletalMesh est une référence d'objet dure (catégorie object). Aucune catégorie softobject n'apparaît dans la table de noms de Content/Structs/SMBool.uasset. SoftObjectProperty y vient de FStructVariableDescription::SubCategoryObject (TSoftObjectPtr, UE_5.8/.../UnrealEd/Classes/UserDefinedStructure/UserDefinedStructEditorData.h:58). SkeletalMeshPath est un FName (NotEqual_NameName, L6f_dump_GI.txt:38504) extraction des chaînes imprimables de SMBool.uasset (python -I)
Écriture du maillage par le Blueprint GI.LoadHairMeshIfNeeded lit l'entrée (Break) puis la réécrit (Make + Array_Set) en recopiant tous les champs. Seul SkeletalMesh change (getSkeletalMeshFromPath). Un IsValid en tête évite le rechargement si le champ est déjà rempli. Le chemin reste donc la clé de l'entrée L6f_dump_GI.txt:38472-38520
API moteur (5.5 et 5.8) FObjectPropertyBase::GetObjectPropertyValue_InContainer est publique et virtuelle (5.5 UnrealType.h:2653, 5.8 :2896), surchargée publiquement dans FObjectProperty (5.5 :2933, 5.8 :3191). SetObjectPropertyValue_InContainer est publique (5.5 :2658, 5.8 :2901) et non masquée dans TFObjectPropertyBase ni dans FObjectProperty (5.8 :3056-3195). Implémentation 5.8 : PropertyBaseObject.cpp:706-716 (seul refus : valeur nulle sur un champ CPF_NonNullable, cas exclu ici puisque seules les valeurs non nulles sont rendues). TFieldIterator<FObjectProperty> est défini en 5.5 UnrealType.h:6764 et en 5.8 :7193, et Epic l'utilise dans Actor.cpp:945 (5.8) et :861 (5.5) sources moteur installées

Le premier critère d'arrêt n'est pas atteint : le champ objet est lu et écrit proprement par réflexion, avec des API communes à 5.5 et 5.8 et sans garde de version.

3. Diff par fonction (numéros de ligne du fichier modifié)

3.1 ULPHairstyleSubsystem::ScanFiles — .cpp:260-270 (C2)

@@ -257,6 +257,17 @@
 		ParseFile(Relative, Module, Text);
 	}
+	// Signature (lot U7-T12, C2) : modules dont Sync lit l'etat, c'est-a-dire ceux qui apportent des coiffures ou une
+	// pilosite (le fichier de base est applique quel que soit l'etat de son module). Un mod refuse n'y figure pas.
+	for (const FHairDef& Def : Defs)
+	{
+		HairModules.AddUnique(Def.Module);
+	}
+	for (const FTextureDef& Def : Textures)
+	{
+		HairModules.AddUnique(Def.Module);
+	}
+	HairModules.Sort();
 	if (Files.Num() > 0)

3.2 ULPHairstyleSubsystem::EnabledSignature — .cpp:365-377 (C2)

@@ -353,18 +364,14 @@
 FString ULPHairstyleSubsystem::EnabledSignature() const
 {
-	const UObject* GI = GetGameInstance();
-	const UScriptStruct* Struct = nullptr;
-	const FArrayProperty* Prop = StructArray(GI, ModEnabledsName, Struct);
-	if (!Prop)
-	{
-		return FString();
-	}
+	// Lot U7-T12, C2 : seul compte l'etat des modules de coiffures ou de pilosite, dans un ordre fixe, avec la regle de
+	// Sync (IsModuleEnabled : module absent = inactif). Ouvrir le gestionnaire de mods (GI.CheckIfModAlreadyFound insere
+	// les modules absents, decoches), reordonner la liste, cocher un module sans coiffure ou recharger les reglages ne
+	// reconstruit plus la table ; cocher ou decocher un module de coiffures la reconstruit toujours.
 	FString Signature;
-	FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr<void>(GI));
-	for (int32 i = 0; i < Helper.Num(); ++i)
+	for (const FString& Module : HairModules)
 	{
-		Signature += GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("UniqueID")) + TEXT("=") + GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("IsEnabled")) + TEXT(";");
+		Signature += Module + (IsModuleEnabled(Module) ? TEXT("=1;") : TEXT("=0;"));
 	}
 	return Signature;
 }

3.3 ULPHairstyleSubsystem::Sync — .cpp:440, :450-476 (relevé), :583-608 (restitution) (C1)

@@ -430,6 +437,7 @@
 		return;
 	}
+	const bool bRebuild = bCaptured;   // faux au premier passage (Initialize) : construction, pas reconstruction
 	if (!bCaptured)
@@ -439,6 +447,34 @@
 	FScriptArrayHelper Table(TableProp, TableProp->ContainerPtrToValuePtr<void>(GI));
 	FScriptMapHelper Map(MapProp, MapProp->ContainerPtrToValuePtr<void>(GI));
 
+	// 0. Lot U7-T12, C1 : objets deja charges dans la table, par chemin. Le Blueprint remplit SkeletalMesh a la demande
+	// (GI.LoadHairMeshIfNeeded, prechargement des coiffures Daz a l'ecran titre) et GI.UpdateDefaultHair le lit ensuite
+	// sans recharger : la reconstruction ne doit pas l'effacer. Tous les champs objet de la structure sont releves
+	// (SkeletalMesh seul dans SMBool) ; ils sont rendus apres l'etape 2 aux entrees de meme chemin, base et modules.
+	TArray<const FObjectProperty*> ObjectFields;
+	for (TFieldIterator<FObjectProperty> It(Struct); It; ++It)
+	{
+		ObjectFields.Add(*It);
+	}
+	TMap<FString, TArray<UObject*>> Loaded;
+	for (int32 i = 0; i < Table.Num() && ObjectFields.Num() > 0; ++i)
+	{
+		const void* Entry = Table.GetRawPtr(i);
+		TArray<UObject*> Values;
+		bool bAny = false;
+		for (const FObjectProperty* Field : ObjectFields)
+		{
+			UObject* Value = Field->GetObjectPropertyValue_InContainer(Entry);
+			Values.Add(Value);
+			bAny |= Value != nullptr;
+		}
+		const FString Path = GetFieldText(Struct, Entry, TEXT("SkeletalMeshPath"));
+		if (bAny && !Path.IsEmpty())   // entrees NONE (chemin vide) et coiffures jamais chargees : rien a rendre
+		{
+			Loaded.Add(Path, MoveTemp(Values));
+		}
+	}
+
 	// 1. Base : fichier MODE: BASE reconstruit dans son ordre, sinon valeurs du Blueprint.
@@ -543,6 +579,33 @@
 		UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s ajoutee (index %d, %s, module %s)"), ...);
 	}
+
+	// 3. Lot U7-T12, C1 : objets releves a l'etape 0 rendus aux entrees reconstruites de meme chemin. Une coiffure d'un
+	// module desactive n'a plus d'entree : son maillage n'est pas rendu (compte "sans entree").
+	int32 Kept = 0;
+	TSet<FString> KeptPaths;
+	for (int32 i = 0; i < Table.Num() && Loaded.Num() > 0; ++i)
+	{
+		void* Entry = Table.GetRawPtr(i);
+		const FString Path = GetFieldText(Struct, Entry, TEXT("SkeletalMeshPath"));
+		if (const TArray<UObject*>* Values = Loaded.Find(Path))
+		{
+			for (int32 Field = 0; Field < ObjectFields.Num(); ++Field)
+			{
+				if ((*Values)[Field])
+				{
+					ObjectFields[Field]->SetObjectPropertyValue_InContainer(Entry, (*Values)[Field]);
+				}
+			}
+			KeptPaths.Add(Path);
+			++Kept;
+		}
+	}
+	if (bRebuild)
+	{
+		UE_LOG(LogLifePlay, Display, TEXT("Coiffures : %d maillage(s) conserve(s) a la reconstruction (%d chemin(s) charge(s) avant, %d sans entree)"),
+			Kept, Loaded.Num(), Loaded.Num() - KeptPaths.Num());
+	}
 	UE_LOG(LogLifePlay, Display, TEXT("Coiffures : base %d entree(s) ..."), ...);

Le relevé (étape 0) précède le vidage, dans les deux modes : base (EmptyValues) et Blueprint (ImportText_Direct du texte capturé). La restitution (étape 3) vient après l'ajout des modules. Elle couvre donc les entrées de base et celles des modules. La vérification -LPVerifyHairstyles (étape 1) se fait avant la restitution : elle compare toujours la table de base au Blueprint, comme avant.

3.4 LPHairstyleSubsystem.h — commentaire d'en-tête :28-31, déclaration :89, membre :100-101

@@ -27,3 +27,6 @@
-// Les modules desactives dans GI.ModEnableds sont ignores ; la table est reconstruite quand cette liste change.
+// Les modules desactives dans GI.ModEnableds sont ignores ; la table est reconstruite quand l'etat (actif ou non) d'un
+// module qui apporte des coiffures ou une pilosite change, pas pour un autre changement de la liste (ordre, module sans
+// coiffure, gestionnaire de mods ouvert : lot U7-T12, C2). La reconstruction rend aux entrees de meme chemin les maillages
+// deja charges par le Blueprint (GI.LoadHairMeshIfNeeded) : GI.UpdateDefaultHair les lit sans recharger (lot U7-T12, C1).
@@ -85,2 +88,3 @@
 	void SyncTextureLists(UObject* GI);
+	/** Signature : etat actif des modules de HairModules, dans l'ordre trie (independante de l'ordre de GI.ModEnableds). */
 	FString EnabledSignature() const;
@@ -95,2 +99,4 @@
 	FString LastSignature;
+	/** Modules (hors fichier de base) qui apportent des coiffures ou une pilosite, tries : seuls a compter dans la signature. */
+	TArray<FString> HairModules;

Initialize, Tick, IsModuleEnabled, EnsureDefaultEnabledModules, ParseFile et SyncTextureLists sont inchangés. ModEnabledsName reste utilisé, par IsModuleEnabled et EnsureDefaultEnabledModules.

4. git diff --stat

Par rapport à HEAD (git -C "D:/Game/LifePlay-master 5.8" diff --stat -- Source/LifePlay/LPHairstyleSubsystem.*), garde U7-T7 comprise :

 Source/LifePlay/LPHairstyleSubsystem.cpp | 96 ++++++++++++++++++++++++++++----
 Source/LifePlay/LPHairstyleSubsystem.h   | 10 +++-
 2 files changed, 95 insertions(+), 11 deletions(-)

Part de T12 seule (git diff --no-index --stat contre la copie d'avant T12, dans le scratchpad de session) : .cpp 73 insertions(+), 10 deletions(-) ; .h 7 insertions(+), 1 deletion(-). Lignes : .cpp 619 → 682, .h 112 → 118. MD5 après T12 : .cpp 5b7d8d5b2cb9c60aa775ecf3470685ab, .h 1edf6a9381210def5a2fda09c0315f3f. git status --short de la copie 5.8 : la liste des fichiers modifiés est la même qu'avant T12 (U7-T7/T9). Aucun fichier nouveau.

5. Fins de ligne et encodage (vérifiés après modification)

grep -c $'\r' = 0 pour les deux fichiers. file donne .cpp en « ASCII text » et .h en « UTF-8 text », comme avant. Les fichiers ne commencent pas par un BOM (premiers octets //) et se terminent par un LF. git ls-files --eol donne i/lf w/lf attr/-text. Le texte ajouté est entièrement en ASCII.

6. Comportement attendu (estimé, raisonnement sur le code)

  • C2 : HairModules regroupe les modules non refusés qui ont au moins une définition HAIRSTYLE ou de pilosité dans un .lphair non base. Dans la copie 5.8 et dans le build U7c sans mod, il n'existe que Content/Modules/vin_Base/Hairstyles/vin_Base.lphair, avec MODE: BASE (find … -iname "*.lphair"). HairModules est donc vide, la signature vaut "" en permanence et aucune reconstruction n'a lieu : ni à l'ouverture ou à la fermeture du gestionnaire, ni quand on coche un module texte, ni à LoadSettings. La coiffure préchargée à l'écran titre reste dans la table.
  • Module de coiffures coché ou décoché : son état entre dans la signature par IsModuleEnabled, la règle même de Sync. Le changement déclenche la reconstruction, et Sync saute ses définitions s'il est décoché : ses coiffures disparaissent toujours. Le second critère d'arrêt n'est pas atteint. Ses maillages relevés n'ont plus d'entrée : ils sont comptés « sans entrée » et ne sont pas rendus.
  • Mod refusé par la garde (Pocolov en 5.8) : ses fichiers ne sont pas lus. Il ne figure donc ni dans Defs ni dans HairModules, et la case du gestionnaire ne reconstruit rien. La ligne Coiffures : lpr_hair_pocolov ignore (mod refuse) est inchangée.
  • C1 : à chaque reconstruction, chaque entrée dont le chemin existait déjà retrouve son SkeletalMesh. Avec un module de coiffures accepté, l'indice 38 (ToulouseHR) garde donc son maillage après un basculement. LoadHairMeshIfNeeded voit ensuite un champ valide et ne recharge rien.

Journal attendu

  • Démarrage : inchangé (Coiffures : N fichier(s) .lphair ..., Coiffures : base ...). La nouvelle ligne n'apparaît pas au premier Sync (bRebuild faux).
  • À chaque reconstruction, juste après Coiffures : liste des modules actifs modifiee, reconstruction (niveau Log) : LogLifePlay: Display: Coiffures : N maillage(s) conserve(s) a la reconstruction (M chemin(s) charge(s) avant, K sans entree), puis la ligne Coiffures : base ... habituelle.
  • Build U7c sans mod : aucune de ces deux lignes, quelle que soit la manipulation du gestionnaire. Aucune ligne Accessed None ... SkeletalMesh_2_93573D... avec UpdateDefaultHair dans la pile.

7. Essai proposé (après le BUILD par lp-builder et un nouveau build empaqueté ; à faire par l'utilisateur)

Le build D:\Game\LifePlay_Build_5.8_U7c contient l'ancienne DLL. L'essai demande un build empaqueté neuf qui contient T12 (Tools\BuildDebug.ps1, dossier neuf, Mods\ vide) ou, à défaut, une copie jetable mise à jour.

  1. Sans mod, sans SaveGames : à l'écran titre, ouvrir le gestionnaire de mods puis le fermer sans rien changer. Faire New Game puis Female. Attendu : Bridget coiffée (ToulouseHR) ; LifePlay.log sans reconstruction, sans maillage(s) conserve(s), sans Accessed None ... SkeletalMesh_2_93573D.
  2. Même chose en cochant puis décochant un module texte (vin_NTR par exemple) avant New Game. Attendu identique.
  3. (C1, facultatif) Le build sans mod ne déclenche aucune reconstruction : C1 n'y est donc pas exercé. Pour le voir à l'œuvre, il faut un module de coiffures accepté par la garde : coché puis décoché à l'écran titre, après le préchargement. Attendu : N maillage(s) conserve(s) avec N proche du nombre de coiffures Daz préchargées, puis Bridget coiffée.

8. Écarts à la SPEC et à la fiche

  • E1 (forme, généralisation demandée) : l'esquisse de U7-T11 visait le seul champ SkeletalMesh, par FindField(Struct, "SkeletalMesh"). J'ai relevé tous les champs FObjectProperty de la structure, par TFieldIterator<FObjectProperty>, comme le demande la décision C1 (« tout autre champ objet chargé »). Sur SMBool, on n'obtient que SkeletalMesh (§ 2). Le message du journal parle de « maillage(s) » et compte des entrées rendues : c'est égal au nombre de maillages tant que SkeletalMesh est le seul champ objet.
  • E2 (ajout) : la ligne de journal précise aussi M chemin(s) charge(s) avant, K sans entree, pour rendre visible la disparition des maillages d'un module décoché. Elle n'est émise qu'en reconstruction, pas au premier Sync.
  • E3 (C2, périmètre de la signature) : le module du fichier MODE: BASE n'entre pas dans la signature. Sync applique la base sans regarder l'état de ce module (code inchangé, étape 1, .cpp:478-524) : sa case ne changeait déjà rien au résultat. L'état retenu est l'état effectif (IsModuleEnabled : absent = inactif), plutôt que le texte brut IsEnabled. Un module de coiffures absent de la liste puis inséré décoché par le gestionnaire ne reconstruit donc plus rien.
  • E4 (instantané) : l'instantané Backup_5.5\U7 conserve la version d'origine, sans la garde. L'état d'avant T12 (avec la garde T7) n'a pas d'instantané durable. Je n'en ai pas créé dans Backup_5.5, qui est hors des chemins autorisés. Retour arrière : appliquer à l'envers le diff du § 3, MD5 attendus d'avant T12 au § 2.
  • Rien d'autre : pas de C3, aucune modification de LPModGuard, de LPModInfoSubsystem ni d'un Blueprint, pas de compilation.

9. Incertitudes et risques

  • Incertain : compilation. Le code est « proposé, non compilé ». À établir par le BUILD 5.8 (Tools\Build.ps1) : on attend 0 erreur et aucun avertissement nouveau dans LPHairstyleSubsystem.cpp. Les noms locaux (Field, Entry, Path, Values) sont déclarés dans des portées disjointes et ne masquent aucun membre de la classe (relu). Le risque d'un avertissement de masquage est faible.
  • Estimé : pas de ramasse-miettes pendant Sync(). Entre le vidage et la restitution, les maillages ne sont tenus que par des pointeurs bruts locaux. Sync() est synchrone sur le fil de jeu et n'appelle aucun chargement (registre d'assets, DoesPackageExist, ImportText d'un texte où les maillages valent None) : aucune collecte n'est attendue. La restitution passe par SetObjectPropertyValue_InContainer (affectation d'un TObjectPtr).
  • Estimé : en éditeur, une reconstruction fortuite (provoquée par n'importe quel changement de la liste) pouvait rattraper des coiffures de module encore absentes du registre au premier Sync. Elle n'a plus lieu. Le repli « paquet présent » (.cpp:549-554) ajoute déjà ces coiffures au premier passage. Le risque est faible et ne touche que l'éditeur.
  • Hors périmètre, pour information : LPDirectControlSubsystem.cpp:198-215 et :626-635 calculent leur propre signature sur toute la liste ModEnableds, ordre compris, et ré-indexent les invites à chaque changement. Je n'ai constaté aucune perte d'objet chargé à cet endroit. Je n'y ai pas touché.

10. Compte rendu sk-protocol

[sk-protocol] Skills dans le contexte : sk-protocol, ue-cpp-build, lp-build, lp-ue-compat.

  1. Résultat : réussi en statique (PASS-STATIC). C1 et C2 sont écrits dans la copie 5.8 seulement, la garde U7-T7 est intacte.
  2. Preuves : diff au § 3 ; API à UnrealType.h 5.5 :2653/:2658/:2933/:6764 et 5.8 :2896/:2901/:3191/:7193, PropertyBaseObject.cpp:706-716 ; structure à DOC-MOD…083554.txt:19944-19951 et L6f_dump_GI.txt:38472-38520.
  3. Chiffres : git diff --stat 95+/11- au total, dont T12 : .cpp 73+/10-, .h 7+/1- ; 0 CR ; 1 seul .lphair (base) dans Content/Modules.
  4. Incertitudes : la compilation (non faite, interdite) ; C1 n'est pas exercé par l'essai sans mod (§ 7.3).
  5. Écarts : E1 à E4 (§ 8). Fichiers écrits : D:\Game\LifePlay-master 5.8\Source\LifePlay\LPHairstyleSubsystem.cpp, D:\Game\LifePlay-master 5.8\Source\LifePlay\LPHairstyleSubsystem.h, ce livrable.
  6. Suite : lp-builder doit compiler la copie 5.8 (Tools\Build.ps1 -Lot U7-T12), puis l'utilisateur doit faire l'essai du § 7 sur un build empaqueté neuf ; état : code écrit, non compilé, non commité.