320 lines
19 KiB
PowerShell
320 lines
19 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\ ;
|
|
0. -Anim <pack> : chaine complete d'animations du pack (Tools\anim\AnimPipeline.ps1 -Pack <pack>, lot AP7) ;
|
|
-From <etape> et -SansCuisson lui sont transmis ; rien d'autre n'est execute ;
|
|
1b. -Retarget bake|rigs : recopie les clips Quaternius sur Bridget et Michael (Tools/anim/RetargetPack.py
|
|
dans UnrealEditor.exe, Slate requis ; -RetargetConfig <json> : copie vers Saved\anim_retarget.json, methode et pack). L'editeur doit etre ferme. -RetargetOnly s'arrete avant la cuisson ;
|
|
1c. -Target sandbox|game ecrit Saved\Config\Windows\LPAnimTarget.ini. -Open lance cette cible
|
|
(sandbox : chambre d'essai ; game : partie, carte par defaut) et ne cuit pas ;
|
|
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 lpr_hair_pocolov
|
|
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Retarget bake
|
|
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Retarget bake -RetargetOnly
|
|
.\Tools\BuildMod.ps1 -Anim freeanims
|
|
.\Tools\BuildMod.ps1 -Target sandbox -Open
|
|
.\Tools\BuildMod.ps1 -Target game -Open
|
|
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Install D:\Game\LifePlay_Build_5.5_H3d\Windows
|
|
#>
|
|
param(
|
|
[string] $Name = '',
|
|
[string] $Version = '',
|
|
[string] $Out = 'D:\Game\LifePlay_Publication\Mods',
|
|
[string] $Install = '',
|
|
[string] $Retarget = '',
|
|
[string] $RetargetConfig = '',
|
|
[switch] $RetargetOnly,
|
|
[string] $Target = '',
|
|
[switch] $Open,
|
|
[string] $Anim = '',
|
|
[int] $From = 0,
|
|
[switch] $SansCuisson
|
|
)
|
|
|
|
$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'
|
|
|
|
# AP7 : chaine d'animations d'un pack.
|
|
if ($Anim) {
|
|
$pipe = @{ Pack = $Anim }
|
|
if ($PSBoundParameters.ContainsKey('From')) { $pipe.From = $From }
|
|
if ($SansCuisson) { $pipe.SansCuisson = $true }
|
|
& (Join-Path $PSScriptRoot 'anim\AnimPipeline.ps1') @pipe
|
|
exit $LASTEXITCODE
|
|
}
|
|
|
|
# AP7 : un resultat ou un echec de conversion n'est retenu que s'il porte le jeton de ce lancement
|
|
# (un ancien journal, ex. LifePlay-backup-<date>.log relu depuis l'octet 0, n'a jamais ce jeton).
|
|
function Find-RetargetLine([string] $Text, [string] $Mode, [string] $Jeton) {
|
|
$tok = [regex]::Escape($Jeton)
|
|
$success = if ($Mode -eq 'rigs') { "RETARGET rigs prets jeton $tok(?![0-9A-Za-z])" } else { "RETARGET bridget \d+ michael \d+ deplaces \d+[^\r\n]*? jeton $tok(?![0-9A-Za-z])" }
|
|
$ok = [regex]::Match($Text, $success)
|
|
$bad = [regex]::Match($Text, "RETARGET echec[^\r\n]*? jeton $tok(?![0-9A-Za-z])")
|
|
[pscustomobject]@{
|
|
Hit = if ($ok.Success) { $ok.Value.Trim() } else { $null }
|
|
Failure = if ($bad.Success) { $bad.Value.Trim() } else { $null }
|
|
}
|
|
}
|
|
|
|
function Invoke-AnimToGenesis([string] $ProjectRoot, [string] $Mode, [string] $ConfigFile = '') {
|
|
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
|
|
if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant la conversion" }
|
|
$configTarget = Join-Path $ProjectRoot 'Saved\anim_retarget.json'
|
|
New-Item -ItemType Directory -Force (Split-Path $configTarget) | Out-Null
|
|
$jeton = [guid]::NewGuid().ToString('N').Substring(0, 12)
|
|
if ($ConfigFile) {
|
|
if (-not (Test-Path $ConfigFile)) { throw "-RetargetConfig : fichier introuvable $ConfigFile" }
|
|
$cfg = Get-Content -LiteralPath $ConfigFile -Raw -Encoding utf8 | ConvertFrom-Json
|
|
$cfg | Add-Member -NotePropertyName jeton -NotePropertyValue $jeton -Force
|
|
[IO.File]::WriteAllText($configTarget, (($cfg | ConvertTo-Json -Depth 20) + "`r`n"), (New-Object Text.UTF8Encoding $false))
|
|
}
|
|
else {
|
|
[IO.File]::WriteAllText($configTarget, ('{"mode": "' + $Mode.Trim().ToLower() + '", "jeton": "' + $jeton + '"}' + "`r`n"), (New-Object Text.UTF8Encoding $false))
|
|
}
|
|
$effective = Get-Content -LiteralPath $configTarget -Raw -Encoding utf8 | ConvertFrom-Json
|
|
$methode = if ($effective.methode) { [string] $effective.methode } else { 'ue4' }
|
|
$clipsDir = if ($effective.clips_dir) { [string] $effective.clips_dir } else { '/Game/Quaternius/UAL' }
|
|
$editor = Join-Path $UE 'Binaries\Win64\UnrealEditor.exe'
|
|
$project = Join-Path $ProjectRoot 'LifePlay.uproject'
|
|
$script = Join-Path $ProjectRoot 'Tools\anim\RetargetPack.py'
|
|
$logDir = Join-Path $ProjectRoot 'Saved\Logs'
|
|
# Journaux relus en entier s'ils ont change depuis le lancement : le jeton suffit a ecarter les lignes d'une ancienne
|
|
# cuisson. (AP7 : l'ancienne lecture a partir de la taille d'avant sautait le nouveau LifePlay.log, recree plus petit
|
|
# par l'editeur au demarrage, et ne voyait jamais la ligne de resultat.)
|
|
$launched = Get-Date
|
|
Write-Host "Conversion des clips de $clipsDir vers Bridget et Michael (methode $methode, $Mode)"
|
|
# Chemins entre guillemets : le dossier du projet contient une espace (AP7 : sans guillemets, l'editeur
|
|
# recevait un chemin de projet coupe et se fermait sans journal).
|
|
$proc = Start-Process -FilePath $editor -ArgumentList "`"$project`" -ExecutePythonScript=`"$script`" -nosplash -unattended" -WorkingDirectory $ProjectRoot -PassThru
|
|
$deadline = (Get-Date).AddMinutes(25)
|
|
$hit = $null
|
|
$failure = $null
|
|
while ((Get-Date) -lt $deadline) {
|
|
Start-Sleep -Seconds 5
|
|
$logs = @(Get-ChildItem $logDir -Filter 'LifePlay*.log' -ErrorAction SilentlyContinue)
|
|
foreach ($file in $logs) {
|
|
if ($file.LastWriteTime -lt $launched) { continue }
|
|
try {
|
|
$stream = [IO.File]::Open($file.FullName, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
|
|
}
|
|
catch [System.IO.IOException] { continue }
|
|
try {
|
|
$reader = New-Object IO.StreamReader($stream)
|
|
$text = $reader.ReadToEnd()
|
|
}
|
|
finally { $stream.Close() }
|
|
$found = Find-RetargetLine $text $Mode $jeton
|
|
if ($found.Hit) { $hit = $found.Hit }
|
|
if ($found.Failure) { $failure = $found.Failure }
|
|
}
|
|
if ($hit) { break }
|
|
$editorGone = -not (Get-Process UnrealEditor -ErrorAction SilentlyContinue)
|
|
if ($editorGone -and $proc.HasExited) { break }
|
|
}
|
|
if ($failure -and -not $hit) { throw "Conversion en echec : $failure" }
|
|
if (-not $hit) { throw "Conversion sans ligne RETARGET : voir Saved\Logs\LifePlay.log" }
|
|
# L'editeur se ferme seul apres la conversion (RetargetPack.py), mais l'enregistrement des assets peut prendre
|
|
# plusieurs minutes (AP7 : plus de 2 min pour 84 clips) : attendre jusqu'a 10 min que plus aucun editeur ne tourne.
|
|
$quiet = (Get-Date).AddMinutes(10)
|
|
while ((Get-Date) -lt $quiet -and @(Get-Process UnrealEditor -ErrorAction SilentlyContinue).Count -gt 0) { Start-Sleep -Seconds 2 }
|
|
$still = @(Get-Process UnrealEditor -ErrorAction SilentlyContinue)
|
|
if ($still) { throw "Conversion terminee ($hit) mais UnrealEditor est encore ouvert : le fermer avant la cuisson" }
|
|
Write-Host " $hit"
|
|
}
|
|
|
|
function Write-AnimTarget([string] $ProjectRoot, [string] $Which) {
|
|
$dir = Join-Path $ProjectRoot 'Saved\Config\Windows'
|
|
New-Item -ItemType Directory -Force $dir | Out-Null
|
|
$ini = Join-Path $dir 'LPAnimTarget.ini'
|
|
[IO.File]::WriteAllText($ini, "[LifePlay.Anim]`r`nTarget=$Which`r`n", (New-Object Text.UTF8Encoding $false))
|
|
Write-Host "Cible des animations : $Which ($ini)"
|
|
}
|
|
|
|
function Open-AnimTarget([string] $ProjectRoot, [string] $Which) {
|
|
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
|
|
if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant d'ouvrir la cible" }
|
|
$editor = Join-Path $UE 'Binaries\Win64\UnrealEditor.exe'
|
|
$project = Join-Path $ProjectRoot 'LifePlay.uproject'
|
|
if ($Which -eq 'sandbox') {
|
|
# Le .bat quote l'argument -ini. Start-Process le coupe sur les deux-points.
|
|
& (Join-Path $ProjectRoot 'Tools\debug\LifePlay_interaction_sandbox.bat')
|
|
}
|
|
else {
|
|
Start-Process -FilePath $editor -ArgumentList @($project, '-game', '-windowed', '-ResX=1600', '-ResY=900') -WorkingDirectory $ProjectRoot | Out-Null
|
|
}
|
|
Write-Host "Ouvert : $Which"
|
|
}
|
|
|
|
# 1. Controles. Cuisson seulement : -Target -Open peut partir sans mod.
|
|
if ($Open -and -not $Target) { throw "-Open demande -Target sandbox ou -Target game" }
|
|
if ($Target -and $Target -notin 'sandbox', 'game') { throw "-Target vaut sandbox ou game (recu : $Target)" }
|
|
$bCook = -not $RetargetOnly -and -not $Open
|
|
if ($bCook -and -not $Name) { throw "Preciser -Name du mod" }
|
|
if (-not $bCook -and -not $Target -and -not $Retarget) { throw "Preciser -Name du mod, ou -Target sandbox|game" }
|
|
if ($bCook) {
|
|
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()
|
|
# Metadonnees (H3d) : le jeu lit les trois premieres lignes par position.
|
|
$lpmodLines = @(Get-Content $lpmod -Encoding utf8)
|
|
$expected = 'MODULE_UNIQUEID:', 'MODULE_NAME:', 'MODULE_AUTHOR:'
|
|
for ($i = 0; $i -lt 3; $i++) {
|
|
if ($lpmodLines.Count -le $i -or -not $lpmodLines[$i].StartsWith($expected[$i])) { throw "$Name.lpmod : la ligne $($i + 1) doit commencer par $($expected[$i]) (le jeu lit ces lignes par position)" }
|
|
}
|
|
function Get-LpmodValue([string] $Key) {
|
|
$m = $lpmodLines | Select-String -Pattern "^$([regex]::Escape($Key)):\s*(.*)$" | Select-Object -First 1
|
|
if ($m) { $m.Matches[0].Groups[1].Value.Trim() } else { '' }
|
|
}
|
|
$modVersion = Get-LpmodValue 'MODULE_VERSION'
|
|
# Mod payant : aucune licence d'asset non commerciale ou de magasin dans les credits.
|
|
$price = Get-LpmodValue 'MODULE_PRICE'
|
|
if ($price -and $price -notmatch '^(free|gratuit|0+([.,]0+)?)$') {
|
|
$blocking = Select-String -Path (Join-Path $src 'Module\CREDITS.txt') -Pattern '\bNC\b|NonCommercial|Non-Commercial|Standard License|Personal' -ErrorAction SilentlyContinue
|
|
if ($blocking) { throw "MODULE_PRICE '$price' : licence(s) incompatible(s) avec la vente dans CREDITS.txt ($($blocking[0].Line.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" }
|
|
}
|
|
|
|
# 1b. Conversion, puis redirecteur sandbox ou game.
|
|
if ($RetargetOnly -and -not $Retarget) { $Retarget = 'bake' }
|
|
if ($Retarget -and $Retarget -notin 'bake', 'rigs') { throw "-Retarget vaut bake ou rigs (recu : $Retarget)" }
|
|
if ($Target) { Write-AnimTarget $Root $Target }
|
|
if ($Retarget) { Invoke-AnimToGenesis $Root $Retarget $RetargetConfig }
|
|
if ($Open) {
|
|
Open-AnimTarget $Root $Target
|
|
exit 0
|
|
}
|
|
if (-not $bCook) {
|
|
Write-Host "Cuisson non lancee"
|
|
exit 0
|
|
}
|
|
|
|
# 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 = if ($modVersion) { "$Name-$modVersion-for-LifePlay-$Version" } else { "$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
|
|
# Copie livree (pas les sources) : date de paquet et version du jeu visee (avertissement en jeu si elle differe).
|
|
$stagedLpmod = Join-Path $stage "LifePlay\Content\Modules\$Name\$Name.lpmod"
|
|
$stamped = @($lpmodLines | Where-Object { $_ -notmatch '^MODULE_(PACKAGED|GAME_VERSION):' }) + "MODULE_PACKAGED: $(Get-Date -Format 'yyyy-MM-dd')" + "MODULE_GAME_VERSION: $Version"
|
|
[IO.File]::WriteAllText($stagedLpmod, ($stamped -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding $false))
|
|
# Descripteur livre : version du mod et lien de l'auteur dans les champs natifs du plugin.
|
|
$stagedPlugin = Join-Path $modDir "$Name.uplugin"
|
|
$desc = Get-Content $stagedPlugin -Raw | ConvertFrom-Json
|
|
if ($modVersion) { $desc.VersionName = $modVersion }
|
|
$authorLink = Get-LpmodValue 'MODULE_AUTHOR_LINK'
|
|
if ($authorLink) { $desc | Add-Member CreatedByURL $authorLink -Force }
|
|
[IO.File]::WriteAllText($stagedPlugin, ($desc | ConvertTo-Json) + "`r`n", (New-Object Text.UTF8Encoding $false))
|
|
$readme = @(
|
|
"$title ($Name)$(if ($modVersion) { " $modVersion" }) - 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)
|
|
|
|
# Index de diffusion (Tools/SplitModules.ps1, mode per_module) : ligne du mod ajoutee ou remplacee.
|
|
$index = Join-Path $Out 'INDEX.txt'
|
|
if (Test-Path $index) {
|
|
$entry = '{0} ({1}) : {2}, {3:N0} bytes, SHA-256 {4}' -f $title, $Name, (Split-Path $zip -Leaf), (Get-Item $zip).Length, $hash.ToLower()
|
|
$kept = @(Get-Content $index -Encoding utf8 | Where-Object { $_ -notlike "* ($Name) : *" })
|
|
[IO.File]::WriteAllText($index, (($kept + $entry) -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding $false))
|
|
Write-Host "Index mis a jour : $index"
|
|
}
|
|
|
|
# 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
|