unreal-engine-mcp-server 0.3.0 → 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 +6 -1
- package/Dockerfile +11 -28
- package/README.md +1 -2
- package/dist/index.js +120 -54
- package/dist/resources/actors.js +71 -13
- 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 +173 -8
- package/dist/tools/consolidated-tool-handlers.js +331 -718
- package/dist/tools/debug.js +4 -6
- package/dist/tools/rc.js +2 -2
- package/dist/tools/sequence.js +21 -2
- package/dist/unreal-bridge.d.ts +4 -1
- package/dist/unreal-bridge.js +211 -53
- package/dist/utils/http.js +4 -2
- package/dist/utils/response-validator.d.ts +6 -1
- package/dist/utils/response-validator.js +43 -15
- package/package.json +5 -5
- package/server.json +2 -2
- package/src/index.ts +120 -56
- package/src/resources/actors.ts +51 -13
- 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 +173 -8
- package/src/tools/consolidated-tool-handlers.ts +318 -747
- package/src/tools/debug.ts +4 -6
- package/src/tools/rc.ts +2 -2
- package/src/tools/sequence.ts +21 -2
- package/src/unreal-bridge.ts +163 -60
- package/src/utils/http.ts +7 -4
- package/src/utils/response-validator.ts +48 -19
- package/dist/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1007
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- package/src/tools/tool-definitions.ts +0 -1023
- package/src/tools/tool-handlers.ts +0 -973
|
@@ -1,973 +0,0 @@
|
|
|
1
|
-
// Tool handlers for all 16 MCP tools
|
|
2
|
-
import { UnrealBridge } from '../unreal-bridge.js';
|
|
3
|
-
import { ActorTools } from './actors.js';
|
|
4
|
-
import { AssetTools } from './assets.js';
|
|
5
|
-
import { MaterialTools } from './materials.js';
|
|
6
|
-
import { EditorTools } from './editor.js';
|
|
7
|
-
import { AnimationTools } from './animation.js';
|
|
8
|
-
import { PhysicsTools } from './physics.js';
|
|
9
|
-
import { NiagaraTools } from './niagara.js';
|
|
10
|
-
import { BlueprintTools } from './blueprint.js';
|
|
11
|
-
import { LevelTools } from './level.js';
|
|
12
|
-
import { LightingTools } from './lighting.js';
|
|
13
|
-
import { LandscapeTools } from './landscape.js';
|
|
14
|
-
import { FoliageTools } from './foliage.js';
|
|
15
|
-
import { DebugVisualizationTools } from './debug.js';
|
|
16
|
-
import { PerformanceTools } from './performance.js';
|
|
17
|
-
import { AudioTools } from './audio.js';
|
|
18
|
-
import { UITools } from './ui.js';
|
|
19
|
-
import { RcTools } from './rc.js';
|
|
20
|
-
import { SequenceTools } from './sequence.js';
|
|
21
|
-
import { IntrospectionTools } from './introspection.js';
|
|
22
|
-
import { VisualTools } from './visual.js';
|
|
23
|
-
import { EngineTools } from './engine.js';
|
|
24
|
-
import { Logger } from '../utils/logger.js';
|
|
25
|
-
import { toVec3Object, toRotObject, toVec3Array } from '../utils/normalize.js';
|
|
26
|
-
import { cleanObject } from '../utils/safe-json.js';
|
|
27
|
-
|
|
28
|
-
const log = new Logger('ToolHandler');
|
|
29
|
-
|
|
30
|
-
export async function handleToolCall(
|
|
31
|
-
name: string,
|
|
32
|
-
args: any,
|
|
33
|
-
tools: {
|
|
34
|
-
actorTools: ActorTools,
|
|
35
|
-
assetTools: AssetTools,
|
|
36
|
-
materialTools: MaterialTools,
|
|
37
|
-
editorTools: EditorTools,
|
|
38
|
-
animationTools: AnimationTools,
|
|
39
|
-
physicsTools: PhysicsTools,
|
|
40
|
-
niagaraTools: NiagaraTools,
|
|
41
|
-
blueprintTools: BlueprintTools,
|
|
42
|
-
levelTools: LevelTools,
|
|
43
|
-
lightingTools: LightingTools,
|
|
44
|
-
landscapeTools: LandscapeTools,
|
|
45
|
-
foliageTools: FoliageTools,
|
|
46
|
-
debugTools: DebugVisualizationTools,
|
|
47
|
-
performanceTools: PerformanceTools,
|
|
48
|
-
audioTools: AudioTools,
|
|
49
|
-
uiTools: UITools,
|
|
50
|
-
rcTools: RcTools,
|
|
51
|
-
sequenceTools: SequenceTools,
|
|
52
|
-
introspectionTools: IntrospectionTools,
|
|
53
|
-
visualTools: VisualTools,
|
|
54
|
-
engineTools: EngineTools,
|
|
55
|
-
bridge: UnrealBridge
|
|
56
|
-
}
|
|
57
|
-
) {
|
|
58
|
-
try {
|
|
59
|
-
let result: any;
|
|
60
|
-
let message: string;
|
|
61
|
-
|
|
62
|
-
switch (name) {
|
|
63
|
-
// Asset Tools
|
|
64
|
-
case 'list_assets':
|
|
65
|
-
// Initialize message variable
|
|
66
|
-
message = '';
|
|
67
|
-
|
|
68
|
-
// Validate directory argument
|
|
69
|
-
if (args.directory === null) {
|
|
70
|
-
result = { assets: [], error: 'Directory cannot be null' };
|
|
71
|
-
message = 'Failed to list assets: directory cannot be null';
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
if (args.directory === undefined) {
|
|
75
|
-
result = { assets: [], error: 'Directory cannot be undefined' };
|
|
76
|
-
message = 'Failed to list assets: directory cannot be undefined';
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
if (typeof args.directory !== 'string') {
|
|
80
|
-
result = { assets: [], error: `Invalid directory type: expected string, got ${typeof args.directory}` };
|
|
81
|
-
message = `Failed to list assets: directory must be a string path, got ${typeof args.directory}`;
|
|
82
|
-
break;
|
|
83
|
-
} else if (args.directory.trim() === '') {
|
|
84
|
-
result = { assets: [], error: 'Directory path cannot be empty' };
|
|
85
|
-
message = 'Failed to list assets: directory path cannot be empty';
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Normalize virtual content path: map /Content -> /Game (case-insensitive)
|
|
90
|
-
const rawDir: string = String(args.directory).trim();
|
|
91
|
-
let normDir = rawDir.replace(/^\/?content(\/|$)/i, '/Game$1');
|
|
92
|
-
// Ensure leading slash
|
|
93
|
-
if (!normDir.startsWith('/')) normDir = '/' + normDir;
|
|
94
|
-
// Collapse duplicate slashes
|
|
95
|
-
normDir = normDir.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
96
|
-
|
|
97
|
-
// Try multiple approaches to list assets
|
|
98
|
-
try {
|
|
99
|
-
console.log('[list_assets] Starting asset listing for directory:', normDir);
|
|
100
|
-
|
|
101
|
-
// First try: Use Python for most reliable listing (recursive if /Game)
|
|
102
|
-
const pythonCode = `
|
|
103
|
-
import unreal
|
|
104
|
-
import json
|
|
105
|
-
|
|
106
|
-
directory = '${normDir || '/Game'}'
|
|
107
|
-
# Use recursive for /Game to find assets in subdirectories, but limit depth
|
|
108
|
-
recursive = True if directory == '/Game' else False
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
|
|
112
|
-
|
|
113
|
-
# Create filter - ARFilter properties must be set in constructor
|
|
114
|
-
filter = unreal.ARFilter(
|
|
115
|
-
package_paths=[directory],
|
|
116
|
-
recursive_paths=recursive
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# Get all assets in the directory (limit to prevent timeout)
|
|
120
|
-
all_assets = asset_registry.get_assets(filter)
|
|
121
|
-
# Limit results to prevent timeout
|
|
122
|
-
assets = all_assets[:100] if len(all_assets) > 100 else all_assets
|
|
123
|
-
|
|
124
|
-
# Format asset information
|
|
125
|
-
asset_list = []
|
|
126
|
-
for asset in assets:
|
|
127
|
-
asset_info = {
|
|
128
|
-
"Name": str(asset.asset_name),
|
|
129
|
-
"Path": str(asset.package_path) + "/" + str(asset.asset_name),
|
|
130
|
-
"Class": str(asset.asset_class_path.asset_name if hasattr(asset.asset_class_path, "asset_name") else asset.asset_class),
|
|
131
|
-
"PackagePath": str(asset.package_path)
|
|
132
|
-
}
|
|
133
|
-
asset_list.append(asset_info)
|
|
134
|
-
print("Asset: " + asset_info["Path"])
|
|
135
|
-
|
|
136
|
-
result = {
|
|
137
|
-
"success": True,
|
|
138
|
-
"count": len(asset_list),
|
|
139
|
-
"assets": asset_list
|
|
140
|
-
}
|
|
141
|
-
print("RESULT:" + json.dumps(result))
|
|
142
|
-
except Exception as e:
|
|
143
|
-
# Fallback to EditorAssetLibrary if ARFilter fails
|
|
144
|
-
try:
|
|
145
|
-
import unreal
|
|
146
|
-
# For /Game, use recursive to find assets in subdirectories
|
|
147
|
-
recursive_fallback = True if directory == '/Game' else False
|
|
148
|
-
asset_paths = unreal.EditorAssetLibrary.list_assets(directory, recursive_fallback, False)
|
|
149
|
-
asset_list = []
|
|
150
|
-
for path in asset_paths:
|
|
151
|
-
asset_list.append({
|
|
152
|
-
"Name": path.split("/")[-1].split(".")[0],
|
|
153
|
-
"Path": path,
|
|
154
|
-
"PackagePath": "/".join(path.split("/")[:-1])
|
|
155
|
-
})
|
|
156
|
-
result = {
|
|
157
|
-
"success": True,
|
|
158
|
-
"count": len(asset_list),
|
|
159
|
-
"assets": asset_list,
|
|
160
|
-
"method": "EditorAssetLibrary"
|
|
161
|
-
}
|
|
162
|
-
print("RESULT:" + json.dumps(result))
|
|
163
|
-
except Exception as e2:
|
|
164
|
-
print("Error listing assets: " + str(e) + " | Fallback error: " + str(e2))
|
|
165
|
-
print("RESULT:" + json.dumps({"success": False, "error": str(e), "assets": []}))
|
|
166
|
-
`.trim();
|
|
167
|
-
|
|
168
|
-
console.log('[list_assets] Executing Python code...');
|
|
169
|
-
const pyResponse = await tools.bridge.executePython(pythonCode);
|
|
170
|
-
console.log('[list_assets] Python response type:', typeof pyResponse);
|
|
171
|
-
|
|
172
|
-
// Parse Python output
|
|
173
|
-
let outputStr = '';
|
|
174
|
-
if (typeof pyResponse === 'object' && pyResponse !== null) {
|
|
175
|
-
if (pyResponse.LogOutput && Array.isArray(pyResponse.LogOutput)) {
|
|
176
|
-
outputStr = pyResponse.LogOutput
|
|
177
|
-
.map((log: any) => log.Output || '')
|
|
178
|
-
.join('');
|
|
179
|
-
} else {
|
|
180
|
-
outputStr = JSON.stringify(pyResponse);
|
|
181
|
-
}
|
|
182
|
-
} else {
|
|
183
|
-
outputStr = String(pyResponse || '');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Extract result from Python output
|
|
187
|
-
console.log('[list_assets] Python output sample:', outputStr.substring(0, 200));
|
|
188
|
-
const resultMatch = outputStr.match(/RESULT:({.*})/);
|
|
189
|
-
if (resultMatch) {
|
|
190
|
-
console.log('[list_assets] Found RESULT in Python output');
|
|
191
|
-
try {
|
|
192
|
-
const listResult = JSON.parse(resultMatch[1]);
|
|
193
|
-
if (listResult.success && listResult.assets) {
|
|
194
|
-
result = { assets: listResult.assets };
|
|
195
|
-
// Success - skip fallback methods
|
|
196
|
-
}
|
|
197
|
-
} catch {
|
|
198
|
-
// Fall through to HTTP method
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Fallback: Use the search API if Python didn't succeed
|
|
203
|
-
if (!result || !result.assets) {
|
|
204
|
-
try {
|
|
205
|
-
const searchResult = await tools.bridge.httpCall('/remote/search/assets', 'PUT', {
|
|
206
|
-
Query: '', // Empty query to match all (wildcard doesn't work)
|
|
207
|
-
Filter: {
|
|
208
|
-
PackagePaths: [normDir || '/Game'],
|
|
209
|
-
// Recursively search so we actually find assets in subfolders
|
|
210
|
-
RecursivePaths: true,
|
|
211
|
-
ClassNames: [], // Empty to get all types
|
|
212
|
-
RecursiveClasses: true
|
|
213
|
-
},
|
|
214
|
-
Limit: 1000, // Increase limit
|
|
215
|
-
Start: 0
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
if (searchResult?.Assets && Array.isArray(searchResult.Assets)) {
|
|
219
|
-
result = { assets: searchResult.Assets };
|
|
220
|
-
}
|
|
221
|
-
} catch {
|
|
222
|
-
// Continue to next fallback
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Third try: Use console command to get asset registry (only if still no results)
|
|
227
|
-
if (!result || !result.assets || result.assets.length === 0) {
|
|
228
|
-
try {
|
|
229
|
-
await tools.bridge.httpCall('/remote/object/call', 'PUT', {
|
|
230
|
-
objectPath: '/Script/Engine.Default__KismetSystemLibrary',
|
|
231
|
-
functionName: 'ExecuteConsoleCommand',
|
|
232
|
-
parameters: {
|
|
233
|
-
WorldContextObject: null,
|
|
234
|
-
Command: `AssetRegistry.DumpAssets ${normDir || '/Game'}`,
|
|
235
|
-
SpecificPlayer: null
|
|
236
|
-
},
|
|
237
|
-
generateTransaction: false
|
|
238
|
-
});
|
|
239
|
-
} catch {
|
|
240
|
-
// Console command attempt failed, continue
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// If all else fails, set empty result
|
|
244
|
-
if (!result || !result.assets) {
|
|
245
|
-
result = { assets: [], note: 'Asset listing requires proper Remote Control configuration' };
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
} catch (err) {
|
|
250
|
-
result = { assets: [], error: String(err) };
|
|
251
|
-
message = `Failed to list assets: ${err}`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Include full asset list with details in the message
|
|
255
|
-
console.log('[list_assets] Formatting message - result has assets?', !!(result && result.assets), 'count:', result?.assets?.length);
|
|
256
|
-
if (result && result.assets && result.assets.length > 0) {
|
|
257
|
-
// If Python/HTTP gives only paths, generate Name from path
|
|
258
|
-
result.assets = result.assets.map((asset: any) => {
|
|
259
|
-
if (!asset.Name && asset.Path) {
|
|
260
|
-
const base = String(asset.Path).split('/').pop() || '';
|
|
261
|
-
const name = base.includes('.') ? base.split('.').pop() : base;
|
|
262
|
-
return { ...asset, Name: name };
|
|
263
|
-
}
|
|
264
|
-
return asset;
|
|
265
|
-
});
|
|
266
|
-
// Group assets by type for better organization
|
|
267
|
-
result.assets = result.assets.map((a: any) => {
|
|
268
|
-
if (a && !a.Path && a.AssetPath) {
|
|
269
|
-
return { ...a, Path: a.AssetPath, PackagePath: a.AssetPath.split('/').slice(0, -1).join('/') };
|
|
270
|
-
}
|
|
271
|
-
return a;
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
if (result && result.assets && result.assets.length > 0) {
|
|
275
|
-
// Group assets by type for better organization
|
|
276
|
-
const assetsByType: { [key: string]: any[] } = {};
|
|
277
|
-
|
|
278
|
-
result.assets.forEach((asset: any) => {
|
|
279
|
-
const type = asset.Class || asset.class || 'Unknown';
|
|
280
|
-
if (!assetsByType[type]) {
|
|
281
|
-
assetsByType[type] = [];
|
|
282
|
-
}
|
|
283
|
-
assetsByType[type].push(asset);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// Format output with proper structure
|
|
287
|
-
let assetDetails = `📁 Asset Directory: ${normDir || '/Game'}\n`;
|
|
288
|
-
assetDetails += `📊 Total Assets: ${result.assets.length}\n\n`;
|
|
289
|
-
|
|
290
|
-
// Sort types alphabetically
|
|
291
|
-
const sortedTypes = Object.keys(assetsByType).sort();
|
|
292
|
-
|
|
293
|
-
sortedTypes.forEach((type, typeIndex) => {
|
|
294
|
-
const assets = assetsByType[type];
|
|
295
|
-
assetDetails += `${typeIndex + 1}. ${type} (${assets.length} items)\n`;
|
|
296
|
-
|
|
297
|
-
assets.forEach((asset: any, index: number) => {
|
|
298
|
-
const name = asset.Name || asset.name || 'Unknown';
|
|
299
|
-
const path = asset.Path || asset.path || asset.PackagePath || '';
|
|
300
|
-
const packagePath = asset.PackagePath || '';
|
|
301
|
-
|
|
302
|
-
assetDetails += ` ${index + 1}. ${name}\n`;
|
|
303
|
-
if (path !== packagePath && packagePath) {
|
|
304
|
-
assetDetails += ` 📍 Path: ${path}\n`;
|
|
305
|
-
assetDetails += ` 📦 Package: ${packagePath}\n`;
|
|
306
|
-
} else {
|
|
307
|
-
assetDetails += ` 📍 ${path}\n`;
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
assetDetails += '\n';
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
message = assetDetails;
|
|
314
|
-
|
|
315
|
-
// Also keep the structured data in the result for programmatic access
|
|
316
|
-
} else {
|
|
317
|
-
message = `No assets found in ${normDir || '/Game'}`;
|
|
318
|
-
}
|
|
319
|
-
break;
|
|
320
|
-
|
|
321
|
-
case 'import_asset':
|
|
322
|
-
result = await tools.assetTools.importAsset(args.sourcePath, args.destinationPath);
|
|
323
|
-
// Check if import actually succeeded
|
|
324
|
-
if (result.error) {
|
|
325
|
-
message = result.error;
|
|
326
|
-
} else if (result.success && result.paths && result.paths.length > 0) {
|
|
327
|
-
message = result.message || `Successfully imported ${result.paths.length} asset(s) to ${args.destinationPath}`;
|
|
328
|
-
} else {
|
|
329
|
-
message = result.message || result.error || `Import did not report success for source ${args.sourcePath}`;
|
|
330
|
-
}
|
|
331
|
-
break;
|
|
332
|
-
|
|
333
|
-
// Actor Tools
|
|
334
|
-
case 'spawn_actor':
|
|
335
|
-
// Normalize transforms: accept object or array
|
|
336
|
-
if (args.location !== undefined && args.location !== null) {
|
|
337
|
-
const loc = toVec3Object(args.location);
|
|
338
|
-
if (!loc) throw new Error('Invalid location: expected {x,y,z} or [x,y,z]');
|
|
339
|
-
args.location = loc;
|
|
340
|
-
}
|
|
341
|
-
if (args.rotation !== undefined && args.rotation !== null) {
|
|
342
|
-
const rot = toRotObject(args.rotation);
|
|
343
|
-
if (!rot) throw new Error('Invalid rotation: expected {pitch,yaw,roll} or [pitch,yaw,roll]');
|
|
344
|
-
args.rotation = rot;
|
|
345
|
-
}
|
|
346
|
-
result = await tools.actorTools.spawn(args);
|
|
347
|
-
message = `Actor spawned: ${JSON.stringify(result)}`;
|
|
348
|
-
break;
|
|
349
|
-
|
|
350
|
-
case 'delete_actor':
|
|
351
|
-
// Use EditorActorSubsystem instead of deprecated EditorLevelLibrary
|
|
352
|
-
try {
|
|
353
|
-
const pythonCmd = `
|
|
354
|
-
import unreal
|
|
355
|
-
import json
|
|
356
|
-
|
|
357
|
-
result = {"success": False, "message": "", "deleted_count": 0, "deleted_actors": []}
|
|
358
|
-
|
|
359
|
-
try:
|
|
360
|
-
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
361
|
-
actors = actor_subsystem.get_all_level_actors()
|
|
362
|
-
deleted_actors = []
|
|
363
|
-
search_name = "${args.actorName}"
|
|
364
|
-
|
|
365
|
-
for actor in actors:
|
|
366
|
-
if actor:
|
|
367
|
-
# Check both actor name and label
|
|
368
|
-
actor_name = actor.get_name()
|
|
369
|
-
actor_label = actor.get_actor_label()
|
|
370
|
-
|
|
371
|
-
# Case-insensitive partial matching
|
|
372
|
-
if (search_name.lower() in actor_label.lower() or
|
|
373
|
-
actor_label.lower().startswith(search_name.lower() + "_") or
|
|
374
|
-
actor_label.lower() == search_name.lower() or
|
|
375
|
-
actor_name.lower() == search_name.lower()):
|
|
376
|
-
actor_subsystem.destroy_actor(actor)
|
|
377
|
-
deleted_actors.append(actor_label)
|
|
378
|
-
|
|
379
|
-
if deleted_actors:
|
|
380
|
-
result["success"] = True
|
|
381
|
-
result["deleted_count"] = len(deleted_actors)
|
|
382
|
-
result["deleted_actors"] = deleted_actors
|
|
383
|
-
result["message"] = f"Deleted {len(deleted_actors)} actor(s): {deleted_actors}"
|
|
384
|
-
else:
|
|
385
|
-
result["message"] = f"No actors found matching: {search_name}"
|
|
386
|
-
# List available actors for debugging
|
|
387
|
-
all_labels = [a.get_actor_label() for a in actors[:10] if a]
|
|
388
|
-
result["available_actors"] = all_labels
|
|
389
|
-
|
|
390
|
-
except Exception as e:
|
|
391
|
-
result["message"] = f"Error deleting actors: {e}"
|
|
392
|
-
|
|
393
|
-
print(f"RESULT:{json.dumps(result)}")
|
|
394
|
-
`.trim();
|
|
395
|
-
const response = await tools.bridge.executePython(pythonCmd);
|
|
396
|
-
|
|
397
|
-
// Extract output from Python response
|
|
398
|
-
let outputStr = '';
|
|
399
|
-
if (typeof response === 'object' && response !== null) {
|
|
400
|
-
// Check if it has LogOutput (standard Python execution response)
|
|
401
|
-
if (response.LogOutput && Array.isArray(response.LogOutput)) {
|
|
402
|
-
// Concatenate all log outputs
|
|
403
|
-
outputStr = response.LogOutput
|
|
404
|
-
.map((log: any) => log.Output || '')
|
|
405
|
-
.join('');
|
|
406
|
-
} else if ('result' in response) {
|
|
407
|
-
outputStr = String(response.result);
|
|
408
|
-
} else {
|
|
409
|
-
outputStr = JSON.stringify(response);
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
outputStr = String(response || '');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Parse the result
|
|
416
|
-
const resultMatch = outputStr.match(/RESULT:(\{.*\})/);
|
|
417
|
-
if (resultMatch) {
|
|
418
|
-
try {
|
|
419
|
-
const deleteResult = JSON.parse(resultMatch[1]);
|
|
420
|
-
if (!deleteResult.success) {
|
|
421
|
-
throw new Error(deleteResult.message);
|
|
422
|
-
}
|
|
423
|
-
result = deleteResult;
|
|
424
|
-
message = deleteResult.message;
|
|
425
|
-
} catch {
|
|
426
|
-
// Fallback to checking output
|
|
427
|
-
if (outputStr.includes('Deleted')) {
|
|
428
|
-
result = { success: true, message: outputStr };
|
|
429
|
-
message = `Actor deleted: ${args.actorName}`;
|
|
430
|
-
} else {
|
|
431
|
-
throw new Error(outputStr || 'Delete failed');
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
} else {
|
|
435
|
-
// Check for error patterns
|
|
436
|
-
if (outputStr.includes('No actors found') || outputStr.includes('Error')) {
|
|
437
|
-
throw new Error(outputStr || 'Delete failed - no actors found');
|
|
438
|
-
}
|
|
439
|
-
// Only report success if clear indication
|
|
440
|
-
if (outputStr.includes('Deleted')) {
|
|
441
|
-
result = { success: true, message: outputStr };
|
|
442
|
-
message = `Actor deleted: ${args.actorName}`;
|
|
443
|
-
} else {
|
|
444
|
-
throw new Error('No valid result from Python delete operation');
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
} catch (pyErr) {
|
|
448
|
-
// Fallback to console command
|
|
449
|
-
const consoleResult = await tools.bridge.executeConsoleCommand(`DestroyActor ${args.actorName}`);
|
|
450
|
-
|
|
451
|
-
// Check console command result
|
|
452
|
-
if (consoleResult && typeof consoleResult === 'object') {
|
|
453
|
-
// Console commands don't reliably report success/failure
|
|
454
|
-
// Return an error to avoid false positives
|
|
455
|
-
throw new Error(`Delete operation uncertain via console command for '${args.actorName}'. Python execution failed: ${pyErr}`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// If we're here, we can't guarantee the delete worked
|
|
459
|
-
result = {
|
|
460
|
-
success: false,
|
|
461
|
-
message: `Console command fallback attempted for '${args.actorName}', but result uncertain`,
|
|
462
|
-
fallback: true
|
|
463
|
-
};
|
|
464
|
-
message = result.message;
|
|
465
|
-
}
|
|
466
|
-
break;
|
|
467
|
-
|
|
468
|
-
// Material Tools
|
|
469
|
-
case 'create_material':
|
|
470
|
-
result = await tools.materialTools.createMaterial(args.name, args.path);
|
|
471
|
-
message = result.success ? `Material created: ${result.path}` : result.error;
|
|
472
|
-
break;
|
|
473
|
-
|
|
474
|
-
case 'apply_material_to_actor':
|
|
475
|
-
result = await tools.materialTools.applyMaterialToActor(
|
|
476
|
-
args.actorPath,
|
|
477
|
-
args.materialPath,
|
|
478
|
-
args.slotIndex !== undefined ? args.slotIndex : 0
|
|
479
|
-
);
|
|
480
|
-
message = result.success ? result.message : result.error;
|
|
481
|
-
break;
|
|
482
|
-
|
|
483
|
-
// Editor Tools
|
|
484
|
-
case 'play_in_editor':
|
|
485
|
-
result = await tools.editorTools.playInEditor();
|
|
486
|
-
message = result.message || 'PIE started';
|
|
487
|
-
break;
|
|
488
|
-
|
|
489
|
-
case 'stop_play_in_editor':
|
|
490
|
-
result = await tools.editorTools.stopPlayInEditor();
|
|
491
|
-
message = result.message || 'PIE stopped';
|
|
492
|
-
break;
|
|
493
|
-
|
|
494
|
-
case 'set_camera':
|
|
495
|
-
result = await tools.editorTools.setViewportCamera(args.location, args.rotation);
|
|
496
|
-
message = result.message || 'Camera set';
|
|
497
|
-
break;
|
|
498
|
-
|
|
499
|
-
// Animation Tools
|
|
500
|
-
case 'create_animation_blueprint':
|
|
501
|
-
result = await tools.animationTools.createAnimationBlueprint(args);
|
|
502
|
-
message = result.message || `Animation blueprint ${args.name} created`;
|
|
503
|
-
break;
|
|
504
|
-
|
|
505
|
-
case 'play_animation_montage':
|
|
506
|
-
result = await tools.animationTools.playAnimation({
|
|
507
|
-
actorName: args.actorName,
|
|
508
|
-
animationType: 'Montage',
|
|
509
|
-
animationPath: args.montagePath,
|
|
510
|
-
playRate: args.playRate
|
|
511
|
-
});
|
|
512
|
-
message = result.message || `Playing montage ${args.montagePath}`;
|
|
513
|
-
break;
|
|
514
|
-
|
|
515
|
-
// Physics Tools
|
|
516
|
-
case 'setup_ragdoll':
|
|
517
|
-
result = await tools.physicsTools.setupRagdoll(args);
|
|
518
|
-
message = result.message || 'Ragdoll physics configured';
|
|
519
|
-
break;
|
|
520
|
-
|
|
521
|
-
case 'apply_force':
|
|
522
|
-
// Normalize force vector
|
|
523
|
-
const forceVec = toVec3Array(args.force);
|
|
524
|
-
if (!forceVec) throw new Error('Invalid force: expected {x,y,z} or [x,y,z]');
|
|
525
|
-
// Map the simple force schema to PhysicsTools expected format
|
|
526
|
-
result = await tools.physicsTools.applyForce({
|
|
527
|
-
actorName: args.actorName,
|
|
528
|
-
forceType: 'Force', // Default to 'Force' type
|
|
529
|
-
vector: forceVec,
|
|
530
|
-
isLocal: false // World space by default
|
|
531
|
-
});
|
|
532
|
-
// Check if the result indicates an error
|
|
533
|
-
if (result.error || (result.success === false)) {
|
|
534
|
-
throw new Error(result.error || result.message || `Failed to apply force to ${args.actorName}`);
|
|
535
|
-
}
|
|
536
|
-
message = result.message || `Force applied to ${args.actorName}`;
|
|
537
|
-
break;
|
|
538
|
-
|
|
539
|
-
// Niagara Tools
|
|
540
|
-
case 'create_particle_effect':
|
|
541
|
-
result = await tools.niagaraTools.createEffect(args);
|
|
542
|
-
message = result.message || `${args.effectType} effect created`;
|
|
543
|
-
break;
|
|
544
|
-
|
|
545
|
-
case 'spawn_niagara_system':
|
|
546
|
-
result = await tools.niagaraTools.spawnEffect({
|
|
547
|
-
systemPath: args.systemPath,
|
|
548
|
-
location: args.location,
|
|
549
|
-
scale: args.scale ? [args.scale, args.scale, args.scale] : undefined
|
|
550
|
-
});
|
|
551
|
-
message = result.message || 'Niagara system spawned';
|
|
552
|
-
break;
|
|
553
|
-
|
|
554
|
-
// Blueprint Tools
|
|
555
|
-
case 'create_blueprint':
|
|
556
|
-
result = await tools.blueprintTools.createBlueprint(args);
|
|
557
|
-
message = result.message || `Blueprint ${args.name} created`;
|
|
558
|
-
break;
|
|
559
|
-
|
|
560
|
-
case 'add_blueprint_component':
|
|
561
|
-
result = await tools.blueprintTools.addComponent(args);
|
|
562
|
-
message = result.message || `Component ${args.componentName} added`;
|
|
563
|
-
break;
|
|
564
|
-
|
|
565
|
-
// Level Tools
|
|
566
|
-
case 'load_level':
|
|
567
|
-
result = await tools.levelTools.loadLevel(args);
|
|
568
|
-
message = result.message || `Level ${args.levelPath} loaded`;
|
|
569
|
-
break;
|
|
570
|
-
|
|
571
|
-
case 'save_level':
|
|
572
|
-
result = await tools.levelTools.saveLevel(args);
|
|
573
|
-
message = result.message || 'Level saved';
|
|
574
|
-
break;
|
|
575
|
-
|
|
576
|
-
case 'stream_level':
|
|
577
|
-
result = await tools.levelTools.streamLevel(args);
|
|
578
|
-
message = result.message || 'Level streaming updated';
|
|
579
|
-
break;
|
|
580
|
-
|
|
581
|
-
// Lighting Tools
|
|
582
|
-
case 'create_light':
|
|
583
|
-
// Normalize transforms
|
|
584
|
-
const lightLocObj = args.location ? (toVec3Object(args.location) || { x: 0, y: 0, z: 0 }) : { x: 0, y: 0, z: 0 };
|
|
585
|
-
const lightLoc = [lightLocObj.x, lightLocObj.y, lightLocObj.z] as [number, number, number];
|
|
586
|
-
const lightRotObj = args.rotation ? (toRotObject(args.rotation) || { pitch: 0, yaw: 0, roll: 0 }) : { pitch: 0, yaw: 0, roll: 0 };
|
|
587
|
-
const lightRot = [lightRotObj.pitch, lightRotObj.yaw, lightRotObj.roll] as [number, number, number];
|
|
588
|
-
|
|
589
|
-
switch (args.lightType?.toLowerCase()) {
|
|
590
|
-
case 'directional':
|
|
591
|
-
result = await tools.lightingTools.createDirectionalLight({
|
|
592
|
-
name: args.name,
|
|
593
|
-
intensity: args.intensity,
|
|
594
|
-
rotation: lightRot
|
|
595
|
-
});
|
|
596
|
-
break;
|
|
597
|
-
case 'point':
|
|
598
|
-
result = await tools.lightingTools.createPointLight({
|
|
599
|
-
name: args.name,
|
|
600
|
-
location: lightLoc,
|
|
601
|
-
intensity: args.intensity
|
|
602
|
-
});
|
|
603
|
-
break;
|
|
604
|
-
case 'spot':
|
|
605
|
-
result = await tools.lightingTools.createSpotLight({
|
|
606
|
-
name: args.name,
|
|
607
|
-
location: lightLoc,
|
|
608
|
-
rotation: lightRot,
|
|
609
|
-
intensity: args.intensity
|
|
610
|
-
});
|
|
611
|
-
break;
|
|
612
|
-
case 'rect':
|
|
613
|
-
result = await tools.lightingTools.createRectLight({
|
|
614
|
-
name: args.name,
|
|
615
|
-
location: lightLoc,
|
|
616
|
-
rotation: lightRot,
|
|
617
|
-
intensity: args.intensity
|
|
618
|
-
});
|
|
619
|
-
break;
|
|
620
|
-
case 'sky':
|
|
621
|
-
result = await tools.lightingTools.createSkyLight({
|
|
622
|
-
name: args.name,
|
|
623
|
-
intensity: args.intensity,
|
|
624
|
-
recapture: true
|
|
625
|
-
});
|
|
626
|
-
break;
|
|
627
|
-
default:
|
|
628
|
-
throw new Error(`Unknown light type: ${args.lightType}`);
|
|
629
|
-
}
|
|
630
|
-
message = result.message || `${args.lightType} light created`;
|
|
631
|
-
break;
|
|
632
|
-
|
|
633
|
-
case 'build_lighting':
|
|
634
|
-
result = await tools.lightingTools.buildLighting(args);
|
|
635
|
-
message = result.message || 'Lighting built';
|
|
636
|
-
break;
|
|
637
|
-
|
|
638
|
-
// Landscape Tools
|
|
639
|
-
case 'create_landscape':
|
|
640
|
-
result = await tools.landscapeTools.createLandscape(args);
|
|
641
|
-
// Never claim success unless the tool says so; prefer error/message from UE/Python
|
|
642
|
-
if (result && typeof result === 'object') {
|
|
643
|
-
message = result.message || result.error || (result.success ? `Landscape ${args.name} created` : `Failed to create landscape ${args.name}`);
|
|
644
|
-
} else {
|
|
645
|
-
message = `Failed to create landscape ${args.name}`;
|
|
646
|
-
}
|
|
647
|
-
break;
|
|
648
|
-
|
|
649
|
-
case 'sculpt_landscape':
|
|
650
|
-
result = await tools.landscapeTools.sculptLandscape(args);
|
|
651
|
-
message = result.message || 'Landscape sculpted';
|
|
652
|
-
break;
|
|
653
|
-
|
|
654
|
-
// Foliage Tools
|
|
655
|
-
case 'add_foliage_type':
|
|
656
|
-
result = await tools.foliageTools.addFoliageType(args);
|
|
657
|
-
message = result.message || `Foliage type ${args.name} added`;
|
|
658
|
-
break;
|
|
659
|
-
|
|
660
|
-
case 'paint_foliage':
|
|
661
|
-
result = await tools.foliageTools.paintFoliage(args);
|
|
662
|
-
message = result.message || 'Foliage painted';
|
|
663
|
-
break;
|
|
664
|
-
|
|
665
|
-
// Debug Visualization Tools
|
|
666
|
-
case 'draw_debug_shape':
|
|
667
|
-
// Convert position object to array if needed
|
|
668
|
-
const position = Array.isArray(args.position) ? args.position :
|
|
669
|
-
(args.position ? [args.position.x || 0, args.position.y || 0, args.position.z || 0] : [0, 0, 0]);
|
|
670
|
-
|
|
671
|
-
switch (args.shape?.toLowerCase()) {
|
|
672
|
-
case 'line':
|
|
673
|
-
result = await tools.debugTools.drawDebugLine({
|
|
674
|
-
start: position,
|
|
675
|
-
end: args.end || [position[0] + 100, position[1], position[2]],
|
|
676
|
-
color: args.color,
|
|
677
|
-
duration: args.duration
|
|
678
|
-
});
|
|
679
|
-
break;
|
|
680
|
-
case 'box':
|
|
681
|
-
result = await tools.debugTools.drawDebugBox({
|
|
682
|
-
center: position,
|
|
683
|
-
extent: [args.size, args.size, args.size],
|
|
684
|
-
color: args.color,
|
|
685
|
-
duration: args.duration
|
|
686
|
-
});
|
|
687
|
-
break;
|
|
688
|
-
case 'sphere':
|
|
689
|
-
result = await tools.debugTools.drawDebugSphere({
|
|
690
|
-
center: position,
|
|
691
|
-
radius: args.size || 50,
|
|
692
|
-
color: args.color,
|
|
693
|
-
duration: args.duration
|
|
694
|
-
});
|
|
695
|
-
break;
|
|
696
|
-
default:
|
|
697
|
-
throw new Error(`Unknown debug shape: ${args.shape}`);
|
|
698
|
-
}
|
|
699
|
-
message = `Debug ${args.shape} drawn`;
|
|
700
|
-
break;
|
|
701
|
-
|
|
702
|
-
case 'set_view_mode':
|
|
703
|
-
result = await tools.debugTools.setViewMode(args);
|
|
704
|
-
message = `View mode set to ${args.mode}`;
|
|
705
|
-
break;
|
|
706
|
-
|
|
707
|
-
// Performance Tools
|
|
708
|
-
case 'start_profiling':
|
|
709
|
-
result = await tools.performanceTools.startProfiling(args);
|
|
710
|
-
message = result.message || `${args.type} profiling started`;
|
|
711
|
-
break;
|
|
712
|
-
|
|
713
|
-
case 'show_fps':
|
|
714
|
-
result = await tools.performanceTools.showFPS(args);
|
|
715
|
-
message = `FPS display ${args.enabled ? 'enabled' : 'disabled'}`;
|
|
716
|
-
break;
|
|
717
|
-
|
|
718
|
-
case 'set_scalability':
|
|
719
|
-
result = await tools.performanceTools.setScalability(args);
|
|
720
|
-
message = `${args.category} quality set to level ${args.level}`;
|
|
721
|
-
break;
|
|
722
|
-
|
|
723
|
-
// Audio Tools
|
|
724
|
-
case 'play_sound':
|
|
725
|
-
// Check if sound exists first
|
|
726
|
-
const soundCheckPy = `
|
|
727
|
-
import unreal, json
|
|
728
|
-
path = r"${args.soundPath}"
|
|
729
|
-
try:
|
|
730
|
-
exists = unreal.EditorAssetLibrary.does_asset_exist(path)
|
|
731
|
-
print('SOUNDCHECK:' + json.dumps({'exists': bool(exists)}))
|
|
732
|
-
except Exception as e:
|
|
733
|
-
print('SOUNDCHECK:' + json.dumps({'exists': False, 'error': str(e)}))
|
|
734
|
-
`.trim();
|
|
735
|
-
|
|
736
|
-
let soundExists = false;
|
|
737
|
-
try {
|
|
738
|
-
const checkResp = await tools.bridge.executePython(soundCheckPy);
|
|
739
|
-
const checkOut = typeof checkResp === 'string' ? checkResp : JSON.stringify(checkResp);
|
|
740
|
-
const checkMatch = checkOut.match(/SOUNDCHECK:({.*})/);
|
|
741
|
-
if (checkMatch) {
|
|
742
|
-
const checkParsed = JSON.parse(checkMatch[1]);
|
|
743
|
-
soundExists = checkParsed.exists === true;
|
|
744
|
-
}
|
|
745
|
-
} catch {}
|
|
746
|
-
|
|
747
|
-
if (!soundExists && !args.soundPath.includes('/Engine/')) {
|
|
748
|
-
throw new Error(`Sound asset not found: ${args.soundPath}`);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (args.is3D !== false && args.location) {
|
|
752
|
-
result = await tools.audioTools.playSoundAtLocation({
|
|
753
|
-
soundPath: args.soundPath,
|
|
754
|
-
location: args.location,
|
|
755
|
-
volume: args.volume,
|
|
756
|
-
pitch: args.pitch
|
|
757
|
-
});
|
|
758
|
-
} else {
|
|
759
|
-
result = await tools.audioTools.playSound2D({
|
|
760
|
-
soundPath: args.soundPath,
|
|
761
|
-
volume: args.volume,
|
|
762
|
-
pitch: args.pitch
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
message = `Playing sound: ${args.soundPath}`;
|
|
766
|
-
break;
|
|
767
|
-
|
|
768
|
-
case 'create_ambient_sound':
|
|
769
|
-
result = await tools.audioTools.createAmbientSound(args);
|
|
770
|
-
message = result.message || `Ambient sound ${args.name} created`;
|
|
771
|
-
break;
|
|
772
|
-
|
|
773
|
-
// UI Tools
|
|
774
|
-
case 'create_widget':
|
|
775
|
-
result = await tools.uiTools.createWidget(args);
|
|
776
|
-
message = `Widget ${args.name} created`;
|
|
777
|
-
break;
|
|
778
|
-
|
|
779
|
-
case 'show_widget':
|
|
780
|
-
result = await tools.uiTools.setWidgetVisibility(args);
|
|
781
|
-
message = `Widget ${args.widgetName} ${args.visible ? 'shown' : 'hidden'}`;
|
|
782
|
-
break;
|
|
783
|
-
|
|
784
|
-
case 'create_hud':
|
|
785
|
-
result = await tools.uiTools.createHUD(args);
|
|
786
|
-
message = result.message || `HUD ${args.name} created`;
|
|
787
|
-
break;
|
|
788
|
-
|
|
789
|
-
// Console command execution
|
|
790
|
-
case 'console_command':
|
|
791
|
-
// Validate command parameter
|
|
792
|
-
if (!args.command || typeof args.command !== 'string') {
|
|
793
|
-
throw new Error('Invalid command: must be a non-empty string');
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
const command = args.command.trim();
|
|
797
|
-
if (command.length === 0) {
|
|
798
|
-
// Handle empty command gracefully
|
|
799
|
-
result = { success: true, message: 'Empty command ignored' };
|
|
800
|
-
message = 'Empty command ignored';
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Known problematic patterns that will generate warnings
|
|
805
|
-
const problematicPatterns = [
|
|
806
|
-
// /^stat fps$/i, // Removed - allow stat fps as user requested
|
|
807
|
-
/^invalid_/i,
|
|
808
|
-
/^this_is_not/i,
|
|
809
|
-
/^\d+$/, // Just numbers
|
|
810
|
-
/^[^a-zA-Z]/, // Doesn't start with letter
|
|
811
|
-
];
|
|
812
|
-
|
|
813
|
-
// Check for known invalid commands
|
|
814
|
-
const cmdLower = command.toLowerCase();
|
|
815
|
-
const knownInvalid = [
|
|
816
|
-
'invalid_command_xyz',
|
|
817
|
-
'this_is_not_a_valid_command',
|
|
818
|
-
'stat invalid_stat',
|
|
819
|
-
'viewmode invalid_mode',
|
|
820
|
-
'r.invalidcvar',
|
|
821
|
-
'sg.invalidquality'
|
|
822
|
-
];
|
|
823
|
-
|
|
824
|
-
const isKnownInvalid = knownInvalid.some(invalid =>
|
|
825
|
-
cmdLower === invalid.toLowerCase() || cmdLower.includes(invalid));
|
|
826
|
-
|
|
827
|
-
// Allow stat fps without replacement - user knows what they want
|
|
828
|
-
// if (cmdLower === 'stat fps') {
|
|
829
|
-
// command = 'stat unit';
|
|
830
|
-
// log.info('Replacing "stat fps" with "stat unit" to avoid warnings');
|
|
831
|
-
// }
|
|
832
|
-
|
|
833
|
-
// Handle commands with special characters that might fail
|
|
834
|
-
if (command.includes(';')) {
|
|
835
|
-
// Split compound commands
|
|
836
|
-
const commands = command.split(';').map((c: string) => c.trim()).filter((c: string) => c.length > 0);
|
|
837
|
-
if (commands.length > 1) {
|
|
838
|
-
// Execute each command separately
|
|
839
|
-
const results = [];
|
|
840
|
-
for (const cmd of commands) {
|
|
841
|
-
try {
|
|
842
|
-
await tools.bridge.executeConsoleCommand(cmd);
|
|
843
|
-
results.push({ command: cmd, success: true });
|
|
844
|
-
} catch (e: any) {
|
|
845
|
-
results.push({ command: cmd, success: false, error: e.message });
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
result = { multiCommand: true, results };
|
|
849
|
-
message = `Executed ${results.length} commands`;
|
|
850
|
-
break;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
try {
|
|
855
|
-
result = await tools.bridge.executeConsoleCommand(command);
|
|
856
|
-
|
|
857
|
-
if (isKnownInvalid) {
|
|
858
|
-
message = `Command executed (likely unrecognized): ${command}`;
|
|
859
|
-
result = { ...result, warning: 'Command may not be recognized by Unreal Engine' };
|
|
860
|
-
} else if (problematicPatterns.some(p => p.test(command))) {
|
|
861
|
-
message = `Command executed (may have warnings): ${command}`;
|
|
862
|
-
result = { ...result, info: 'Command may generate console warnings' };
|
|
863
|
-
} else {
|
|
864
|
-
message = `Console command executed: ${command}`;
|
|
865
|
-
}
|
|
866
|
-
} catch (error: any) {
|
|
867
|
-
// Don't throw for console commands - they often "succeed" even when unrecognized
|
|
868
|
-
log.warn(`Console command error for '${command}':`, error.message);
|
|
869
|
-
|
|
870
|
-
// Return a warning result instead of failing
|
|
871
|
-
result = {
|
|
872
|
-
success: false,
|
|
873
|
-
command: command,
|
|
874
|
-
error: error.message,
|
|
875
|
-
warning: 'Command may have failed or been unrecognized'
|
|
876
|
-
};
|
|
877
|
-
message = `Console command attempted: ${command} (may have failed)`;
|
|
878
|
-
}
|
|
879
|
-
break;
|
|
880
|
-
|
|
881
|
-
// New tools implemented here (also used by consolidated handler)
|
|
882
|
-
case 'rc_create_preset':
|
|
883
|
-
result = await tools.rcTools.createPreset({ name: args.name, path: args.path });
|
|
884
|
-
message = result.message || (result.success ? `Preset created at ${result.presetPath}` : result.error);
|
|
885
|
-
break;
|
|
886
|
-
case 'rc_expose_actor':
|
|
887
|
-
result = await tools.rcTools.exposeActor({ presetPath: args.presetPath, actorName: args.actorName });
|
|
888
|
-
message = result.message || (result.success ? 'Actor exposed' : result.error);
|
|
889
|
-
break;
|
|
890
|
-
case 'rc_expose_property':
|
|
891
|
-
result = await tools.rcTools.exposeProperty({ presetPath: args.presetPath, objectPath: args.objectPath, propertyName: args.propertyName });
|
|
892
|
-
message = result.message || (result.success ? 'Property exposed' : result.error);
|
|
893
|
-
break;
|
|
894
|
-
case 'rc_list_fields':
|
|
895
|
-
result = await tools.rcTools.listFields({ presetPath: args.presetPath });
|
|
896
|
-
message = result.message || (result.success ? `Found ${(result.fields||[]).length} fields` : result.error);
|
|
897
|
-
break;
|
|
898
|
-
case 'rc_set_property':
|
|
899
|
-
result = await tools.rcTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value });
|
|
900
|
-
message = result.message || (result.success ? 'Property set' : result.error);
|
|
901
|
-
break;
|
|
902
|
-
case 'rc_get_property':
|
|
903
|
-
result = await tools.rcTools.getProperty({ objectPath: args.objectPath, propertyName: args.propertyName });
|
|
904
|
-
message = result.message || (result.success ? 'Property retrieved' : result.error);
|
|
905
|
-
break;
|
|
906
|
-
|
|
907
|
-
case 'seq_create':
|
|
908
|
-
result = await tools.sequenceTools.create({ name: args.name, path: args.path });
|
|
909
|
-
message = result.message || (result.success ? `Sequence created at ${result.sequencePath}` : result.error);
|
|
910
|
-
break;
|
|
911
|
-
case 'seq_open':
|
|
912
|
-
result = await tools.sequenceTools.open({ path: args.path });
|
|
913
|
-
message = result.message || (result.success ? 'Sequence opened' : result.error);
|
|
914
|
-
break;
|
|
915
|
-
case 'seq_add_camera':
|
|
916
|
-
result = await tools.sequenceTools.addCamera({ spawnable: args.spawnable });
|
|
917
|
-
message = result.message || (result.success ? 'Camera added to sequence' : result.error);
|
|
918
|
-
break;
|
|
919
|
-
case 'seq_add_actor':
|
|
920
|
-
result = await tools.sequenceTools.addActor({ actorName: args.actorName });
|
|
921
|
-
message = result.message || (result.success ? 'Actor added to sequence' : result.error);
|
|
922
|
-
break;
|
|
923
|
-
|
|
924
|
-
case 'inspect_object':
|
|
925
|
-
result = await tools.introspectionTools.inspectObject({ objectPath: args.objectPath });
|
|
926
|
-
message = result.message || (result.success ? 'Object inspected' : result.error);
|
|
927
|
-
break;
|
|
928
|
-
case 'inspect_set_property':
|
|
929
|
-
result = await tools.introspectionTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value });
|
|
930
|
-
message = result.message || (result.success ? 'Property set' : result.error);
|
|
931
|
-
break;
|
|
932
|
-
|
|
933
|
-
case 'take_screenshot':
|
|
934
|
-
result = await tools.visualTools.takeScreenshot({ resolution: args.resolution });
|
|
935
|
-
message = result.message || (result.success ? 'Screenshot captured' : result.error);
|
|
936
|
-
break;
|
|
937
|
-
|
|
938
|
-
case 'launch_editor':
|
|
939
|
-
result = await tools.engineTools.launchEditor({ editorExe: args.editorExe, projectPath: args.projectPath });
|
|
940
|
-
message = result.message || (result.success ? 'Launch requested' : result.error);
|
|
941
|
-
break;
|
|
942
|
-
case 'quit_editor':
|
|
943
|
-
result = await tools.engineTools.quitEditor();
|
|
944
|
-
message = result.message || (result.success ? 'Quit requested' : result.error);
|
|
945
|
-
break;
|
|
946
|
-
|
|
947
|
-
default:
|
|
948
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Clean the result to prevent circular references
|
|
952
|
-
const cleanedResult = result && typeof result === 'object' ? cleanObject(result) : result;
|
|
953
|
-
|
|
954
|
-
// Return MCP-compliant response format
|
|
955
|
-
return {
|
|
956
|
-
content: [{
|
|
957
|
-
type: 'text',
|
|
958
|
-
text: message
|
|
959
|
-
}],
|
|
960
|
-
// Include result data as metadata for debugging
|
|
961
|
-
...(cleanedResult && typeof cleanedResult === 'object' ? cleanedResult : {})
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
} catch (err) {
|
|
965
|
-
return {
|
|
966
|
-
content: [{
|
|
967
|
-
type: 'text',
|
|
968
|
-
text: `Failed to execute ${name}: ${err}`
|
|
969
|
-
}],
|
|
970
|
-
isError: true
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
}
|