H3a et UI15 : mod d'assets cuit a part (Pocolov 05), gestionnaire de mods sur l'ecran titre

- H3a : version de reference 2026.10.4 (identique au V1.4 publie), mod d'essai LPHairSample
  (ModSources/, cuisson DLC contre 2026.10.4), pipeline de coiffures compatible plugins.
- Activation des modules alignee sur le jeu (module absent = inactif) ; cle .lpmod
  MODULE_DEFAULT_ENABLED (lp_Revival).
- UI15 : entree Mod Manager sur l'ecran titre, retour au menu titre, choix enregistre
  sans toucher a la derniere sauvegarde (LPTitleModManager, SaveModSelection).

Valide en jeu le 2026-10-03 (CHG-101 a CHG-103, ERR-040, ERR-041).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-10-03 04:07:11 +02:00
parent 6f87c8b920
commit 3c2da7a516
22 changed files with 767 additions and 6 deletions

9
.gitignore vendored
View file

@ -39,3 +39,12 @@ Content/Developers/
# Publication (2026-09-30) : outil d'automatisation IA de l'environnement de developpement, hors jeu.
# Source : https://github.com/ChiR24/Unreal_mcp (MIT). Le .uproject le reference : sans lui, l'editeur propose de le desactiver.
Plugins/McpAutomationBridge/
# H3 (mods, 2026-10-03) : version de reference des publications (cuisson -createreleaseversion, 1,3 Go, livree avec
# l'archive des sources), plugins installes temporairement pour la cuisson DLC, produits de cuisson et assets binaires
# des mods (comme Content/Heavy : livres par l'archive des sources).
/Releases/
/Mods/
/ModSources/*/Saved/
/ModSources/*/Intermediate/
/ModSources/*/Content/

Binary file not shown.

Binary file not shown.

View file

@ -55,6 +55,13 @@ git status; git log --oneline -5; git tag --list '*-valide' # où en est-on
& $Cmd $Proj -run=LPHairFromStaticMesh -Static=<maillage> -FitHead=<tete> -Out=/Game/Heavy/New5/Hairs/<Nom> -DryRun -unattended -nopause
& $Cmd $Proj -run=pythonscript "-script=<projet>/Tools/HairMaterials.py mesh=/Game/Heavy/New5/Hairs/<Nom> diffuse=<f> strands_opacity=<f> cap_opacity=white cap=<emplacement>" -unattended -nopause
# Gestionnaire de mods sur l'écran titre (UI15, 17_Mods_Ecran_Titre.md)
& $Cmd $Proj -run=LPTitleModManager -DryRun -unattended -nopause
# Mod d'assets (H3, 16_H3_Mods_Paquets.md §5) : version de référence puis cuisson DLC, toujours via cmd /c (version)
# cmd /c '"$UE\Build\BatchFiles\RunUAT.bat" BuildCookRun ... -createreleaseversion=2026.10.4 ...'
# cmd /c '"$UE\Build\BatchFiles\RunUAT.bat" BuildCookRun ... -cook -stage -pak -iostore -compressed -dlcname="<projet>\Mods\<Nom>\<Nom>.uplugin" -basedonreleaseversion=2026.10.4 ...'
# Collections thématiques de l'éditeur (O7d), essai à blanc puis écriture
& $Cmd $Proj -run=pythonscript "-script=$($Proj | Split-Path)/Tools/BuildCollections.py -dryrun" -unattended -nopause
& $Cmd $Proj -run=pythonscript "-script=$($Proj | Split-Path)/Tools/BuildCollections.py" -unattended -nopause

View file

@ -66,3 +66,43 @@ mods, activation, données) ; les assets sont dans `Mods` (moteur). Désactiver
| **H3a, prototype** | version de référence `Releases/2026.10.4` ; mod d'essai `LPHairSample` (une coiffure Pocolov non encore intégrée, ex. Pocolov 05/07) ; cuisson DLC ; installation dans une **copie** du build Shipping V1.4 | coiffure du mod visible et fonctionnelle en jeu, sans recuire le jeu |
| **H3b, outillage** | `Tools/BuildMod.ps1` (squelette de plugin, cuisson DLC, archive du mod) ; option `HairPipeline` vers `/<Nom>/` ; repli `DoesPackageExist` dans le sous-système | un mod de coiffures en une commande |
| **H3c, documentation moddeurs** | guide (anglais) : créer, cuire, distribuer, compatibilité de version ; intégration de la création de `Releases/<version>` au protocole de publication | guide publié avec la version suivante |
## 5. Prototype H3a (2026-10-03) : résultats
Validé en jeu le 2026-10-03 : la coiffure Pocolov 05, apportée par un mod cuit à part, est chargée par le jeu publié
sans recuisson du jeu ; décocher le mod la retire.
| Étape | Résultat |
|---|---|
| Version de référence | `-createreleaseversion=2026.10.4` (build Shipping, cuisson incrémentale) : `Releases/2026.10.4/Windows` ; paquets **identiques octet pour octet** à ceux du V1.4 publié. Taille : **1,3 Go** (copie complète des paquets du jeu, à livrer aux moddeurs : H3c) |
| Mod d'essai | plugin `LPHairSample` (`CanContainContent`), coiffure importée par `Tools/HairPipeline.ps1 -Config Tools/mods/LPHairSample.json` dans `/LPHairSample/Hairs` ; dépendances : ses assets et `/Game/Heavy/Daz/Bridget/Hairs/DazHair` |
| Cuisson DLC | `-dlcname=<chemin du .uplugin> -basedonreleaseversion=2026.10.4` : 6 paquets, 0 erreur, 0 avertissement ; `LPHairSampleLifePlay-Windows.pak/.utoc/.ucas` (≈ 337 Ko) ; le `.pak` contient le `AssetRegistry.bin` du mod |
| Installation | `<jeu>/LifePlay/Mods/LPHairSample/` (`.uplugin` + `Content/Paks/Windows/*`) et `<jeu>/LifePlay/Content/Modules/LPHairSample/` (`.lpmod`, `.lphair`, `CREDITS.txt`) |
| Compatibilité | mod cuit contre 2026.10.4, chargé aussi par le build UI15b (menu principal modifié depuis) |
Commandes :
```powershell
cmd /c '"<UE>\Build\BatchFiles\RunUAT.bat" BuildCookRun -project="<projet>" -platform=Win64 -clientconfig=Shipping -build -cook -iterate -stage -pak -iostore -compressed -createreleaseversion=2026.10.4 -archive -archivedirectory=<dossier> -nodebuginfo -unattended -utf8output -nop4'
cmd /c '"<UE>\Build\BatchFiles\RunUAT.bat" BuildCookRun -project="<projet>" -platform=Win64 -clientconfig=Shipping -cook -stage -pak -iostore -compressed -dlcname="<projet>\Mods\LPHairSample\LPHairSample.uplugin" -basedonreleaseversion=2026.10.4 -nodebuginfo -unattended -utf8output -nop4'
```
Enseignements :
- **Version en argument** : appelé directement depuis PowerShell, `-createreleaseversion=2026.10.4` est coupé en
`2026` et `.10.4` (version `2026` créée, déplacée vers `LifePlay_Backup_5.5\H3a`) : passer par `cmd /c '…'`.
- **Sources des mods hors du jeu de base** : un plugin dans `<projet>/Mods/` est un plugin Mod chargé par l'éditeur et
cuit avec le jeu ; un module dans `Content/Modules/` est livré avec le jeu. Les sources du mod d'essai sont donc dans
`ModSources/LPHairSample/` (`.uplugin`, `Content/` non versionné, `Module/` texte) ; pour la cuisson DLC, le plugin est
placé temporairement dans `Mods/` (H3b : `Tools/BuildMod.ps1`). Les builds de test H3a/UI15 ont été faits avec les
sources sorties du projet ; vérifié : aucun asset `/LPHairSample/` dans les paquets du jeu.
- **Activation des modules** (défaut de H1, présent en V1.4) : pour le jeu (`GI.IsModEnabled`, gestionnaire de mods), un
module absent de `GI.ModEnableds` est inactif ; `LPHairstyleSubsystem` le croyait actif (coiffure du mod présente,
mod décoché). Corrigé : même règle que le jeu ; nouvelle clé `.lpmod` `MODULE_DEFAULT_ENABLED: true`, qui ajoute
le module coché à la liste des mods s'il n'y figure pas (`lp_Revival`, livré avec le jeu).
- **Gestionnaire de mods** : accessible seulement en partie ; entrée ajoutée sur l'écran titre (lot UI15,
[17_Mods_Ecran_Titre.md](17_Mods_Ecran_Titre.md)).
- `Tools/HairPipeline.ps1` accepte un dossier de plugin (`/<Plugin>/...` → `Mods/<Plugin>/Content/...`) et un fichier
de crédits propre au mod (`credits`) ; `Tools/HairMaterials.py` contrôle les dépendances par chemin de paquet.
- Relancer `HairMaterials.py` sur une coiffure dont les textures existent déjà dans un plugin fait planter la commandlet
(boîte de dialogue de remplacement à l'import) : à traiter en H3b.

View file

@ -0,0 +1,50 @@
# 17. Gestionnaire de mods depuis l'écran titre (lot UI15, 2026-10-03)
Demande utilisateur (test de H3a) : pouvoir activer ou désactiver les mods dès le démarrage, puis choisir entre une
nouvelle partie et une partie sauvegardée.
## 1. Constat
- Le gestionnaire de mods (`UI/ModManager`) n'était accessible qu'en partie : entrée « Mod Manager » du menu
(`MainMenu.ModManagerButton`, sous-entrée en retrait sous « Resume »), masquée sur l'écran titre par la liaison de
visibilité `ColapsedForTitleScreen`. Même chose en 4.27 (mêmes références à `ModManagerInput`).
- Sa fermeture (`GI.CloseModManager`) suppose une partie en cours : `ShowUI`, `ClearModManagerWidget`, `PopulateRaws`
(relecture des données des modules), puis `CharGenInput(GameStart=true)` ou `DelayThenFollowUp` (suite du tour).
Le paramètre `GameStart` (gestionnaire en tête de nouvelle partie) existe dans `GI` mais n'est appelé par personne.
- `GI.SaveSettings(Slot)` recrée tout `GameSettings.sav` et y mémorise `Slot` comme dernière sauvegarde
(`LastSaveGame`, utilisée par « Load Last Save ») : inutilisable hors sauvegarde de partie.
## 2. Modification
Commandlet `LPTitleModManager` (Blueprint `GI` non modifié) :
| Blueprint | Modification |
|---|---|
| `UI/MainMenu` | nouvelle entrée `ModManagerTitleButton` : copie de `LoadButton` (même taille, police et icône ronde, sans retrait), icône `mod`, placée après « Load », visible seulement sur l'écran titre (liaison `OnlyVisibleForTitleScreen`) ; clic : menu masqué, `GI.CreateModManagerWidget(GameStart=false)`, `GI.ModManager.TitleMenu = self` (7 nœuds) |
| `UI/ModManager` | variable `TitleMenu` (menu titre qui l'a ouvert) ; fermeture : si `TitleMenu` est valide, menu titre réaffiché et focalisé, `GI.ClearModManagerWidget`, `GI.PopulateRaws` (données relues avec les modules choisis), `SaveModSelection` ; sinon circuit d'origine `GI.CloseModManager` (10 nœuds) |
L'entrée d'origine `ModManagerButton` n'est pas modifiée : en partie, le gestionnaire se comporte comme avant.
`ULPModSettingsLibrary::SaveModSelection` (C++, `Source/LifePlay`) relit `GameSettings.sav`, n'y remplace que
`ModEnableds` et le réécrit : la dernière sauvegarde et les autres réglages sont conservés. Sans `GameSettings.sav`
(premier lancement), rien n'est écrit : le choix vaut pour la session et sera enregistré à la première sauvegarde de
partie, comme en partie.
```powershell
& $Cmd $Proj -run=LPTitleModManager -DryRun -unattended -nopause # essai à blanc (compilation comprise)
& $Cmd $Proj -run=LPTitleModManager -unattended -nopause # application ; un Blueprint déjà traité est sauté
```
## 3. Écarts en cours de lot
- Première version : l'entrée d'origine rendue visible sur l'écran titre (liaison retirée). Retour utilisateur : sous-entrée
en retrait et en petite police, non alignée. Remplacée par l'entrée dédiée ci-dessus (`MainMenu` restauré depuis le
snapshot `UI15` avant la nouvelle application).
- `SaveSettings(0)` envisagé puis écarté à la relecture du graphe (il aurait fait de l'emplacement 0 la dernière
sauvegarde) : remplacé par `SaveModSelection`.
## 4. Validation
Build `LifePlay_Build_5.5_UI15b`, validé en jeu le 2026-10-03 : entrée « Mod Manager » de l'écran titre au format des
autres, fermeture vers le menu titre, choix des modules appliqué (mod `LPHairSample` coché ou non) ; en partie, sous-entrée
d'origine inchangée.

View file

@ -945,3 +945,6 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
| CHG-098 | 2026-10-02 | V1.4 | `README.md` : archive des sources V1.4 en téléchargement principal (lien Mega), V1.3 ajoutée aux versions précédentes | lien Mega de l'archive développeur reçu ; liens du jeu et des modules optionnels à venir | — |
| CHG-099 | 2026-10-02 | V1.4 | `README.md` : section V1.4, point des modules optionnels reformulé (plus de nom d'archive ni de mode d'installation) | décision utilisateur : les archives du jeu et des modules optionnels ne sont pas annoncées sur GitHub (la documentation technique garde les détails de l'outil) | `git revert` du commit |
| CHG-100 | 2026-10-03 | H3 | nouveau `Docs/Migration/16_H3_Mods_Paquets.md` | préparation de H3 (demande utilisateur) : étude en lecture seule du moteur 5.5 (dossier `Mods/` lu par le gestionnaire de plugins en jeu empaqueté, plugins Mod activés par défaut, paquets de plugins montés au démarrage, registre `AssetRegistry.bin` des plugins chargé) et de la configuration (paquets non signés, shaders intégrés aux matériaux, IoStore) ; architecture, risques, lots H3a à H3c | — (documentation) |
| CHG-101 | 2026-10-03 | H3a | `Releases/2026.10.4` (non versionné, 1,3 Go) ; `ModSources/LPHairSample/` (`LPHairSample.uplugin`, `Module/LPHairSample.lpmod`, `Module/Hairstyles/pocolov05.lphair`, `Module/CREDITS.txt` ; assets `Content/Hairs/Pocolov05*` non versionnés) ; `Tools/mods/LPHairSample.json` ; `Tools/HairPipeline.ps1` (chemins de plugin, `credits`) ; `Tools/HairMaterials.py` (contrôle des dépendances par chemin de paquet) ; `.gitignore` (`/Releases/`, `/Mods/`, produits et assets de `ModSources`) ; builds `LifePlay_Build_5.5_Ref_2026.10.4`, `LifePlay_Build_5.5_H3a` | prototype H3a : version de référence (paquets identiques au V1.4 publié), mod d'essai Pocolov 05 (Fab, CC BY 4.0, téléchargement accordé) cuit en DLC contre 2026.10.4 (6 paquets, 0 erreur) et installé dans une copie du V1.4 ; version `2026` mal nommée (argument coupé par PowerShell) déplacée vers `LifePlay_Backup_5.5\H3a` ; **validé en jeu le 2026-10-03** (16_H3_Mods_Paquets.md §5) | supprimer `ModSources/LPHairSample`, `Tools/mods`, `Releases/2026.10.4` ; `git revert` du commit |
| CHG-102 | 2026-10-03 | H3a | `Source/LifePlay/LPHairstyleSubsystem.h/.cpp` ; `Content/Modules/lp_Revival/lp_Revival.lpmod` (`MODULE_DEFAULT_ENABLED: true`) | retour utilisateur : coiffure du mod présente alors que le mod est décoché. Le sous-système (H1) tenait pour actif un module absent de `GI.ModEnableds`, alors que le jeu le tient pour inactif (défaut présent en V1.4 : `lp_Revival` affiché décoché mais actif). Même règle que le jeu ; un module à clé `MODULE_DEFAULT_ENABLED: true` absent de la liste y est ajouté coché (au démarrage et après `LoadSettings`). Validé en jeu le 2026-10-03 | `git revert` du commit, recompiler |
| CHG-103 | 2026-10-03 | UI15 | `Content/UI/MainMenu.uasset`, `Content/UI/ModManager.uasset` (snapshot `LifePlay_Backup_5.5\UI15`) ; nouveaux `Source/LifePlayEditorTools/LPTitleModManagerCommandlet.h/.cpp`, `Source/LifePlay/LPModSettingsLibrary.h/.cpp`, `Docs/Migration/17_Mods_Ecran_Titre.md` ; build `LifePlay_Build_5.5_UI15b` | demande utilisateur : gestionnaire de mods dès l'écran titre, puis choix entre nouvelle partie et partie sauvegardée. Entrée dédiée sur l'écran titre (copie de « Load »), fermeture vers le menu titre avec relecture des données et enregistrement du choix (`SaveModSelection`, sans toucher à la dernière sauvegarde) ; entrée d'origine inchangée en partie. Première version (entrée d'origine rendue visible) remplacée après retour (alignement). **Validé en jeu le 2026-10-03** | `.\Tools\Restore.ps1 -Lot UI15` ; `git revert` du commit |

View file

@ -26,7 +26,8 @@ La migration a été reprise le 2026-09-29, après l'échec d'un premier portage
| [13_Protocole_Coiffure.md](13_Protocole_Coiffure.md) | protocole | ajouter une coiffure (FBX statique, Fab) : import, ajustement sur la tête de Bridget, matériaux, fichier `.lphair`, contrôles |
| [14_Interface_V1.4.md](14_Interface_V1.4.md) | lot | menu principal : version année.mois.publication, crédits (Vinfamy, auteur original), liens Donate et Discord, bouton Commission masqué |
| [15_Modules_Optionnels.md](15_Modules_Optionnels.md) | lot | build Shipping réduite aux modules essentiels, modules optionnels dans une archive séparée (`Tools/SplitModules.ps1`) |
| [16_H3_Mods_Paquets.md](16_H3_Mods_Paquets.md) | étude | H3 : assets apportés par les mods (plugins `Mods/`, cuisson DLC contre la version publiée) ; constats, architecture, risques, lots H3a à H3c |
| [16_H3_Mods_Paquets.md](16_H3_Mods_Paquets.md) | étude, prototype | H3 : assets apportés par les mods (plugins `Mods/`, cuisson DLC contre la version publiée) ; constats, architecture, risques, lots H3a à H3c ; §5 résultats du prototype H3a (validé) |
| [17_Mods_Ecran_Titre.md](17_Mods_Ecran_Titre.md) | lot | UI15 : gestionnaire de mods depuis l'écran titre (entrée dédiée, retour au menu titre, choix enregistré) |
| [Journal_Modifications.md](Journal_Modifications.md) | traçabilité | `CHG-001` à `CHG-057`, chacune avec son retour arrière |
| [Registre_Erreurs.md](Registre_Erreurs.md) | traçabilité | `ERR-001` à `ERR-037`, statut et lot |
| `correctifs/` | relecture | écarts de code 4.27 → 5.5 en patchs (`P01` à `P06`) |

View file

@ -44,3 +44,5 @@
| ERR-037 | Quick dress (menu Action) laisse souvent le personnage principal nu | retour utilisateur 2026-10-01 | — | moyenne (visible) | — | **préexistant en 4.27** (confirmé par l'utilisateur le 2026-10-01 : même comportement au travail) : au travail seuls les vêtements « Formal » sont acceptés ; sans bas ni chaussures formels dans l'inventaire, seul le haut est mis. Hors périmètre |
| ERR-038 | Au changement de lieu, les personnages de la pièce précédente apparaissent un instant dans la nouvelle, avant d'être détruits et remplacés | retour utilisateur 2026-10-02 (partie O9) | à chaque changement de lieu | faible (visuel) | — | **préexistant en 4.27** (confirmé par l'utilisateur) : tous les lieux sont reconstruits dans le même studio ; les anciens personnages ne sont détruits qu'après la reconstruction. Essai de correction par sous-système C++ (`LPRoomTransitionSubsystem`, CHG-070/072) **retiré** (CHG-073) : au moment de la reconstruction, les personnages créés pendant le trajet et retirés 2 s après l'arrivée (le défaut) sont indiscernables des vrais clients du lieu d'arrivée, créés de la même façon et conservés ; le sous-système en cachait donc aussi à tort. Une correction fiable devra se faire dans `GI` (décision de retrait après le trajet), dans un lot dédié. **Ouvert, défaut d'origine** |
| ERR-039 | `GroomComponent` : `Failed to find /HairStrands/Materials/…` (4 erreurs au démarrage) | journaux des builds depuis O1 | 4 par lancement | faible (aucune chevelure Groom utilisée) | O1 | **corrigé (CHG-077)** : le module `HairStrands` reste dans le jeu (dépendance d'autres plugins) ; son contenu est de nouveau cuit (`/HairStrands` retiré de `DirectoriesToNeverCook`), plus d'erreur au démarrage |
| ERR-040 | À la création du personnage (nouvelle partie), la femme proposée par défaut est chauve | retour utilisateur 2026-10-03 (test UI15) | à chaque nouvelle partie | faible (visuel) | — | **préexistant en V1.4** (confirmé par l'utilisateur) ; 4.27 et V1.3 non vérifiés. Piste non vérifiée : première entrée de `GI.HairstyleTable` = « NONE » pour une femme. **Ouvert** |
| ERR-041 | Module absent de `GI.ModEnableds` tenu pour actif par `LPHairstyleSubsystem` (coiffures d'un mod décoché présentes) | retour utilisateur 2026-10-03 (test H3a) | toujours | moyenne | H3a | défaut introduit en H1, présent en V1.4 ; **corrigé (CHG-102)**, validé en jeu le 2026-10-03 |

View file

@ -0,0 +1,12 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "LifePlay Hair Sample",
"Description": "Sample LifePlay mod: one hairstyle (Pocolov Hair 05, CC BY 4.0) cooked as a separate package.",
"Category": "LifePlay Mods",
"CreatedBy": "LifePlay Revival",
"CanContainContent": true,
"IsBetaVersion": false,
"Installed": false
}

View file

@ -0,0 +1 @@
- Pocolov Hair 05 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/4025749f-d597-4332-91aa-16e355b94974 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/LPHairSample/Hairs/Pocolov05*).

View file

@ -0,0 +1,6 @@
// Pocolov Hair 05 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
// Source : https://www.fab.com/listings/4025749f-d597-4332-91aa-16e355b94974
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
HAIRSTYLE: /LPHairSample/Hairs/Pocolov05.Pocolov05
SEX: F
DAZ: true

View file

@ -0,0 +1,6 @@
MODULE_UNIQUEID: LPHairSample
MODULE_NAME: LifePlay Hair Sample
MODULE_AUTHOR: LifePlay Revival
MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival
MODULE_DESCRIPTION: Sample mod: one hairstyle (Pocolov Hair 05, CC BY 4.0) shipped as a cooked package in Mods/LPHairSample. Disabling this module removes the hairstyle from the game list.
MODULE_REQUIREMENTS: vin_Base

View file

@ -150,6 +150,7 @@ void ULPHairstyleSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
return;
}
EnsureDefaultEnabledModules();
LastSignature = EnabledSignature();
Sync();
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPHairstyleSubsystem::Tick));
@ -169,6 +170,8 @@ bool ULPHairstyleSubsystem::Tick(float DeltaTime)
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)
{
@ -182,6 +185,43 @@ bool ULPHairstyleSubsystem::Tick(float DeltaTime)
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();
@ -332,7 +372,37 @@ bool ULPHairstyleSubsystem::IsModuleEnabled(const FString& Module) const
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
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()

View file

@ -82,11 +82,17 @@ private:
/** Listes de pilosite : base (fichier ou Blueprint) puis noms des modules actifs. */
void SyncTextureLists(UObject* GI);
FString EnabledSignature() const;
/** Meme regle que GI.IsModEnabled : un module absent de GI.ModEnableds est inactif (lot H3a). */
bool IsModuleEnabled(const FString& Module) const;
/** Modules a cle MODULE_DEFAULT_ENABLED: true absents de GI.ModEnableds : ajoutes actifs (le gestionnaire de mods les
* affiche coches). Renvoie vrai si la liste a change. */
bool EnsureDefaultEnabledModules();
FTSTicker::FDelegateHandle TickHandle;
double NextCheck = 0.0;
FString LastSignature;
/** Modules dont le .lpmod porte MODULE_DEFAULT_ENABLED: true (lp_Revival, livre avec le jeu). */
TArray<FString> DefaultEnabledModules;
/** Coiffures et morphs du fichier de base, dans l'ordre. */
FString BaseFile;
TArray<FHairDef> BaseDefs;

