"""C++ class scaffold — generate .h/.cpp templates under a project module. This does NOT compile the code — pair with live_coding.compile or a regular hot-reload after the files appear. UE will pick up new files on next build. Ops: list_modules {} — read Source/*/Build.cs create_class {name, parent_class, module?(default: primary game module), subfolder?, header_only?:false, public?:true, api_macro?:auto} list_classes {module?, subfolder?} — list .h files under a module read_class {name, module?} — return .h+.cpp text """ from __future__ import annotations import json import os import re import traceback try: import unreal # type: ignore except ImportError: unreal = None def _project_dir(): if unreal is not None: return unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()).rstrip("/\\") return os.getcwd() def _source_dir(): return os.path.join(_project_dir(), "Source") def _project_name(): """Project name = the .uproject base name (also the conventional primary module).""" if unreal is not None: try: up = unreal.Paths.get_project_file_path() # e.g. .../MyGame.uproject return os.path.splitext(os.path.basename(up))[0] except Exception: pass return os.path.basename(_project_dir().rstrip("/\\")) def _default_module(): """Primary game module to scaffold into. Prefer the Runtime module declared in the .uproject; fall back to the module whose folder matches the project name; else the first Source module; else the project name.""" proj = _project_name() # 1. Read the .uproject Modules list for a Runtime/Game module. try: up = unreal.Paths.get_project_file_path() if unreal is not None else None if up and os.path.isfile(up): with open(up, "r", encoding="utf-8") as f: data = json.load(f) mods = data.get("Modules") or [] runtime = [m for m in mods if str(m.get("Type", "")).lower() in ("runtime", "")] if runtime: return runtime[0].get("Name") or proj if mods: return mods[0].get("Name") or proj except Exception: pass # 2. Folder matching the project name. src = _source_dir() if os.path.isdir(os.path.join(src, proj)): return proj # 3. First Source module with a Build.cs. try: for name in sorted(os.listdir(src)): if os.path.isfile(os.path.join(src, name, f"{name}.Build.cs")): return name except Exception: pass return proj def list_modules(args): out = [] src = _source_dir() if not os.path.isdir(src): return {"ok": False, "error": f"no Source dir at {src}"} for name in os.listdir(src): p = os.path.join(src, name) if os.path.isdir(p): build_cs = os.path.join(p, f"{name}.Build.cs") if os.path.isfile(build_cs): out.append({"name": name, "build_cs": build_cs, "has_public": os.path.isdir(os.path.join(p, "Public")), "has_private": os.path.isdir(os.path.join(p, "Private"))}) return {"ok": True, "modules": out} def _api_macro(module): return f"{module.upper()}_API" _HEADER_TPL = """// Generated by UEBlueprintMCP cpp_scaffold. #pragma once #include "CoreMinimal.h" #include "{base_header}" #include "{name}.generated.h" UCLASS() class {api} {name} : public {parent} {{ \tGENERATED_BODY() public: \t{name}(); }}; """ _CPP_TPL = """// Generated by UEBlueprintMCP cpp_scaffold. #include "{include_path}{name}.h" {name}::{name}() {{ \tPrimaryActorTick.bCanEverTick = false; }} """ # Map of common parents -> include they need _PARENT_HEADERS = { "AActor": "GameFramework/Actor.h", "APawn": "GameFramework/Pawn.h", "ACharacter": "GameFramework/Character.h", "APlayerController": "GameFramework/PlayerController.h", "AGameModeBase": "GameFramework/GameModeBase.h", "AAIController": "AIController.h", "UObject": "UObject/NoExportTypes.h", "UActorComponent": "Components/ActorComponent.h", "USceneComponent": "Components/SceneComponent.h", "UPrimitiveComponent": "Components/PrimitiveComponent.h", "UStaticMeshComponent": "Components/StaticMeshComponent.h", "USkeletalMeshComponent":"Components/SkeletalMeshComponent.h", "UAnimInstance": "Animation/AnimInstance.h", "UUserWidget": "Blueprint/UserWidget.h", "UDataAsset": "Engine/DataAsset.h", "UInterface": "UObject/Interface.h", } def create_class(args): name = args["name"] parent = args["parent_class"] # e.g. AActor / UObject / UActorComponent module = args.get("module") or _default_module() subfolder = (args.get("subfolder") or "").replace("\\", "/").strip("/") header_only = bool(args.get("header_only")) public = bool(args.get("public", True)) if not re.match(r"^[AU][A-Z]\w+$", name): return {"ok": False, "error": "class name must start with A or U followed by PascalCase (e.g. AMyActor, UMyComp)"} src_mod = os.path.join(_source_dir(), module) if not os.path.isdir(src_mod): return {"ok": False, "error": f"module not found: {module}"} pub_dir = os.path.join(src_mod, "Public", subfolder) if public else os.path.join(src_mod, subfolder) priv_dir = os.path.join(src_mod, "Private", subfolder) if public else os.path.join(src_mod, subfolder) os.makedirs(pub_dir, exist_ok=True) if not header_only: os.makedirs(priv_dir, exist_ok=True) base_header = args.get("base_header") or _PARENT_HEADERS.get(parent, "CoreMinimal.h") api = args.get("api_macro") or _api_macro(module) h_path = os.path.join(pub_dir, f"{name}.h") c_path = os.path.join(priv_dir, f"{name}.cpp") if os.path.exists(h_path): return {"ok": False, "error": f"file exists: {h_path}"} h_txt = _HEADER_TPL.format(name=name, parent=parent, api=api, base_header=base_header) with open(h_path, "w", encoding="utf-8", newline="\n") as f: f.write(h_txt) out = {"ok": True, "header": h_path} if not header_only: include_rel = (subfolder + "/") if subfolder else "" c_txt = _CPP_TPL.format(name=name, include_path=include_rel) with open(c_path, "w", encoding="utf-8", newline="\n") as f: f.write(c_txt) out["cpp"] = c_path out["note"] = "Files written. Run live_coding.compile or rebuild from VS to register the class." return out def list_classes(args): module = args.get("module") or _default_module() subfolder = (args.get("subfolder") or "").replace("\\", "/").strip("/") mod_root = os.path.join(_source_dir(), module) if not os.path.isdir(mod_root): return {"ok": False, "error": f"module not found: {mod_root}"} scan_root = os.path.join(mod_root, subfolder) if subfolder else mod_root out = [] for dirpath, _, fnames in os.walk(scan_root): for fn in fnames: if fn.endswith(".h"): full = os.path.join(dirpath, fn) rel = os.path.relpath(full, mod_root).replace("\\", "/") # Skip the module's own header (e.g. .h) if rel == f"{module}.h": continue out.append(rel) return {"ok": True, "module": module, "headers": sorted(out), "count": len(out)} def read_class(args): module = args.get("module") or _default_module() name = args["name"] found = {"header": None, "cpp": None} for dirpath, _, fnames in os.walk(os.path.join(_source_dir(), module)): for fn in fnames: if fn == f"{name}.h": found["header"] = os.path.join(dirpath, fn) elif fn == f"{name}.cpp": found["cpp"] = os.path.join(dirpath, fn) out = {"ok": True} for k, p in found.items(): if p and os.path.isfile(p): try: with open(p, "r", encoding="utf-8") as f: out[k] = {"path": p, "content": f.read()} except Exception as e: out[k] = {"path": p, "error": str(e)} if not (found["header"] or found["cpp"]): return {"ok": False, "error": f"class {name} not found under Source/{module}"} return out OPS = { "list_modules": list_modules, "create_class": create_class, "list_classes": list_classes, "read_class": read_class, } 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-CPP] " + txt[:1500]) except Exception: pass return txt