LifePlay-Revival/Tools/anim/AnimPipeline.ps1
2026-10-07 15:49:56 +02:00

563 lines
34 KiB
PowerShell

<#
.SYNOPSIS
Pilote de la chaine d'animations d'un pack, de la source au mod livre (lot AP7, Docs/Migration/20_Chaine_Animations.md, section 3).
.DESCRIPTION
-Pack <nom> : dossier AnimSources\<nom> ; la chaine est decrite par le bloc "chaine" de AnimSources\<nom>\pack.json.
Huit etapes, un controle bloquant a chacune, arret au premier controle en echec :
0 inventaire pack.json complet, licence / credit / auteur renseignes (refus de "A COMPLETER"), sources trouvees,
empreintes SHA-256 des .uasset de la source
1 import glb : script d'import (saute si clips presents et glb inchange) ; uasset : rien. Clips > 0
2 identif. LPAnimIdentify sur le clip de reference ; valide, methode non factice ; la methode est retenue
3 calque LPAnimEval -PoseOnly (pose T des corps) ; rapport.json valide
4 conversion BuildMod -Retarget bake (cmd /c start /wait) ; ligne RETARGET bridget n michael n (n = clips) ;
puis LPAnimTwist ; rapport_torsion.json valide
5 evaluation LPAnimEval complet (un clip rejete bloque), rapport.html, puis sandbox (vignettes et planches)
6 catalogue copie des clips dans le mod (DeliverAnims.ps1, ecrit livraison.json), LPAnimCatalog (fiches),
Catalogue.py (DeliverAnims.ps1 -CatalogueOnly). La copie precede les fiches : le controle exige
fiches_calculees.json plus recent que livraison.json
7 livraison cuisson DLC du mod (BuildMod.ps1 -Name <mod>) ; sans version de reference Releases\<version>\Windows :
avertissement, sans echec ; -SansCuisson la saute
Rapports : <travail>\etapes\<n>_<nom>.json (debut, fin, duree, commandes, code, controles, verdict) et
<travail>\etat.json (avancement, methode retenue, empreintes). -From <n> reprend a l'etape n (etat.json lu ;
etapes precedentes non rejouees) ; -To <n> s'arrete apres l'etape n. Un passage complet (sans -From) deplace
l'ancien contenu de <travail> dans <travail>\precedent_<date> (jamais supprime).
-DryRun : affiche le plan (commandes exactes, dans l'ordre), ne lance ni ne cree rien ; l'etape 0 y est evaluee
pour de bon et ses echecs sont montres comme "echec prevu" sans arreter le plan.
.EXAMPLE
.\Tools\anim\AnimPipeline.ps1 -Pack quaternius -DryRun
.\Tools\anim\AnimPipeline.ps1 -Pack freeanims
.\Tools\anim\AnimPipeline.ps1 -Pack freeanims -From 5 -SansCuisson
.\Tools\BuildMod.ps1 -Anim freeanims
#>
param(
[Parameter(Mandatory)][string] $Pack,
[ValidateRange(0, 7)][int] $From = 0,
[ValidateRange(0, 7)][int] $To = 7,
[switch] $DryRun,
[switch] $SansCuisson
)
$ErrorActionPreference = 'Stop'
$Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$Editor = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
$Pwsh = (Get-Process -Id $PID).Path
$Utf8 = New-Object Text.UTF8Encoding $false
$StepNames = 'inventaire', 'import', 'identification', 'calque', 'conversion', 'evaluation', 'catalogue', 'livraison'
# ---------------------------------------------------------------- etat du passage
$script:DryRun = [bool] $DryRun
$script:Cfg = $null # pack.json
$script:Ch = $null # bloc chaine
$script:Work = '' # dossier de travail (absolu, barres obliques)
$script:Method = '' # methode retenue (etape 2) ; vide tant qu'inconnue
$script:Clips = @() # clips source (noms), hors B_/M_ et exclus
$script:Etat = [ordered]@{ pack = $Pack; etapes = [ordered]@{} }
$script:PrevEtat = $null
$script:St = $null # etape en cours
$script:Planned = @() # echecs prevus (essai a blanc)
function Show-Rel([string] $Path) {
$p = $Path -replace '/', '\'
if ($p.StartsWith($Root, [StringComparison]::OrdinalIgnoreCase)) { return $p.Substring($Root.Length).TrimStart('\') }
$p
}
function Add-Cmd([string] $Text) {
$script:St.Cmds += $Text
Write-Host " > $Text"
}
function Add-Check([string] $Name, [bool] $Ok, [string] $Detail = '') {
$script:St.Checks += [ordered]@{ nom = $Name; ok = $Ok; detail = $Detail }
if ($Ok) { Write-Host " [ok] $Name$(if ($Detail) { " : $Detail" })" }
elseif ($script:DryRun) { Write-Host " [ECHEC PREVU] $Name$(if ($Detail) { " : $Detail" })" -ForegroundColor Yellow; $script:Planned += "etape $($script:St.N) : $Name$(if ($Detail) { " : $Detail" })" }
else { Write-Host " [ECHEC] $Name$(if ($Detail) { " : $Detail" })" -ForegroundColor Red }
}
function Add-Warn([string] $Text) {
$script:St.Warns += $Text
Write-Host " [avertissement] $Text" -ForegroundColor Yellow
}
function Read-Json([string] $Path) {
if (-not (Test-Path -LiteralPath $Path)) { return $null }
try { Get-Content -LiteralPath $Path -Raw -Encoding utf8 | ConvertFrom-Json } catch { $null }
}
function Write-Text([string] $Path, [string] $Text) {
New-Item -ItemType Directory -Force (Split-Path $Path -Parent) | Out-Null
[IO.File]::WriteAllText($Path, $Text, $Utf8)
}
function ConvertTo-Disk([string] $GamePath) {
if ($GamePath -notmatch '^/Game/(.+)$') { throw "chemin Unreal invalide (attendu /Game/...) : $GamePath" }
Join-Path $Root ('Content\' + ($Matches[1] -replace '/', '\'))
}
function Get-SourceClips {
$dir = ConvertTo-Disk $script:Ch.source.clips
if (-not (Test-Path $dir)) { return @() }
$excl = @($script:Cfg.exclus | Where-Object { $_ })
# Hors maillage et squelette importes avec les clips (glb : <maillage>, <maillage>_Skeleton, <maillage>_PhysicsAsset).
$mesh = ([string] $script:Ch.source.maillage -replace '^.*/', '' -replace '\..*$', '')
@(Get-ChildItem $dir -File -Filter '*.uasset' | ForEach-Object { $_.BaseName } |
Where-Object { $_ -notlike 'B_*' -and $_ -notlike 'M_*' -and $_ -notin $excl -and -not ($mesh -and $_ -like "$mesh*") -and $_ -notlike '*_Skeleton' -and $_ -notlike '*_PhysicsAsset' } | Sort-Object)
}
function Test-Placeholder($Value) {
$s = [string] $Value
(-not $s.Trim()) -or ($s -match 'A COMPLETER')
}
function Test-EditorClosed {
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
Add-Check 'editeur ferme' ($running.Count -eq 0) $(if ($running) { "UnrealEditor ouvert (pid $($running[0].Id))" } else { '' })
$running.Count -eq 0
}
function Get-MethodArg { if ($script:Method) { $script:Method } elseif ($script:Ch.methode -and $script:Ch.methode -ne 'auto') { $script:Ch.methode } else { '<methode-etape-2>' } }
function Get-WorkRel { Show-Rel $script:Work }
# Commandlet via Tools\RunCommandlet.ps1 ; rend le code de sortie.
function Invoke-Commandlet([string] $Commandlet, [string] $CmdArgs) {
Add-Cmd "Tools\RunCommandlet.ps1 -Lot AP7 -Commandlet $Commandlet -Args '$CmdArgs'"
if ($script:DryRun) { return 0 }
$global:LASTEXITCODE = 0
& (Join-Path $Root 'Tools\RunCommandlet.ps1') -Lot AP7 -Commandlet $Commandlet -Args $CmdArgs | Out-Host
[int] $LASTEXITCODE
}
# Arguments communs a LPAnimEval / LPAnimTwist.
function Get-EvalArgs {
$bodies = (@($script:Ch.corps | ForEach-Object { '{0}:{1}' -f $_.nom, $_.prefixe })) -join ','
$templates = (@($script:Ch.corps | ForEach-Object { '{0}:{1}' -f $_.nom, $_.gabarit })) -join ','
'-Methode={0} -ClipDir={1} -RefClip={2} -BodyClipDir={3} -Templates={4} -Bodies={5}' -f (Get-MethodArg), $script:Ch.source.clips, $script:Ch.source.clip_reference, $script:Ch.sortie.clips, $templates, $bodies
}
function Get-WorkArg([string] $Name) { '-{0}="{1}"' -f $Name, $script:Work }
function Test-Fresh([string] $Path, [datetime] $Since) {
(Test-Path -LiteralPath $Path) -and ((Get-Item -LiteralPath $Path).LastWriteTime -ge $Since.AddSeconds(-2))
}
# ---------------------------------------------------------------- etapes
function Step-Inventaire {
$packFile = Join-Path $Root "AnimSources\$Pack\pack.json"
$script:Cfg = Read-Json $packFile
Add-Check 'pack.json lisible' ($null -ne $script:Cfg) (Show-Rel $packFile)
if (-not $script:Cfg) { throw "pack.json illisible : $packFile" }
$c = $script:Cfg.chaine
$script:Ch = $c
Add-Check 'bloc chaine present' ($null -ne $c)
if (-not $c) { throw 'bloc "chaine" absent de pack.json' }
$missing = @()
foreach ($k in 'methode', 'profil_attendu', 'mod', 'travail', 'sandbox') { if (Test-Placeholder $c.$k) { $missing += "chaine.$k" } }
foreach ($k in 'type', 'clips', 'maillage', 'clip_reference') { if (Test-Placeholder $c.source.$k) { $missing += "chaine.source.$k" } }
foreach ($k in 'clips', 'rigs') { if (Test-Placeholder $c.sortie.$k) { $missing += "chaine.sortie.$k" } }
if ($c.source.type -eq 'glb') { foreach ($k in 'fichier', 'import') { if (Test-Placeholder $c.source.$k) { $missing += "chaine.source.$k" } } }
elseif ($c.source.type -ne 'uasset') { $missing += "chaine.source.type ($($c.source.type) : glb ou uasset)" }
if (@($c.corps).Count -eq 0) { $missing += 'chaine.corps' }
foreach ($b in @($c.corps)) { foreach ($k in 'nom', 'maillage', 'prefixe', 'gabarit') { if (Test-Placeholder $b.$k) { $missing += "chaine.corps[$($b.nom)].$k" } } }
if (Test-Placeholder $script:Cfg.prefixe) { $missing += 'prefixe' }
Add-Check 'bloc chaine complet' ($missing.Count -eq 0) ($missing -join ', ')
$legal = @()
foreach ($k in 'licence', 'credit', 'auteur') { if (Test-Placeholder $script:Cfg.$k) { $legal += "$k = '$($script:Cfg.$k)'" } }
Add-Check 'licence, credit et auteur renseignes' ($legal.Count -eq 0) $(if ($legal) { 'refus : ' + ($legal -join ' ; ') } else { "$($script:Cfg.licence) / $($script:Cfg.auteur)" })
if ($missing.Count -gt 0) { throw 'bloc chaine incomplet' }
$script:Work = (Join-Path $Root ($c.travail -replace '/', '\')).TrimEnd('\') -replace '\\', '/'
$files = @()
if ($c.source.type -eq 'glb') {
$glb = [string] $c.source.fichier
$ok = Test-Path -LiteralPath $glb
Add-Check 'fichier glb trouve' $ok $glb
if ($ok) { $script:Etat.glb_sha256 = (Get-FileHash -LiteralPath $glb -Algorithm SHA256).Hash }
$imp = Join-Path $Root ($c.source.import -replace '/', '\')
Add-Check 'script d''import trouve' (Test-Path $imp) (Show-Rel $imp)
}
$dir = ConvertTo-Disk $c.source.clips
$dirOk = Test-Path $dir
if ($c.source.type -eq 'uasset') { Add-Check 'dossier des clips trouve' $dirOk (Show-Rel $dir) }
elseif (-not $dirOk) { Write-Host " (dossier des clips absent : cree par l'import) $(Show-Rel $dir)" }
$hashes = [ordered]@{}
if ($dirOk) {
foreach ($clip in Get-SourceClips) {
$hashes["$clip.uasset"] = (Get-FileHash -LiteralPath (Join-Path $dir "$clip.uasset") -Algorithm SHA256).Hash
}
}
if ($c.source.type -eq 'uasset') { Add-Check 'empreintes SHA-256 des .uasset' ($hashes.Count -gt 0) "$($hashes.Count) fichier(s)" }
else { Write-Host " empreintes SHA-256 : $($hashes.Count) .uasset deja presents + glb" }
$script:Etat.empreintes = $hashes
$script:St.Extra.empreintes = $hashes
if ($script:Etat.glb_sha256) { $script:St.Extra.glb_sha256 = $script:Etat.glb_sha256 }
}
function Step-Import {
$c = $script:Ch
if ($c.source.type -eq 'uasset') {
$script:St.Skipped = $true
Write-Host ' source uasset : rien a importer'
}
else {
$dir = ConvertTo-Disk $c.source.clips
$have = @(Get-SourceClips).Count
$prev = if ($script:PrevEtat) { $script:PrevEtat.glb_sha256 } else { $null }
$now = if ($script:Etat.glb_sha256) { $script:Etat.glb_sha256 } elseif (Test-Path -LiteralPath $c.source.fichier) { (Get-FileHash -LiteralPath $c.source.fichier -Algorithm SHA256).Hash } else { $null }
$script:Etat.glb_sha256 = $now
if ($have -gt 0 -and $prev -and $prev -eq $now) {
$script:St.Skipped = $true
Write-Host " import saute : $have clip(s) presents, glb inchange (SHA-256 $($now.Substring(0, 12))...)"
}
else {
$py = (Join-Path $Root ($c.source.import -replace '/', '\')) -replace '\\', '/'
$log = Join-Path $Root ('Saved\Logs\portage\AP7_Import_{0}.log' -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
Add-Cmd "UnrealEditor-Cmd.exe `"$Root\LifePlay.uproject`" -run=pythonscript `"-script=$py`" -unattended -nopause -nosplash -stdout -FullStdOutLogOutput > $(Show-Rel $log)"
if (-not $script:DryRun) {
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
New-Item -ItemType Directory -Force (Split-Path $log -Parent) | Out-Null
& $Editor (Join-Path $Root 'LifePlay.uproject') -run=pythonscript "-script=$py" -unattended -nopause -nosplash -stdout -FullStdOutLogOutput *> $log
$script:St.Code = $LASTEXITCODE
$imported = Select-String -Path $log -Pattern 'QUATERNIUS dossier \d+ asset' -Quiet
Add-Check 'import termine (journal)' $imported (Show-Rel $log)
}
}
}
if (-not $script:DryRun) {
$n = @(Get-SourceClips).Count
Add-Check 'clips source > 0' ($n -gt 0) "$n clip(s)"
$script:Clips = Get-SourceClips
}
else { Write-Host " clips source actuellement : $(@(Get-SourceClips).Count)" }
}
function Step-Identification {
$c = $script:Ch
$clip = '{0}/{1}' -f $c.source.clips.TrimEnd('/'), $c.source.clip_reference
$out = "$($script:Work)/identification.json"
$code = Invoke-Commandlet 'LPAnimIdentify' ('-Clip={0} -Out="{1}" -Attendu={2}' -f $clip, $out, $c.profil_attendu)
if ($script:DryRun) {
if ($c.methode -and $c.methode -ne 'auto') { Write-Host " methode imposee par pack.json : $($c.methode)" } else { Write-Host ' methode retenue : celle de identification.json (champ methode)' }
return
}
$script:St.Code = $code
Add-Check 'LPAnimIdentify code 0' ($code -eq 0) "code $code"
$r = Read-Json $out
Add-Check 'identification.json valide' ($r -and $r.valide -eq $true) $(if ($r) { "profil $($r.profil), methode $($r.methode)" } else { 'rapport absent' })
Add-Check 'methode non factice' ($r -and $r.methode -and $r.methode_factice -ne $true) $(if ($r) { "methode '$($r.methode)'" })
if ($c.methode -and $c.methode -ne 'auto') { $script:Method = [string] $c.methode; Write-Host " methode imposee par pack.json : $($script:Method)" }
elseif ($r -and $r.methode) { $script:Method = [string] $r.methode }
$script:Etat.methode = $script:Method
$script:St.Extra.methode = $script:Method
}
function Step-Calque {
if (-not $script:DryRun -and -not (Test-EditorClosed)) { throw 'editeur ouvert' }
$a = (Get-EvalArgs) + " -PoseOnly -PoseDir=`"$($script:Work)/pose`" " + (Get-WorkArg 'Out')
$code = Invoke-Commandlet 'LPAnimEval' $a
if ($script:DryRun) { return }
$script:St.Code = $code
$r = Read-Json (Join-Path $script:Work 'rapport.json')
Add-Check 'rapport.json valide (pose T)' ($r -and $r.valide -eq $true) $(if ($r -and $r.echecs) { (@($r.echecs) -join '; ') } elseif (-not $r) { 'rapport absent' } else { "code $code" })
foreach ($b in @($script:Ch.corps)) {
Add-Check "retarget_pose_$($b.nom).txt ecrit" (Test-Path (Join-Path $script:Work "pose\retarget_pose_$($b.nom).txt"))
}
}
function Step-Conversion {
$c = $script:Ch
$cfgFile = "$($script:Work)/anim_retarget.json"
$cfg = [ordered]@{
mode = 'bake'; methode = (Get-MethodArg); source_mesh = $c.source.maillage; clips_dir = $c.source.clips
out_dir = $c.sortie.clips; dest = $c.sortie.rigs
corps = @($c.corps | ForEach-Object { [ordered]@{ nom = $_.nom; maillage = $_.maillage; prefixe = $_.prefixe } })
exclure = @($script:Cfg.exclus | Where-Object { $_ })
pose_dir = "$($script:Work)/pose"
}
$log = "$($script:Work)/etapes/4_conversion_bake.log"
$bat = "$($script:Work)/etapes/4_conversion_bake.cmd"
$buildMod = Join-Path $Root 'Tools\BuildMod.ps1'
$line = "@`"$Pwsh`" -NoProfile -ExecutionPolicy Bypass -File `"$buildMod`" -Retarget bake -RetargetOnly -RetargetConfig `"$($cfgFile -replace '/', '\')`" > `"$($log -replace '/', '\')`" 2>&1"
Write-Host " config de cuisson : $(Show-Rel $cfgFile)"
Add-Cmd "cmd /c start `"`" /wait `"$(Show-Rel $bat)`" (contenu : $(Show-Rel $buildMod) -Retarget bake -RetargetOnly -RetargetConfig $(Show-Rel $cfgFile) > $(Show-Rel $log))"
if (-not $script:DryRun) {
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
Write-Text $cfgFile (($cfg | ConvertTo-Json -Depth 10) + "`r`n")
Write-Text $bat ($line + "`r`n@exit /b %ERRORLEVEL%`r`n")
# start /wait sur un .cmd laisse sa console ouverte (cmd /K) et ne rend jamais la main : passer par cmd /c (AP7).
$p = Start-Process cmd -ArgumentList ('/c start "" /wait cmd /c "{0}"' -f ($bat -replace '/', '\')) -Wait -PassThru -NoNewWindow
$script:St.Code = $p.ExitCode
$text = if (Test-Path $log) { Get-Content $log -Raw -Encoding utf8 } else { '' }
$n = @(Get-SourceClips).Count
$m = [regex]::Match($text, 'RETARGET bridget (\d+) michael (\d+)')
Add-Check 'conversion : code 0' ($script:St.Code -eq 0) "code $($script:St.Code)"
Add-Check "ligne RETARGET bridget $n michael $n" ($m.Success -and [int] $m.Groups[1].Value -eq $n -and [int] $m.Groups[2].Value -eq $n) $(if ($m.Success) { $m.Value } else { 'ligne absente : voir ' + (Show-Rel $log) })
if (-not $m.Success) { throw 'conversion non confirmee' }
}
$a = (Get-EvalArgs) + ' ' + (Get-WorkArg 'Out')
$code = Invoke-Commandlet 'LPAnimTwist' $a
if ($script:DryRun) { return }
$r = Read-Json (Join-Path $script:Work 'rapport_torsion.json')
Add-Check 'rapport_torsion.json valide' ($r -and $r.valide -eq $true) $(if (-not $r) { "rapport absent (code $code)" } else { '' })
}
# Sandbox : vignettes et planches. Fichiers de Saved\ remplaces le temps de la tournee, puis deplaces ou restaures.
function Invoke-Sandbox {
$c = $script:Ch
$saved = Join-Path $Root 'Saved'
$ini = Join-Path $saved 'sandbox_pack.ini'
$modeF = Join-Path $saved 'sandbox_mode.txt'
$tourF = Join-Path $saved 'sandbox_tour.txt'
$bat = Join-Path $Root 'Tools\debug\LifePlay_interaction_sandbox.bat'
$sbRel = ($c.sortie.clips -replace '^/Game/', '')
$clipRel = ($c.source.clips -replace '^/Game/', '')
$sbRoot = [string] $c.sandbox
$iniText = @(
'; Ecrit par Tools\anim\AnimPipeline.ps1 (lot AP7)', '[Sandbox.Pack]', "Name=$Pack", "Method=$(Get-MethodArg)",
"ClipRoot=$($c.source.clips)", "ClipDisk=$clipRel", "BodyClipRoot=$($c.sortie.clips)", "BodyClipDisk=$sbRel",
"RefClip=$($c.source.clip_reference)", "MannequinMesh=$($c.source.maillage)", '', '[Sandbox.Output]', "MeasureOutputRoot=$sbRoot") -join "`r`n"
$outDir = Join-Path $saved ($sbRoot -replace '/', '\')
Add-Cmd "ecrire Saved\sandbox_pack.ini (Name=$Pack, Method=$(Get-MethodArg), ClipRoot=$($c.source.clips), BodyClipRoot=$($c.sortie.clips), RefClip=$($c.source.clip_reference), MeasureOutputRoot=$sbRoot)"
Add-Cmd 'ecrire Saved\sandbox_mode.txt = Mesure ; Saved\sandbox_tour.txt = vignettes planches quitter'
Add-Cmd "Tools\debug\LifePlay_interaction_sandbox.bat (attente de la fermeture de UnrealEditor, 40 min maximum)"
Add-Cmd "deplacer Saved\sandbox_pack.ini vers $(Get-WorkRel)\sandbox_pack.ini ; restaurer sandbox_mode.txt d'origine"
if ($script:DryRun) { Write-Host " controle : <Clip>.png dans $(Show-Rel (Join-Path $outDir 'vignettes')) ; <Clip>.jpg et <Clip>.json dans $(Show-Rel (Join-Path $outDir 'planches')), par clip"; return }
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$t0 = Get-Date
New-Item -ItemType Directory -Force $script:Work | Out-Null
$origMode = if (Test-Path $modeF) { Get-Content $modeF -Raw } else { $null }
foreach ($f in $ini, $tourF) {
if (Test-Path $f) { Move-Item $f (Join-Path $script:Work ("{0}.avant_{1}" -f (Split-Path $f -Leaf), $stamp)) }
}
try {
Write-Text $ini ($iniText + "`r`n")
Write-Text $modeF 'Mesure'
Write-Text $tourF 'vignettes planches quitter'
# Sans tuyau ni attente : avec `& $bat | Out-Null`, PowerShell attendait la fin de l'editeur lance par le .bat
# (heritier de la sortie), puis ne le trouvait plus et concluait a tort « pas lancee » (AP7).
Start-Process -FilePath 'cmd.exe' -ArgumentList ('/c "{0}"' -f $bat) -WindowStyle Hidden | Out-Null
$deadline = (Get-Date).AddMinutes(3)
while (-not (Get-Process UnrealEditor -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 3 }
if (-not (Get-Process UnrealEditor -ErrorAction SilentlyContinue)) { throw 'la sandbox ne s''est pas lancee (3 min)' }
$deadline = (Get-Date).AddMinutes(40)
while ((Get-Process UnrealEditor -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 10 }
if (Get-Process UnrealEditor -ErrorAction SilentlyContinue) { throw 'la sandbox ne s''est pas fermee en 40 min (laissee ouverte)' }
}
finally {
if (Test-Path $ini) { Move-Item $ini (Join-Path $script:Work 'sandbox_pack.ini') -Force }
if (Test-Path $tourF) { Move-Item $tourF (Join-Path $script:Work 'sandbox_tour.txt') -Force }
if (Test-Path $modeF) { Move-Item $modeF (Join-Path $script:Work 'sandbox_mode.txt') -Force }
if ($null -ne $origMode) { Write-Text $modeF $origMode }
}
$missV = @(); $missP = @()
foreach ($clip in (Get-SourceClips)) {
if (-not (Test-Fresh (Join-Path $outDir "vignettes\$clip.png") $t0)) { $missV += $clip }
if (-not ((Test-Fresh (Join-Path $outDir "planches\$clip.jpg") $t0) -and (Test-Fresh (Join-Path $outDir "planches\$clip.json") $t0))) { $missP += $clip }
}
Add-Check 'une vignette <Clip>.png par clip' ($missV.Count -eq 0) $(if ($missV) { 'manquantes : ' + (($missV | Select-Object -First 8) -join ', ') })
Add-Check 'une planche <Clip>.jpg + .json par clip' ($missP.Count -eq 0) $(if ($missP) { 'manquantes : ' + (($missP | Select-Object -First 8) -join ', ') })
}
function Step-Evaluation {
if (-not $script:DryRun -and -not (Test-EditorClosed)) { throw 'editeur ouvert' }
$a = (Get-EvalArgs) + " -PoseDir=`"$($script:Work)/pose`" " + (Get-WorkArg 'Out')
$code = Invoke-Commandlet 'LPAnimEval' $a
Add-Cmd "python Tools\anim\RapportControles.py $(Get-WorkRel)"
if (-not $script:DryRun) {
$script:St.Code = $code
$r = Read-Json (Join-Path $script:Work 'rapport.json')
Add-Check 'rapport.json valide (aucun clip rejete)' ($r -and $r.valide -eq $true) $(if ($r -and $r.echecs) { (@($r.echecs) -join '; ') } elseif (-not $r) { 'rapport absent' })
if ($r) { foreach ($x in @($r.a_revoir)) { Add-Warn "a revoir : $x" } }
if (-not ($r -and $r.valide -eq $true)) { throw 'evaluation en echec' }
& python (Join-Path $Root 'Tools\anim\RapportControles.py') $script:Work | Out-Host
Add-Check 'rapport.html ecrit' ((Test-Path (Join-Path $script:Work 'rapport.html')) -and $LASTEXITCODE -eq 0)
}
Invoke-Sandbox
}
function Get-DeliverParams([switch] $CatalogueOnly) {
$c = $script:Ch
$outDir = Join-Path (Join-Path $Root 'Saved') (($c.sandbox -replace '/', '\'))
$p = [ordered]@{
Name = $c.mod; Source = $c.sortie.clips; Prefix = $script:Cfg.prefixe; Exclude = (@($script:Cfg.exclus | Where-Object { $_ }) -join ',')
Pipeline = (Get-WorkRel); Pack = (Join-Path $Root "AnimSources\$Pack\pack.json")
}
if ($CatalogueOnly) {
$p.Vignettes = Join-Path $outDir 'vignettes'; $p.Planches = Join-Path $outDir 'planches'; $p.CatalogueOnly = $true
}
$p
}
function Format-Params($P) { (@($P.GetEnumerator() | ForEach-Object { if ($_.Value -is [bool]) { "-$($_.Key)" } else { "-$($_.Key) '$($_.Value)'" } })) -join ' ' }
function Invoke-Deliver($P) {
Add-Cmd "Tools\anim\DeliverAnims.ps1 $(Format-Params $P)"
if ($script:DryRun) { return 0 }
$global:LASTEXITCODE = 0
& (Join-Path $Root 'Tools\anim\DeliverAnims.ps1') @P | Out-Host
[int] $LASTEXITCODE
}
function Step-Catalogue {
$c = $script:Ch
$liv = Join-Path $script:Work 'livraison.json'
$fic = Join-Path $script:Work 'fiches_calculees.json'
$t0 = Get-Date
# a. copie des clips dans le mod (ecrit livraison.json)
$code = Invoke-Deliver (Get-DeliverParams)
if (-not $script:DryRun) {
$script:St.Code = $code
Add-Check 'livraison (copie dans le mod) : code 0' ($code -eq 0) "code $code"
Add-Check 'livraison.json ecrit' (Test-Fresh $liv $t0)
if ($code -ne 0 -or -not (Test-Fresh $liv $t0)) { throw 'livraison en echec' }
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
}
# b. fiches calculees
$code = Invoke-Commandlet 'LPAnimCatalog' ('-Livraison="{0}" -Out="{1}"' -f "$($script:Work)/livraison.json", $script:Work)
if (-not $script:DryRun) {
$f = Read-Json $fic
$newer = (Test-Path $fic) -and (Test-Path $liv) -and ((Get-Item $fic).LastWriteTime -gt (Get-Item $liv).LastWriteTime)
Add-Check 'fiches_calculees.json plus recent que livraison.json' $newer
Add-Check 'fiches calculees sans echec' ($f -and @($f.echecs).Count -eq 0 -and $code -eq 0) $(if ($f -and $f.echecs) { (@($f.echecs) -join '; ') } else { "code $code" })
if (-not $newer) { throw 'fiches non calculees' }
}
# c. catalogue (Catalogue.py)
$code = Invoke-Deliver (Get-DeliverParams -CatalogueOnly)
if (-not $script:DryRun) {
$r = Read-Json (Join-Path $script:Work 'catalogue_rapport.json')
$cat = Join-Path $Root "ModSources\$($c.mod)\Module\Animations\catalogue.json"
Add-Check 'Catalogue.py : code 0' ($code -eq 0) "code $code"
Add-Check 'catalogue_rapport.json ok' ($r -and $r.ok -eq $true) $(if ($r) { "$($r.fiches) fiche(s)" })
Add-Check 'catalogue.json dans le mod' (Test-Path $cat) (Show-Rel $cat)
}
}
function Step-Livraison {
$c = $script:Ch
$version = (Select-String -Path (Join-Path $Root 'Config\DefaultGame.ini') -Pattern '^ProjectVersion=(.+)$' | Select-Object -First 1).Matches[0].Groups[1].Value.Trim()
$release = Join-Path $Root "Releases\$version\Windows\AssetRegistry.bin"
$log = "$($script:Work)/etapes/7_livraison_cuisson.log"
if ($SansCuisson) {
$script:St.Skipped = $true
Write-Host ' -SansCuisson : cuisson DLC sautee'
return
}
if (-not (Test-Path $release)) {
Add-Cmd "(saute) Tools\BuildMod.ps1 -Name $($c.mod) version de reference absente : $(Show-Rel $release)"
Add-Warn 'version de reference absente : cuisson DLC non faite'
return
}
Add-Cmd "$(Split-Path $Pwsh -Leaf) -NoProfile -File Tools\BuildMod.ps1 -Name $($c.mod) > $(Show-Rel $log) (version de reference $version)"
if ($script:DryRun) { return }
New-Item -ItemType Directory -Force (Split-Path $log -Parent) | Out-Null
& $Pwsh -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Root 'Tools\BuildMod.ps1') -Name $c.mod *>&1 | Tee-Object -FilePath $log | Out-Host
$script:St.Code = $LASTEXITCODE
$text = Get-Content $log -Raw -Encoding utf8
$m = [regex]::Match($text, 'Archive : (.+?\.zip) \(')
Add-Check 'BuildMod : code 0' ($script:St.Code -eq 0) "code $($script:St.Code)"
Add-Check 'archive .zip produite' ($m.Success -and (Test-Path $m.Groups[1].Value)) $(if ($m.Success) { $m.Groups[1].Value })
}
# ---------------------------------------------------------------- moteur
function Save-Etat {
if ($script:DryRun) { return }
$script:Etat.maj = Get-Date -Format s
Write-Text (Join-Path $script:Work 'etat.json') (($script:Etat | ConvertTo-Json -Depth 10) + "`r`n")
}
function Move-Previous {
$w = $script:Work
$keep = @(Get-ChildItem $w -Force -ErrorAction SilentlyContinue | Where-Object { -not ($_.PSIsContainer -and $_.Name -like 'precedent_*') })
if (-not $keep.Count) { return }
$dest = Join-Path $w ("precedent_{0}" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
New-Item -ItemType Directory -Force $dest | Out-Null
foreach ($k in $keep) { Move-Item -LiteralPath $k.FullName -Destination $dest }
Write-Host " resultat precedent deplace : $($keep.Count) element(s) -> $(Show-Rel $dest)"
}
function Invoke-Step([int] $N, [scriptblock] $Block) {
if ($N -lt $From -or $N -gt $To) { return }
$name = $StepNames[$N]
Write-Host ''
Write-Host "== Etape $N : $name"
$script:St = [ordered]@{ N = $N; Cmds = @(); Checks = @(); Warns = @(); Extra = [ordered]@{}; Code = $null; Skipped = $false }
$t0 = Get-Date
try { $null = & $Block }
catch {
$msg = $_.Exception.Message
$script:St.Checks += [ordered]@{ nom = 'erreur'; ok = $false; detail = $msg }
if ($script:DryRun) { $script:Planned += "etape $N : $msg"; Write-Host " [ECHEC PREVU] $msg" -ForegroundColor Yellow }
else { Write-Host " [ECHEC] $msg" -ForegroundColor Red }
}
$bad = @($script:St.Checks | Where-Object { $_.ok -eq $false })
$verdict = if ($bad.Count) { 'echec' } elseif ($script:St.Skipped) { 'saute' } elseif ($script:St.Warns.Count) { 'avertissement' } else { 'ok' }
if ($script:DryRun) { return $(if ($N -eq 0 -and -not $script:Ch) { 'stop' }) }
if (-not $script:Work) { throw "Etape $N ($name) en echec : $((@($bad | ForEach-Object { $_.detail })) -join ' ; ')" }
# Passage complet : l'ancien resultat est deplace une fois l'inventaire accepte (un refus ne touche a rien).
$recName = "${N}_$name"
if ($N -eq 0 -and $From -eq 0) {
if ($bad.Count) { $recName = "${N}_${name}_refus_$(Get-Date -Format 'yyyyMMdd-HHmmss')" } else { Move-Previous }
}
$t1 = Get-Date
$rec = [ordered]@{
etape = $N; nom = $name; debut = $t0.ToString('s'); fin = $t1.ToString('s'); duree_s = [int] ($t1 - $t0).TotalSeconds
commandes = $script:St.Cmds; code = $script:St.Code; controles = $script:St.Checks; avertissements = $script:St.Warns; verdict = $verdict
}
foreach ($k in $script:St.Extra.Keys) { $rec[$k] = $script:St.Extra[$k] }
Write-Text (Join-Path $script:Work "etapes\$recName.json") (($rec | ConvertTo-Json -Depth 10) + "`r`n")
$script:Etat.etapes["$N"] = [ordered]@{ nom = $name; verdict = $verdict; fin = $t1.ToString('s') }
Save-Etat
Write-Host " verdict : $verdict ($($rec.duree_s) s)"
if ($verdict -eq 'echec') {
throw "Etape $N ($name) en echec : " + ((@($bad | ForEach-Object { "$($_.nom)$(if ($_.detail) { " ($($_.detail))" })" })) -join ' ; ')
}
}
try {
if ($Pack -notmatch '^[A-Za-z0-9_-]+$') { throw "nom de pack invalide : $Pack" }
if ($From -gt $To) { throw "-From ($From) apres -To ($To)" }
Write-Host "Pilote AnimPipeline : pack $Pack, etapes $From a $To$(if ($DryRun) { ' (A BLANC : rien lance, rien cree)' })"
# Le bloc chaine est lu des l'etape 0 ; pour -From > 0 il faut le charger sans rejouer l'inventaire.
if ($From -gt 0) {
$script:Cfg = Read-Json (Join-Path $Root "AnimSources\$Pack\pack.json")
if (-not $script:Cfg -or -not $script:Cfg.chaine) { throw "pack.json illisible ou sans bloc chaine (AnimSources\$Pack)" }
$script:Ch = $script:Cfg.chaine
$script:Work = (Join-Path $Root ($script:Ch.travail -replace '/', '\')).TrimEnd('\') -replace '\\', '/'
$prev = if (Test-Path (Join-Path $script:Work 'etat.json')) { Get-Content (Join-Path $script:Work 'etat.json') -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable } else { $null }
if ($prev) { $script:Etat = $prev; $script:PrevEtat = $prev; if ($prev.methode) { $script:Method = [string] $prev.methode } }
if (-not $script:Method -and $script:Ch.methode -and $script:Ch.methode -ne 'auto') { $script:Method = [string] $script:Ch.methode }
for ($i = 0; $i -lt $From; $i++) {
$e = if ($prev -and $prev.etapes) { $prev.etapes["$i"] } else { $null }
if (-not $e -or $e.verdict -notin 'ok', 'saute', 'avertissement') {
if ($DryRun) { Write-Host " (etape $i : pas de verdict enregistre)" } else { Write-Warning "etape $i : pas de verdict enregistre dans etat.json (reprise sur la foi de l'utilisateur)" }
}
}
if ($From -ge 3 -and -not $script:Method -and -not $DryRun) { throw "methode inconnue (etat.json sans methode et pack.json = auto) : reprendre depuis l'etape 2" }
$script:Clips = Get-SourceClips
}
else {
# Passage complet : etat precedent lu avant tout deplacement.
$cfgPeek = Read-Json (Join-Path $Root "AnimSources\$Pack\pack.json")
if ($cfgPeek -and $cfgPeek.chaine.travail) {
$w = (Join-Path $Root ($cfgPeek.chaine.travail -replace '/', '\')).TrimEnd('\')
$ef = Join-Path $w 'etat.json'
if (Test-Path $ef) { $script:PrevEtat = Get-Content $ef -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable }
$keep = @(Get-ChildItem $w -Force -ErrorAction SilentlyContinue | Where-Object { -not ($_.PSIsContainer -and $_.Name -like 'precedent_*') })
if ($keep.Count) { Write-Host "Resultat precedent : $($keep.Count) element(s) de $(Show-Rel $w) a deplacer dans $(Show-Rel $w)\precedent_<date> apres l'inventaire accepte" }
}
}
$stop = $null
$stop = Invoke-Step 0 { Step-Inventaire }
if ($stop -eq 'stop') { throw 'plan interrompu : pack.json illisible ou sans bloc chaine' }
Invoke-Step 1 { Step-Import }
Invoke-Step 2 { Step-Identification }
Invoke-Step 3 { Step-Calque }
Invoke-Step 4 { Step-Conversion }
Invoke-Step 5 { Step-Evaluation }
Invoke-Step 6 { Step-Catalogue }
Invoke-Step 7 { Step-Livraison }
Write-Host ''
if ($DryRun) {
if ($script:Planned.Count) {
Write-Host "A blanc : plan complet affiche ; $($script:Planned.Count) echec(s) prevu(s) :" -ForegroundColor Yellow
$script:Planned | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
}
else { Write-Host 'A blanc : plan complet affiche, aucun echec prevu' }
}
else { Write-Host "Chaine terminee : pack $Pack, etapes $From a $To (rapports : $(Get-WorkRel)\etapes)" }
exit 0
}
catch {
Write-Host "ECHEC : $($_.Exception.Message)" -ForegroundColor Red
exit 1
}