View file

@ -0,0 +1,36 @@
// Portage UE 5.5 - voir LPModSettingsLibrary.h
#include "LPModSettingsLibrary.h"
#include "GameFramework/SaveGame.h"
#include "Kismet/GameplayStatics.h"
#include "LifePlay.h"
#include "UObject/UnrealType.h"
bool ULPModSettingsLibrary::SaveModSelection(UObject* GameInstance)
{
static const TCHAR* Slot = TEXT("GameSettings");
static const FName ModEnabledsName(TEXT("ModEnableds"));
if (!GameInstance)
{
return false;
}
if (!UGameplayStatics::DoesSaveGameExist(Slot, 0))
{
UE_LOG(LogLifePlay, Display, TEXT("Modules : pas encore de %s.sav, choix enregistre a la premiere sauvegarde"), Slot);
return false;
}
USaveGame* Settings = UGameplayStatics::LoadGameFromSlot(Slot, 0);
const FArrayProperty* From = CastField<FArrayProperty>(GameInstance->GetClass()->FindPropertyByName(ModEnabledsName));
const FArrayProperty* To = Settings ? CastField<FArrayProperty>(Settings->GetClass()->FindPropertyByName(ModEnabledsName)) : nullptr;
if (!From || !To || !From->SameType(To))
{
UE_LOG(LogLifePlay, Warning, TEXT("Modules : %s.sav illisible ou sans ModEnableds compatible, choix non enregistre"), Slot);
return false;
}
To->CopyCompleteValue(To->ContainerPtrToValuePtr<void>(Settings), From->ContainerPtrToValuePtr<void>(GameInstance));
const bool bSaved = UGameplayStatics::SaveGameToSlot(Settings, Slot, 0);
FScriptArrayHelper Helper(To, To->ContainerPtrToValuePtr<void>(Settings));
UE_LOG(LogLifePlay, Display, TEXT("Modules : choix (%d module(s)) %s dans %s.sav"), Helper.Num(), bSaved ? TEXT("enregistre") : TEXT("NON enregistre"), Slot);
return bSaved;
}

