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:
parent
6f87c8b920
commit
3c2da7a516
22 changed files with 767 additions and 6 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
50
Docs/Migration/17_Mods_Ecran_Titre.md
Normal file
50
Docs/Migration/17_Mods_Ecran_Titre.md
Normal 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.
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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`) |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
12
ModSources/LPHairSample/LPHairSample.uplugin
Normal file
12
ModSources/LPHairSample/LPHairSample.uplugin
Normal 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
|
||||
}
|
||||
1
ModSources/LPHairSample/Module/CREDITS.txt
Normal file
1
ModSources/LPHairSample/Module/CREDITS.txt
Normal 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*).
|
||||
|
|
@ -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
|
||||
6
ModSources/LPHairSample/Module/LPHairSample.lpmod
Normal file
6
ModSources/LPHairSample/Module/LPHairSample.lpmod
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
36
Source/LifePlay/LPModSettingsLibrary.cpp
Normal file
36
Source/LifePlay/LPModSettingsLibrary.cpp
Normal 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;
|
||||
}
|
||||
25
Source/LifePlay/LPModSettingsLibrary.h
Normal file
25
Source/LifePlay/LPModSettingsLibrary.h
Normal 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);
|
||||
};
|
||||
403
Source/LifePlayEditorTools/LPTitleModManagerCommandlet.cpp
Normal file
403
Source/LifePlayEditorTools/LPTitleModManagerCommandlet.cpp
Normal 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;
|
||||
}
|
||||
37
Source/LifePlayEditorTools/LPTitleModManagerCommandlet.h
Normal file
37
Source/LifePlayEditorTools/LPTitleModManagerCommandlet.h
Normal 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;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
32
Tools/mods/LPHairSample.json
Normal file
32
Tools/mods/LPHairSample.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue