From 5fc30338963ea8705e7dcc2dea3a18f353fd572d Mon Sep 17 00:00:00 2001 From: Carl Roach <38408082+BoldPhoenix@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:30:28 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat(tools):=20reference-mocap=20front=20en?= =?UTF-8?q?d=20=E2=80=94=20import=5Fmocap=20+=20retarget=5Fanim=20(IK=20Re?= =?UTF-8?q?targeter,=20verified=20live)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Front end of the reference-driven motion pipeline (REFERENCE_MOCAP_PIPELINE.md, #6): video -> [DeepMotion/Move.ai] -> import_mocap -> retarget_anim -> cr_bake_anim -> seq_keyframe -> MRQ. Turns the "freehand performance is the wall" lesson into a capability: capture real motion, retarget, then clean — converting a feel problem into a data problem. - import_mocap: import a mocap FBX (DeepMotion Animate 3D / Move.ai) as source mesh+skeleton+anim, or anim-only against an existing skeleton; classifies the imports and returns the paths for retarget_anim. - retarget_anim: the core tool. retargeter=None auto-authors an IK Rig per skeleton (humanoid-template auto chains + FBIK via apply_auto_generated_retarget_definition / apply_auto_fbik) and an IK Retargeter (set_ik_rig SOURCE/TARGET + auto_map_chains + auto_align_all_bones), then runs the batch retarget. Character-agnostic; pass a pre-authored IKRetargeter to override for stylised rigs (Buck = stocky ogre, residual proportion fixes then live in seq_keyframe). Verified live on UE5.8: a Buck->Buck retarget of Buck_Playing_Guitar produced a real AnimSequence end-to-end (both IK-rig templates matched, chains auto-mapped). Uses the non-deprecated run_batch_retarget(IKRetargetBatchOperationInputs) + EditorAssetLibrary.find_asset_data (duplicate_and_retarget is deprecated and rejects loaded assets — the struct wants AssetData). Open item (needs a real source FBX + Carl's DeepMotion account): full cross-skeleton end-to-end on an actual air-guitar reference, then bake -> tweak -> render as the pipeline's unit test. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FhGE51iZAN4bvfTGHzBkCN --- src/unreal_mcp/server.py | 23 +++++ src/unreal_mcp/snippets/retarget.py | 155 ++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/unreal_mcp/snippets/retarget.py diff --git a/src/unreal_mcp/server.py b/src/unreal_mcp/server.py index 22da15f..313ba0a 100644 --- a/src/unreal_mcp/server.py +++ b/src/unreal_mcp/server.py @@ -33,6 +33,7 @@ live_coding as _livecoding, move_actor as _move, open_level as _open_level, + retarget as _retarget, run_console_command as _console, save_level as _save_level, seq_keyframe as _seqkey, @@ -749,6 +750,28 @@ def cr_list_controls(sequence_path: str) -> dict: return client.run_snippet(_crseq.build_cr_list_controls(sequence_path)) +# --- Reference-driven motion: mocap import + retarget (front end of REFERENCE_MOCAP_PIPELINE.md) --- +@mcp.tool() +def import_mocap(fbx_path: str, destination_path: str = "/Game/Mocap", skeleton: str | None = None, + import_mesh: bool = True, replace_existing: bool = True, save: bool = True) -> dict: + """Import a mocap FBX (e.g. a DeepMotion Animate 3D export) for the reference-motion pipeline. + skeleton=None -> full import (source SkeletalMesh + Skeleton + AnimSequence); skeleton='/Game/...' -> + anim-only import bound to that skeleton. Returns classified {source_mesh, source_skeleton, anim} paths + to feed retarget_anim. (Free-tier DeepMotion output is non-commercial: R&D/test only; ship on a paid plan.)""" + return client.run_snippet(_retarget.build_import_mocap(fbx_path, destination_path, skeleton, import_mesh, replace_existing, save)) + + +@mcp.tool() +def retarget_anim(source_anim: str, source_mesh: str, target_mesh: str, destination_path: str = "/Game/Mocap", + retargeter: str | None = None, name: str | None = None, save: bool = True) -> dict: + """Retarget an AnimSequence from source_mesh onto target_mesh (e.g. DeepMotion human -> Buck) - the core + front-end tool. retargeter=None AUTO-authors an IK Rig per skeleton (humanoid-template auto chains + + FBIK) and an IK Retargeter (auto chain-map + bone-align), then runs the one-call batch retarget. Pass a + pre-authored IKRetargeter path to override for stylised rigs. Returns the retargeted AnimSequence path, + ready for cr_bake_anim -> seq_keyframe -> render.""" + return client.run_snippet(_retarget.build_retarget_anim(source_anim, source_mesh, target_mesh, destination_path, retargeter, name, save)) + + def main() -> None: """Run the server over stdio (transport is implicit when an MCP client launches us).""" mcp.run() diff --git a/src/unreal_mcp/snippets/retarget.py b/src/unreal_mcp/snippets/retarget.py new file mode 100644 index 0000000..61da0bc --- /dev/null +++ b/src/unreal_mcp/snippets/retarget.py @@ -0,0 +1,155 @@ +"""Reference-driven motion: import external mocap (DeepMotion / Move.ai FBX) and RETARGET it onto any +character skeleton, fully headless. The FRONT END of the reference-mocap pipeline (REFERENCE_MOCAP_PIPELINE.md): + + video -> [DeepMotion/Move.ai: external] -> import_mocap -> retarget_anim -> cr_bake_anim -> seq_keyframe -> MRQ render + +WHY: freehand performance authoring is the wall (timing/coordination can't be authored blind). Real footage +encodes that timing; mocap converts a *feel* problem into a *data* problem. The back half (cr_bake_anim + +seq_keyframe + MRQ) already shipped; this is the missing front end. + +retarget_anim AUTO-AUTHORS the IK Rigs + IK Retargeter when none is supplied: it analyses each skeleton +against UE's known humanoid templates (apply_auto_generated_retarget_definition / apply_auto_fbik) and +string-maps the chains (auto_map_chains) -> works for ANY humanoid source->target pair with no hand +authoring. Supply a pre-authored IKRetargeter asset to override the auto path for stylised rigs whose +chains need hand tuning (Buck = stocky ogre; residual proportion fixes then happen in seq_keyframe). + +PROVEN live on UE5.8 (2026-06-23) - a Buck->Buck retarget of Buck_Playing_Guitar produced a real +AnimSequence. IKRetargetBatchOperation.run_batch_retarget(IKRetargetBatchOperationInputs) is the one-call +batch (duplicate_and_retarget is DEPRECATED and rejects loaded assets - the inputs struct wants AssetData, +fetched via EditorAssetLibrary.find_asset_data). IKRigController.get_controller(rig).set_skeletal_mesh + +apply_auto_generated_retarget_definition + apply_auto_fbik auto-build a rig; IKRetargeterController +set_ik_rig(SOURCE/TARGET) + auto_map_chains + auto_align_all_bones configure the map. Factories: +IKRigDefinitionFactory, IKRetargetFactory. +""" + +import json + +from unreal_mcp.snippets import wrap + + +def build_import_mocap(fbx_path, destination_path="/Game/Mocap", skeleton=None, + import_mesh=True, replace_existing=True, save=True) -> str: + """Import a mocap FBX (e.g. a DeepMotion Animate 3D export). skeleton=None -> full import (brings the + source SkeletalMesh + Skeleton + AnimSequence, the usual first import of a new mocap rig). skeleton= + '/Game/...': anim-ONLY import bound to that existing skeleton. Classifies + returns the mesh / skeleton + / anim object paths so retarget_anim can consume them directly.""" + body = ( + "import os\n" + f"src = {json.dumps(fbx_path)}\n" + f"dst = {json.dumps(destination_path)}\n" + f"SKEL = {json.dumps(skeleton)}\n" + f"IMPORT_MESH = {bool(import_mesh)!r}\n" + f"replace = {bool(replace_existing)!r}\n" + f"do_save = {bool(save)!r}\n" + "try:\n" + " if not os.path.exists(src):\n" + " _emit({'error': 'source FBX not found on disk', 'source': src})\n" + " else:\n" + " task = unreal.AssetImportTask()\n" + " task.set_editor_property('filename', src)\n" + " task.set_editor_property('destination_path', dst)\n" + " task.set_editor_property('automated', True)\n" + " task.set_editor_property('replace_existing', replace)\n" + " task.set_editor_property('save', do_save)\n" + " ui = unreal.FbxImportUI()\n" + " ui.set_editor_property('import_as_skeletal', True)\n" + " ui.set_editor_property('import_animations', True)\n" + " ui.set_editor_property('import_materials', False)\n" + " ui.set_editor_property('import_textures', False)\n" + " if SKEL:\n" + " ui.set_editor_property('skeleton', unreal.load_asset(SKEL))\n" + " ui.set_editor_property('import_mesh', False)\n" + " ui.set_editor_property('mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)\n" + " else:\n" + " ui.set_editor_property('import_mesh', IMPORT_MESH)\n" + " ui.set_editor_property('mesh_type_to_import', unreal.FBXImportType.FBXIT_SKELETAL_MESH)\n" + " task.set_editor_property('options', ui)\n" + " unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n" + " paths = [str(p) for p in (task.get_editor_property('imported_object_paths') or [])]\n" + " mesh = skel = anim = None; others = []\n" + " for p in paths:\n" + " a = unreal.load_asset(p)\n" + " if isinstance(a, unreal.SkeletalMesh): mesh = mesh or p\n" + " elif isinstance(a, unreal.Skeleton): skel = skel or p\n" + " elif isinstance(a, unreal.AnimSequence): anim = anim or p\n" + " else: others.append(p)\n" + " if skel is None and mesh is not None:\n" + " m = unreal.load_asset(mesh); sk = m.get_editor_property('skeleton') if m else None\n" + " skel = sk.get_path_name() if sk else None\n" + " _emit({'imported': paths, 'count': len(paths), 'source_mesh': mesh, 'source_skeleton': skel,\n" + " 'anim': anim, 'others': others, 'destination': dst,\n" + " 'next': 'retarget_anim(source_anim=anim, source_mesh=source_mesh, target_mesh=)'})\n" + "except Exception as e:\n" + " _emit({'error': repr(e)})\n" + ) + return wrap(body) + + +def build_retarget_anim(source_anim, source_mesh, target_mesh, destination_path="/Game/Mocap", + retargeter=None, name=None, save=True) -> str: + """Retarget an AnimSequence from a source skeletal mesh onto a target one (e.g. DeepMotion human -> + Buck). retargeter=None AUTO-authors an IK Rig per mesh (humanoid-template auto chains + FBIK) and an + IK Retargeter (auto chain-map + bone-align), then runs the batch retarget. Pass a pre-authored + IKRetargeter asset path to override the auto path. Returns the new (retargeted) AnimSequence path.""" + body = ( + f"SRC_ANIM = {json.dumps(source_anim)}\n" + f"SRC_MESH = {json.dumps(source_mesh)}\n" + f"TGT_MESH = {json.dumps(target_mesh)}\n" + f"DEST = {json.dumps(destination_path)}\n" + f"RT = {json.dumps(retargeter)}\n" + f"NAME = {json.dumps(name)}\n" + "try:\n" + " anim = unreal.load_asset(SRC_ANIM)\n" + " smesh = unreal.load_asset(SRC_MESH)\n" + " tmesh = unreal.load_asset(TGT_MESH)\n" + " if anim is None or smesh is None or tmesh is None:\n" + " _emit({'error': 'source_anim / source_mesh / target_mesh not found',\n" + " 'source_anim': SRC_ANIM, 'source_mesh': SRC_MESH, 'target_mesh': TGT_MESH})\n" + " else:\n" + " at = unreal.AssetToolsHelpers.get_asset_tools()\n" + " base = NAME or anim.get_name()\n" + " authored = {}; src_auto = tgt_auto = None; rt_asset = None\n" + " if RT:\n" + " rt_asset = unreal.load_asset(RT)\n" + " else:\n" + " src_rig = at.create_asset(base + '_SrcRig', DEST, unreal.IKRigDefinition, unreal.IKRigDefinitionFactory())\n" + " rc = unreal.IKRigController.get_controller(src_rig)\n" + " rc.set_skeletal_mesh(smesh); src_auto = bool(rc.apply_auto_generated_retarget_definition()); rc.apply_auto_fbik()\n" + " tgt_rig = at.create_asset(base + '_TgtRig', DEST, unreal.IKRigDefinition, unreal.IKRigDefinitionFactory())\n" + " tc = unreal.IKRigController.get_controller(tgt_rig)\n" + " tc.set_skeletal_mesh(tmesh); tgt_auto = bool(tc.apply_auto_generated_retarget_definition()); tc.apply_auto_fbik()\n" + " rt_asset = at.create_asset(base + '_RT', DEST, unreal.IKRetargeter, unreal.IKRetargetFactory())\n" + " rtc = unreal.IKRetargeterController.get_controller(rt_asset)\n" + " rtc.set_ik_rig(unreal.RetargetSourceOrTarget.SOURCE, src_rig)\n" + " rtc.set_ik_rig(unreal.RetargetSourceOrTarget.TARGET, tgt_rig)\n" + " rtc.auto_map_chains(unreal.AutoMapChainType.FUZZY, True)\n" + " rtc.auto_align_all_bones(unreal.RetargetSourceOrTarget.SOURCE)\n" + " rtc.auto_align_all_bones(unreal.RetargetSourceOrTarget.TARGET)\n" + " for a in (src_rig, tgt_rig, rt_asset): unreal.EditorAssetLibrary.save_loaded_asset(a, False)\n" + " authored = {'source_ik_rig': src_rig.get_path_name(), 'target_ik_rig': tgt_rig.get_path_name(), 'retargeter': rt_asset.get_path_name()}\n" + " if rt_asset is None:\n" + " _emit({'error': 'retargeter not found / not created', 'retargeter': RT})\n" + " else:\n" + " suffix = '_' + tmesh.get_name()\n" + " inp = unreal.IKRetargetBatchOperationInputs()\n" + " inp.set_editor_property('assets_to_retarget', [unreal.EditorAssetLibrary.find_asset_data(anim.get_path_name())])\n" + " inp.set_editor_property('source_mesh', smesh)\n" + " inp.set_editor_property('target_mesh', tmesh)\n" + " inp.set_editor_property('ik_retarget_asset', rt_asset)\n" + " inp.set_editor_property('suffix', suffix)\n" + " inp.set_editor_property('target_path', DEST)\n" + " inp.set_editor_property('overwrite_existing_files', True)\n" + " res = unreal.IKRetargetBatchOperation.run_batch_retarget(inp)\n" + " outs = []\n" + " for ad in (res or []):\n" + " try: outs.append(str(ad.package_name) + '.' + str(ad.asset_name))\n" + " except Exception: outs.append(str(ad))\n" + " _emit({'retargeted_anim': outs[0] if outs else None, 'all_outputs': outs, 'count': len(outs),\n" + " 'source_template_matched': src_auto, 'target_template_matched': tgt_auto,\n" + " 'authored_assets': authored, 'retargeter_used': rt_asset.get_path_name(),\n" + " 'next': 'cr_bake_anim(sequence, target_actor, retargeted_anim) -> editable rig -> seq_keyframe -> render',\n" + " 'note': 'template_matched False => skeleton did not match a UE humanoid template; hand-author chains or pass a retargeter asset.'})\n" + "except Exception as e:\n" + " _emit({'error': repr(e)})\n" + ) + return wrap(body) From 577d058ee2fdc019016623ce94b6df937a012835 Mon Sep 17 00:00:00 2001 From: Carl Roach <38408082+BoldPhoenix@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:23:04 -0500 Subject: [PATCH 2/3] =?UTF-8?q?fix(retarget):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20re-run=20collisions,=20zero-asset=20guard,=20traceb?= =?UTF-8?q?ack,=20honest=20"verified"=20wording?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Wally's review of #7 (approve w/ minor notes): - retarget_anim auto-author path is now re-run safe: a _mk(name, cls, factory) helper delete-if-exists before create_asset, so a second run with the same source anim no longer hits create_asset->None-> get_controller(None)->throw. VERIFIED: ran the auto-author+retarget twice back-to-back, both emit an AnimSequence (the 2nd run was the collision case). - import_mocap now guards a zero-asset import: `if not paths` emits an explicit error instead of a success-shaped {anim: None} that would feed retarget_anim(source_anim=None) downstream. - retarget_anim's except now attaches traceback.format_exc() tail — this is the most multi-step, UE-API-heavy tool in the repo, so debuggability beats strict repr(e) house style here (import_mocap keeps repr(e)). - Softened the module docstring's "PROVEN" -> "VERIFIED ... PLUMBING only": Buck->Buck proves the auto-author + batch chain, NOT the cross-skeleton retarget math (same skeleton = trivial chain map). The human->stocky-Buck case remains the pending unit test, gated on a real DeepMotion FBX. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FhGE51iZAN4bvfTGHzBkCN --- src/unreal_mcp/snippets/retarget.py | 49 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/unreal_mcp/snippets/retarget.py b/src/unreal_mcp/snippets/retarget.py index 61da0bc..3d0770b 100644 --- a/src/unreal_mcp/snippets/retarget.py +++ b/src/unreal_mcp/snippets/retarget.py @@ -13,8 +13,11 @@ authoring. Supply a pre-authored IKRetargeter asset to override the auto path for stylised rigs whose chains need hand tuning (Buck = stocky ogre; residual proportion fixes then happen in seq_keyframe). -PROVEN live on UE5.8 (2026-06-23) - a Buck->Buck retarget of Buck_Playing_Guitar produced a real -AnimSequence. IKRetargetBatchOperation.run_batch_retarget(IKRetargetBatchOperationInputs) is the one-call +VERIFIED live on UE5.8 (2026-06-23): a Buck->Buck retarget of Buck_Playing_Guitar ran the full chain +(auto-author both IK rigs -> auto-map -> run_batch_retarget) and emitted a real AnimSequence. NOTE this +proves the PLUMBING only - same skeleton on both ends makes the chain map trivial, so the cross-skeleton +retarget MATH (mocap human -> stocky Buck) is still unproven; that's the pending unit test, gated on a real +DeepMotion FBX. IKRetargetBatchOperation.run_batch_retarget(IKRetargetBatchOperationInputs) is the one-call batch (duplicate_and_retarget is DEPRECATED and rejects loaded assets - the inputs struct wants AssetData, fetched via EditorAssetLibrary.find_asset_data). IKRigController.get_controller(rig).set_skeletal_mesh + apply_auto_generated_retarget_definition + apply_auto_fbik auto-build a rig; IKRetargeterController @@ -66,19 +69,22 @@ def build_import_mocap(fbx_path, destination_path="/Game/Mocap", skeleton=None, " task.set_editor_property('options', ui)\n" " unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n" " paths = [str(p) for p in (task.get_editor_property('imported_object_paths') or [])]\n" - " mesh = skel = anim = None; others = []\n" - " for p in paths:\n" - " a = unreal.load_asset(p)\n" - " if isinstance(a, unreal.SkeletalMesh): mesh = mesh or p\n" - " elif isinstance(a, unreal.Skeleton): skel = skel or p\n" - " elif isinstance(a, unreal.AnimSequence): anim = anim or p\n" - " else: others.append(p)\n" - " if skel is None and mesh is not None:\n" - " m = unreal.load_asset(mesh); sk = m.get_editor_property('skeleton') if m else None\n" - " skel = sk.get_path_name() if sk else None\n" - " _emit({'imported': paths, 'count': len(paths), 'source_mesh': mesh, 'source_skeleton': skel,\n" - " 'anim': anim, 'others': others, 'destination': dst,\n" - " 'next': 'retarget_anim(source_anim=anim, source_mesh=source_mesh, target_mesh=)'})\n" + " if not paths:\n" + " _emit({'error': 'import produced no assets - check the FBX path and skeleton compatibility', 'source': src, 'destination': dst})\n" + " else:\n" + " mesh = skel = anim = None; others = []\n" + " for p in paths:\n" + " a = unreal.load_asset(p)\n" + " if isinstance(a, unreal.SkeletalMesh): mesh = mesh or p\n" + " elif isinstance(a, unreal.Skeleton): skel = skel or p\n" + " elif isinstance(a, unreal.AnimSequence): anim = anim or p\n" + " else: others.append(p)\n" + " if skel is None and mesh is not None:\n" + " m = unreal.load_asset(mesh); sk = m.get_editor_property('skeleton') if m else None\n" + " skel = sk.get_path_name() if sk else None\n" + " _emit({'imported': paths, 'count': len(paths), 'source_mesh': mesh, 'source_skeleton': skel,\n" + " 'anim': anim, 'others': others, 'destination': dst,\n" + " 'next': 'retarget_anim(source_anim=anim, source_mesh=source_mesh, target_mesh=)'})\n" "except Exception as e:\n" " _emit({'error': repr(e)})\n" ) @@ -109,16 +115,20 @@ def build_retarget_anim(source_anim, source_mesh, target_mesh, destination_path= " at = unreal.AssetToolsHelpers.get_asset_tools()\n" " base = NAME or anim.get_name()\n" " authored = {}; src_auto = tgt_auto = None; rt_asset = None\n" + " def _mk(nm, cls, fac):\n" + " p = DEST + '/' + nm\n" + " if unreal.EditorAssetLibrary.does_asset_exist(p): unreal.EditorAssetLibrary.delete_asset(p)\n" + " return at.create_asset(nm, DEST, cls, fac)\n" " if RT:\n" " rt_asset = unreal.load_asset(RT)\n" " else:\n" - " src_rig = at.create_asset(base + '_SrcRig', DEST, unreal.IKRigDefinition, unreal.IKRigDefinitionFactory())\n" + " src_rig = _mk(base + '_SrcRig', unreal.IKRigDefinition, unreal.IKRigDefinitionFactory())\n" " rc = unreal.IKRigController.get_controller(src_rig)\n" " rc.set_skeletal_mesh(smesh); src_auto = bool(rc.apply_auto_generated_retarget_definition()); rc.apply_auto_fbik()\n" - " tgt_rig = at.create_asset(base + '_TgtRig', DEST, unreal.IKRigDefinition, unreal.IKRigDefinitionFactory())\n" + " tgt_rig = _mk(base + '_TgtRig', unreal.IKRigDefinition, unreal.IKRigDefinitionFactory())\n" " tc = unreal.IKRigController.get_controller(tgt_rig)\n" " tc.set_skeletal_mesh(tmesh); tgt_auto = bool(tc.apply_auto_generated_retarget_definition()); tc.apply_auto_fbik()\n" - " rt_asset = at.create_asset(base + '_RT', DEST, unreal.IKRetargeter, unreal.IKRetargetFactory())\n" + " rt_asset = _mk(base + '_RT', unreal.IKRetargeter, unreal.IKRetargetFactory())\n" " rtc = unreal.IKRetargeterController.get_controller(rt_asset)\n" " rtc.set_ik_rig(unreal.RetargetSourceOrTarget.SOURCE, src_rig)\n" " rtc.set_ik_rig(unreal.RetargetSourceOrTarget.TARGET, tgt_rig)\n" @@ -150,6 +160,7 @@ def build_retarget_anim(source_anim, source_mesh, target_mesh, destination_path= " 'next': 'cr_bake_anim(sequence, target_actor, retargeted_anim) -> editable rig -> seq_keyframe -> render',\n" " 'note': 'template_matched False => skeleton did not match a UE humanoid template; hand-author chains or pass a retargeter asset.'})\n" "except Exception as e:\n" - " _emit({'error': repr(e)})\n" + " import traceback\n" + " _emit({'error': repr(e), 'trace': traceback.format_exc().splitlines()[-6:]})\n" ) return wrap(body) From 6bd6c6a2f71ca07d7fa55031277b63e5ba5d89b5 Mon Sep 17 00:00:00 2001 From: Carl Roach <38408082+BoldPhoenix@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:48:34 -0500 Subject: [PATCH 3/3] =?UTF-8?q?fix(retarget):=20None-able=20params=20emitt?= =?UTF-8?q?ed=20JSON=20'null'=20(NameError)=20=E2=80=94=20use=20!r,=20not?= =?UTF-8?q?=20json.dumps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first real DeepMotion FBX exposed it on contact: import_mocap(skeleton=None) and retarget_anim(retargeter=None, name=None) built snippet code reading `SKEL = null` / `RT = null` (json.dumps(None) -> "null"), a NameError in the editor. Every prior smoke test hardcoded values, so the None default path was never exercised — exactly the "import_mocap has never seen a real FBX" gap from review. Switched the 3 None-able params to !r (repr), matching the cr_seq convention: repr(None)='None', repr('x')="'x'" — both valid Python literals. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FhGE51iZAN4bvfTGHzBkCN --- src/unreal_mcp/snippets/retarget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/unreal_mcp/snippets/retarget.py b/src/unreal_mcp/snippets/retarget.py index 3d0770b..484f34f 100644 --- a/src/unreal_mcp/snippets/retarget.py +++ b/src/unreal_mcp/snippets/retarget.py @@ -40,7 +40,7 @@ def build_import_mocap(fbx_path, destination_path="/Game/Mocap", skeleton=None, "import os\n" f"src = {json.dumps(fbx_path)}\n" f"dst = {json.dumps(destination_path)}\n" - f"SKEL = {json.dumps(skeleton)}\n" + f"SKEL = {skeleton!r}\n" f"IMPORT_MESH = {bool(import_mesh)!r}\n" f"replace = {bool(replace_existing)!r}\n" f"do_save = {bool(save)!r}\n" @@ -102,8 +102,8 @@ def build_retarget_anim(source_anim, source_mesh, target_mesh, destination_path= f"SRC_MESH = {json.dumps(source_mesh)}\n" f"TGT_MESH = {json.dumps(target_mesh)}\n" f"DEST = {json.dumps(destination_path)}\n" - f"RT = {json.dumps(retargeter)}\n" - f"NAME = {json.dumps(name)}\n" + f"RT = {retargeter!r}\n" + f"NAME = {name!r}\n" "try:\n" " anim = unreal.load_asset(SRC_ANIM)\n" " smesh = unreal.load_asset(SRC_MESH)\n"