563 lines
34 KiB
PowerShell
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
|
|
}
|