From f8bdf90bb21f10bd97edf4ab324cdc5370693b7b Mon Sep 17 00:00:00 2001 From: edujiro5972 <336072401+edujiro5972@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:45:57 +0200 Subject: [PATCH] H2b : pilosite (barbe, torse, pubis) dans les fichiers .lphair Listes GI.FHairstyles, BHairstyles_M et BHairstyles reconstruites depuis vin_Base.lphair, completees par les modules ; verification : identiques au Blueprint. Co-Authored-By: Claude Opus 5.5 --- Docs/Migration/11_Audit_Optimisation.md | 17 ++++ Docs/Migration/Journal_Modifications.md | 1 + Source/LifePlay/LPHairstyleSubsystem.cpp | 101 ++++++++++++++++++++++- Source/LifePlay/LPHairstyleSubsystem.h | 21 +++++ 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/Docs/Migration/11_Audit_Optimisation.md b/Docs/Migration/11_Audit_Optimisation.md index c11310f..20e9cf3 100644 --- a/Docs/Migration/11_Audit_Optimisation.md +++ b/Docs/Migration/11_Audit_Optimisation.md @@ -655,6 +655,23 @@ générés normaux, Continue. **Usage pour un moddeur** : ajouter une coiffure = un fichier `.lphair` dans son module ; modifier la liste de base (retirer une coiffure du tirage, changer son sexe, son morph) = éditer `vin_Base.lphair`, sans éditeur Unreal. +### 16.10 H2b : pilosité (barbe, torse, pubis) en fichier de données (2026-10-02) + +**Constat** : la pilosité n'est pas faite de maillages mais de **textures** appliquées à la peau par +`Person.setBHairstyle` (paramètre de matériau `BHairstyle`), cherchées dans un dossier fixe + nom : hommes +`/Game/Heavy/Daz/Michael/hairs/other/`, femmes `/Game/Heavy/Daz/Bridget/Hairs/Pubic/`. Les noms tirés viennent de trois +listes de `GI` : `FHairstyles` (barbe, 29 noms dont `NoHair`), `BHairstyles_M` (torse masculin, 6) et `BHairstyles` +(pubis féminin, 9). `GI.OtherTable` (13 barbes et pilosités en maillages sous `/Game/Heavy/Characters/Hair/Other`) est +un reste inutilisable : ces maillages sont absents du projet, déjà en 4.27 ; elle n'est pas reprise. + +**Réalisation** : trois clés de plus dans les fichiers `.lphair` — `FACIAL_HAIR`, `BODY_HAIR_M`, `BODY_HAIR_F` (un nom +de texture par ligne). Le fichier de base `vin_Base.lphair` reconstruit chaque liste dans l'ordre du Blueprint ; les +autres modules y ajoutent des noms, après contrôle qu'une texture de ce nom existe dans le dossier imposé (la texture +d'un mod doit donc être cuite dans ce dossier). + +**Vérification** (`-LPVerifyHairstyles`, build `LifePlay_Build_5.5_H2`) : `FHairstyles`, `BHairstyles_M` et +`BHairstyles` « IDENTIQUE au Blueprint » ; coiffures et morphs toujours identiques. + ## 17. Couche de données : stratégie (2026-10-02) Proposition utilisateur : convertir les fichiers texte de `Modules` en fichiers JSON groupés par type, avec un calque de diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index 09469ac..f352d99 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -934,3 +934,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol | CHG-087 | 2026-10-02 | O7e | `Pocolov17_Cap` (hors dépôt) : `MapOpacity` = texture blanche du moteur ; `11_Audit_Optimisation.md` §16.8 | second essai en jeu correct (crâne couvert, couleur appliquée, profil bien placé) ; trous au-dessus de l'oreille dus à la lisière découpée de la calotte ; retouche choisie par l'utilisateur ; build `LifePlay_Build_5.5_O7e` reconstruit | remettre `Pocolov17_CapOpacity` dans `MapOpacity` | | CHG-088 | 2026-10-02 | O7e | — | O7e validé en jeu par l'utilisateur (profil sans trou, mouvement suivi) ; défaut mineur accepté : bas de la calotte visible sous le lobe de l'oreille | — | | CHG-089 | 2026-10-02 | H2 | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (mode base, plusieurs coiffures par fichier, `NONE`, `MORPHABLE`, vérification `-LPVerifyHairstyles`) ; nouveau `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (126 coiffures, 89 morphs) ; `11_Audit_Optimisation.md` §16.9 | liste de base des coiffures sortie du Blueprint (demande utilisateur) ; vérifié : table et morphs reconstruits identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; validé en jeu le 2026-10-02 (nouvelle partie, liste du menu inchangée avec Pocolov17 en dernier, personnages générés, Continue) | supprimer `vin_Base.lphair` (retour à la table du Blueprint) ; `git checkout H2-avant -- Source/LifePlay` | +| CHG-090 | 2026-10-02 | H2b | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (clés `FACIAL_HAIR`, `BODY_HAIR_M`, `BODY_HAIR_F`) ; `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (+ 44 noms) ; `11_Audit_Optimisation.md` §16.10 | pilosité sortie du Blueprint (demande utilisateur) : listes `GI.FHairstyles`, `BHairstyles_M`, `BHairstyles` ; `GI.OtherTable` laissée (maillages absents depuis 4.27) ; vérifié : 3 listes identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; test en jeu en attente | retirer les lignes `*_HAIR` de `vin_Base.lphair` ; `git checkout H2b-avant -- Source/LifePlay` | diff --git a/Source/LifePlay/LPHairstyleSubsystem.cpp b/Source/LifePlay/LPHairstyleSubsystem.cpp index 179edab..61cbd1d 100644 --- a/Source/LifePlay/LPHairstyleSubsystem.cpp +++ b/Source/LifePlay/LPHairstyleSubsystem.cpp @@ -4,7 +4,9 @@ #include "AssetRegistry/IAssetRegistry.h" #include "Engine/GameInstance.h" +#include "Algo/Find.h" #include "Engine/SkeletalMesh.h" +#include "Engine/Texture2D.h" #include "HAL/FileManager.h" #include "HAL/PlatformTime.h" #include "LifePlay.h" @@ -20,6 +22,19 @@ namespace const FName ModEnabledsName(TEXT("ModEnableds")); constexpr double CheckInterval = 2.0; + /** Listes de pilosite : cle du fichier, propriete de GI, dossier des textures (prefixe de Person.setBHairstyle). */ + struct FTextureList + { + const TCHAR* Key; + const TCHAR* Property; + const TCHAR* Folder; + }; + const FTextureList TextureLists[] = { + { TEXT("FACIAL_HAIR"), TEXT("FHairstyles"), TEXT("/Game/Heavy/Daz/Michael/hairs/other/") }, + { TEXT("BODY_HAIR_M"), TEXT("BHairstyles_M"), TEXT("/Game/Heavy/Daz/Michael/hairs/other/") }, + { TEXT("BODY_HAIR_F"), TEXT("BHairstyles"), TEXT("/Game/Heavy/Daz/Bridget/Hairs/Pubic/") }, + }; + /** Champ d'une structure Blueprint par son nom d'auteur (les noms internes portent un suffixe GUID). */ const FProperty* FindField(const UStruct* Struct, const TCHAR* AuthoredName) { @@ -131,7 +146,7 @@ void ULPHairstyleSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); ScanFiles(); - if (Defs.Num() == 0 && BaseDefs.Num() == 0) + if (Defs.Num() == 0 && BaseDefs.Num() == 0 && Textures.Num() == 0 && BaseTextures.Num() == 0) { return; } @@ -190,8 +205,8 @@ void ULPHairstyleSubsystem::ScanFiles() } if (Files.Num() > 0) { - UE_LOG(LogLifePlay, Display, TEXT("Coiffures : %d fichier(s) .lphair, base %s (%d coiffure(s), %d morph(s)), %d coiffure(s) de modules"), - Files.Num(), BaseFile.IsEmpty() ? TEXT("du Blueprint") : *BaseFile, BaseDefs.Num(), BaseMorphs.Num(), Defs.Num()); + UE_LOG(LogLifePlay, Display, TEXT("Coiffures : %d fichier(s) .lphair, base %s (%d coiffure(s), %d morph(s), %d pilosite(s)), modules : %d coiffure(s), %d pilosite(s)"), + Files.Num(), BaseFile.IsEmpty() ? TEXT("du Blueprint") : *BaseFile, BaseDefs.Num(), BaseMorphs.Num(), BaseTextures.Num(), Defs.Num(), Textures.Num()); } } @@ -199,6 +214,7 @@ void ULPHairstyleSubsystem::ParseFile(const FString& Relative, const FString& Mo { TArray FileDefs; TArray FileMorphs; + TArray FileTextures; bool bBase = false; TArray Lines; Text.ParseIntoArrayLines(Lines, true); @@ -238,6 +254,10 @@ void ULPHairstyleSubsystem::ParseFile(const FString& Relative, const FString& Mo FileMorphs.Add({ Parts[0], Parts.IsValidIndex(1) ? Parts[1] : FString(), Parts.IsValidIndex(2) ? Parts[2] : FString() }); } } + else if (const FTextureList* List = Algo::FindByPredicate(TextureLists, [&Key](const FTextureList& L) { return Key == L.Key; })) + { + FileTextures.Add({ int32(List - TextureLists), Value, Relative, Module }); + } else if (FileDefs.Num() == 0) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s, ligne '%s' avant toute ligne HAIRSTYLE, ignoree"), *Relative, *Line); @@ -266,13 +286,15 @@ void ULPHairstyleSubsystem::ParseFile(const FString& Relative, const FString& Mo BaseFile = Relative; BaseDefs = MoveTemp(FileDefs); BaseMorphs = MoveTemp(FileMorphs); + BaseTextures = MoveTemp(FileTextures); return; } - if (FileDefs.Num() == 0) + if (FileDefs.Num() == 0 && FileTextures.Num() == 0) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s sans ligne HAIRSTYLE, ignore"), *Relative); } Defs.Append(MoveTemp(FileDefs)); + Textures.Append(MoveTemp(FileTextures)); } FString ULPHairstyleSubsystem::EnabledSignature() const @@ -433,4 +455,75 @@ void ULPHairstyleSubsystem::Sync() } UE_LOG(LogLifePlay, Display, TEXT("Coiffures : base %d entree(s) (%s), %d ajoutee(s) par les modules, %d ignoree(s), table de %d entree(s), %d morph(s)"), BaseCount, BaseFile.IsEmpty() ? TEXT("Blueprint") : *BaseFile, Added, Skipped, Table.Num(), Map.Num()); + SyncTextureLists(GI); +} + +void ULPHairstyleSubsystem::SyncTextureLists(UObject* GI) +{ + static const bool bVerify = FParse::Param(FCommandLine::Get(), TEXT("LPVerifyHairstyles")); + IAssetRegistry& Registry = IAssetRegistry::GetChecked(); + const bool bFirst = BlueprintListTexts.Num() == 0; + for (int32 ListIndex = 0; ListIndex < UE_ARRAY_COUNT(TextureLists); ++ListIndex) + { + const FTextureList& Spec = TextureLists[ListIndex]; + const FArrayProperty* Prop = CastField(GI->GetClass()->FindPropertyByName(Spec.Property)); + if (!Prop || !CastField(Prop->Inner)) + { + UE_LOG(LogLifePlay, Error, TEXT("Pilosite : GI.%s introuvable ou de type inattendu"), Spec.Property); + if (bFirst) + { + BlueprintListTexts.Add(FString()); + } + continue; + } + TArray& List = *Prop->ContainerPtrToValuePtr>(GI); + if (bFirst) + { + BlueprintListTexts.Add(ExportProperty(Prop, GI)); + } + + // 1. Base : noms du fichier de base pour cette liste, sinon valeur du Blueprint. + TArray Base; + for (const FTextureDef& Def : BaseTextures) + { + if (Def.List == ListIndex) + { + Base.Add(Def.Name); + } + } + if (Base.Num() > 0) + { + List = Base; + if (bVerify) + { + const bool bSame = ExportProperty(Prop, GI) == BlueprintListTexts[ListIndex]; + UE_LOG(LogLifePlay, Display, TEXT("Pilosite, verification de la base : GI.%s %s (%d noms)"), Spec.Property, + bSame ? TEXT("IDENTIQUE au Blueprint") : TEXT("DIFFERENTE du Blueprint"), List.Num()); + } + } + else + { + Prop->ImportText_Direct(*BlueprintListTexts[ListIndex], &List, GI, PPF_None); + } + const int32 BaseCount = List.Num(); + + // 2. Noms des modules actifs : texture presente dans le dossier impose, pas de doublon. + for (const FTextureDef& Def : Textures) + { + if (Def.List != ListIndex || !IsModuleEnabled(Def.Module) || List.Contains(Def.Name)) + { + continue; + } + const FString Path = FString(Spec.Folder) + Def.Name + TEXT(".") + Def.Name; + const FAssetData Asset = Registry.GetAssetByObjectPath(FSoftObjectPath(Path)); + if (!Asset.IsValid() || !Asset.IsInstanceOf(UTexture2D::StaticClass())) + { + UE_LOG(LogLifePlay, Warning, TEXT("Pilosite : %s introuvable ou n'est pas une texture (%s), ignore"), *Path, *Def.File); + continue; + } + List.Add(Def.Name); + UE_LOG(LogLifePlay, Log, TEXT("Pilosite : %s ajoute a GI.%s (module %s)"), *Def.Name, Spec.Property, *Def.Module); + } + UE_LOG(LogLifePlay, Display, TEXT("Pilosite : GI.%s, base %d nom(s), %d ajoute(s) par les modules"), Spec.Property, BaseCount, List.Num() - BaseCount); + } } diff --git a/Source/LifePlay/LPHairstyleSubsystem.h b/Source/LifePlay/LPHairstyleSubsystem.h index 34f14dd..74eb34e 100644 --- a/Source/LifePlay/LPHairstyleSubsystem.h +++ b/Source/LifePlay/LPHairstyleSubsystem.h @@ -15,6 +15,13 @@ // MODE: BASE fichier de base (H2) : la table est reconstruite a partir de // ce fichier, dans son ordre, avant l'ajout des autres modules // +// Pilosite (textures appliquees a la peau par Person.setBHairstyle, lot H2b) : listes de noms de textures, dans le +// dossier impose par le Blueprint (prefixe + nom) : +// FACIAL_HAIR: Facial_Hair__1_ GI.FHairstyles (barbe ; /Game/Heavy/Daz/Michael/hairs/other/) +// BODY_HAIR_M: JPBH78-TorsoNG-01 GI.BHairstyles_M (torse masculin ; meme dossier) +// BODY_HAIR_F: Pubes01 GI.BHairstyles (pubis feminin ; /Game/Heavy/Daz/Bridget/Hairs/Pubic/) +// Le fichier de base reconstruit chaque liste qu'il decrit ; les autres modules y ajoutent des noms (texture verifiee). +// // Sans fichier de base, les coiffures sont ajoutees en fin de la table du Blueprint (H1). Les coiffures des modules // sont controlees (SkeletalMesh present, pas de doublon) ; celles du fichier de base sont reprises telles quelles, // comme dans le Blueprint (35 coiffures "legacy" absentes du projet depuis 4.27 y figurent). @@ -51,6 +58,14 @@ private: FString FitValue; }; + struct FTextureDef + { + int32 List = 0; + FString Name; + FString File; + FString Module; + }; + struct FMorphDef { FString Key; @@ -64,6 +79,8 @@ private: void ParseFile(const FString& Relative, const FString& Module, const FString& Text); /** Reconstruit la table : base (fichier MODE: BASE ou Blueprint) puis coiffures des modules actifs. */ void Sync(); + /** Listes de pilosite : base (fichier ou Blueprint) puis noms des modules actifs. */ + void SyncTextureLists(UObject* GI); FString EnabledSignature() const; bool IsModuleEnabled(const FString& Module) const; @@ -76,6 +93,10 @@ private: TArray BaseMorphs; /** Coiffures des autres fichiers. */ TArray Defs; + /** Noms de textures de pilosite : fichier de base et autres fichiers ; listes du Blueprint (texte exporte). */ + TArray BaseTextures; + TArray Textures; + TArray BlueprintListTexts; /** Table et morphs du Blueprint, captures au premier passage (texte exporte, pour revenir a la base sans fichier). */ FString BlueprintTableText; FString BlueprintMorphsText;