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>
This commit is contained in:
156
ue_side/validation.py
Normal file
156
ue_side/validation.py
Normal file
@ -0,0 +1,156 @@
|
||||
"""Asset validation, redirector cleanup, reference analysis.
|
||||
|
||||
Ops:
|
||||
validate_assets {paths:[]} — run EditorValidatorSubsystem
|
||||
validate_folder {path, recursive?}
|
||||
fix_redirectors {path} — recursively resolve UObjectRedirectors under folder
|
||||
get_references {asset_path} — assets THIS asset depends on
|
||||
get_referencers {asset_path} — assets that REFER TO this asset
|
||||
find_unused {folder, ignore_paths?:[]} — assets in folder with zero referencers
|
||||
disk_size {folder} — total .uasset size on disk
|
||||
list_redirectors {folder}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
|
||||
try:
|
||||
import unreal # type: ignore
|
||||
except ImportError:
|
||||
unreal = None
|
||||
|
||||
|
||||
def _ar():
|
||||
return unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
|
||||
def validate_assets(args):
|
||||
paths = args.get("paths") or []
|
||||
evs = unreal.get_editor_subsystem(unreal.EditorValidatorSubsystem)
|
||||
if evs is None:
|
||||
return {"ok": False, "error": "EditorValidatorSubsystem unavailable"}
|
||||
objs = [unreal.EditorAssetLibrary.load_asset(p) for p in paths]
|
||||
objs = [o for o in objs if o is not None]
|
||||
results = evs.validate_assets(objs, True, True) if hasattr(evs, "validate_assets") else None
|
||||
return {"ok": True, "validated_count": len(objs), "result": str(results) if results is not None else None}
|
||||
|
||||
|
||||
def validate_folder(args):
|
||||
folder = args["path"]
|
||||
recursive = bool(args.get("recursive", True))
|
||||
filt = unreal.ARFilter(package_paths=[folder], recursive_paths=recursive)
|
||||
assets = _ar().get_assets(filt)
|
||||
paths = [str(d.package_name) for d in assets]
|
||||
return validate_assets({"paths": paths})
|
||||
|
||||
|
||||
def fix_redirectors(args):
|
||||
folder = args["path"]
|
||||
filt = unreal.ARFilter(package_paths=[folder], recursive_paths=True, class_names=["ObjectRedirector"])
|
||||
redirs = _ar().get_assets(filt)
|
||||
if not redirs:
|
||||
return {"ok": True, "fixed": 0}
|
||||
objs = [d.get_asset() for d in redirs]
|
||||
objs = [o for o in objs if o is not None]
|
||||
try:
|
||||
unreal.AssetToolsHelpers.get_asset_tools().fixup_referencers(objs)
|
||||
return {"ok": True, "fixed": len(objs)}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e), "found": len(redirs)}
|
||||
|
||||
|
||||
def get_references(args):
|
||||
deps = _ar().get_dependencies(unreal.Name(args["asset_path"]),
|
||||
unreal.AssetRegistryDependencyOptions(include_soft_package_references=True,
|
||||
include_hard_package_references=True))
|
||||
return {"ok": True, "dependencies": [str(d) for d in (deps or [])]}
|
||||
|
||||
|
||||
def get_referencers(args):
|
||||
refs = _ar().get_referencers(unreal.Name(args["asset_path"]),
|
||||
unreal.AssetRegistryDependencyOptions(include_soft_package_references=True,
|
||||
include_hard_package_references=True))
|
||||
return {"ok": True, "referencers": [str(d) for d in (refs or [])]}
|
||||
|
||||
|
||||
def find_unused(args):
|
||||
folder = args["folder"]
|
||||
ignore = set(args.get("ignore_paths") or [])
|
||||
filt = unreal.ARFilter(package_paths=[folder], recursive_paths=True)
|
||||
out = []
|
||||
for d in _ar().get_assets(filt):
|
||||
pkg = str(d.package_name)
|
||||
if pkg in ignore or any(pkg.startswith(p) for p in ignore):
|
||||
continue
|
||||
refs = _ar().get_referencers(unreal.Name(pkg),
|
||||
unreal.AssetRegistryDependencyOptions(include_soft_package_references=True,
|
||||
include_hard_package_references=True)) or []
|
||||
if not refs:
|
||||
out.append({"path": pkg, "class": str(d.asset_class)})
|
||||
return {"ok": True, "count": len(out), "unused": out}
|
||||
|
||||
|
||||
def disk_size(args):
|
||||
folder = args["folder"]
|
||||
# Translate /Game -> Content path
|
||||
proj_content = unreal.Paths.project_content_dir()
|
||||
rel = folder.replace("/Game", "").lstrip("/")
|
||||
root = os.path.join(proj_content, rel)
|
||||
total = 0; files = 0
|
||||
for dirpath, _, fnames in os.walk(root):
|
||||
for f in fnames:
|
||||
if f.endswith((".uasset", ".umap")):
|
||||
total += os.path.getsize(os.path.join(dirpath, f))
|
||||
files += 1
|
||||
return {"ok": True, "folder": folder, "bytes": total, "files": files, "mb": round(total / 1024 / 1024, 2)}
|
||||
|
||||
|
||||
def list_redirectors(args):
|
||||
folder = args["folder"]
|
||||
filt = unreal.ARFilter(package_paths=[folder], recursive_paths=True, class_names=["ObjectRedirector"])
|
||||
out = [str(d.package_name) for d in _ar().get_assets(filt)]
|
||||
return {"ok": True, "count": len(out), "redirectors": out}
|
||||
|
||||
|
||||
OPS = {
|
||||
"validate_assets": validate_assets,
|
||||
"validate_folder": validate_folder,
|
||||
"fix_redirectors": fix_redirectors,
|
||||
"get_references": get_references,
|
||||
"get_referencers": get_referencers,
|
||||
"find_unused": find_unused,
|
||||
"disk_size": disk_size,
|
||||
"list_redirectors": list_redirectors,
|
||||
}
|
||||
|
||||
|
||||
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-VALIDATE] " + txt[:1500])
|
||||
except Exception:
|
||||
pass
|
||||
return txt
|
||||
Reference in New Issue
Block a user