H2 : liste de base des coiffures dans vin_Base/Hairstyles/vin_Base.lphair
Table reconstruite depuis le fichier (MODE: BASE) puis coiffures des modules ; verification -LPVerifyHairstyles : table et morphs identiques au Blueprint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cb32c69350
commit
bac557d829
4 changed files with 272 additions and 111 deletions
|
|
@ -628,6 +628,30 @@ appliquées, suivi des animations. Reste mineur accepté : pointe sombre du bas
|
|||
**Limites connues** : pas de morphs de forme (la coiffure ne suit pas les morphs du visage) ; rigide sur la tête
|
||||
(pas de mouvement des mèches) ; ajustement automatique à contrôler visuellement.
|
||||
|
||||
### 16.9 H2 : liste de base des coiffures en fichier de données (2026-10-02)
|
||||
|
||||
La valeur par défaut de `GI.HairstyleTable` (126 entrées) et de `GI.MorphableHairs` (89 entrées) est exportée dans
|
||||
`Modules/vin_Base/Hairstyles/vin_Base.lphair` (marqué `MODE: BASE`), généré depuis les relevés `LPDumpCDO` du Blueprint.
|
||||
Le sous-système **reconstruit** la table à partir de ce fichier, dans son ordre, puis ajoute les coiffures des autres
|
||||
modules. Le Blueprint n'est pas modifié : sans fichier de base, sa table reste la base (comportement H1).
|
||||
|
||||
**Format étendu** (rétrocompatible) : plusieurs coiffures par fichier (chaque `HAIRSTYLE` ouvre une entrée) ;
|
||||
`HAIRSTYLE: NONE` (option sans cheveux) ; `MORPHABLE: Clé [Morph Valeur]` (entrée brute de `MorphableHairs`) ;
|
||||
`MODE: BASE` (un seul fichier de base, le premier par ordre alphabétique).
|
||||
|
||||
**Fidélité** : les entrées du fichier de base ne sont pas filtrées (les 35 coiffures « legacy » absentes du projet depuis
|
||||
4.27 sont reprises, comme dans le Blueprint ; une clé de morph sans coiffure, `LongHair_223410`, aussi). Détails
|
||||
reproduits : trois options sans cheveux (F, M, et une F marquée Daz) dont le chemin est une chaîne explicitement vide,
|
||||
différente de la valeur par défaut du champ.
|
||||
|
||||
**Vérification** (option `-LPVerifyHairstyles`, build `LifePlay_Build_5.5_H2`) : table et morphs reconstruits comparés
|
||||
au Blueprint en texte exporté. Deux écarts trouvés et corrigés (chemin vide des options sans cheveux, puis affectation
|
||||
directe de la chaîne vide), puis : « table IDENTIQUE au Blueprint (126 entrees), morphs IDENTIQUES (89 entrees) » ;
|
||||
`Pocolov17` ajoutée ensuite à l'index 126.
|
||||
|
||||
**Usage pour un moddeur** : ajouter une coiffure = un fichier `.lphair` dans son module ; modifier la liste de base
|
||||
(retirer une coiffure du tirage, changer son sexe, son morph) = éditer `vin_Base.lphair`, sans éditeur Unreal.
|
||||
|
||||
## 17. Couche de données : stratégie (2026-10-02)
|
||||
|
||||
Proposition utilisateur : convertir les fichiers texte de `Modules` en fichiers JSON groupés par type, avec un calque de
|
||||
|
|
|
|||
|
|
@ -933,3 +933,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
|
|||
| CHG-086 | 2026-10-02 | O7e | `LPHairFromStaticMeshCommandlet` : ajustement sur le haut du crâne (`-Cranium`, `-Margin`) ; `Pocolov17` et `_Skeleton` régénérés (premier essai dans `O7e\essai1`), `Pocolov17_Strands` : `Thicken` 10 → 1 ; `11_Audit_Optimisation.md` §16.8 | premier essai en jeu (capture utilisateur) : coiffure trop étroite enfoncée dans le crâne, mèches opaques ; échelle 0,74 → 1,19 en largeur ; build `LifePlay_Build_5.5_O7e` reconstruit ; test en jeu en attente | `Tools`/assets : remettre `O7e\essai1` ; `git checkout O7e-apres -- Source/LifePlayEditorTools` |
|
||||
| CHG-087 | 2026-10-02 | O7e | `Pocolov17_Cap` (hors dépôt) : `MapOpacity` = texture blanche du moteur ; `11_Audit_Optimisation.md` §16.8 | second essai en jeu correct (crâne couvert, couleur appliquée, profil bien placé) ; trous au-dessus de l'oreille dus à la lisière découpée de la calotte ; retouche choisie par l'utilisateur ; build `LifePlay_Build_5.5_O7e` reconstruit | remettre `Pocolov17_CapOpacity` dans `MapOpacity` |
|
||||
| CHG-088 | 2026-10-02 | O7e | — | O7e validé en jeu par l'utilisateur (profil sans trou, mouvement suivi) ; défaut mineur accepté : bas de la calotte visible sous le lobe de l'oreille | — |
|
||||
| CHG-089 | 2026-10-02 | H2 | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` (mode base, plusieurs coiffures par fichier, `NONE`, `MORPHABLE`, vérification `-LPVerifyHairstyles`) ; nouveau `Content/Modules/vin_Base/Hairstyles/vin_Base.lphair` (126 coiffures, 89 morphs) ; `11_Audit_Optimisation.md` §16.9 | liste de base des coiffures sortie du Blueprint (demande utilisateur) ; vérifié : table et morphs reconstruits identiques au Blueprint ; build `LifePlay_Build_5.5_H2` ; test en jeu en attente | supprimer `vin_Base.lphair` (retour à la table du Blueprint) ; `git checkout H2-avant -- Source/LifePlay` |
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "HAL/FileManager.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "LifePlay.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
|
|
@ -39,6 +40,23 @@ namespace
|
|||
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;
|
||||
|
|
@ -56,13 +74,64 @@ namespace
|
|||
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)
|
||||
if (Defs.Num() == 0 && BaseDefs.Num() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -89,7 +158,7 @@ bool ULPHairstyleSubsystem::Tick(float DeltaTime)
|
|||
if (Signature != LastSignature)
|
||||
{
|
||||
LastSignature = Signature;
|
||||
UE_LOG(LogLifePlay, Log, TEXT("Coiffures : liste des modules actifs modifiee, resynchronisation"));
|
||||
UE_LOG(LogLifePlay, Log, TEXT("Coiffures : liste des modules actifs modifiee, reconstruction"));
|
||||
Sync();
|
||||
}
|
||||
return true;
|
||||
|
|
@ -117,55 +186,93 @@ void ULPHairstyleSubsystem::ScanFiles()
|
|||
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)
|
||||
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))
|
||||
{
|
||||
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(".")))
|
||||
Key = Key.TrimStartAndEnd().ToUpper();
|
||||
Value = Value.TrimStartAndEnd();
|
||||
if (Key == TEXT("MODE"))
|
||||
{
|
||||
Def.Path += TEXT(".") + FPaths::GetBaseFilename(Def.Path);
|
||||
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 (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();
|
||||
}
|
||||
Defs.Add(MoveTemp(Def));
|
||||
}
|
||||
if (Defs.Num() > 0)
|
||||
if (bBase)
|
||||
{
|
||||
UE_LOG(LogLifePlay, Display, TEXT("Coiffures : %d fichier(s) .lphair trouve(s) dans Modules"), Defs.Num());
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (FileDefs.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s sans ligne HAIRSTYLE, ignore"), *Relative);
|
||||
}
|
||||
Defs.Append(MoveTemp(FileDefs));
|
||||
}
|
||||
|
||||
FString ULPHairstyleSubsystem::EnabledSignature() const
|
||||
|
|
@ -212,51 +319,75 @@ void ULPHairstyleSubsystem::Sync()
|
|||
const UScriptStruct* Struct = nullptr;
|
||||
const FArrayProperty* TableProp = StructArray(GI, HairstyleTableName, Struct);
|
||||
const FMapProperty* MapProp = GI ? CastField<FMapProperty>(GI->GetClass()->FindPropertyByName(MorphableHairsName)) : nullptr;
|
||||
if (!TableProp)
|
||||
if (!TableProp || !MapProp)
|
||||
{
|
||||
UE_LOG(LogLifePlay, Error, TEXT("Coiffures : GI.HairstyleTable introuvable sur %s, aucune coiffure ajoutee"), *GetNameSafe(GI));
|
||||
UE_LOG(LogLifePlay, Error, TEXT("Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable sur %s, table laissee telle quelle"), *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 (!bCaptured)
|
||||
{
|
||||
if (Injected.Last() == GetFieldText(Struct, Table.GetRawPtr(i), TEXT("SkeletalMeshPath")))
|
||||
{
|
||||
Table.RemoveValues(i, 1);
|
||||
Injected.Pop();
|
||||
}
|
||||
BlueprintTableText = ExportProperty(TableProp, GI);
|
||||
BlueprintMorphsText = ExportProperty(MapProp, GI);
|
||||
bCaptured = true;
|
||||
}
|
||||
Injected.Reset();
|
||||
const FStructProperty* MapValueProp = MapProp ? CastField<FStructProperty>(MapProp->ValueProp) : nullptr;
|
||||
FScriptMapHelper Map(MapProp, MapProp ? MapProp->ContainerPtrToValuePtr<void>(GI) : nullptr);
|
||||
if (MapProp)
|
||||
FScriptArrayHelper Table(TableProp, TableProp->ContainerPtrToValuePtr<void>(GI));
|
||||
FScriptMapHelper Map(MapProp, MapProp->ContainerPtrToValuePtr<void>(GI));
|
||||
|
||||
// 1. Base : fichier MODE: BASE reconstruit dans son ordre, sinon valeurs du Blueprint.
|
||||
if (BaseDefs.Num() > 0)
|
||||
{
|
||||
for (const FString& Key : InjectedMorphs)
|
||||
const FString BlueprintMorphsSorted = ExportMapSorted(MapProp, GI);
|
||||
Table.EmptyValues();
|
||||
Map.EmptyValues();
|
||||
int32 BadFields = 0;
|
||||
for (const FHairDef& Def : BaseDefs)
|
||||
{
|
||||
for (FScriptMapHelper::FIterator It(Map); It; ++It)
|
||||
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))))
|
||||
{
|
||||
FString Existing;
|
||||
MapProp->KeyProp->ExportTextItem_Direct(Existing, Map.GetKeyPtr(It), nullptr, nullptr, PPF_None);
|
||||
if (Existing.TrimQuotes() == Key)
|
||||
{
|
||||
Map.RemoveAt(It.GetInternalIndex());
|
||||
break;
|
||||
}
|
||||
++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));
|
||||
}
|
||||
}
|
||||
}
|
||||
InjectedMorphs.Reset();
|
||||
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. Chemins deja presents (Blueprint).
|
||||
// 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")));
|
||||
}
|
||||
|
||||
// 3. Ajout des coiffures des modules actifs.
|
||||
IAssetRegistry& Registry = IAssetRegistry::GetChecked();
|
||||
int32 Added = 0, Skipped = 0;
|
||||
for (const FHairDef& Def : Defs)
|
||||
|
|
@ -266,9 +397,9 @@ void ULPHairstyleSubsystem::Sync()
|
|||
++Skipped;
|
||||
continue;
|
||||
}
|
||||
if (Known.Contains(Def.Path))
|
||||
if (Def.bNone || Known.Contains(Def.Path))
|
||||
{
|
||||
UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s deja dans la table (%s)"), *Def.Path, *Def.File);
|
||||
UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s deja dans la table ou vide (%s)"), *Def.Path, *Def.File);
|
||||
++Skipped;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -281,10 +412,9 @@ void ULPHairstyleSubsystem::Sync()
|
|||
}
|
||||
const int32 Index = Table.AddValue();
|
||||
void* Entry = Table.GetRawPtr(Index);
|
||||
const bool bOk = SetField(Struct, Entry, TEXT("SkeletalMeshPath"), Def.Path)
|
||||
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"));
|
||||
if (!bOk)
|
||||
&& 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);
|
||||
|
|
@ -292,31 +422,15 @@ void ULPHairstyleSubsystem::Sync()
|
|||
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)
|
||||
const FString Name = FPaths::GetExtension(Def.Path);
|
||||
if (!Def.FitMorph.IsEmpty())
|
||||
{
|
||||
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);
|
||||
}
|
||||
AddMorph(MapProp, GI, Name, Def.FitMorph, Def.FitValue);
|
||||
}
|
||||
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, Log, TEXT("Coiffures : %s ajoutee (index %d, %s, module %s)"), *Name, 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());
|
||||
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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
// Portage UE 5.5 - lot H1 (Docs/Migration/11_Audit_Optimisation.md §16.7).
|
||||
// Portage UE 5.5 - lots H1 et H2 (Docs/Migration/11_Audit_Optimisation.md §16.7 et §16.9).
|
||||
//
|
||||
// La liste des coiffures du jeu est la valeur par defaut de GI.HairstyleTable (126 entrees ecrites dans le Blueprint) :
|
||||
// un mod ne peut pas en ajouter. Ce sous-systeme lit des fichiers texte Modules/<module>/**/*.lphair et ajoute leurs
|
||||
// coiffures en fin de table, ainsi que leur morph d'ajustement dans GI.MorphableHairs. Format (une coiffure par fichier) :
|
||||
// La liste des coiffures du jeu est la valeur par defaut de GI.HairstyleTable (126 entrees ecrites dans le Blueprint),
|
||||
// avec GI.MorphableHairs (morph d'ajustement par coiffure) : un mod ne pouvait pas y toucher. Ce sous-systeme lit les
|
||||
// fichiers texte Modules/<module>/**/*.lphair. Un fichier peut decrire plusieurs coiffures : chaque ligne HAIRSTYLE
|
||||
// ouvre une entree, les lignes suivantes la completent.
|
||||
//
|
||||
// HAIRSTYLE: /Game/Heavy/New5/Hairs/Exemple.Exemple chemin complet d'un SkeletalMesh (squelette Genesis 8)
|
||||
// HAIRSTYLE: /Game/Heavy/New5/Hairs/Exemple.Exemple chemin complet d'un SkeletalMesh (squelette Genesis 8),
|
||||
// ou NONE (option "sans cheveux")
|
||||
// SEX: F F ou M (defaut F)
|
||||
// DAZ: true defaut true ; false = coiffure "legacy", chargee seulement
|
||||
// si GI.LegacyPercentage > 0
|
||||
// FIT_MORPH: Exemple__AllExpand 0.45 facultatif : morph applique par Person.FitMorphableHair
|
||||
// MORPHABLE: Cle [Morph Valeur] entree brute de GI.MorphableHairs (fichier de base)
|
||||
// MODE: BASE fichier de base (H2) : la table est reconstruite a partir de
|
||||
// ce fichier, dans son ordre, avant l'ajout des autres modules
|
||||
//
|
||||
// Les modules desactives dans GI.ModEnableds sont ignores ; la table est resynchronisee quand cette liste change.
|
||||
// Aucun Blueprint n'est modifie : sans fichier .lphair, la table reste celle du Blueprint. Les entrees du Blueprint
|
||||
// ne sont jamais modifiees ni reordonnees, une coiffure deja presente n'est pas ajoutee une seconde fois.
|
||||
// Sans fichier de base, les coiffures sont ajoutees en fin de la table du Blueprint (H1). Les coiffures des modules
|
||||
// sont controlees (SkeletalMesh present, pas de doublon) ; celles du fichier de base sont reprises telles quelles,
|
||||
// comme dans le Blueprint (35 coiffures "legacy" absentes du projet depuis 4.27 y figurent).
|
||||
// Les modules desactives dans GI.ModEnableds sont ignores ; la table est reconstruite quand cette liste change.
|
||||
// Option -LPVerifyHairstyles : la table et les morphs construits depuis le fichier de base sont compares a ceux du
|
||||
// Blueprint (texte exporte), avant l'ajout des modules.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
|
@ -36,16 +44,25 @@ private:
|
|||
FString File;
|
||||
FString Module;
|
||||
FString Path;
|
||||
bool bNone = false;
|
||||
bool bMale = false;
|
||||
bool bDaz = true;
|
||||
FString FitMorph;
|
||||
FString FitValue;
|
||||
};
|
||||
|
||||
struct FMorphDef
|
||||
{
|
||||
FString Key;
|
||||
FString Morph;
|
||||
FString Value;
|
||||
};
|
||||
|
||||
bool Tick(float DeltaTime);
|
||||
/** Releve des fichiers .lphair (une fois : les fichiers de Modules ne changent pas en cours de partie). */
|
||||
void ScanFiles();
|
||||
/** Retire les entrees ajoutees precedemment puis ajoute celles des modules actifs. */
|
||||
void ParseFile(const FString& Relative, const FString& Module, const FString& Text);
|
||||
/** Reconstruit la table : base (fichier MODE: BASE ou Blueprint) puis coiffures des modules actifs. */
|
||||
void Sync();
|
||||
FString EnabledSignature() const;
|
||||
bool IsModuleEnabled(const FString& Module) const;
|
||||
|
|
@ -53,9 +70,14 @@ private:
|
|||
FTSTicker::FDelegateHandle TickHandle;
|
||||
double NextCheck = 0.0;
|
||||
FString LastSignature;
|
||||
/** Coiffures et morphs du fichier de base, dans l'ordre. */
|
||||
FString BaseFile;
|
||||
TArray<FHairDef> BaseDefs;
|
||||
TArray<FMorphDef> BaseMorphs;
|
||||
/** Coiffures des autres fichiers. */
|
||||
TArray<FHairDef> Defs;
|
||||
/** Chemins ajoutes a HairstyleTable par ce sous-systeme, dans l'ordre. */
|
||||
TArray<FString> Injected;
|
||||
/** Cles ajoutees a MorphableHairs par ce sous-systeme. */
|
||||
TArray<FString> InjectedMorphs;
|
||||
/** Table et morphs du Blueprint, captures au premier passage (texte exporte, pour revenir a la base sans fichier). */
|
||||
FString BlueprintTableText;
|
||||
FString BlueprintMorphsText;
|
||||
bool bCaptured = false;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue