2026-10-02 12:56:48 +00:00
// 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"
2026-10-02 14:21:16 +00:00
# include "Misc/CommandLine.h"
2026-10-02 12:56:48 +00:00
# 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 ;
}
2026-10-02 14:21:16 +00:00
/** 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 ;
}
2026-10-02 12:56:48 +00:00
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 ;
}
2026-10-02 14:21:16 +00:00
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 ;
}
2026-10-02 12:56:48 +00:00
}
void ULPHairstyleSubsystem : : Initialize ( FSubsystemCollectionBase & Collection )
{
Super : : Initialize ( Collection ) ;
ScanFiles ( ) ;
2026-10-02 14:21:16 +00:00
if ( Defs . Num ( ) = = 0 & & BaseDefs . Num ( ) = = 0 )
2026-10-02 12:56:48 +00:00
{
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 ;
2026-10-02 14:21:16 +00:00
UE_LOG ( LogLifePlay , Log , TEXT ( " Coiffures : liste des modules actifs modifiee, reconstruction " ) ) ;
2026-10-02 12:56:48 +00:00
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 ;
}
2026-10-02 14:21:16 +00:00
ParseFile ( Relative , Module , Text ) ;
}
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 ( ) ) ;
}
}
void ULPHairstyleSubsystem : : ParseFile ( const FString & Relative , const FString & Module , const FString & Text )
{
TArray < FHairDef > FileDefs ;
TArray < FMorphDef > FileMorphs ;
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 ) )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
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 ( " . " ) ) )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
Def . Path + = TEXT ( " . " ) + FPaths : : GetBaseFilename ( Def . Path ) ;
2026-10-02 12:56:48 +00:00
}
2026-10-02 14:21:16 +00:00
}
else if ( Key = = TEXT ( " MORPHABLE " ) )
{
TArray < FString > Parts ;
Value . ParseIntoArrayWS ( Parts ) ;
if ( Parts . Num ( ) > 0 )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
FileMorphs . Add ( { Parts [ 0 ] , Parts . IsValidIndex ( 1 ) ? Parts [ 1 ] : FString ( ) , Parts . IsValidIndex ( 2 ) ? Parts [ 2 ] : FString ( ) } ) ;
2026-10-02 12:56:48 +00:00
}
}
2026-10-02 14:21:16 +00:00
else if ( FileDefs . Num ( ) = = 0 )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
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 ) ;
2026-10-02 12:56:48 +00:00
}
2026-10-02 14:21:16 +00:00
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 ( ) )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
UE_LOG ( LogLifePlay , Warning , TEXT ( " Coiffures : %s ignore, la base est deja %s " ) , * Relative , * BaseFile ) ;
return ;
2026-10-02 12:56:48 +00:00
}
2026-10-02 14:21:16 +00:00
BaseFile = Relative ;
BaseDefs = MoveTemp ( FileDefs ) ;
BaseMorphs = MoveTemp ( FileMorphs ) ;
return ;
2026-10-02 12:56:48 +00:00
}
2026-10-02 14:21:16 +00:00
if ( FileDefs . Num ( ) = = 0 )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
UE_LOG ( LogLifePlay , Warning , TEXT ( " Coiffures : %s sans ligne HAIRSTYLE, ignore " ) , * Relative ) ;
2026-10-02 12:56:48 +00:00
}
2026-10-02 14:21:16 +00:00
Defs . Append ( MoveTemp ( FileDefs ) ) ;
2026-10-02 12:56:48 +00:00
}
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 ;
2026-10-02 14:21:16 +00:00
if ( ! TableProp | | ! MapProp )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
UE_LOG ( LogLifePlay , Error , TEXT ( " Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable sur %s, table laissee telle quelle " ) , * GetNameSafe ( GI ) ) ;
2026-10-02 12:56:48 +00:00
return ;
}
2026-10-02 14:21:16 +00:00
if ( ! bCaptured )
{
BlueprintTableText = ExportProperty ( TableProp , GI ) ;
BlueprintMorphsText = ExportProperty ( MapProp , GI ) ;
bCaptured = true ;
}
2026-10-02 12:56:48 +00:00
FScriptArrayHelper Table ( TableProp , TableProp - > ContainerPtrToValuePtr < void > ( GI ) ) ;
2026-10-02 14:21:16 +00:00
FScriptMapHelper Map ( MapProp , MapProp - > ContainerPtrToValuePtr < void > ( GI ) ) ;
2026-10-02 12:56:48 +00:00
2026-10-02 14:21:16 +00:00
// 1. Base : fichier MODE: BASE reconstruit dans son ordre, sinon valeurs du Blueprint.
if ( BaseDefs . Num ( ) > 0 )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
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 )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
AddMorph ( MapProp , GI , Morph . Key , Morph . Morph , Morph . Value ) ;
2026-10-02 12:56:48 +00:00
}
2026-10-02 14:21:16 +00:00
if ( BadFields > 0 )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
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 )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
UE_LOG ( LogLifePlay , Warning , TEXT ( " Coiffures : Blueprint = %s " ) , * BlueprintTableText . Left ( 2000 ) ) ;
UE_LOG ( LogLifePlay , Warning , TEXT ( " Coiffures : fichier = %s " ) , * ExportProperty ( TableProp , GI ) . Left ( 2000 ) ) ;
2026-10-02 12:56:48 +00:00
}
}
}
2026-10-02 14:21:16 +00:00
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 ( ) ;
2026-10-02 12:56:48 +00:00
2026-10-02 14:21:16 +00:00
// 2. Coiffures des modules actifs, en fin de table.
2026-10-02 12:56:48 +00:00
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 ;
}
2026-10-02 14:21:16 +00:00
if ( Def . bNone | | Known . Contains ( Def . Path ) )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
UE_LOG ( LogLifePlay , Log , TEXT ( " Coiffures : %s deja dans la table ou vide (%s) " ) , * Def . Path , * Def . File ) ;
2026-10-02 12:56:48 +00:00
+ + 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 ) ;
2026-10-02 14:21:16 +00:00
if ( ! ( SetField ( Struct , Entry , TEXT ( " SkeletalMeshPath " ) , Def . Path )
2026-10-02 12:56:48 +00:00
& & SetField ( Struct , Entry , TEXT ( " Bool " ) , Def . bMale ? TEXT ( " True " ) : TEXT ( " False " ) )
2026-10-02 14:21:16 +00:00
& & SetField ( Struct , Entry , TEXT ( " Daz " ) , Def . bDaz ? TEXT ( " True " ) : TEXT ( " False " ) ) ) )
2026-10-02 12:56:48 +00:00
{
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.
2026-10-02 14:21:16 +00:00
const FString Name = FPaths : : GetExtension ( Def . Path ) ;
if ( ! Def . FitMorph . IsEmpty ( ) )
2026-10-02 12:56:48 +00:00
{
2026-10-02 14:21:16 +00:00
AddMorph ( MapProp , GI , Name , Def . FitMorph , Def . FitValue ) ;
2026-10-02 12:56:48 +00:00
}
2026-10-02 14:21:16 +00:00
UE_LOG ( LogLifePlay , Log , TEXT ( " Coiffures : %s ajoutee (index %d, %s, module %s) " ) , * Name , Index , Def . bMale ? TEXT ( " M " ) : TEXT ( " F " ) , * Def . Module ) ;
2026-10-02 12:56:48 +00:00
}
2026-10-02 14:21:16 +00:00
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 ( ) ) ;
2026-10-02 12:56:48 +00:00
}