Files
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

146 lines
5.1 KiB
Python

"""Audio ops — MetaSounds, SoundCue, Attenuation, SoundClass/Mix.
Ops:
create_metasound_source {path}
create_metasound_patch {path}
create_sound_cue {path}
create_sound_attenuation {path}
create_sound_class {path}
create_sound_mix {path}
set_property {path, property, value}
get_property {path, property}
list_sound_assets {folder?}
play_sound_2d {path} — preview in editor
"""
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
def _create(path, asset_cls_name, factory_name):
folder, name = _split(path)
cls = getattr(unreal, asset_cls_name, None)
factory_cls = getattr(unreal, factory_name, None)
if cls is None or factory_cls is None:
return {"ok": False, "error": f"{asset_cls_name}/{factory_name} not exposed (plugin disabled?)"}
asset = _at().create_asset(name, folder, cls, factory_cls())
if asset is None:
return {"ok": False, "error": "create_asset returned None"}
unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
return {"ok": True, "path": asset.get_path_name().split(".")[0]}
def create_metasound_source(args): return _create(args["path"], "MetaSoundSource", "MetaSoundSourceFactory")
def create_metasound_patch(args): return _create(args["path"], "MetaSoundPatch", "MetaSoundPatchFactory")
def create_sound_cue(args): return _create(args["path"], "SoundCue", "SoundCueFactoryNew")
def create_sound_attenuation(args): return _create(args["path"], "SoundAttenuation", "SoundAttenuationFactory")
def create_sound_class(args): return _create(args["path"], "SoundClass", "SoundClassFactory")
def create_sound_mix(args): return _create(args["path"], "SoundMix", "SoundMixFactory")
def set_property(args):
a = _load(args["path"])
if a is None:
return {"ok": False, "error": "asset not found"}
try:
a.set_editor_property(args["property"], args["value"])
unreal.EditorAssetLibrary.save_asset(a.get_path_name())
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
def get_property(args):
a = _load(args["path"])
if a is None:
return {"ok": False, "error": "asset not found"}
try:
v = a.get_editor_property(args["property"])
return {"ok": True, "value": str(v)}
except Exception as e:
return {"ok": False, "error": str(e)}
def list_sound_assets(args):
folder = args.get("folder") or "/Game"
ar = unreal.AssetRegistryHelpers.get_asset_registry()
classes = ["SoundCue", "SoundWave", "MetaSoundSource", "MetaSoundPatch",
"SoundAttenuation", "SoundClass", "SoundMix", "SoundConcurrency"]
filt = unreal.ARFilter(package_paths=[folder], recursive_paths=True, class_names=classes)
items = [{"path": str(d.package_name), "class": str(d.asset_class)} for d in ar.get_assets(filt)]
return {"ok": True, "count": len(items), "items": items}
def play_sound_2d(args):
a = _load(args["path"])
if a is None:
return {"ok": False, "error": "not found"}
try:
world = unreal.EditorLevelLibrary.get_editor_world()
unreal.GameplayStatics.play_sound2d(world, a)
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
OPS = {
"create_metasound_source": create_metasound_source,
"create_metasound_patch": create_metasound_patch,
"create_sound_cue": create_sound_cue,
"create_sound_attenuation": create_sound_attenuation,
"create_sound_class": create_sound_class,
"create_sound_mix": create_sound_mix,
"set_property": set_property,
"get_property": get_property,
"list_sound_assets": list_sound_assets,
"play_sound_2d": play_sound_2d,
}
def entrypoint(payload_json: str) -> str:
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-AUDIO] " + txt[:1500])
except Exception:
pass
return txt