blueprint-extractor-mcp 6.0.7 → 6.1.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 +32 -20
- package/dist/active-editor-session.d.ts +2 -0
- package/dist/active-editor-session.js +48 -0
- package/dist/catalogs/example-catalog.js +44 -0
- package/dist/execution/adapters/commandlet-adapter.d.ts +10 -1
- package/dist/execution/adapters/commandlet-adapter.js +16 -7
- package/dist/helpers/next-step-hints.js +1 -1
- package/dist/helpers/project-resolution.d.ts +2 -0
- package/dist/helpers/project-resolution.js +67 -28
- package/dist/helpers/tool-help.js +12 -0
- package/dist/helpers/workspace-project.d.ts +1 -1
- package/dist/helpers/workspace-project.js +25 -7
- package/dist/project-controller.d.ts +2 -0
- package/dist/project-controller.js +36 -6
- package/dist/prompts/prompt-catalog.js +58 -0
- package/dist/register-server-tools.js +10 -0
- package/dist/resources/static-doc-resources.js +33 -0
- package/dist/schemas/tool-inputs.d.ts +14 -14
- package/dist/schemas/tool-results.d.ts +1333 -27
- package/dist/schemas/tool-results.js +141 -0
- package/dist/server-config.js +8 -2
- package/dist/tool-context.d.ts +26 -0
- package/dist/tool-surface-manager.d.ts +1 -1
- package/dist/tool-surface-manager.js +28 -0
- package/dist/tools/analysis-tools.d.ts +8 -0
- package/dist/tools/analysis-tools.js +502 -0
- package/dist/tools/project-control.d.ts +2 -1
- package/dist/tools/project-control.js +28 -0
- package/dist/tools/project-intelligence.d.ts +9 -0
- package/dist/tools/project-intelligence.js +399 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,9 +23,10 @@ Blueprint Extractor MCP is a [Model Context Protocol](https://modelcontextprotoc
|
|
|
23
23
|
AI Assistant stdio MCP Server HTTP :30010 Unreal Editor
|
|
24
24
|
───────────── ◄────────────► ───────────────── ◄──────────────────► ─────────────────
|
|
25
25
|
Claude Code Node.js process Remote Control API
|
|
26
|
-
Codex
|
|
27
|
-
...
|
|
28
|
-
|
|
26
|
+
Codex 106 tools BlueprintExtractor
|
|
27
|
+
... 38 resources plugin
|
|
28
|
+
4 resource templates
|
|
29
|
+
12 prompts
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
**What the assistant can do through this server:**
|
|
@@ -69,7 +70,7 @@ Connects to the editor at `127.0.0.1:30010` by default.
|
|
|
69
70
|
```bash
|
|
70
71
|
claude mcp add -s user -t stdio blueprint-extractor \
|
|
71
72
|
-e UE_REMOTE_CONTROL_PORT=30010 \
|
|
72
|
-
-- npx -y blueprint-extractor-mcp@6.0
|
|
73
|
+
-- npx -y blueprint-extractor-mcp@6.1.0
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
</td></tr>
|
|
@@ -78,7 +79,7 @@ claude mcp add -s user -t stdio blueprint-extractor \
|
|
|
78
79
|
|
|
79
80
|
```bash
|
|
80
81
|
codex mcp add --env UE_REMOTE_CONTROL_PORT=30010 \
|
|
81
|
-
blueprint-extractor -- npx -y blueprint-extractor-mcp@6.0
|
|
82
|
+
blueprint-extractor -- npx -y blueprint-extractor-mcp@6.1.0
|
|
82
83
|
```
|
|
83
84
|
|
|
84
85
|
</td></tr>
|
|
@@ -90,20 +91,25 @@ codex mcp add --env UE_REMOTE_CONTROL_PORT=30010 \
|
|
|
90
91
|
|
|
91
92
|
## Tool Surface
|
|
92
93
|
|
|
93
|
-
Only
|
|
94
|
-
|
|
95
|
-
| Scope |
|
|
96
|
-
|
|
97
|
-
| **Core** *(always on)* |
|
|
98
|
-
| `widget_authoring` |
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
94
|
+
Only the compact core surface is visible by default to keep the context window lean. Specialized families are loaded on demand via `activate_workflow_scope`.
|
|
95
|
+
|
|
96
|
+
| Scope | What It Unlocks |
|
|
97
|
+
|:------|:----------------|
|
|
98
|
+
| **Core** *(always on)* | Search, extraction, list/save/help, editor-session binding, and project-control entry points such as `extract_asset`, `search_assets`, `save_assets`, `get_tool_help`, and `activate_workflow_scope` |
|
|
99
|
+
| `widget_authoring` | Parent scope that loads `widget_authoring_structure`, `widget_authoring_visual`, and `widget_verification` together |
|
|
100
|
+
| `widget_authoring_structure` | Widget tree structure, hierarchy edits, wrapping, moving, replacement, and batch operations |
|
|
101
|
+
| `widget_authoring_visual` | Widget compile flows, CommonUI styles, widget animations, and widget preview capture |
|
|
102
|
+
| `widget_verification` | Widget capture, checkpoint bundles, capture listing, cleanup, and reference comparison |
|
|
103
|
+
| `material_authoring` | Material creation, `material_graph_operation`, compile, and material-instance edits |
|
|
104
|
+
| `blueprint_authoring` | Blueprint creation, member edits, graph edits, and Live Coding trigger |
|
|
105
|
+
| `schema_ai_authoring` | Structs, enums, Blackboards, Behavior Trees, and State Trees |
|
|
106
|
+
| `animation_authoring` | Anim sequences, montages, blend spaces, and widget motion authoring |
|
|
107
|
+
| `data_tables` | Data assets, data tables, curves, Input Actions, and Input Mapping Contexts |
|
|
108
|
+
| `import` | Async asset import and import-job polling |
|
|
109
|
+
| `automation_testing` | Host-side automation runs, coarse project automation context, and PIE lifecycle control |
|
|
110
|
+
| `analysis` | Deterministic Blueprint review and low-noise project asset audits |
|
|
111
|
+
| `project_intelligence` | Bounded editor context, project indexing, freshness status, and snippet-first context search |
|
|
112
|
+
| `verification` | Editor/runtime screenshots, capture comparison, motion verification, and artifact inspection |
|
|
107
113
|
|
|
108
114
|
### Contract Design
|
|
109
115
|
|
|
@@ -133,7 +139,9 @@ See [../docs/CURRENT_STATUS.md](../docs/CURRENT_STATUS.md) for the current valid
|
|
|
133
139
|
| `UE_BUILD_PLATFORM` | — | e.g. `Win64` |
|
|
134
140
|
| `UE_BUILD_CONFIGURATION` | — | e.g. `Development` |
|
|
135
141
|
|
|
136
|
-
`get_project_automation_context` surfaces the editor-derived `engineRoot`, `projectFilePath`, `editorTarget`, and `isPlayingInEditor` state that project-control and verification flows use for fallback or guard logic.
|
|
142
|
+
`get_project_automation_context` surfaces the coarse editor-derived `engineRoot`, `projectFilePath`, `editorTarget`, and `isPlayingInEditor` state that project-control and verification flows use for fallback or guard logic.
|
|
143
|
+
|
|
144
|
+
`get_editor_context` is the separate read-only editor-state snapshot for selection, open asset editors, active level, and PIE summary. It stays session-bound and intentionally does not open assets, change focus, or switch viewports.
|
|
137
145
|
|
|
138
146
|
<br>
|
|
139
147
|
|
|
@@ -162,6 +170,10 @@ blueprint://test-runs/{run_id}/{artifact} Automation test artifacts
|
|
|
162
170
|
| `plan_widget_motion_verification` | Keyframe-bundle verification planning |
|
|
163
171
|
| `wire_hud_widget_classes` | Class-default wiring for HUD assets |
|
|
164
172
|
| `debug_widget_compile_errors` | Diagnosing and recovering from compile failures |
|
|
173
|
+
| `understand_blueprint_project` | Building a project-understanding pass over indexed assets, docs, prompts, and resources |
|
|
174
|
+
| `review_blueprint_asset` | Running a deterministic read-only Blueprint review flow |
|
|
175
|
+
| `snapshot_editor_context` | Inspecting bounded editor state without changing editor focus |
|
|
176
|
+
| `audit_blueprint_project` | Running a low-noise project asset audit |
|
|
165
177
|
|
|
166
178
|
<br>
|
|
167
179
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActiveEditorState, EditorInstanceSnapshot } from './editor-instance-types.js';
|
|
2
|
+
import type { EditorContextSnapshot } from './tool-context.js';
|
|
2
3
|
type ActiveEditorSessionOptions = {
|
|
3
4
|
cwd?: string;
|
|
4
5
|
env?: NodeJS.ProcessEnv;
|
|
@@ -44,6 +45,7 @@ export declare class ActiveEditorSession {
|
|
|
44
45
|
}): Promise<EditorInstanceSnapshot | undefined>;
|
|
45
46
|
getBoundSnapshot(): EditorInstanceSnapshot | undefined;
|
|
46
47
|
getWorkspaceProjectPath(): Promise<string | undefined>;
|
|
48
|
+
getEditorContext(): Promise<EditorContextSnapshot>;
|
|
47
49
|
private ensureActiveEditor;
|
|
48
50
|
private resolveCurrentEditorForConnection;
|
|
49
51
|
private bindFromExplicitPins;
|
|
@@ -30,6 +30,12 @@ function buildActiveEditorDriftMessage(snapshot) {
|
|
|
30
30
|
+ `${snapshot.remoteControlHost}:${snapshot.remoteControlPort}. The session has been returned to an unbound state. `
|
|
31
31
|
+ 'Call list_running_editors and select_editor to continue.';
|
|
32
32
|
}
|
|
33
|
+
function toStringArray(value) {
|
|
34
|
+
if (!Array.isArray(value)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
return value.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
38
|
+
}
|
|
33
39
|
export class ActiveEditorSession {
|
|
34
40
|
cwd;
|
|
35
41
|
env;
|
|
@@ -222,6 +228,48 @@ export class ActiveEditorSession {
|
|
|
222
228
|
const resolution = await this.workspaceProjectPromise;
|
|
223
229
|
return resolution.projectPath;
|
|
224
230
|
}
|
|
231
|
+
async getEditorContext() {
|
|
232
|
+
const raw = await this.callSubsystem('GetEditorContext', {});
|
|
233
|
+
const parsed = JSON.parse(raw);
|
|
234
|
+
if (typeof parsed.error === 'string' && parsed.error.length > 0) {
|
|
235
|
+
throw new Error(parsed.error);
|
|
236
|
+
}
|
|
237
|
+
const instanceId = typeof parsed.instanceId === 'string' ? parsed.instanceId : undefined;
|
|
238
|
+
const projectFilePath = typeof parsed.projectFilePath === 'string' ? parsed.projectFilePath : undefined;
|
|
239
|
+
if (!instanceId || !projectFilePath) {
|
|
240
|
+
throw new Error('GetEditorContext did not return instanceId and projectFilePath.');
|
|
241
|
+
}
|
|
242
|
+
const pieSummary = typeof parsed.pieSummary === 'object' && parsed.pieSummary !== null
|
|
243
|
+
? parsed.pieSummary
|
|
244
|
+
: undefined;
|
|
245
|
+
return {
|
|
246
|
+
success: typeof parsed.success === 'boolean' ? parsed.success : true,
|
|
247
|
+
operation: typeof parsed.operation === 'string' ? parsed.operation : 'get_editor_context',
|
|
248
|
+
instanceId,
|
|
249
|
+
projectName: typeof parsed.projectName === 'string' ? parsed.projectName : undefined,
|
|
250
|
+
projectFilePath,
|
|
251
|
+
projectDir: typeof parsed.projectDir === 'string' ? parsed.projectDir : undefined,
|
|
252
|
+
engineRoot: typeof parsed.engineRoot === 'string' ? parsed.engineRoot : undefined,
|
|
253
|
+
editorTarget: typeof parsed.editorTarget === 'string' ? parsed.editorTarget : undefined,
|
|
254
|
+
remoteControlHost: typeof parsed.remoteControlHost === 'string' ? parsed.remoteControlHost : this.activeEditorSnapshot?.remoteControlHost ?? '127.0.0.1',
|
|
255
|
+
remoteControlPort: typeof parsed.remoteControlPort === 'number'
|
|
256
|
+
? parsed.remoteControlPort
|
|
257
|
+
: this.activeEditorSnapshot?.remoteControlPort ?? 30010,
|
|
258
|
+
lastSeenAt: typeof parsed.lastSeenAt === 'string' ? parsed.lastSeenAt : undefined,
|
|
259
|
+
selectedAssetPaths: toStringArray(parsed.selectedAssetPaths),
|
|
260
|
+
selectedActorNames: toStringArray(parsed.selectedActorNames),
|
|
261
|
+
openAssetEditors: toStringArray(parsed.openAssetEditors),
|
|
262
|
+
activeLevel: typeof parsed.activeLevel === 'string' ? parsed.activeLevel : undefined,
|
|
263
|
+
pieSummary: pieSummary ? {
|
|
264
|
+
isPlayingInEditor: typeof pieSummary.isPlayingInEditor === 'boolean' ? pieSummary.isPlayingInEditor : undefined,
|
|
265
|
+
isSimulatingInEditor: typeof pieSummary.isSimulatingInEditor === 'boolean' ? pieSummary.isSimulatingInEditor : undefined,
|
|
266
|
+
worldName: typeof pieSummary.worldName === 'string' ? pieSummary.worldName : undefined,
|
|
267
|
+
worldPath: typeof pieSummary.worldPath === 'string' ? pieSummary.worldPath : undefined,
|
|
268
|
+
} : undefined,
|
|
269
|
+
partial: typeof parsed.partial === 'boolean' ? parsed.partial : undefined,
|
|
270
|
+
unsupportedSections: toStringArray(parsed.unsupportedSections),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
225
273
|
async ensureActiveEditor() {
|
|
226
274
|
const validation = await this.validateActiveSelection();
|
|
227
275
|
if (validation.healthy && this.activeEditorSnapshot) {
|
|
@@ -667,4 +667,48 @@ export const exampleCatalog = {
|
|
|
667
667
|
},
|
|
668
668
|
],
|
|
669
669
|
},
|
|
670
|
+
blueprint_review: {
|
|
671
|
+
summary: 'Run deterministic review first, then inspect the cited graph/variable evidence before planning any fix.',
|
|
672
|
+
recommended_flow: [
|
|
673
|
+
'review_blueprint',
|
|
674
|
+
'extract_blueprint',
|
|
675
|
+
],
|
|
676
|
+
examples: [
|
|
677
|
+
{
|
|
678
|
+
title: 'review_gameplay_blueprint',
|
|
679
|
+
tool: 'review_blueprint',
|
|
680
|
+
arguments: {
|
|
681
|
+
asset_path: '/Game/Blueprints/BP_PlayerCharacter',
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
],
|
|
685
|
+
},
|
|
686
|
+
project_intelligence: {
|
|
687
|
+
summary: 'Warm the project index, search published context and asset metadata, snapshot bounded editor state when needed, then audit the relevant package scope.',
|
|
688
|
+
recommended_flow: [
|
|
689
|
+
'refresh_project_index',
|
|
690
|
+
'get_project_index_status',
|
|
691
|
+
'search_project_context',
|
|
692
|
+
'get_editor_context',
|
|
693
|
+
'audit_project_assets',
|
|
694
|
+
],
|
|
695
|
+
examples: [
|
|
696
|
+
{
|
|
697
|
+
title: 'search_replication_context',
|
|
698
|
+
tool: 'search_project_context',
|
|
699
|
+
arguments: {
|
|
700
|
+
query: 'replication authority review',
|
|
701
|
+
page: 1,
|
|
702
|
+
per_page: 5,
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
title: 'audit_ui_assets',
|
|
707
|
+
tool: 'audit_project_assets',
|
|
708
|
+
arguments: {
|
|
709
|
+
package_path: '/Game/UI',
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
],
|
|
713
|
+
},
|
|
670
714
|
};
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CommandletAdapter spawns UnrealEditor-Cmd
|
|
2
|
+
* CommandletAdapter spawns the platform-appropriate UnrealEditor-Cmd binary with -run=blueprintextractor
|
|
3
3
|
* and communicates via stdin/stdout JSON-RPC.
|
|
4
4
|
*/
|
|
5
|
+
import { spawn } from 'child_process';
|
|
5
6
|
import type { ExecutionAdapter, ToolCapability } from '../execution-adapter.js';
|
|
6
7
|
export interface CommandletAdapterOptions {
|
|
7
8
|
engineRoot: string;
|
|
8
9
|
projectPath: string;
|
|
10
|
+
platform?: NodeJS.Platform;
|
|
11
|
+
spawnProcess?: typeof spawn;
|
|
12
|
+
startupTimeoutMs?: number;
|
|
13
|
+
requestTimeoutMs?: number;
|
|
9
14
|
}
|
|
10
15
|
export declare class CommandletAdapter implements ExecutionAdapter {
|
|
11
16
|
private options;
|
|
@@ -13,6 +18,10 @@ export declare class CommandletAdapter implements ExecutionAdapter {
|
|
|
13
18
|
private requestId;
|
|
14
19
|
private pendingRequests;
|
|
15
20
|
private buffer;
|
|
21
|
+
private readonly spawnProcess;
|
|
22
|
+
private readonly platform;
|
|
23
|
+
private readonly startupTimeoutMs;
|
|
24
|
+
private readonly requestTimeoutMs;
|
|
16
25
|
constructor(options: CommandletAdapterOptions);
|
|
17
26
|
initialize(): Promise<void>;
|
|
18
27
|
execute(_subsystem: string, method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CommandletAdapter spawns UnrealEditor-Cmd
|
|
2
|
+
* CommandletAdapter spawns the platform-appropriate UnrealEditor-Cmd binary with -run=blueprintextractor
|
|
3
3
|
* and communicates via stdin/stdout JSON-RPC.
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import { COMMANDLET_CAPABILITIES } from '../execution-adapter.js';
|
|
7
|
+
import { resolveEditorExecutable } from '../../project-controller.js';
|
|
7
8
|
const STARTUP_TIMEOUT_MS = 30_000;
|
|
8
9
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
10
|
export class CommandletAdapter {
|
|
@@ -12,14 +13,22 @@ export class CommandletAdapter {
|
|
|
12
13
|
requestId = 0;
|
|
13
14
|
pendingRequests = new Map();
|
|
14
15
|
buffer = '';
|
|
16
|
+
spawnProcess;
|
|
17
|
+
platform;
|
|
18
|
+
startupTimeoutMs;
|
|
19
|
+
requestTimeoutMs;
|
|
15
20
|
constructor(options) {
|
|
16
21
|
this.options = options;
|
|
22
|
+
this.spawnProcess = options.spawnProcess ?? spawn;
|
|
23
|
+
this.platform = options.platform ?? process.platform;
|
|
24
|
+
this.startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
|
|
25
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
17
26
|
}
|
|
18
27
|
async initialize() {
|
|
19
28
|
if (this.process)
|
|
20
29
|
return;
|
|
21
|
-
const editorCmd =
|
|
22
|
-
this.process =
|
|
30
|
+
const editorCmd = await resolveEditorExecutable(this.options.engineRoot, this.platform, 'commandlet');
|
|
31
|
+
this.process = this.spawnProcess(editorCmd, [
|
|
23
32
|
this.options.projectPath,
|
|
24
33
|
'-run=blueprintextractor',
|
|
25
34
|
'-stdin',
|
|
@@ -45,8 +54,8 @@ export class CommandletAdapter {
|
|
|
45
54
|
// Wait for startup
|
|
46
55
|
await new Promise((resolve, reject) => {
|
|
47
56
|
const timer = setTimeout(() => {
|
|
48
|
-
reject(new Error(`Commandlet startup timed out after ${
|
|
49
|
-
},
|
|
57
|
+
reject(new Error(`Commandlet startup timed out after ${this.startupTimeoutMs}ms`));
|
|
58
|
+
}, this.startupTimeoutMs);
|
|
50
59
|
const onData = () => {
|
|
51
60
|
clearTimeout(timer);
|
|
52
61
|
this.process?.stdout?.off('data', onData);
|
|
@@ -69,8 +78,8 @@ export class CommandletAdapter {
|
|
|
69
78
|
return new Promise((resolve, reject) => {
|
|
70
79
|
const timer = setTimeout(() => {
|
|
71
80
|
this.pendingRequests.delete(id);
|
|
72
|
-
reject(new Error(`Commandlet request timed out after ${
|
|
73
|
-
},
|
|
81
|
+
reject(new Error(`Commandlet request timed out after ${this.requestTimeoutMs}ms`));
|
|
82
|
+
}, this.requestTimeoutMs);
|
|
74
83
|
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
75
84
|
this.process.stdin.write(request + '\n');
|
|
76
85
|
});
|
|
@@ -173,7 +173,7 @@ export const NEXT_STEP_HINTS_REGISTRY = new Map([
|
|
|
173
173
|
'Use get_tool_help for detailed usage of newly available tools.',
|
|
174
174
|
],
|
|
175
175
|
on_error: [
|
|
176
|
-
'Check available scope names: widget_authoring, material_authoring, blueprint_authoring, schema_ai_authoring, animation_authoring, data_tables, import, automation_testing, verification.',
|
|
176
|
+
'Check available scope names: widget_authoring, material_authoring, blueprint_authoring, schema_ai_authoring, animation_authoring, data_tables, import, automation_testing, verification, analysis, project_intelligence.',
|
|
177
177
|
],
|
|
178
178
|
}],
|
|
179
179
|
['find_and_extract', {
|
|
@@ -16,9 +16,11 @@ type ResolveProjectInputsDeps = {
|
|
|
16
16
|
firstDefinedString(...values: Array<unknown>): string | undefined;
|
|
17
17
|
env?: NodeJS.ProcessEnv;
|
|
18
18
|
workspaceProjectPath?: string;
|
|
19
|
+
platform?: NodeJS.Platform;
|
|
19
20
|
};
|
|
20
21
|
export declare function rememberExternalBuild(result: CompileProjectCodeResult): Record<string, unknown>;
|
|
21
22
|
export declare function getProjectAutomationContext(deps: GetProjectAutomationContextDeps): Promise<ProjectAutomationContext>;
|
|
23
|
+
export declare function getHeuristicEngineCandidates(platform?: NodeJS.Platform): string[];
|
|
22
24
|
export declare const HEURISTIC_ENGINE_CANDIDATES: string[];
|
|
23
25
|
export declare function resolveProjectInputs(request: ProjectInputsRequest, deps: ResolveProjectInputsDeps): Promise<ResolvedProjectInputs>;
|
|
24
26
|
export {};
|
|
@@ -31,46 +31,85 @@ export async function getProjectAutomationContext(deps) {
|
|
|
31
31
|
setCachedContext(nextContext);
|
|
32
32
|
return nextContext;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (cachedHeuristicEngineRoot !== undefined) {
|
|
45
|
-
return cachedHeuristicEngineRoot || undefined;
|
|
34
|
+
const cachedHeuristicEngineRoots = new Map();
|
|
35
|
+
export function getHeuristicEngineCandidates(platform = process.platform) {
|
|
36
|
+
if (platform === 'win32') {
|
|
37
|
+
return [
|
|
38
|
+
'C:/Program Files/Epic Games/UE_5.7',
|
|
39
|
+
'C:/Program Files/Epic Games/UE_5.6',
|
|
40
|
+
'C:/Program Files/Epic Games/UE_5.5',
|
|
41
|
+
'C:/Program Files/Epic Games/UE_5.4',
|
|
42
|
+
'C:/Program Files/Epic Games/UE_5.3',
|
|
43
|
+
];
|
|
46
44
|
}
|
|
47
|
-
|
|
45
|
+
if (platform === 'darwin') {
|
|
46
|
+
return [
|
|
47
|
+
'/Users/Shared/Epic Games/UE_5.7',
|
|
48
|
+
'/Users/Shared/Epic Games/UE_5.6',
|
|
49
|
+
'/Users/Shared/Epic Games/UE_5.5',
|
|
50
|
+
'/Users/Shared/Epic Games/UE_5.4',
|
|
51
|
+
'/Users/Shared/Epic Games/UE_5.3',
|
|
52
|
+
'/Users/Shared/EpicGames/UE_5.7',
|
|
53
|
+
'/Users/Shared/EpicGames/UE_5.6',
|
|
54
|
+
'/Users/Shared/EpicGames/UE_5.5',
|
|
55
|
+
'/Users/Shared/EpicGames/UE_5.4',
|
|
56
|
+
'/Users/Shared/EpicGames/UE_5.3',
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
export const HEURISTIC_ENGINE_CANDIDATES = getHeuristicEngineCandidates();
|
|
62
|
+
function getEngineMarkers(platform) {
|
|
63
|
+
if (platform === 'win32') {
|
|
64
|
+
return ['Engine/Build/BatchFiles/Build.bat'];
|
|
65
|
+
}
|
|
66
|
+
if (platform === 'darwin') {
|
|
67
|
+
return [
|
|
68
|
+
'Engine/Build/BatchFiles/Mac/Build.sh',
|
|
69
|
+
'Engine/Build/BatchFiles/Build.sh',
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
'Engine/Build/BatchFiles/Linux/Build.sh',
|
|
74
|
+
'Engine/Build/BatchFiles/Build.sh',
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
async function accessFirstMatchingMarker(root, markers) {
|
|
78
|
+
for (const marker of markers) {
|
|
48
79
|
try {
|
|
49
|
-
await access(resolve(
|
|
50
|
-
|
|
51
|
-
return candidate;
|
|
80
|
+
await access(resolve(root, marker), fsConstants.F_OK);
|
|
81
|
+
return true;
|
|
52
82
|
}
|
|
53
83
|
catch {
|
|
54
|
-
//
|
|
84
|
+
// try the next marker
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
async function probeEngineRootHeuristic(platform) {
|
|
90
|
+
if (cachedHeuristicEngineRoots.has(platform)) {
|
|
91
|
+
return cachedHeuristicEngineRoots.get(platform) || undefined;
|
|
92
|
+
}
|
|
93
|
+
for (const candidate of getHeuristicEngineCandidates(platform)) {
|
|
94
|
+
if (await accessFirstMatchingMarker(candidate, getEngineMarkers(platform))) {
|
|
95
|
+
cachedHeuristicEngineRoots.set(platform, candidate);
|
|
96
|
+
return candidate;
|
|
55
97
|
}
|
|
56
98
|
}
|
|
57
|
-
|
|
99
|
+
cachedHeuristicEngineRoots.set(platform, '');
|
|
58
100
|
return undefined;
|
|
59
101
|
}
|
|
60
|
-
async function probePreferredEngineRoot(candidates) {
|
|
102
|
+
async function probePreferredEngineRoot(candidates, platform) {
|
|
103
|
+
const markers = getEngineMarkers(platform);
|
|
61
104
|
for (const candidate of candidates) {
|
|
62
|
-
|
|
63
|
-
await access(resolve(candidate, ENGINE_MARKER), fsConstants.F_OK);
|
|
105
|
+
if (await accessFirstMatchingMarker(candidate, markers)) {
|
|
64
106
|
return candidate;
|
|
65
107
|
}
|
|
66
|
-
catch {
|
|
67
|
-
// try next candidate
|
|
68
|
-
}
|
|
69
108
|
}
|
|
70
109
|
return undefined;
|
|
71
110
|
}
|
|
72
111
|
export async function resolveProjectInputs(request, deps) {
|
|
73
|
-
const { getProjectAutomationContext, firstDefinedString, env = process.env, workspaceProjectPath, } = deps;
|
|
112
|
+
const { getProjectAutomationContext, firstDefinedString, env = process.env, workspaceProjectPath, platform = process.platform, } = deps;
|
|
74
113
|
let context = null;
|
|
75
114
|
let contextError;
|
|
76
115
|
if (!request.engine_root || !request.project_path || !request.target) {
|
|
@@ -94,7 +133,7 @@ export async function resolveProjectInputs(request, deps) {
|
|
|
94
133
|
const projectPath = firstDefinedString(request.project_path, projectPathFromContext, projectPathFromWorkspace, projectPathFromEnv);
|
|
95
134
|
const target = firstDefinedString(request.target, targetFromContext, targetFromWorkspace, targetFromEnv);
|
|
96
135
|
const engineAssociation = projectPath ? await readProjectEngineAssociation(projectPath) : undefined;
|
|
97
|
-
const associationCandidates = buildEngineAssociationCandidates(engineAssociation);
|
|
136
|
+
const associationCandidates = buildEngineAssociationCandidates(engineAssociation, platform);
|
|
98
137
|
let engineRoot = firstDefinedString(request.engine_root, engineRootFromContext, engineRootFromEnv);
|
|
99
138
|
let engineRootSource;
|
|
100
139
|
if (request.engine_root) {
|
|
@@ -107,8 +146,8 @@ export async function resolveProjectInputs(request, deps) {
|
|
|
107
146
|
engineRootSource = 'environment';
|
|
108
147
|
}
|
|
109
148
|
else {
|
|
110
|
-
const preferredCandidate = await probePreferredEngineRoot(associationCandidates);
|
|
111
|
-
const heuristicRoot = preferredCandidate ?? await probeEngineRootHeuristic();
|
|
149
|
+
const preferredCandidate = await probePreferredEngineRoot(associationCandidates, platform);
|
|
150
|
+
const heuristicRoot = preferredCandidate ?? await probeEngineRootHeuristic(platform);
|
|
112
151
|
if (heuristicRoot) {
|
|
113
152
|
engineRoot = heuristicRoot;
|
|
114
153
|
engineRootSource = 'filesystem_heuristic';
|
|
@@ -178,6 +178,18 @@ export function collectRelatedResources(toolName) {
|
|
|
178
178
|
|| toolName === 'relaunch_pie') {
|
|
179
179
|
resources.add('blueprint://project-automation');
|
|
180
180
|
}
|
|
181
|
+
if (toolName === 'review_blueprint' || toolName === 'audit_project_assets') {
|
|
182
|
+
resources.add('blueprint://analysis-workflows');
|
|
183
|
+
}
|
|
184
|
+
if (toolName === 'refresh_project_index'
|
|
185
|
+
|| toolName === 'get_project_index_status'
|
|
186
|
+
|| toolName === 'search_project_context'
|
|
187
|
+
|| toolName === 'get_editor_context') {
|
|
188
|
+
resources.add('blueprint://project-intelligence-workflows');
|
|
189
|
+
}
|
|
190
|
+
if (toolName === 'get_editor_context') {
|
|
191
|
+
resources.add('blueprint://project-automation');
|
|
192
|
+
}
|
|
181
193
|
if (toolName.includes('animation')
|
|
182
194
|
|| toolName.includes('motion')
|
|
183
195
|
|| toolName === 'compare_motion_capture_bundle') {
|
|
@@ -7,4 +7,4 @@ export declare function normalizeFilesystemPath(input: string | undefined): stri
|
|
|
7
7
|
export declare function filesystemPathsEqual(left: string | undefined, right: string | undefined): boolean;
|
|
8
8
|
export declare function findNearestWorkspaceProject(startDir?: string): Promise<WorkspaceProjectResolution>;
|
|
9
9
|
export declare function readProjectEngineAssociation(projectPath: string): Promise<string | undefined>;
|
|
10
|
-
export declare function buildEngineAssociationCandidates(engineAssociation: string | undefined): string[];
|
|
10
|
+
export declare function buildEngineAssociationCandidates(engineAssociation: string | undefined, platform?: NodeJS.Platform): string[];
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { readFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[\\/]/;
|
|
4
|
+
const WINDOWS_UNC_PATH = /^(?:\\\\|\/\/)[^\\/]+[\\/][^\\/]+/;
|
|
5
|
+
function isWindowsStylePath(input) {
|
|
6
|
+
return WINDOWS_DRIVE_PATH.test(input) || WINDOWS_UNC_PATH.test(input);
|
|
7
|
+
}
|
|
3
8
|
export function normalizeFilesystemPath(input) {
|
|
4
9
|
if (!input) {
|
|
5
10
|
return undefined;
|
|
6
11
|
}
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const trimmed = input.trim();
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (isWindowsStylePath(trimmed)) {
|
|
17
|
+
return path.win32.normalize(trimmed).replaceAll('\\', '/').toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
return path.posix.normalize(trimmed);
|
|
11
20
|
}
|
|
12
21
|
export function filesystemPathsEqual(left, right) {
|
|
13
22
|
const normalizedLeft = normalizeFilesystemPath(left);
|
|
@@ -58,7 +67,7 @@ export async function readProjectEngineAssociation(projectPath) {
|
|
|
58
67
|
return undefined;
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
|
-
export function buildEngineAssociationCandidates(engineAssociation) {
|
|
70
|
+
export function buildEngineAssociationCandidates(engineAssociation, platform = process.platform) {
|
|
62
71
|
if (!engineAssociation) {
|
|
63
72
|
return [];
|
|
64
73
|
}
|
|
@@ -67,11 +76,20 @@ export function buildEngineAssociationCandidates(engineAssociation) {
|
|
|
67
76
|
return [];
|
|
68
77
|
}
|
|
69
78
|
const candidates = new Set();
|
|
79
|
+
const installRoots = platform === 'win32'
|
|
80
|
+
? ['C:/Program Files/Epic Games']
|
|
81
|
+
: platform === 'darwin'
|
|
82
|
+
? ['/Users/Shared/Epic Games', '/Users/Shared/EpicGames']
|
|
83
|
+
: [];
|
|
70
84
|
if (/^\d+\.\d+$/.test(trimmed)) {
|
|
71
|
-
|
|
85
|
+
for (const root of installRoots) {
|
|
86
|
+
candidates.add(`${root}/UE_${trimmed}`);
|
|
87
|
+
}
|
|
72
88
|
}
|
|
73
89
|
if (/^UE_\d+\.\d+$/.test(trimmed)) {
|
|
74
|
-
|
|
90
|
+
for (const root of installRoots) {
|
|
91
|
+
candidates.add(`${root}/${trimmed}`);
|
|
92
|
+
}
|
|
75
93
|
}
|
|
76
94
|
candidates.add(trimmed);
|
|
77
95
|
return Array.from(candidates);
|
|
@@ -122,6 +122,8 @@ export interface ProjectControllerLike {
|
|
|
122
122
|
}): Promise<RestartReconnectResult>;
|
|
123
123
|
}
|
|
124
124
|
export declare function resolveCommandInvocation(executable: string, args: string[], platform: NodeJS.Platform, env?: NodeJS.ProcessEnv): CommandInvocation;
|
|
125
|
+
export declare function getBuildScriptCandidates(engineRoot: string, platform: NodeJS.Platform): string[];
|
|
126
|
+
export declare function resolveBuildScript(engineRoot: string, platform: NodeJS.Platform): Promise<string>;
|
|
125
127
|
export declare function resolveEditorExecutable(engineRoot: string, platform: NodeJS.Platform, mode?: 'editor' | 'commandlet'): Promise<string>;
|
|
126
128
|
export declare function classifyChangedPaths(changedPaths: string[], forceRebuild?: boolean): SyncStrategyPlan;
|
|
127
129
|
/** Classify UBT build output into an error category. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execSync, spawn } from 'node:child_process';
|
|
2
2
|
import { access, readdir, unlink } from 'node:fs/promises';
|
|
3
3
|
import { constants as fsConstants } from 'node:fs';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { dirname, resolve, win32 as win32Path } from 'node:path';
|
|
5
5
|
const DEFAULT_BUILD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
6
6
|
const DEFAULT_DISCONNECT_TIMEOUT_MS = 60 * 1000;
|
|
7
7
|
const DEFAULT_RECONNECT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
@@ -42,6 +42,35 @@ export function resolveCommandInvocation(executable, args, platform, env = proce
|
|
|
42
42
|
args: ['/d', '/s', '/c', commandLine],
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
export function getBuildScriptCandidates(engineRoot, platform) {
|
|
46
|
+
if (platform === 'win32') {
|
|
47
|
+
return [
|
|
48
|
+
resolve(engineRoot, 'Engine', 'Build', 'BatchFiles', 'Build.bat'),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
if (platform === 'darwin') {
|
|
52
|
+
return [
|
|
53
|
+
resolve(engineRoot, 'Engine', 'Build', 'BatchFiles', 'Mac', 'Build.sh'),
|
|
54
|
+
resolve(engineRoot, 'Engine', 'Build', 'BatchFiles', 'Build.sh'),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
resolve(engineRoot, 'Engine', 'Build', 'BatchFiles', 'Linux', 'Build.sh'),
|
|
59
|
+
resolve(engineRoot, 'Engine', 'Build', 'BatchFiles', 'Build.sh'),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
export async function resolveBuildScript(engineRoot, platform) {
|
|
63
|
+
for (const candidate of getBuildScriptCandidates(engineRoot, platform)) {
|
|
64
|
+
try {
|
|
65
|
+
await access(candidate, fsConstants.F_OK);
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Try the next platform-specific location.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Unable to locate an Unreal build script under ${engineRoot} for platform ${platform}.`);
|
|
73
|
+
}
|
|
45
74
|
export async function resolveEditorExecutable(engineRoot, platform, mode = 'editor') {
|
|
46
75
|
const executableName = mode === 'commandlet'
|
|
47
76
|
? (platform === 'win32' ? 'UnrealEditor-Cmd.exe' : 'UnrealEditor-Cmd')
|
|
@@ -135,6 +164,9 @@ function fileName(path) {
|
|
|
135
164
|
const lastSlash = normalized.lastIndexOf('/');
|
|
136
165
|
return lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
|
137
166
|
}
|
|
167
|
+
function normalizeFilesystemPathForCommand(path, platform) {
|
|
168
|
+
return platform === 'win32' ? win32Path.normalize(path) : path;
|
|
169
|
+
}
|
|
138
170
|
export function classifyChangedPaths(changedPaths, forceRebuild = false) {
|
|
139
171
|
const reasons = new Set();
|
|
140
172
|
if (forceRebuild) {
|
|
@@ -325,10 +357,8 @@ export class ProjectController {
|
|
|
325
357
|
if (!target) {
|
|
326
358
|
throw new Error('compile_project_code requires target or UE_PROJECT_TARGET/UE_EDITOR_TARGET');
|
|
327
359
|
}
|
|
328
|
-
const buildScript = this.platform
|
|
329
|
-
|
|
330
|
-
: resolve(engineRoot, 'Engine', 'Build', 'BatchFiles', 'Build.sh');
|
|
331
|
-
await access(buildScript, fsConstants.F_OK);
|
|
360
|
+
const buildScript = await resolveBuildScript(engineRoot, this.platform);
|
|
361
|
+
const commandProjectPath = normalizeFilesystemPathForCommand(projectPath, this.platform);
|
|
332
362
|
let uhtCacheFilesDeleted;
|
|
333
363
|
if (request.clearUhtCache) {
|
|
334
364
|
uhtCacheFilesDeleted = await clearUhtCacheFiles(dirname(projectPath));
|
|
@@ -337,7 +367,7 @@ export class ProjectController {
|
|
|
337
367
|
target,
|
|
338
368
|
platform,
|
|
339
369
|
configuration,
|
|
340
|
-
`-Project=${
|
|
370
|
+
`-Project=${commandProjectPath}`,
|
|
341
371
|
'-WaitMutex',
|
|
342
372
|
'-NoHotReloadFromIDE',
|
|
343
373
|
];
|