H3b : outillage des mods d'assets (BuildMod, ModSources, pipeline en mode mod)

- Tools/BuildMod.ps1 : controles, cuisson DLC contre la version de reference, controle du
  conteneur, archive installable testee, installation optionnelle dans un jeu de test.
- Tools/ModSource.ps1 : sources dans ModSources/<Nom>, montees dans Mods/ le temps d'un
  travail ; le build du jeu de base ne contient rien des mods.
- HairPipeline en mode mod ; HairMaterials : analyse du dossier (plantage dans un plugin),
  reutilisation des textures ; sous-systeme : repli DoesPackageExist.

Valide en jeu le 2026-10-03 (CHG-104).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-10-03 09:06:48 +02:00
parent 3c2da7a516
commit a680f0bf19
10 changed files with 313 additions and 38 deletions

View file

@ -58,7 +58,9 @@ git status; git log --oneline -5; git tag --list '*-valide' # où en est-on
# 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)
# Mod d'assets (H3, 16_H3_Mods_Paquets.md §5-6) : sources dans ModSources\<Nom>, cuisson et archive en une commande
.\Tools\BuildMod.ps1 -Name <Nom> [-Install <jeu>\Windows]
# Détail (version de référence puis cuisson DLC, toujours via cmd /c ou un .cmd : la version serait coupée)
# 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 ...'

View file

@ -106,3 +106,47 @@ Enseignements :
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.
## 6. Outillage H3b (2026-10-03)
Validé en jeu le 2026-10-03 (build `LifePlay_Build_5.5_H3b`, mod installé par l'outil).
Organisation d'un mod d'assets :
```
ModSources/<Nom>/<Nom>.uplugin plugin de contenu (versionné)
ModSources/<Nom>/Content/ assets du mod (non versionnés, comme Content/Heavy)
ModSources/<Nom>/Module/ module LifePlay en texte : <Nom>.lpmod (MODULE_UNIQUEID = <Nom>),
Hairstyles/*.lphair, CREDITS.txt (versionnés)
```
`<projet>/Mods/` reste vide : `Tools/ModSource.ps1` y déplace le mod le temps d'un travail (import, cuisson) puis le
remet dans `ModSources/` (déplacements seulement). Le build du jeu de base ne contient donc rien des mods sans
manipulation (vérifié : aucun asset `/LPHairSample/`, ni `Mods/`, ni module dans le build H3b).
Fabrication :
```powershell
.\Tools\HairPipeline.ps1 -Config Tools\mods\<Nom>.json -Step Inspect # "mod": "<Nom>", "folder": "/<Nom>/Hairs"
.\Tools\HairPipeline.ps1 -Config Tools\mods\<Nom>.json -Step Apply
.\Tools\BuildMod.ps1 -Name <Nom> [-Version 2026.10.4] [-Out D:\Game\LifePlay_Publication\Mods] [-Install <jeu>\Windows]
```
`BuildMod.ps1` : contrôles (plugin, module, version de référence `Releases/<version>`, aucun autre plugin dans `Mods/`),
cuisson DLC par un `.cmd` écrit à côté du journal (`Saved/Logs/portage/mods/`), contrôle du conteneur (toutes les
entrées sous `Mods/<Nom>/`), assemblage `<Nom>-for-LifePlay-<version>` (`LifePlay/Mods/<Nom>`,
`LifePlay/Content/Modules/<Nom>`, `README.txt` bilingue), archive `.zip` testée ; un assemblage précédent est déplacé
(`precedent_<date>`). Mod d'essai : `LPHairSample-for-LifePlay-2026.10.4.zip`, 334 Ko, 8 fichiers.
Corrections du lot :
- `Tools/HairMaterials.py` : dans un plugin, le registre d'assets de la commandlet ne connaît pas encore les assets
existants (`does_asset_exist` faux) ; la création d'un matériau existant ouvrait une boîte de dialogue et plantait
(`Assertion failed: IsValid()`). Analyse synchrone du dossier au départ ; texture déjà importée depuis la même
source réutilisée (autre source dans un plugin : refus, `-Rebuild`).
- `HairPipeline.ps1 -Rebuild` d'un mod déplace aussi les textures vers la sauvegarde ; une reconstruction demande une
nouvelle inspection (les maillages de travail sont rangés dans la sauvegarde après chaque application).
- `LPHairstyleSubsystem` : coiffure de mod absente du registre d'assets acceptée si son paquet existe
(`FPackageName::DoesPackageExist`, avertissement).
- `BuildMod.ps1` : `cmd /c` avec guillemets imbriqués échoue depuis PowerShell (« syntaxe du nom de fichier
incorrecte ») ; d'où le fichier `.cmd`.

View file

@ -948,3 +948,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
| 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 |
| CHG-104 | 2026-10-03 | H3b | nouveaux `Tools/BuildMod.ps1`, `Tools/ModSource.ps1` ; `Tools/HairPipeline.ps1` (mode `mod` : montage temporaire, `.lphair` et crédits dans `ModSources/<Nom>/Module`, `-Rebuild` déplace aussi les textures) ; `Tools/HairMaterials.py` (analyse synchrone du dossier, réutilisation des textures) ; `Tools/mods/LPHairSample.json` (`mod`) ; `Source/LifePlay/LPHairstyleSubsystem.cpp` (repli `DoesPackageExist`) ; Pocolov 05 reconstruite (précédente dans `LifePlay_Backup_5.5\H3a\Pocolov05\precedente_*`) ; build `LifePlay_Build_5.5_H3b` ; archive `D:\Game\LifePlay_Publication\Mods\LPHairSample-for-LifePlay-2026.10.4.zip` (334 473 octets, SHA-256 `8F6A45ED…CBC8`) | lot H3b : un mod d'assets en une commande ; plantage de `HairMaterials.py` dans un plugin expliqué (registre de la commandlet sans les assets du plugin) et corrigé ; build de base sans rien du mod par construction. **Validé en jeu le 2026-10-03** (16_H3_Mods_Paquets.md §6) | `git revert` du commit, recompiler |

View file

@ -26,7 +26,7 @@ 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, 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é) |
| [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, §6 outillage H3b (`ModSources/`, `Tools/BuildMod.ps1`), validés |
| [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 |

View file

@ -12,6 +12,7 @@
#include "LifePlay.h"
#include "Misc/CommandLine.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
#include "UObject/UnrealType.h"
@ -496,7 +497,13 @@ void ULPHairstyleSubsystem::Sync()
continue;
}
const FAssetData Asset = Registry.GetAssetByObjectPath(FSoftObjectPath(Def.Path));
if (!Asset.IsValid() || !Asset.IsInstanceOf(USkeletalMesh::StaticClass()))
// Mod (H3) dont le registre d'assets manquerait : le paquet monte suffit (classe non verifiable sans le charger).
const bool bPackageOnly = !Asset.IsValid() && FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(Def.Path));
if (bPackageOnly)
{
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s absent du registre d'assets mais paquet present, ajoute (%s)"), *Def.Path, *Def.File);
}
else if (!Asset.IsValid() || !Asset.IsInstanceOf(USkeletalMesh::StaticClass()))
{
UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s introuvable ou n'est pas un SkeletalMesh (%s), ignore"), *Def.Path, *Def.File);
++Skipped;

132
Tools/BuildMod.ps1 Normal file
View file

@ -0,0 +1,132 @@
<#
.SYNOPSIS
Cuit un mod d'assets et l'assemble en archive installable (lot H3b, Docs/Migration/16_H3_Mods_Paquets.md).
.DESCRIPTION
Sources : ModSources\<Nom>\ (<Nom>.uplugin, Content\, Module\ ; voir Tools/ModSource.ps1).
1. controles : plugin et module (MODULE_UNIQUEID = <Nom>), version de reference Releases\<Version>\Windows, aucun
autre plugin dans Mods\ ;
2. montage du mod dans Mods\<Nom>, cuisson DLC contre la version de reference (BuildCookRun -dlcname
-basedonreleaseversion, via cmd /c : appelee directement depuis PowerShell, la version est coupee en deux),
journal dans Saved\Logs\portage\mods ; remise en place dans ModSources\<Nom> dans tous les cas ;
3. controle du conteneur : chaque paquet est sous Mods/<Nom>/ (aucun asset du jeu embarque) ;
4. assemblage <Out>\<Nom>-for-LifePlay-<Version>\LifePlay\{Mods\<Nom>, Content\Modules\<Nom>} + notice bilingue,
archive .zip testee (7-Zip) ; un assemblage precedent du meme nom est deplace (precedent_<date>), jamais supprime ;
5. -Install <dossier du jeu> : copie le mod dans un jeu de test (dossier contenant LifePlay.exe).
.EXAMPLE
.\Tools\BuildMod.ps1 -Name LPHairSample
.\Tools\BuildMod.ps1 -Name LPHairSample -Install D:\Game\LifePlay_Build_5.5_H3b\Windows
#>
param(
[Parameter(Mandatory)] [string] $Name,
[string] $Version = '',
[string] $Out = 'D:\Game\LifePlay_Publication\Mods',
[string] $Install = ''
)
$ErrorActionPreference = 'Stop'
$Root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'ModSource.ps1')
$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine'
$SevenZip = 'C:\Program Files\7-Zip\7z.exe'
# 1. Controles.
if (-not $Version) {
$line = Select-String -Path (Join-Path $Root 'Config\DefaultGame.ini') -Pattern '^ProjectVersion=(.+)$' | Select-Object -First 1
if (-not $line) { throw 'ProjectVersion absent de Config\DefaultGame.ini : preciser -Version' }
$Version = $line.Matches[0].Groups[1].Value.Trim()
}
$src = Join-Path $Root "ModSources\$Name"
$lpmod = Join-Path $src "Module\$Name.lpmod"
if (-not (Test-Path (Join-Path $src "$Name.uplugin"))) { throw "ModSources\$Name\$Name.uplugin introuvable" }
if (-not (Test-Path $lpmod)) { throw "ModSources\$Name\Module\$Name.lpmod introuvable" }
$id = (Select-String -Path $lpmod -Pattern '^MODULE_UNIQUEID:\s*(.+)$' | Select-Object -First 1).Matches[0].Groups[1].Value.Trim()
if ($id -ne $Name) { throw "MODULE_UNIQUEID ($id) different du nom du mod ($Name)" }
$title = (Select-String -Path $lpmod -Pattern '^MODULE_NAME:\s*(.+)$' | Select-Object -First 1).Matches[0].Groups[1].Value.Trim()
$release = Join-Path $Root "Releases\$Version\Windows"
if (-not (Test-Path (Join-Path $release 'AssetRegistry.bin'))) { throw "Version de reference absente : $release (build de publication avec -createreleaseversion=$Version)" }
$stray = Get-StrayMods $Root
if ($stray) { throw "Autres plugins dans Mods\ ($($stray -join ', ')) : les remettre dans ModSources avant de cuire" }
# 2. Cuisson DLC.
$logDir = Join-Path $Root 'Saved\Logs\portage\mods'
New-Item -ItemType Directory -Force $logDir | Out-Null
$log = Join-Path $logDir ("{0}_{1}.log" -f $Name, (Get-Date -Format 'yyyyMMdd_HHmmss'))
$mounted = Mount-ModSource $Root $Name
try {
$uplugin = Join-Path $mounted "$Name.uplugin"
# Commande ecrite dans un .cmd a cote du journal (guillemets intacts, commande exacte conservee).
$batch = [IO.Path]::ChangeExtension($log, '.cmd')
$cmd = "@call `"$UE\Build\BatchFiles\RunUAT.bat`" BuildCookRun -project=`"$Root\LifePlay.uproject`" -platform=Win64 -clientconfig=Shipping -cook -stage -pak -iostore -compressed -dlcname=`"$uplugin`" -basedonreleaseversion=$Version -nodebuginfo -unattended -utf8output -nop4 > `"$log`" 2>&1"
[IO.File]::WriteAllText($batch, $cmd + "`r`n@exit /b %ERRORLEVEL%`r`n", (New-Object Text.UTF8Encoding $false))
Write-Host "Cuisson DLC de $Name contre $Version (journal : $log)"
& cmd /c $batch
$exit = $LASTEXITCODE
$cooked = Select-String -Path $log -Pattern 'Packages Cooked: (\d+)' | Select-Object -Last 1
if ($exit -ne 0 -or -not $cooked) { throw "Cuisson en echec (code $exit) : voir $log" }
Write-Host " $($cooked.Matches[0].Groups[1].Value) paquet(s) cuit(s)"
$paks = Join-Path $mounted "Saved\StagedBuilds\Windows\LifePlay\Mods\$Name\Content\Paks\Windows"
$files = @(Get-ChildItem $paks -File | Where-Object { $_.LastWriteTime -gt (Get-Item $log).CreationTime })
if ($files.Count -lt 3) { throw "Paquets du mod absents ou anciens dans $paks" }
# 3. Contenu du conteneur : rien hors de Mods/<Nom>/.
$utoc = ($files | Where-Object Extension -eq '.utoc' | Select-Object -First 1).FullName
$entries = @(& "$UE\Binaries\Win64\UnrealPak.exe" $utoc -List 2>&1 | ForEach-Object { "$_" } | Where-Object { $_ -match '"\.\./\.\./\.\./' } | ForEach-Object { ($_ -split '"')[1] })
$outside = @($entries | Where-Object { $_ -notlike "../../../LifePlay/Mods/$Name/*" })
if (-not $entries -or $outside) { throw "Conteneur : $($entries.Count) entree(s), $($outside.Count) hors de Mods/$Name ($($outside | Select-Object -First 3))" }
Write-Host " conteneur : $($entries.Count) fichier(s), tous sous Mods/$Name/"
# 4. Assemblage.
New-Item -ItemType Directory -Force $Out | Out-Null
$base = "$Name-for-LifePlay-$Version"
$stage = Join-Path $Out $base
$zip = "$stage.zip"
$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
if (Test-Path $stage) { Move-Item $stage "$stage.precedent_$stamp" }
if (Test-Path $zip) { Move-Item $zip "$stage.precedent_$stamp.zip" }
$modDir = Join-Path $stage "LifePlay\Mods\$Name"
New-Item -ItemType Directory -Force (Join-Path $modDir 'Content\Paks\Windows') | Out-Null
Copy-Item $uplugin $modDir
$files | Copy-Item -Destination (Join-Path $modDir 'Content\Paks\Windows')
Copy-Item (Join-Path $mounted 'Module') (Join-Path $stage "LifePlay\Content\Modules\$Name") -Recurse
$readme = @(
"$title ($Name) - built for LifePlay $Version",
'',
'INSTALL: extract this archive into the game folder (the one containing LifePlay.exe), merging the LifePlay folder.',
"Then open `"Mod Manager`" on the title screen and enable the mod. To uninstall, delete LifePlay\Mods\$Name",
"and LifePlay\Content\Modules\$Name. Credits and licenses: LifePlay\Content\Modules\$Name\CREDITS.txt.",
"This mod contains cooked assets: it is made for LifePlay $Version and may need a rebuild for other versions.",
'',
'INSTALLATION : extraire cette archive dans le dossier du jeu (celui de LifePlay.exe) en fusionnant le dossier',
'LifePlay, puis activer le mod dans "Mod Manager" sur l''ecran titre. Desinstallation : supprimer',
"LifePlay\Mods\$Name et LifePlay\Content\Modules\$Name. Credits et licences : CREDITS.txt du module.",
"Ce mod contient des assets cuits pour LifePlay $Version : une autre version du jeu peut demander de le recuire.")
[IO.File]::WriteAllText((Join-Path $stage 'README.txt'), ($readme -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding $false))
}
finally {
Dismount-ModSource $Root $Name
}
Push-Location $stage
try {
& $SevenZip a -tzip $zip '.\*' | Out-Null
& $SevenZip t $zip | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Archive $zip invalide" }
}
finally { Pop-Location }
$staged = @(Get-ChildItem $stage -Recurse -File).Count
$listed = @(& $SevenZip l -slt $zip | Where-Object { $_ -match '^Attributes = [^D]*$' }).Count
if ($staged -ne $listed) { throw "Archive : $listed fichier(s) pour $staged assembles" }
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash
Write-Host ("Archive : {0} ({1:N0} octets, {2} fichiers, SHA-256 {3})" -f $zip, (Get-Item $zip).Length, $listed, $hash)
# 5. Installation dans un jeu de test.
if ($Install) {
if (-not (Test-Path (Join-Path $Install 'LifePlay.exe'))) { throw "$Install ne contient pas LifePlay.exe" }
robocopy (Join-Path $stage 'LifePlay') (Join-Path $Install 'LifePlay') /E /NFL /NDL /NJH /NJS /NP | Out-Null
if ($LASTEXITCODE -ge 8) { throw "Copie vers $Install en echec (robocopy $LASTEXITCODE)" }
Write-Host "Installe dans $Install"
}
# Code de sortie : succes (robocopy renvoie 1 a 7 quand il a copie).
exit 0

View file

@ -24,6 +24,18 @@ args = dict(a.split("=", 1) for a in sys.argv[1:] if "=" in a)
def import_texture(file, folder, name):
# Texture deja importee depuis le meme fichier : reutilisee. Une autre source dans un plugin (mod, H3) : refus,
# HairPipeline -Rebuild deplace d'abord les textures d'un mod vers la sauvegarde.
path = folder + "/" + name
if eal.does_asset_exist(path):
existing = unreal.load_asset(path)
data = existing.get_editor_property("asset_import_data")
source = data.get_first_filename().replace("\\", "/") if data else ""
if source.lower() == file.replace("\\", "/").lower():
unreal.log_warning("HAIRMAT texture %s reutilisee (meme source)" % name)
return existing
if not folder.startswith("/Game/"):
raise RuntimeError("HAIRMAT texture %s existe deja avec une autre source (%s) : relancer HairPipeline avec -Rebuild" % (path, source))
task = unreal.AssetImportTask()
task.filename = file.replace("\\", "/")
task.destination_path = folder
@ -53,6 +65,9 @@ if not mesh_path or not args.get("diffuse") or not args.get("strands_opacity"):
unreal.log_error("HAIRMAT usage : mesh=/Game/... diffuse=<fichier> strands_opacity=<fichier> [cap_opacity=<fichier>|white] [cap=Slot1,Slot2] [thicken=1]")
else:
folder, name = mesh_path.rsplit("/", 1)
# Dans un plugin (mod, H3), le registre de la commandlet ne connait pas encore les assets du dossier : sans cette
# analyse, does_asset_exist repond faux, la creation d'un asset existant ouvre une boite de dialogue et plante.
unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([folder], True)
thicken = float(args.get("thicken", "1"))
cap_slots = [s for s in args.get("cap", "").split(",") if s]
parent = unreal.load_asset("/Game/Heavy/Daz/Bridget/Hairs/DazHair")

