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:
parent
3c2da7a516
commit
a680f0bf19
10 changed files with 313 additions and 38 deletions
|
|
@ -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 ...'
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
132
Tools/BuildMod.ps1
Normal 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
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
51
Tools/ModSource.ps1
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue