LifePlay-Revival/Tools/BuildMod.ps1
edujiro5972 a680f0bf19 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>
2026-10-03 09:06:48 +02:00

132 lines
8.1 KiB
PowerShell

<#
.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