View file

@ -28,6 +28,11 @@
Options de LPHairFromStaticMesh par coiffure dans le JSON : margin, scale, offset_z, offset_y, skin (transfer|rigid),
gap (cm).
Mod d'assets (H3b) : avec "mod": "<Nom>" et "folder": "/<Nom>/Hairs", les sources ModSources\<Nom> sont montees dans
Mods\<Nom> pendant le travail (Tools/ModSource.ps1) puis remises en place ; les .lphair et les credits vont dans
ModSources\<Nom>\Module ; -Rebuild deplace aussi les textures du mod vers la sauvegarde (pas de remplacement de
texture dans un plugin). Le mod se cuit ensuite avec Tools/BuildMod.ps1.
.EXAMPLE
.\Tools\HairPipeline.ps1 -Config Tools\hairs\HAIRS1.json -Step Inspect
.\Tools\HairPipeline.ps1 -Config Tools\hairs\HAIRS1.json -Step Apply
@ -45,10 +50,12 @@ $Proj = Join-Path $Root 'LifePlay.uproject'
$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
$Tools = ($PSScriptRoot -replace '\\', '/')
Add-Type -AssemblyName System.Drawing
. (Join-Path $PSScriptRoot 'ModSource.ps1')
$cfg = Get-Content $Config -Raw | ConvertFrom-Json
$backup = $cfg.backup
$folder = $cfg.folder
$mod = $cfg.mod
New-Item -ItemType Directory -Force $backup | Out-Null
function Invoke-UE([string[]] $Arguments, [string] $Filter) {
@ -136,6 +143,11 @@ function Select-Textures($all, [string[]] $StrandSlots) {
}
$report = @()
if ($mod) {
if ($folder -notlike "/$mod/*") { throw "folder ($folder) doit etre dans /$mod/" }
Mount-ModSource $Root $mod | Out-Null
}
try {
foreach ($h in $cfg.hairs) {
if ($Only -and $h.name -ne $Only) { continue }
$name = $h.name
@ -210,6 +222,11 @@ foreach ($h in $cfg.hairs) {
New-Item -ItemType Directory -Force $previous | Out-Null
Move-Item $existing $previous
Move-Item ($existing -replace '\.uasset$', '_Skeleton.uasset') $previous -ErrorAction SilentlyContinue
if ($mod) {
foreach ($suffix in '_Diffuse', '_StrandsOpacity', '_CapOpacity') {
Move-Item ($existing -replace '\.uasset$', "$suffix.uasset") $previous -ErrorAction SilentlyContinue
}
}
}
$common = @("-Static=$($h.mesh)", "-FitHead=$($h.head)", "-Out=$meshPath")
if ($h.margin) { $common += "-Margin=$($h.margin)" }
@ -230,7 +247,9 @@ foreach ($h in $cfg.hairs) {
$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"
# Mod (H3b) : module texte du mod, dans ses sources (montees dans Mods\<mod> pendant le travail).
$moduleDir = if ($mod) { Join-Path $Root "Mods\$mod\Module" } else { Join-Path $Root "Content\Modules\$($cfg.module)" }
$lphairDir = Join-Path $moduleDir 'Hairstyles'
New-Item -ItemType Directory -Force $lphairDir | Out-Null
$credit = "$($h.title) by $($h.author), $($h.license), $($h.url)"
$lines = @(
@ -239,8 +258,8 @@ 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 : 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' })
# Credits : fichier du jeu, ou Module\CREDITS.txt d'un mod.
$credits = if ($mod) { Join-Path $moduleDir 'CREDITS.txt' } else { Join-Path $Root '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
@ -255,6 +274,10 @@ if ($Step -eq 'Inspect') {
else {
Invoke-UE @('-run=pythonscript', "-script=$Tools/BuildCollections.py") 'collection\(s\) ecrite' | Write-Host
}
}
finally {
if ($mod) { Dismount-ModSource $Root $mod }
}
$reportFile = Join-Path $backup "rapport_$Step.txt"
$report | Set-Content $reportFile -Encoding utf8
Write-Host "`n=== Bilan ($Step) : $reportFile"

51
Tools/ModSource.ps1 Normal file
View file

@ -0,0 +1,51 @@
<#
.SYNOPSIS
Montage temporaire d'un mod d'assets (lot H3b, Docs/Migration/16_H3_Mods_Paquets.md).
.DESCRIPTION
Les sources d'un mod vivent dans ModSources\<Nom>\ :
<Nom>.uplugin plugin de contenu (CanContainContent)
Content\ assets du mod (non versionnes, comme Content/Heavy)
Module\ module LifePlay en texte (<Nom>.lpmod, Hairstyles\*.lphair, CREDITS.txt)
L'editeur ne monte /<Nom>/ que si le plugin est dans <projet>\Mods\ ; mais un plugin laisse dans Mods\ serait cuit
avec le jeu de base. Mount-ModSource deplace donc ModSources\<Nom> vers Mods\<Nom> le temps d'un travail (import,
cuisson DLC) et Dismount-ModSource le remet en place (deplacements seulement, rien n'est copie ni supprime).
A charger par dot-sourcing : . (Join-Path $PSScriptRoot 'ModSource.ps1')
#>
function Get-ModSourcePaths([string] $Root, [string] $Name) {
[pscustomobject]@{
Source = Join-Path $Root "ModSources\$Name"
Mounted = Join-Path $Root "Mods\$Name"
}
}
function Mount-ModSource([string] $Root, [string] $Name) {
$p = Get-ModSourcePaths $Root $Name
if (Test-Path $p.Mounted) {
throw "Mods\$Name existe deja (travail precedent interrompu ?) : le remettre dans ModSources\$Name a la main avant de relancer"
}
if (-not (Test-Path (Join-Path $p.Source "$Name.uplugin"))) {
throw "ModSources\$Name\$Name.uplugin introuvable"
}
New-Item -ItemType Directory -Force (Split-Path $p.Mounted -Parent) | Out-Null
Move-Item $p.Source $p.Mounted
Write-Host "Mod $Name monte dans Mods\$Name"
$p.Mounted
}
function Dismount-ModSource([string] $Root, [string] $Name) {
$p = Get-ModSourcePaths $Root $Name
if (Test-Path $p.Mounted) {
Move-Item $p.Mounted $p.Source
Write-Host "Mod $Name remis dans ModSources\$Name"
}
}
# Autres plugins presents dans Mods\ (ils seraient cuits avec le jeu de base).
function Get-StrayMods([string] $Root, [string] $Except = '') {
$mods = Join-Path $Root 'Mods'
if (-not (Test-Path $mods)) { return @() }
@(Get-ChildItem $mods -Directory | Where-Object { $_.Name -ne $Except } | ForEach-Object Name)
}

View file

@ -1,32 +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
}
]
}
{
"mod": "LPHairSample",
"module": "LPHairSample",
"folder": "/LPHairSample/Hairs",
"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
}
]
}