Merge of the two project copies into one self-contained plugin (the superset: variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring). Made fully project- and machine-agnostic — no hardcoded paths: - New src/projectPaths.js auto-detects the host .uproject (walk-up), project name, Editor build target, log file, and engine install (EngineAssociation via launcher manifest/registry, else installed-engine scan). All overridable via UE_* env vars. - Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver. - Voxel made optional: Build.cs auto-detects the Voxel plugin (env UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL, so the module builds in projects without Voxel; .uplugin marks Voxel optional. - De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded node_modules/Binaries/Intermediate/__pycache__/secrets from the repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
187 lines
5.5 KiB
Python
187 lines
5.5 KiB
Python
"""Single-node / single-edge edit ops + navigation.
|
|
|
|
Ops:
|
|
delete_node {bp_path, function?, guid}
|
|
break_link {bp_path, function?, from:[guid,pin], to:[guid,pin]}
|
|
break_all {bp_path, function?, guid}
|
|
move_nodes {bp_path, function?, moves:[{guid,x,y}]}
|
|
resize_comment {bp_path, function?, guid, width, height}
|
|
set_comment {bp_path, function?, guid, text, bubble?}
|
|
open_bp {bp_path}
|
|
open_graph {bp_path, function}
|
|
jump_to_node {bp_path, function?, guid}
|
|
add_case_pin {bp_path, function?, guid} — switch ops
|
|
format_text {bp_path, function?, guid, format}
|
|
reconstruct {bp_path, function?, guid}
|
|
"""
|
|
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 _graph(bp, fn: str | None):
|
|
L = _lib()
|
|
if fn in (None, "", "EventGraph"):
|
|
return L.find_event_graph(bp)
|
|
return L.find_function_graph(bp, fn)
|
|
|
|
|
|
def _compile_save(bp):
|
|
_lib().compile_and_save_blueprint(bp)
|
|
unreal.EditorAssetLibrary.save_loaded_asset(bp)
|
|
|
|
|
|
def delete_node(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
ok = bool(_lib().delete_node(g, args["guid"]))
|
|
if ok: _compile_save(bp)
|
|
return {"ok": ok, "guid": args["guid"]}
|
|
|
|
|
|
def break_link(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
(fg, fp) = args["from"]; (tg, tp) = args["to"]
|
|
ok = bool(_lib().break_link(g, fg, fp, tg, tp))
|
|
if ok: _compile_save(bp)
|
|
return {"ok": ok}
|
|
|
|
|
|
def break_all(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
ok = bool(_lib().break_all_node_links(g, args["guid"]))
|
|
if ok: _compile_save(bp)
|
|
return {"ok": ok}
|
|
|
|
|
|
def move_nodes(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
moves = args.get("moves") or []
|
|
ids = [m["guid"] for m in moves]
|
|
pos = [unreal.Vector2D(float(m["x"]), float(m["y"])) for m in moves]
|
|
n = int(_lib().move_nodes(g, ids, pos))
|
|
return {"ok": n > 0, "moved": n}
|
|
|
|
|
|
def resize_comment(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
ok = bool(_lib().resize_comment_node(g, args["guid"], unreal.Vector2D(float(args["width"]), float(args["height"]))))
|
|
return {"ok": ok}
|
|
|
|
|
|
def set_comment(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
ok = bool(_lib().set_node_comment(g, args["guid"], str(args.get("text", "")), bool(args.get("bubble", False))))
|
|
return {"ok": ok}
|
|
|
|
|
|
def open_bp(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
return {"ok": bool(_lib().open_blueprint_editor(bp))}
|
|
|
|
|
|
def open_graph(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
return {"ok": bool(_lib().open_graph_in_editor(bp, g))}
|
|
|
|
|
|
def jump_to_node(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
return {"ok": bool(_lib().jump_to_node_in_editor(bp, g, args["guid"]))}
|
|
|
|
|
|
def add_case_pin(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
ok = bool(_lib().add_case_pin_to_switch(g, args["guid"]))
|
|
if ok: _compile_save(bp)
|
|
return {"ok": ok}
|
|
|
|
|
|
def format_text(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
ok = bool(_lib().format_text_set_format(g, args["guid"], args["format"]))
|
|
if ok: _compile_save(bp)
|
|
return {"ok": ok}
|
|
|
|
|
|
def reconstruct(args: dict) -> dict:
|
|
bp = _load_bp(args["bp_path"])
|
|
g = _graph(bp, args.get("function"))
|
|
ok = bool(_lib().reconstruct_node(g, args["guid"]))
|
|
return {"ok": ok}
|
|
|
|
|
|
OPS = {
|
|
"delete_node": delete_node,
|
|
"break_link": break_link,
|
|
"break_all": break_all,
|
|
"move_nodes": move_nodes,
|
|
"resize_comment": resize_comment,
|
|
"set_comment": set_comment,
|
|
"open_bp": open_bp,
|
|
"open_graph": open_graph,
|
|
"jump_to_node": jump_to_node,
|
|
"add_case_pin": add_case_pin,
|
|
"format_text": format_text,
|
|
"reconstruct": reconstruct,
|
|
}
|
|
|
|
|
|
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}"}, {})
|
|
fn = OPS.get(p.get("op"))
|
|
if not fn:
|
|
return _tee({"ok": False, "error": f"unknown op {p.get('op')!r}", "available": list(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 + f"{rid}.json", "w", encoding="utf-8") as f:
|
|
f.write(txt)
|
|
unreal.log("[MCP-EDIT] " + txt[:1500])
|
|
except Exception:
|
|
pass
|
|
return txt
|