@uemcp/server 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/index.mjs +1676 -10
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import net from "node:net";
|
|
9
9
|
import Fastify from "fastify";
|
|
10
10
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
@@ -362,6 +362,12 @@ const TOOL_METADATA = {
|
|
|
362
362
|
"build.cs"
|
|
363
363
|
]),
|
|
364
364
|
add_post_process_material: mutates("fx", "add_post_process_material"),
|
|
365
|
+
add_reflected_member: mutates("build", void 0, [
|
|
366
|
+
"uproperty",
|
|
367
|
+
"ufunction",
|
|
368
|
+
"header",
|
|
369
|
+
"member"
|
|
370
|
+
]),
|
|
365
371
|
add_sequence_binding: mutates("sequence", "add_sequence_binding"),
|
|
366
372
|
add_sequence_keys: mutates("sequence", "add_sequence_keys"),
|
|
367
373
|
add_sequence_track: mutates("sequence", "add_sequence_track"),
|
|
@@ -463,6 +469,12 @@ const TOOL_METADATA = {
|
|
|
463
469
|
"command",
|
|
464
470
|
"stat"
|
|
465
471
|
]),
|
|
472
|
+
export_blueprint_cpp: readOnly("blueprint", "get_blueprint_summary", [
|
|
473
|
+
"export",
|
|
474
|
+
"c++",
|
|
475
|
+
"skeleton",
|
|
476
|
+
"pseudocode"
|
|
477
|
+
]),
|
|
466
478
|
export_data_table: reachesOutside("asset", true, "export_data_table"),
|
|
467
479
|
find_assets: readOnly("core", "find_assets", [
|
|
468
480
|
"search",
|
|
@@ -470,6 +482,13 @@ const TOOL_METADATA = {
|
|
|
470
482
|
"registry"
|
|
471
483
|
]),
|
|
472
484
|
find_blueprint_references: readOnly("blueprint", "find_blueprint_references"),
|
|
485
|
+
find_symbol: readOnly("build", void 0, [
|
|
486
|
+
"symbol",
|
|
487
|
+
"definition",
|
|
488
|
+
"reference",
|
|
489
|
+
"c++",
|
|
490
|
+
"index"
|
|
491
|
+
]),
|
|
473
492
|
focus_actors: idempotentMutates("level", "focus_actors"),
|
|
474
493
|
generate_collision: idempotentMutates("asset", "generate_collision"),
|
|
475
494
|
generate_lods: idempotentMutates("asset", "generate_lods"),
|
|
@@ -498,6 +517,11 @@ const TOOL_METADATA = {
|
|
|
498
517
|
"reflection"
|
|
499
518
|
]),
|
|
500
519
|
get_compile_status: readOnly("build", "get_compile_status"),
|
|
520
|
+
get_live_compile_details: readOnly("build", "get_live_compile_details", [
|
|
521
|
+
"live coding",
|
|
522
|
+
"patch",
|
|
523
|
+
"module"
|
|
524
|
+
]),
|
|
501
525
|
get_config_value: readOnly("core", "get_config_value"),
|
|
502
526
|
get_crash_callstack: readOnly("core", "get_crash_callstack", [
|
|
503
527
|
"crash",
|
|
@@ -576,11 +600,22 @@ const TOOL_METADATA = {
|
|
|
576
600
|
replace_actors: destroys("level", "replace_actors"),
|
|
577
601
|
run_automation_tests: mutates("test", "start_automation_run"),
|
|
578
602
|
run_clang_tidy: readOnly("build", void 0, ["lint", "static analysis"]),
|
|
603
|
+
run_editor_python: destroys("core", "run_editor_python", [
|
|
604
|
+
"python",
|
|
605
|
+
"script",
|
|
606
|
+
"unreal"
|
|
607
|
+
]),
|
|
579
608
|
run_editor_commandlet: mutates("build"),
|
|
580
609
|
run_geometry_script: mutates("level", "run_geometry_script"),
|
|
581
610
|
run_pcg_generate: mutates("fx", "run_pcg_generate"),
|
|
582
611
|
save_asset: idempotentMutates("asset", "save_asset"),
|
|
583
612
|
save_dirty_packages: idempotentMutates("core", "save_dirty_packages"),
|
|
613
|
+
scaffold_class_template: mutates("build", void 0, [
|
|
614
|
+
"scaffold",
|
|
615
|
+
"template",
|
|
616
|
+
"c++",
|
|
617
|
+
"class"
|
|
618
|
+
]),
|
|
584
619
|
sculpt_landscape: mutates("level", "sculpt_landscape"),
|
|
585
620
|
search_tools: readOnly("core", void 0, ["discover", "find tool"]),
|
|
586
621
|
select_actors: idempotentMutates("level", "select_actors"),
|
|
@@ -1068,6 +1103,13 @@ function resolveDefaultRegistryFilePath(options = {}) {
|
|
|
1068
1103
|
if (platform === "win32") return joinPath(getAbsoluteDirectoryOverride(env.APPDATA, isAbsolutePath) ?? joinPath(homeDirectory, "AppData", "Roaming"), "MCPBridge", "registry.json");
|
|
1069
1104
|
return joinPath(getAbsoluteDirectoryOverride(env.XDG_STATE_HOME, isAbsolutePath) ?? joinPath(homeDirectory, ".local", "state"), "MCPBridge", "registry.json");
|
|
1070
1105
|
}
|
|
1106
|
+
function resolveSymbolIndexCacheDirectory(options = {}) {
|
|
1107
|
+
const env = options.env ?? process.env;
|
|
1108
|
+
const pathModule = (options.platform ?? process.platform) === "win32" ? path.win32 : path.posix;
|
|
1109
|
+
const configured = getAbsoluteDirectoryOverride(env.MCP_SERVER_SYMBOL_INDEX_DIR, pathModule.isAbsolute);
|
|
1110
|
+
if (configured !== void 0) return configured;
|
|
1111
|
+
return pathModule.join(pathModule.dirname(resolveDefaultRegistryFilePath(options)), "symbol-index");
|
|
1112
|
+
}
|
|
1071
1113
|
function getConfiguredRegistryFilePath(env) {
|
|
1072
1114
|
for (const candidate of [env.MCP_SERVER_REGISTRY_FILE, env.MCP_REGISTRY_FILE]) if (candidate && candidate.trim().length > 0) return candidate.trim();
|
|
1073
1115
|
}
|
|
@@ -1674,6 +1716,7 @@ function createBridgeLaunchOnDeviceRequest(deviceId, map, requestId = randomUUID
|
|
|
1674
1716
|
const BRIDGE_START_LIVE_COMPILE_REQUEST_TYPE = "start_live_compile";
|
|
1675
1717
|
const BRIDGE_START_HOT_RELOAD_REQUEST_TYPE = "start_hot_reload";
|
|
1676
1718
|
const BRIDGE_GET_COMPILE_STATUS_REQUEST_TYPE = "get_compile_status";
|
|
1719
|
+
const BRIDGE_GET_LIVE_COMPILE_DETAILS_REQUEST_TYPE = "get_live_compile_details";
|
|
1677
1720
|
const BRIDGE_LIVE_COMPILE_STATES = [
|
|
1678
1721
|
"compiling",
|
|
1679
1722
|
"already_compiling",
|
|
@@ -1710,6 +1753,12 @@ const bridgeGetCompileStatusRequestSchema = z.object({
|
|
|
1710
1753
|
type: z.literal(BRIDGE_GET_COMPILE_STATUS_REQUEST_TYPE),
|
|
1711
1754
|
version: z.literal(2)
|
|
1712
1755
|
});
|
|
1756
|
+
const bridgeGetLiveCompileDetailsRequestSchema = z.object({
|
|
1757
|
+
compileJobId: compileJobIdSchema,
|
|
1758
|
+
requestId: requestIdSchema,
|
|
1759
|
+
type: z.literal(BRIDGE_GET_LIVE_COMPILE_DETAILS_REQUEST_TYPE),
|
|
1760
|
+
version: z.literal(2)
|
|
1761
|
+
});
|
|
1713
1762
|
const bridgeStartLiveCompileSuccessResponseSchema = z.object({
|
|
1714
1763
|
compileJobId: z.string().trim().min(1),
|
|
1715
1764
|
message: z.string().trim().min(1).optional(),
|
|
@@ -1761,9 +1810,30 @@ const bridgeGetCompileStatusSuccessResponseSchema = z.object({
|
|
|
1761
1810
|
path: [field]
|
|
1762
1811
|
});
|
|
1763
1812
|
});
|
|
1813
|
+
const bridgeGetLiveCompileDetailsSuccessResponseSchema = z.object({
|
|
1814
|
+
compileJobId: compileJobIdSchema,
|
|
1815
|
+
ok: z.literal(true),
|
|
1816
|
+
patchedFunctions: z.array(z.string()),
|
|
1817
|
+
patchedModules: z.array(z.string()),
|
|
1818
|
+
rejections: z.array(z.string()),
|
|
1819
|
+
requestId: requestIdSchema,
|
|
1820
|
+
restartReason: z.string().trim().min(1).optional(),
|
|
1821
|
+
restartRequired: z.boolean().optional(),
|
|
1822
|
+
state: compileStatusStateSchema,
|
|
1823
|
+
truncated: z.boolean(),
|
|
1824
|
+
type: z.literal(BRIDGE_GET_LIVE_COMPILE_DETAILS_REQUEST_TYPE),
|
|
1825
|
+
version: z.literal(2)
|
|
1826
|
+
}).superRefine((value, ctx) => {
|
|
1827
|
+
if (value.restartReason !== void 0 && value.restartRequired !== true) ctx.addIssue({
|
|
1828
|
+
code: z.ZodIssueCode.custom,
|
|
1829
|
+
message: "restartReason must be absent unless restartRequired is true",
|
|
1830
|
+
path: ["restartReason"]
|
|
1831
|
+
});
|
|
1832
|
+
});
|
|
1764
1833
|
const bridgeStartLiveCompileResponseSchema = z.union([bridgeStartLiveCompileSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1765
1834
|
const bridgeStartHotReloadResponseSchema = z.union([bridgeStartHotReloadSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1766
1835
|
const bridgeGetCompileStatusResponseSchema = z.union([bridgeGetCompileStatusSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1836
|
+
const bridgeGetLiveCompileDetailsResponseSchema = z.union([bridgeGetLiveCompileDetailsSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1767
1837
|
function createBridgeStartLiveCompileRequest(requestId = randomUUID()) {
|
|
1768
1838
|
return {
|
|
1769
1839
|
requestId,
|
|
@@ -1786,6 +1856,59 @@ function createBridgeGetCompileStatusRequest(compileJobId, requestId = randomUUI
|
|
|
1786
1856
|
version: 2
|
|
1787
1857
|
});
|
|
1788
1858
|
}
|
|
1859
|
+
function createBridgeGetLiveCompileDetailsRequest(compileJobId, requestId = randomUUID()) {
|
|
1860
|
+
return bridgeGetLiveCompileDetailsRequestSchema.parse({
|
|
1861
|
+
compileJobId,
|
|
1862
|
+
requestId,
|
|
1863
|
+
type: BRIDGE_GET_LIVE_COMPILE_DETAILS_REQUEST_TYPE,
|
|
1864
|
+
version: 2
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
//#endregion
|
|
1868
|
+
//#region src/protocol/python.ts
|
|
1869
|
+
const BRIDGE_RUN_EDITOR_PYTHON_REQUEST_TYPE = "run_editor_python";
|
|
1870
|
+
const BRIDGE_PYTHON_EXECUTION_MODES = [
|
|
1871
|
+
"script",
|
|
1872
|
+
"statement",
|
|
1873
|
+
"evaluate"
|
|
1874
|
+
];
|
|
1875
|
+
const BRIDGE_PYTHON_STATES = [
|
|
1876
|
+
"succeeded",
|
|
1877
|
+
"failed",
|
|
1878
|
+
"disabled",
|
|
1879
|
+
"unsupported"
|
|
1880
|
+
];
|
|
1881
|
+
const bridgeRunEditorPythonRequestSchema = z.object({
|
|
1882
|
+
command: z.string().min(1).max(16384),
|
|
1883
|
+
executionMode: z.enum(BRIDGE_PYTHON_EXECUTION_MODES),
|
|
1884
|
+
requestId: requestIdSchema,
|
|
1885
|
+
type: z.literal(BRIDGE_RUN_EDITOR_PYTHON_REQUEST_TYPE),
|
|
1886
|
+
unattended: z.boolean(),
|
|
1887
|
+
version: z.literal(2)
|
|
1888
|
+
});
|
|
1889
|
+
const bridgeRunEditorPythonSuccessResponseSchema = z.object({
|
|
1890
|
+
errors: z.array(z.string()),
|
|
1891
|
+
message: z.string().trim().min(1).optional(),
|
|
1892
|
+
ok: z.literal(true),
|
|
1893
|
+
output: z.array(z.string()),
|
|
1894
|
+
requestId: requestIdSchema,
|
|
1895
|
+
result: z.string().optional(),
|
|
1896
|
+
state: z.enum(BRIDGE_PYTHON_STATES),
|
|
1897
|
+
truncated: z.boolean(),
|
|
1898
|
+
type: z.literal(BRIDGE_RUN_EDITOR_PYTHON_REQUEST_TYPE),
|
|
1899
|
+
version: z.literal(2)
|
|
1900
|
+
});
|
|
1901
|
+
const bridgeRunEditorPythonResponseSchema = z.union([bridgeRunEditorPythonSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1902
|
+
function createBridgeRunEditorPythonRequest(command, options = {}, requestId = randomUUID()) {
|
|
1903
|
+
return bridgeRunEditorPythonRequestSchema.parse({
|
|
1904
|
+
command,
|
|
1905
|
+
executionMode: options.executionMode ?? "script",
|
|
1906
|
+
requestId,
|
|
1907
|
+
type: BRIDGE_RUN_EDITOR_PYTHON_REQUEST_TYPE,
|
|
1908
|
+
unattended: options.unattended ?? true,
|
|
1909
|
+
version: 2
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1789
1912
|
//#endregion
|
|
1790
1913
|
//#region src/protocol/editor.ts
|
|
1791
1914
|
const BRIDGE_GET_EDITOR_STATE_REQUEST_TYPE = "get_editor_state";
|
|
@@ -9127,7 +9250,7 @@ function createBridgeSetDefaultGamemodeRequest(scope, gameModePath, options = {}
|
|
|
9127
9250
|
}
|
|
9128
9251
|
//#endregion
|
|
9129
9252
|
//#region src/version.ts
|
|
9130
|
-
const SERVER_VERSION = "1.
|
|
9253
|
+
const SERVER_VERSION = "1.8.0";
|
|
9131
9254
|
//#endregion
|
|
9132
9255
|
//#region src/bridgeSocketClient.ts
|
|
9133
9256
|
var BridgeSocketError = class extends Error {
|
|
@@ -9162,6 +9285,12 @@ async function sendBridgeStartHotReloadRequest(request, options) {
|
|
|
9162
9285
|
async function sendBridgeGetCompileStatusRequest(request, options) {
|
|
9163
9286
|
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetCompileStatusResponseSchema, request.requestId);
|
|
9164
9287
|
}
|
|
9288
|
+
async function sendBridgeGetLiveCompileDetailsRequest(request, options) {
|
|
9289
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetLiveCompileDetailsResponseSchema, request.requestId);
|
|
9290
|
+
}
|
|
9291
|
+
async function sendBridgeRunEditorPythonRequest(request, options) {
|
|
9292
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeRunEditorPythonResponseSchema, request.requestId);
|
|
9293
|
+
}
|
|
9165
9294
|
async function sendBridgeGetCrashCallstackRequest(request, options) {
|
|
9166
9295
|
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetCrashCallstackResponseSchema, request.requestId);
|
|
9167
9296
|
}
|
|
@@ -13113,8 +13242,323 @@ const behaviorTreeToolDomain = {
|
|
|
13113
13242
|
"add_bt_service"
|
|
13114
13243
|
]
|
|
13115
13244
|
};
|
|
13116
|
-
|
|
13117
|
-
|
|
13245
|
+
const EXEC_PIN_NAMES = /* @__PURE__ */ new Set(["execute", "then"]);
|
|
13246
|
+
const ENTRY_NODE_CLASSES = /* @__PURE__ */ new Set([
|
|
13247
|
+
"K2Node_ComponentBoundEvent",
|
|
13248
|
+
"K2Node_CustomEvent",
|
|
13249
|
+
"K2Node_Event",
|
|
13250
|
+
"K2Node_FunctionEntry",
|
|
13251
|
+
"K2Node_InputAction",
|
|
13252
|
+
"K2Node_InputKey",
|
|
13253
|
+
"K2Node_Timeline"
|
|
13254
|
+
]);
|
|
13255
|
+
const HIDDEN_INPUT_PIN_NAMES = new Set(["self", "WorldContextObject"].map((name) => name.toLowerCase()));
|
|
13256
|
+
function firstTitleLine(node) {
|
|
13257
|
+
const [first] = node.title.split(/\r?\n/u);
|
|
13258
|
+
return (first ?? node.title).trim();
|
|
13259
|
+
}
|
|
13260
|
+
function isExecPin(pin) {
|
|
13261
|
+
return pin.type.trim().toLowerCase() === "exec" || EXEC_PIN_NAMES.has(pin.name.toLowerCase());
|
|
13262
|
+
}
|
|
13263
|
+
function execOutputs(node) {
|
|
13264
|
+
return node.pins.filter((pin) => pin.direction === "output" && isExecPin(pin));
|
|
13265
|
+
}
|
|
13266
|
+
function execInputs(node) {
|
|
13267
|
+
return node.pins.filter((pin) => pin.direction === "input" && isExecPin(pin));
|
|
13268
|
+
}
|
|
13269
|
+
function dataInputs(node) {
|
|
13270
|
+
return node.pins.filter((pin) => pin.direction === "input" && !isExecPin(pin) && !HIDDEN_INPUT_PIN_NAMES.has(pin.name.toLowerCase()));
|
|
13271
|
+
}
|
|
13272
|
+
function findPin(node, names) {
|
|
13273
|
+
const wanted = new Set(names.map((name) => name.toLowerCase()));
|
|
13274
|
+
return node.pins.find((pin) => wanted.has(pin.name.toLowerCase()));
|
|
13275
|
+
}
|
|
13276
|
+
function indent(depth) {
|
|
13277
|
+
return " ".repeat(depth);
|
|
13278
|
+
}
|
|
13279
|
+
var GraphWalker = class {
|
|
13280
|
+
nodesById;
|
|
13281
|
+
lines = [];
|
|
13282
|
+
emitted = /* @__PURE__ */ new Set();
|
|
13283
|
+
truncated = false;
|
|
13284
|
+
constructor(nodes) {
|
|
13285
|
+
this.nodesById = new Map(nodes.map((node) => [node.nodeId, node]));
|
|
13286
|
+
}
|
|
13287
|
+
render(nodes) {
|
|
13288
|
+
for (const root of this.findRoots(nodes)) {
|
|
13289
|
+
this.lines.push(`${this.entryHeader(root)} {`);
|
|
13290
|
+
this.walkFrom(root, 1);
|
|
13291
|
+
this.lines.push("}");
|
|
13292
|
+
this.lines.push("");
|
|
13293
|
+
}
|
|
13294
|
+
while (this.lines.at(-1) === "") this.lines.pop();
|
|
13295
|
+
return {
|
|
13296
|
+
lines: this.lines,
|
|
13297
|
+
truncated: this.truncated
|
|
13298
|
+
};
|
|
13299
|
+
}
|
|
13300
|
+
findRoots(nodes) {
|
|
13301
|
+
return nodes.filter((node) => {
|
|
13302
|
+
if (ENTRY_NODE_CLASSES.has(node.nodeClass)) return true;
|
|
13303
|
+
const inputs = execInputs(node);
|
|
13304
|
+
return inputs.length > 0 && inputs.every((pin) => pin.linkedTo.length === 0) && execOutputs(node).length > 0;
|
|
13305
|
+
}).sort((left, right) => {
|
|
13306
|
+
const byTitle = firstTitleLine(left).localeCompare(firstTitleLine(right));
|
|
13307
|
+
return byTitle === 0 ? left.nodeId.localeCompare(right.nodeId) : byTitle;
|
|
13308
|
+
});
|
|
13309
|
+
}
|
|
13310
|
+
entryHeader(node) {
|
|
13311
|
+
if (node.nodeClass === "K2Node_FunctionEntry") return `function ${firstTitleLine(node)}()`;
|
|
13312
|
+
return `event ${firstTitleLine(node)}()`;
|
|
13313
|
+
}
|
|
13314
|
+
walkFrom(node, depth) {
|
|
13315
|
+
const outputs = execOutputs(node);
|
|
13316
|
+
if (outputs.length <= 1) {
|
|
13317
|
+
const [only] = outputs;
|
|
13318
|
+
if (only !== void 0) this.walkPin(only, depth);
|
|
13319
|
+
return;
|
|
13320
|
+
}
|
|
13321
|
+
const then = findPin(node, ["then"]);
|
|
13322
|
+
if (then !== void 0) {
|
|
13323
|
+
this.walkPin(then, depth);
|
|
13324
|
+
return;
|
|
13325
|
+
}
|
|
13326
|
+
for (const output of outputs) {
|
|
13327
|
+
if (output.linkedTo.length === 0) continue;
|
|
13328
|
+
this.lines.push(`${indent(depth)}// ${output.name}:`);
|
|
13329
|
+
this.walkPin(output, depth + 1);
|
|
13330
|
+
}
|
|
13331
|
+
}
|
|
13332
|
+
walkPin(pin, depth) {
|
|
13333
|
+
const link = pin.linkedTo[0];
|
|
13334
|
+
if (link === void 0) return;
|
|
13335
|
+
const target = this.nodesById.get(link.nodeId);
|
|
13336
|
+
if (target === void 0) {
|
|
13337
|
+
this.truncated = true;
|
|
13338
|
+
return;
|
|
13339
|
+
}
|
|
13340
|
+
this.walkNode(target, depth);
|
|
13341
|
+
}
|
|
13342
|
+
walkNode(node, depth) {
|
|
13343
|
+
if (depth > 32) {
|
|
13344
|
+
this.lines.push(`${indent(depth)}// nesting cap reached`);
|
|
13345
|
+
this.truncated = true;
|
|
13346
|
+
return;
|
|
13347
|
+
}
|
|
13348
|
+
if (this.emitted.has(node.nodeId)) {
|
|
13349
|
+
this.lines.push(`${indent(depth)}goto ${firstTitleLine(node)};`);
|
|
13350
|
+
this.truncated = true;
|
|
13351
|
+
return;
|
|
13352
|
+
}
|
|
13353
|
+
this.emitted.add(node.nodeId);
|
|
13354
|
+
if (node.nodeClass === "K2Node_IfThenElse") {
|
|
13355
|
+
this.emitBranch(node, depth);
|
|
13356
|
+
return;
|
|
13357
|
+
}
|
|
13358
|
+
if (node.nodeClass === "K2Node_ExecutionSequence") {
|
|
13359
|
+
for (const output of execOutputs(node)) this.walkPin(output, depth);
|
|
13360
|
+
return;
|
|
13361
|
+
}
|
|
13362
|
+
const loop = this.loopHeader(node);
|
|
13363
|
+
if (loop !== void 0) {
|
|
13364
|
+
this.lines.push(`${indent(depth)}${loop} {`);
|
|
13365
|
+
const body = findPin(node, ["LoopBody", "Loop Body"]);
|
|
13366
|
+
if (body !== void 0) this.walkPin(body, depth + 1);
|
|
13367
|
+
this.lines.push(`${indent(depth)}}`);
|
|
13368
|
+
const completed = findPin(node, ["Completed"]);
|
|
13369
|
+
if (completed !== void 0) this.walkPin(completed, depth);
|
|
13370
|
+
return;
|
|
13371
|
+
}
|
|
13372
|
+
this.lines.push(`${indent(depth)}${this.statement(node)};`);
|
|
13373
|
+
this.walkFrom(node, depth);
|
|
13374
|
+
}
|
|
13375
|
+
emitBranch(node, depth) {
|
|
13376
|
+
const condition = findPin(node, ["Condition"]);
|
|
13377
|
+
this.lines.push(`${indent(depth)}if (${condition === void 0 ? "true" : this.expression(condition, 0)}) {`);
|
|
13378
|
+
const trueBranch = findPin(node, ["then", "True"]);
|
|
13379
|
+
if (trueBranch !== void 0) this.walkPin(trueBranch, depth + 1);
|
|
13380
|
+
const falseBranch = findPin(node, ["else", "False"]);
|
|
13381
|
+
if (falseBranch !== void 0 && falseBranch.linkedTo.length > 0) {
|
|
13382
|
+
this.lines.push(`${indent(depth)}} else {`);
|
|
13383
|
+
this.walkPin(falseBranch, depth + 1);
|
|
13384
|
+
}
|
|
13385
|
+
this.lines.push(`${indent(depth)}}`);
|
|
13386
|
+
}
|
|
13387
|
+
loopHeader(node) {
|
|
13388
|
+
if (node.nodeClass !== "K2Node_MacroInstance") return;
|
|
13389
|
+
const lowered = firstTitleLine(node).toLowerCase();
|
|
13390
|
+
if (lowered.startsWith("while loop")) {
|
|
13391
|
+
const condition = findPin(node, ["Condition"]);
|
|
13392
|
+
return `while (${condition === void 0 ? "true" : this.expression(condition, 0)})`;
|
|
13393
|
+
}
|
|
13394
|
+
if (lowered.startsWith("for each loop")) {
|
|
13395
|
+
const array = findPin(node, ["Array"]);
|
|
13396
|
+
return `for (Element : ${array === void 0 ? "Array" : this.expression(array, 0)})`;
|
|
13397
|
+
}
|
|
13398
|
+
if (lowered.startsWith("for loop")) {
|
|
13399
|
+
const first = findPin(node, ["First Index", "FirstIndex"]);
|
|
13400
|
+
const last = findPin(node, ["Last Index", "LastIndex"]);
|
|
13401
|
+
return `for (Index = ${first === void 0 ? "0" : this.expression(first, 0)}; Index <= ${last === void 0 ? "0" : this.expression(last, 0)}; Index++)`;
|
|
13402
|
+
}
|
|
13403
|
+
}
|
|
13404
|
+
statement(node) {
|
|
13405
|
+
if (node.nodeClass === "K2Node_VariableSet") {
|
|
13406
|
+
const [assigned] = dataInputs(node);
|
|
13407
|
+
return assigned === void 0 ? firstTitleLine(node) : `${assigned.name} = ${this.expression(assigned, 0)}`;
|
|
13408
|
+
}
|
|
13409
|
+
if (node.nodeClass === "K2Node_FunctionResult") {
|
|
13410
|
+
const values = dataInputs(node).map((pin) => this.expression(pin, 0));
|
|
13411
|
+
return values.length === 0 ? "return" : `return ${values.join(", ")}`;
|
|
13412
|
+
}
|
|
13413
|
+
return this.callExpression(node);
|
|
13414
|
+
}
|
|
13415
|
+
callExpression(node) {
|
|
13416
|
+
const args = dataInputs(node).map((pin) => this.expression(pin, 0));
|
|
13417
|
+
return `${firstTitleLine(node)}(${args.join(", ")})`;
|
|
13418
|
+
}
|
|
13419
|
+
expression(pin, depth) {
|
|
13420
|
+
const link = pin.linkedTo[0];
|
|
13421
|
+
if (link === void 0) {
|
|
13422
|
+
const literal = pin.defaultValue?.trim();
|
|
13423
|
+
return literal !== void 0 && literal.length > 0 ? literal : pin.name;
|
|
13424
|
+
}
|
|
13425
|
+
if (depth >= 8) {
|
|
13426
|
+
this.truncated = true;
|
|
13427
|
+
return pin.name;
|
|
13428
|
+
}
|
|
13429
|
+
const source = this.nodesById.get(link.nodeId);
|
|
13430
|
+
if (source === void 0) {
|
|
13431
|
+
this.truncated = true;
|
|
13432
|
+
return pin.name;
|
|
13433
|
+
}
|
|
13434
|
+
if (source.nodeClass === "K2Node_VariableGet") return source.pins.find((candidate) => candidate.direction === "output" && !isExecPin(candidate))?.name ?? firstTitleLine(source);
|
|
13435
|
+
const args = dataInputs(source).map((input) => this.expression(input, depth + 1));
|
|
13436
|
+
if (args.length === 0 && execInputs(source).length === 0 && source.pins.every((candidate) => !isExecPin(candidate))) return `${firstTitleLine(source)}()`;
|
|
13437
|
+
return `${firstTitleLine(source)}(${args.join(", ")})`;
|
|
13438
|
+
}
|
|
13439
|
+
};
|
|
13440
|
+
function renderGraphPseudoCode(graph) {
|
|
13441
|
+
const { lines, truncated } = new GraphWalker(graph.nodes).render(graph.nodes);
|
|
13442
|
+
return {
|
|
13443
|
+
graphKind: graph.graphKind,
|
|
13444
|
+
graphName: graph.graphName,
|
|
13445
|
+
lines,
|
|
13446
|
+
truncated: truncated || graph.truncated
|
|
13447
|
+
};
|
|
13448
|
+
}
|
|
13449
|
+
const BLUEPRINT_TYPE_TO_CPP = /* @__PURE__ */ new Map([
|
|
13450
|
+
["boolean", "bool"],
|
|
13451
|
+
["float (single-precision)", "float"],
|
|
13452
|
+
["float (double-precision)", "double"],
|
|
13453
|
+
["byte", "uint8"],
|
|
13454
|
+
["integer", "int32"],
|
|
13455
|
+
["integer64", "int64"],
|
|
13456
|
+
["float", "double"],
|
|
13457
|
+
["real", "double"],
|
|
13458
|
+
["name", "FName"],
|
|
13459
|
+
["string", "FString"],
|
|
13460
|
+
["text", "FText"],
|
|
13461
|
+
["vector", "FVector"],
|
|
13462
|
+
["rotator", "FRotator"],
|
|
13463
|
+
["transform", "FTransform"]
|
|
13464
|
+
]);
|
|
13465
|
+
function blueprintTypeToCpp(type) {
|
|
13466
|
+
const trimmed = type.trim();
|
|
13467
|
+
const arrayMatch = /^array of (?<inner>.+)$/iu.exec(trimmed);
|
|
13468
|
+
if (arrayMatch?.groups?.inner !== void 0) return `TArray<${blueprintTypeToCpp(arrayMatch.groups.inner)}>`;
|
|
13469
|
+
return BLUEPRINT_TYPE_TO_CPP.get(trimmed.toLowerCase()) ?? trimmed;
|
|
13470
|
+
}
|
|
13471
|
+
const ACTOR_DERIVED_PARENT_NAMES = new Set([
|
|
13472
|
+
"Actor",
|
|
13473
|
+
"AIController",
|
|
13474
|
+
"Character",
|
|
13475
|
+
"DefaultPawn",
|
|
13476
|
+
"GameMode",
|
|
13477
|
+
"GameModeBase",
|
|
13478
|
+
"GameSession",
|
|
13479
|
+
"GameState",
|
|
13480
|
+
"GameStateBase",
|
|
13481
|
+
"HUD",
|
|
13482
|
+
"Pawn",
|
|
13483
|
+
"PlayerController",
|
|
13484
|
+
"PlayerState",
|
|
13485
|
+
"SpectatorPawn",
|
|
13486
|
+
"WorldSettings"
|
|
13487
|
+
].map((name) => name.toLowerCase()));
|
|
13488
|
+
function blueprintParentToCpp(parentClass) {
|
|
13489
|
+
if (parentClass === void 0 || parentClass.trim().length === 0) return {
|
|
13490
|
+
name: "UObject",
|
|
13491
|
+
prefix: "U"
|
|
13492
|
+
};
|
|
13493
|
+
const sanitized = sanitizeIdentifier(parentClass);
|
|
13494
|
+
if (ACTOR_DERIVED_PARENT_NAMES.has(sanitized.toLowerCase())) return {
|
|
13495
|
+
name: `A${sanitized}`,
|
|
13496
|
+
prefix: "A"
|
|
13497
|
+
};
|
|
13498
|
+
if (/^[AUI][A-Z]/u.test(sanitized)) return {
|
|
13499
|
+
name: sanitized,
|
|
13500
|
+
prefix: sanitized.startsWith("A") ? "A" : "U"
|
|
13501
|
+
};
|
|
13502
|
+
return {
|
|
13503
|
+
name: `U${sanitized}`,
|
|
13504
|
+
prefix: "U"
|
|
13505
|
+
};
|
|
13506
|
+
}
|
|
13507
|
+
function sanitizeIdentifier(value) {
|
|
13508
|
+
const cleaned = value.replace(/[^A-Za-z0-9_]/gu, "");
|
|
13509
|
+
return cleaned.length === 0 ? "Unnamed" : cleaned;
|
|
13510
|
+
}
|
|
13511
|
+
function variableSpecifiers(variable) {
|
|
13512
|
+
const specifiers = ["EditAnywhere", "BlueprintReadWrite"];
|
|
13513
|
+
if (variable.replication === "replicated") specifiers.push("Replicated");
|
|
13514
|
+
if (variable.replication === "rep_notify") specifiers.push(`ReplicatedUsing = OnRep_${sanitizeIdentifier(variable.name)}`);
|
|
13515
|
+
if (variable.category !== void 0 && variable.category.trim().length > 0) specifiers.push(`Category = "${variable.category.trim()}"`);
|
|
13516
|
+
return specifiers.join(", ");
|
|
13517
|
+
}
|
|
13518
|
+
function repNotifyDeclaration(variable) {
|
|
13519
|
+
return [" UFUNCTION()", `\tvoid OnRep_${sanitizeIdentifier(variable.name)}();`];
|
|
13520
|
+
}
|
|
13521
|
+
function functionDeclaration(member) {
|
|
13522
|
+
const returnParameter = member.parameters.find((parameter) => parameter.isReturn);
|
|
13523
|
+
const returnType = returnParameter === void 0 ? "void" : blueprintTypeToCpp(returnParameter.type);
|
|
13524
|
+
const parameters = member.parameters.filter((parameter) => !parameter.isReturn).map((parameter) => {
|
|
13525
|
+
const type = blueprintTypeToCpp(parameter.type);
|
|
13526
|
+
return parameter.isOut ? `${type}& ${sanitizeIdentifier(parameter.name)}` : `${type} ${sanitizeIdentifier(parameter.name)}`;
|
|
13527
|
+
});
|
|
13528
|
+
return [`\tUFUNCTION(${member.kind === "event" ? "BlueprintCallable, Category = \"Events\"" : "BlueprintCallable"})`, `\t${returnType} ${sanitizeIdentifier(member.name)}(${parameters.join(", ")});`];
|
|
13529
|
+
}
|
|
13530
|
+
function renderCppSkeleton(source) {
|
|
13531
|
+
const parent = blueprintParentToCpp(source.parentClass);
|
|
13532
|
+
const lines = [
|
|
13533
|
+
"#pragma once",
|
|
13534
|
+
"",
|
|
13535
|
+
"#include \"CoreMinimal.h\"",
|
|
13536
|
+
"",
|
|
13537
|
+
"UCLASS(BlueprintType, Blueprintable)",
|
|
13538
|
+
`class ${`${parent.prefix}${sanitizeIdentifier(source.blueprintName)}`} : public ${parent.name}`,
|
|
13539
|
+
"{",
|
|
13540
|
+
" GENERATED_BODY()",
|
|
13541
|
+
"",
|
|
13542
|
+
"public:"
|
|
13543
|
+
];
|
|
13544
|
+
for (const interfaceName of source.interfaces) lines.push(`\t// implements ${interfaceName}`);
|
|
13545
|
+
for (const variable of source.variables) {
|
|
13546
|
+
lines.push(`\tUPROPERTY(${variableSpecifiers(variable)})`);
|
|
13547
|
+
lines.push(`\t${blueprintTypeToCpp(variable.type)} ${sanitizeIdentifier(variable.name)};`);
|
|
13548
|
+
if (variable.replication === "rep_notify") {
|
|
13549
|
+
lines.push("");
|
|
13550
|
+
lines.push(...repNotifyDeclaration(variable));
|
|
13551
|
+
}
|
|
13552
|
+
lines.push("");
|
|
13553
|
+
}
|
|
13554
|
+
for (const member of source.functions) {
|
|
13555
|
+
lines.push(...functionDeclaration(member));
|
|
13556
|
+
lines.push("");
|
|
13557
|
+
}
|
|
13558
|
+
while (lines.at(-1) === "") lines.pop();
|
|
13559
|
+
lines.push("};");
|
|
13560
|
+
return lines.join("\n");
|
|
13561
|
+
}
|
|
13118
13562
|
const findBlueprintReferencesInputSchema = {
|
|
13119
13563
|
className: bridgeFindBlueprintReferencesRequestSchema.shape.className.describe("C++ class to look for, as a class path (/Script/MyGame.MyActor) or a class name with or without its prefix (AMyActor, MyActor)."),
|
|
13120
13564
|
cursor: bridgeFindBlueprintReferencesRequestSchema.shape.cursor.describe("Offset into the stable-ordered reference list (default 0). Pass the previous response nextCursor to fetch the next page."),
|
|
@@ -13558,6 +14002,132 @@ function registerBlueprintTools(server, dependencies) {
|
|
|
13558
14002
|
transactionTitle
|
|
13559
14003
|
}, dependencies));
|
|
13560
14004
|
});
|
|
14005
|
+
server.registerTool("export_blueprint_cpp", {
|
|
14006
|
+
description: "Render a Blueprint as readable pseudo-code plus a C++ class skeleton, as the starting point for porting it to C++. Drains every page of the Blueprint summary and of each selected graph, walks exec pins into statements (branches, loops, function calls, variable assignment, returns), and declares each Blueprint variable and function as a UPROPERTY/UFUNCTION. The skeleton is a scaffold, not compilable nativization; types the editor spells differently from C++ are left verbatim.",
|
|
14007
|
+
inputSchema: exportBlueprintCppInputSchema,
|
|
14008
|
+
title: "Export Blueprint as C++"
|
|
14009
|
+
}, async (input) => {
|
|
14010
|
+
return toToolSuccess(await exportBlueprintCpp(input, dependencies));
|
|
14011
|
+
});
|
|
14012
|
+
}
|
|
14013
|
+
const exportBlueprintCppInputSchema = {
|
|
14014
|
+
blueprintPath: bridgeGetBlueprintGraphRequestSchema.shape.blueprintPath.describe("Package name or object path of the Blueprint to export."),
|
|
14015
|
+
graphName: bridgeGetBlueprintGraphRequestSchema.shape.graphName.optional().describe("Export only this graph; defaults to every graph on the Blueprint, up to maxGraphs."),
|
|
14016
|
+
loadIfNeeded: bridgeGetBlueprintGraphRequestSchema.shape.loadIfNeeded.describe("Load the Blueprint if it is not already in memory (default false). Graph contents require the asset in memory."),
|
|
14017
|
+
maxGraphs: z.number().int().min(1).max(50).default(10).describe("Maximum graphs to render when graphName is not given (1-50, default 10)."),
|
|
14018
|
+
maxNodesPerGraph: bridgeGetBlueprintGraphRequestSchema.shape.maxNodes.default(25).describe("Nodes fetched per bridge page while draining a graph (1-200, default 25)."),
|
|
14019
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should read the Blueprint.")
|
|
14020
|
+
};
|
|
14021
|
+
async function collectBlueprintSummary(input, dependencies) {
|
|
14022
|
+
const first = await getBlueprintSummary({
|
|
14023
|
+
blueprintPath: input.blueprintPath,
|
|
14024
|
+
...input.loadIfNeeded === void 0 ? {} : { loadIfNeeded: input.loadIfNeeded },
|
|
14025
|
+
projectId: input.projectId
|
|
14026
|
+
}, dependencies);
|
|
14027
|
+
const summary = {
|
|
14028
|
+
...first,
|
|
14029
|
+
functions: [...first.functions],
|
|
14030
|
+
graphs: [...first.graphs],
|
|
14031
|
+
variables: [...first.variables]
|
|
14032
|
+
};
|
|
14033
|
+
let truncated = first.truncated;
|
|
14034
|
+
let functionCursor = first.nextFunctionCursor;
|
|
14035
|
+
let graphCursor = first.nextGraphCursor;
|
|
14036
|
+
let variableCursor = first.nextVariableCursor;
|
|
14037
|
+
for (let page = 0; page < 20; page += 1) {
|
|
14038
|
+
if (functionCursor === void 0 && graphCursor === void 0 && variableCursor === void 0) return {
|
|
14039
|
+
summary,
|
|
14040
|
+
truncated
|
|
14041
|
+
};
|
|
14042
|
+
const next = await getBlueprintSummary({
|
|
14043
|
+
blueprintPath: input.blueprintPath,
|
|
14044
|
+
...functionCursor === void 0 ? {} : { functionCursor },
|
|
14045
|
+
...graphCursor === void 0 ? {} : { graphCursor },
|
|
14046
|
+
...input.loadIfNeeded === void 0 ? {} : { loadIfNeeded: input.loadIfNeeded },
|
|
14047
|
+
projectId: input.projectId,
|
|
14048
|
+
...variableCursor === void 0 ? {} : { variableCursor }
|
|
14049
|
+
}, dependencies);
|
|
14050
|
+
if (functionCursor !== void 0) summary.functions.push(...next.functions);
|
|
14051
|
+
if (graphCursor !== void 0) summary.graphs.push(...next.graphs);
|
|
14052
|
+
if (variableCursor !== void 0) summary.variables.push(...next.variables);
|
|
14053
|
+
truncated = truncated || next.truncated;
|
|
14054
|
+
functionCursor = functionCursor === void 0 ? void 0 : next.nextFunctionCursor;
|
|
14055
|
+
graphCursor = graphCursor === void 0 ? void 0 : next.nextGraphCursor;
|
|
14056
|
+
variableCursor = variableCursor === void 0 ? void 0 : next.nextVariableCursor;
|
|
14057
|
+
}
|
|
14058
|
+
return {
|
|
14059
|
+
summary,
|
|
14060
|
+
truncated: true
|
|
14061
|
+
};
|
|
14062
|
+
}
|
|
14063
|
+
async function collectGraphNodes(input, graphName, dependencies) {
|
|
14064
|
+
const nodes = [];
|
|
14065
|
+
let cursor;
|
|
14066
|
+
let graphKind = "unknown";
|
|
14067
|
+
let truncated = false;
|
|
14068
|
+
for (let page = 0; page < 40; page += 1) {
|
|
14069
|
+
const response = await getBlueprintGraph({
|
|
14070
|
+
blueprintPath: input.blueprintPath,
|
|
14071
|
+
graphName,
|
|
14072
|
+
...input.loadIfNeeded === void 0 ? {} : { loadIfNeeded: input.loadIfNeeded },
|
|
14073
|
+
maxNodes: input.maxNodesPerGraph,
|
|
14074
|
+
...cursor === void 0 ? {} : { nodeCursor: cursor },
|
|
14075
|
+
projectId: input.projectId
|
|
14076
|
+
}, dependencies);
|
|
14077
|
+
nodes.push(...response.nodes);
|
|
14078
|
+
graphKind = response.graphKind;
|
|
14079
|
+
truncated = truncated || response.truncated;
|
|
14080
|
+
if (response.nextNodeCursor === void 0) return {
|
|
14081
|
+
graphKind,
|
|
14082
|
+
nodes,
|
|
14083
|
+
truncated
|
|
14084
|
+
};
|
|
14085
|
+
cursor = response.nextNodeCursor;
|
|
14086
|
+
}
|
|
14087
|
+
return {
|
|
14088
|
+
graphKind,
|
|
14089
|
+
nodes,
|
|
14090
|
+
truncated: true
|
|
14091
|
+
};
|
|
14092
|
+
}
|
|
14093
|
+
async function exportBlueprintCpp(input, dependencies) {
|
|
14094
|
+
const { summary, truncated: summaryTruncated } = await collectBlueprintSummary(input, dependencies);
|
|
14095
|
+
const selected = input.graphName === void 0 ? summary.graphs.slice(0, input.maxGraphs) : summary.graphs.filter((graph) => graph.name === input.graphName);
|
|
14096
|
+
if (input.graphName !== void 0 && selected.length === 0) throw new Error(`Blueprint '${summary.blueprintPath}' has no graph named '${input.graphName}'.`);
|
|
14097
|
+
const rendered = [];
|
|
14098
|
+
let truncated = summaryTruncated || input.graphName === void 0 && selected.length < summary.graphs.length;
|
|
14099
|
+
for (const graph of selected) {
|
|
14100
|
+
const { graphKind, nodes, truncated: graphTruncated } = await collectGraphNodes(input, graph.name, dependencies);
|
|
14101
|
+
const pseudoCode = renderGraphPseudoCode({
|
|
14102
|
+
graphKind,
|
|
14103
|
+
graphName: graph.name,
|
|
14104
|
+
nodes,
|
|
14105
|
+
truncated: graphTruncated
|
|
14106
|
+
});
|
|
14107
|
+
truncated = truncated || pseudoCode.truncated;
|
|
14108
|
+
rendered.push(pseudoCode);
|
|
14109
|
+
}
|
|
14110
|
+
const skeleton = renderCppSkeleton({
|
|
14111
|
+
blueprintName: summary.blueprintName,
|
|
14112
|
+
functions: summary.functions,
|
|
14113
|
+
interfaces: summary.interfaces,
|
|
14114
|
+
...summary.parentClass === void 0 ? {} : { parentClass: summary.parentClass },
|
|
14115
|
+
variables: summary.variables
|
|
14116
|
+
});
|
|
14117
|
+
dependencies.logger.info({
|
|
14118
|
+
blueprintPath: summary.blueprintPath,
|
|
14119
|
+
graphCount: rendered.length,
|
|
14120
|
+
projectId: summary.projectId
|
|
14121
|
+
}, "Exported Blueprint graphs as pseudo-code");
|
|
14122
|
+
return {
|
|
14123
|
+
blueprintName: summary.blueprintName,
|
|
14124
|
+
blueprintPath: summary.blueprintPath,
|
|
14125
|
+
graphs: rendered,
|
|
14126
|
+
projectId: summary.projectId,
|
|
14127
|
+
pseudoCode: rendered.map((graph) => [`// ${graph.graphName} (${graph.graphKind})`, ...graph.lines].join("\n")).join("\n\n"),
|
|
14128
|
+
skeleton,
|
|
14129
|
+
truncated
|
|
14130
|
+
};
|
|
13561
14131
|
}
|
|
13562
14132
|
const blueprintToolDomain = {
|
|
13563
14133
|
domain: "blueprint",
|
|
@@ -13576,10 +14146,498 @@ const blueprintToolDomain = {
|
|
|
13576
14146
|
"add_bp_component",
|
|
13577
14147
|
"add_bp_interface",
|
|
13578
14148
|
"set_bp_default",
|
|
13579
|
-
"batch_edit"
|
|
14149
|
+
"batch_edit",
|
|
14150
|
+
"export_blueprint_cpp"
|
|
13580
14151
|
]
|
|
13581
14152
|
};
|
|
13582
14153
|
//#endregion
|
|
14154
|
+
//#region src/classScaffold.ts
|
|
14155
|
+
const REFLECTED_MEMBER_KINDS = ["property", "function"];
|
|
14156
|
+
const CLASS_TEMPLATES = [
|
|
14157
|
+
"interface",
|
|
14158
|
+
"game_instance_subsystem",
|
|
14159
|
+
"world_subsystem",
|
|
14160
|
+
"editor_subsystem"
|
|
14161
|
+
];
|
|
14162
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
14163
|
+
const INTERFACE_NAME_PATTERN = /^I[A-Za-z0-9_]+$/u;
|
|
14164
|
+
const UCLASS_NAME_PATTERN = /^U[A-Za-z0-9_]+$/u;
|
|
14165
|
+
function specifierSet(names) {
|
|
14166
|
+
return new Set(names.map((name) => name.toLowerCase()));
|
|
14167
|
+
}
|
|
14168
|
+
const PROPERTY_SPECIFIERS = specifierSet([
|
|
14169
|
+
"AdvancedDisplay",
|
|
14170
|
+
"AssetRegistrySearchable",
|
|
14171
|
+
"BlueprintAssignable",
|
|
14172
|
+
"BlueprintAuthorityOnly",
|
|
14173
|
+
"BlueprintCallable",
|
|
14174
|
+
"BlueprintGetter",
|
|
14175
|
+
"BlueprintReadOnly",
|
|
14176
|
+
"BlueprintReadWrite",
|
|
14177
|
+
"BlueprintSetter",
|
|
14178
|
+
"Config",
|
|
14179
|
+
"DuplicateTransient",
|
|
14180
|
+
"EditAnywhere",
|
|
14181
|
+
"EditDefaultsOnly",
|
|
14182
|
+
"EditFixedSize",
|
|
14183
|
+
"EditInline",
|
|
14184
|
+
"EditInstanceOnly",
|
|
14185
|
+
"ExportObject",
|
|
14186
|
+
"GlobalConfig",
|
|
14187
|
+
"Instanced",
|
|
14188
|
+
"Interp",
|
|
14189
|
+
"Meta",
|
|
14190
|
+
"NoClear",
|
|
14191
|
+
"NonPIEDuplicateTransient",
|
|
14192
|
+
"NonTransactional",
|
|
14193
|
+
"NotReplicated",
|
|
14194
|
+
"Replicated",
|
|
14195
|
+
"ReplicatedUsing",
|
|
14196
|
+
"SaveGame",
|
|
14197
|
+
"SimpleDisplay",
|
|
14198
|
+
"SkipSerialization",
|
|
14199
|
+
"TextExportTransient",
|
|
14200
|
+
"Transient",
|
|
14201
|
+
"VisibleAnywhere",
|
|
14202
|
+
"VisibleDefaultsOnly",
|
|
14203
|
+
"VisibleInstanceOnly"
|
|
14204
|
+
]);
|
|
14205
|
+
const FUNCTION_SPECIFIERS = specifierSet([
|
|
14206
|
+
"BlueprintAuthorityOnly",
|
|
14207
|
+
"BlueprintCallable",
|
|
14208
|
+
"BlueprintCosmetic",
|
|
14209
|
+
"BlueprintImplementableEvent",
|
|
14210
|
+
"BlueprintInternalUseOnly",
|
|
14211
|
+
"BlueprintNativeEvent",
|
|
14212
|
+
"BlueprintPure",
|
|
14213
|
+
"CallInEditor",
|
|
14214
|
+
"Client",
|
|
14215
|
+
"Const",
|
|
14216
|
+
"Custom",
|
|
14217
|
+
"CustomThunk",
|
|
14218
|
+
"Exec",
|
|
14219
|
+
"Meta",
|
|
14220
|
+
"NetMulticast",
|
|
14221
|
+
"Reliable",
|
|
14222
|
+
"SealedEvent",
|
|
14223
|
+
"Server",
|
|
14224
|
+
"ServiceRequest",
|
|
14225
|
+
"ServiceResponse",
|
|
14226
|
+
"Unreliable",
|
|
14227
|
+
"WithValidation"
|
|
14228
|
+
]);
|
|
14229
|
+
const EDIT_SPECIFIER_NAMES = [
|
|
14230
|
+
"EditAnywhere",
|
|
14231
|
+
"EditDefaultsOnly",
|
|
14232
|
+
"EditInstanceOnly"
|
|
14233
|
+
];
|
|
14234
|
+
const VISIBLE_SPECIFIER_NAMES = [
|
|
14235
|
+
"VisibleAnywhere",
|
|
14236
|
+
"VisibleDefaultsOnly",
|
|
14237
|
+
"VisibleInstanceOnly"
|
|
14238
|
+
];
|
|
14239
|
+
const RPC_SPECIFIER_NAMES = [
|
|
14240
|
+
"Server",
|
|
14241
|
+
"Client",
|
|
14242
|
+
"NetMulticast"
|
|
14243
|
+
];
|
|
14244
|
+
const CATEGORY_REQUIRING_SPECIFIER_NAMES = [
|
|
14245
|
+
...EDIT_SPECIFIER_NAMES,
|
|
14246
|
+
...VISIBLE_SPECIFIER_NAMES,
|
|
14247
|
+
"BlueprintReadOnly",
|
|
14248
|
+
"BlueprintReadWrite"
|
|
14249
|
+
];
|
|
14250
|
+
function normalizeSpecifierName(specifier) {
|
|
14251
|
+
const [name] = specifier.split("=");
|
|
14252
|
+
return (name ?? specifier).trim().toLowerCase();
|
|
14253
|
+
}
|
|
14254
|
+
function hasSpecifier(specifiers, name) {
|
|
14255
|
+
const wanted = name.toLowerCase();
|
|
14256
|
+
return specifiers.some((specifier) => normalizeSpecifierName(specifier) === wanted);
|
|
14257
|
+
}
|
|
14258
|
+
function assertKnownSpecifiers(specifiers, kind) {
|
|
14259
|
+
const known = kind === "property" ? PROPERTY_SPECIFIERS : FUNCTION_SPECIFIERS;
|
|
14260
|
+
const macro = kind === "property" ? "UPROPERTY" : "UFUNCTION";
|
|
14261
|
+
for (const specifier of specifiers) {
|
|
14262
|
+
const trimmed = specifier.trim();
|
|
14263
|
+
if (trimmed.length === 0) throw new ModuleScaffoldError(`${macro} specifiers cannot contain an empty entry.`);
|
|
14264
|
+
if (!known.has(normalizeSpecifierName(trimmed))) throw new ModuleScaffoldError(`'${trimmed}' is not a recognised ${macro} specifier.`);
|
|
14265
|
+
}
|
|
14266
|
+
}
|
|
14267
|
+
function validatePropertySpecifiers(specifiers) {
|
|
14268
|
+
assertKnownSpecifiers(specifiers, "property");
|
|
14269
|
+
const editSpecifiers = EDIT_SPECIFIER_NAMES.filter((name) => hasSpecifier(specifiers, name));
|
|
14270
|
+
const visibleSpecifiers = VISIBLE_SPECIFIER_NAMES.filter((name) => hasSpecifier(specifiers, name));
|
|
14271
|
+
if (editSpecifiers.length + visibleSpecifiers.length > 1) throw new ModuleScaffoldError(`UPROPERTY takes at most one of ${[...EDIT_SPECIFIER_NAMES, ...VISIBLE_SPECIFIER_NAMES].join(", ")}; got ${[...editSpecifiers, ...visibleSpecifiers].join(", ")}.`);
|
|
14272
|
+
if (hasSpecifier(specifiers, "BlueprintReadOnly") && hasSpecifier(specifiers, "BlueprintReadWrite")) throw new ModuleScaffoldError("UPROPERTY cannot be both BlueprintReadOnly and BlueprintReadWrite.");
|
|
14273
|
+
if (hasSpecifier(specifiers, "Replicated") && hasSpecifier(specifiers, "ReplicatedUsing")) throw new ModuleScaffoldError("UPROPERTY cannot be both Replicated and ReplicatedUsing.");
|
|
14274
|
+
const replicatedUsing = specifiers.find((specifier) => normalizeSpecifierName(specifier) === "ReplicatedUsing".toLowerCase());
|
|
14275
|
+
if (replicatedUsing !== void 0 && !replicatedUsing.includes("=")) throw new ModuleScaffoldError("ReplicatedUsing needs a handler function, e.g. ReplicatedUsing = OnRep_Health.");
|
|
14276
|
+
if (hasSpecifier(specifiers, "Config") && hasSpecifier(specifiers, "GlobalConfig")) throw new ModuleScaffoldError("UPROPERTY cannot be both Config and GlobalConfig.");
|
|
14277
|
+
if (hasSpecifier(specifiers, "Transient") && hasSpecifier(specifiers, "Config")) throw new ModuleScaffoldError("A Transient UPROPERTY cannot also be Config.");
|
|
14278
|
+
if ((hasSpecifier(specifiers, "Replicated") || hasSpecifier(specifiers, "ReplicatedUsing")) && hasSpecifier(specifiers, "NotReplicated")) throw new ModuleScaffoldError("UPROPERTY cannot be both replicated and NotReplicated.");
|
|
14279
|
+
}
|
|
14280
|
+
function validateFunctionSpecifiers(specifiers, returnType) {
|
|
14281
|
+
assertKnownSpecifiers(specifiers, "function");
|
|
14282
|
+
if (hasSpecifier(specifiers, "BlueprintPure") && hasSpecifier(specifiers, "BlueprintCallable")) throw new ModuleScaffoldError("UFUNCTION cannot be both BlueprintPure and BlueprintCallable.");
|
|
14283
|
+
if (hasSpecifier(specifiers, "BlueprintImplementableEvent") && hasSpecifier(specifiers, "BlueprintNativeEvent")) throw new ModuleScaffoldError("UFUNCTION cannot be both BlueprintImplementableEvent and BlueprintNativeEvent.");
|
|
14284
|
+
const rpcs = RPC_SPECIFIER_NAMES.filter((name) => hasSpecifier(specifiers, name));
|
|
14285
|
+
if (rpcs.length > 1) throw new ModuleScaffoldError(`UFUNCTION takes at most one of Server, Client, NetMulticast; got ${rpcs.join(", ")}.`);
|
|
14286
|
+
if (rpcs.length === 1) {
|
|
14287
|
+
if (!hasSpecifier(specifiers, "Reliable") && !hasSpecifier(specifiers, "Unreliable")) throw new ModuleScaffoldError("A replicated UFUNCTION must also be Reliable or Unreliable.");
|
|
14288
|
+
if (returnType.trim() !== "void") throw new ModuleScaffoldError(`A replicated UFUNCTION must return void, got '${returnType.trim()}'.`);
|
|
14289
|
+
}
|
|
14290
|
+
if (hasSpecifier(specifiers, "Reliable") && hasSpecifier(specifiers, "Unreliable")) throw new ModuleScaffoldError("UFUNCTION cannot be both Reliable and Unreliable.");
|
|
14291
|
+
if (rpcs.length === 0 && (hasSpecifier(specifiers, "Reliable") || hasSpecifier(specifiers, "Unreliable"))) throw new ModuleScaffoldError("Reliable and Unreliable only apply to a Server, Client or NetMulticast UFUNCTION.");
|
|
14292
|
+
}
|
|
14293
|
+
function renderSpecifierList(specifiers, category) {
|
|
14294
|
+
const rendered = specifiers.map((specifier) => specifier.trim());
|
|
14295
|
+
if (category !== void 0 && category.trim().length > 0 && !rendered.some((specifier) => normalizeSpecifierName(specifier) === "Category".toLowerCase())) rendered.push(`Category = "${category.trim()}"`);
|
|
14296
|
+
return rendered.join(", ");
|
|
14297
|
+
}
|
|
14298
|
+
function requiresCategory(specifiers) {
|
|
14299
|
+
return CATEGORY_REQUIRING_SPECIFIER_NAMES.some((name) => hasSpecifier(specifiers, name));
|
|
14300
|
+
}
|
|
14301
|
+
function maskCommentsAndLiterals(text) {
|
|
14302
|
+
const masked = text.split("");
|
|
14303
|
+
const charLiteralPattern = /'(?:\\.|[^'\\\n])'/y;
|
|
14304
|
+
const blank = (start, end) => {
|
|
14305
|
+
for (let index = start; index < end && index < masked.length; index += 1) if (masked[index] !== "\n") masked[index] = " ";
|
|
14306
|
+
};
|
|
14307
|
+
let index = 0;
|
|
14308
|
+
while (index < text.length) {
|
|
14309
|
+
const character = text[index];
|
|
14310
|
+
if (character === "/" && text[index + 1] === "/") {
|
|
14311
|
+
const end = text.indexOf("\n", index);
|
|
14312
|
+
const stop = end === -1 ? text.length : end;
|
|
14313
|
+
blank(index, stop);
|
|
14314
|
+
index = stop;
|
|
14315
|
+
continue;
|
|
14316
|
+
}
|
|
14317
|
+
if (character === "/" && text[index + 1] === "*") {
|
|
14318
|
+
const end = text.indexOf("*/", index + 2);
|
|
14319
|
+
const stop = end === -1 ? text.length : end + 2;
|
|
14320
|
+
blank(index, stop);
|
|
14321
|
+
index = stop;
|
|
14322
|
+
continue;
|
|
14323
|
+
}
|
|
14324
|
+
if (character === "R" && text[index + 1] === "\"") {
|
|
14325
|
+
const open = text.indexOf("(", index + 2);
|
|
14326
|
+
const delimiter = open === -1 ? void 0 : text.slice(index + 2, open);
|
|
14327
|
+
if (delimiter !== void 0 && !/[\s()\\]/u.test(delimiter)) {
|
|
14328
|
+
const end = text.indexOf(`)${delimiter}"`, open + 1);
|
|
14329
|
+
const stop = end === -1 ? text.length : end + delimiter.length + 2;
|
|
14330
|
+
blank(index, stop);
|
|
14331
|
+
index = stop;
|
|
14332
|
+
continue;
|
|
14333
|
+
}
|
|
14334
|
+
}
|
|
14335
|
+
if (character === "\"") {
|
|
14336
|
+
let scan = index + 1;
|
|
14337
|
+
while (scan < text.length && text[scan] !== "\"") scan += text[scan] === "\\" ? 2 : 1;
|
|
14338
|
+
const stop = Math.min(scan + 1, text.length);
|
|
14339
|
+
blank(index, stop);
|
|
14340
|
+
index = stop;
|
|
14341
|
+
continue;
|
|
14342
|
+
}
|
|
14343
|
+
if (character === "'") {
|
|
14344
|
+
charLiteralPattern.lastIndex = index;
|
|
14345
|
+
const match = charLiteralPattern.exec(text);
|
|
14346
|
+
if (match !== null) {
|
|
14347
|
+
blank(index, index + match[0].length);
|
|
14348
|
+
index += match[0].length;
|
|
14349
|
+
continue;
|
|
14350
|
+
}
|
|
14351
|
+
}
|
|
14352
|
+
index += 1;
|
|
14353
|
+
}
|
|
14354
|
+
return masked.join("");
|
|
14355
|
+
}
|
|
14356
|
+
function findDeclarationHeadEnd(masked, start) {
|
|
14357
|
+
let depth = 0;
|
|
14358
|
+
for (let index = start; index < masked.length; index += 1) {
|
|
14359
|
+
const character = masked[index];
|
|
14360
|
+
if (character === "(") {
|
|
14361
|
+
depth += 1;
|
|
14362
|
+
continue;
|
|
14363
|
+
}
|
|
14364
|
+
if (character === ")") {
|
|
14365
|
+
depth -= 1;
|
|
14366
|
+
continue;
|
|
14367
|
+
}
|
|
14368
|
+
if (depth === 0 && (character === ";" || character === "{" || character === ":")) return index;
|
|
14369
|
+
}
|
|
14370
|
+
return -1;
|
|
14371
|
+
}
|
|
14372
|
+
function findClassBody(masked, className) {
|
|
14373
|
+
const declarationPattern = /^[ \t]*(?:class|struct)\b/gmu;
|
|
14374
|
+
for (let match = declarationPattern.exec(masked); match !== null; match = declarationPattern.exec(masked)) {
|
|
14375
|
+
const headEnd = findDeclarationHeadEnd(masked, match.index);
|
|
14376
|
+
if (headEnd === -1) continue;
|
|
14377
|
+
if (masked.slice(match.index, headEnd).trim().split(/\s+/u).filter((token) => token !== "final").at(-1) !== className || masked[headEnd] === ";") continue;
|
|
14378
|
+
const bodyStart = masked.indexOf("{", headEnd);
|
|
14379
|
+
if (bodyStart === -1) continue;
|
|
14380
|
+
let depth = 0;
|
|
14381
|
+
for (let index = bodyStart; index < masked.length; index += 1) {
|
|
14382
|
+
if (masked[index] === "{") {
|
|
14383
|
+
depth += 1;
|
|
14384
|
+
continue;
|
|
14385
|
+
}
|
|
14386
|
+
if (masked[index] !== "}") continue;
|
|
14387
|
+
depth -= 1;
|
|
14388
|
+
if (depth === 0) return {
|
|
14389
|
+
end: index,
|
|
14390
|
+
start: bodyStart
|
|
14391
|
+
};
|
|
14392
|
+
}
|
|
14393
|
+
return {
|
|
14394
|
+
end: masked.length,
|
|
14395
|
+
start: bodyStart
|
|
14396
|
+
};
|
|
14397
|
+
}
|
|
14398
|
+
}
|
|
14399
|
+
function insertMemberIntoHeader(headerText, className, memberLines) {
|
|
14400
|
+
const masked = maskCommentsAndLiterals(headerText);
|
|
14401
|
+
const body = findClassBody(masked, className);
|
|
14402
|
+
if (body === void 0) throw new ModuleScaffoldError(`Header does not declare a class or struct named '${className}'.`);
|
|
14403
|
+
const bodyMatch = /^[ \t]*GENERATED_(?:BODY|UCLASS_BODY|USTRUCT_BODY|IINTERFACE_BODY)\(\)[ \t]*$/mu.exec(masked.slice(body.start, body.end));
|
|
14404
|
+
if (bodyMatch === null) throw new ModuleScaffoldError(`Class '${className}' has no GENERATED_BODY() to insert after.`);
|
|
14405
|
+
const insertionOffset = body.start + bodyMatch.index + bodyMatch[0].length;
|
|
14406
|
+
const block = [
|
|
14407
|
+
"",
|
|
14408
|
+
"",
|
|
14409
|
+
"public:",
|
|
14410
|
+
...memberLines
|
|
14411
|
+
].join("\n");
|
|
14412
|
+
return `${headerText.slice(0, insertionOffset)}${block}${headerText.slice(insertionOffset)}`;
|
|
14413
|
+
}
|
|
14414
|
+
const UE_CLASS_REFERENCE_PATTERN = /\b([AUI][A-Z][A-Za-z0-9_]*)\s*[*&>]/gu;
|
|
14415
|
+
function ensureForwardDeclarations(headerText, signature, className) {
|
|
14416
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
14417
|
+
for (const match of signature.matchAll(UE_CLASS_REFERENCE_PATTERN)) {
|
|
14418
|
+
const typeName = match[1];
|
|
14419
|
+
if (typeName === void 0 || typeName === className) continue;
|
|
14420
|
+
if (new RegExp(String.raw`\b${typeName}\b`, "u").test(headerText)) continue;
|
|
14421
|
+
candidates.add(typeName);
|
|
14422
|
+
}
|
|
14423
|
+
if (candidates.size === 0) return {
|
|
14424
|
+
addedTypes: [],
|
|
14425
|
+
headerText
|
|
14426
|
+
};
|
|
14427
|
+
const macroMatch = /^[ \t]*(?:UCLASS|USTRUCT|UINTERFACE|UENUM)\s*\(/mu.exec(headerText);
|
|
14428
|
+
if (macroMatch === null) return {
|
|
14429
|
+
addedTypes: [],
|
|
14430
|
+
headerText
|
|
14431
|
+
};
|
|
14432
|
+
const addedTypes = [...candidates].sort();
|
|
14433
|
+
const block = `${addedTypes.map((typeName) => `class ${typeName};`).join("\n")}\n\n`;
|
|
14434
|
+
return {
|
|
14435
|
+
addedTypes,
|
|
14436
|
+
headerText: `${headerText.slice(0, macroMatch.index)}${block}${headerText.slice(macroMatch.index)}`
|
|
14437
|
+
};
|
|
14438
|
+
}
|
|
14439
|
+
function memberAlreadyDeclared(headerText, name) {
|
|
14440
|
+
return new RegExp(String.raw`\b${name}\b\s*(?:[;=(\[])`, "u").test(headerText);
|
|
14441
|
+
}
|
|
14442
|
+
function deriveSourcePath(headerPath) {
|
|
14443
|
+
const parsed = path.parse(headerPath);
|
|
14444
|
+
const segments = parsed.dir.split(path.sep);
|
|
14445
|
+
const publicIndex = segments.lastIndexOf("Public");
|
|
14446
|
+
const directory = publicIndex === -1 ? parsed.dir : [
|
|
14447
|
+
...segments.slice(0, publicIndex),
|
|
14448
|
+
"Private",
|
|
14449
|
+
...segments.slice(publicIndex + 1)
|
|
14450
|
+
].join(path.sep);
|
|
14451
|
+
return path.join(directory, `${parsed.name}.cpp`);
|
|
14452
|
+
}
|
|
14453
|
+
async function fileExists(filePath) {
|
|
14454
|
+
try {
|
|
14455
|
+
await stat(filePath);
|
|
14456
|
+
return true;
|
|
14457
|
+
} catch {
|
|
14458
|
+
return false;
|
|
14459
|
+
}
|
|
14460
|
+
}
|
|
14461
|
+
async function addReflectedMember(input) {
|
|
14462
|
+
const name = input.name.trim();
|
|
14463
|
+
if (!IDENTIFIER_PATTERN.test(name)) throw new ModuleScaffoldError(`Member name '${name}' is not a valid C++ identifier.`);
|
|
14464
|
+
const className = input.className.trim();
|
|
14465
|
+
if (!IDENTIFIER_PATTERN.test(className)) throw new ModuleScaffoldError(`Class name '${className}' is not a valid C++ identifier.`);
|
|
14466
|
+
const headerPath = path.resolve(input.headerPath.trim());
|
|
14467
|
+
let headerText;
|
|
14468
|
+
try {
|
|
14469
|
+
headerText = await readFile(headerPath, "utf8");
|
|
14470
|
+
} catch (error) {
|
|
14471
|
+
throw new ModuleScaffoldError(`Header not found at ${headerPath}.`, { cause: error });
|
|
14472
|
+
}
|
|
14473
|
+
if (memberAlreadyDeclared(headerText, name)) throw new ModuleScaffoldError(`'${name}' is already declared in ${headerPath}.`);
|
|
14474
|
+
if (input.kind === "property") return await addProperty(input, {
|
|
14475
|
+
className,
|
|
14476
|
+
headerPath,
|
|
14477
|
+
headerText,
|
|
14478
|
+
name
|
|
14479
|
+
});
|
|
14480
|
+
return await addFunction(input, {
|
|
14481
|
+
className,
|
|
14482
|
+
headerPath,
|
|
14483
|
+
headerText,
|
|
14484
|
+
name
|
|
14485
|
+
});
|
|
14486
|
+
}
|
|
14487
|
+
async function addProperty(input, context) {
|
|
14488
|
+
const type = input.type?.trim();
|
|
14489
|
+
if (type === void 0 || type.length === 0) throw new ModuleScaffoldError("A UPROPERTY needs a type.");
|
|
14490
|
+
validatePropertySpecifiers(input.specifiers);
|
|
14491
|
+
if (requiresCategory(input.specifiers) && (input.category === void 0 || input.category.trim().length === 0)) throw new ModuleScaffoldError("A UPROPERTY exposed to the editor or Blueprint needs a category.");
|
|
14492
|
+
const initializer = input.defaultValue?.trim();
|
|
14493
|
+
const declaration = `\t${type} ${context.name}${initializer === void 0 || initializer.length === 0 ? "" : ` = ${initializer}`};`;
|
|
14494
|
+
const memberLines = [`\tUPROPERTY(${renderSpecifierList(input.specifiers, input.category)})`, declaration];
|
|
14495
|
+
const forwardDeclared = ensureForwardDeclarations(context.headerText, type, context.className);
|
|
14496
|
+
await writeFile(context.headerPath, insertMemberIntoHeader(forwardDeclared.headerText, context.className, memberLines), "utf8");
|
|
14497
|
+
return {
|
|
14498
|
+
className: context.className,
|
|
14499
|
+
declaration: memberLines.join("\n"),
|
|
14500
|
+
forwardDeclaredTypes: forwardDeclared.addedTypes,
|
|
14501
|
+
headerPath: context.headerPath,
|
|
14502
|
+
name: context.name
|
|
14503
|
+
};
|
|
14504
|
+
}
|
|
14505
|
+
async function addFunction(input, context) {
|
|
14506
|
+
const returnType = input.returnType?.trim() ?? "void";
|
|
14507
|
+
const parameters = input.parameters?.trim() ?? "";
|
|
14508
|
+
validateFunctionSpecifiers(input.specifiers, returnType);
|
|
14509
|
+
if (requiresCategory(input.specifiers) && (input.category === void 0 || input.category.trim().length === 0)) throw new ModuleScaffoldError("A UFUNCTION exposed to Blueprint needs a category.");
|
|
14510
|
+
const declaration = `\t${returnType} ${context.name}(${parameters});`;
|
|
14511
|
+
const memberLines = [`\tUFUNCTION(${renderSpecifierList(input.specifiers, input.category)})`, declaration];
|
|
14512
|
+
const forwardDeclared = ensureForwardDeclarations(context.headerText, `${returnType} ${parameters}`, context.className);
|
|
14513
|
+
await writeFile(context.headerPath, insertMemberIntoHeader(forwardDeclared.headerText, context.className, memberLines), "utf8");
|
|
14514
|
+
const result = {
|
|
14515
|
+
className: context.className,
|
|
14516
|
+
declaration: memberLines.join("\n"),
|
|
14517
|
+
forwardDeclaredTypes: forwardDeclared.addedTypes,
|
|
14518
|
+
headerPath: context.headerPath,
|
|
14519
|
+
name: context.name
|
|
14520
|
+
};
|
|
14521
|
+
if (hasSpecifier(input.specifiers, "BlueprintImplementableEvent")) return result;
|
|
14522
|
+
const sourcePath = path.resolve((input.sourcePath ?? deriveSourcePath(context.headerPath)).trim());
|
|
14523
|
+
if (!await fileExists(sourcePath)) return result;
|
|
14524
|
+
const body = returnType === "void" ? "" : " return {};\n";
|
|
14525
|
+
const definedName = hasSpecifier(input.specifiers, "BlueprintNativeEvent") ? `${context.name}_Implementation` : context.name;
|
|
14526
|
+
const definition = `\n${returnType} ${context.className}::${definedName}(${parameters})\n{\n${body}}\n`;
|
|
14527
|
+
const sourceText = await readFile(sourcePath, "utf8");
|
|
14528
|
+
await writeFile(sourcePath, `${sourceText.replace(/\s*$/u, "\n")}${definition}`, "utf8");
|
|
14529
|
+
result.definition = definition.trim();
|
|
14530
|
+
result.sourcePath = sourcePath;
|
|
14531
|
+
return result;
|
|
14532
|
+
}
|
|
14533
|
+
const SUBSYSTEM_LIFECYCLE = [{
|
|
14534
|
+
declaration: "virtual void Initialize(FSubsystemCollectionBase& Collection) override;",
|
|
14535
|
+
definition: "void %CLASS%::Initialize(FSubsystemCollectionBase& Collection)\n{\n Super::Initialize(Collection);\n}"
|
|
14536
|
+
}, {
|
|
14537
|
+
declaration: "virtual void Deinitialize() override;",
|
|
14538
|
+
definition: "void %CLASS%::Deinitialize()\n{\n Super::Deinitialize();\n}"
|
|
14539
|
+
}];
|
|
14540
|
+
const CLASS_TEMPLATE_SPECS = {
|
|
14541
|
+
editor_subsystem: {
|
|
14542
|
+
headerIncludes: ["EditorSubsystem.h"],
|
|
14543
|
+
overrides: SUBSYSTEM_LIFECYCLE,
|
|
14544
|
+
parentClass: "UEditorSubsystem",
|
|
14545
|
+
requiredModuleDependencies: ["EditorSubsystem"]
|
|
14546
|
+
},
|
|
14547
|
+
game_instance_subsystem: {
|
|
14548
|
+
headerIncludes: ["Subsystems/GameInstanceSubsystem.h"],
|
|
14549
|
+
overrides: SUBSYSTEM_LIFECYCLE,
|
|
14550
|
+
parentClass: "UGameInstanceSubsystem",
|
|
14551
|
+
requiredModuleDependencies: ["Engine"]
|
|
14552
|
+
},
|
|
14553
|
+
world_subsystem: {
|
|
14554
|
+
headerIncludes: ["Subsystems/WorldSubsystem.h"],
|
|
14555
|
+
overrides: SUBSYSTEM_LIFECYCLE,
|
|
14556
|
+
parentClass: "UWorldSubsystem",
|
|
14557
|
+
requiredModuleDependencies: ["Engine"]
|
|
14558
|
+
}
|
|
14559
|
+
};
|
|
14560
|
+
function renderInterfaceHeader(interfaceName, apiMacro, includePath) {
|
|
14561
|
+
const shimClassName = `U${interfaceName.slice(1)}`;
|
|
14562
|
+
return `#pragma once
|
|
14563
|
+
|
|
14564
|
+
#include "CoreMinimal.h"
|
|
14565
|
+
#include "UObject/Interface.h"
|
|
14566
|
+
|
|
14567
|
+
#include "${path.basename(includePath, ".h")}.generated.h"
|
|
14568
|
+
|
|
14569
|
+
UINTERFACE(MinimalAPI, BlueprintType)
|
|
14570
|
+
class ${shimClassName} : public UInterface
|
|
14571
|
+
{
|
|
14572
|
+
\tGENERATED_BODY()
|
|
14573
|
+
};
|
|
14574
|
+
|
|
14575
|
+
class ${apiMacro} ${interfaceName}
|
|
14576
|
+
{
|
|
14577
|
+
\tGENERATED_BODY()
|
|
14578
|
+
|
|
14579
|
+
public:
|
|
14580
|
+
};
|
|
14581
|
+
`;
|
|
14582
|
+
}
|
|
14583
|
+
function renderSubsystemHeader(className, apiMacro, includePath, spec) {
|
|
14584
|
+
const includes = spec.headerIncludes.map((include) => `#include "${include}"`).join("\n");
|
|
14585
|
+
const overrides = spec.overrides.map((entry) => `\t${entry.declaration}`).join("\n");
|
|
14586
|
+
return `#pragma once
|
|
14587
|
+
|
|
14588
|
+
#include "CoreMinimal.h"
|
|
14589
|
+
${includes}
|
|
14590
|
+
|
|
14591
|
+
#include "${path.basename(includePath, ".h")}.generated.h"
|
|
14592
|
+
|
|
14593
|
+
UCLASS()
|
|
14594
|
+
class ${apiMacro} ${className} : public ${spec.parentClass}
|
|
14595
|
+
{
|
|
14596
|
+
\tGENERATED_BODY()
|
|
14597
|
+
|
|
14598
|
+
public:
|
|
14599
|
+
${overrides}
|
|
14600
|
+
};
|
|
14601
|
+
`;
|
|
14602
|
+
}
|
|
14603
|
+
function renderTemplateSource(className, includePath, spec) {
|
|
14604
|
+
const header = `#include "${includePath}"\n`;
|
|
14605
|
+
if (spec === void 0 || spec.overrides.length === 0) return header;
|
|
14606
|
+
return `${header}\n${spec.overrides.map((entry) => entry.definition.replaceAll("%CLASS%", className)).join("\n\n")}\n`;
|
|
14607
|
+
}
|
|
14608
|
+
async function scaffoldClassTemplate(input) {
|
|
14609
|
+
const className = input.className.trim();
|
|
14610
|
+
const moduleName = input.moduleName.trim();
|
|
14611
|
+
if (!IDENTIFIER_PATTERN.test(moduleName)) throw new ModuleScaffoldError(`Module name '${moduleName}' is not a valid C++ identifier.`);
|
|
14612
|
+
if (input.template === "interface") {
|
|
14613
|
+
if (!INTERFACE_NAME_PATTERN.test(className)) throw new ModuleScaffoldError(`Interface name '${className}' must start with I, e.g. IDamageable.`);
|
|
14614
|
+
} else if (!UCLASS_NAME_PATTERN.test(className)) throw new ModuleScaffoldError(`Class name '${className}' must start with U, e.g. UCombatSubsystem.`);
|
|
14615
|
+
const subdirectory = input.subdirectory?.trim().replace(/^[\\/]+|[\\/]+$/gu, "") ?? "";
|
|
14616
|
+
if (subdirectory.includes("..")) throw new ModuleScaffoldError(`Subdirectory '${subdirectory}' must stay inside the module.`);
|
|
14617
|
+
const fileStem = className.slice(1);
|
|
14618
|
+
const moduleRoot = path.resolve(input.moduleRoot.trim());
|
|
14619
|
+
const headerPath = path.join(moduleRoot, "Public", subdirectory, `${fileStem}.h`);
|
|
14620
|
+
const sourcePath = path.join(moduleRoot, "Private", subdirectory, `${fileStem}.cpp`);
|
|
14621
|
+
if (await fileExists(headerPath)) throw new ModuleScaffoldError(`Header already exists at ${headerPath}; refusing to overwrite it.`);
|
|
14622
|
+
if (await fileExists(sourcePath)) throw new ModuleScaffoldError(`Source already exists at ${sourcePath}; refusing to overwrite it.`);
|
|
14623
|
+
const apiMacro = `${moduleName.toUpperCase()}_API`;
|
|
14624
|
+
const includePath = subdirectory.length === 0 ? `${fileStem}.h` : `${subdirectory.replaceAll("\\", "/")}/${fileStem}.h`;
|
|
14625
|
+
const spec = input.template === "interface" ? void 0 : CLASS_TEMPLATE_SPECS[input.template];
|
|
14626
|
+
const headerText = spec === void 0 ? renderInterfaceHeader(className, apiMacro, includePath) : renderSubsystemHeader(className, apiMacro, includePath, spec);
|
|
14627
|
+
await mkdir(path.dirname(headerPath), { recursive: true });
|
|
14628
|
+
await mkdir(path.dirname(sourcePath), { recursive: true });
|
|
14629
|
+
await writeFile(headerPath, headerText, "utf8");
|
|
14630
|
+
await writeFile(sourcePath, renderTemplateSource(className, includePath, spec), "utf8");
|
|
14631
|
+
return {
|
|
14632
|
+
className,
|
|
14633
|
+
createdFiles: [headerPath, sourcePath],
|
|
14634
|
+
headerPath,
|
|
14635
|
+
requiredModuleDependencies: spec?.requiredModuleDependencies ?? ["CoreUObject"],
|
|
14636
|
+
sourcePath,
|
|
14637
|
+
template: input.template
|
|
14638
|
+
};
|
|
14639
|
+
}
|
|
14640
|
+
//#endregion
|
|
13583
14641
|
//#region src/moduleScaffold.ts
|
|
13584
14642
|
const MODULE_DESCRIPTOR_TYPES = [
|
|
13585
14643
|
"Runtime",
|
|
@@ -13793,8 +14851,412 @@ function isPlainObject(value) {
|
|
|
13793
14851
|
function getErrorCode(error) {
|
|
13794
14852
|
if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
|
|
13795
14853
|
}
|
|
14854
|
+
const SKIPPED_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
|
|
14855
|
+
".git",
|
|
14856
|
+
".svn",
|
|
14857
|
+
"Binaries",
|
|
14858
|
+
"DerivedDataCache",
|
|
14859
|
+
"Intermediate",
|
|
14860
|
+
"Saved",
|
|
14861
|
+
"ThirdParty",
|
|
14862
|
+
"node_modules"
|
|
14863
|
+
]);
|
|
14864
|
+
const UCLASS_MACRO_PATTERN = /^\s*UCLASS\s*\(/u;
|
|
14865
|
+
const UINTERFACE_MACRO_PATTERN = /^\s*UINTERFACE\s*\(/u;
|
|
14866
|
+
const USTRUCT_MACRO_PATTERN = /^\s*USTRUCT\s*\(/u;
|
|
14867
|
+
const UENUM_MACRO_PATTERN = /^\s*UENUM\s*\(/u;
|
|
14868
|
+
const UFUNCTION_MACRO_PATTERN = /^\s*UFUNCTION\s*\(/u;
|
|
14869
|
+
const UPROPERTY_MACRO_PATTERN = /^\s*UPROPERTY\s*\(/u;
|
|
14870
|
+
const CLASS_DECLARATION_PATTERN = /^\s*class\s+(?:[A-Za-z_][A-Za-z0-9_]*_API\s+)?(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\b(?<rest>.*)$/u;
|
|
14871
|
+
const STRUCT_DECLARATION_PATTERN = /^\s*struct\s+(?:[A-Za-z_][A-Za-z0-9_]*_API\s+)?(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\b(?<rest>.*)$/u;
|
|
14872
|
+
const ENUM_DECLARATION_PATTERN = /^\s*enum(?:\s+(?:class|struct))?\s+(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\b/u;
|
|
14873
|
+
const BASE_CLASS_PATTERN = /:\s*public\s+(?<parent>[A-Za-z_][A-Za-z0-9_:]*)/u;
|
|
14874
|
+
const FUNCTION_DECLARATION_PATTERN = /(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\s*\(/u;
|
|
14875
|
+
const PROPERTY_DECLARATION_PATTERN = /(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\s*(?:\[[^\]]*\])?\s*(?:=[^;]*)?;\s*$/u;
|
|
14876
|
+
const symbolKindSchema = z.enum([
|
|
14877
|
+
"class",
|
|
14878
|
+
"enum",
|
|
14879
|
+
"function",
|
|
14880
|
+
"interface",
|
|
14881
|
+
"property",
|
|
14882
|
+
"struct"
|
|
14883
|
+
]);
|
|
14884
|
+
const symbolRecordSchema = z.object({
|
|
14885
|
+
file: z.string(),
|
|
14886
|
+
includePath: z.string().optional(),
|
|
14887
|
+
kind: symbolKindSchema,
|
|
14888
|
+
line: z.number().int().min(1),
|
|
14889
|
+
module: z.string().optional(),
|
|
14890
|
+
owner: z.string().optional(),
|
|
14891
|
+
parent: z.string().optional(),
|
|
14892
|
+
symbol: z.string()
|
|
14893
|
+
});
|
|
14894
|
+
const symbolIndexCacheSchema = z.object({
|
|
14895
|
+
files: z.record(z.string(), z.object({
|
|
14896
|
+
mtimeMs: z.number(),
|
|
14897
|
+
records: z.array(symbolRecordSchema),
|
|
14898
|
+
size: z.number()
|
|
14899
|
+
})),
|
|
14900
|
+
version: z.literal(1)
|
|
14901
|
+
});
|
|
14902
|
+
const nodeFileSystem = {
|
|
14903
|
+
listDirectory: async (dirPath) => {
|
|
14904
|
+
return (await readdir(dirPath, { withFileTypes: true })).map((entry) => ({
|
|
14905
|
+
isDirectory: entry.isDirectory(),
|
|
14906
|
+
name: entry.name
|
|
14907
|
+
}));
|
|
14908
|
+
},
|
|
14909
|
+
readCache: async (filePath) => readFile(filePath, "utf8"),
|
|
14910
|
+
readFile: async (filePath) => readFile(filePath, "utf8"),
|
|
14911
|
+
stat: async (filePath) => {
|
|
14912
|
+
const stats = await stat(filePath);
|
|
14913
|
+
return {
|
|
14914
|
+
mtimeMs: stats.mtimeMs,
|
|
14915
|
+
size: stats.size
|
|
14916
|
+
};
|
|
14917
|
+
},
|
|
14918
|
+
writeCache: async (filePath, contents) => {
|
|
14919
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
14920
|
+
await writeFile(filePath, contents, "utf8");
|
|
14921
|
+
}
|
|
14922
|
+
};
|
|
14923
|
+
function symbolIndexCacheFileName(roots) {
|
|
14924
|
+
return `symbols-${createHash("sha256").update([...roots].sort().join("\0")).digest("hex").slice(0, 16)}.json`;
|
|
14925
|
+
}
|
|
14926
|
+
function deriveIncludePath(filePath) {
|
|
14927
|
+
const segments = filePath.split(/[\\/]/u);
|
|
14928
|
+
for (let index = segments.length - 2; index >= 0; index -= 1) {
|
|
14929
|
+
const segment = segments[index];
|
|
14930
|
+
if (segment === "Public" || segment === "Classes" || segment === "Private") return segments.slice(index + 1).join("/");
|
|
14931
|
+
}
|
|
14932
|
+
}
|
|
14933
|
+
function stripLineComment(line) {
|
|
14934
|
+
const commentIndex = line.indexOf("//");
|
|
14935
|
+
return commentIndex === -1 ? line : line.slice(0, commentIndex);
|
|
14936
|
+
}
|
|
14937
|
+
function isDeclarationCandidate(line) {
|
|
14938
|
+
const trimmed = stripLineComment(line).trim();
|
|
14939
|
+
return trimmed.length > 0 && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*");
|
|
14940
|
+
}
|
|
14941
|
+
function parseReflectedSymbols(contents, filePath, module) {
|
|
14942
|
+
const includePath = deriveIncludePath(filePath);
|
|
14943
|
+
const lines = contents.split(/\r?\n/u);
|
|
14944
|
+
const records = [];
|
|
14945
|
+
let owner;
|
|
14946
|
+
function base(kind, symbol, line) {
|
|
14947
|
+
const record = {
|
|
14948
|
+
file: filePath,
|
|
14949
|
+
kind,
|
|
14950
|
+
line,
|
|
14951
|
+
symbol
|
|
14952
|
+
};
|
|
14953
|
+
if (includePath !== void 0) record.includePath = includePath;
|
|
14954
|
+
if (module !== void 0) record.module = module;
|
|
14955
|
+
return record;
|
|
14956
|
+
}
|
|
14957
|
+
function nextDeclaration(startIndex) {
|
|
14958
|
+
for (let index = startIndex; index < Math.min(lines.length, startIndex + 8); index += 1) {
|
|
14959
|
+
const raw = lines[index];
|
|
14960
|
+
if (raw === void 0) return;
|
|
14961
|
+
if (isDeclarationCandidate(raw)) return {
|
|
14962
|
+
index,
|
|
14963
|
+
text: stripLineComment(raw)
|
|
14964
|
+
};
|
|
14965
|
+
}
|
|
14966
|
+
}
|
|
14967
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
14968
|
+
const line = lines[index];
|
|
14969
|
+
if (line === void 0) continue;
|
|
14970
|
+
const isEnum = UENUM_MACRO_PATTERN.test(line);
|
|
14971
|
+
const isStruct = USTRUCT_MACRO_PATTERN.test(line);
|
|
14972
|
+
const isInterface = UINTERFACE_MACRO_PATTERN.test(line);
|
|
14973
|
+
if (UCLASS_MACRO_PATTERN.test(line) || isStruct || isEnum || isInterface) {
|
|
14974
|
+
const declaration = nextDeclaration(index + 1);
|
|
14975
|
+
if (!declaration) continue;
|
|
14976
|
+
if (isEnum) {
|
|
14977
|
+
const match = ENUM_DECLARATION_PATTERN.exec(declaration.text);
|
|
14978
|
+
if (match?.groups?.symbol !== void 0) {
|
|
14979
|
+
owner = void 0;
|
|
14980
|
+
records.push(base("enum", match.groups.symbol, declaration.index + 1));
|
|
14981
|
+
}
|
|
14982
|
+
continue;
|
|
14983
|
+
}
|
|
14984
|
+
const match = (isStruct ? STRUCT_DECLARATION_PATTERN : CLASS_DECLARATION_PATTERN).exec(declaration.text);
|
|
14985
|
+
const symbol = match?.groups?.symbol;
|
|
14986
|
+
if (symbol === void 0) continue;
|
|
14987
|
+
const record = base(isStruct ? "struct" : isInterface ? "interface" : "class", symbol, declaration.index + 1);
|
|
14988
|
+
const parent = BASE_CLASS_PATTERN.exec(match?.groups?.rest ?? "")?.groups?.parent;
|
|
14989
|
+
if (parent !== void 0) record.parent = parent;
|
|
14990
|
+
owner = symbol;
|
|
14991
|
+
records.push(record);
|
|
14992
|
+
continue;
|
|
14993
|
+
}
|
|
14994
|
+
if (UFUNCTION_MACRO_PATTERN.test(line) || UPROPERTY_MACRO_PATTERN.test(line)) {
|
|
14995
|
+
const isFunction = UFUNCTION_MACRO_PATTERN.test(line);
|
|
14996
|
+
const declaration = nextDeclaration(index + 1);
|
|
14997
|
+
if (!declaration) continue;
|
|
14998
|
+
const symbol = (isFunction ? FUNCTION_DECLARATION_PATTERN : PROPERTY_DECLARATION_PATTERN).exec(declaration.text)?.groups?.symbol;
|
|
14999
|
+
if (symbol === void 0) continue;
|
|
15000
|
+
const record = base(isFunction ? "function" : "property", symbol, declaration.index + 1);
|
|
15001
|
+
if (owner !== void 0) record.owner = owner;
|
|
15002
|
+
records.push(record);
|
|
15003
|
+
}
|
|
15004
|
+
}
|
|
15005
|
+
return records;
|
|
15006
|
+
}
|
|
15007
|
+
async function collectHeaderFiles(root, fileSystem, limit, accumulator) {
|
|
15008
|
+
const queue = [root];
|
|
15009
|
+
while (queue.length > 0) {
|
|
15010
|
+
const current = queue.pop();
|
|
15011
|
+
if (current === void 0) break;
|
|
15012
|
+
let entries;
|
|
15013
|
+
try {
|
|
15014
|
+
entries = await fileSystem.listDirectory(current);
|
|
15015
|
+
} catch {
|
|
15016
|
+
continue;
|
|
15017
|
+
}
|
|
15018
|
+
for (const entry of entries) {
|
|
15019
|
+
if (entry.isDirectory) {
|
|
15020
|
+
if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) queue.push(path.join(current, entry.name));
|
|
15021
|
+
continue;
|
|
15022
|
+
}
|
|
15023
|
+
if (!entry.name.endsWith(".h") || entry.name.endsWith(".generated.h")) continue;
|
|
15024
|
+
if (accumulator.length >= limit) return true;
|
|
15025
|
+
accumulator.push(path.join(current, entry.name));
|
|
15026
|
+
}
|
|
15027
|
+
}
|
|
15028
|
+
return false;
|
|
15029
|
+
}
|
|
15030
|
+
async function resolveModuleName(filePath, fileSystem, cache) {
|
|
15031
|
+
const directories = [];
|
|
15032
|
+
let current = path.dirname(filePath);
|
|
15033
|
+
for (let depth = 0; depth < 12; depth += 1) {
|
|
15034
|
+
const cached = cache.get(current);
|
|
15035
|
+
if (cached !== void 0 || cache.has(current)) {
|
|
15036
|
+
for (const directory of directories) cache.set(directory, cached);
|
|
15037
|
+
return cached;
|
|
15038
|
+
}
|
|
15039
|
+
directories.push(current);
|
|
15040
|
+
let moduleName;
|
|
15041
|
+
try {
|
|
15042
|
+
moduleName = (await fileSystem.listDirectory(current)).find((entry) => !entry.isDirectory && entry.name.endsWith(".Build.cs"))?.name.slice(0, -9);
|
|
15043
|
+
} catch {
|
|
15044
|
+
moduleName = void 0;
|
|
15045
|
+
}
|
|
15046
|
+
if (moduleName !== void 0) {
|
|
15047
|
+
for (const directory of directories) cache.set(directory, moduleName);
|
|
15048
|
+
return moduleName;
|
|
15049
|
+
}
|
|
15050
|
+
const parent = path.dirname(current);
|
|
15051
|
+
if (parent === current) break;
|
|
15052
|
+
current = parent;
|
|
15053
|
+
}
|
|
15054
|
+
for (const directory of directories) cache.set(directory, void 0);
|
|
15055
|
+
}
|
|
15056
|
+
async function loadCache(cacheFilePath, fileSystem) {
|
|
15057
|
+
if (cacheFilePath === void 0) return {
|
|
15058
|
+
files: {},
|
|
15059
|
+
version: 1
|
|
15060
|
+
};
|
|
15061
|
+
try {
|
|
15062
|
+
const contents = await fileSystem.readCache(cacheFilePath);
|
|
15063
|
+
const parsed = symbolIndexCacheSchema.safeParse(JSON.parse(contents));
|
|
15064
|
+
if (parsed.success) return parsed.data;
|
|
15065
|
+
} catch {}
|
|
15066
|
+
return {
|
|
15067
|
+
files: {},
|
|
15068
|
+
version: 1
|
|
15069
|
+
};
|
|
15070
|
+
}
|
|
15071
|
+
async function buildSymbolIndex(options) {
|
|
15072
|
+
const fileSystem = options.fileSystem ?? nodeFileSystem;
|
|
15073
|
+
const limit = options.maxFiles ?? 8e4;
|
|
15074
|
+
const files = [];
|
|
15075
|
+
let truncated = false;
|
|
15076
|
+
for (const root of options.roots) if (await collectHeaderFiles(root, fileSystem, limit, files)) {
|
|
15077
|
+
truncated = true;
|
|
15078
|
+
break;
|
|
15079
|
+
}
|
|
15080
|
+
const previous = options.refresh === true ? {
|
|
15081
|
+
files: {},
|
|
15082
|
+
version: 1
|
|
15083
|
+
} : await loadCache(options.cacheFilePath, fileSystem);
|
|
15084
|
+
const next = {
|
|
15085
|
+
files: {},
|
|
15086
|
+
version: 1
|
|
15087
|
+
};
|
|
15088
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
15089
|
+
const records = [];
|
|
15090
|
+
let parsedFileCount = 0;
|
|
15091
|
+
for (const file of files) {
|
|
15092
|
+
let stats;
|
|
15093
|
+
try {
|
|
15094
|
+
stats = await fileSystem.stat(file);
|
|
15095
|
+
} catch {
|
|
15096
|
+
continue;
|
|
15097
|
+
}
|
|
15098
|
+
const cached = previous.files[file];
|
|
15099
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
15100
|
+
next.files[file] = cached;
|
|
15101
|
+
records.push(...cached.records);
|
|
15102
|
+
continue;
|
|
15103
|
+
}
|
|
15104
|
+
let contents;
|
|
15105
|
+
try {
|
|
15106
|
+
contents = await fileSystem.readFile(file);
|
|
15107
|
+
} catch {
|
|
15108
|
+
continue;
|
|
15109
|
+
}
|
|
15110
|
+
const module = await resolveModuleName(file, fileSystem, moduleCache);
|
|
15111
|
+
const parsed = parseReflectedSymbols(contents, file, module);
|
|
15112
|
+
parsedFileCount += 1;
|
|
15113
|
+
next.files[file] = {
|
|
15114
|
+
mtimeMs: stats.mtimeMs,
|
|
15115
|
+
records: parsed,
|
|
15116
|
+
size: stats.size
|
|
15117
|
+
};
|
|
15118
|
+
records.push(...parsed);
|
|
15119
|
+
}
|
|
15120
|
+
if (options.cacheFilePath !== void 0) try {
|
|
15121
|
+
await fileSystem.writeCache(options.cacheFilePath, JSON.stringify(next));
|
|
15122
|
+
} catch {}
|
|
15123
|
+
const result = {
|
|
15124
|
+
fileCount: files.length,
|
|
15125
|
+
parsedFileCount,
|
|
15126
|
+
records,
|
|
15127
|
+
truncated
|
|
15128
|
+
};
|
|
15129
|
+
if (options.cacheFilePath !== void 0) result.cacheFilePath = options.cacheFilePath;
|
|
15130
|
+
return result;
|
|
15131
|
+
}
|
|
15132
|
+
function searchSymbolIndex(records, options) {
|
|
15133
|
+
const needle = options.symbol.toLowerCase();
|
|
15134
|
+
const allowedKinds = options.kinds === void 0 ? void 0 : new Set(options.kinds);
|
|
15135
|
+
const exact = [];
|
|
15136
|
+
const prefix = [];
|
|
15137
|
+
const substring = [];
|
|
15138
|
+
for (const record of records) {
|
|
15139
|
+
if (allowedKinds && !allowedKinds.has(record.kind)) continue;
|
|
15140
|
+
const candidate = record.symbol.toLowerCase();
|
|
15141
|
+
if (candidate === needle) {
|
|
15142
|
+
exact.push({
|
|
15143
|
+
...record,
|
|
15144
|
+
match: "exact"
|
|
15145
|
+
});
|
|
15146
|
+
continue;
|
|
15147
|
+
}
|
|
15148
|
+
if (candidate.startsWith(needle)) {
|
|
15149
|
+
prefix.push({
|
|
15150
|
+
...record,
|
|
15151
|
+
match: "prefix"
|
|
15152
|
+
});
|
|
15153
|
+
continue;
|
|
15154
|
+
}
|
|
15155
|
+
if (candidate.includes(needle)) substring.push({
|
|
15156
|
+
...record,
|
|
15157
|
+
match: "substring"
|
|
15158
|
+
});
|
|
15159
|
+
}
|
|
15160
|
+
const ordered = [
|
|
15161
|
+
...exact,
|
|
15162
|
+
...prefix,
|
|
15163
|
+
...substring
|
|
15164
|
+
];
|
|
15165
|
+
return {
|
|
15166
|
+
matches: ordered.slice(0, options.maxResults),
|
|
15167
|
+
totalCount: ordered.length
|
|
15168
|
+
};
|
|
15169
|
+
}
|
|
13796
15170
|
//#endregion
|
|
13797
15171
|
//#region src/tools/codeGen.ts
|
|
15172
|
+
const findSymbolInputSchema = {
|
|
15173
|
+
engineVersion: engineVersionSchema,
|
|
15174
|
+
kinds: z.array(z.enum([
|
|
15175
|
+
"class",
|
|
15176
|
+
"enum",
|
|
15177
|
+
"function",
|
|
15178
|
+
"interface",
|
|
15179
|
+
"property",
|
|
15180
|
+
"struct"
|
|
15181
|
+
])).min(1).optional().describe("Restrict results to these reflected symbol kinds; defaults to every kind."),
|
|
15182
|
+
maxResults: z.number().int().min(1).max(200).default(25).describe("Maximum matches to return, ordered exact then prefix then substring (1-200, default 25)."),
|
|
15183
|
+
projectPath: z.string().trim().min(1).optional().describe("Path to the .uproject file or its directory; required unless scope is \"engine\"."),
|
|
15184
|
+
refresh: z.boolean().default(false).describe("Discard the cached index and rescan every header."),
|
|
15185
|
+
scope: z.enum([
|
|
15186
|
+
"all",
|
|
15187
|
+
"engine",
|
|
15188
|
+
"project"
|
|
15189
|
+
]).default("all").describe("Which source trees to index: the engine install, the project, or both."),
|
|
15190
|
+
symbol: z.string().trim().min(1).describe("Reflected symbol name to look up, e.g. \"UCharacterMovementComponent\".")
|
|
15191
|
+
};
|
|
15192
|
+
const addReflectedMemberInputSchema = {
|
|
15193
|
+
category: z.string().trim().min(1).optional().describe("Editor category for the member; required when a Blueprint or editor specifier is used."),
|
|
15194
|
+
className: z.string().trim().min(1).describe("Class or struct in the header to add the member to."),
|
|
15195
|
+
defaultValue: z.string().trim().min(1).optional().describe("In-class initializer for a property, e.g. \"100.f\"; properties only."),
|
|
15196
|
+
headerPath: z.string().trim().min(1).describe("Absolute path to the header declaring the class."),
|
|
15197
|
+
kind: z.enum(REFLECTED_MEMBER_KINDS).describe("Whether to add a UPROPERTY or a UFUNCTION."),
|
|
15198
|
+
name: z.string().trim().min(1).describe("Member name, a valid C++ identifier."),
|
|
15199
|
+
parameters: z.string().trim().optional().describe("Parameter list for a function, verbatim C++ (e.g. \"float Amount, AActor* Instigator\")."),
|
|
15200
|
+
returnType: z.string().trim().min(1).optional().describe("Return type for a function; defaults to void."),
|
|
15201
|
+
sourcePath: z.string().trim().min(1).optional().describe("Where to append the function stub; defaults to the Private/*.cpp matching the header."),
|
|
15202
|
+
specifiers: z.array(z.string().trim().min(1)).max(32).default([]).describe("UPROPERTY/UFUNCTION specifiers, e.g. [\"EditAnywhere\", \"BlueprintReadWrite\"]. Validated against the UHT rules before anything is written."),
|
|
15203
|
+
type: z.string().trim().min(1).optional().describe("C++ type for a property, e.g. \"float\" or \"TObjectPtr<AActor>\".")
|
|
15204
|
+
};
|
|
15205
|
+
const scaffoldClassTemplateInputSchema = {
|
|
15206
|
+
className: z.string().trim().min(1).describe("Class name with its UE prefix: I-prefixed for an interface, U-prefixed for a subsystem."),
|
|
15207
|
+
moduleName: z.string().trim().min(1).describe("Owning module, used for the MODULENAME_API export macro."),
|
|
15208
|
+
moduleRoot: z.string().trim().min(1).describe("Absolute path to the module directory holding Public/ and Private/."),
|
|
15209
|
+
subdirectory: z.string().trim().optional().describe("Optional path under Public/ and Private/ to place the pair in, e.g. \"Combat\"."),
|
|
15210
|
+
template: z.enum(CLASS_TEMPLATES).describe("Which class template to generate.")
|
|
15211
|
+
};
|
|
15212
|
+
async function findSymbol(input, dependencies, context = createEngineToolchainContext(), overrides = {}) {
|
|
15213
|
+
if (input.scope !== "engine" && input.projectPath === void 0) throw new EngineToolchainError(`find_symbol scope "${input.scope}" needs a projectPath.`);
|
|
15214
|
+
const roots = [];
|
|
15215
|
+
let engineVersion;
|
|
15216
|
+
if (input.scope !== "project") {
|
|
15217
|
+
const uprojectPath = input.projectPath === void 0 ? void 0 : (await resolveProjectPaths(input.projectPath, context)).uprojectPath;
|
|
15218
|
+
const engine = await resolveEngine({
|
|
15219
|
+
context,
|
|
15220
|
+
engineVersion: input.engineVersion,
|
|
15221
|
+
uprojectPath
|
|
15222
|
+
});
|
|
15223
|
+
engineVersion = engine.engineVersion;
|
|
15224
|
+
roots.push(path.join(engine.engineRoot, "Engine", "Source"), path.join(engine.engineRoot, "Engine", "Plugins"));
|
|
15225
|
+
}
|
|
15226
|
+
if (input.scope !== "engine" && input.projectPath !== void 0) {
|
|
15227
|
+
const { projectDirectory } = await resolveProjectPaths(input.projectPath, context);
|
|
15228
|
+
roots.push(path.join(projectDirectory, "Source"), path.join(projectDirectory, "Plugins"));
|
|
15229
|
+
}
|
|
15230
|
+
const cacheDirectory = overrides.cacheDirectory ?? resolveSymbolIndexCacheDirectory({ env: context.env });
|
|
15231
|
+
const index = await buildSymbolIndex({
|
|
15232
|
+
cacheFilePath: path.join(cacheDirectory, symbolIndexCacheFileName(roots)),
|
|
15233
|
+
...overrides.fileSystem === void 0 ? {} : { fileSystem: overrides.fileSystem },
|
|
15234
|
+
refresh: input.refresh,
|
|
15235
|
+
roots
|
|
15236
|
+
});
|
|
15237
|
+
const { matches, totalCount } = searchSymbolIndex(index.records, {
|
|
15238
|
+
...input.kinds === void 0 ? {} : { kinds: input.kinds },
|
|
15239
|
+
maxResults: input.maxResults,
|
|
15240
|
+
symbol: input.symbol
|
|
15241
|
+
});
|
|
15242
|
+
dependencies.logger.info({
|
|
15243
|
+
fileCount: index.fileCount,
|
|
15244
|
+
parsedFileCount: index.parsedFileCount,
|
|
15245
|
+
symbol: input.symbol,
|
|
15246
|
+
totalCount
|
|
15247
|
+
}, "Searched the Unreal reflected symbol index");
|
|
15248
|
+
return {
|
|
15249
|
+
...engineVersion === void 0 ? {} : { engineVersion },
|
|
15250
|
+
fileCount: index.fileCount,
|
|
15251
|
+
matches,
|
|
15252
|
+
parsedFileCount: index.parsedFileCount,
|
|
15253
|
+
roots,
|
|
15254
|
+
scope: input.scope,
|
|
15255
|
+
symbol: input.symbol,
|
|
15256
|
+
totalCount,
|
|
15257
|
+
truncated: index.truncated || matches.length < totalCount
|
|
15258
|
+
};
|
|
15259
|
+
}
|
|
13798
15260
|
const createCppClassInputSchema = {
|
|
13799
15261
|
className: bridgeCreateCppClassRequestSchema.shape.className.describe("Unprefixed C++ class name to generate (e.g. MyCharacter); the engine adds the A/U/F prefix that matches the parent class."),
|
|
13800
15262
|
classPath: bridgeCreateCppClassRequestSchema.shape.classPath.describe("Optional path relative to the target module's source directory (e.g. Public/Characters). Defaults to the module source root."),
|
|
@@ -13911,6 +15373,27 @@ function registerCodeGenTools(server, dependencies) {
|
|
|
13911
15373
|
projectId
|
|
13912
15374
|
}, dependencies));
|
|
13913
15375
|
});
|
|
15376
|
+
server.registerTool("find_symbol", {
|
|
15377
|
+
description: "Look up a reflected Unreal symbol (UCLASS/USTRUCT/UENUM/UINTERFACE type, or a UFUNCTION/UPROPERTY member) across the engine install and the project, returning the declaring header, the owning module and the #include path to use. Results are ordered exact, then prefix, then substring. The header index is cached per source-root set and refreshed incrementally by file mtime, so the first call after an engine install pays a full scan. Server-side; does not require a running editor.",
|
|
15378
|
+
inputSchema: findSymbolInputSchema,
|
|
15379
|
+
title: "Find Symbol"
|
|
15380
|
+
}, async (input) => {
|
|
15381
|
+
return toToolSuccess(await findSymbol(input, dependencies));
|
|
15382
|
+
});
|
|
15383
|
+
server.registerTool("add_reflected_member", {
|
|
15384
|
+
description: "Insert a UPROPERTY or UFUNCTION into an existing class or struct header, and append a matching .cpp stub for a function. Specifiers are validated against the UHT rules (conflicting edit/visibility specifiers, BlueprintReadOnly with BlueprintReadWrite, an RPC without Reliable/Unreliable or with a non-void return, a bare ReplicatedUsing) and nothing is written when they fail. The member goes in its own public: section right after GENERATED_BODY(), and any UE type it names by pointer or reference is forward-declared above the class. Server-side; does not require a running editor.",
|
|
15385
|
+
inputSchema: addReflectedMemberInputSchema,
|
|
15386
|
+
title: "Add Reflected Member"
|
|
15387
|
+
}, async (input) => {
|
|
15388
|
+
return toToolSuccess(await addReflectedMember(input));
|
|
15389
|
+
});
|
|
15390
|
+
server.registerTool("scaffold_class_template", {
|
|
15391
|
+
description: "Write a header/source pair for a class shape the New C++ Class wizard does not template well: a UINTERFACE/IInterface pair, or a Game Instance, World or Editor subsystem with its Initialize/Deinitialize overrides. Returns the module dependencies the template needs in Build.cs. Refuses to overwrite an existing file. Server-side; does not require a running editor.",
|
|
15392
|
+
inputSchema: scaffoldClassTemplateInputSchema,
|
|
15393
|
+
title: "Scaffold Class Template"
|
|
15394
|
+
}, async (input) => {
|
|
15395
|
+
return toToolSuccess(await scaffoldClassTemplate(input));
|
|
15396
|
+
});
|
|
13914
15397
|
}
|
|
13915
15398
|
const codeGenToolDomain = {
|
|
13916
15399
|
domain: "codeGen",
|
|
@@ -13918,7 +15401,10 @@ const codeGenToolDomain = {
|
|
|
13918
15401
|
toolNames: [
|
|
13919
15402
|
"add_module",
|
|
13920
15403
|
"create_cpp_class",
|
|
13921
|
-
"get_create_class_status"
|
|
15404
|
+
"get_create_class_status",
|
|
15405
|
+
"find_symbol",
|
|
15406
|
+
"add_reflected_member",
|
|
15407
|
+
"scaffold_class_template"
|
|
13922
15408
|
]
|
|
13923
15409
|
};
|
|
13924
15410
|
//#endregion
|
|
@@ -13929,6 +15415,10 @@ const getCompileStatusInputSchema = {
|
|
|
13929
15415
|
compileJobId: z.string().trim().min(1).max(512).describe("compileJobId returned by trigger_live_compile or trigger_hot_reload identifying the compile job to poll."),
|
|
13930
15416
|
projectId: projectIdSchema
|
|
13931
15417
|
};
|
|
15418
|
+
const getLiveCompileDetailsInputSchema = {
|
|
15419
|
+
compileJobId: z.string().trim().min(1).max(512).describe("compileJobId returned by trigger_live_compile identifying the compile job to report on."),
|
|
15420
|
+
projectId: projectIdSchema
|
|
15421
|
+
};
|
|
13932
15422
|
async function triggerLiveCompile(projectId, dependencies) {
|
|
13933
15423
|
const normalizedProjectId = projectIdSchema.parse(projectId);
|
|
13934
15424
|
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
@@ -13996,6 +15486,29 @@ function buildGetCompileStatusResult(response, projectId) {
|
|
|
13996
15486
|
if (response.restartReason !== void 0) result.restartReason = response.restartReason;
|
|
13997
15487
|
return result;
|
|
13998
15488
|
}
|
|
15489
|
+
async function getLiveCompileDetails(input, dependencies) {
|
|
15490
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
15491
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
15492
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
15493
|
+
const response = await sendBridgeGetLiveCompileDetailsRequest(createBridgeGetLiveCompileDetailsRequest(input.compileJobId), {
|
|
15494
|
+
connectTimeoutMs: dependencies.pingTimeoutMs,
|
|
15495
|
+
host: "127.0.0.1",
|
|
15496
|
+
port: entry.port,
|
|
15497
|
+
readTimeoutMs: dependencies.pingTimeoutMs
|
|
15498
|
+
});
|
|
15499
|
+
if (!response.ok) throw new Error(`Bridge get_live_compile_details failed (${response.error.code}): ${response.error.message}`);
|
|
15500
|
+
return {
|
|
15501
|
+
compileJobId: response.compileJobId,
|
|
15502
|
+
patchedFunctions: response.patchedFunctions,
|
|
15503
|
+
patchedModules: response.patchedModules,
|
|
15504
|
+
projectId: normalizedProjectId,
|
|
15505
|
+
rejections: response.rejections,
|
|
15506
|
+
...response.restartReason === void 0 ? {} : { restartReason: response.restartReason },
|
|
15507
|
+
...response.restartRequired === void 0 ? {} : { restartRequired: response.restartRequired },
|
|
15508
|
+
state: response.state,
|
|
15509
|
+
truncated: response.truncated
|
|
15510
|
+
};
|
|
15511
|
+
}
|
|
13999
15512
|
function registerCompileTools(server, dependencies) {
|
|
14000
15513
|
server.registerTool("trigger_live_compile", {
|
|
14001
15514
|
description: "Trigger an Unreal Live Coding recompile of patched modules for a projectId; tail stream_output_log (LogLiveCoding) for build output.",
|
|
@@ -14021,6 +15534,16 @@ function registerCompileTools(server, dependencies) {
|
|
|
14021
15534
|
projectId
|
|
14022
15535
|
}, dependencies));
|
|
14023
15536
|
});
|
|
15537
|
+
server.registerTool("get_live_compile_details", {
|
|
15538
|
+
description: "Report what a Live Coding compile actually did for a projectId: the modules it patched, the reasons it refused a change, and whether the editor needs restarting because a reflected type's layout changed. get_compile_status answers pass/fail; this answers what was applied. Windows only in practice — Live Coding ships only where the engine supports it.",
|
|
15539
|
+
inputSchema: getLiveCompileDetailsInputSchema,
|
|
15540
|
+
title: "Get Live Compile Details"
|
|
15541
|
+
}, async ({ compileJobId, projectId }) => {
|
|
15542
|
+
return toToolSuccess(await getLiveCompileDetails({
|
|
15543
|
+
compileJobId,
|
|
15544
|
+
projectId
|
|
15545
|
+
}, dependencies));
|
|
15546
|
+
});
|
|
14024
15547
|
}
|
|
14025
15548
|
const compileToolDomain = {
|
|
14026
15549
|
domain: "compile",
|
|
@@ -14028,7 +15551,8 @@ const compileToolDomain = {
|
|
|
14028
15551
|
toolNames: [
|
|
14029
15552
|
"trigger_live_compile",
|
|
14030
15553
|
"trigger_hot_reload",
|
|
14031
|
-
"get_compile_status"
|
|
15554
|
+
"get_compile_status",
|
|
15555
|
+
"get_live_compile_details"
|
|
14032
15556
|
]
|
|
14033
15557
|
};
|
|
14034
15558
|
//#endregion
|
|
@@ -16229,6 +17753,8 @@ async function sendProgressNotification(extra, logger, params) {
|
|
|
16229
17753
|
//#region src/buildDiagnostics.ts
|
|
16230
17754
|
const CLANG_DIAGNOSTIC_PATTERN = /^(?<file>.+?):(?<line>\d+):(?:(?<column>\d+):)?\s+(?<severity>fatal error|error|warning):\s+(?<message>.*)$/u;
|
|
16231
17755
|
const MSVC_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>fatal error|error|warning)\s+(?<code>[A-Z]+\d+):\s+(?<message>.*)$/u;
|
|
17756
|
+
const UHT_LINE_PREFIX_PATTERN = /^(?:\[\d+\/\d+\]\s+)?(?:(?:LogCompile|LogInit|UnrealHeaderTool)\s*:\s*)?/u;
|
|
17757
|
+
const UHT_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>Error|Warning)(?:\s+(?<code>UHT\d+))?:\s+(?<message>.+)$/u;
|
|
16232
17758
|
const CLANG_TIDY_PATTERN = /^(?<file>.+?):(?<line>\d+):(?<column>\d+):\s+(?<severity>error|warning):\s+(?<message>.*?)(?:\s+\[(?<check>[^\]]+)\])?$/u;
|
|
16233
17759
|
const COOK_LINE_PREFIX_PATTERN = /^(?:\[[^\]]*\]\s*)*(?:[A-Za-z]+Helper:\s*[^:]*:\s*)?/u;
|
|
16234
17760
|
const COOK_DIAGNOSTIC_PATTERN = /^(?<category>[A-Za-z_][A-Za-z0-9_]*):\s+(?<severity>Error|Warning)(?::)?\s+(?<message>.+)$/u;
|
|
@@ -16312,7 +17838,7 @@ function parseBuildDiagnostics(output) {
|
|
|
16312
17838
|
for (const rawLine of output.split(/\r?\n/u)) {
|
|
16313
17839
|
const line = rawLine.trim();
|
|
16314
17840
|
if (line.length === 0) continue;
|
|
16315
|
-
const match = CLANG_DIAGNOSTIC_PATTERN.exec(line) ?? MSVC_DIAGNOSTIC_PATTERN.exec(line);
|
|
17841
|
+
const match = CLANG_DIAGNOSTIC_PATTERN.exec(line) ?? MSVC_DIAGNOSTIC_PATTERN.exec(line) ?? UHT_DIAGNOSTIC_PATTERN.exec(line.replace(UHT_LINE_PREFIX_PATTERN, ""));
|
|
16316
17842
|
if (!match?.groups) continue;
|
|
16317
17843
|
const groups = match.groups;
|
|
16318
17844
|
const { file, line: lineText, message, severity } = groups;
|
|
@@ -16323,7 +17849,7 @@ function parseBuildDiagnostics(output) {
|
|
|
16323
17849
|
line: Number.parseInt(lineText, 10),
|
|
16324
17850
|
message: `${code}${message}`.trim()
|
|
16325
17851
|
};
|
|
16326
|
-
const isError = severity === "error" || severity === "fatal error";
|
|
17852
|
+
const isError = severity === "error" || severity === "fatal error" || severity === "Error";
|
|
16327
17853
|
const target = isError ? errors : warnings;
|
|
16328
17854
|
const seen = isError ? seenErrors : seenWarnings;
|
|
16329
17855
|
const key = diagnosticKey(diagnostic);
|
|
@@ -16363,6 +17889,79 @@ function parseClangTidyFindings(output) {
|
|
|
16363
17889
|
}
|
|
16364
17890
|
return findings;
|
|
16365
17891
|
}
|
|
17892
|
+
function clampInteger(value, minimum, maximum) {
|
|
17893
|
+
return Math.min(Math.max(Math.trunc(value), minimum), maximum);
|
|
17894
|
+
}
|
|
17895
|
+
async function readSourceLines(filePath, cache, maxFileBytes, readFile) {
|
|
17896
|
+
const cached = cache.get(filePath);
|
|
17897
|
+
if (cached !== void 0) return cached;
|
|
17898
|
+
let lines = null;
|
|
17899
|
+
try {
|
|
17900
|
+
const contents = await readFile(filePath);
|
|
17901
|
+
lines = Buffer.byteLength(contents, "utf8") > maxFileBytes ? null : contents.split(/\r?\n/u);
|
|
17902
|
+
} catch {
|
|
17903
|
+
lines = null;
|
|
17904
|
+
}
|
|
17905
|
+
cache.set(filePath, lines);
|
|
17906
|
+
return lines;
|
|
17907
|
+
}
|
|
17908
|
+
async function attachSourceContext(diagnostics, options) {
|
|
17909
|
+
const contextLines = clampInteger(options.contextLines ?? 3, 0, 10);
|
|
17910
|
+
const maxFileBytes = Math.max(0, Math.trunc(options.maxFileBytes ?? 524288));
|
|
17911
|
+
const cache = /* @__PURE__ */ new Map();
|
|
17912
|
+
let budget = clampInteger(options.maxDiagnostics ?? 20, 0, 50);
|
|
17913
|
+
const readAllowance = budget;
|
|
17914
|
+
let reads = 0;
|
|
17915
|
+
let truncated = false;
|
|
17916
|
+
async function decorate(entries) {
|
|
17917
|
+
const decorated = [];
|
|
17918
|
+
for (const entry of entries) {
|
|
17919
|
+
if (entry.file === void 0 || entry.line === void 0) {
|
|
17920
|
+
decorated.push({ ...entry });
|
|
17921
|
+
continue;
|
|
17922
|
+
}
|
|
17923
|
+
if (budget <= 0) {
|
|
17924
|
+
truncated = true;
|
|
17925
|
+
decorated.push({ ...entry });
|
|
17926
|
+
continue;
|
|
17927
|
+
}
|
|
17928
|
+
if (!cache.has(entry.file)) {
|
|
17929
|
+
if (reads >= readAllowance) {
|
|
17930
|
+
truncated = true;
|
|
17931
|
+
decorated.push({ ...entry });
|
|
17932
|
+
continue;
|
|
17933
|
+
}
|
|
17934
|
+
reads += 1;
|
|
17935
|
+
}
|
|
17936
|
+
const lines = await readSourceLines(entry.file, cache, maxFileBytes, options.readFile);
|
|
17937
|
+
if (!lines || entry.line > lines.length) {
|
|
17938
|
+
decorated.push({ ...entry });
|
|
17939
|
+
continue;
|
|
17940
|
+
}
|
|
17941
|
+
const startLine = Math.max(1, entry.line - contextLines);
|
|
17942
|
+
const endLine = Math.min(lines.length, entry.line + contextLines);
|
|
17943
|
+
budget -= 1;
|
|
17944
|
+
decorated.push({
|
|
17945
|
+
...entry,
|
|
17946
|
+
sourceContext: {
|
|
17947
|
+
endLine,
|
|
17948
|
+
lines: lines.slice(startLine - 1, endLine),
|
|
17949
|
+
startLine
|
|
17950
|
+
}
|
|
17951
|
+
});
|
|
17952
|
+
}
|
|
17953
|
+
return decorated;
|
|
17954
|
+
}
|
|
17955
|
+
const errors = await decorate(diagnostics.errors);
|
|
17956
|
+
const warnings = await decorate(diagnostics.warnings);
|
|
17957
|
+
return {
|
|
17958
|
+
errorCount: diagnostics.errorCount,
|
|
17959
|
+
errors,
|
|
17960
|
+
sourceContextTruncated: truncated,
|
|
17961
|
+
warningCount: diagnostics.warningCount,
|
|
17962
|
+
warnings
|
|
17963
|
+
};
|
|
17964
|
+
}
|
|
16366
17965
|
const PACKAGE_JOB_MAX_OUTPUT_BYTES = 2097152;
|
|
16367
17966
|
var PackageJobCapacityError = class extends Error {
|
|
16368
17967
|
constructor(message) {
|
|
@@ -18971,6 +20570,62 @@ const projectSettingsToolDomain = {
|
|
|
18971
20570
|
toolNames: ["set_project_settings", "set_default_gamemode"]
|
|
18972
20571
|
};
|
|
18973
20572
|
//#endregion
|
|
20573
|
+
//#region src/tools/python.ts
|
|
20574
|
+
const runEditorPythonInputSchema = {
|
|
20575
|
+
command: bridgeRunEditorPythonRequestSchema.shape.command.describe("Python to run in the editor: a multi-statement block, a single statement, an expression to evaluate, or a .py file path with optional arguments."),
|
|
20576
|
+
executionMode: bridgeRunEditorPythonRequestSchema.shape.executionMode.default("script").describe("\"script\" (default) runs a whole block of Python, or a .py file path with arguments; \"statement\" runs exactly one statement; \"evaluate\" evaluates one expression and returns its value."),
|
|
20577
|
+
projectId: projectIdSchema,
|
|
20578
|
+
unattended: bridgeRunEditorPythonRequestSchema.shape.unattended.default(true).describe("Suppress modal dialogs the script would otherwise block the editor on (default true).")
|
|
20579
|
+
};
|
|
20580
|
+
async function runEditorPython(input, dependencies) {
|
|
20581
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
20582
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
20583
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
20584
|
+
const response = await sendBridgeRunEditorPythonRequest(createBridgeRunEditorPythonRequest(input.command, {
|
|
20585
|
+
executionMode: input.executionMode,
|
|
20586
|
+
unattended: input.unattended
|
|
20587
|
+
}), {
|
|
20588
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
20589
|
+
host: "127.0.0.1",
|
|
20590
|
+
port: entry.port,
|
|
20591
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
20592
|
+
});
|
|
20593
|
+
if (!response.ok) throw new Error(`Bridge run_editor_python failed (${response.error.code}): ${response.error.message}`);
|
|
20594
|
+
dependencies.logger.info({
|
|
20595
|
+
executionMode: input.executionMode,
|
|
20596
|
+
projectId: normalizedProjectId,
|
|
20597
|
+
state: response.state
|
|
20598
|
+
}, "Ran editor Python through the bridge");
|
|
20599
|
+
return {
|
|
20600
|
+
errors: response.errors,
|
|
20601
|
+
...response.message === void 0 ? {} : { message: response.message },
|
|
20602
|
+
output: response.output,
|
|
20603
|
+
projectId: normalizedProjectId,
|
|
20604
|
+
...response.result === void 0 ? {} : { result: response.result },
|
|
20605
|
+
state: response.state,
|
|
20606
|
+
truncated: response.truncated
|
|
20607
|
+
};
|
|
20608
|
+
}
|
|
20609
|
+
function registerPythonTools(server, dependencies) {
|
|
20610
|
+
server.registerTool("run_editor_python", {
|
|
20611
|
+
description: "Execute Python inside the running editor for a projectId and return its log output, error lines and evaluated result. Runs inside one transaction, so whatever the script changes through Unreal's transaction system undoes as a single step; work the engine does not record in a transaction, such as an asset import or a file write, is not undoable. A failed run is rolled back. Disabled by default: the tool answers state 'disabled' until a human enables Allow run_editor_python in the project's MCP Bridge settings, and 'unsupported' when the Python Script Plugin is not available.",
|
|
20612
|
+
inputSchema: runEditorPythonInputSchema,
|
|
20613
|
+
title: "Run Editor Python"
|
|
20614
|
+
}, async ({ command, executionMode, projectId, unattended }) => {
|
|
20615
|
+
return toToolSuccess(await runEditorPython({
|
|
20616
|
+
command,
|
|
20617
|
+
executionMode,
|
|
20618
|
+
projectId,
|
|
20619
|
+
unattended
|
|
20620
|
+
}, dependencies));
|
|
20621
|
+
});
|
|
20622
|
+
}
|
|
20623
|
+
const pythonToolDomain = {
|
|
20624
|
+
domain: "python",
|
|
20625
|
+
register: registerPythonTools,
|
|
20626
|
+
toolNames: ["run_editor_python"]
|
|
20627
|
+
};
|
|
20628
|
+
//#endregion
|
|
18974
20629
|
//#region src/tools/reflection.ts
|
|
18975
20630
|
const getClassReflectionInputSchema = {
|
|
18976
20631
|
className: bridgeGetClassReflectionRequestSchema.shape.className.describe("Reflected UCLASS or USTRUCT name. Both the UHT name and the C++ spelling work (Actor or AActor); a full path such as /Script/Engine.Actor also resolves."),
|
|
@@ -19218,8 +20873,10 @@ const buildTargetInputSchema = {
|
|
|
19218
20873
|
configuration: configurationSchema,
|
|
19219
20874
|
engineVersion: engineVersionSchema,
|
|
19220
20875
|
force: forceSchema,
|
|
20876
|
+
includeSourceContext: z.boolean().default(false).describe("Attach the source lines around each located diagnostic. Off by default; snippets are capped at 20 diagnostics per build."),
|
|
19221
20877
|
platform: platformSchema,
|
|
19222
20878
|
projectPath: projectPathSchema,
|
|
20879
|
+
sourceContextLines: z.number().int().min(0).max(10).default(3).describe("Source lines to include on each side of a diagnostic when includeSourceContext is true (0-10)."),
|
|
19223
20880
|
target: targetSchema,
|
|
19224
20881
|
timeoutMs: buildTimeoutSchema
|
|
19225
20882
|
};
|
|
@@ -19271,7 +20928,14 @@ async function buildTarget(input, dependencies, context = createEngineToolchainC
|
|
|
19271
20928
|
cwd: engine.engineRoot,
|
|
19272
20929
|
timeoutMs: input.timeoutMs
|
|
19273
20930
|
});
|
|
19274
|
-
const
|
|
20931
|
+
const parsed = parseBuildDiagnostics(result.combinedOutput);
|
|
20932
|
+
const diagnostics = input.includeSourceContext ? await attachSourceContext(parsed, {
|
|
20933
|
+
contextLines: input.sourceContextLines,
|
|
20934
|
+
readFile: context.readFile
|
|
20935
|
+
}) : {
|
|
20936
|
+
...parsed,
|
|
20937
|
+
sourceContextTruncated: void 0
|
|
20938
|
+
};
|
|
19275
20939
|
const success = result.exitCode === 0 && !result.timedOut;
|
|
19276
20940
|
if (!success) dependencies.logger.warn({
|
|
19277
20941
|
errorCount: diagnostics.errorCount,
|
|
@@ -19288,6 +20952,7 @@ async function buildTarget(input, dependencies, context = createEngineToolchainC
|
|
|
19288
20952
|
exitCode: result.exitCode,
|
|
19289
20953
|
outputTruncated: result.truncated,
|
|
19290
20954
|
platform,
|
|
20955
|
+
sourceContextTruncated: diagnostics.sourceContextTruncated,
|
|
19291
20956
|
success,
|
|
19292
20957
|
target: input.target,
|
|
19293
20958
|
timedOut: result.timedOut,
|
|
@@ -19930,6 +21595,7 @@ function registerWorldPartitionTools(server, dependencies) {
|
|
|
19930
21595
|
const TOOL_CATALOG = [
|
|
19931
21596
|
projectToolDomain,
|
|
19932
21597
|
doctorToolDomain,
|
|
21598
|
+
pythonToolDomain,
|
|
19933
21599
|
codeGenToolDomain,
|
|
19934
21600
|
compileToolDomain,
|
|
19935
21601
|
diagnosticsToolDomain,
|