unreal-engine-mcp-server 0.2.1

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.
Files changed (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -0,0 +1,992 @@
1
+ import WebSocket from 'ws';
2
+ import { createHttpClient } from './utils/http.js';
3
+ import { Logger } from './utils/logger.js';
4
+ import { loadEnv } from './types/env.js';
5
+ export class UnrealBridge {
6
+ ws;
7
+ http = createHttpClient('');
8
+ env = loadEnv();
9
+ log = new Logger('UnrealBridge');
10
+ connected = false;
11
+ reconnectTimer;
12
+ reconnectAttempts = 0;
13
+ MAX_RECONNECT_ATTEMPTS = 5;
14
+ BASE_RECONNECT_DELAY = 1000;
15
+ // Command queue for throttling
16
+ commandQueue = [];
17
+ isProcessing = false;
18
+ MIN_COMMAND_DELAY = 100; // Increased to prevent console spam
19
+ MAX_COMMAND_DELAY = 500; // Maximum delay for heavy operations
20
+ STAT_COMMAND_DELAY = 300; // Special delay for stat commands to avoid warnings
21
+ lastCommandTime = 0;
22
+ lastStatCommandTime = 0; // Track stat commands separately
23
+ // Safe viewmodes that won't cause crashes (per docs and testing)
24
+ _SAFE_VIEWMODES = [
25
+ 'Lit', 'Unlit', 'Wireframe', 'DetailLighting',
26
+ 'LightingOnly', 'ReflectionOverride', 'ShaderComplexity'
27
+ ];
28
+ // Unsafe viewmodes that can cause crashes or instability via visualizeBuffer
29
+ UNSAFE_VIEWMODES = [
30
+ 'BaseColor', 'WorldNormal', 'Metallic', 'Specular',
31
+ 'Roughness', 'SubsurfaceColor', 'Opacity',
32
+ 'LightComplexity', 'LightmapDensity',
33
+ 'StationaryLightOverlap', 'CollisionPawn', 'CollisionVisibility'
34
+ ];
35
+ // Python script templates for EditorLevelLibrary access
36
+ PYTHON_TEMPLATES = {
37
+ GET_ALL_ACTORS: {
38
+ name: 'get_all_actors',
39
+ script: `
40
+ import unreal
41
+ # Use EditorActorSubsystem instead of deprecated EditorLevelLibrary
42
+ subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
43
+ actors = subsys.get_all_level_actors()
44
+ result = [{'name': a.get_name(), 'class': a.get_class().get_name(), 'path': a.get_path_name()} for a in actors]
45
+ print(f"RESULT:{result}")
46
+ `.trim()
47
+ },
48
+ SPAWN_ACTOR_AT_LOCATION: {
49
+ name: 'spawn_actor',
50
+ script: `
51
+ import unreal
52
+ location = unreal.Vector({x}, {y}, {z})
53
+ rotation = unreal.Rotator({pitch}, {yaw}, {roll})
54
+ actor_class = unreal.EditorAssetLibrary.load_asset("{class_path}")
55
+ if actor_class:
56
+ # Use EditorActorSubsystem instead of deprecated EditorLevelLibrary
57
+ subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
58
+ spawned = subsys.spawn_actor_from_object(actor_class, location, rotation)
59
+ print(f"RESULT:{{'success': True, 'actor': spawned.get_name()}}")
60
+ else:
61
+ print(f"RESULT:{{'success': False, 'error': 'Failed to load actor class'}}")
62
+ `.trim()
63
+ },
64
+ DELETE_ACTOR: {
65
+ name: 'delete_actor',
66
+ script: `
67
+ import unreal
68
+ import json
69
+ subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
70
+ actors = subsys.get_all_level_actors()
71
+ found = False
72
+ for actor in actors:
73
+ if not actor:
74
+ continue
75
+ label = actor.get_actor_label()
76
+ name = actor.get_name()
77
+ if label == "{actor_name}" or name == "{actor_name}" or label.lower().startswith("{actor_name}".lower()+"_"):
78
+ subsys.destroy_actor(actor)
79
+ print("RESULT:" + json.dumps({'success': True, 'deleted': label}))
80
+ found = True
81
+ break
82
+ if not found:
83
+ print("RESULT:" + json.dumps({'success': False, 'error': 'Actor not found'}))
84
+ `.trim()
85
+ },
86
+ CREATE_ASSET: {
87
+ name: 'create_asset',
88
+ script: `
89
+ import unreal
90
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
91
+ factory = unreal.{factory_class}()
92
+ asset = asset_tools.create_asset("{asset_name}", "{package_path}", {asset_class}, factory)
93
+ if asset:
94
+ unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
95
+ print(f"RESULT:{{'success': True, 'path': asset.get_path_name()}}")
96
+ else:
97
+ print(f"RESULT:{{'success': False, 'error': 'Failed to create asset'}}")
98
+ `.trim()
99
+ },
100
+ SET_VIEWPORT_CAMERA: {
101
+ name: 'set_viewport_camera',
102
+ script: `
103
+ import unreal
104
+ location = unreal.Vector({x}, {y}, {z})
105
+ rotation = unreal.Rotator({pitch}, {yaw}, {roll})
106
+ # Use UnrealEditorSubsystem for viewport operations (UE5.1+)
107
+ ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
108
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
109
+ if ues:
110
+ ues.set_level_viewport_camera_info(location, rotation)
111
+ try:
112
+ if les:
113
+ les.editor_invalidate_viewports()
114
+ except Exception:
115
+ pass
116
+ print(f"RESULT:{{'success': True, 'location': [{x}, {y}, {z}], 'rotation': [{pitch}, {yaw}, {roll}]}}")
117
+ else:
118
+ print(f"RESULT:{{'success': False, 'error': 'UnrealEditorSubsystem not available'}}")
119
+ `.trim()
120
+ },
121
+ BUILD_LIGHTING: {
122
+ name: 'build_lighting',
123
+ script: `
124
+ import unreal
125
+ try:
126
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
127
+ if les:
128
+ q = unreal.LightingBuildQuality.{quality}
129
+ les.build_light_maps(q, True)
130
+ print(f"RESULT:{{'success': True, 'quality': '{quality}', 'method': 'LevelEditorSubsystem'}}")
131
+ else:
132
+ print(f"RESULT:{{'success': False, 'error': 'LevelEditorSubsystem not available'}}")
133
+ except Exception as e:
134
+ print(f"RESULT:{{'success': False, 'error': str(e)}}")
135
+ `.trim()
136
+ },
137
+ SAVE_ALL_DIRTY_PACKAGES: {
138
+ name: 'save_dirty_packages',
139
+ script: `
140
+ import unreal
141
+ saved = unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
142
+ print(f"RESULT:{{'success': {saved}, 'message': 'All dirty packages saved'}}")
143
+ `.trim()
144
+ }
145
+ };
146
+ get isConnected() { return this.connected; }
147
+ /**
148
+ * Attempt to connect with retries
149
+ * @param maxAttempts Maximum number of connection attempts
150
+ * @param timeoutMs Timeout for each connection attempt in milliseconds
151
+ * @param retryDelayMs Delay between retry attempts in milliseconds
152
+ * @returns Promise that resolves when connected or rejects after all attempts fail
153
+ */
154
+ async tryConnect(maxAttempts = 3, timeoutMs = 5000, retryDelayMs = 2000) {
155
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
156
+ try {
157
+ this.log.info(`Connection attempt ${attempt}/${maxAttempts}`);
158
+ await this.connect(timeoutMs);
159
+ return true; // Successfully connected
160
+ }
161
+ catch (err) {
162
+ this.log.warn(`Connection attempt ${attempt} failed:`, err);
163
+ if (attempt < maxAttempts) {
164
+ this.log.info(`Retrying in ${retryDelayMs}ms...`);
165
+ await new Promise(resolve => setTimeout(resolve, retryDelayMs));
166
+ }
167
+ else {
168
+ this.log.error(`All ${maxAttempts} connection attempts failed`);
169
+ return false; // All attempts failed
170
+ }
171
+ }
172
+ }
173
+ return false;
174
+ }
175
+ async connect(timeoutMs = 5000) {
176
+ const wsUrl = `ws://${this.env.UE_HOST}:${this.env.UE_RC_WS_PORT}`;
177
+ const httpBase = `http://${this.env.UE_HOST}:${this.env.UE_RC_HTTP_PORT}`;
178
+ this.http = createHttpClient(httpBase);
179
+ this.log.info(`Connecting to UE Remote Control: ${wsUrl}`);
180
+ this.ws = new WebSocket(wsUrl);
181
+ await new Promise((resolve, reject) => {
182
+ if (!this.ws)
183
+ return reject(new Error('WS not created'));
184
+ // Setup timeout
185
+ const timeout = setTimeout(() => {
186
+ this.log.warn(`Connection timeout after ${timeoutMs}ms`);
187
+ if (this.ws) {
188
+ this.ws.removeAllListeners();
189
+ // Only close if the websocket is in CONNECTING state
190
+ if (this.ws.readyState === WebSocket.CONNECTING) {
191
+ try {
192
+ this.ws.terminate(); // Use terminate instead of close for immediate cleanup
193
+ }
194
+ catch (e) {
195
+ // Ignore close errors
196
+ }
197
+ }
198
+ this.ws = undefined;
199
+ }
200
+ reject(new Error('Connection timeout: Unreal Engine may not be running or Remote Control is not enabled'));
201
+ }, timeoutMs);
202
+ // Success handler
203
+ const onOpen = () => {
204
+ clearTimeout(timeout);
205
+ this.connected = true;
206
+ this.log.info('Connected to Unreal Remote Control');
207
+ this.startCommandProcessor(); // Start command processor on connect
208
+ resolve();
209
+ };
210
+ // Error handler
211
+ const onError = (err) => {
212
+ clearTimeout(timeout);
213
+ this.log.error('WebSocket error', err);
214
+ if (this.ws) {
215
+ this.ws.removeAllListeners();
216
+ try {
217
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
218
+ this.ws.terminate();
219
+ }
220
+ }
221
+ catch (e) {
222
+ // Ignore close errors
223
+ }
224
+ this.ws = undefined;
225
+ }
226
+ reject(new Error(`Failed to connect: ${err.message}`));
227
+ };
228
+ // Close handler (if closed before open)
229
+ const onClose = () => {
230
+ if (!this.connected) {
231
+ clearTimeout(timeout);
232
+ reject(new Error('Connection closed before establishing'));
233
+ }
234
+ else {
235
+ // Normal close after connection was established
236
+ this.connected = false;
237
+ this.log.warn('WebSocket closed');
238
+ this.scheduleReconnect();
239
+ }
240
+ };
241
+ // Message handler (currently best-effort logging)
242
+ const onMessage = (raw) => {
243
+ try {
244
+ const msg = JSON.parse(String(raw));
245
+ this.log.debug('WS message', msg);
246
+ }
247
+ catch (e) {
248
+ this.log.error('Failed parsing WS message', e);
249
+ }
250
+ };
251
+ // Attach listeners
252
+ this.ws.once('open', onOpen);
253
+ this.ws.once('error', onError);
254
+ this.ws.on('close', onClose);
255
+ this.ws.on('message', onMessage);
256
+ });
257
+ }
258
+ async httpCall(path, method = 'POST', body) {
259
+ const url = path.startsWith('/') ? path : `/${path}`;
260
+ const started = Date.now();
261
+ // Fix Content-Length header issue - ensure body is properly handled
262
+ if (body === undefined || body === null) {
263
+ body = method === 'GET' ? undefined : {};
264
+ }
265
+ // Add timeout wrapper to prevent hanging
266
+ const CALL_TIMEOUT = 10000; // 10 seconds timeout
267
+ // CRITICAL: Intercept and block dangerous console commands at HTTP level
268
+ if (url === '/remote/object/call' && body?.functionName === 'ExecuteConsoleCommand') {
269
+ const command = body?.parameters?.Command;
270
+ if (command && typeof command === 'string') {
271
+ const cmdLower = command.trim().toLowerCase();
272
+ // List of commands that cause crashes
273
+ const crashCommands = [
274
+ 'buildpaths', // Causes access violation 0x0000000000000060
275
+ 'rebuildnavigation', // Can crash without nav system
276
+ 'buildhierarchicallod', // Can crash without proper setup
277
+ 'buildlandscapeinfo', // Can crash without landscape
278
+ 'rebuildselectednavigation' // Nav-related crash
279
+ ];
280
+ // Check if this is a crash-inducing command
281
+ if (crashCommands.some(dangerous => cmdLower === dangerous || cmdLower.startsWith(dangerous + ' '))) {
282
+ this.log.warn(`BLOCKED dangerous command that causes crashes: ${command}`);
283
+ // Return a safe error response instead of executing
284
+ return {
285
+ success: false,
286
+ error: `Command '${command}' blocked: This command can cause Unreal Engine to crash. Use the Python API alternatives instead.`
287
+ };
288
+ }
289
+ // Also block other dangerous commands
290
+ const dangerousPatterns = [
291
+ 'quit', 'exit', 'r.gpucrash', 'debug crash',
292
+ 'viewmode visualizebuffer' // These can crash in certain states
293
+ ];
294
+ if (dangerousPatterns.some(pattern => cmdLower.includes(pattern))) {
295
+ this.log.warn(`BLOCKED potentially dangerous command: ${command}`);
296
+ return {
297
+ success: false,
298
+ error: `Command '${command}' blocked for safety.`
299
+ };
300
+ }
301
+ }
302
+ }
303
+ // Retry logic with exponential backoff and timeout
304
+ let lastError;
305
+ for (let attempt = 0; attempt < 3; attempt++) {
306
+ try {
307
+ // For GET requests, send payload as query parameters (not in body)
308
+ const config = { url, method, timeout: CALL_TIMEOUT };
309
+ if (method === 'GET' && body && typeof body === 'object') {
310
+ config.params = body;
311
+ }
312
+ else if (body !== undefined) {
313
+ config.data = body;
314
+ }
315
+ // Wrap with timeout promise to ensure we don't hang
316
+ const requestPromise = this.http.request(config);
317
+ const timeoutPromise = new Promise((_, reject) => {
318
+ setTimeout(() => {
319
+ reject(new Error(`Request timeout after ${CALL_TIMEOUT}ms`));
320
+ }, CALL_TIMEOUT);
321
+ });
322
+ const resp = await Promise.race([requestPromise, timeoutPromise]);
323
+ const ms = Date.now() - started;
324
+ this.log.debug(`[HTTP ${method}] ${url} -> ${ms}ms`);
325
+ return resp.data;
326
+ }
327
+ catch (error) {
328
+ lastError = error;
329
+ const delay = Math.min(1000 * Math.pow(2, attempt), 5000); // Exponential backoff with 5s max
330
+ // Log timeout errors specifically
331
+ if (error.message?.includes('timeout')) {
332
+ this.log.warn(`HTTP request timed out (attempt ${attempt + 1}/3): ${url}`);
333
+ }
334
+ if (attempt < 2) {
335
+ this.log.warn(`HTTP request failed (attempt ${attempt + 1}/3), retrying in ${delay}ms...`);
336
+ await new Promise(resolve => setTimeout(resolve, delay));
337
+ // If connection error, try to reconnect
338
+ if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.message?.includes('timeout')) {
339
+ this.scheduleReconnect();
340
+ }
341
+ }
342
+ }
343
+ }
344
+ throw lastError;
345
+ }
346
+ // Generic function call via Remote Control HTTP API
347
+ async call(body) {
348
+ // Using HTTP endpoint /remote/object/call
349
+ const result = await this.httpCall('/remote/object/call', 'PUT', {
350
+ generateTransaction: false,
351
+ ...body
352
+ });
353
+ return result;
354
+ }
355
+ async getExposed() {
356
+ return this.httpCall('/remote/preset', 'GET');
357
+ }
358
+ // Execute a console command safely with validation and throttling
359
+ async executeConsoleCommand(command) {
360
+ // Validate command is not empty
361
+ if (!command || typeof command !== 'string') {
362
+ throw new Error('Invalid command: must be a non-empty string');
363
+ }
364
+ const cmdTrimmed = command.trim();
365
+ if (cmdTrimmed.length === 0) {
366
+ // Return success for empty commands to match UE behavior
367
+ return { success: true, message: 'Empty command ignored' };
368
+ }
369
+ // Check for dangerous commands
370
+ const dangerousCommands = [
371
+ 'quit', 'exit', 'delete', 'destroy', 'kill', 'crash',
372
+ 'viewmode visualizebuffer basecolor',
373
+ 'viewmode visualizebuffer worldnormal',
374
+ 'r.gpucrash',
375
+ 'buildpaths', // Can cause access violation if nav system not initialized
376
+ 'rebuildnavigation' // Can also crash without proper nav setup
377
+ ];
378
+ const cmdLower = cmdTrimmed.toLowerCase();
379
+ if (dangerousCommands.some(dangerous => cmdLower.includes(dangerous))) {
380
+ throw new Error(`Dangerous command blocked: ${command}`);
381
+ }
382
+ // Determine priority based on command type
383
+ let priority = 7; // Default priority
384
+ if (command.includes('BuildLighting') || command.includes('BuildPaths')) {
385
+ priority = 1; // Heavy operation
386
+ }
387
+ else if (command.includes('summon') || command.includes('spawn')) {
388
+ priority = 5; // Medium operation
389
+ }
390
+ else if (command.startsWith('stat') || command.startsWith('show')) {
391
+ priority = 9; // Light operation
392
+ }
393
+ // Known invalid command patterns
394
+ const invalidPatterns = [
395
+ /^\d+$/, // Just numbers
396
+ /^invalid_command/i,
397
+ /^this_is_not_a_valid/i,
398
+ ];
399
+ const isLikelyInvalid = invalidPatterns.some(pattern => pattern.test(cmdTrimmed));
400
+ if (isLikelyInvalid) {
401
+ this.log.warn(`Command appears invalid: ${cmdTrimmed}`);
402
+ }
403
+ try {
404
+ const result = await this.executeThrottledCommand(() => this.httpCall('/remote/object/call', 'PUT', {
405
+ objectPath: '/Script/Engine.Default__KismetSystemLibrary',
406
+ functionName: 'ExecuteConsoleCommand',
407
+ parameters: {
408
+ WorldContextObject: null,
409
+ Command: cmdTrimmed,
410
+ SpecificPlayer: null
411
+ },
412
+ generateTransaction: false
413
+ }), priority);
414
+ return result;
415
+ }
416
+ catch (error) {
417
+ this.log.error(`Console command failed: ${cmdTrimmed}`, error);
418
+ throw error;
419
+ }
420
+ }
421
+ // Try to execute a Python command via the PythonScriptPlugin, fallback to `py` console command.
422
+ async executePython(command) {
423
+ const isMultiLine = /[\r\n]/.test(command) || command.includes(';');
424
+ try {
425
+ // Use ExecutePythonCommandEx with appropriate mode based on content
426
+ return await this.httpCall('/remote/object/call', 'PUT', {
427
+ objectPath: '/Script/PythonScriptPlugin.Default__PythonScriptLibrary',
428
+ functionName: 'ExecutePythonCommandEx',
429
+ parameters: {
430
+ PythonCommand: command,
431
+ ExecutionMode: isMultiLine ? 'ExecuteFile' : 'ExecuteStatement',
432
+ FileExecutionScope: 'Private'
433
+ },
434
+ generateTransaction: false
435
+ });
436
+ }
437
+ catch {
438
+ try {
439
+ // Fallback to ExecutePythonCommand (more tolerant for multi-line)
440
+ return await this.httpCall('/remote/object/call', 'PUT', {
441
+ objectPath: '/Script/PythonScriptPlugin.Default__PythonScriptLibrary',
442
+ functionName: 'ExecutePythonCommand',
443
+ parameters: {
444
+ Command: command
445
+ },
446
+ generateTransaction: false
447
+ });
448
+ }
449
+ catch {
450
+ // Final fallback: execute via console py command
451
+ this.log.warn('PythonScriptLibrary not available or failed, falling back to console `py` command');
452
+ // For simple single-line commands
453
+ if (!isMultiLine) {
454
+ return await this.executeConsoleCommand(`py ${command}`);
455
+ }
456
+ // For multi-line scripts, try to execute as a block
457
+ try {
458
+ // Try executing as a single exec block
459
+ // Properly escape the script for Python exec
460
+ const escapedScript = command
461
+ .replace(/\\/g, '\\\\')
462
+ .replace(/"/g, '\\"')
463
+ .replace(/\n/g, '\\n')
464
+ .replace(/\r/g, '');
465
+ return await this.executeConsoleCommand(`py exec("${escapedScript}")`);
466
+ }
467
+ catch {
468
+ // If that fails, break into smaller chunks
469
+ try {
470
+ // First ensure unreal is imported
471
+ await this.executeConsoleCommand('py import unreal');
472
+ // For complex multi-line scripts, execute in logical chunks
473
+ const commandWithoutImport = command.replace(/^\s*import\s+unreal\s*;?\s*/m, '');
474
+ // Split by semicolons first, then by newlines
475
+ const statements = commandWithoutImport
476
+ .split(/[;\n]/)
477
+ .map(s => s.trim())
478
+ .filter(s => s.length > 0 && !s.startsWith('#'));
479
+ let result = null;
480
+ for (const stmt of statements) {
481
+ // Skip if statement is too long for console
482
+ if (stmt.length > 200) {
483
+ // Try to execute as a single exec block
484
+ const miniScript = `exec("""${stmt.replace(/"/g, '\\"')}""")`;
485
+ result = await this.executeConsoleCommand(`py ${miniScript}`);
486
+ }
487
+ else {
488
+ result = await this.executeConsoleCommand(`py ${stmt}`);
489
+ }
490
+ // Small delay between commands
491
+ await new Promise(resolve => setTimeout(resolve, 30));
492
+ }
493
+ return result;
494
+ }
495
+ catch {
496
+ // Final fallback: execute line by line
497
+ const lines = command.split('\n').filter(line => line.trim().length > 0);
498
+ let result = null;
499
+ for (const line of lines) {
500
+ // Skip comments
501
+ if (line.trim().startsWith('#')) {
502
+ continue;
503
+ }
504
+ result = await this.executeConsoleCommand(`py ${line.trim()}`);
505
+ // Small delay between commands to ensure execution order
506
+ await new Promise(resolve => setTimeout(resolve, 50));
507
+ }
508
+ return result;
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ // Connection recovery
515
+ scheduleReconnect() {
516
+ if (this.reconnectTimer || this.connected) {
517
+ return;
518
+ }
519
+ if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
520
+ this.log.error('Max reconnection attempts reached. Please check Unreal Engine.');
521
+ return;
522
+ }
523
+ // Exponential backoff with jitter
524
+ const delay = Math.min(this.BASE_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000, 30000 // Max 30 seconds
525
+ );
526
+ this.log.info(`Scheduling reconnection attempt ${this.reconnectAttempts + 1}/${this.MAX_RECONNECT_ATTEMPTS} in ${Math.round(delay)}ms`);
527
+ this.reconnectTimer = setTimeout(async () => {
528
+ this.reconnectTimer = undefined;
529
+ this.reconnectAttempts++;
530
+ try {
531
+ await this.connect();
532
+ this.reconnectAttempts = 0;
533
+ this.log.info('Successfully reconnected to Unreal Engine');
534
+ }
535
+ catch (err) {
536
+ this.log.error('Reconnection attempt failed:', err);
537
+ this.scheduleReconnect();
538
+ }
539
+ }, delay);
540
+ }
541
+ // Graceful shutdown
542
+ async disconnect() {
543
+ if (this.reconnectTimer) {
544
+ clearTimeout(this.reconnectTimer);
545
+ this.reconnectTimer = undefined;
546
+ }
547
+ if (this.ws) {
548
+ this.ws.close();
549
+ this.ws = undefined;
550
+ }
551
+ this.connected = false;
552
+ }
553
+ /**
554
+ * Enhanced Editor Function Access
555
+ * Use Python scripting as a bridge to access modern Editor Subsystem functions
556
+ */
557
+ async executeEditorFunction(functionName, params) {
558
+ const template = this.PYTHON_TEMPLATES[functionName];
559
+ if (!template) {
560
+ throw new Error(`Unknown editor function: ${functionName}`);
561
+ }
562
+ let script = template.script;
563
+ // Replace parameters in the script
564
+ if (params) {
565
+ for (const [key, value] of Object.entries(params)) {
566
+ const placeholder = `{${key}}`;
567
+ script = script.replace(new RegExp(placeholder, 'g'), String(value));
568
+ }
569
+ }
570
+ try {
571
+ // Execute Python script with result parsing
572
+ const result = await this.executePythonWithResult(script);
573
+ return result;
574
+ }
575
+ catch (error) {
576
+ this.log.error(`Failed to execute editor function ${functionName}:`, error);
577
+ // Fallback to console command if Python fails
578
+ return this.executeFallbackCommand(functionName, params);
579
+ }
580
+ }
581
+ /**
582
+ * Execute Python script and parse the result
583
+ */
584
+ async executePythonWithResult(script) {
585
+ try {
586
+ // Wrap script to capture output so we can parse RESULT: lines reliably
587
+ const wrappedScript = `
588
+ import sys
589
+ import io
590
+ old_stdout = sys.stdout
591
+ sys.stdout = buffer = io.StringIO()
592
+ try:
593
+ ${script.split('\n').join('\n ')}
594
+ finally:
595
+ output = buffer.getvalue()
596
+ sys.stdout = old_stdout
597
+ if output:
598
+ print(output)
599
+ `.trim()
600
+ .replace(/\r?\n/g, '\n');
601
+ const response = await this.executePython(wrappedScript);
602
+ // Extract textual output from various response shapes
603
+ let out = '';
604
+ try {
605
+ if (response && typeof response === 'string') {
606
+ out = response;
607
+ }
608
+ else if (response && typeof response === 'object') {
609
+ // Common RC Python response contains LogOutput array entries with .Output strings
610
+ if (Array.isArray(response.LogOutput)) {
611
+ out = response.LogOutput.map((l) => l.Output || '').join('');
612
+ }
613
+ else if (typeof response.Output === 'string') {
614
+ out = response.Output;
615
+ }
616
+ else if (typeof response.result === 'string') {
617
+ out = response.result;
618
+ }
619
+ else {
620
+ // Fallback to stringifying object (may still include RESULT in nested fields)
621
+ out = JSON.stringify(response);
622
+ }
623
+ }
624
+ }
625
+ catch {
626
+ out = String(response || '');
627
+ }
628
+ // Find the last RESULT: JSON block in the output for robustness
629
+ const matches = Array.from(out.matchAll(/RESULT:({[\s\S]*?})/g));
630
+ if (matches.length > 0) {
631
+ const last = matches[matches.length - 1][1];
632
+ try {
633
+ // Accept single quotes and True/False from Python repr if present
634
+ const normalized = last
635
+ .replace(/'/g, '"')
636
+ .replace(/\bTrue\b/g, 'true')
637
+ .replace(/\bFalse\b/g, 'false');
638
+ return JSON.parse(normalized);
639
+ }
640
+ catch {
641
+ return { raw: last };
642
+ }
643
+ }
644
+ // If no RESULT: marker, return the best-effort textual output or original response
645
+ return typeof response !== 'undefined' ? response : out;
646
+ }
647
+ catch {
648
+ this.log.warn('Python execution failed, trying direct execution');
649
+ return this.executePython(script);
650
+ }
651
+ }
652
+ /**
653
+ * Get the Unreal Engine version via Python and parse major/minor/patch.
654
+ */
655
+ async getEngineVersion() {
656
+ try {
657
+ const script = `
658
+ import unreal, json, re
659
+ ver = str(unreal.SystemLibrary.get_engine_version())
660
+ m = re.match(r'^(\\d+)\\.(\\d+)\\.(\\d+)', ver)
661
+ major = int(m.group(1)) if m else 0
662
+ minor = int(m.group(2)) if m else 0
663
+ patch = int(m.group(3)) if m else 0
664
+ print('RESULT:' + json.dumps({'version': ver, 'major': major, 'minor': minor, 'patch': patch}))
665
+ `.trim();
666
+ const result = await this.executePythonWithResult(script);
667
+ const version = String(result?.version ?? 'unknown');
668
+ const major = Number(result?.major ?? 0) || 0;
669
+ const minor = Number(result?.minor ?? 0) || 0;
670
+ const patch = Number(result?.patch ?? 0) || 0;
671
+ const isUE56OrAbove = major > 5 || (major === 5 && minor >= 6);
672
+ return { version, major, minor, patch, isUE56OrAbove };
673
+ }
674
+ catch (error) {
675
+ this.log.warn('Failed to get engine version via Python', error);
676
+ return { version: 'unknown', major: 0, minor: 0, patch: 0, isUE56OrAbove: false };
677
+ }
678
+ }
679
+ /**
680
+ * Query feature flags (Python availability, editor subsystems) via Python.
681
+ */
682
+ async getFeatureFlags() {
683
+ try {
684
+ const script = `
685
+ import unreal, json
686
+ flags = {}
687
+ # Python plugin availability (class exists)
688
+ try:
689
+ _ = unreal.PythonScriptLibrary
690
+ flags['pythonEnabled'] = True
691
+ except Exception:
692
+ flags['pythonEnabled'] = False
693
+ # Editor subsystems
694
+ try:
695
+ flags['unrealEditor'] = bool(unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem))
696
+ except Exception:
697
+ flags['unrealEditor'] = False
698
+ try:
699
+ flags['levelEditor'] = bool(unreal.get_editor_subsystem(unreal.LevelEditorSubsystem))
700
+ except Exception:
701
+ flags['levelEditor'] = False
702
+ try:
703
+ flags['editorActor'] = bool(unreal.get_editor_subsystem(unreal.EditorActorSubsystem))
704
+ except Exception:
705
+ flags['editorActor'] = False
706
+ print('RESULT:' + json.dumps(flags))
707
+ `.trim();
708
+ const res = await this.executePythonWithResult(script);
709
+ return {
710
+ pythonEnabled: Boolean(res?.pythonEnabled),
711
+ subsystems: {
712
+ unrealEditor: Boolean(res?.unrealEditor),
713
+ levelEditor: Boolean(res?.levelEditor),
714
+ editorActor: Boolean(res?.editorActor)
715
+ }
716
+ };
717
+ }
718
+ catch (e) {
719
+ this.log.warn('Failed to get feature flags via Python', e);
720
+ return { pythonEnabled: false, subsystems: { unrealEditor: false, levelEditor: false, editorActor: false } };
721
+ }
722
+ }
723
+ /**
724
+ * Fallback commands when Python is not available
725
+ */
726
+ async executeFallbackCommand(functionName, params) {
727
+ switch (functionName) {
728
+ case 'SPAWN_ACTOR_AT_LOCATION':
729
+ return this.executeConsoleCommand(`summon ${params?.class_path || 'StaticMeshActor'} ${params?.x || 0} ${params?.y || 0} ${params?.z || 0}`);
730
+ case 'DELETE_ACTOR':
731
+ // Use Python-based deletion to avoid unsafe console command and improve reliability
732
+ return this.executePythonWithResult(this.PYTHON_TEMPLATES.DELETE_ACTOR.script.replace('{actor_name}', String(params?.actor_name || '')));
733
+ case 'BUILD_LIGHTING':
734
+ return this.executeConsoleCommand('BuildLighting');
735
+ default:
736
+ throw new Error(`No fallback available for ${functionName}`);
737
+ }
738
+ }
739
+ /**
740
+ * SOLUTION 2: Safe ViewMode Switching
741
+ * Prevent crashes by validating and safely switching viewmodes
742
+ */
743
+ async setSafeViewMode(mode) {
744
+ const normalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1).toLowerCase();
745
+ // Check if the viewmode is known to be unsafe
746
+ if (this.UNSAFE_VIEWMODES.includes(normalizedMode)) {
747
+ this.log.warn(`Viewmode '${normalizedMode}' is known to cause crashes. Using safe alternative.`);
748
+ // For visualizeBuffer modes, we need special handling
749
+ if (normalizedMode === 'BaseColor' || normalizedMode === 'WorldNormal') {
750
+ // First ensure we're in a safe state
751
+ await this.executeConsoleCommand('viewmode Lit');
752
+ await this.delay(100);
753
+ // Try to use a safer alternative or skip
754
+ this.log.info(`Skipping unsafe visualizeBuffer mode: ${normalizedMode}`);
755
+ return {
756
+ success: false,
757
+ message: `Viewmode ${normalizedMode} skipped for safety`,
758
+ alternative: 'Lit'
759
+ };
760
+ }
761
+ // For other unsafe modes, switch to safe alternatives
762
+ const safeAlternative = this.getSafeAlternative(normalizedMode);
763
+ return this.executeConsoleCommand(`viewmode ${safeAlternative}`);
764
+ }
765
+ // Safe mode - execute with delay
766
+ return this.executeThrottledCommand(() => this.httpCall('/remote/object/call', 'PUT', {
767
+ objectPath: '/Script/Engine.Default__KismetSystemLibrary',
768
+ functionName: 'ExecuteConsoleCommand',
769
+ parameters: {
770
+ WorldContextObject: null,
771
+ Command: `viewmode ${normalizedMode}`,
772
+ SpecificPlayer: null
773
+ },
774
+ generateTransaction: false
775
+ }));
776
+ }
777
+ /**
778
+ * Get safe alternative for unsafe viewmodes
779
+ */
780
+ getSafeAlternative(unsafeMode) {
781
+ const alternatives = {
782
+ 'BaseColor': 'Unlit',
783
+ 'WorldNormal': 'Lit',
784
+ 'Metallic': 'Lit',
785
+ 'Specular': 'Lit',
786
+ 'Roughness': 'Lit',
787
+ 'LightComplexity': 'LightingOnly',
788
+ 'ShaderComplexity': 'Wireframe',
789
+ 'CollisionPawn': 'Wireframe',
790
+ 'CollisionVisibility': 'Wireframe'
791
+ };
792
+ return alternatives[unsafeMode] || 'Lit';
793
+ }
794
+ /**
795
+ * SOLUTION 3: Command Throttling and Queueing
796
+ * Prevent rapid command execution that can overwhelm the engine
797
+ */
798
+ async executeThrottledCommand(command, priority = 5) {
799
+ return new Promise((resolve, reject) => {
800
+ this.commandQueue.push({
801
+ command,
802
+ resolve,
803
+ reject,
804
+ priority
805
+ });
806
+ // Sort by priority (lower number = higher priority)
807
+ this.commandQueue.sort((a, b) => a.priority - b.priority);
808
+ // Process queue if not already processing
809
+ if (!this.isProcessing) {
810
+ this.processCommandQueue();
811
+ }
812
+ });
813
+ }
814
+ /**
815
+ * Process command queue with appropriate delays
816
+ */
817
+ async processCommandQueue() {
818
+ if (this.isProcessing || this.commandQueue.length === 0) {
819
+ return;
820
+ }
821
+ this.isProcessing = true;
822
+ while (this.commandQueue.length > 0) {
823
+ const item = this.commandQueue.shift();
824
+ if (!item)
825
+ continue; // Skip if undefined
826
+ // Calculate delay based on time since last command
827
+ const timeSinceLastCommand = Date.now() - this.lastCommandTime;
828
+ const requiredDelay = this.calculateDelay(item.priority);
829
+ if (timeSinceLastCommand < requiredDelay) {
830
+ await this.delay(requiredDelay - timeSinceLastCommand);
831
+ }
832
+ try {
833
+ const result = await item.command();
834
+ item.resolve(result);
835
+ }
836
+ catch (error) {
837
+ // Retry logic for transient failures
838
+ if (item.retryCount === undefined) {
839
+ item.retryCount = 0;
840
+ }
841
+ if (item.retryCount < 3) {
842
+ item.retryCount++;
843
+ this.log.warn(`Command failed, retrying (${item.retryCount}/3)`);
844
+ // Re-add to queue with increased priority
845
+ this.commandQueue.unshift({
846
+ command: item.command,
847
+ resolve: item.resolve,
848
+ reject: item.reject,
849
+ priority: Math.max(1, item.priority - 1),
850
+ retryCount: item.retryCount
851
+ });
852
+ // Add extra delay before retry
853
+ await this.delay(500);
854
+ }
855
+ else {
856
+ item.reject(error);
857
+ }
858
+ }
859
+ this.lastCommandTime = Date.now();
860
+ }
861
+ this.isProcessing = false;
862
+ }
863
+ /**
864
+ * Calculate appropriate delay based on command priority and type
865
+ */
866
+ calculateDelay(priority) {
867
+ // Priority 1-3: Heavy operations (asset creation, lighting build)
868
+ if (priority <= 3) {
869
+ return this.MAX_COMMAND_DELAY;
870
+ }
871
+ // Priority 4-6: Medium operations (actor spawning, material changes)
872
+ else if (priority <= 6) {
873
+ return 200;
874
+ }
875
+ // Priority 8: Stat commands - need special handling
876
+ else if (priority === 8) {
877
+ // Check time since last stat command to avoid FindConsoleObject warnings
878
+ const timeSinceLastStat = Date.now() - this.lastStatCommandTime;
879
+ if (timeSinceLastStat < this.STAT_COMMAND_DELAY) {
880
+ return this.STAT_COMMAND_DELAY;
881
+ }
882
+ this.lastStatCommandTime = Date.now();
883
+ return 150;
884
+ }
885
+ // Priority 7,9-10: Light operations (console commands, queries)
886
+ else {
887
+ return this.MIN_COMMAND_DELAY;
888
+ }
889
+ }
890
+ /**
891
+ * SOLUTION 4: Enhanced Asset Creation
892
+ * Use Python scripting for complex asset creation that requires editor scripting
893
+ */
894
+ async createComplexAsset(assetType, params) {
895
+ const assetCreators = {
896
+ 'Material': 'MaterialFactoryNew',
897
+ 'MaterialInstance': 'MaterialInstanceConstantFactoryNew',
898
+ 'Blueprint': 'BlueprintFactory',
899
+ 'AnimationBlueprint': 'AnimBlueprintFactory',
900
+ 'ControlRig': 'ControlRigBlueprintFactory',
901
+ 'NiagaraSystem': 'NiagaraSystemFactoryNew',
902
+ 'NiagaraEmitter': 'NiagaraEmitterFactoryNew',
903
+ 'LandscapeGrassType': 'LandscapeGrassTypeFactory',
904
+ 'PhysicsAsset': 'PhysicsAssetFactory'
905
+ };
906
+ const factoryClass = assetCreators[assetType];
907
+ if (!factoryClass) {
908
+ throw new Error(`Unknown asset type: ${assetType}`);
909
+ }
910
+ const createParams = {
911
+ factory_class: factoryClass,
912
+ asset_class: `unreal.${assetType}`,
913
+ asset_name: params.name || `New${assetType}`,
914
+ package_path: params.path || '/Game/CreatedAssets',
915
+ ...params
916
+ };
917
+ return this.executeEditorFunction('CREATE_ASSET', createParams);
918
+ }
919
+ /**
920
+ * Start the command processor
921
+ */
922
+ startCommandProcessor() {
923
+ // Periodic queue processing to handle any stuck commands
924
+ setInterval(() => {
925
+ if (!this.isProcessing && this.commandQueue.length > 0) {
926
+ this.processCommandQueue();
927
+ }
928
+ }, 1000);
929
+ }
930
+ /**
931
+ * Helper delay function
932
+ */
933
+ delay(ms) {
934
+ return new Promise(resolve => setTimeout(resolve, ms));
935
+ }
936
+ /**
937
+ * Batch command execution with proper delays
938
+ */
939
+ async executeBatch(commands) {
940
+ const results = [];
941
+ for (const cmd of commands) {
942
+ const result = await this.executeConsoleCommand(cmd.command);
943
+ results.push(result);
944
+ }
945
+ return results;
946
+ }
947
+ /**
948
+ * Get safe console commands for common operations
949
+ */
950
+ getSafeCommands() {
951
+ return {
952
+ // Health check (safe, no side effects)
953
+ 'HealthCheck': 'echo MCP Server Health Check',
954
+ // Performance monitoring (safe)
955
+ 'ShowFPS': 'stat unit', // Use 'stat unit' instead of 'stat fps'
956
+ 'ShowMemory': 'stat memory',
957
+ 'ShowGame': 'stat game',
958
+ 'ShowRendering': 'stat scenerendering',
959
+ 'ClearStats': 'stat none',
960
+ // Safe viewmodes
961
+ 'ViewLit': 'viewmode lit',
962
+ 'ViewUnlit': 'viewmode unlit',
963
+ 'ViewWireframe': 'viewmode wireframe',
964
+ 'ViewDetailLighting': 'viewmode detaillighting',
965
+ 'ViewLightingOnly': 'viewmode lightingonly',
966
+ // Safe show flags
967
+ 'ShowBounds': 'show bounds',
968
+ 'ShowCollision': 'show collision',
969
+ 'ShowNavigation': 'show navigation',
970
+ 'ShowFog': 'show fog',
971
+ 'ShowGrid': 'show grid',
972
+ // PIE controls
973
+ 'PlayInEditor': 'play',
974
+ 'StopPlay': 'stop',
975
+ 'PausePlay': 'pause',
976
+ // Time control
977
+ 'SlowMotion': 'slomo 0.5',
978
+ 'NormalSpeed': 'slomo 1',
979
+ 'FastForward': 'slomo 2',
980
+ // Camera controls
981
+ 'CameraSpeed1': 'camspeed 1',
982
+ 'CameraSpeed4': 'camspeed 4',
983
+ 'CameraSpeed8': 'camspeed 8',
984
+ // Rendering quality (safe)
985
+ 'LowQuality': 'sg.ViewDistanceQuality 0',
986
+ 'MediumQuality': 'sg.ViewDistanceQuality 1',
987
+ 'HighQuality': 'sg.ViewDistanceQuality 2',
988
+ 'EpicQuality': 'sg.ViewDistanceQuality 3'
989
+ };
990
+ }
991
+ }
992
+ //# sourceMappingURL=unreal-bridge.js.map