214 lines
12 KiB
PowerShell
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
|
|
}
|