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 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-10-02 16:45:57 +02:00
parent 991d1999f7
commit f8bdf90bb2
4 changed files with 136 additions and 4 deletions

View file

@ -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

View file

@ -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` |

View file

@ -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<FHairDef> FileDefs;
TArray<FMorphDef> FileMorphs;
TArray<FTextureDef> FileTextures;
bool bBase = false;
TArray<FString> 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<FArrayProperty>(GI->GetClass()->FindPropertyByName(Spec.Property));
if (!Prop || !CastField<FStrProperty>(Prop->Inner))
{
UE_LOG(LogLifePlay, Error, TEXT("Pilosite : GI.%s introuvable ou de type inattendu"), Spec.Property);
if (bFirst)
{
BlueprintListTexts.Add(FString());
}
continue;
}
TArray<FString>& List = *Prop->ContainerPtrToValuePtr<TArray<FString>>(GI);
if (bFirst)
{
BlueprintListTexts.Add(ExportProperty(Prop, GI));
}
// 1. Base : noms du fichier de base pour cette liste, sinon valeur du Blueprint.
TArray<FString> 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);
}
}

View file

@ -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<FMorphDef> BaseMorphs;
/** Coiffures des autres fichiers. */
TArray<FHairDef> Defs;
/** Noms de textures de pilosite : fichier de base et autres fichiers ; listes du Blueprint (texte exporte). */
TArray<FTextureDef> BaseTextures;
TArray<FTextureDef> Textures;
TArray<FString> BlueprintListTexts;
/** Table et morphs du Blueprint, captures au premier passage (texte exporte, pour revenir a la base sans fichier). */
FString BlueprintTableText;
FString BlueprintMorphsText;