<# .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 : dossier AnimSources\ ; la chaine est decrite par le bloc "chaine" de AnimSources\\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 ) ; sans version de reference Releases\\Windows : avertissement, sans echec ; -SansCuisson la saute Rapports : \etapes\_.json (debut, fin, duree, commandes, code, controles, verdict) et \etat.json (avancement, methode retenue, empreintes). -From reprend a l'etape n (etat.json lu ; etapes precedentes non rejouees) ; -To s'arrete apres l'etape n. Un passage complet (sans -From) deplace l'ancien contenu de dans \precedent_ (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 : , _Skeleton, _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 { '' } } 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 : .png dans $(Show-Rel (Join-Path $outDir 'vignettes')) ; .jpg et .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 .png par clip' ($missV.Count -eq 0) $(if ($missV) { 'manquantes : ' + (($missV | Select-Object -First 8) -join ', ') }) Add-Check 'une planche .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_ 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 }