diff --git a/src/doctor.js b/src/doctor.js index cbddd39..bd3acb4 100644 --- a/src/doctor.js +++ b/src/doctor.js @@ -12,6 +12,7 @@ import { ping, executePythonInUE } from "./ueBridge.js"; import * as PP from "./projectPaths.js"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -68,6 +69,23 @@ export async function runDiagnostics({ deep = true } = {}) { } else { checks.push({ id: "python", label: "Python plugin", status: STATUS.OK, detail: probe.engine ? `UE ${probe.engine}` : "ExecutePythonCommand works" }); + + // The Remote Control editor might be a DIFFERENT project than the one the + // plugin resolved from disk — in which case every project-relative tool + // would target the wrong project. Flag it loudly. + const resolved = PP.projectName(); + const editorName = probe.project + ? path.basename(probe.project.replace(/\\/g, "/")).replace(/\.uproject$/i, "") + : null; + if (resolved && editorName && editorName.toLowerCase() !== resolved.toLowerCase()) { + checks.push({ id: "project_match", label: "Editor ↔ project", status: STATUS.WARN, + detail: `Remote Control is connected to "${editorName}", but this plugin resolves to "${resolved}".`, + fix: `Open the ${resolved} editor (with Remote Control on), or set UE_PROJECT_DIR / run setup so the server targets "${editorName}".` }); + } else if (editorName) { + checks.push({ id: "project_match", label: "Editor ↔ project", status: STATUS.OK, + detail: `live editor = ${editorName}` }); + } + checks.push(probe.has_mcp ? { id: "cpp", label: "C++ module", status: STATUS.OK, detail: "UMCPGraphLibrary is loaded" } : { id: "cpp", label: "C++ module", status: STATUS.FAIL, @@ -107,22 +125,25 @@ async function checkNodeDeps() { } // Ask the live editor to write a probe file we can read back (RC doesn't echo -// Python stdout, so we tee to /Saved/MCP/doctor.json). +// Python stdout). We tee to a MACHINE-LOCAL temp path — not a project-relative +// one — because Remote Control is always localhost, and because the live editor +// may be a different project than the one we resolved from disk (which we want +// to detect, not trip over). The probe reports the editor's own project path. async function probeEditor() { - const outFile = path.join(PP.projectDir(), "Saved", "MCP", "doctor.json").replace(/\\/g, "/"); + const outFile = path.join(os.tmpdir(), "ue_mcp_doctor.json").replace(/\\/g, "/"); try { fs.rmSync(outFile, { force: true }); } catch { /* ignore */ } const py = [ - "import unreal, json, os", + "import unreal, json", "d = {'ok': True}", "try: d['has_mcp'] = hasattr(unreal, 'MCPGraphLibrary')", - "except Exception as e: d['has_mcp'] = False", - "try: d['has_voxel'] = hasattr(unreal, 'MCPVoxelGraphLibrary') and unreal.MCPVoxelGraphLibrary.is_voxel_graph_asset is not None", + "except Exception: d['has_mcp'] = False", + "try: d['has_voxel'] = hasattr(unreal, 'MCPVoxelGraphLibrary')", "except Exception: d['has_voxel'] = False", "try: d['engine'] = unreal.SystemLibrary.get_engine_version()", "except Exception: pass", - "p = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_saved_dir()) + 'MCP/'", - "os.makedirs(p, exist_ok=True)", - "open(p + 'doctor.json', 'w', encoding='utf-8').write(json.dumps(d))", + "try: d['project'] = unreal.Paths.get_project_file_path()", + "except Exception: pass", + `open(r'${outFile}', 'w', encoding='utf-8').write(json.dumps(d))`, ].join("\n"); const r = await executePythonInUE(py, { timeoutMs: 8000 }); if (!r.ok) return { ok: false, error: r.error }; @@ -133,7 +154,7 @@ async function probeEditor() { return { ok: true, ...JSON.parse(txt) }; } catch { await sleep(100); } } - return { ok: false, error: "probe file was not written (Python ran but produced no output)" }; + return { ok: false, error: "Python ran but wrote no probe file — the 'Python Editor Script Plugin' may be disabled." }; } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } @@ -166,5 +187,7 @@ if (isMain) { console.log("\nUnreal Engine MCP — diagnostics\n"); console.log(formatReport(res)); console.log(res.ok ? "\nAll good. ✓\n" : "\nSome checks failed — run `npm run setup` to fix interactively.\n"); - process.exit(res.ok ? 0 : 1); + // Set the code and let the event loop drain naturally — calling process.exit() + // here can abort mid-socket-teardown on Windows (libuv assertion). + process.exitCode = res.ok ? 0 : 1; }