View file

@ -0,0 +1,25 @@
// Portage UE 5.5 - lot UI15 (gestionnaire de mods depuis l'ecran titre, Docs/Migration/17_Mods_Ecran_Titre.md).
//
// Le choix des modules (GI.ModEnableds) est enregistre dans GameSettings.sav par GI.SaveSettings(Slot), appele a chaque
// sauvegarde de partie ; cet evenement recree tout l'objet et memorise Slot comme derniere sauvegarde
// (LastSaveGame, "Load Last Save"). Depuis l'ecran titre, aucune partie n'est sauvegardee : SaveModSelection relit
// GameSettings.sav, n'y remplace que ModEnableds et le reecrit (tout le reste, LastSaveGame compris, est conserve).
// Sans GameSettings.sav (premier lancement) : rien n'est ecrit, le choix vaut pour la session et sera enregistre a la
// premiere sauvegarde de partie, comme en partie.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "LPModSettingsLibrary.generated.h"
UCLASS()
class LIFEPLAY_API ULPModSettingsLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Ecrit GI.ModEnableds dans GameSettings.sav sans toucher au reste ; vrai si le fichier a ete reecrit. */
UFUNCTION(BlueprintCallable, Category = "LifePlay|Mods")
static bool SaveModSelection(UObject* GameInstance);
};

View file

@ -0,0 +1,403 @@
// Portage UE 5.5 - voir LPTitleModManagerCommandlet.h
#include "LPTitleModManagerCommandlet.h"
#include "Blueprint/WidgetBlueprintLibrary.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Button.h"
#include "Components/Image.h"
#include "Components/PanelWidget.h"
#include "Components/TextBlock.h"
#include "Components/Widget.h"
#include "Engine/Texture2D.h"
#include "EdGraph/EdGraph.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_CallFunction.h"
#include "K2Node_ComponentBoundEvent.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_Self.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "LPModSettingsLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Misc/PackageName.h"
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
#include "WidgetBlueprint.h"
#include "WidgetBlueprintEditorUtils.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPTitleMods, Log, All);
namespace
{
const FName TitleMenuName(TEXT("TitleMenu"));
const FName TitleButtonName(TEXT("ModManagerTitleButton"));
/** Noeud cree a cote d'un noeud de reference ; Init renseigne la fonction ou la variable avant la creation des broches. */
template <class T>
T* MakeNode(UEdGraph* Graph, const UEdGraphNode* Anchor, int32 Column, int32 Row, TFunctionRef<void(T*)> Init)
{
FGraphNodeCreator<T> Creator(*Graph);
T* Node = Creator.CreateNode(false);
Init(Node);
Node->NodePosX = Anchor->NodePosX + 300 * Column;
Node->NodePosY = Anchor->NodePosY + 400 + 160 * Row;
Creator.Finalize();
return Node;
}
struct FEditor
{
const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
int32 Errors = 0;
void Link(UEdGraphPin* A, UEdGraphPin* B, const TCHAR* What)
{
if (!A || !B || !Schema->TryCreateConnection(A, B))
{
UE_LOG(LogLPTitleMods, Error, TEXT(" liaison impossible : %s"), What);
++Errors;
}
}
void Default(UEdGraphPin* Pin, const TCHAR* Value, const TCHAR* What)
{
if (!Pin)
{
UE_LOG(LogLPTitleMods, Error, TEXT(" broche introuvable : %s"), What);
++Errors;
return;
}
Schema->TrySetDefaultValue(*Pin, Value);
if (Pin->DefaultValue != Value)
{
UE_LOG(LogLPTitleMods, Error, TEXT(" %s : valeur relue '%s', attendue '%s'"), What, *Pin->DefaultValue, Value);
++Errors;
}
}
UFunction* Function(UClass* Class, const TCHAR* Name)
{
UFunction* F = Class ? Class->FindFunctionByName(Name) : nullptr;
if (!F)
{
UE_LOG(LogLPTitleMods, Error, TEXT(" fonction introuvable : %s::%s"), Class ? *Class->GetName() : TEXT("?"), Name);
++Errors;
}
return F;
}
UK2Node_CallFunction* Call(UEdGraph* Graph, const UEdGraphNode* Anchor, int32 Column, int32 Row, UFunction* F)
{
return MakeNode<UK2Node_CallFunction>(Graph, Anchor, Column, Row, [F](UK2Node_CallFunction* N) { if (F) { N->SetFromFunction(F); } });
}
UK2Node_VariableGet* GetSelf(UEdGraph* Graph, const UEdGraphNode* Anchor, int32 Column, int32 Row, FName Name)
{
return MakeNode<UK2Node_VariableGet>(Graph, Anchor, Column, Row, [Name](UK2Node_VariableGet* N) { N->VariableReference.SetSelfMember(Name); });
}
/** Evenement de clic d'un element, et le noeud qu'il execute aujourd'hui. */
UK2Node_ComponentBoundEvent* BoundEvent(UBlueprint* BP, FName Component, const TCHAR* FirstCall, UEdGraphNode*& OutFirst)
{
TArray<UK2Node_ComponentBoundEvent*> Events;
FBlueprintEditorUtils::GetAllNodesOfClass(BP, Events);
for (UK2Node_ComponentBoundEvent* Event : Events)
{
UEdGraphPin* Then = Event->FindPin(UEdGraphSchema_K2::PN_Then);
if (Event->ComponentPropertyName != Component || !Then || Then->LinkedTo.Num() != 1)
{
continue;
}
const UK2Node_CallFunction* Call = Cast<UK2Node_CallFunction>(Then->LinkedTo[0]->GetOwningNode());
if (Call && Call->FunctionReference.GetMemberName() == FirstCall)
{
OutFirst = Then->LinkedTo[0]->GetOwningNode();
return Event;
}
}
UE_LOG(LogLPTitleMods, Error, TEXT(" %s : evenement de %s suivi de %s introuvable"), *BP->GetName(), *Component.ToString(), FirstCall);
++Errors;
return nullptr;
}
};
bool Save(UBlueprint* BP)
{
UPackage* Package = BP->GetOutermost();
Package->MarkPackageDirty();
const FString Filename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
SaveArgs.Error = GWarn;
const bool bOk = UPackage::SavePackage(Package, nullptr, *Filename, SaveArgs);
UE_LOG(LogLPTitleMods, Warning, TEXT("%s : %s"), bOk ? TEXT("sauvegarde") : TEXT("ECHEC de sauvegarde"), *Filename);
return bOk;
}
}
ULPTitleModManagerCommandlet::ULPTitleModManagerCommandlet()
{
IsClient = false;
IsEditor = true;
IsServer = false;
LogToConsole = true;
}
int32 ULPTitleModManagerCommandlet::Main(const FString& Params)
{
TArray<FString> Tokens, Switches;
TMap<FString, FString> P;
ParseCommandLine(*Params, Tokens, Switches, P);
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
UWidgetBlueprint* MainMenu = LoadObject<UWidgetBlueprint>(nullptr, TEXT("/Game/UI/MainMenu.MainMenu"));
UWidgetBlueprint* ModManager = LoadObject<UWidgetBlueprint>(nullptr, TEXT("/Game/UI/ModManager.ModManager"));
UClass* GIClass = LoadObject<UClass>(nullptr, TEXT("/Game/BPs/GI.GI_C"));
if (!MainMenu || !ModManager || !GIClass)
{
UE_LOG(LogLPTitleMods, Error, TEXT("MainMenu, ModManager ou GI introuvable"));
return 1;
}
const bool bModManagerDone = FBlueprintEditorUtils::FindNewVariableIndex(ModManager, TitleMenuName) != INDEX_NONE;
const bool bMainMenuDone = MainMenu->WidgetTree->FindWidget(TitleButtonName) != nullptr;
if (bModManagerDone && bMainMenuDone)
{
UE_LOG(LogLPTitleMods, Warning, TEXT("Deja applique (ModManager.TitleMenu et MainMenu.%s existent) : rien a faire"), *TitleButtonName.ToString());
return 0;
}
FEditor E;
// GI.ModManager doit etre type ModManager_C (cible de TitleMenu).
const FObjectProperty* GIModManager = CastField<FObjectProperty>(GIClass->FindPropertyByName(TEXT("ModManager")));
if (!GIModManager || !ModManager->GeneratedClass || !GIModManager->PropertyClass->IsChildOf(ModManager->GeneratedClass))
{
UE_LOG(LogLPTitleMods, Error, TEXT("GI.ModManager absent ou de type %s (ModManager_C attendu)"), GIModManager ? *GIModManager->PropertyClass->GetName() : TEXT("?"));
return 1;
}
// 1. ModManager.TitleMenu (reference au menu titre qui l'a ouvert), compile pour que MainMenu puisse l'affecter.
if (!bModManagerDone)
{
FEdGraphPinType MenuType;
MenuType.PinCategory = UEdGraphSchema_K2::PC_Object;
MenuType.PinSubCategoryObject = MainMenu->GeneratedClass;
if (!FBlueprintEditorUtils::AddMemberVariable(ModManager, TitleMenuName, MenuType))
{
UE_LOG(LogLPTitleMods, Error, TEXT("Ajout de ModManager.TitleMenu impossible"));
return 1;
}
FKismetEditorUtilities::CompileBlueprint(ModManager);
UE_LOG(LogLPTitleMods, Warning, TEXT("ModManager.TitleMenu (%s) ajoutee"), *MainMenu->GeneratedClass->GetName());
}
// 2. MainMenu : entree "Mod Manager" de l'ecran titre, copie de LoadButton (meme taille, meme style) placee juste
// apres lui, visible seulement sur l'ecran titre (liaison OnlyVisibleForTitleScreen, comme NewGameButton...).
// L'entree d'origine ModManagerButton (sous-entree du menu en partie) n'est pas modifiee.
if (!bMainMenuDone)
{
UWidgetTree* Tree = MainMenu->WidgetTree;
UWidget* Load = Tree->FindWidget(TEXT("LoadButton"));
UPanelWidget* Options = Load ? Load->GetParent() : nullptr;
const FDelegateEditorBinding* TitleOnly = MainMenu->Bindings.FindByPredicate([](const FDelegateEditorBinding& B) { return B.PropertyName == TEXT("Visibility") && B.FunctionName == TEXT("OnlyVisibleForTitleScreen"); });
const UTexture2D* ModIcon = LoadObject<UTexture2D>(nullptr, TEXT("/Game/Heavy/New/mod.mod"));
if (!Options || !TitleOnly || !ModIcon)
{
UE_LOG(LogLPTitleMods, Error, TEXT("LoadButton, sa boite, une liaison OnlyVisibleForTitleScreen ou l'icone mod introuvable"));
return 1;
}
// Copie du sous-arbre de LoadButton (export/import texte, comme le copier-coller de l'editeur).
TArray<UWidget*> Source;
Source.Add(Load);
UWidgetTree::GetChildWidgets(Load, Source);
FString Text;
FWidgetBlueprintEditorUtils::ExportWidgetsToText(Source, Text);
TSet<UWidget*> Imported;
TMap<FName, UWidgetSlotPair*> SlotData;
FWidgetBlueprintEditorUtils::ImportWidgetsFromText(MainMenu, Text, Imported, SlotData);
UButton* Button = nullptr;
for (UWidget* W : Imported)
{
if (!W->Slot && Cast<UButton>(W))
{
Button = Cast<UButton>(W);
}
}
if (!Button || Imported.Num() != Source.Num())
{
UE_LOG(LogLPTitleMods, Error, TEXT("Copie de LoadButton : %d element(s) importe(s) pour %d"), Imported.Num(), Source.Num());
return 1;
}
Button->Rename(*TitleButtonName.ToString(), Tree);
Button->SetDisplayLabel(TitleButtonName.ToString());
Button->bIsVariable = true;
Button->SetToolTipText(FText::FromString(TEXT("Choose which mods (modules) are enabled, then start or load a game")));
UPanelSlot* Slot = Options->InsertChildAt(Options->GetChildIndex(Load) + 1, Button);
// Proprietes d'emplacement (marges) : celles de LoadButton.
TMap<FName, FString> SlotProps;
FWidgetBlueprintEditorUtils::ExportPropertiesToText(Load->Slot, SlotProps);
FWidgetBlueprintEditorUtils::ImportPropertiesFromText(Slot, SlotProps);
int32 Icons = 0, Texts = 0;
for (UWidget* W : Imported)
{
if (UImage* Image = Cast<UImage>(W))
{
if (Image->GetBrush().GetResourceObject() && Image->GetBrush().GetResourceObject()->GetName() == TEXT("loadgame"))
{
Image->SetBrushResourceObject(const_cast<UTexture2D*>(ModIcon));
++Icons;
}
}
else if (UTextBlock* Label = Cast<UTextBlock>(W))
{
Label->SetText(FText::FromString(TEXT("Mod Manager")));
++Texts;
}
}
if (Icons != 1 || Texts != 1)
{
UE_LOG(LogLPTitleMods, Error, TEXT("Copie de LoadButton : %d icone(s) loadgame et %d texte(s) (1 et 1 attendus)"), Icons, Texts);
return 1;
}
FDelegateEditorBinding Binding = *TitleOnly;
Binding.ObjectName = TitleButtonName.ToString();
MainMenu->Bindings.Add(Binding);
UE_LOG(LogLPTitleMods, Warning, TEXT("MainMenu.%s : copie de LoadButton (%d elements), apres Load, icone mod, visible sur l'ecran titre (%s)"),
*TitleButtonName.ToString(), Imported.Num(), *Binding.FunctionName.ToString());
// La variable du bouton doit exister dans la classe generee pour y lier l'evenement de clic.
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(MainMenu);
FKismetEditorUtilities::CompileBlueprint(MainMenu);
const FObjectProperty* ButtonProp = CastField<FObjectProperty>(MainMenu->GeneratedClass->FindPropertyByName(TitleButtonName));
const FMulticastDelegateProperty* OnClicked = FindFProperty<FMulticastDelegateProperty>(UButton::StaticClass(), TEXT("OnClicked"));
UEdGraph* G = FBlueprintEditorUtils::FindEventGraph(MainMenu);
if (!ButtonProp || !OnClicked || !G)
{
UE_LOG(LogLPTitleMods, Error, TEXT("Variable %s, OnClicked ou graphe d'evenements introuvable"), *TitleButtonName.ToString());
return 1;
}
// Ancre : l'evenement de clic de l'entree d'origine (les nouveaux noeuds sont poses en dessous).
UEdGraphNode* Ignored = nullptr;
const UK2Node_ComponentBoundEvent* Anchor = E.BoundEvent(MainMenu, TEXT("ModManagerButton"), TEXT("CloseSelf"), Ignored);
if (!Anchor)
{
return 1;
}
auto* Click = MakeNode<UK2Node_ComponentBoundEvent>(G, Anchor, 0, 3, [ButtonProp, OnClicked](UK2Node_ComponentBoundEvent* N) { N->InitializeComponentBoundEventParams(ButtonProp, OnClicked); });
auto* Hide = E.Call(G, Anchor, 1, 3, E.Function(UWidget::StaticClass(), TEXT("SetVisibility")));
E.Default(Hide->FindPin(TEXT("InVisibility")), TEXT("Collapsed"), TEXT("SetVisibility.InVisibility"));
auto* GI = E.GetSelf(G, Anchor, 1, 4, TEXT("GI"));
auto* Open = E.Call(G, Anchor, 2, 3, E.Function(GIClass, TEXT("CreateModManagerWidget")));
E.Default(Open->FindPin(TEXT("GameStart")), TEXT("false"), TEXT("CreateModManagerWidget.GameStart"));
auto* GetMM = MakeNode<UK2Node_VariableGet>(G, Anchor, 2, 4, [GIClass](UK2Node_VariableGet* N) { N->VariableReference.SetExternalMember(TEXT("ModManager"), GIClass); });
auto* SetTitle = MakeNode<UK2Node_VariableSet>(G, Anchor, 3, 3, [ModManager](UK2Node_VariableSet* N) { N->VariableReference.SetExternalMember(TitleMenuName, ModManager->GeneratedClass); });
auto* Self = MakeNode<UK2Node_Self>(G, Anchor, 3, 4, [](UK2Node_Self*) {});
E.Link(Click->FindPin(UEdGraphSchema_K2::PN_Then), Hide->GetExecPin(), TEXT("clic -> masquer le menu"));
E.Link(Hide->GetThenPin(), Open->GetExecPin(), TEXT("-> CreateModManagerWidget"));
E.Link(GI->GetValuePin(), Open->FindPin(UEdGraphSchema_K2::PN_Self), TEXT("GI -> CreateModManagerWidget"));
E.Link(Open->GetThenPin(), SetTitle->GetExecPin(), TEXT("-> TitleMenu"));
E.Link(GI->GetValuePin(), GetMM->FindPin(UEdGraphSchema_K2::PN_Self), TEXT("GI -> GI.ModManager"));
E.Link(GetMM->GetValuePin(), SetTitle->FindPin(UEdGraphSchema_K2::PN_Self), TEXT("GI.ModManager -> cible de TitleMenu"));
E.Link(Self->FindPin(UEdGraphSchema_K2::PN_Self), SetTitle->FindPin(TitleMenuName), TEXT("self -> TitleMenu"));
UE_LOG(LogLPTitleMods, Warning, TEXT("MainMenu : clic %s -> menu masque, CreateModManagerWidget, TitleMenu = self (7 noeuds)"), *TitleButtonName.ToString());
}
// 3. ModManager : fermeture.
if (!bModManagerDone)
{
UEdGraphNode* CloseMM = nullptr;
UK2Node_ComponentBoundEvent* Close = nullptr;
TArray<UK2Node_ComponentBoundEvent*> Events;
FBlueprintEditorUtils::GetAllNodesOfClass(ModManager, Events);
for (UK2Node_ComponentBoundEvent* Event : Events)
{
UEdGraphPin* Then = Event->FindPin(UEdGraphSchema_K2::PN_Then);
if (Then && Then->LinkedTo.Num() == 1)
{
const UK2Node_CallFunction* Call = Cast<UK2Node_CallFunction>(Then->LinkedTo[0]->GetOwningNode());
if (Call && Call->FunctionReference.GetMemberName() == TEXT("CloseModManager"))
{
Close = Event;
CloseMM = Then->LinkedTo[0]->GetOwningNode();
}
}
}
if (!Close)
{
UE_LOG(LogLPTitleMods, Error, TEXT("ModManager : evenement suivi de CloseModManager introuvable"));
++E.Errors;
}
else
{
UEdGraph* G = Close->GetGraph();
UEdGraphPin* Then = Close->FindPin(UEdGraphSchema_K2::PN_Then);
Then->BreakAllPinLinks();
auto* Branch = MakeNode<UK2Node_IfThenElse>(G, Close, 1, 0, [](UK2Node_IfThenElse*) {});
auto* Menu = E.GetSelf(G, Close, 0, 1, TitleMenuName);
auto* Valid = E.Call(G, Close, 0, 2, E.Function(UKismetSystemLibrary::StaticClass(), TEXT("IsValid")));
E.Link(Then, Branch->GetExecPin(), TEXT("fermeture -> branche"));
E.Link(Menu->GetValuePin(), Valid->FindPin(TEXT("Object")), TEXT("TitleMenu -> IsValid"));
E.Link(Valid->GetReturnValuePin(), Branch->GetConditionPin(), TEXT("IsValid -> condition"));
E.Link(Branch->GetElsePin(), CloseMM->FindPin(UEdGraphSchema_K2::PN_Execute), TEXT("sinon -> CloseModManager"));
auto* Show = E.Call(G, Close, 2, 0, E.Function(UWidget::StaticClass(), TEXT("SetVisibility")));
E.Default(Show->FindPin(TEXT("InVisibility")), TEXT("Visible"), TEXT("SetVisibility.InVisibility"));
auto* PC = E.Call(G, Close, 2, 1, E.Function(UGameplayStatics::StaticClass(), TEXT("GetPlayerController")));
auto* Focus = E.Call(G, Close, 3, 0, E.Function(UWidgetBlueprintLibrary::StaticClass(), TEXT("SetInputMode_UIOnlyEx")));
auto* GI = E.GetSelf(G, Close, 3, 1, TEXT("GI"));
auto* Clear = E.Call(G, Close, 4, 0, E.Function(GIClass, TEXT("ClearModManagerWidget")));
auto* Populate = E.Call(G, Close, 5, 0, E.Function(GIClass, TEXT("PopulateRaws")));
auto* SaveSelection = E.Call(G, Close, 6, 0, E.Function(ULPModSettingsLibrary::StaticClass(), TEXT("SaveModSelection")));
E.Link(Branch->GetThenPin(), Show->GetExecPin(), TEXT("titre -> reafficher le menu"));
E.Link(Menu->GetValuePin(), Show->FindPin(UEdGraphSchema_K2::PN_Self), TEXT("TitleMenu -> SetVisibility"));
E.Link(Show->GetThenPin(), Focus->GetExecPin(), TEXT("-> SetInputMode_UIOnlyEx"));
E.Link(PC->GetReturnValuePin(), Focus->FindPin(TEXT("PlayerController")), TEXT("GetPlayerController -> PlayerController"));
E.Link(Menu->GetValuePin(), Focus->FindPin(TEXT("InWidgetToFocus")), TEXT("TitleMenu -> InWidgetToFocus"));
E.Link(Focus->GetThenPin(), Clear->GetExecPin(), TEXT("-> ClearModManagerWidget"));
E.Link(Clear->GetThenPin(), Populate->GetExecPin(), TEXT("-> PopulateRaws"));
E.Link(Populate->GetThenPin(), SaveSelection->GetExecPin(), TEXT("-> SaveModSelection"));
E.Link(GI->GetValuePin(), SaveSelection->FindPin(TEXT("GameInstance")), TEXT("GI -> SaveModSelection"));
for (UK2Node_CallFunction* N : { Clear, Populate })
{
E.Link(GI->GetValuePin(), N->FindPin(UEdGraphSchema_K2::PN_Self), TEXT("GI -> appel"));
}
UE_LOG(LogLPTitleMods, Warning, TEXT("ModManager : fermeture -> branche TitleMenu valide (%d noeuds ajoutes)"), 10);
}
}
if (E.Errors > 0)
{
UE_LOG(LogLPTitleMods, Error, TEXT("%d erreur(s) : rien n'est sauvegarde"), E.Errors);
return 1;
}
FKismetEditorUtilities::CompileBlueprint(ModManager);
FKismetEditorUtilities::CompileBlueprint(MainMenu);
if (ModManager->Status == BS_Error || MainMenu->Status == BS_Error)
{
UE_LOG(LogLPTitleMods, Error, TEXT("Compilation en erreur (ModManager %d, MainMenu %d) : rien n'est sauvegarde"), int32(ModManager->Status), int32(MainMenu->Status));
return 1;
}
UE_LOG(LogLPTitleMods, Warning, TEXT("ModManager et MainMenu compiles sans erreur (etat ModManager %d, MainMenu %d)"), int32(ModManager->Status), int32(MainMenu->Status));
if (bDryRun)
{
UE_LOG(LogLPTitleMods, Warning, TEXT("Essai a blanc : rien n'est sauvegarde"));
return 0;
}
bool bOk = true;
if (!bModManagerDone)
{
bOk &= Save(ModManager);
}
if (!bMainMenuDone)
{
bOk &= Save(MainMenu);
}
return bOk ? 0 : 1;
}

