// Portage UE 5.5 - voir LPHairstyleSubsystem.h #include "LPHairstyleSubsystem.h" #include "AssetRegistry/IAssetRegistry.h" #include "Engine/GameInstance.h" #include "Engine/SkeletalMesh.h" #include "HAL/FileManager.h" #include "HAL/PlatformTime.h" #include "LifePlay.h" #include "Misc/FileHelper.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; /** 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 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; } 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(GI->GetClass()->FindPropertyByName(Name)) : nullptr; const FStructProperty* Inner = Prop ? CastField(Prop->Inner) : nullptr; OutStruct = Inner ? Inner->Struct : nullptr; return OutStruct ? Prop : nullptr; } } void ULPHairstyleSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); ScanFiles(); if (Defs.Num() == 0) { return; } 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; const FString Signature = EnabledSignature(); if (Signature != LastSignature) { LastSignature = Signature; UE_LOG(LogLifePlay, Log, TEXT("Coiffures : liste des modules actifs modifiee, resynchronisation")); Sync(); } return true; } void ULPHairstyleSubsystem::ScanFiles() { const FString ModulesDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir() / TEXT("Modules")); TArray Files; IFileManager::Get().FindFilesRecursive(Files, *ModulesDir, TEXT("*.lphair"), true, false); Files.Sort(); 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; } FString Text; if (!FFileHelper::LoadFileToString(Text, *File)) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : lecture impossible de %s"), *Relative); continue; } FHairDef Def; Def.File = Relative; Def.Module = Module; TArray 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("HAIRSTYLE")) { Def.Path = Value; } else if (Key == TEXT("SEX")) { Def.bMale = Value.StartsWith(TEXT("M"), ESearchCase::IgnoreCase); } else if (Key == TEXT("DAZ")) { Def.bDaz = !Value.Equals(TEXT("false"), ESearchCase::IgnoreCase); } else if (Key == TEXT("FIT_MORPH")) { Value.Split(TEXT(" "), &Def.FitMorph, &Def.FitValue); Def.FitValue.TrimStartAndEndInline(); } } if (Def.Path.IsEmpty()) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s sans ligne HAIRSTYLE, ignore"), *Relative); continue; } // Un chemin de paquet sans nom d'objet (/Game/X/Nom) designe l'objet du meme nom. if (!Def.Path.Contains(TEXT("."))) { Def.Path += TEXT(".") + FPaths::GetBaseFilename(Def.Path); } Defs.Add(MoveTemp(Def)); } if (Defs.Num() > 0) { UE_LOG(LogLifePlay, Display, TEXT("Coiffures : %d fichier(s) .lphair trouve(s) dans Modules"), Defs.Num()); } } FString ULPHairstyleSubsystem::EnabledSignature() const { const UObject* GI = GetGameInstance(); const UScriptStruct* Struct = nullptr; const FArrayProperty* Prop = StructArray(GI, ModEnabledsName, Struct); if (!Prop) { return FString(); } FString Signature; FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); for (int32 i = 0; i < Helper.Num(); ++i) { Signature += GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("UniqueID")) + TEXT("=") + GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("IsEnabled")) + TEXT(";"); } 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(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 true; // module absent de la liste : actif, comme pour le jeu } void ULPHairstyleSubsystem::Sync() { UObject* GI = GetGameInstance(); const UScriptStruct* Struct = nullptr; const FArrayProperty* TableProp = StructArray(GI, HairstyleTableName, Struct); const FMapProperty* MapProp = GI ? CastField(GI->GetClass()->FindPropertyByName(MorphableHairsName)) : nullptr; if (!TableProp) { UE_LOG(LogLifePlay, Error, TEXT("Coiffures : GI.HairstyleTable introuvable sur %s, aucune coiffure ajoutee"), *GetNameSafe(GI)); return; } FScriptArrayHelper Table(TableProp, TableProp->ContainerPtrToValuePtr(GI)); // 1. Retrait des entrees ajoutees precedemment (en fin de table, reconnues par leur chemin). for (int32 i = Table.Num() - 1; i >= 0 && Injected.Num() > 0; --i) { if (Injected.Last() == GetFieldText(Struct, Table.GetRawPtr(i), TEXT("SkeletalMeshPath"))) { Table.RemoveValues(i, 1); Injected.Pop(); } } Injected.Reset(); const FStructProperty* MapValueProp = MapProp ? CastField(MapProp->ValueProp) : nullptr; FScriptMapHelper Map(MapProp, MapProp ? MapProp->ContainerPtrToValuePtr(GI) : nullptr); if (MapProp) { for (const FString& Key : InjectedMorphs) { for (FScriptMapHelper::FIterator It(Map); It; ++It) { FString Existing; MapProp->KeyProp->ExportTextItem_Direct(Existing, Map.GetKeyPtr(It), nullptr, nullptr, PPF_None); if (Existing.TrimQuotes() == Key) { Map.RemoveAt(It.GetInternalIndex()); break; } } } } InjectedMorphs.Reset(); // 2. Chemins deja presents (Blueprint). TSet Known; for (int32 i = 0; i < Table.Num(); ++i) { Known.Add(GetFieldText(Struct, Table.GetRawPtr(i), TEXT("SkeletalMeshPath"))); } // 3. Ajout des coiffures des modules actifs. IAssetRegistry& Registry = IAssetRegistry::GetChecked(); int32 Added = 0, Skipped = 0; for (const FHairDef& Def : Defs) { if (!IsModuleEnabled(Def.Module)) { ++Skipped; continue; } if (Known.Contains(Def.Path)) { UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s deja dans la table (%s)"), *Def.Path, *Def.File); ++Skipped; continue; } const FAssetData Asset = Registry.GetAssetByObjectPath(FSoftObjectPath(Def.Path)); 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); const bool bOk = 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")); if (!bOk) { 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); Injected.Add(Def.Path); ++Added; // Morph d'ajustement : cle = nom de l'objet, comme pour les coiffures du Blueprint. const FString Key = FPaths::GetExtension(Def.Path); if (!Def.FitMorph.IsEmpty() && MapProp && MapValueProp) { bool bExists = false; for (FScriptMapHelper::FIterator It(Map); It; ++It) { FString Existing; MapProp->KeyProp->ExportTextItem_Direct(Existing, Map.GetKeyPtr(It), nullptr, nullptr, PPF_None); bExists |= Existing.TrimQuotes() == Key; } if (!bExists) { const int32 Pair = Map.AddDefaultValue_Invalid_NeedsRehash(); MapProp->KeyProp->ImportText_Direct(*Key, Map.GetKeyPtr(Pair), nullptr, PPF_None); SetField(MapValueProp->Struct, Map.GetValuePtr(Pair), TEXT("Name"), Def.FitMorph); SetField(MapValueProp->Struct, Map.GetValuePtr(Pair), TEXT("Float"), Def.FitValue); Map.Rehash(); InjectedMorphs.Add(Key); } } UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s ajoutee (index %d, %s, module %s)"), *Key, Index, Def.bMale ? TEXT("M") : TEXT("F"), *Def.Module); } UE_LOG(LogLifePlay, Display, TEXT("Coiffures : %d ajoutee(s) depuis les fichiers .lphair, %d ignoree(s), table de %d entree(s)"), Added, Skipped, Table.Num()); }