<# .SYNOPSIS Copie les clips d'animation convertis dans un mod d'assets, renommes a la convention du jeu (lot AP5). .DESCRIPTION /Game/Quaternius/UAL/B_ -> //Anims/Female/qua__F ; M_ -> //Anims/Male/qua__M. Le jeu appelle Actor.animate() et cherche _F et _M (cle = ce qui precede le dernier _). 1. controle bloquant : rapport.json existe et valide = vrai (les a_revoir sont affiches, non bloquants) ; 2. controles : editeur ferme, aucun autre plugin dans Mods\ ; 3. table cle / sources / destinations calculee depuis Content\Quaternius\UAL\B_*.uasset et M_*.uasset ; 4. -DryRun : affiche la table et s'arrete (rien n'est lance ni cree) ; 5. sinon : cree ModSources\ s'il manque (jamais d'ecrasement), monte le mod, lance Tools\anim\DeliverAnims.py dans UnrealEditor-Cmd (journal Saved\Logs\portage\AP5_DeliverAnims_.log), remet le mod en place, controle la sortie (LIVRAISON copies 2n erreurs 0, n fichiers .uasset par sexe). Code de sortie 1 au moindre ecart. .EXAMPLE .\Tools\anim\DeliverAnims.ps1 -DryRun .\Tools\anim\DeliverAnims.ps1 #> param( [string] $Name = 'lpr_anim_quaternius', [string] $Source = '/Game/Quaternius/UAL', [string] $Prefix = 'qua', [string] $Exclude = 'A_TPose', [string] $Report = '', [switch] $DryRun ) $ErrorActionPreference = 'Stop' $Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent . (Join-Path $Root 'Tools\ModSource.ps1') $Editor = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' if (-not $Report) { $Report = Join-Path $Root 'Saved\AnimPipeline\quaternius\rapport.json' } function New-IfMissing([string] $Path, [string] $Text) { if (Test-Path $Path) { return } New-Item -ItemType Directory -Force (Split-Path $Path -Parent) | Out-Null [IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false)) Write-Host " cree : $Path" } try { # 1. Verdict des controles qualite. if (-not (Test-Path $Report)) { throw "rapport.json introuvable : $Report" } $rapport = Get-Content $Report -Raw -Encoding utf8 | ConvertFrom-Json if ($rapport.valide -ne $true) { $echecs = @($rapport.echecs) -join '; ' throw "Rapport non valide ($Report). Echecs : $echecs" } foreach ($r in @($rapport.a_revoir)) { Write-Warning "a revoir : $r" } # 2. Controles d'environnement. $running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue) if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant la livraison" } $stray = Get-StrayMods $Root if ($stray) { throw "Autres plugins dans Mods\ ($($stray -join ', ')) : les remettre dans ModSources" } # 3. Table des cles. if ($Source -notmatch '^/Game/(.+)$') { throw "-Source doit commencer par /Game/ (recu : $Source)" } $srcDir = Join-Path $Root ('Content\' + ($Matches[1] -replace '/', '\')) if (-not (Test-Path $srcDir)) { throw "Dossier source introuvable : $srcDir" } $excluded = @($Exclude -split ',' | Where-Object { $_ }) $fem = @{}; $mal = @{} foreach ($f in Get-ChildItem $srcDir -File -Filter '*.uasset') { $base = $f.BaseName if ($base -like 'B_*') { $fem[$base.Substring(2)] = $Source.TrimEnd('/') + '/' + $base } elseif ($base -like 'M_*') { $mal[$base.Substring(2)] = $Source.TrimEnd('/') + '/' + $base } } $clips = @(($fem.Keys + $mal.Keys) | Sort-Object -Unique | Where-Object { $_ -notin $excluded }) $table = @() $seen = @{} foreach ($clip in $clips) { if (-not $fem.ContainsKey($clip)) { throw "Clip $clip : source B_ absente" } if (-not $mal.ContainsKey($clip)) { throw "Clip $clip : source M_ absente" } $cle = ($Prefix + '_' + $clip).ToLowerInvariant() if ($cle -notmatch '^[a-z0-9_]+$') { throw "Cle invalide (caracteres hors [a-z0-9_]) : $cle" } if ($seen.ContainsKey($cle)) { throw "Cle en double : $cle ($clip et $($seen[$cle]))" } $seen[$cle] = $clip $table += [pscustomobject]@{ Cle = $cle; SourceF = $fem[$clip]; SourceM = $mal[$clip] AssetF = "/$Name/Anims/Female/${cle}_F"; AssetM = "/$Name/Anims/Male/${cle}_M" } } if ($table.Count -eq 0) { throw "Aucun clip a livrer dans $srcDir" } Write-Host "Table : $($table.Count) cle(s) pour /$Name/ (exclus : $($excluded -join ', '))" $table | Select-Object -First 5 | ForEach-Object { Write-Host (" {0} : {1} -> {2} ; {3} -> {4}" -f $_.Cle, $_.SourceF, $_.AssetF, $_.SourceM, $_.AssetM) } if ($DryRun) { Write-Host "A blanc : rien lance, rien cree" exit 0 } # 5. Sources du mod (jamais d'ecrasement). $paths = Get-ModSourcePaths $Root $Name $src = $paths.Source $desc = 'Free animation clips from the Quaternius Universal Animation Library (CC0), retargeted for LifePlay' $author = 'LifePlay Revival (animations by Quaternius)' $uplugin = [ordered]@{ FileVersion = 3; Version = 1; VersionName = '1.0'; FriendlyName = 'Quaternius Animations' Description = $desc; Category = 'LifePlay Mods'; CreatedBy = $author CanContainContent = $true; IsBetaVersion = $false; Installed = $false CreatedByURL = 'https://quaternius.com' } New-IfMissing (Join-Path $src "$Name.uplugin") (($uplugin | ConvertTo-Json) + "`r`n") $lpmod = @( "MODULE_UNIQUEID: $Name", 'MODULE_NAME: Quaternius Animations', "MODULE_AUTHOR: $author", 'MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival', "MODULE_DESCRIPTION: $desc", 'MODULE_REQUIREMENTS: vin_Base', 'MODULE_VERSION: 1.0', "MODULE_CREATED: $(Get-Date -Format 'yyyy-MM-dd')", 'MODULE_LICENSE: CC0', 'MODULE_AUTHOR_LINK: https://quaternius.com', 'MODULE_PRICE: Free', 'MODULE_DEFAULT_ENABLED: true') New-IfMissing (Join-Path $src "Module\$Name.lpmod") (($lpmod -join "`r`n") + "`r`n") $credits = "Credits and licenses of the assets in Quaternius Animations (one line per asset: title, author, license, source URL, changes).`r`n" + "- Universal Animation Library by Quaternius, CC0 1.0, https://quaternius.com . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed qua__F / qua__M.`r`n" New-IfMissing (Join-Path $src 'Module\CREDITS.txt') $credits New-Item -ItemType Directory -Force (Join-Path $src 'Content') | Out-Null # Lancement d'Unreal, mod monte. $logDir = Join-Path $Root 'Saved\Logs\portage' New-Item -ItemType Directory -Force $logDir | Out-Null $log = Join-Path $logDir ('AP5_DeliverAnims_{0}.log' -f (Get-Date -Format 'yyyyMMdd-HHmmss')) $py = (Join-Path $Root 'Tools\anim\DeliverAnims.py') -replace '\\', '/' $mounted = Mount-ModSource $Root $Name try { $script = "-script=$py mod=$Name src=$Source prefix=$Prefix exclude=$Exclude" Write-Host "Livraison dans /$Name/ (journal : $log)" & $Editor (Join-Path $Root 'LifePlay.uproject') -run=pythonscript $script -unattended -nopause -nosplash -stdout -FullStdOutLogOutput *> $log } finally { Dismount-ModSource $Root $Name } # Controle de sortie. $n = $table.Count if (-not (Select-String -Path $log -Pattern "LIVRAISON copies $($n * 2) erreurs 0\b" -Quiet)) { $last = Select-String -Path $log -Pattern 'LIVRAISON' | Select-Object -Last 3 | ForEach-Object { $_.Line } throw "Ligne 'LIVRAISON copies $($n * 2) erreurs 0' absente : $($last -join ' | ') (voir $log)" } foreach ($sexe in 'Female', 'Male') { $dir = Join-Path $src "Content\Anims\$sexe" $count = @(Get-ChildItem $dir -Filter '*.uasset' -ErrorAction SilentlyContinue).Count if ($count -ne $n) { throw "$dir : $count fichier(s) .uasset pour $n cle(s)" } } Write-Host "Livraison terminee : $n cle(s), $($n * 2) copies" exit 0 } catch { Write-Host "ECHEC : $($_.Exception.Message)" -ForegroundColor Red exit 1 }