View file

@ -0,0 +1,37 @@
// Portage UE 5.5 - lot UI15 (gestionnaire de mods depuis l'ecran titre, Docs/Migration/17_Mods_Ecran_Titre.md).
//
// Le gestionnaire de mods (UI/ModManager) n'etait accessible qu'en partie (entree "Mod Manager" du menu, masquee sur
// l'ecran titre par la liaison ColapsedForTitleScreen). Cette commandlet ajoute le circuit "ecran titre" :
//
// MainMenu : nouvelle entree ModManagerTitleButton, copie de LoadButton (meme taille et style) placee apres lui,
// visible seulement sur l'ecran titre (liaison OnlyVisibleForTitleScreen) ; clic : menu masque,
// GI.CreateModManagerWidget(GameStart=false), GI.ModManager.TitleMenu = self. L'entree d'origine ModManagerButton
// (sous-entree du menu en partie, masquee sur l'ecran titre) n'est pas modifiee.
// ModManager, fermeture :
// TitleMenu valide ? -> menu titre reaffiche et focalise, GI.ClearModManagerWidget, GI.PopulateRaws (donnees
// rechargees avec les modules choisis), SaveModSelection (choix ecrit dans GameSettings.sav ;
// GI.SaveSettings n'est pas utilise : il ferait de son Slot la "derniere sauvegarde")
// : circuit d'origine (GI.CloseModManager)
//
// Le joueur revient au menu titre et choisit lui-meme : derniere sauvegarde, chargement ou nouvelle partie.
// Le Blueprint GI (circuit en partie) n'est pas modifie. Toute broche ou noeud introuvable, ou erreur de compilation :
// rien n'est sauvegarde. Chaque Blueprint deja traite (ModManager.TitleMenu, MainMenu.ModManagerTitleButton) est saute.
//
// Usage :
// UnrealEditor-Cmd.exe <projet> -run=LPTitleModManager [-DryRun]
#pragma once
#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "LPTitleModManagerCommandlet.generated.h"
UCLASS()
class ULPTitleModManagerCommandlet : public UCommandlet
{
GENERATED_BODY()
public:
ULPTitleModManagerCommandlet();
virtual int32 Main(const FString& Params) override;
};

