unreal-engine-mcp-server 0.3.1 → 0.4.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/.env.production +1 -1
- package/README.md +1 -2
- package/dist/index.js +16 -18
- package/dist/resources/assets.d.ts +3 -2
- package/dist/resources/assets.js +96 -72
- package/dist/resources/levels.js +2 -2
- package/dist/tools/assets.js +6 -2
- package/dist/tools/build_environment_advanced.js +46 -42
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +46 -3
- package/dist/tools/consolidated-tool-handlers.js +327 -717
- package/dist/tools/debug.js +4 -6
- package/dist/tools/rc.js +2 -2
- package/dist/tools/sequence.js +21 -2
- package/dist/utils/response-validator.d.ts +6 -1
- package/dist/utils/response-validator.js +41 -14
- package/package.json +4 -4
- package/server.json +2 -2
- package/src/index.ts +18 -19
- package/src/resources/assets.ts +97 -73
- package/src/resources/levels.ts +2 -2
- package/src/tools/assets.ts +6 -2
- package/src/tools/build_environment_advanced.ts +46 -42
- package/src/tools/consolidated-tool-definitions.ts +46 -3
- package/src/tools/consolidated-tool-handlers.ts +313 -746
- package/src/tools/debug.ts +4 -6
- package/src/tools/rc.ts +2 -2
- package/src/tools/sequence.ts +21 -2
- package/src/utils/response-validator.ts +46 -18
- package/dist/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- package/src/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
package/dist/tools/debug.js
CHANGED
|
@@ -20,13 +20,11 @@ export class DebugVisualizationTools {
|
|
|
20
20
|
async pyDraw(scriptBody) {
|
|
21
21
|
const script = `
|
|
22
22
|
import unreal
|
|
23
|
-
#
|
|
23
|
+
# Strict modern API: require UnrealEditorSubsystem (UE 5.1+)
|
|
24
24
|
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
25
|
-
if ues:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# Fallback to deprecated API if subsystem not available
|
|
29
|
-
world = unreal.EditorLevelLibrary.get_editor_world()
|
|
25
|
+
if not ues:
|
|
26
|
+
raise Exception('UnrealEditorSubsystem not available')
|
|
27
|
+
world = ues.get_editor_world()
|
|
30
28
|
${scriptBody}
|
|
31
29
|
`.trim()
|
|
32
30
|
.replace(/\r?\n/g, '\n');
|
package/dist/tools/rc.js
CHANGED
|
@@ -142,7 +142,7 @@ except Exception as e:
|
|
|
142
142
|
}
|
|
143
143
|
// Expose an actor by label/name into a preset
|
|
144
144
|
async exposeActor(params) {
|
|
145
|
-
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target,
|
|
145
|
+
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n # Expose with a default-initialized optional args struct (cannot pass None)\n args = unreal.RemoteControlOptionalExposeArgs()\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, args)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
|
|
146
146
|
const resp = await this.executeWithRetry(() => this.bridge.executePython(python), 'exposeActor');
|
|
147
147
|
const result = this.parsePythonResult(resp, 'exposeActor');
|
|
148
148
|
// Clear cache for this preset to force refresh
|
|
@@ -153,7 +153,7 @@ except Exception as e:
|
|
|
153
153
|
}
|
|
154
154
|
// Expose a property on an object into a preset
|
|
155
155
|
async exposeProperty(params) {
|
|
156
|
-
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name,
|
|
156
|
+
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n # Expose with default optional args struct (cannot pass None)\n args = unreal.RemoteControlOptionalExposeArgs()\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name, args)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
|
|
157
157
|
const resp = await this.executeWithRetry(() => this.bridge.executePython(python), 'exposeProperty');
|
|
158
158
|
const result = this.parsePythonResult(resp, 'exposeProperty');
|
|
159
159
|
// Clear cache for this preset to force refresh
|
package/dist/tools/sequence.js
CHANGED
|
@@ -282,12 +282,31 @@ except Exception as e:
|
|
|
282
282
|
* Play the current level sequence
|
|
283
283
|
*/
|
|
284
284
|
async play(params) {
|
|
285
|
+
const loop = params?.loopMode || '';
|
|
285
286
|
const py = `
|
|
286
287
|
import unreal, json
|
|
288
|
+
|
|
289
|
+
# Helper to resolve SequencerLoopMode from a friendly string
|
|
290
|
+
def _resolve_loop_mode(mode_str):
|
|
291
|
+
try:
|
|
292
|
+
m = str(mode_str).lower()
|
|
293
|
+
slm = unreal.SequencerLoopMode
|
|
294
|
+
if m in ('once','noloop','no_loop'):
|
|
295
|
+
return getattr(slm, 'SLM_NoLoop', getattr(slm, 'NoLoop'))
|
|
296
|
+
if m in ('loop',):
|
|
297
|
+
return getattr(slm, 'SLM_Loop', getattr(slm, 'Loop'))
|
|
298
|
+
if m in ('pingpong','ping_pong'):
|
|
299
|
+
return getattr(slm, 'SLM_PingPong', getattr(slm, 'PingPong'))
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
return None
|
|
303
|
+
|
|
287
304
|
try:
|
|
288
305
|
unreal.LevelSequenceEditorBlueprintLibrary.play()
|
|
289
|
-
|
|
290
|
-
|
|
306
|
+
loop_mode = _resolve_loop_mode('${loop}')
|
|
307
|
+
if loop_mode is not None:
|
|
308
|
+
unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode(loop_mode)
|
|
309
|
+
print('RESULT:' + json.dumps({'success': True, 'playing': True, 'loopMode': '${loop || 'default'}'}))
|
|
291
310
|
except Exception as e:
|
|
292
311
|
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
293
312
|
`.trim();
|
|
@@ -19,7 +19,12 @@ export declare class ResponseValidator {
|
|
|
19
19
|
structuredContent?: any;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Wrap a tool response with validation
|
|
22
|
+
* Wrap a tool response with validation and MCP-compliant content shape.
|
|
23
|
+
*
|
|
24
|
+
* MCP tools/call responses must contain a `content` array. Many internal
|
|
25
|
+
* handlers return structured JSON objects (e.g., { success, message, ... }).
|
|
26
|
+
* This wrapper serializes such objects into a single text block while keeping
|
|
27
|
+
* existing `content` responses intact.
|
|
23
28
|
*/
|
|
24
29
|
wrapResponse(toolName: string, response: any): any;
|
|
25
30
|
/**
|
|
@@ -76,14 +76,17 @@ export class ResponseValidator {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* Wrap a tool response with validation
|
|
79
|
+
* Wrap a tool response with validation and MCP-compliant content shape.
|
|
80
|
+
*
|
|
81
|
+
* MCP tools/call responses must contain a `content` array. Many internal
|
|
82
|
+
* handlers return structured JSON objects (e.g., { success, message, ... }).
|
|
83
|
+
* This wrapper serializes such objects into a single text block while keeping
|
|
84
|
+
* existing `content` responses intact.
|
|
80
85
|
*/
|
|
81
86
|
wrapResponse(toolName, response) {
|
|
82
87
|
// Ensure response is safe to serialize first
|
|
83
88
|
try {
|
|
84
|
-
// The response should already be cleaned, but double-check
|
|
85
89
|
if (response && typeof response === 'object') {
|
|
86
|
-
// Make sure we can serialize it
|
|
87
90
|
JSON.stringify(response);
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -91,21 +94,45 @@ export class ResponseValidator {
|
|
|
91
94
|
log.error(`Response for ${toolName} contains circular references, cleaning...`);
|
|
92
95
|
response = cleanObject(response);
|
|
93
96
|
}
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
// If handler already returned MCP content, keep it as-is (still validate)
|
|
98
|
+
const alreadyMcpShaped = response && typeof response === 'object' && Array.isArray(response.content);
|
|
99
|
+
// Choose the payload to validate: if already MCP-shaped, validate the
|
|
100
|
+
// structured content extracted from text; otherwise validate the object directly.
|
|
101
|
+
const validationTarget = alreadyMcpShaped ? response : response;
|
|
102
|
+
const validation = this.validateResponse(toolName, validationTarget);
|
|
96
103
|
if (!validation.valid) {
|
|
97
104
|
log.warn(`Tool ${toolName} response validation failed:`, validation.errors);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
}
|
|
106
|
+
// If it's already MCP-shaped, return as-is (optionally append validation meta)
|
|
107
|
+
if (alreadyMcpShaped) {
|
|
108
|
+
if (!validation.valid) {
|
|
109
|
+
try {
|
|
110
|
+
response._validation = { valid: false, errors: validation.errors };
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
104
113
|
}
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
// Otherwise, wrap structured result into MCP content
|
|
117
|
+
let text;
|
|
118
|
+
try {
|
|
119
|
+
// Pretty-print small objects for readability
|
|
120
|
+
text = typeof response === 'string'
|
|
121
|
+
? response
|
|
122
|
+
: JSON.stringify(response ?? { success: true }, null, 2);
|
|
123
|
+
}
|
|
124
|
+
catch (_e) {
|
|
125
|
+
text = String(response);
|
|
126
|
+
}
|
|
127
|
+
const wrapped = {
|
|
128
|
+
content: [
|
|
129
|
+
{ type: 'text', text }
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
if (!validation.valid) {
|
|
133
|
+
wrapped._validation = { valid: false, errors: validation.errors };
|
|
105
134
|
}
|
|
106
|
-
|
|
107
|
-
// Adding it can cause circular references
|
|
108
|
-
return response;
|
|
135
|
+
return wrapped;
|
|
109
136
|
}
|
|
110
137
|
/**
|
|
111
138
|
* Get validation statistics
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unreal-engine-mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Production-ready MCP server for Unreal Engine integration
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Production-ready MCP server for Unreal Engine integration using consolidated tools only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"mcpName": "io.github.ChiR24/unreal-engine-mcp",
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.18.1",
|
|
38
38
|
"ajv": "^8.12.0",
|
|
39
|
-
"axios": "^1.
|
|
39
|
+
"axios": "^1.12.2",
|
|
40
40
|
"dotenv": "^16.4.5",
|
|
41
41
|
"ws": "^8.18.0",
|
|
42
42
|
"yargs": "^17.7.2",
|
package/server.json
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
|
|
3
3
|
"name": "io.github.ChiR24/unreal-engine-mcp",
|
|
4
4
|
"description": "Production-ready MCP server for Unreal Engine with comprehensive game development tools",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.0",
|
|
6
6
|
"packages": [
|
|
7
7
|
{
|
|
8
8
|
"registry_type": "npm",
|
|
9
9
|
"registry_base_url": "https://registry.npmjs.org",
|
|
10
10
|
"identifier": "unreal-engine-mcp-server",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.4.0",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio"
|
|
14
14
|
},
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { BlueprintTools } from './tools/blueprint.js';
|
|
|
17
17
|
import { LevelTools } from './tools/level.js';
|
|
18
18
|
import { LightingTools } from './tools/lighting.js';
|
|
19
19
|
import { LandscapeTools } from './tools/landscape.js';
|
|
20
|
+
import { BuildEnvironmentAdvanced } from './tools/build_environment_advanced.js';
|
|
20
21
|
import { FoliageTools } from './tools/foliage.js';
|
|
21
22
|
import { DebugVisualizationTools } from './tools/debug.js';
|
|
22
23
|
import { PerformanceTools } from './tools/performance.js';
|
|
@@ -27,8 +28,6 @@ import { SequenceTools } from './tools/sequence.js';
|
|
|
27
28
|
import { IntrospectionTools } from './tools/introspection.js';
|
|
28
29
|
import { VisualTools } from './tools/visual.js';
|
|
29
30
|
import { EngineTools } from './tools/engine.js';
|
|
30
|
-
import { toolDefinitions } from './tools/tool-definitions.js';
|
|
31
|
-
import { handleToolCall } from './tools/tool-handlers.js';
|
|
32
31
|
import { consolidatedToolDefinitions } from './tools/consolidated-tool-definitions.js';
|
|
33
32
|
import { handleConsolidatedToolCall } from './tools/consolidated-tool-handlers.js';
|
|
34
33
|
import { prompts } from './prompts/index.js';
|
|
@@ -81,14 +80,13 @@ let lastHealthSuccessAt = 0;
|
|
|
81
80
|
|
|
82
81
|
// Configuration
|
|
83
82
|
const CONFIG = {
|
|
84
|
-
//
|
|
85
|
-
USE_CONSOLIDATED_TOOLS: process.env.USE_CONSOLIDATED_TOOLS !== 'false',
|
|
83
|
+
// Tooling: use consolidated tools only (13 tools)
|
|
86
84
|
// Connection retry settings
|
|
87
85
|
MAX_RETRY_ATTEMPTS: 3,
|
|
88
86
|
RETRY_DELAY_MS: 2000,
|
|
89
87
|
// Server info
|
|
90
88
|
SERVER_NAME: 'unreal-engine-mcp',
|
|
91
|
-
SERVER_VERSION: '0.
|
|
89
|
+
SERVER_VERSION: '0.4.0',
|
|
92
90
|
// Monitoring
|
|
93
91
|
HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
|
|
94
92
|
};
|
|
@@ -149,9 +147,9 @@ export async function createServer() {
|
|
|
149
147
|
// Disable auto-reconnect loops; connect only on-demand
|
|
150
148
|
bridge.setAutoReconnectEnabled(false);
|
|
151
149
|
|
|
152
|
-
|
|
150
|
+
// Initialize response validation with schemas
|
|
153
151
|
log.debug('Initializing response validation...');
|
|
154
|
-
const toolDefs =
|
|
152
|
+
const toolDefs = consolidatedToolDefinitions;
|
|
155
153
|
toolDefs.forEach((tool: any) => {
|
|
156
154
|
if (tool.outputSchema) {
|
|
157
155
|
responseValidator.registerSchema(tool.name, tool.outputSchema);
|
|
@@ -227,6 +225,7 @@ export async function createServer() {
|
|
|
227
225
|
const lightingTools = new LightingTools(bridge);
|
|
228
226
|
const landscapeTools = new LandscapeTools(bridge);
|
|
229
227
|
const foliageTools = new FoliageTools(bridge);
|
|
228
|
+
const buildEnvAdvanced = new BuildEnvironmentAdvanced(bridge);
|
|
230
229
|
const debugTools = new DebugVisualizationTools(bridge);
|
|
231
230
|
const performanceTools = new PerformanceTools(bridge);
|
|
232
231
|
const audioTools = new AudioTools(bridge);
|
|
@@ -433,15 +432,15 @@ export async function createServer() {
|
|
|
433
432
|
throw new Error(`Unknown resource: ${uri}`);
|
|
434
433
|
});
|
|
435
434
|
|
|
436
|
-
|
|
435
|
+
// Handle tool listing - consolidated tools only
|
|
437
436
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
438
|
-
log.info(
|
|
437
|
+
log.info('Serving consolidated tools');
|
|
439
438
|
return {
|
|
440
|
-
tools:
|
|
439
|
+
tools: consolidatedToolDefinitions
|
|
441
440
|
};
|
|
442
441
|
});
|
|
443
442
|
|
|
444
|
-
// Handle tool calls -
|
|
443
|
+
// Handle tool calls - consolidated tools only (13)
|
|
445
444
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
446
445
|
const { name, arguments: args } = request.params;
|
|
447
446
|
const startTime = Date.now();
|
|
@@ -460,7 +459,7 @@ export async function createServer() {
|
|
|
460
459
|
return notConnected;
|
|
461
460
|
}
|
|
462
461
|
|
|
463
|
-
|
|
462
|
+
// Create tools object for handler
|
|
464
463
|
const tools = {
|
|
465
464
|
actorTools,
|
|
466
465
|
assetTools,
|
|
@@ -474,6 +473,7 @@ export async function createServer() {
|
|
|
474
473
|
lightingTools,
|
|
475
474
|
landscapeTools,
|
|
476
475
|
foliageTools,
|
|
476
|
+
buildEnvAdvanced,
|
|
477
477
|
debugTools,
|
|
478
478
|
performanceTools,
|
|
479
479
|
audioTools,
|
|
@@ -483,19 +483,18 @@ export async function createServer() {
|
|
|
483
483
|
introspectionTools,
|
|
484
484
|
visualTools,
|
|
485
485
|
engineTools,
|
|
486
|
+
// Resources for listing and info
|
|
487
|
+
assetResources,
|
|
488
|
+
actorResources,
|
|
489
|
+
levelResources,
|
|
486
490
|
bridge
|
|
487
491
|
};
|
|
488
492
|
|
|
489
|
-
|
|
493
|
+
// Execute consolidated tool handler
|
|
490
494
|
try {
|
|
491
495
|
log.debug(`Executing tool: ${name}`);
|
|
492
496
|
|
|
493
|
-
let result;
|
|
494
|
-
if (CONFIG.USE_CONSOLIDATED_TOOLS) {
|
|
495
|
-
result = await handleConsolidatedToolCall(name, args, tools);
|
|
496
|
-
} else {
|
|
497
|
-
result = await handleToolCall(name, args, tools);
|
|
498
|
-
}
|
|
497
|
+
let result = await handleConsolidatedToolCall(name, args, tools);
|
|
499
498
|
|
|
500
499
|
log.debug(`Tool ${name} returned result`);
|
|
501
500
|
|
package/src/resources/assets.ts
CHANGED
|
@@ -10,11 +10,35 @@ export class AssetResources {
|
|
|
10
10
|
return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Normalize UE content paths:
|
|
14
|
+
// - Map '/Content' -> '/Game'
|
|
15
|
+
// - Ensure forward slashes
|
|
16
|
+
private normalizeDir(dir: string): string {
|
|
17
|
+
try {
|
|
18
|
+
if (!dir || typeof dir !== 'string') return '/Game';
|
|
19
|
+
let d = dir.replace(/\\/g, '/');
|
|
20
|
+
if (!d.startsWith('/')) d = '/' + d;
|
|
21
|
+
if (d.toLowerCase().startsWith('/content')) {
|
|
22
|
+
d = '/Game' + d.substring('/Content'.length);
|
|
23
|
+
}
|
|
24
|
+
// Collapse multiple slashes
|
|
25
|
+
d = d.replace(/\/+/g, '/');
|
|
26
|
+
// Remove trailing slash except root
|
|
27
|
+
if (d.length > 1) d = d.replace(/\/$/, '');
|
|
28
|
+
return d;
|
|
29
|
+
} catch {
|
|
30
|
+
return '/Game';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
async list(dir = '/Game', _recursive = false, limit = 50) {
|
|
14
35
|
// ALWAYS use non-recursive listing to show only immediate children
|
|
15
36
|
// This prevents timeouts and makes navigation clearer
|
|
16
37
|
_recursive = false; // Force non-recursive
|
|
17
38
|
|
|
39
|
+
// Normalize directory first
|
|
40
|
+
dir = this.normalizeDir(dir);
|
|
41
|
+
|
|
18
42
|
// Cache fast-path
|
|
19
43
|
try {
|
|
20
44
|
const key = this.makeKey(dir, false);
|
|
@@ -50,7 +74,8 @@ export class AssetResources {
|
|
|
50
74
|
const safePageSize = Math.min(pageSize, 50);
|
|
51
75
|
const offset = page * safePageSize;
|
|
52
76
|
|
|
53
|
-
//
|
|
77
|
+
// Normalize directory and check cache for this specific page
|
|
78
|
+
dir = this.normalizeDir(dir);
|
|
54
79
|
const cacheKey = this.makeKey(dir, recursive, page);
|
|
55
80
|
const cached = this.cache.get(cacheKey);
|
|
56
81
|
if (cached && (Date.now() - cached.timestamp) < this.ttlMs) {
|
|
@@ -102,8 +127,8 @@ export class AssetResources {
|
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
/**
|
|
105
|
-
* Directory-based listing
|
|
106
|
-
*
|
|
130
|
+
* Directory-based listing of immediate children using AssetRegistry.
|
|
131
|
+
* Returns both subfolders and assets at the given path.
|
|
107
132
|
*/
|
|
108
133
|
private async listDirectoryOnly(dir: string, _recursive: boolean, limit: number) {
|
|
109
134
|
// Always return only immediate children to avoid timeout and improve navigation
|
|
@@ -112,65 +137,43 @@ export class AssetResources {
|
|
|
112
137
|
import unreal
|
|
113
138
|
import json
|
|
114
139
|
|
|
115
|
-
_dir = r"${dir}"
|
|
140
|
+
_dir = r"${this.normalizeDir(dir)}"
|
|
116
141
|
|
|
117
142
|
try:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# Add folders first
|
|
144
|
-
for folder in sorted(immediate_folders):
|
|
145
|
-
result.append({
|
|
146
|
-
'n': folder,
|
|
147
|
-
'p': _dir + '/' + folder,
|
|
148
|
-
'c': 'Folder',
|
|
149
|
-
'isFolder': True
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
# Add immediate assets (limit to prevent socket issues)
|
|
153
|
-
for asset_path in immediate_assets[:min(${limit}, len(immediate_assets))]:
|
|
154
|
-
name = asset_path.split('/')[-1].split('.')[0]
|
|
155
|
-
result.append({
|
|
156
|
-
'n': name,
|
|
157
|
-
'p': asset_path,
|
|
158
|
-
'c': 'Asset'
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
# Always showing immediate children only
|
|
162
|
-
note = f'Showing immediate children of {_dir} ({len(immediate_folders)} folders, {len(immediate_assets)} files)'
|
|
163
|
-
|
|
143
|
+
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
|
144
|
+
# Immediate subfolders
|
|
145
|
+
sub_paths = ar.get_sub_paths(_dir, False)
|
|
146
|
+
folders_list = []
|
|
147
|
+
for p in sub_paths:
|
|
148
|
+
try:
|
|
149
|
+
name = p.split('/')[-1]
|
|
150
|
+
folders_list.append({'n': name, 'p': p})
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# Immediate assets at this path
|
|
155
|
+
assets_data = ar.get_assets_by_path(_dir, False)
|
|
156
|
+
assets = []
|
|
157
|
+
for a in assets_data[:${limit}]:
|
|
158
|
+
try:
|
|
159
|
+
assets.append({
|
|
160
|
+
'n': str(a.asset_name),
|
|
161
|
+
'p': str(a.object_path),
|
|
162
|
+
'c': str(a.asset_class)
|
|
163
|
+
})
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
|
|
164
167
|
print("RESULT:" + json.dumps({
|
|
165
|
-
'success': True,
|
|
166
|
-
'
|
|
167
|
-
'
|
|
168
|
-
'
|
|
169
|
-
'
|
|
170
|
-
'
|
|
168
|
+
'success': True,
|
|
169
|
+
'path': _dir,
|
|
170
|
+
'folders': len(folders_list),
|
|
171
|
+
'files': len(assets),
|
|
172
|
+
'folders_list': folders_list,
|
|
173
|
+
'assets': assets
|
|
171
174
|
}))
|
|
172
175
|
except Exception as e:
|
|
173
|
-
print("RESULT:" + json.dumps({'success': False, 'error': str(e), '
|
|
176
|
+
print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'path': _dir}))
|
|
174
177
|
`.trim();
|
|
175
178
|
|
|
176
179
|
const resp = await this.bridge.executePython(py);
|
|
@@ -188,21 +191,37 @@ except Exception as e:
|
|
|
188
191
|
try {
|
|
189
192
|
const parsed = JSON.parse(m[1]);
|
|
190
193
|
if (parsed.success) {
|
|
191
|
-
//
|
|
192
|
-
const
|
|
194
|
+
// Map folders and assets to a clear response
|
|
195
|
+
const foldersArr = Array.isArray(parsed.folders_list) ? parsed.folders_list.map((f: any) => ({
|
|
196
|
+
Name: f.n,
|
|
197
|
+
Path: f.p,
|
|
198
|
+
Class: 'Folder',
|
|
199
|
+
isFolder: true
|
|
200
|
+
})) : [];
|
|
201
|
+
|
|
202
|
+
const assetsArr = Array.isArray(parsed.assets) ? parsed.assets.map((a: any) => ({
|
|
193
203
|
Name: a.n,
|
|
194
204
|
Path: a.p,
|
|
195
|
-
Class: a.c,
|
|
196
|
-
isFolder:
|
|
197
|
-
}));
|
|
198
|
-
|
|
205
|
+
Class: a.c || 'Asset',
|
|
206
|
+
isFolder: false
|
|
207
|
+
})) : [];
|
|
208
|
+
|
|
209
|
+
const total = foldersArr.length + assetsArr.length;
|
|
210
|
+
const summary = {
|
|
211
|
+
total,
|
|
212
|
+
folders: foldersArr.length,
|
|
213
|
+
assets: assetsArr.length
|
|
214
|
+
};
|
|
215
|
+
|
|
199
216
|
return {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
217
|
+
success: true,
|
|
218
|
+
path: parsed.path || this.normalizeDir(dir),
|
|
219
|
+
summary,
|
|
220
|
+
foldersList: foldersArr,
|
|
221
|
+
assets: assetsArr,
|
|
222
|
+
count: total,
|
|
223
|
+
note: `Immediate children of ${parsed.path || this.normalizeDir(dir)}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
|
|
224
|
+
method: 'asset_registry_listing'
|
|
206
225
|
};
|
|
207
226
|
}
|
|
208
227
|
} catch {}
|
|
@@ -213,10 +232,13 @@ except Exception as e:
|
|
|
213
232
|
|
|
214
233
|
// Fallback: return empty with explanation
|
|
215
234
|
return {
|
|
235
|
+
success: true,
|
|
236
|
+
path: this.normalizeDir(dir),
|
|
237
|
+
summary: { total: 0, folders: 0, assets: 0 },
|
|
238
|
+
foldersList: [],
|
|
216
239
|
assets: [],
|
|
217
|
-
warning: '
|
|
218
|
-
|
|
219
|
-
method: 'directory_timeout_fallback'
|
|
240
|
+
warning: 'No items at this path or failed to query AssetRegistry.',
|
|
241
|
+
method: 'asset_registry_fallback'
|
|
220
242
|
};
|
|
221
243
|
}
|
|
222
244
|
|
|
@@ -226,9 +248,11 @@ except Exception as e:
|
|
|
226
248
|
return false;
|
|
227
249
|
}
|
|
228
250
|
|
|
251
|
+
// Normalize asset path (support users passing /Content/...)
|
|
252
|
+
const ap = this.normalizeDir(assetPath);
|
|
229
253
|
const py = `
|
|
230
254
|
import unreal
|
|
231
|
-
apath = r"${
|
|
255
|
+
apath = r"${ap}"
|
|
232
256
|
try:
|
|
233
257
|
exists = unreal.EditorAssetLibrary.does_asset_exist(apath)
|
|
234
258
|
print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False'))
|
package/src/resources/levels.ts
CHANGED
|
@@ -56,9 +56,9 @@ export class LevelResources {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
async saveCurrentLevel() {
|
|
59
|
-
//
|
|
59
|
+
// Strict modern API: require LevelEditorSubsystem
|
|
60
60
|
try {
|
|
61
|
-
const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if les
|
|
61
|
+
const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if not les:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': \'LevelEditorSubsystem not available\'}))\n else:\n les.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
|
|
62
62
|
const resp: any = await this.bridge.executePython(py);
|
|
63
63
|
// Handle LogOutput format from executePython
|
|
64
64
|
let out = '';
|
package/src/tools/assets.ts
CHANGED
|
@@ -7,8 +7,12 @@ export class AssetTools {
|
|
|
7
7
|
|
|
8
8
|
async importAsset(sourcePath: string, destinationPath: string) {
|
|
9
9
|
try {
|
|
10
|
-
// Sanitize destination path (remove trailing slash)
|
|
11
|
-
|
|
10
|
+
// Sanitize destination path (remove trailing slash) and normalize UE path
|
|
11
|
+
let cleanDest = destinationPath.replace(/\/$/, '');
|
|
12
|
+
// Map /Content -> /Game for UE asset destinations
|
|
13
|
+
if (/^\/?content(\/|$)/i.test(cleanDest)) {
|
|
14
|
+
cleanDest = '/Game' + cleanDest.replace(/^\/?content/i, '');
|
|
15
|
+
}
|
|
12
16
|
|
|
13
17
|
// Create test FBX file if it's a test file
|
|
14
18
|
if (sourcePath.includes('test_model.fbx')) {
|