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>
This commit is contained in:
parent
d0fc9f7069
commit
cad164957e
4 changed files with 426 additions and 0 deletions
|
|
@ -534,6 +534,48 @@ plugin `HairStrands` n'est plus actif dans le jeu depuis O1. Restent ≈ 12 mail
|
|||
5. Test en jeu : personnage `.lpcharacter` avec `HAIR Hairstyle: <nom>`, menu de création, suivi des animations.
|
||||
Morphs de forme : hors périmètre du premier essai.
|
||||
|
||||
### 16.7 H1 : coiffures déclarées par fichiers `.lphair` (2026-10-02)
|
||||
|
||||
Demande utilisateur : une table figée dans un Blueprint ne se prête pas au modding ; externaliser la liste des
|
||||
coiffures. Réalisation sans modifier de Blueprint : `Source/LifePlay/LPHairstyleSubsystem` (sous-système de
|
||||
`GameInstance`).
|
||||
|
||||
**Format** : un fichier par coiffure, `Modules/<module>/**/*.lphair` :
|
||||
|
||||
```
|
||||
HAIRSTYLE: /Game/Heavy/New5/Hairs/Exemple.Exemple
|
||||
SEX: F (F ou M, défaut F)
|
||||
DAZ: true (défaut true)
|
||||
FIT_MORPH: Exemple__AllExpand 0.45 (facultatif)
|
||||
```
|
||||
|
||||
**Fonctionnement**
|
||||
|
||||
- au démarrage, relevé des fichiers `.lphair` de `Modules` ; sans fichier, le sous-système ne fait rien ;
|
||||
- chaque coiffure d'un module actif (`GI.ModEnableds`) est ajoutée **en fin** de `GI.HairstyleTable` ; son morph
|
||||
d'ajustement éventuel est ajouté à `GI.MorphableHairs` ;
|
||||
- contrôles : maillage présent dans le registre d'assets et de classe `SkeletalMesh`, chemin absent de la table
|
||||
(pas de doublon) ; chaque refus est journalisé (`LogLifePlay`) ;
|
||||
- les entrées du Blueprint ne sont jamais modifiées ni réordonnées ; les personnages stockent la coiffure par nom :
|
||||
les sauvegardes restent compatibles ;
|
||||
- la liste des modules actifs est relue toutes les 2 s : activer ou désactiver un module dans le gestionnaire ajoute
|
||||
ou retire ses coiffures (les entrées ajoutées sont retirées puis reconstruites).
|
||||
|
||||
**Premier module** : `Modules/lp_Revival` (`lp_Revival.lpmod`, désactivable), qui déclare la coiffure de test
|
||||
`/Game/Heavy/New5/Hairs/H1_TestHair` : copie de `New4/Hairs/FloraHair_New` sous un nouveau nom, donc absente de la
|
||||
table (à remplacer par une coiffure Fab, lot O7e). Les 88 coiffures présentes dans le projet sont toutes dans la table.
|
||||
|
||||
**Vérifications** (build `LifePlay_Build_5.5_H1`, journal du jeu) :
|
||||
|
||||
- doublons : deux fichiers d'essai désignant `soldier` et `armani` (déjà dans la table sous `Soldier`, `Armani`) sont
|
||||
refusés avec « déjà dans la table » ; la comparaison des chemins ignore la casse, comme les noms d'assets d'UE ;
|
||||
- ajout : `H1_TestHair ajoutee (index 126, F, module lp_Revival)`, table de 127 entrées ;
|
||||
- piège de build : avec `-iterate`, la mise en place ne supprime pas les fichiers retirés de `Modules` ; un fichier
|
||||
supprimé doit l'être aussi du build (ou le build refait à neuf avant publication).
|
||||
|
||||
**Limite** : le maillage doit être importé dans le projet et cuit avec le jeu ; un mod ne peut pas encore apporter
|
||||
son propre maillage (étape H3 : paquets `.pak` montés depuis `Modules/<mod>/`).
|
||||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -928,3 +928,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
|
|||
| CHG-081 | 2026-10-02 | O7d | nouveau `Tools/BuildCollections.py` ; 13 collections statiques `Content/Collections/LifePlay*.collection` (hors dépôt, régénérables) ; `11_Audit_Optimisation.md` §16.5, `08_Guide_Developpeur.md` §2.1, `09_Commandes_Utiles.md` §3 | suite d'O7 (choix utilisateur) : regrouper les 8 561 assets par thème dans l'éditeur sans renommer de dossier ; collection transversale `LifePlay_Chemins_Fixes` (4 984 assets à ne pas déplacer) ; 13 545 chemins, 0 inconnu, chargées par l'éditeur ; aucun asset modifié, pas de build | supprimer `Content/Collections/LifePlay*.collection` (`git checkout O7d-avant` pour le reste) |
|
||||
| CHG-082 | 2026-10-02 | O7d | `Tools/BuildCollections.py` : collections écrites dans `Saved/Collections` (locales) au lieu de `Content/Collections` (partagées) ; 13 fichiers déplacés vers `D:\Game\LifePlay_Backup_5.5\O7d\Content_Collections` puis régénérés (identiques) ; `11_Audit_Optimisation.md` §16.5, `08_Guide_Developpeur.md` §2.1 | avertissement à l'ouverture de l'éditeur (utilisateur) : une collection partagée exige un fournisseur de contrôle de révision, désactivé dans ce projet ; une collection locale n'en utilise pas ; validé par l'utilisateur le 2026-10-02 (plus d'avertissement) | `git checkout O7d-apres -- Tools/BuildCollections.py` et remettre les fichiers sauvegardés |
|
||||
| CHG-083 | 2026-10-02 | O7e | `11_Audit_Optimisation.md` §16.6 | étude en lecture seule de l'intégration d'une coiffure Fab (demande utilisateur) : liste des coiffures = valeur par défaut `GI.HairstyleTable` (126 entrées, 35 absentes déjà en 4.27), suivi par noms d'os Genesis 8, morphs de forme facultatifs, sauvegardes par nom ; 34 coiffures Fab, ≈ 12 maillages utilisables | — (documentation) |
|
||||
| CHG-084 | 2026-10-02 | H1 | nouveaux `Source/LifePlay/LPHairstyleSubsystem.h/.cpp`, module `Content/Modules/lp_Revival` (`lp_Revival.lpmod`, `Hairstyles/h1_testhair.lphair`) ; asset de test `Content/Heavy/New5/Hairs/H1_TestHair` (copie de `FloraHair_New`, hors dépôt) ; `11_Audit_Optimisation.md` §16.7 | liste des coiffures ouverte aux mods (demande utilisateur) : fichiers `.lphair` ajoutés en fin de `GI.HairstyleTable` sans modifier de Blueprint ; vérifié au journal : doublons refusés, 1 coiffure ajoutée (table 126 → 127) ; build `LifePlay_Build_5.5_H1` ; test en jeu en attente | supprimer le module `lp_Revival` (ou le désactiver) ; `git checkout H1-avant -- Source` |
|
||||
|
|
|
|||
322
Source/LifePlay/LPHairstyleSubsystem.cpp
Normal file
322
Source/LifePlay/LPHairstyleSubsystem.cpp
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
// 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());
|
||||
}
|
||||
61
Source/LifePlay/LPHairstyleSubsystem.h
Normal file
61
Source/LifePlay/LPHairstyleSubsystem.h
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Portage UE 5.5 - lot H1 (Docs/Migration/11_Audit_Optimisation.md §16.7).
|
||||
//
|
||||
// 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) :
|
||||
//
|
||||
// HAIRSTYLE: /Game/Heavy/New5/Hairs/Exemple.Exemple chemin complet d'un SkeletalMesh (squelette Genesis 8)
|
||||
// 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
|
||||
//
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Containers/Ticker.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "LPHairstyleSubsystem.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class LIFEPLAY_API ULPHairstyleSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
private:
|
||||
struct FHairDef
|
||||
{
|
||||
FString File;
|
||||
FString Module;
|
||||
FString Path;
|
||||
bool bMale = false;
|
||||
bool bDaz = true;
|
||||
FString FitMorph;
|
||||
FString FitValue;
|
||||
};
|
||||
|
||||
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 Sync();
|
||||
FString EnabledSignature() const;
|
||||
bool IsModuleEnabled(const FString& Module) const;
|
||||
|
||||
FTSTicker::FDelegateHandle TickHandle;
|
||||
double NextCheck = 0.0;
|
||||
FString LastSignature;
|
||||
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;
|
||||
};
|
||||
Loading…
Reference in a new issue