"""Blueprint member-variable authoring ops. Wraps the (already-compiled) C++ helpers on unreal.MCPGraphLibrary: AddMemberVariable / SetVariableDefaultValue / SetVariableMetadata / RenameVariable / DeleteVariable / DescribeBlueprintJson. Reachable from JS as the `variable_op` tool, and also merged into `asset_op`'s op-table for zero-restart availability (see assets.py). Ops: add {bp_path, name, type, default?, instance_editable?, expose_on_spawn?, category?, tooltip?} — create a member variable, optionally set its default value + metadata. type is one of bool|int|float|double|string|name|text or a class path / short name for an object ref. set_default {bp_path, name, value} set_meta {bp_path, name, instance_editable?, expose_on_spawn?, category?, tooltip?} rename {bp_path, old, new} delete {bp_path, name} list {bp_path} — current member variables (name+type) as JSON. """ from __future__ import annotations import json import os import traceback try: import unreal # type: ignore except ImportError: unreal = None def _lib(): L = getattr(unreal, "MCPGraphLibrary", None) if L is None: raise RuntimeError("MCPGraphLibrary missing — rebuild C++ plugin") return L def _load_bp(path: str): a = unreal.EditorAssetLibrary.load_asset(path) if not isinstance(a, unreal.Blueprint): raise RuntimeError(f"not a blueprint: {path!r}") return a def _compile_save(bp): _lib().compile_and_save_blueprint(bp) unreal.EditorAssetLibrary.save_loaded_asset(bp) def _variables(bp) -> list: """Return [{name,type,...}] from DescribeBlueprintJson.""" try: info = json.loads(_lib().describe_blueprint_json(bp)) return info.get("variables", []) except Exception: # noqa: BLE001 return [] def _has_var(bp, name: str) -> bool: return any(v.get("name") == name for v in _variables(bp)) def add(args: dict) -> dict: bp = _load_bp(args["bp_path"]) name = args["name"] type_spec = args.get("type") or args.get("type_spec") if not name or not type_spec: return {"ok": False, "error": "add requires {name, type}"} if _has_var(bp, name): return {"ok": False, "error": f"variable {name!r} already exists", "exists": True} ok = bool(_lib().add_member_variable(bp, name, type_spec)) if not ok: return {"ok": False, "error": f"add_member_variable failed (bad type {type_spec!r}?)"} # Optional default value + metadata in the same transaction. if "default" in args and args["default"] is not None: _lib().set_variable_default_value(bp, name, str(args["default"])) if any(k in args for k in ("instance_editable", "expose_on_spawn", "category", "tooltip")): _lib().set_variable_metadata( bp, name, bool(args.get("instance_editable", False)), bool(args.get("expose_on_spawn", False)), str(args.get("category", "")), str(args.get("tooltip", "")), ) _compile_save(bp) return {"ok": True, "name": name, "type": type_spec} def set_default(args: dict) -> dict: bp = _load_bp(args["bp_path"]) ok = bool(_lib().set_variable_default_value(bp, args["name"], str(args.get("value", "")))) if ok: _compile_save(bp) return {"ok": ok, "name": args["name"]} def set_meta(args: dict) -> dict: bp = _load_bp(args["bp_path"]) ok = bool(_lib().set_variable_metadata( bp, args["name"], bool(args.get("instance_editable", False)), bool(args.get("expose_on_spawn", False)), str(args.get("category", "")), str(args.get("tooltip", "")), )) if ok: _compile_save(bp) return {"ok": ok, "name": args["name"]} def rename(args: dict) -> dict: bp = _load_bp(args["bp_path"]) ok = bool(_lib().rename_variable(bp, args["old"], args["new"])) if ok: _compile_save(bp) return {"ok": ok, "old": args["old"], "new": args["new"]} def delete(args: dict) -> dict: bp = _load_bp(args["bp_path"]) ok = bool(_lib().delete_variable(bp, args["name"])) if ok: _compile_save(bp) return {"ok": ok, "name": args["name"]} def list_vars(args: dict) -> dict: bp = _load_bp(args["bp_path"]) return {"ok": True, "variables": _variables(bp)} # op-table — also merged into assets.OPS for zero-restart availability. VAR_OPS = { "add_variable": add, "add": add, "set_var_default": set_default, "set_default": set_default, "set_var_meta": set_meta, "set_meta": set_meta, "rename_variable": rename, "rename": rename, "delete_variable": delete, "delete": delete, "list_variables": list_vars, "list": list_vars, } def entrypoint(payload_json: str) -> str: try: p = json.loads(payload_json) except Exception as e: # noqa: BLE001 return _tee({"ok": False, "error": f"bad json: {e}"}, {}) op = p.get("op") fn = VAR_OPS.get(op) if not fn: return _tee({"ok": False, "error": f"unknown op {op!r}", "available": sorted(VAR_OPS)}, p) try: return _tee(fn(p), p) except Exception as e: # noqa: BLE001 return _tee({"ok": False, "error": str(e), "traceback": traceback.format_exc()}, p) def _tee(result: dict, p: dict) -> str: txt = json.dumps(result) try: if unreal is not None: sd = unreal.Paths.project_saved_dir() + "MCP/" os.makedirs(sd, exist_ok=True) rid = p.get("__rid") or "last" with open(sd + "last.json", "w", encoding="utf-8") as f: f.write(txt) if rid != "last": with open(sd + f"{rid}.json", "w", encoding="utf-8") as f: f.write(txt) unreal.log("[MCP-VARS] " + txt[:1500]) except Exception: pass return txt