<# .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 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\ 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. -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__F / qua__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)__F / $($pk.prefixe)__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 }