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>
157 lines
6.0 KiB
Python
157 lines
6.0 KiB
Python
"""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
|