LifePlay-Revival/Source/LifePlay/LPHairstyleSubsystem.cpp
edujiro5972 cad164957e H1 : coiffures declarees par fichiers .lphair (LPHairstyleSubsystem, module lp_Revival)
Ajout en fin de GI.HairstyleTable sans modifier de Blueprint ; doublons refuses,
modules desactives ignores ; build H1 : 1 coiffure de test ajoutee (126 -> 127).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:56:48 +02:00

322 lines
10 KiB
C++

// 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<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;
}
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;
}
}
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<FString> 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<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("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<void>(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<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 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<FMapProperty>(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<void>(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<FStructProperty>(MapProp->ValueProp) : nullptr;
FScriptMapHelper Map(MapProp, MapProp ? MapProp->ContainerPtrToValuePtr<void>(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<FString> 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());
}