"""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