"""Network ops for Blueprint assets. Ops: audit_bp {bp_path} set_actor_flags {bp_path, replicates?, replicate_movement?, net_load_on_client?, always_relevant?, only_relevant_to_owner?, use_owner_relevancy?, net_cull_distance_squared?, net_update_frequency?, min_net_update_frequency?, net_priority?, net_dormancy?} set_component_flags {bp_path, component, replicated?, net_addressable?} set_custom_event_rpc {bp_path, function?, event?, guid?, mode, reliable?} apply_profile {bp_path, profile} RPC modes for set_custom_event_rpc: none | server | client | multicast """ from __future__ import annotations import json import os import traceback try: import unreal # type: ignore except ImportError: unreal = None ACTOR_PROP_MAP = { "replicates": "replicates", "replicate_movement": "replicate_movement", "net_load_on_client": "net_load_on_client", "always_relevant": "always_relevant", "only_relevant_to_owner": "only_relevant_to_owner", "use_owner_relevancy": "net_use_owner_relevancy", "net_cull_distance_squared": "net_cull_distance_squared", "net_update_frequency": "net_update_frequency", "min_net_update_frequency": "min_net_update_frequency", "net_priority": "net_priority", "net_dormancy": "net_dormancy", } def _load_bp(path: str): bp = unreal.EditorAssetLibrary.load_asset(path) if not isinstance(bp, unreal.Blueprint): raise RuntimeError(f"not a Blueprint: {path!r}") return bp def _generated_class(bp_path: str): cls_path = f"{bp_path}.{bp_path.rsplit('/', 1)[-1]}_C" cls = unreal.load_class(None, cls_path) if cls is None: raise RuntimeError(f"generated class not found: {cls_path}") return cls def _cdo(bp_path: str): return unreal.get_default_object(_generated_class(bp_path)) def _lib(): L = getattr(unreal, "MCPGraphLibrary", None) if L is None: raise RuntimeError("MCPGraphLibrary missing - rebuild C++ plugin") return L def _find_graph(bp, name: str): L = _lib() if name in (None, "", "EventGraph"): return L.find_event_graph(bp) return L.find_function_graph(bp, name) def _get_prop(obj, prop): try: return obj.get_editor_property(prop) except Exception: return getattr(obj, prop) def _set_prop(obj, prop, value): try: obj.set_editor_property(prop, value) except Exception: setattr(obj, prop, value) def _save_compile(bp): L = getattr(unreal, "MCPGraphLibrary", None) messages = [] try: if L: messages = list(L.compile_with_messages(bp)) else: unreal.KismetEditorUtilities.compile_blueprint(bp) except Exception as e: messages = [f"WARN||compile failed from network.py: {e}"] unreal.EditorAssetLibrary.save_loaded_asset(bp) return messages def _read_actor_flags(cdo): out = {} for public_name, prop in ACTOR_PROP_MAP.items(): try: v = _get_prop(cdo, prop) out[public_name] = str(v) if public_name == "net_dormancy" else v except Exception as e: out[public_name] = {"error": str(e)} return out def audit_bp(args: dict) -> dict: bp_path = args["bp_path"] cdo = _cdo(bp_path) result = { "ok": True, "bp_path": bp_path, "actor": _read_actor_flags(cdo), "components": [], "custom_events": [], "notes": [], } for name in args.get("components") or []: item = {"name": name} try: comp = _get_prop(cdo, name) item["class"] = comp.get_class().get_name() for prop in ("replicates", "is_replicated", "net_addressable"): try: item[prop] = _get_prop(comp, prop) except Exception: pass except Exception as e: item["error"] = str(e) result["components"].append(item) try: import apply_graph raw = apply_graph.read_graph(bp_path, args.get("function") or "EventGraph") for node in raw.get("graph", {}).get("nodes", []): if node.get("class") == "K2Node_CustomEvent": result["custom_events"].append({ "guid": node.get("guid"), "title": node.get("title"), "target": node.get("target"), }) result["notes"].append("Custom event RPC flags require set_custom_event_rpc; read_graph does not expose them.") except Exception as e: result["notes"].append(f"custom event scan failed: {e}") return result def set_actor_flags(args: dict) -> dict: bp_path = args["bp_path"] bp = _load_bp(bp_path) cdo = _cdo(bp_path) changed = {} errors = {} for public_name, prop in ACTOR_PROP_MAP.items(): if public_name not in args: continue try: _set_prop(cdo, prop, args[public_name]) changed[public_name] = args[public_name] except Exception as e: errors[public_name] = str(e) messages = _save_compile(bp) return {"ok": not errors, "bp_path": bp_path, "changed": changed, "errors": errors, "compile_messages": messages} def _component_from_cdo(cdo, name: str): try: return _get_prop(cdo, name) except Exception: pass getter = getattr(cdo, "get_component_by_class", None) if getter: # Fallback is intentionally conservative: caller should pass the BP component variable name. return None return None def set_component_flags(args: dict) -> dict: bp_path = args["bp_path"] bp = _load_bp(bp_path) cdo = _cdo(bp_path) comp_name = args["component"] comp = _component_from_cdo(cdo, comp_name) if comp is None: return {"ok": False, "error": f"component not found on CDO by name: {comp_name}"} changed = {} errors = {} for public_name, candidates in { "replicated": ("replicates", "is_replicated"), "net_addressable": ("net_addressable",), }.items(): if public_name not in args: continue done = False for prop in candidates: try: _set_prop(comp, prop, args[public_name]) changed[public_name] = args[public_name] done = True break except Exception: continue if not done: errors[public_name] = f"no writable property among {candidates}" messages = _save_compile(bp) return {"ok": not errors, "bp_path": bp_path, "component": comp_name, "changed": changed, "errors": errors, "compile_messages": messages} def set_custom_event_rpc(args: dict) -> dict: bp_path = args["bp_path"] bp = _load_bp(bp_path) graph = _find_graph(bp, args.get("function") or "EventGraph") if graph is None: return {"ok": False, "error": "graph not found"} target = args.get("guid") or args.get("event") if not target: return {"ok": False, "error": "set_custom_event_rpc requires guid or event"} mode = args.get("mode") or "none" reliable = bool(args.get("reliable", False)) L = _lib() setter = getattr(L, "set_custom_event_network_flags", None) if setter is None: return { "ok": False, "error": "MCPGraphLibrary.set_custom_event_network_flags is unavailable. Rebuild/restart UE after the plugin C++ update.", "requested": {"target": target, "mode": mode, "reliable": reliable}, } ok = bool(setter(bp, graph, target, mode, reliable)) messages = _save_compile(bp) if ok else [] return {"ok": ok, "bp_path": bp_path, "target": target, "mode": mode, "reliable": reliable, "compile_messages": messages} def apply_profile(args: dict) -> dict: profile = args["profile"] bp_path = args["bp_path"] if profile == "replicated_world_actor": return set_actor_flags({ "bp_path": bp_path, "replicates": True, "net_load_on_client": True, "replicate_movement": bool(args.get("replicate_movement", False)), }) if profile == "always_relevant_world_actor": return set_actor_flags({ "bp_path": bp_path, "replicates": True, "net_load_on_client": True, "always_relevant": True, "replicate_movement": bool(args.get("replicate_movement", False)), }) return {"ok": False, "error": f"unknown profile {profile!r}", "available": ["replicated_world_actor", "always_relevant_world_actor"]} OPS = { "audit_bp": audit_bp, "set_actor_flags": set_actor_flags, "set_component_flags": set_component_flags, "set_custom_event_rpc": set_custom_event_rpc, "apply_profile": apply_profile, } 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}"}, {}) fn = OPS.get(p.get("op")) if not fn: return _tee({"ok": False, "error": f"unknown op {p.get('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: dict, p: dict) -> str: 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 + f"{rid}.json", "w", encoding="utf-8") as f: f.write(txt) unreal.log("[MCP-NETWORK] " + txt[:1500]) except Exception: pass return txt