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,480 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+
3
+ export class ActorTools {
4
+ constructor(private bridge: UnrealBridge) {}
5
+
6
+ async spawn(params: { classPath: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number } }) {
7
+ // Validate classPath
8
+ if (!params.classPath || typeof params.classPath !== 'string') {
9
+ throw new Error(`Invalid classPath: ${params.classPath}`);
10
+ }
11
+
12
+ // Auto-map common shape names to proper asset paths
13
+ const shapeMapping: { [key: string]: string } = {
14
+ 'cube': '/Engine/BasicShapes/Cube',
15
+ 'sphere': '/Engine/BasicShapes/Sphere',
16
+ 'cylinder': '/Engine/BasicShapes/Cylinder',
17
+ 'cone': '/Engine/BasicShapes/Cone',
18
+ 'plane': '/Engine/BasicShapes/Plane',
19
+ 'torus': '/Engine/BasicShapes/Torus',
20
+ 'box': '/Engine/BasicShapes/Cube', // Common alias
21
+ 'ball': '/Engine/BasicShapes/Sphere', // Common alias
22
+ };
23
+
24
+ // Check if classPath is just a simple shape name (case-insensitive)
25
+ const lowerPath = params.classPath.toLowerCase();
26
+ if (shapeMapping[lowerPath]) {
27
+ params.classPath = shapeMapping[lowerPath];
28
+ }
29
+
30
+ // Auto-detect and handle asset paths (like /Engine/BasicShapes/Cube)
31
+ // The Python code will automatically spawn a StaticMeshActor and assign the mesh
32
+ // So we don't reject asset paths anymore - let Python handle them intelligently
33
+
34
+ // Only reject obviously invalid patterns
35
+ if (params.classPath === 'InvalidActorClass' ||
36
+ params.classPath === 'NoSlash' ||
37
+ params.classPath.startsWith('/Invalid/') ||
38
+ params.classPath.startsWith('/NotExist/')) {
39
+ throw new Error(`Invalid actor class: ${params.classPath}`);
40
+ }
41
+
42
+ // Try Python API first for better control and naming
43
+ try {
44
+ return await this.spawnViaPython(params);
45
+ } catch (pythonErr: any) {
46
+ // Check if this is a known failure that shouldn't fall back
47
+ const errorStr = String(pythonErr).toLowerCase();
48
+ if (errorStr.includes('abstract') || errorStr.includes('class not found')) {
49
+ // Don't try console fallback for abstract or non-existent classes
50
+ throw pythonErr;
51
+ }
52
+
53
+ // Check if the error is because of PIE mode
54
+ if (String(pythonErr).includes('Play In Editor mode')) {
55
+ // Don't fall back to console if we're in PIE mode
56
+ throw pythonErr;
57
+ }
58
+
59
+ // Fallback to console if Python fails for other reasons
60
+ // Only log if not a known/expected error
61
+ if (!String(pythonErr).includes('No valid result from Python')) {
62
+ console.error('Python spawn failed, falling back to console:', pythonErr);
63
+ }
64
+ return this.spawnViaConsole(params);
65
+ }
66
+ }
67
+
68
+ async spawnViaPython(params: { classPath: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number } }) {
69
+ try {
70
+ // Normalize and validate location
71
+ const loc = params.location ?? { x: 0, y: 0, z: 100 };
72
+ if (loc === null) {
73
+ throw new Error('Invalid location: null is not allowed');
74
+ }
75
+ if (typeof loc !== 'object' ||
76
+ typeof loc.x !== 'number' ||
77
+ typeof loc.y !== 'number' ||
78
+ typeof loc.z !== 'number') {
79
+ throw new Error('Invalid location: must have numeric x, y, z properties');
80
+ }
81
+
82
+ // Normalize and validate rotation
83
+ const rot = params.rotation ?? { pitch: 0, yaw: 0, roll: 0 };
84
+ if (rot === null) {
85
+ throw new Error('Invalid rotation: null is not allowed');
86
+ }
87
+ if (typeof rot !== 'object' ||
88
+ typeof rot.pitch !== 'number' ||
89
+ typeof rot.yaw !== 'number' ||
90
+ typeof rot.roll !== 'number') {
91
+ throw new Error('Invalid rotation: must have numeric pitch, yaw, roll properties');
92
+ }
93
+
94
+ // Resolve the class path
95
+ const fullClassPath = this.resolveActorClass(params.classPath);
96
+ let className = params.classPath;
97
+
98
+ // Extract simple class name for naming the actor
99
+ if (fullClassPath.includes('.')) {
100
+ className = fullClassPath.split('.').pop() || params.classPath;
101
+ }
102
+
103
+ const pythonCmd = `
104
+ import unreal
105
+ import json
106
+
107
+ result = {"success": False, "message": "", "actor_name": ""}
108
+
109
+ # Check if editor is in play mode first
110
+ try:
111
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
112
+ if les and les.is_in_play_in_editor():
113
+ result["message"] = "Cannot spawn actors while in Play In Editor mode. Please stop PIE first."
114
+ print(f"RESULT:{json.dumps(result)}")
115
+ # Exit early from this script
116
+ raise SystemExit(0)
117
+ except SystemExit:
118
+ # Re-raise the SystemExit to exit properly
119
+ raise
120
+ except:
121
+ pass # Continue if we can't check PIE state
122
+
123
+ # List of abstract classes that cannot be spawned
124
+ abstract_classes = ['PlaneReflectionCapture', 'ReflectionCapture', 'Actor', 'Pawn', 'Character']
125
+
126
+ # Check for abstract classes
127
+ if "${params.classPath}" in abstract_classes:
128
+ result["message"] = f"Cannot spawn ${params.classPath}: class is abstract and cannot be instantiated"
129
+ print(f"RESULT:{json.dumps(result)}")
130
+ else:
131
+ try:
132
+ # Get the world using the modern subsystem API
133
+ editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
134
+ world = editor_subsystem.get_editor_world() if hasattr(editor_subsystem, 'get_editor_world') else None
135
+ if not world:
136
+ # Try LevelEditorSubsystem as fallback
137
+ level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
138
+ if hasattr(level_subsystem, 'get_editor_world'):
139
+ world = level_subsystem.get_editor_world()
140
+
141
+ # Handle content paths (assets) vs class names
142
+ class_path = "${params.classPath}"
143
+ location = unreal.Vector(${loc.x}, ${loc.y}, ${loc.z})
144
+ rotation = unreal.Rotator(${rot.pitch}, ${rot.yaw}, ${rot.roll})
145
+ actor = None
146
+
147
+ # Check if this is a content path (starts with /Game or /Engine)
148
+ if class_path.startswith('/Game') or class_path.startswith('/Engine'):
149
+ # This is a content asset path - try to load and spawn it
150
+ try:
151
+ # For blueprint classes or static meshes
152
+ asset = unreal.EditorAssetLibrary.load_asset(class_path)
153
+ if asset:
154
+ # If it's a blueprint class
155
+ if isinstance(asset, unreal.Blueprint):
156
+ actor_class = asset.generated_class()
157
+ if actor_class:
158
+ # Use modern EditorActorSubsystem API
159
+ actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
160
+ actor = actor_subsys.spawn_actor_from_class(
161
+ actor_class,
162
+ location,
163
+ rotation
164
+ )
165
+ # If it's a static mesh, spawn a StaticMeshActor and assign the mesh
166
+ elif isinstance(asset, unreal.StaticMesh):
167
+ # Use modern EditorActorSubsystem API
168
+ actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
169
+ actor = actor_subsys.spawn_actor_from_class(
170
+ unreal.StaticMeshActor,
171
+ location,
172
+ rotation
173
+ )
174
+ if actor:
175
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
176
+ if mesh_component:
177
+ mesh_component.set_static_mesh(asset)
178
+ # Make it movable so physics can be applied
179
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
180
+ except Exception as load_err:
181
+ # If asset loading fails, try basic shapes from Engine content
182
+ shape_map = {
183
+ 'cube': '/Engine/BasicShapes/Cube',
184
+ 'sphere': '/Engine/BasicShapes/Sphere',
185
+ 'cylinder': '/Engine/BasicShapes/Cylinder',
186
+ 'cone': '/Engine/BasicShapes/Cone',
187
+ 'plane': '/Engine/BasicShapes/Plane',
188
+ 'torus': '/Engine/BasicShapes/Torus'
189
+ }
190
+
191
+ # Check if it's a basic shape name or path
192
+ mesh_path = None
193
+ lower_path = class_path.lower()
194
+ for shape_name, shape_path in shape_map.items():
195
+ if shape_name in lower_path:
196
+ mesh_path = shape_path
197
+ break
198
+
199
+ if mesh_path:
200
+ # Use Engine's basic shape
201
+ shape_mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
202
+ if shape_mesh:
203
+ # Use modern EditorActorSubsystem API
204
+ actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
205
+ actor = actor_subsys.spawn_actor_from_class(
206
+ unreal.StaticMeshActor,
207
+ location,
208
+ rotation
209
+ )
210
+ if actor:
211
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
212
+ if mesh_component:
213
+ mesh_component.set_static_mesh(shape_mesh)
214
+ # Make it movable so physics can be applied
215
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
216
+
217
+ # If not a content path or content spawn failed, try as a class name
218
+ if not actor:
219
+ if class_path == "StaticMeshActor":
220
+ # Use modern EditorActorSubsystem API
221
+ actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
222
+ actor = actor_subsys.spawn_actor_from_class(
223
+ unreal.StaticMeshActor,
224
+ location,
225
+ rotation
226
+ )
227
+
228
+ if actor:
229
+ # Assign a default mesh (cube)
230
+ cube_mesh = unreal.EditorAssetLibrary.load_asset('/Engine/BasicShapes/Cube')
231
+ if cube_mesh:
232
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
233
+ if mesh_component:
234
+ mesh_component.set_static_mesh(cube_mesh)
235
+ # Make it movable so physics can be applied
236
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
237
+
238
+ elif class_path == "CameraActor":
239
+ # Use modern EditorActorSubsystem API
240
+ actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
241
+ actor = actor_subsys.spawn_actor_from_class(
242
+ unreal.CameraActor,
243
+ location,
244
+ rotation
245
+ )
246
+ else:
247
+ # Try to get the class by name for other actors (e.g., PointLight)
248
+ actor_class = None
249
+ if hasattr(unreal, class_path):
250
+ actor_class = getattr(unreal, class_path)
251
+
252
+ if actor_class:
253
+ # Use modern EditorActorSubsystem API
254
+ actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
255
+ actor = actor_subsys.spawn_actor_from_class(
256
+ actor_class,
257
+ location,
258
+ rotation
259
+ )
260
+
261
+ # Set the actor label and return result
262
+ if actor:
263
+ import time
264
+ timestamp = int(time.time() * 1000) % 10000
265
+ base_name = class_path.split('/')[-1] if '/' in class_path else class_path
266
+ actor_name = f"{base_name}_{timestamp}"
267
+ actor.set_actor_label(actor_name)
268
+ result["success"] = True
269
+ result["message"] = f"Spawned {actor_name} at ({location.x}, {location.y}, {location.z})"
270
+ result["actor_name"] = actor_name
271
+ else:
272
+ result["message"] = f"Failed to spawn actor from: {class_path}. Try using /Engine/BasicShapes/Cube or StaticMeshActor"
273
+
274
+ except Exception as e:
275
+ result["message"] = f"Error spawning actor: {e}"
276
+
277
+ print(f"RESULT:{json.dumps(result)}")
278
+ `.trim();
279
+
280
+ const response = await this.bridge.executePython(pythonCmd);
281
+
282
+ // Extract output from Python response
283
+ let outputStr = '';
284
+ if (typeof response === 'object' && response !== null) {
285
+ // Check if it has LogOutput (standard Python execution response)
286
+ if (response.LogOutput && Array.isArray(response.LogOutput)) {
287
+ // Concatenate all log outputs
288
+ outputStr = response.LogOutput
289
+ .map((log: any) => log.Output || '')
290
+ .join('');
291
+ } else if ('result' in response) {
292
+ outputStr = String(response.result);
293
+ } else if ('ReturnValue' in response && typeof response.ReturnValue === 'string') {
294
+ outputStr = response.ReturnValue;
295
+ } else {
296
+ outputStr = JSON.stringify(response);
297
+ }
298
+ } else {
299
+ outputStr = String(response || '');
300
+ }
301
+
302
+ // Parse the result from Python output
303
+ const resultMatch = outputStr.match(/RESULT:({.*})/);
304
+ if (resultMatch) {
305
+ try {
306
+ const result = JSON.parse(resultMatch[1]);
307
+ if (!result.success) {
308
+ throw new Error(result.message || 'Spawn failed');
309
+ }
310
+ return result;
311
+ } catch {
312
+ // If we can't parse, check for common success patterns
313
+ if (outputStr.includes('Spawned')) {
314
+ return { success: true, message: outputStr };
315
+ }
316
+ throw new Error(`Failed to parse Python result: ${outputStr}`);
317
+ }
318
+ } else {
319
+ // Check output for success/failure patterns
320
+ if (outputStr.includes('Failed') || outputStr.includes('Error') || outputStr.includes('not found')) {
321
+ throw new Error(outputStr || 'Spawn failed');
322
+ }
323
+ // Default fallback - but this shouldn't report success for failed operations
324
+ // Only report success if Python execution was successful and no error markers
325
+ if (response?.ReturnValue === true && !outputStr.includes('abstract')) {
326
+ return { success: true, message: `Actor spawned: ${className} at ${loc.x},${loc.y},${loc.z}` };
327
+ } else {
328
+ throw new Error(`Failed to spawn ${className}: No valid result from Python`);
329
+ }
330
+ }
331
+ } catch (err) {
332
+ throw new Error(`Failed to spawn actor via Python: ${err}`);
333
+ }
334
+ }
335
+
336
+ async spawnViaConsole(params: { classPath: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number } }) {
337
+ try {
338
+ // Check if editor is in play mode first
339
+ try {
340
+ const pieCheckPython = `
341
+ import unreal
342
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
343
+ if les and les.is_in_play_in_editor():
344
+ print("PIE_ACTIVE")
345
+ else:
346
+ print("PIE_INACTIVE")
347
+ `.trim();
348
+
349
+ const pieCheckResult = await this.bridge.executePython(pieCheckPython);
350
+ const outputStr = typeof pieCheckResult === 'string' ? pieCheckResult : JSON.stringify(pieCheckResult);
351
+
352
+ if (outputStr.includes('PIE_ACTIVE')) {
353
+ throw new Error('Cannot spawn actors while in Play In Editor mode. Please stop PIE first.');
354
+ }
355
+ } catch (pieErr: any) {
356
+ // If the error is about PIE, throw it
357
+ if (String(pieErr).includes('Play In Editor')) {
358
+ throw pieErr;
359
+ }
360
+ // Otherwise ignore and continue
361
+ }
362
+
363
+ // List of known abstract classes that cannot be spawned
364
+ const abstractClasses = ['PlaneReflectionCapture', 'ReflectionCapture', 'Actor'];
365
+
366
+ // Check if this is an abstract class
367
+ if (abstractClasses.includes(params.classPath)) {
368
+ throw new Error(`Cannot spawn ${params.classPath}: class is abstract and cannot be instantiated`);
369
+ }
370
+
371
+ // Get the console-friendly class name
372
+ const spawnClass = this.getConsoleClassName(params.classPath);
373
+
374
+ // Use summon command with location if provided
375
+ const loc = params.location || { x: 0, y: 0, z: 100 };
376
+ const command = `summon ${spawnClass} ${loc.x} ${loc.y} ${loc.z}`;
377
+
378
+ await this.bridge.httpCall('/remote/object/call', 'PUT', {
379
+ objectPath: '/Script/Engine.Default__KismetSystemLibrary',
380
+ functionName: 'ExecuteConsoleCommand',
381
+ parameters: {
382
+ WorldContextObject: null,
383
+ Command: command,
384
+ SpecificPlayer: null
385
+ },
386
+ generateTransaction: false
387
+ });
388
+
389
+ // Console commands don't reliably report success/failure
390
+ // We can't guarantee this actually worked, so indicate uncertainty
391
+ return {
392
+ success: true,
393
+ message: `Actor spawn attempted via console: ${spawnClass} at ${loc.x},${loc.y},${loc.z}`,
394
+ note: 'Console spawn result uncertain - verify in editor'
395
+ };
396
+ } catch (err) {
397
+ throw new Error(`Failed to spawn actor: ${err}`);
398
+ }
399
+ }
400
+
401
+ private resolveActorClass(classPath: string): string {
402
+ // Map common names to full Unreal class paths
403
+ const classMap: { [key: string]: string } = {
404
+ 'PointLight': '/Script/Engine.PointLight',
405
+ 'DirectionalLight': '/Script/Engine.DirectionalLight',
406
+ 'SpotLight': '/Script/Engine.SpotLight',
407
+ 'RectLight': '/Script/Engine.RectLight',
408
+ 'SkyLight': '/Script/Engine.SkyLight',
409
+ 'StaticMeshActor': '/Script/Engine.StaticMeshActor',
410
+ 'PlayerStart': '/Script/Engine.PlayerStart',
411
+ 'Camera': '/Script/Engine.CameraActor',
412
+ 'CameraActor': '/Script/Engine.CameraActor',
413
+ 'Pawn': '/Script/Engine.DefaultPawn',
414
+ 'Character': '/Script/Engine.Character',
415
+ 'TriggerBox': '/Script/Engine.TriggerBox',
416
+ 'TriggerSphere': '/Script/Engine.TriggerSphere',
417
+ 'BlockingVolume': '/Script/Engine.BlockingVolume',
418
+ 'PostProcessVolume': '/Script/Engine.PostProcessVolume',
419
+ 'LightmassImportanceVolume': '/Script/Engine.LightmassImportanceVolume',
420
+ 'NavMeshBoundsVolume': '/Script/Engine.NavMeshBoundsVolume',
421
+ 'ExponentialHeightFog': '/Script/Engine.ExponentialHeightFog',
422
+ 'AtmosphericFog': '/Script/Engine.AtmosphericFog',
423
+ 'SphereReflectionCapture': '/Script/Engine.SphereReflectionCapture',
424
+ 'BoxReflectionCapture': '/Script/Engine.BoxReflectionCapture',
425
+ // PlaneReflectionCapture is abstract and cannot be spawned
426
+ 'DecalActor': '/Script/Engine.DecalActor'
427
+ };
428
+
429
+ // Check if it's a simple name that needs mapping
430
+ if (classMap[classPath]) {
431
+ return classMap[classPath];
432
+ }
433
+
434
+ // Check if it already looks like a full path
435
+ if (classPath.startsWith('/Script/') || classPath.startsWith('/Game/')) {
436
+ return classPath;
437
+ }
438
+
439
+ // Check for Blueprint paths
440
+ if (classPath.includes('Blueprint') || classPath.includes('BP_')) {
441
+ // Ensure it has the proper prefix
442
+ if (!classPath.startsWith('/Game/')) {
443
+ return '/Game/' + classPath;
444
+ }
445
+ return classPath;
446
+ }
447
+
448
+ // Default: assume it's an engine class
449
+ return '/Script/Engine.' + classPath;
450
+ }
451
+
452
+ private getConsoleClassName(classPath: string): string {
453
+ // Normalize class path for console 'summon'
454
+ const input = classPath;
455
+
456
+ // Engine classes: reduce '/Script/Engine.ClassName' to 'ClassName'
457
+ if (input.startsWith('/Script/Engine.')) {
458
+ return input.replace('/Script/Engine.', '');
459
+ }
460
+
461
+ // If it's already a simple class name (no path) and not a /Game asset, strip optional _C and return
462
+ if (!input.startsWith('/Game/') && !input.includes('/')) {
463
+ if (input.endsWith('_C')) return input.slice(0, -2);
464
+ return input;
465
+ }
466
+
467
+ // Blueprint assets under /Game: ensure '/Game/Path/Asset.Asset_C'
468
+ if (input.startsWith('/Game/')) {
469
+ // Remove any existing ".Something" suffix to rebuild normalized class ref
470
+ const pathWithoutSuffix = input.split('.')[0];
471
+ const parts = pathWithoutSuffix.split('/');
472
+ const assetName = parts[parts.length - 1].replace(/_C$/, '');
473
+ const normalized = `${pathWithoutSuffix}.${assetName}_C`;
474
+ return normalized;
475
+ }
476
+
477
+ // Fallback: return input unchanged
478
+ return input;
479
+ }
480
+ }