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

136 lines
5.7 KiB
Python

# Livraison des clips d'animation convertis dans un mod d'assets (lot AP5).
# Lancement :
# UnrealEditor-Cmd.exe <projet> -run=pythonscript "-script=<chemin>/Tools/anim/DeliverAnims.py mod=<Nom> src=/Game/Quaternius/UAL prefix=qua exclude=A_TPose pipeline=AnimPipeline/quaternius"
# Copie <src>/B_<Clip> -> /<mod>/Anims/Female/<prefix>_<clip>_F
# <src>/M_<Clip> -> /<mod>/Anims/Male/<prefix>_<clip>_M (src = dossier des clips convertis B_/M_)
# Les sources ne sont jamais deplacees ni renommees. Seules des copies du mod peuvent etre supprimees.
import json
import os
import re
import sys
import unreal
# Arguments : mod=... src=... prefix=... exclude=A,B (le script peut recevoir les arguments en un seul bloc)
# pipeline= : dossier de livraison.json, relatif a Saved/ (ou absolu) ; sans espace (arguments decoupes sur les espaces).
args = {"mod": "lpr_anim_quaternius", "src": "/Game/Quaternius/UAL", "prefix": "qua", "exclude": "A_TPose",
"pipeline": "AnimPipeline/quaternius"}
tokens = []
for a in sys.argv[1:]:
tokens.extend(a.split())
for t in tokens:
if "=" in t:
k, v = t.split("=", 1)
args[k.strip().lower()] = v.strip()
MOD = args["mod"]
SRC = args["src"].rstrip("/")
PREFIX = args["prefix"]
EXCLUDE = set(x for x in args["exclude"].split(",") if x)
erreurs = []
livraison = []
def erreur(msg):
erreurs.append(msg)
unreal.log_error("LIVRAISON erreur : " + msg)
def lister(dossier, debut):
noms = set()
for p in unreal.EditorAssetLibrary.list_assets(dossier, recursive=False, include_folder=False):
nom = p.split("/")[-1].split(".")[0]
if nom.startswith(debut):
noms.add(nom[len(debut):])
return noms
def infos(asset):
return asset.get_skeleton(), asset.get_editor_property("number_of_sampled_keys"), asset.get_editor_property("sequence_length")
# Garde-fou : on ne supprime et n'ecrit que dans le mod.
def sous_mod(chemin):
return chemin.startswith("/" + MOD + "/")
# En commandlet, le registre n'a pas encore explore le dossier du mod : sans ce scan, les copies d'une passe
# precedente sont ignorees par does_asset_exist et duplicate_asset echoue (AP5, deuxieme passe).
unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous(["/" + MOD], True)
clips = sorted((lister(SRC, "B_") & lister(SRC, "M_")) - EXCLUDE)
unreal.log("LIVRAISON %d clip(s) a livrer vers /%s/" % (len(clips), MOD))
for clip in clips:
cle = PREFIX + "_" + clip.lower()
if not re.match(r"^[a-z0-9_]+$", cle):
erreur("cle invalide : " + cle)
continue
sources = {"F": SRC + "/B_" + clip, "M": SRC + "/M_" + clip}
dests = {"F": "/%s/Anims/Female/%s_F" % (MOD, cle), "M": "/%s/Anims/Male/%s_M" % (MOD, cle)}
ref = {}
ok = True
for sexe in ("F", "M"):
s, d = sources[sexe], dests[sexe]
if not sous_mod(d):
erreur("destination hors du mod : " + d)
ok = False
continue
asrc = unreal.EditorAssetLibrary.load_asset(s)
if not isinstance(asrc, unreal.AnimSequence):
erreur("source absente ou pas une AnimSequence : " + s)
ok = False
continue
if unreal.EditorAssetLibrary.does_asset_exist(d):
if not unreal.EditorAssetLibrary.delete_asset(d):
erreur("suppression de la copie precedente impossible : " + d)
ok = False
continue
copie = unreal.EditorAssetLibrary.duplicate_asset(s, d)
if copie is None:
erreur("copie impossible : %s -> %s" % (s, d))
ok = False
continue
# Un mod DLC ne peut pas referencer du contenu moteur non cuit : les clips d'un pack UE5 pointent
# explicitement sur /Engine/Animation/DefaultAnim*CompressionSettings (cuisson refusee, AP7). Sans
# reglage (None), la compression par defaut du projet s'applique, comme pour les clips Quaternius.
for prop in ("bone_compression_settings", "curve_compression_settings"):
valeur = copie.get_editor_property(prop)
if valeur is not None and valeur.get_path_name().startswith("/Engine/"):
copie.set_editor_property(prop, None)
if not unreal.EditorAssetLibrary.save_asset(d, only_if_is_dirty=False):
erreur("enregistrement impossible : " + d)
ok = False
continue
# Controles : existe, AnimSequence, meme squelette, meme nombre d'images, meme duree.
acopie = unreal.EditorAssetLibrary.load_asset(d)
if not unreal.EditorAssetLibrary.does_asset_exist(d) or not isinstance(acopie, unreal.AnimSequence):
erreur("copie absente ou pas une AnimSequence : " + d)
ok = False
continue
sk0, n0, l0 = infos(asrc)
sk1, n1, l1 = infos(acopie)
if sk0 != sk1:
erreur("squelette different : " + d)
ok = False
if n0 != n1:
erreur("nombre d'images different (%s / %s) : %s" % (n0, n1, d))
ok = False
if abs(l0 - l1) > 1e-4:
erreur("duree differente (%s / %s) : %s" % (l0, l1, d))
ok = False
ref[sexe] = (n1, l1)
if ok:
livraison.append({
"cle": cle,
"source_F": sources["F"], "source_M": sources["M"],
"asset_F": dests["F"], "asset_M": dests["M"],
"images": ref["F"][0], "duree_s": ref["F"][1],
})
sortie = os.path.join(unreal.Paths.project_saved_dir(), args["pipeline"], "livraison.json") # join : absolu prime
os.makedirs(os.path.dirname(sortie), exist_ok=True)
with open(sortie, "w", encoding="utf-8") as f:
json.dump(livraison, f, indent=2, ensure_ascii=False)
unreal.log("LIVRAISON copies %d erreurs %d" % (len(livraison) * 2, len(erreurs)))