From a680f0bf19402461af2fbf9690cf50138eb4a9e4 Mon Sep 17 00:00:00 2001 From: edujiro5972 <336072401+edujiro5972@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:06:48 +0200 Subject: [PATCH] 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/, 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 --- Docs/Migration/09_Commandes_Utiles.md | 4 +- Docs/Migration/16_H3_Mods_Paquets.md | 44 ++++++++ Docs/Migration/Journal_Modifications.md | 1 + Docs/Migration/README.md | 2 +- Source/LifePlay/LPHairstyleSubsystem.cpp | 9 +- Tools/BuildMod.ps1 | 132 +++++++++++++++++++++++ Tools/HairMaterials.py | 15 +++ Tools/HairPipeline.ps1 | 29 ++++- Tools/ModSource.ps1 | 51 +++++++++ Tools/mods/LPHairSample.json | 64 +++++------ 10 files changed, 313 insertions(+), 38 deletions(-) create mode 100644 Tools/BuildMod.ps1 create mode 100644 Tools/ModSource.ps1 diff --git a/Docs/Migration/09_Commandes_Utiles.md b/Docs/Migration/09_Commandes_Utiles.md index 643f1ce..9a339b5 100644 --- a/Docs/Migration/09_Commandes_Utiles.md +++ b/Docs/Migration/09_Commandes_Utiles.md @@ -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\, cuisson et archive en une commande +.\Tools\BuildMod.ps1 -Name [-Install \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="\Mods\\.uplugin" -basedonreleaseversion=2026.10.4 ...' diff --git a/Docs/Migration/16_H3_Mods_Paquets.md b/Docs/Migration/16_H3_Mods_Paquets.md index 6e16383..2d77c01 100644 --- a/Docs/Migration/16_H3_Mods_Paquets.md +++ b/Docs/Migration/16_H3_Mods_Paquets.md @@ -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//.uplugin plugin de contenu (versionné) +ModSources//Content/ assets du mod (non versionnés, comme Content/Heavy) +ModSources//Module/ module LifePlay en texte : .lpmod (MODULE_UNIQUEID = ), + Hairstyles/*.lphair, CREDITS.txt (versionnés) +``` + +`/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\.json -Step Inspect # "mod": "", "folder": "//Hairs" +.\Tools\HairPipeline.ps1 -Config Tools\mods\.json -Step Apply +.\Tools\BuildMod.ps1 -Name [-Version 2026.10.4] [-Out D:\Game\LifePlay_Publication\Mods] [-Install \Windows] +``` + +`BuildMod.ps1` : contrôles (plugin, module, version de référence `Releases/`, 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//`), assemblage `-for-LifePlay-` (`LifePlay/Mods/`, +`LifePlay/Content/Modules/`, `README.txt` bilingue), archive `.zip` testée ; un assemblage précédent est déplacé +(`precedent_`). 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`. diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index 6a77a3a..047e87b 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -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//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 | diff --git a/Docs/Migration/README.md b/Docs/Migration/README.md index 6c8f689..18870ea 100644 --- a/Docs/Migration/README.md +++ b/Docs/Migration/README.md @@ -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 | diff --git a/Source/LifePlay/LPHairstyleSubsystem.cpp b/Source/LifePlay/LPHairstyleSubsystem.cpp index 71a138a..0c88088 100644 --- a/Source/LifePlay/LPHairstyleSubsystem.cpp +++ b/Source/LifePlay/LPHairstyleSubsystem.cpp @@ -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; diff --git a/Tools/BuildMod.ps1 b/Tools/BuildMod.ps1 new file mode 100644 index 0000000..0d750b7 --- /dev/null +++ b/Tools/BuildMod.ps1 @@ -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\\ (.uplugin, Content\, Module\ ; voir Tools/ModSource.ps1). + 1. controles : plugin et module (MODULE_UNIQUEID = ), version de reference Releases\\Windows, aucun + autre plugin dans Mods\ ; + 2. montage du mod dans Mods\, 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\ dans tous les cas ; + 3. controle du conteneur : chaque paquet est sous Mods// (aucun asset du jeu embarque) ; + 4. assemblage \-for-LifePlay-\LifePlay\{Mods\, Content\Modules\} + notice bilingue, + archive .zip testee (7-Zip) ; un assemblage precedent du meme nom est deplace (precedent_), jamais supprime ; + 5. -Install : 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//. + $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 diff --git a/Tools/HairMaterials.py b/Tools/HairMaterials.py index 2e3e125..a4eb0fc 100644 --- a/Tools/HairMaterials.py +++ b/Tools/HairMaterials.py @@ -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= strands_opacity= [cap_opacity=|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") diff --git a/Tools/HairPipeline.ps1 b/Tools/HairPipeline.ps1 index f37d98e..d40bf13 100644 --- a/Tools/HairPipeline.ps1 +++ b/Tools/HairPipeline.ps1 @@ -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": "" et "folder": "//Hairs", les sources ModSources\ sont montees dans + Mods\ pendant le travail (Tools/ModSource.ps1) puis remises en place ; les .lphair et les credits vont dans + ModSources\\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\ 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" diff --git a/Tools/ModSource.ps1 b/Tools/ModSource.ps1 new file mode 100644 index 0000000..1f27fe6 --- /dev/null +++ b/Tools/ModSource.ps1 @@ -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\\ : + .uplugin plugin de contenu (CanContainContent) + Content\ assets du mod (non versionnes, comme Content/Heavy) + Module\ module LifePlay en texte (.lpmod, Hairstyles\*.lphair, CREDITS.txt) + L'editeur ne monte // que si le plugin est dans \Mods\ ; mais un plugin laisse dans Mods\ serait cuit + avec le jeu de base. Mount-ModSource deplace donc ModSources\ vers Mods\ 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) +} diff --git a/Tools/mods/LPHairSample.json b/Tools/mods/LPHairSample.json index 9607e1c..5f2c2eb 100644 --- a/Tools/mods/LPHairSample.json +++ b/Tools/mods/LPHairSample.json @@ -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 + } + ] +}