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>
216 lines
7.9 KiB
Python
216 lines
7.9 KiB
Python
"""AI ops — BehaviorTree, Blackboard, EQS, AIController.
|
|
|
|
Ops:
|
|
create_behavior_tree {path, blackboard?}
|
|
create_blackboard {path, parent?}
|
|
create_eqs_query {path}
|
|
create_aicontroller_bp {path, parent_class?:'AIController'}
|
|
add_blackboard_key {path, name, type:'Bool|Int|Float|String|Name|Vector|Rotator|Object|Class|Enum', base_class?}
|
|
remove_blackboard_key {path, name}
|
|
list_blackboard_keys {path}
|
|
set_bt_blackboard {bt_path, blackboard_path}
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import traceback
|
|
|
|
try:
|
|
import unreal # type: ignore
|
|
except ImportError:
|
|
unreal = None
|
|
|
|
|
|
def _at(): return unreal.AssetToolsHelpers.get_asset_tools()
|
|
def _load(p): return unreal.EditorAssetLibrary.load_asset(p)
|
|
def _split(p):
|
|
folder, name = p.rsplit("/", 1)
|
|
return folder, name
|
|
|
|
|
|
KEY_TYPES = {
|
|
"Bool": "BlackboardKeyType_Bool",
|
|
"Int": "BlackboardKeyType_Int",
|
|
"Float": "BlackboardKeyType_Float",
|
|
"String": "BlackboardKeyType_String",
|
|
"Name": "BlackboardKeyType_Name",
|
|
"Vector": "BlackboardKeyType_Vector",
|
|
"Rotator": "BlackboardKeyType_Rotator",
|
|
"Object": "BlackboardKeyType_Object",
|
|
"Class": "BlackboardKeyType_Class",
|
|
"Enum": "BlackboardKeyType_Enum",
|
|
}
|
|
|
|
|
|
def create_behavior_tree(args):
|
|
folder, name = _split(args["path"])
|
|
factory_cls = getattr(unreal, "BehaviorTreeFactory", None)
|
|
if factory_cls is None:
|
|
return {"ok": False, "error": "BehaviorTreeFactory missing — enable AIModule"}
|
|
asset = _at().create_asset(name, folder, unreal.BehaviorTree, factory_cls())
|
|
if asset is None:
|
|
return {"ok": False, "error": "create returned None"}
|
|
if args.get("blackboard"):
|
|
bb = _load(args["blackboard"])
|
|
if bb is not None:
|
|
asset.set_editor_property("blackboard_asset", bb)
|
|
unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
|
|
return {"ok": True, "path": asset.get_path_name().split(".")[0]}
|
|
|
|
|
|
def create_blackboard(args):
|
|
folder, name = _split(args["path"])
|
|
factory_cls = getattr(unreal, "BlackboardDataFactory", None)
|
|
if factory_cls is None:
|
|
return {"ok": False, "error": "BlackboardDataFactory missing"}
|
|
asset = _at().create_asset(name, folder, unreal.BlackboardData, factory_cls())
|
|
if asset is None:
|
|
return {"ok": False, "error": "create returned None"}
|
|
if args.get("parent"):
|
|
parent = _load(args["parent"])
|
|
if parent is not None:
|
|
try:
|
|
asset.set_editor_property("parent", parent)
|
|
except Exception:
|
|
pass
|
|
unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
|
|
return {"ok": True, "path": asset.get_path_name().split(".")[0]}
|
|
|
|
|
|
def create_eqs_query(args):
|
|
folder, name = _split(args["path"])
|
|
factory_cls = getattr(unreal, "EnvQueryFactory", None)
|
|
asset_cls = getattr(unreal, "EnvQuery", None)
|
|
if factory_cls is None or asset_cls is None:
|
|
return {"ok": False, "error": "EnvQuery factory missing — enable EQS in project settings"}
|
|
asset = _at().create_asset(name, folder, asset_cls, factory_cls())
|
|
if asset is None:
|
|
return {"ok": False, "error": "create returned None"}
|
|
unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
|
|
return {"ok": True, "path": asset.get_path_name().split(".")[0]}
|
|
|
|
|
|
def create_aicontroller_bp(args):
|
|
folder, name = _split(args["path"])
|
|
parent_name = args.get("parent_class") or "AIController"
|
|
parent = unreal.load_class(None, "/Script/AIModule." + parent_name)
|
|
if parent is None:
|
|
parent = unreal.load_class(None, parent_name)
|
|
if parent is None:
|
|
return {"ok": False, "error": f"parent {parent_name!r} not found"}
|
|
factory = unreal.BlueprintFactory()
|
|
factory.set_editor_property("parent_class", parent)
|
|
asset = _at().create_asset(name, folder, unreal.Blueprint, factory)
|
|
if asset is None:
|
|
return {"ok": False, "error": "create returned None"}
|
|
unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
|
|
return {"ok": True, "path": asset.get_path_name().split(".")[0]}
|
|
|
|
|
|
def add_blackboard_key(args):
|
|
bb = _load(args["path"])
|
|
if bb is None:
|
|
return {"ok": False, "error": "blackboard not found"}
|
|
type_name = KEY_TYPES.get(args["type"])
|
|
if not type_name:
|
|
return {"ok": False, "error": f"unknown key type {args['type']!r}; valid: {list(KEY_TYPES)}"}
|
|
key_type_cls = getattr(unreal, type_name, None)
|
|
if key_type_cls is None:
|
|
return {"ok": False, "error": f"{type_name} not exposed"}
|
|
entry_cls = getattr(unreal, "BlackboardEntry", None)
|
|
if entry_cls is None:
|
|
return {"ok": False, "error": "BlackboardEntry not exposed; edit via UI"}
|
|
entry = entry_cls()
|
|
entry.set_editor_property("entry_name", args["name"])
|
|
kt = key_type_cls()
|
|
if args.get("base_class") and hasattr(kt, "base_class"):
|
|
cls = unreal.load_class(None, args["base_class"])
|
|
if cls is not None:
|
|
kt.set_editor_property("base_class", cls)
|
|
entry.set_editor_property("key_type", kt)
|
|
keys = list(bb.get_editor_property("keys") or [])
|
|
keys.append(entry)
|
|
bb.set_editor_property("keys", keys)
|
|
unreal.EditorAssetLibrary.save_asset(bb.get_path_name())
|
|
return {"ok": True, "name": args["name"]}
|
|
|
|
|
|
def remove_blackboard_key(args):
|
|
bb = _load(args["path"])
|
|
if bb is None:
|
|
return {"ok": False, "error": "blackboard not found"}
|
|
target = args["name"]
|
|
keys = [k for k in (bb.get_editor_property("keys") or [])
|
|
if str(k.get_editor_property("entry_name")) != target]
|
|
bb.set_editor_property("keys", keys)
|
|
unreal.EditorAssetLibrary.save_asset(bb.get_path_name())
|
|
return {"ok": True, "removed": target}
|
|
|
|
|
|
def list_blackboard_keys(args):
|
|
bb = _load(args["path"])
|
|
if bb is None:
|
|
return {"ok": False, "error": "blackboard not found"}
|
|
out = []
|
|
for k in (bb.get_editor_property("keys") or []):
|
|
try:
|
|
kt = k.get_editor_property("key_type")
|
|
out.append({"name": str(k.get_editor_property("entry_name")),
|
|
"type": kt.get_class().get_name() if kt else None})
|
|
except Exception:
|
|
pass
|
|
return {"ok": True, "keys": out}
|
|
|
|
|
|
def set_bt_blackboard(args):
|
|
bt = _load(args["bt_path"])
|
|
bb = _load(args["blackboard_path"])
|
|
if bt is None or bb is None:
|
|
return {"ok": False, "error": "bt or blackboard not found"}
|
|
bt.set_editor_property("blackboard_asset", bb)
|
|
unreal.EditorAssetLibrary.save_asset(bt.get_path_name())
|
|
return {"ok": True}
|
|
|
|
|
|
OPS = {
|
|
"create_behavior_tree": create_behavior_tree,
|
|
"create_blackboard": create_blackboard,
|
|
"create_eqs_query": create_eqs_query,
|
|
"create_aicontroller_bp": create_aicontroller_bp,
|
|
"add_blackboard_key": add_blackboard_key,
|
|
"remove_blackboard_key": remove_blackboard_key,
|
|
"list_blackboard_keys": list_blackboard_keys,
|
|
"set_bt_blackboard": set_bt_blackboard,
|
|
}
|
|
|
|
|
|
def entrypoint(payload_json):
|
|
try:
|
|
p = json.loads(payload_json)
|
|
except Exception as e:
|
|
return _tee({"ok": False, "error": f"bad json: {e}"}, {})
|
|
op = p.get("op"); fn = OPS.get(op)
|
|
if not fn:
|
|
return _tee({"ok": False, "error": f"unknown op {op!r}", "available": list(OPS)}, p)
|
|
try:
|
|
return _tee(fn(p), p)
|
|
except Exception as e:
|
|
return _tee({"ok": False, "error": str(e), "traceback": traceback.format_exc()}, p)
|
|
|
|
|
|
def _tee(result, p):
|
|
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-AI] " + txt[:1500])
|
|
except Exception:
|
|
pass
|
|
return txt
|