diff --git a/.gitignore b/.gitignore index ed97967..8752552 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/Content/UI/MainMenu.uasset b/Content/UI/MainMenu.uasset index 7dcd5ae..249090b 100644 Binary files a/Content/UI/MainMenu.uasset and b/Content/UI/MainMenu.uasset differ diff --git a/Content/UI/ModManager.uasset b/Content/UI/ModManager.uasset index 7612615..f1025a9 100644 Binary files a/Content/UI/ModManager.uasset and b/Content/UI/ModManager.uasset differ diff --git a/Docs/Migration/09_Commandes_Utiles.md b/Docs/Migration/09_Commandes_Utiles.md index 163f6d7..643f1ce 100644 --- a/Docs/Migration/09_Commandes_Utiles.md +++ b/Docs/Migration/09_Commandes_Utiles.md @@ -55,6 +55,13 @@ git status; git log --oneline -5; git tag --list '*-valide' # où en est-on & $Cmd $Proj -run=LPHairFromStaticMesh -Static= -FitHead= -Out=/Game/Heavy/New5/Hairs/ -DryRun -unattended -nopause & $Cmd $Proj -run=pythonscript "-script=/Tools/HairMaterials.py mesh=/Game/Heavy/New5/Hairs/ diffuse= strands_opacity= cap_opacity=white cap=" -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="\Mods\\.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 diff --git a/Docs/Migration/16_H3_Mods_Paquets.md b/Docs/Migration/16_H3_Mods_Paquets.md index d8a3a9e..6e16383 100644 --- a/Docs/Migration/16_H3_Mods_Paquets.md +++ b/Docs/Migration/16_H3_Mods_Paquets.md @@ -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 `//` ; 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/` 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= -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 | `/LifePlay/Mods/LPHairSample/` (`.uplugin` + `Content/Paks/Windows/*`) et `/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 '"\Build\BatchFiles\RunUAT.bat" BuildCookRun -project="" -platform=Win64 -clientconfig=Shipping -build -cook -iterate -stage -pak -iostore -compressed -createreleaseversion=2026.10.4 -archive -archivedirectory= -nodebuginfo -unattended -utf8output -nop4' +cmd /c '"\Build\BatchFiles\RunUAT.bat" BuildCookRun -project="" -platform=Win64 -clientconfig=Shipping -cook -stage -pak -iostore -compressed -dlcname="\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 `/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 (`//...` → `Mods//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. diff --git a/Docs/Migration/17_Mods_Ecran_Titre.md b/Docs/Migration/17_Mods_Ecran_Titre.md new file mode 100644 index 0000000..f1fa4aa --- /dev/null +++ b/Docs/Migration/17_Mods_Ecran_Titre.md @@ -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. diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index 1c6e4d8..6a77a3a 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -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 | diff --git a/Docs/Migration/README.md b/Docs/Migration/README.md index 73d86e4..6c8f689 100644 --- a/Docs/Migration/README.md +++ b/Docs/Migration/README.md @@ -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`) | diff --git a/Docs/Migration/Registre_Erreurs.md b/Docs/Migration/Registre_Erreurs.md index 7bb00d9..104dfd9 100644 --- a/Docs/Migration/Registre_Erreurs.md +++ b/Docs/Migration/Registre_Erreurs.md @@ -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 | diff --git a/ModSources/LPHairSample/LPHairSample.uplugin b/ModSources/LPHairSample/LPHairSample.uplugin new file mode 100644 index 0000000..0f345ed --- /dev/null +++ b/ModSources/LPHairSample/LPHairSample.uplugin @@ -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 +} diff --git a/ModSources/LPHairSample/Module/CREDITS.txt b/ModSources/LPHairSample/Module/CREDITS.txt new file mode 100644 index 0000000..d4ab565 --- /dev/null +++ b/ModSources/LPHairSample/Module/CREDITS.txt @@ -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*). diff --git a/ModSources/LPHairSample/Module/Hairstyles/pocolov05.lphair b/ModSources/LPHairSample/Module/Hairstyles/pocolov05.lphair new file mode 100644 index 0000000..5bda587 --- /dev/null +++ b/ModSources/LPHairSample/Module/Hairstyles/pocolov05.lphair @@ -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 diff --git a/ModSources/LPHairSample/Module/LPHairSample.lpmod b/ModSources/LPHairSample/Module/LPHairSample.lpmod new file mode 100644 index 0000000..4092b48 --- /dev/null +++ b/ModSources/LPHairSample/Module/LPHairSample.lpmod @@ -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 diff --git a/Source/LifePlay/LPHairstyleSubsystem.cpp b/Source/LifePlay/LPHairstyleSubsystem.cpp index 61cbd1d..71a138a 100644 --- a/Source/LifePlay/LPHairstyleSubsystem.cpp +++ b/Source/LifePlay/LPHairstyleSubsystem.cpp @@ -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//.lpmod avec MODULE_DEFAULT_ENABLED: true. + TArray 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 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 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(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() diff --git a/Source/LifePlay/LPHairstyleSubsystem.h b/Source/LifePlay/LPHairstyleSubsystem.h index 74eb34e..7faaf37 100644 --- a/Source/LifePlay/LPHairstyleSubsystem.h +++ b/Source/LifePlay/LPHairstyleSubsystem.h @@ -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 DefaultEnabledModules; /** Coiffures et morphs du fichier de base, dans l'ordre. */ FString BaseFile; TArray BaseDefs; diff --git a/Source/LifePlay/LPModSettingsLibrary.cpp b/Source/LifePlay/LPModSettingsLibrary.cpp new file mode 100644 index 0000000..51d0ff9 --- /dev/null +++ b/Source/LifePlay/LPModSettingsLibrary.cpp @@ -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(GameInstance->GetClass()->FindPropertyByName(ModEnabledsName)); + const FArrayProperty* To = Settings ? CastField(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(Settings), From->ContainerPtrToValuePtr(GameInstance)); + const bool bSaved = UGameplayStatics::SaveGameToSlot(Settings, Slot, 0); + FScriptArrayHelper Helper(To, To->ContainerPtrToValuePtr(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; +} diff --git a/Source/LifePlay/LPModSettingsLibrary.h b/Source/LifePlay/LPModSettingsLibrary.h new file mode 100644 index 0000000..173c74e --- /dev/null +++ b/Source/LifePlay/LPModSettingsLibrary.h @@ -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); +}; diff --git a/Source/LifePlayEditorTools/LPTitleModManagerCommandlet.cpp b/Source/LifePlayEditorTools/LPTitleModManagerCommandlet.cpp new file mode 100644 index 0000000..ef1e3a9 --- /dev/null +++ b/Source/LifePlayEditorTools/LPTitleModManagerCommandlet.cpp @@ -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 + T* MakeNode(UEdGraph* Graph, const UEdGraphNode* Anchor, int32 Column, int32 Row, TFunctionRef Init) + { + FGraphNodeCreator 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(); + 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(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(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 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(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 Tokens, Switches; + TMap P; + ParseCommandLine(*Params, Tokens, Switches, P); + const bool bDryRun = Switches.Contains(TEXT("DryRun")); + + UWidgetBlueprint* MainMenu = LoadObject(nullptr, TEXT("/Game/UI/MainMenu.MainMenu")); + UWidgetBlueprint* ModManager = LoadObject(nullptr, TEXT("/Game/UI/ModManager.ModManager")); + UClass* GIClass = LoadObject(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(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(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 Source; + Source.Add(Load); + UWidgetTree::GetChildWidgets(Load, Source); + FString Text; + FWidgetBlueprintEditorUtils::ExportWidgetsToText(Source, Text); + TSet Imported; + TMap SlotData; + FWidgetBlueprintEditorUtils::ImportWidgetsFromText(MainMenu, Text, Imported, SlotData); + UButton* Button = nullptr; + for (UWidget* W : Imported) + { + if (!W->Slot && Cast(W)) + { + Button = Cast(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 SlotProps; + FWidgetBlueprintEditorUtils::ExportPropertiesToText(Load->Slot, SlotProps); + FWidgetBlueprintEditorUtils::ImportPropertiesFromText(Slot, SlotProps); + int32 Icons = 0, Texts = 0; + for (UWidget* W : Imported) + { + if (UImage* Image = Cast(W)) + { + if (Image->GetBrush().GetResourceObject() && Image->GetBrush().GetResourceObject()->GetName() == TEXT("loadgame")) + { + Image->SetBrushResourceObject(const_cast(ModIcon)); + ++Icons; + } + } + else if (UTextBlock* Label = Cast(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(MainMenu->GeneratedClass->FindPropertyByName(TitleButtonName)); + const FMulticastDelegateProperty* OnClicked = FindFProperty(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(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(G, Anchor, 2, 4, [GIClass](UK2Node_VariableGet* N) { N->VariableReference.SetExternalMember(TEXT("ModManager"), GIClass); }); + auto* SetTitle = MakeNode(G, Anchor, 3, 3, [ModManager](UK2Node_VariableSet* N) { N->VariableReference.SetExternalMember(TitleMenuName, ModManager->GeneratedClass); }); + auto* Self = MakeNode(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 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(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(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; +} diff --git a/Source/LifePlayEditorTools/LPTitleModManagerCommandlet.h b/Source/LifePlayEditorTools/LPTitleModManagerCommandlet.h new file mode 100644 index 0000000..cae0e4a --- /dev/null +++ b/Source/LifePlayEditorTools/LPTitleModManagerCommandlet.h @@ -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 -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; +}; diff --git a/Tools/HairMaterials.py b/Tools/HairMaterials.py index 6cc248d..2e3e125 100644 --- a/Tools/HairMaterials.py +++ b/Tools/HairMaterials.py @@ -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 (//..., 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 diff --git a/Tools/HairPipeline.ps1 b/Tools/HairPipeline.ps1 index 6920578..f37d98e 100644 --- a/Tools/HairPipeline.ps1 +++ b/Tools/HairPipeline.ps1 @@ -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 ; //X -> Mods//Content/X (mod, H3) +# ou Plugins//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 } diff --git a/Tools/mods/LPHairSample.json b/Tools/mods/LPHairSample.json new file mode 100644 index 0000000..9607e1c --- /dev/null +++ b/Tools/mods/LPHairSample.json @@ -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 + } + ] +}