LifePlay-Revival/Source/LifePlay/LPHairstyleSubsystem.cpp
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

682 lines
24 KiB
C++

// Portage UE 5.5 - voir LPHairstyleSubsystem.h
#include "LPHairstyleSubsystem.h"
#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"
#include "LPModGuard.h"
#include "Misc/CommandLine.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
#include "UObject/UnrealType.h"
namespace
{
const FName HairstyleTableName(TEXT("HairstyleTable"));
const FName MorphableHairsName(TEXT("MorphableHairs"));
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)
{
for (TFieldIterator<FProperty> It(Struct); It; ++It)
{
if (It->GetAuthoredName() == AuthoredName)
{
return *It;
}
}
return nullptr;
}
/** Affecte un champ a partir de son texte (FName, FString, bool, flottant : meme syntaxe que les valeurs par defaut). */
bool SetField(const UStruct* Struct, void* Data, const TCHAR* AuthoredName, const FString& Text)
{
const FProperty* Prop = FindField(Struct, AuthoredName);
return Prop && Prop->ImportText_InContainer(*Text, Data, nullptr, PPF_None) != nullptr;
}
/** Vide un champ texte (chaine vide, nom nul) : ImportText ne sait pas produire une chaine vide. */
bool ClearField(const UStruct* Struct, void* Data, const TCHAR* AuthoredName)
{
const FProperty* Prop = FindField(Struct, AuthoredName);
if (const FStrProperty* Str = CastField<FStrProperty>(Prop))
{
Str->SetPropertyValue_InContainer(Data, FString());
return true;
}
if (const FNameProperty* Name = CastField<FNameProperty>(Prop))
{
Name->SetPropertyValue_InContainer(Data, NAME_None);
return true;
}
return false;
}
FString GetFieldText(const UStruct* Struct, const void* Data, const TCHAR* AuthoredName)
{
FString Out;
if (const FProperty* Prop = FindField(Struct, AuthoredName))
{
Prop->ExportTextItem_InContainer(Out, Data, nullptr, nullptr, PPF_None);
}
return Out.TrimQuotes();
}
const FArrayProperty* StructArray(const UObject* GI, FName Name, const UScriptStruct*& OutStruct)
{
const FArrayProperty* Prop = GI ? CastField<FArrayProperty>(GI->GetClass()->FindPropertyByName(Name)) : nullptr;
const FStructProperty* Inner = Prop ? CastField<FStructProperty>(Prop->Inner) : nullptr;
OutStruct = Inner ? Inner->Struct : nullptr;
return OutStruct ? Prop : nullptr;
}
FString ExportProperty(const FProperty* Prop, const UObject* GI)
{
FString Out;
Prop->ExportTextItem_Direct(Out, Prop->ContainerPtrToValuePtr<void>(GI), nullptr, const_cast<UObject*>(GI), PPF_None);
return Out;
}
/** Entrees d'une map "cle=valeur", triees : comparaison independante de l'ordre interne. */
FString ExportMapSorted(const FMapProperty* MapProp, const UObject* GI)
{
TArray<FString> Lines;
FScriptMapHelper Map(MapProp, MapProp->ContainerPtrToValuePtr<void>(GI));
for (FScriptMapHelper::FIterator It(Map); It; ++It)
{
FString Key, Value;
MapProp->KeyProp->ExportTextItem_Direct(Key, Map.GetKeyPtr(It), nullptr, nullptr, PPF_None);
MapProp->ValueProp->ExportTextItem_Direct(Value, Map.GetValuePtr(It), nullptr, nullptr, PPF_None);
Lines.Add(Key + TEXT("=") + Value);
}
Lines.Sort();
return FString::Join(Lines, TEXT("\n"));
}
bool AddMorph(const FMapProperty* MapProp, UObject* GI, const FString& Key, const FString& Morph, const FString& Value)
{
const FStructProperty* ValueProp = CastField<FStructProperty>(MapProp->ValueProp);
if (!ValueProp)
{
return false;
}
FScriptMapHelper Map(MapProp, MapProp->ContainerPtrToValuePtr<void>(GI));
for (FScriptMapHelper::FIterator It(Map); It; ++It)
{
FString Existing;
MapProp->KeyProp->ExportTextItem_Direct(Existing, Map.GetKeyPtr(It), nullptr, nullptr, PPF_None);
if (Existing.TrimQuotes().Equals(Key, ESearchCase::IgnoreCase))
{
return false;
}
}
const int32 Pair = Map.AddDefaultValue_Invalid_NeedsRehash();
MapProp->KeyProp->ImportText_Direct(*Key, Map.GetKeyPtr(Pair), nullptr, PPF_None);
if (!Morph.IsEmpty())
{
SetField(ValueProp->Struct, Map.GetValuePtr(Pair), TEXT("Name"), Morph);
SetField(ValueProp->Struct, Map.GetValuePtr(Pair), TEXT("Float"), Value);
}
Map.Rehash();
return true;
}
}
void ULPHairstyleSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
ScanFiles();
if (Defs.Num() == 0 && BaseDefs.Num() == 0 && Textures.Num() == 0 && BaseTextures.Num() == 0)
{
return;
}
EnsureDefaultEnabledModules();
LastSignature = EnabledSignature();
Sync();
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPHairstyleSubsystem::Tick));
}
void ULPHairstyleSubsystem::Deinitialize()
{
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
Super::Deinitialize();
}
bool ULPHairstyleSubsystem::Tick(float DeltaTime)
{
const double Now = FPlatformTime::Seconds();
if (Now < NextCheck)
{
return true;
}
NextCheck = Now + CheckInterval;
// GI.LoadSettings remplace GI.ModEnableds par la liste sauvegardee : les modules actifs par defaut y sont remis.
EnsureDefaultEnabledModules();
const FString Signature = EnabledSignature();
if (Signature != LastSignature)
{
LastSignature = Signature;
UE_LOG(LogLifePlay, Log, TEXT("Coiffures : liste des modules actifs modifiee, reconstruction"));
Sync();
}
return true;
}
void ULPHairstyleSubsystem::ScanFiles()
{
const FString ModulesDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir() / TEXT("Modules"));
// Modules actifs par defaut : Modules/<id>/<id>.lpmod avec MODULE_DEFAULT_ENABLED: true.
TArray<FString> ModFiles;
IFileManager::Get().FindFilesRecursive(ModFiles, *ModulesDir, TEXT("*.lpmod"), true, false);
for (const FString& ModFile : ModFiles)
{
FString ModText;
if (!FFileHelper::LoadFileToString(ModText, *ModFile))
{
continue;
}
FString UniqueId;
bool bDefault = false;
TArray<FString> ModLines;
ModText.ParseIntoArrayLines(ModLines, true);
for (const FString& ModLine : ModLines)
{
FString Key, Value;
if (ModLine.Split(TEXT(":"), &Key, &Value))
{
Key = Key.TrimStartAndEnd().ToUpper();
Value = Value.TrimStartAndEnd();
if (Key == TEXT("MODULE_UNIQUEID"))
{
UniqueId = Value;
}
else if (Key == TEXT("MODULE_DEFAULT_ENABLED"))
{
bDefault = Value.Equals(TEXT("true"), ESearchCase::IgnoreCase);
}
}
}
if (bDefault && !UniqueId.IsEmpty())
{
DefaultEnabledModules.AddUnique(UniqueId);
}
}
TArray<FString> Files;
IFileManager::Get().FindFilesRecursive(Files, *ModulesDir, TEXT("*.lphair"), true, false);
Files.Sort();
TSet<FString> Refused; // garde des mods (lot U7) : mods refuses deja signales
for (const FString& File : Files)
{
FString Relative = File;
FPaths::MakePathRelativeTo(Relative, *(ModulesDir + TEXT("/")));
FString Module, Rest;
if (!Relative.Split(TEXT("/"), &Module, &Rest))
{
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s ignore (doit etre dans un dossier de module)"), *Relative);
continue;
}
// Garde des mods (lot U7) : aucun fichier d'un mod refuse n'est lu, rien n'arrive dans les listes de GI.
if (FLPModGuard::IsRefused(Module))
{
bool bAlready = false;
Refused.Add(Module, &bAlready);
if (!bAlready)
{
UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s ignore (mod refuse)"), *Module);
}
continue;
}
FString Text;
if (!FFileHelper::LoadFileToString(Text, *File))
{
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : lecture impossible de %s"), *Relative);
continue;
}
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)
{
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());
}
}
void ULPHairstyleSubsystem::ParseFile(const FString& Relative, const FString& Module, const FString& Text)
{
TArray<FHairDef> FileDefs;
TArray<FMorphDef> FileMorphs;
TArray<FTextureDef> FileTextures;
bool bBase = false;
TArray<FString> Lines;
Text.ParseIntoArrayLines(Lines, true);
for (const FString& RawLine : Lines)
{
const FString Line = RawLine.TrimStartAndEnd();
FString Key, Value;
if (Line.StartsWith(TEXT("//")) || !Line.Split(TEXT(":"), &Key, &Value))
{
continue;
}
Key = Key.TrimStartAndEnd().ToUpper();
Value = Value.TrimStartAndEnd();
if (Key == TEXT("MODE"))
{
bBase = Value.Equals(TEXT("BASE"), ESearchCase::IgnoreCase);
}
else if (Key == TEXT("HAIRSTYLE"))
{
FHairDef& Def = FileDefs.AddDefaulted_GetRef();
Def.File = Relative;
Def.Module = Module;
Def.bNone = Value.Equals(TEXT("NONE"), ESearchCase::IgnoreCase);
Def.Path = Def.bNone ? FString() : Value;
// Un chemin de paquet sans nom d'objet (/Game/X/Nom) designe l'objet du meme nom.
if (!Def.bNone && !Def.Path.Contains(TEXT(".")))
{
Def.Path += TEXT(".") + FPaths::GetBaseFilename(Def.Path);
}
}
else if (Key == TEXT("MORPHABLE"))
{
TArray<FString> Parts;
Value.ParseIntoArrayWS(Parts);
if (Parts.Num() > 0)
{
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);
}
else if (Key == TEXT("SEX"))
{
FileDefs.Last().bMale = Value.StartsWith(TEXT("M"), ESearchCase::IgnoreCase);
}
else if (Key == TEXT("DAZ"))
{
FileDefs.Last().bDaz = !Value.Equals(TEXT("false"), ESearchCase::IgnoreCase);
}
else if (Key == TEXT("FIT_MORPH"))
{
Value.Split(TEXT(" "), &FileDefs.Last().FitMorph, &FileDefs.Last().FitValue);
FileDefs.Last().FitValue.TrimStartAndEndInline();
}
}
if (bBase)
{
if (!BaseFile.IsEmpty())
{
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s ignore, la base est deja %s"), *Relative, *BaseFile);
return;
}
BaseFile = Relative;
BaseDefs = MoveTemp(FileDefs);
BaseMorphs = MoveTemp(FileMorphs);
BaseTextures = MoveTemp(FileTextures);
return;
}
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
{
// 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;
for (const FString& Module : HairModules)
{
Signature += Module + (IsModuleEnabled(Module) ? TEXT("=1;") : TEXT("=0;"));
}
return Signature;
}
bool ULPHairstyleSubsystem::IsModuleEnabled(const FString& Module) const
{
const UObject* GI = GetGameInstance();
const UScriptStruct* Struct = nullptr;
const FArrayProperty* Prop = StructArray(GI, ModEnabledsName, Struct);
if (!Prop)
{
return true;
}
FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr<void>(GI));
for (int32 i = 0; i < Helper.Num(); ++i)
{
if (GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("UniqueID")).Equals(Module, ESearchCase::IgnoreCase))
{
return GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("IsEnabled")).Equals(TEXT("True"), ESearchCase::IgnoreCase);
}
}
return false; // module absent de la liste : inactif, comme GI.IsModEnabled (le gestionnaire l'ajoute decoche)
}
bool ULPHairstyleSubsystem::EnsureDefaultEnabledModules()
{
UObject* GI = GetGameInstance();
const UScriptStruct* Struct = nullptr;
const FArrayProperty* Prop = StructArray(GI, ModEnabledsName, Struct);
if (!Prop || DefaultEnabledModules.Num() == 0)
{
return false;
}
FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr<void>(GI));
bool bChanged = false;
for (const FString& Module : DefaultEnabledModules)
{
bool bFound = false;
for (int32 i = 0; i < Helper.Num() && !bFound; ++i)
{
bFound = GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("UniqueID")).Equals(Module, ESearchCase::IgnoreCase);
}
if (!bFound)
{
const int32 Index = Helper.AddValue();
SetField(Struct, Helper.GetRawPtr(Index), TEXT("UniqueID"), Module);
SetField(Struct, Helper.GetRawPtr(Index), TEXT("IsEnabled"), TEXT("True"));
UE_LOG(LogLifePlay, Display, TEXT("Modules : %s actif par defaut (MODULE_DEFAULT_ENABLED), ajoute a la liste des mods"), *Module);
bChanged = true;
}
}
return bChanged;
}
void ULPHairstyleSubsystem::Sync()
{
UObject* GI = GetGameInstance();
const UScriptStruct* Struct = nullptr;
const FArrayProperty* TableProp = StructArray(GI, HairstyleTableName, Struct);
const FMapProperty* MapProp = GI ? CastField<FMapProperty>(GI->GetClass()->FindPropertyByName(MorphableHairsName)) : nullptr;
if (!TableProp || !MapProp)
{
UE_LOG(LogLifePlay, Error, TEXT("Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable sur %s, table laissee telle quelle"), *GetNameSafe(GI));
return;
}
const bool bRebuild = bCaptured; // faux au premier passage (Initialize) : construction, pas reconstruction
if (!bCaptured)
{
BlueprintTableText = ExportProperty(TableProp, GI);
BlueprintMorphsText = ExportProperty(MapProp, GI);
bCaptured = true;
}
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.
if (BaseDefs.Num() > 0)
{
const FString BlueprintMorphsSorted = ExportMapSorted(MapProp, GI);
Table.EmptyValues();
Map.EmptyValues();
int32 BadFields = 0;
for (const FHairDef& Def : BaseDefs)
{
void* Entry = Table.GetRawPtr(Table.AddValue());
// Entree "sans cheveux" (NONE) : chemin vide ; sexe et indicateur Daz repris tels quels (le Blueprint en a trois).
if (!(SetField(Struct, Entry, TEXT("Bool"), Def.bMale ? TEXT("True") : TEXT("False"))
&& SetField(Struct, Entry, TEXT("Daz"), Def.bDaz ? TEXT("True") : TEXT("False"))
// Le Blueprint stocke un chemin explicitement vide (""), distinct de la valeur par defaut du champ.
&& (Def.bNone ? ClearField(Struct, Entry, TEXT("SkeletalMeshPath")) : SetField(Struct, Entry, TEXT("SkeletalMeshPath"), Def.Path))))
{
++BadFields;
}
}
for (const FMorphDef& Morph : BaseMorphs)
{
AddMorph(MapProp, GI, Morph.Key, Morph.Morph, Morph.Value);
}
if (BadFields > 0)
{
UE_LOG(LogLifePlay, Error, TEXT("Coiffures : %d entree(s) de base incompletes (structure de HairstyleTable inattendue)"), BadFields);
}
static const bool bVerify = FParse::Param(FCommandLine::Get(), TEXT("LPVerifyHairstyles"));
if (bVerify)
{
const bool bSameTable = ExportProperty(TableProp, GI) == BlueprintTableText;
const bool bSameMorphs = ExportMapSorted(MapProp, GI) == BlueprintMorphsSorted;
UE_LOG(LogLifePlay, Display, TEXT("Coiffures, verification de la base %s : table %s (%d entrees), morphs %s (%d entrees)"), *BaseFile,
bSameTable ? TEXT("IDENTIQUE au Blueprint") : TEXT("DIFFERENTE du Blueprint"), Table.Num(),
bSameMorphs ? TEXT("IDENTIQUES") : TEXT("DIFFERENTS"), Map.Num());
if (!bSameTable)
{
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : Blueprint = %s"), *BlueprintTableText.Left(2000));
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : fichier = %s"), *ExportProperty(TableProp, GI).Left(2000));
}
}
}
else
{
TableProp->ImportText_Direct(*BlueprintTableText, TableProp->ContainerPtrToValuePtr<void>(GI), GI, PPF_None);
MapProp->ImportText_Direct(*BlueprintMorphsText, MapProp->ContainerPtrToValuePtr<void>(GI), GI, PPF_None);
}
const int32 BaseCount = Table.Num();
// 2. Coiffures des modules actifs, en fin de table.
TSet<FString> Known;
for (int32 i = 0; i < Table.Num(); ++i)
{
Known.Add(GetFieldText(Struct, Table.GetRawPtr(i), TEXT("SkeletalMeshPath")));
}
IAssetRegistry& Registry = IAssetRegistry::GetChecked();
int32 Added = 0, Skipped = 0;
for (const FHairDef& Def : Defs)
{
if (!IsModuleEnabled(Def.Module))
{
++Skipped;
continue;
}
if (Def.bNone || Known.Contains(Def.Path))
{
UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s deja dans la table ou vide (%s)"), *Def.Path, *Def.File);
++Skipped;
continue;
}
const FAssetData Asset = Registry.GetAssetByObjectPath(FSoftObjectPath(Def.Path));
// Mod (H3) dont le registre d'assets manquerait : le paquet monte suffit (classe non verifiable sans le charger).
const bool bPackageOnly = !Asset.IsValid() && FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(Def.Path));
if (bPackageOnly)
{
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s absent du registre d'assets mais paquet present, ajoute (%s)"), *Def.Path, *Def.File);
}
else if (!Asset.IsValid() || !Asset.IsInstanceOf(USkeletalMesh::StaticClass()))
{
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s introuvable ou n'est pas un SkeletalMesh (%s), ignore"), *Def.Path, *Def.File);
++Skipped;
continue;
}
const int32 Index = Table.AddValue();
void* Entry = Table.GetRawPtr(Index);
if (!(SetField(Struct, Entry, TEXT("SkeletalMeshPath"), Def.Path)
&& SetField(Struct, Entry, TEXT("Bool"), Def.bMale ? TEXT("True") : TEXT("False"))
&& SetField(Struct, Entry, TEXT("Daz"), Def.bDaz ? TEXT("True") : TEXT("False"))))
{
Table.RemoveValues(Index, 1);
UE_LOG(LogLifePlay, Error, TEXT("Coiffures : structure de HairstyleTable inattendue, %s non ajoute"), *Def.Path);
++Skipped;
continue;
}
Known.Add(Def.Path);
++Added;
// Morph d'ajustement : cle = nom de l'objet, comme pour les coiffures du Blueprint.
const FString Name = FPaths::GetExtension(Def.Path);
if (!Def.FitMorph.IsEmpty())
{
AddMorph(MapProp, GI, Name, Def.FitMorph, Def.FitValue);
}
UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s ajoutee (index %d, %s, module %s)"), *Name, Index, Def.bMale ? TEXT("M") : TEXT("F"), *Def.Module);
}
// 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) (%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);
}
}