View file

@ -81,7 +81,8 @@ else:
eal.save_loaded_asset(mesh, only_if_is_dirty=False)
registry = unreal.AssetRegistryHelpers.get_asset_registry()
registry.scan_modified_asset_files([unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_content_dir()) + mesh_path[len("/Game/"):] + ".uasset"])
# Chemin de paquet (et non de fichier) : vaut aussi pour un plugin de mod (/<Mod>/..., H3).
registry.scan_paths_synchronous([folder], force_rescan=True)
options = unreal.AssetRegistryDependencyOptions(include_soft_package_references=True, include_hard_package_references=True)
deps = [str(d) for d in registry.get_dependencies(mesh_path, options) if not str(d).startswith("/Script/")]
expected = {mesh_path + "_Skeleton"} | used

View file

@ -56,6 +56,18 @@ function Invoke-UE([string[]] $Arguments, [string] $Filter) {
$out | Where-Object { $_ -match $Filter -and $_ -notmatch 'LogInit: Display' }
}
# Chemin disque d'un chemin de paquet : /Game/X -> Content/X ; /<Plugin>/X -> Mods/<Plugin>/Content/X (mod, H3)
# ou Plugins/<Plugin>/Content/X.
function Get-DiskPath([string] $PackagePath) {
$parts = $PackagePath.TrimStart('/').Split('/', 2)
if ($parts[0] -eq 'Game') { return Join-Path $Root ('Content\' + ($parts[1] -replace '/', '\')) }
foreach ($base in 'Mods', 'Plugins') {
$pluginDir = Join-Path $Root "$base\$($parts[0])"
if (Test-Path $pluginDir) { return Join-Path $pluginDir ('Content\' + ($parts[1] -replace '/', '\')) }
}
throw "Dossier de contenu introuvable pour $PackagePath"
}
$SevenZip = 'C:\Program Files\7-Zip\7z.exe'
function Expand-One([string] $Archive, [string] $Dest) {
# 7-Zip lit zip, rar et 7z (Pocolov 03 : source/Hair3.rar) ; Expand-Archive en secours pour les zip.
@ -190,7 +202,7 @@ foreach ($h in $cfg.hairs) {
else {
Write-Host "== $name : application"
if (-not $h.inspected) { $report += "$name : non inspectee, sautee"; continue }
$existing = Join-Path $Root ("Content" + $meshPath.Substring(5) + '.uasset')
$existing = (Get-DiskPath $meshPath) + '.uasset'
if (Test-Path $existing) {
if (-not $Rebuild) { $report += "$name : $meshPath existe deja, sautee (-Rebuild pour la refaire)"; continue }
# -Rebuild : coiffure et squelette precedents deplaces vers la sauvegarde (materiaux et textures mis a jour sur place).
@ -215,7 +227,7 @@ foreach ($h in $cfg.hairs) {
$mat | Write-Host
$depOk = [bool]($mat -match 'dependances.*-> OK')
# Nettoyage : dossier de travail deplace vers la sauvegarde.
$workDir = Join-Path $Root ("Content" + $work.Substring(5))
$workDir = Get-DiskPath $work
if (Test-Path $workDir) { Move-Item $workDir (Join-Path $dir 'work') -Force }
# Declaration et credits.
$lphairDir = Join-Path $Root "Content\Modules\$($cfg.module)\Hairstyles"
@ -227,7 +239,9 @@ foreach ($h in $cfg.hairs) {
'// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.',
"HAIRSTYLE: $meshPath.$name", "SEX: $($h.sex)", 'DAZ: true')
[IO.File]::WriteAllText((Join-Path $lphairDir "$($name.ToLower()).lphair"), ($lines -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding $false))
$credits = Join-Path $Root 'Licenses\Free_3D_Models_Credits.txt'
# Credits : fichier du jeu par defaut ; un mod (H3) declare le sien ("credits", relatif a la racine).
$credits = Join-Path $Root $(if ($cfg.credits) { $cfg.credits } else { 'Licenses\Free_3D_Models_Credits.txt' })
if (-not (Test-Path $credits)) { New-Item -ItemType File -Force $credits | Out-Null }
if (-not (Select-String -LiteralPath $credits -SimpleMatch $h.url -Quiet)) {
Add-Content -LiteralPath $credits -Value ("- $credit . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials ($meshPath*).") -Encoding utf8
}

View file

@ -0,0 +1,32 @@
{
"module": "LPHairSample",
"folder": "/LPHairSample/Hairs",
"credits": "ModSources/LPHairSample/Module/CREDITS.txt",
"backup": "D:/Game/LifePlay_Backup_5.5/H3a",
"hairs": [
{
"name": "Pocolov05",
"archive": "D:/Game/pocolov-hair-05.zip",
"title": "Pocolov Hair 05",
"url": "https://www.fab.com/listings/4025749f-d597-4332-91aa-16e355b94974",
"mesh": "/LPHairSample/Hairs/_Work_Pocolov05/Box001",
"head": "/LPHairSample/Hairs/_Work_Pocolov05/Head",
"cap_slots": "Hair_Base",
"diffuse": "D:/Game/LifePlay_Backup_5.5/H3a/Pocolov05/norm/tex01_Hair_01_Gray.png",
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/H3a/Pocolov05/norm/tex02_Hair_01A1.jpg",
"cap_opacity": "white",
"thicken": 1,
"sex": "F",
"license": "CC BY 4.0",
"author": "Pocolov (Pocolov Studio, artist Thang)",
"auto_fields": [
"mesh",
"head",
"cap_slots",
"diffuse",
"strands_opacity"
],
"inspected": true
}
]
}