682 lines
24 KiB
C++
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);
|
|
}
|
|
}
|