Files
unreal-engine-mcp-system-pl…/ue_side/input.py
Bonchellon eba71c4ca8 Unified, portable Unreal Engine MCP system plugin
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>
2026-06-24 01:07:24 +03:00

188 lines
6.5 KiB
Python

"""Enhanced Input ops — Input Actions, Input Mapping Contexts.
Ops:
create_input_action {path, value_type?:'Bool|Axis1D|Axis2D|Axis3D'}
create_input_mapping_context {path}
add_mapping {imc_path, action_path, key, modifiers?:[class_path], triggers?:[class_path]}
remove_mapping {imc_path, index}
list_mappings {imc_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
VALUE_TYPES = {
"Bool": "Boolean",
"Axis1D": "Axis1D",
"Axis2D": "Axis2D",
"Axis3D": "Axis3D",
}
def create_input_action(args):
folder, name = _split(args["path"])
factory_cls = getattr(unreal, "InputActionFactory", None)
asset_cls = getattr(unreal, "InputAction", None)
if factory_cls is None or asset_cls is None:
return {"ok": False, "error": "Enhanced Input plugin not enabled"}
asset = _at().create_asset(name, folder, asset_cls, factory_cls())
if asset is None:
return {"ok": False, "error": "create returned None"}
vt = args.get("value_type") or "Bool"
enum_name = VALUE_TYPES.get(vt)
if enum_name:
try:
enum_obj = getattr(unreal.InputActionValueType, enum_name)
asset.set_editor_property("value_type", enum_obj)
except Exception:
pass
unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
return {"ok": True, "path": asset.get_path_name().split(".")[0]}
def create_input_mapping_context(args):
folder, name = _split(args["path"])
factory_cls = getattr(unreal, "InputMappingContextFactory", None)
asset_cls = getattr(unreal, "InputMappingContext", None)
if factory_cls is None or asset_cls is None:
return {"ok": False, "error": "Enhanced Input plugin not enabled"}
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 add_mapping(args):
imc = _load(args["imc_path"])
ia = _load(args["action_path"])
if imc is None or ia is None:
return {"ok": False, "error": "imc or action not found"}
key = unreal.InputCoreLibrary.input_key_from_string(args["key"]) \
if hasattr(unreal, "InputCoreLibrary") and hasattr(unreal.InputCoreLibrary, "input_key_from_string") \
else unreal.Key(args["key"])
mapping_cls = getattr(unreal, "EnhancedActionKeyMapping", None)
if mapping_cls is None:
try:
imc.map_key(ia, key)
unreal.EditorAssetLibrary.save_asset(imc.get_path_name())
return {"ok": True, "note": "added via map_key()"}
except Exception as e:
return {"ok": False, "error": f"map_key failed: {e}"}
m = mapping_cls()
m.set_editor_property("action", ia)
m.set_editor_property("key", key)
# Modifiers/triggers (class CDOs):
for cp in (args.get("modifiers") or []):
mc = unreal.load_class(None, cp)
if mc is not None:
try:
inst = unreal.new_object(mc)
mods = list(m.get_editor_property("modifiers") or [])
mods.append(inst)
m.set_editor_property("modifiers", mods)
except Exception:
pass
for cp in (args.get("triggers") or []):
tc = unreal.load_class(None, cp)
if tc is not None:
try:
inst = unreal.new_object(tc)
trigs = list(m.get_editor_property("triggers") or [])
trigs.append(inst)
m.set_editor_property("triggers", trigs)
except Exception:
pass
mappings = list(imc.get_editor_property("mappings") or [])
mappings.append(m)
imc.set_editor_property("mappings", mappings)
unreal.EditorAssetLibrary.save_asset(imc.get_path_name())
return {"ok": True, "index": len(mappings) - 1}
def remove_mapping(args):
imc = _load(args["imc_path"])
if imc is None:
return {"ok": False, "error": "imc not found"}
mappings = list(imc.get_editor_property("mappings") or [])
idx = int(args["index"])
if idx < 0 or idx >= len(mappings):
return {"ok": False, "error": "index out of range"}
mappings.pop(idx)
imc.set_editor_property("mappings", mappings)
unreal.EditorAssetLibrary.save_asset(imc.get_path_name())
return {"ok": True}
def list_mappings(args):
imc = _load(args["imc_path"])
if imc is None:
return {"ok": False, "error": "imc not found"}
out = []
for i, m in enumerate(imc.get_editor_property("mappings") or []):
try:
a = m.get_editor_property("action")
k = m.get_editor_property("key")
out.append({
"index": i,
"action": a.get_path_name().split(".")[0] if a else None,
"key": str(k),
})
except Exception:
pass
return {"ok": True, "mappings": out}
OPS = {
"create_input_action": create_input_action,
"create_input_mapping_context": create_input_mapping_context,
"add_mapping": add_mapping,
"remove_mapping": remove_mapping,
"list_mappings": list_mappings,
}
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-INPUT] " + txt[:1500])
except Exception:
pass
return txt