# Livraison des clips d'animation convertis dans un mod d'assets (lot AP5). # Lancement : # UnrealEditor-Cmd.exe -run=pythonscript "-script=/Tools/anim/DeliverAnims.py mod= src=/Game/Quaternius/UAL prefix=qua exclude=A_TPose pipeline=AnimPipeline/quaternius" # Copie /B_ -> //Anims/Female/__F # /M_ -> //Anims/Male/__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)))