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

214 lines
12 KiB
PowerShell

<#
.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_<Clip> -> /<Nom>/Anims/Female/qua_<clip>_F ; M_<Clip> -> /<Nom>/Anims/Male/qua_<clip>_M.
Le jeu appelle Actor.animate(<cle>) et cherche <cle>_F et <cle>_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 le dossier disque de -Source (/Game/X/Y -> Content\X\Y), B_*.uasset et M_*.uasset ;
4. -DryRun : affiche la table et s'arrete (rien n'est lance ni cree) ;
5. sinon : cree ModSources\<Nom> s'il manque (jamais d'ecrasement), monte le mod, lance Tools\anim\DeliverAnims.py
dans UnrealEditor-Cmd (journal Saved\Logs\portage\AP5_DeliverAnims_<horodatage>.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.
-Catalogue : apres une livraison reussie, lance Tools\anim\Catalogue.py (lot AP6).
-CatalogueOnly : saute la copie et ne produit que le catalogue (rapport valide exige), sans lancer Unreal.
Parametres (defaut = pack Quaternius) : -Pipeline (dossier des rapports, defaut de -Report, lieu de livraison.json ; sous Saved\),
-Pack (pack.json ; donne aussi, quand il est passe explicitement, les textes du mod cree : source, auteur, licence, credit, url ;
refus si licence, credit ou auteur est vide ou "A COMPLETER" ; sans -Pack, textes Quaternius d'origine), -Vignettes / -Planches (dossiers transmis a Catalogue.py), -Source (clips convertis B_/M_), -Name, -Prefix, -Exclude.
.EXAMPLE
.\Tools\anim\DeliverAnims.ps1 -DryRun
.\Tools\anim\DeliverAnims.ps1 -DryRun -Pipeline Saved\AnimPipeline\freeanims -Source /Game/AnimPacks/FreeAnimationsPack -Name lpr_anim_freeanims -Prefix fap -Exclude ''
.\Tools\anim\DeliverAnims.ps1
#>
param(
[string] $Name = 'lpr_anim_quaternius',
[string] $Source = '/Game/Quaternius/UAL',
[string] $Prefix = 'qua',
[string] $Exclude = 'A_TPose',
[string] $Report = '',
[string] $Pipeline = 'Saved\AnimPipeline\quaternius',
[string] $Pack = 'AnimSources\quaternius\pack.json',
[string] $Vignettes = '',
[string] $Planches = '',
[switch] $DryRun,
[switch] $Catalogue,
[switch] $CatalogueOnly
)
$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'
# -Pipeline : dossier des rapports du pack (relatif a la racine du projet, ou absolu) ; defaut de -Report, lieu de livraison.json.
# -Report seul (sans -Pipeline) : le dossier du rapport fait office de dossier de pipeline (comportement d'avant).
if ($Report -and -not $PSBoundParameters.ContainsKey('Pipeline')) { $Pipeline = Split-Path $Report -Parent }
if (-not [IO.Path]::IsPathRooted($Pipeline)) { $Pipeline = Join-Path $Root $Pipeline }
$Pipeline = $Pipeline.TrimEnd('\', '/')
if (-not $Report) { $Report = Join-Path $Pipeline 'rapport.json' }
if (-not [IO.Path]::IsPathRooted($Pack)) { $Pack = Join-Path $Root $Pack }
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"
}
# Lot AP6 : catalogue du mod (Tools\anim\Catalogue.py), version lue dans le .lpmod.
function Invoke-Catalogue {
$lpmodPath = Join-Path $Root "ModSources\$Name\Module\$Name.lpmod"
if (-not (Test-Path $lpmodPath)) { throw "lpmod introuvable : $lpmodPath" }
$line = Select-String -Path $lpmodPath -Pattern '^MODULE_VERSION:\s*(\S+)' | Select-Object -First 1
if (-not $line) { throw "MODULE_VERSION absente de $lpmodPath" }
$version = $line.Matches[0].Groups[1].Value
$out = Join-Path $Root "ModSources\$Name\Module\Animations"
Write-Host "Catalogue : version $version -> $out"
$extra = @()
if ($Vignettes) { $extra += @('--vignettes', $Vignettes) }
if ($Planches) { $extra += @('--planches', $Planches) }
& python (Join-Path $Root 'Tools\anim\Catalogue.py') --pack $Pack `
--pipeline $Pipeline --mod $Name --version $version --out $out @extra
if ($LASTEXITCODE -ne 0) { throw "Catalogue.py a echoue (code $LASTEXITCODE)" }
}
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" }
if ($CatalogueOnly -and -not $DryRun) {
Invoke-Catalogue
exit 0
}
# 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). Textes : pack.json si -Pack est donne, sinon ceux de Quaternius.
$paths = Get-ModSourcePaths $Root $Name
$src = $paths.Source
$title = 'Quaternius Animations'
$desc = 'Free animation clips from the Quaternius Universal Animation Library (CC0), retargeted for LifePlay'
$author = 'LifePlay Revival (animations by Quaternius)'
$licence = 'CC0'
$link = 'https://quaternius.com'
$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_<clip>_F / qua_<clip>_M.`r`n"
if ($PSBoundParameters.ContainsKey('Pack')) {
$pk = Get-Content $Pack -Raw -Encoding utf8 | ConvertFrom-Json
foreach ($k in 'source', 'auteur', 'licence', 'credit') {
if (-not ([string] $pk.$k).Trim() -or ([string] $pk.$k) -match 'A COMPLETER') { throw "pack.json : '$k' vide ou 'A COMPLETER' (mod non cree)" }
}
$title = [string] $pk.source
$desc = "Free animation clips from $($pk.source) ($($pk.licence)), retargeted for LifePlay"
$author = "LifePlay Revival (animations by $($pk.auteur))"
$licence = [string] $pk.licence
# Lien de l'auteur : url_auteur (page de l'auteur) s'il est donne, sinon url (page du pack).
$authorUrl = if ($pk.PSObject.Properties['url_auteur']) { [string] $pk.url_auteur } else { [string] $pk.url }
$link = if ($authorUrl.Trim() -and $authorUrl -notmatch 'A COMPLETER') { $authorUrl } else { '' }
$credits = "Credits and licenses of the assets in $title (one line per asset: title, author, license, source URL, changes).`r`n" +
"- $($pk.credit)$(if ($pk.url -and ([string] $pk.url) -notmatch 'A COMPLETER') { ', ' + $pk.url }) . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed $($pk.prefixe)_<clip>_F / $($pk.prefixe)_<clip>_M.`r`n"
}
$uplugin = [ordered]@{
FileVersion = 3; Version = 1; VersionName = '1.0'; FriendlyName = $title
Description = $desc; Category = 'LifePlay Mods'; CreatedBy = $author
CanContainContent = $true; IsBetaVersion = $false; Installed = $false
}
if ($link) { $uplugin.CreatedByURL = $link }
New-IfMissing (Join-Path $src "$Name.uplugin") (($uplugin | ConvertTo-Json) + "`r`n")
$lpmod = @(
"MODULE_UNIQUEID: $Name", "MODULE_NAME: $title", "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: $licence") + @($(if ($link) { "MODULE_AUTHOR_LINK: $link" })) + @('MODULE_PRICE: Free', 'MODULE_DEFAULT_ENABLED: true')
New-IfMissing (Join-Path $src "Module\$Name.lpmod") (($lpmod -join "`r`n") + "`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 {
# Le script Python decoupe ses arguments sur les espaces : pipeline= est donne relatif a Saved\ (sans espace).
$savedDir = (Join-Path $Root 'Saved').TrimEnd('\') + '\'
if (-not $Pipeline.StartsWith($savedDir, [StringComparison]::OrdinalIgnoreCase)) { throw "-Pipeline doit etre sous $savedDir (recu : $Pipeline)" }
$pipeRel = $Pipeline.Substring($savedDir.Length) -replace '\\', '/'
if ($pipeRel -match '\s') { throw "-Pipeline ne doit pas contenir d'espace sous Saved\ (recu : $pipeRel)" }
$script = "-script=$py mod=$Name src=$Source prefix=$Prefix exclude=$Exclude pipeline=$pipeRel"
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"
if ($Catalogue) { Invoke-Catalogue }
exit 0
}
catch {
Write-Host "ECHEC : $($_.Exception.Message)" -ForegroundColor Red
exit 1
}