unreal-engine-mcp-server 0.3.1 → 0.4.3

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 (144) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +22 -7
  5. package/dist/index.js +137 -46
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.d.ts +3 -2
  11. package/dist/resources/assets.js +117 -109
  12. package/dist/resources/levels.d.ts +21 -3
  13. package/dist/resources/levels.js +31 -56
  14. package/dist/tools/actors.d.ts +3 -14
  15. package/dist/tools/actors.js +246 -302
  16. package/dist/tools/animation.d.ts +57 -102
  17. package/dist/tools/animation.js +429 -450
  18. package/dist/tools/assets.d.ts +13 -2
  19. package/dist/tools/assets.js +58 -46
  20. package/dist/tools/audio.d.ts +22 -13
  21. package/dist/tools/audio.js +467 -121
  22. package/dist/tools/blueprint.d.ts +32 -13
  23. package/dist/tools/blueprint.js +699 -448
  24. package/dist/tools/build_environment_advanced.d.ts +0 -1
  25. package/dist/tools/build_environment_advanced.js +236 -87
  26. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  27. package/dist/tools/consolidated-tool-definitions.js +124 -255
  28. package/dist/tools/consolidated-tool-handlers.js +749 -766
  29. package/dist/tools/debug.d.ts +72 -10
  30. package/dist/tools/debug.js +170 -36
  31. package/dist/tools/editor.d.ts +9 -2
  32. package/dist/tools/editor.js +30 -44
  33. package/dist/tools/foliage.d.ts +34 -15
  34. package/dist/tools/foliage.js +97 -107
  35. package/dist/tools/introspection.js +19 -21
  36. package/dist/tools/landscape.d.ts +1 -2
  37. package/dist/tools/landscape.js +311 -168
  38. package/dist/tools/level.d.ts +3 -28
  39. package/dist/tools/level.js +642 -192
  40. package/dist/tools/lighting.d.ts +14 -3
  41. package/dist/tools/lighting.js +236 -123
  42. package/dist/tools/materials.d.ts +25 -7
  43. package/dist/tools/materials.js +102 -79
  44. package/dist/tools/niagara.d.ts +10 -12
  45. package/dist/tools/niagara.js +74 -94
  46. package/dist/tools/performance.d.ts +12 -4
  47. package/dist/tools/performance.js +38 -79
  48. package/dist/tools/physics.d.ts +34 -10
  49. package/dist/tools/physics.js +364 -292
  50. package/dist/tools/rc.js +98 -24
  51. package/dist/tools/sequence.d.ts +1 -0
  52. package/dist/tools/sequence.js +146 -24
  53. package/dist/tools/ui.d.ts +31 -4
  54. package/dist/tools/ui.js +83 -66
  55. package/dist/tools/visual.d.ts +11 -0
  56. package/dist/tools/visual.js +245 -30
  57. package/dist/types/tool-types.d.ts +0 -6
  58. package/dist/types/tool-types.js +1 -8
  59. package/dist/unreal-bridge.d.ts +32 -2
  60. package/dist/unreal-bridge.js +621 -127
  61. package/dist/utils/elicitation.d.ts +57 -0
  62. package/dist/utils/elicitation.js +104 -0
  63. package/dist/utils/error-handler.d.ts +0 -33
  64. package/dist/utils/error-handler.js +4 -111
  65. package/dist/utils/http.d.ts +2 -22
  66. package/dist/utils/http.js +12 -75
  67. package/dist/utils/normalize.d.ts +4 -4
  68. package/dist/utils/normalize.js +15 -7
  69. package/dist/utils/python-output.d.ts +18 -0
  70. package/dist/utils/python-output.js +290 -0
  71. package/dist/utils/python.d.ts +2 -0
  72. package/dist/utils/python.js +4 -0
  73. package/dist/utils/response-validator.d.ts +6 -1
  74. package/dist/utils/response-validator.js +66 -13
  75. package/dist/utils/result-helpers.d.ts +27 -0
  76. package/dist/utils/result-helpers.js +147 -0
  77. package/dist/utils/safe-json.d.ts +0 -2
  78. package/dist/utils/safe-json.js +0 -43
  79. package/dist/utils/validation.d.ts +16 -0
  80. package/dist/utils/validation.js +70 -7
  81. package/mcp-config-example.json +2 -2
  82. package/package.json +11 -10
  83. package/server.json +37 -14
  84. package/src/index.ts +146 -50
  85. package/src/prompts/index.ts +211 -13
  86. package/src/resources/actors.ts +59 -44
  87. package/src/resources/assets.ts +123 -102
  88. package/src/resources/levels.ts +37 -47
  89. package/src/tools/actors.ts +269 -313
  90. package/src/tools/animation.ts +556 -539
  91. package/src/tools/assets.ts +59 -45
  92. package/src/tools/audio.ts +507 -113
  93. package/src/tools/blueprint.ts +778 -462
  94. package/src/tools/build_environment_advanced.ts +312 -106
  95. package/src/tools/consolidated-tool-definitions.ts +136 -267
  96. package/src/tools/consolidated-tool-handlers.ts +871 -795
  97. package/src/tools/debug.ts +179 -38
  98. package/src/tools/editor.ts +35 -37
  99. package/src/tools/foliage.ts +110 -104
  100. package/src/tools/introspection.ts +24 -22
  101. package/src/tools/landscape.ts +334 -181
  102. package/src/tools/level.ts +683 -182
  103. package/src/tools/lighting.ts +244 -123
  104. package/src/tools/materials.ts +114 -83
  105. package/src/tools/niagara.ts +87 -81
  106. package/src/tools/performance.ts +49 -88
  107. package/src/tools/physics.ts +393 -299
  108. package/src/tools/rc.ts +103 -25
  109. package/src/tools/sequence.ts +157 -30
  110. package/src/tools/ui.ts +101 -70
  111. package/src/tools/visual.ts +250 -29
  112. package/src/types/tool-types.ts +0 -9
  113. package/src/unreal-bridge.ts +658 -140
  114. package/src/utils/elicitation.ts +129 -0
  115. package/src/utils/error-handler.ts +4 -159
  116. package/src/utils/http.ts +16 -115
  117. package/src/utils/normalize.ts +20 -10
  118. package/src/utils/python-output.ts +351 -0
  119. package/src/utils/python.ts +3 -0
  120. package/src/utils/response-validator.ts +68 -17
  121. package/src/utils/result-helpers.ts +193 -0
  122. package/src/utils/safe-json.ts +0 -50
  123. package/src/utils/validation.ts +94 -7
  124. package/tests/run-unreal-tool-tests.mjs +720 -0
  125. package/tsconfig.json +2 -2
  126. package/dist/python-utils.d.ts +0 -29
  127. package/dist/python-utils.js +0 -54
  128. package/dist/tools/tool-definitions.d.ts +0 -4919
  129. package/dist/tools/tool-definitions.js +0 -1065
  130. package/dist/tools/tool-handlers.d.ts +0 -47
  131. package/dist/tools/tool-handlers.js +0 -863
  132. package/dist/types/index.d.ts +0 -323
  133. package/dist/types/index.js +0 -28
  134. package/dist/utils/cache-manager.d.ts +0 -64
  135. package/dist/utils/cache-manager.js +0 -176
  136. package/dist/utils/errors.d.ts +0 -133
  137. package/dist/utils/errors.js +0 -256
  138. package/src/python/editor_compat.py +0 -181
  139. package/src/python-utils.ts +0 -57
  140. package/src/tools/tool-definitions.ts +0 -1081
  141. package/src/tools/tool-handlers.ts +0 -973
  142. package/src/types/index.ts +0 -414
  143. package/src/utils/cache-manager.ts +0 -213
  144. package/src/utils/errors.ts +0 -312
@@ -1,333 +1,270 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { ensureRotation, ensureVector3 } from '../utils/validation.js';
3
+ import { coerceString, coerceVector3, interpretStandardResult } from '../utils/result-helpers.js';
4
+ import { escapePythonString } from '../utils/python.js';
2
5
 
3
6
  export class ActorTools {
4
7
  constructor(private bridge: UnrealBridge) {}
5
8
 
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
+ async spawn(params: { classPath: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number }; actorName?: string }) {
10
+ if (!params.classPath || typeof params.classPath !== 'string' || params.classPath.trim().length === 0) {
9
11
  throw new Error(`Invalid classPath: ${params.classPath}`);
10
12
  }
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);
13
+
14
+ const className = params.classPath.trim();
15
+ const requestedActorName = typeof params.actorName === 'string' ? params.actorName.trim() : undefined;
16
+ if (params.actorName !== undefined && (!requestedActorName || requestedActorName.length === 0)) {
17
+ throw new Error(`Invalid actorName: ${params.actorName}`);
65
18
  }
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 = `
19
+ const sanitizedActorName = requestedActorName?.replace(/[^A-Za-z0-9_-]/g, '_');
20
+ const lowerName = className.toLowerCase();
21
+
22
+ const shapeMapping: Record<string, string> = {
23
+ cube: '/Engine/BasicShapes/Cube',
24
+ sphere: '/Engine/BasicShapes/Sphere',
25
+ cylinder: '/Engine/BasicShapes/Cylinder',
26
+ cone: '/Engine/BasicShapes/Cone',
27
+ plane: '/Engine/BasicShapes/Plane',
28
+ torus: '/Engine/BasicShapes/Torus'
29
+ };
30
+
31
+ const mappedClassPath = shapeMapping[lowerName] ?? this.resolveActorClass(className);
32
+
33
+ const [locX, locY, locZ] = ensureVector3(
34
+ params.location ?? { x: 0, y: 0, z: 100 },
35
+ 'actor location'
36
+ );
37
+ const [rotPitch, rotYaw, rotRoll] = ensureRotation(
38
+ params.rotation ?? { pitch: 0, yaw: 0, roll: 0 },
39
+ 'actor rotation'
40
+ );
41
+
42
+ const escapedResolvedClassPath = escapePythonString(mappedClassPath);
43
+ const escapedRequestedPath = escapePythonString(className);
44
+ const escapedRequestedActorName = sanitizedActorName ? escapePythonString(sanitizedActorName) : '';
45
+
46
+ const pythonCmd = `
104
47
  import unreal
105
48
  import json
49
+ import time
106
50
 
107
- result = {"success": False, "message": "", "actor_name": ""}
51
+ result = {
52
+ "success": False,
53
+ "message": "",
54
+ "error": "",
55
+ "actorName": "",
56
+ "requestedClass": "${escapedRequestedPath}",
57
+ "resolvedClass": "${escapedResolvedClassPath}",
58
+ "location": [${locX}, ${locY}, ${locZ}],
59
+ "rotation": [${rotPitch}, ${rotYaw}, ${rotRoll}],
60
+ "requestedActorName": "${escapedRequestedActorName}",
61
+ "warnings": [],
62
+ "details": []
63
+ }
108
64
 
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
65
+ ${this.getPythonSpawnHelper()}
122
66
 
123
- # List of abstract classes that cannot be spawned
124
67
  abstract_classes = ['PlaneReflectionCapture', 'ReflectionCapture', 'Actor', 'Pawn', 'Character']
125
68
 
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)}")
69
+ def finalize():
70
+ data = dict(result)
71
+ if data.get("success"):
72
+ if not data.get("message"):
73
+ data["message"] = "Actor spawned successfully"
74
+ data.pop("error", None)
75
+ else:
76
+ if not data.get("error"):
77
+ data["error"] = data.get("message") or "Failed to spawn actor"
78
+ if not data.get("message"):
79
+ data["message"] = data["error"]
80
+ if not data.get("warnings"):
81
+ data.pop("warnings", None)
82
+ if not data.get("details"):
83
+ data.pop("details", None)
84
+ return data
85
+
86
+ try:
87
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
88
+ if les and les.is_in_play_in_editor():
89
+ result["message"] = "Cannot spawn actors while in Play In Editor mode. Please stop PIE first."
90
+ result["error"] = result["message"]
91
+ result["details"].append("Play In Editor mode detected")
92
+ print('RESULT:' + json.dumps(finalize()))
93
+ raise SystemExit(0)
94
+ except SystemExit:
95
+ raise
96
+ except Exception:
97
+ result["warnings"].append("Unable to determine Play In Editor state")
98
+
99
+ if result["requestedClass"] in abstract_classes:
100
+ result["message"] = f"Cannot spawn {result['requestedClass']}: class is abstract and cannot be instantiated"
101
+ result["error"] = result["message"]
130
102
  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
103
+ try:
104
+ class_path = result["resolvedClass"]
105
+ requested_path = result["requestedClass"]
106
+ location = unreal.Vector(${locX}, ${locY}, ${locZ})
107
+ rotation = unreal.Rotator(${rotPitch}, ${rotYaw}, ${rotRoll})
108
+ actor = None
109
+
110
+ simple_name = requested_path.split('/')[-1] if '/' in requested_path else requested_path
111
+ if '.' in simple_name:
112
+ simple_name = simple_name.split('.')[-1]
113
+ simple_name_lower = simple_name.lower()
114
+ class_lookup_name = class_path.split('.')[-1] if '.' in class_path else simple_name
115
+
116
+ result["details"].append(f"Attempting spawn using class path: {class_path}")
117
+
118
+ if class_path.startswith('/Game') or class_path.startswith('/Engine'):
119
+ try:
120
+ asset = unreal.EditorAssetLibrary.load_asset(class_path)
121
+ except Exception as asset_error:
122
+ asset = None
123
+ result["warnings"].append(f"Failed to load asset for {class_path}: {asset_error}")
124
+ if asset:
125
+ if isinstance(asset, unreal.Blueprint):
126
+ try:
127
+ actor_class = asset.generated_class()
128
+ except Exception as blueprint_error:
129
+ actor_class = None
130
+ result["warnings"].append(f"Failed to resolve blueprint class: {blueprint_error}")
131
+ if actor_class:
132
+ actor = spawn_actor_from_class(actor_class, location, rotation)
133
+ if actor:
134
+ result["details"].append("Spawned using Blueprint generated class")
135
+ elif isinstance(asset, unreal.StaticMesh):
136
+ actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
137
+ if actor:
138
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
139
+ if mesh_component:
140
+ mesh_component.set_static_mesh(asset)
141
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
142
+ result["details"].append("Applied static mesh to spawned StaticMeshActor")
143
+
144
+ if not actor:
145
+ shape_map = {
146
+ 'cube': '/Engine/BasicShapes/Cube',
147
+ 'sphere': '/Engine/BasicShapes/Sphere',
148
+ 'cylinder': '/Engine/BasicShapes/Cylinder',
149
+ 'cone': '/Engine/BasicShapes/Cone',
150
+ 'plane': '/Engine/BasicShapes/Plane',
151
+ 'torus': '/Engine/BasicShapes/Torus'
152
+ }
153
+ mesh_path = shape_map.get(simple_name_lower)
154
+ if not mesh_path and class_path.startswith('/Engine/BasicShapes'):
155
+ mesh_path = class_path
156
+ if mesh_path:
157
+ try:
158
+ shape_mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
159
+ except Exception as shape_error:
160
+ shape_mesh = None
161
+ result["warnings"].append(f"Failed to load shape mesh {mesh_path}: {shape_error}")
162
+ if shape_mesh:
163
+ actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
164
+ if actor:
165
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
166
+ if mesh_component:
167
+ mesh_component.set_static_mesh(shape_mesh)
168
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
169
+ result["details"].append(f"Spawned StaticMeshActor with mesh {mesh_path}")
170
+
171
+ if not actor:
172
+ if class_lookup_name == "StaticMeshActor":
173
+ actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
262
174
  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)}")
175
+ try:
176
+ cube_mesh = unreal.EditorAssetLibrary.load_asset('/Engine/BasicShapes/Cube')
177
+ except Exception as cube_error:
178
+ cube_mesh = None
179
+ result["warnings"].append(f"Failed to load default cube mesh: {cube_error}")
180
+ if cube_mesh:
181
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
182
+ if mesh_component:
183
+ mesh_component.set_static_mesh(cube_mesh)
184
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
185
+ result["details"].append("Applied default cube mesh to StaticMeshActor")
186
+ elif class_lookup_name == "CameraActor":
187
+ actor = spawn_actor_from_class(unreal.CameraActor, location, rotation)
188
+ if actor:
189
+ result["details"].append("Spawned CameraActor via reflected class lookup")
190
+ else:
191
+ actor_class = getattr(unreal, class_lookup_name, None)
192
+ if actor_class:
193
+ actor = spawn_actor_from_class(actor_class, location, rotation)
194
+ if actor:
195
+ result["details"].append(f"Spawned {class_lookup_name} via reflected class lookup")
196
+
197
+ if actor:
198
+ desired_name = (result.get("requestedActorName") or "").strip()
199
+ actor_name = ""
200
+ if desired_name:
201
+ try:
202
+ try:
203
+ actor.set_actor_label(desired_name, True)
204
+ except TypeError:
205
+ actor.set_actor_label(desired_name)
206
+ actor_name = actor.get_actor_label() or desired_name
207
+ except Exception as label_error:
208
+ result["warnings"].append(f"Failed to honor requested actor name '{desired_name}': {label_error}")
209
+ if not actor_name:
210
+ timestamp = int(time.time() * 1000) % 10000
211
+ base_name = simple_name or class_lookup_name or class_path.split('/')[-1]
212
+ fallback_name = f"{base_name}_{timestamp}"
213
+ try:
214
+ actor.set_actor_label(fallback_name)
215
+ except Exception as label_error:
216
+ result["warnings"].append(f"Failed to set actor label: {label_error}")
217
+ actor_name = actor.get_actor_label() or fallback_name
218
+ result["success"] = True
219
+ result["actorName"] = actor_name
220
+ if not result["message"]:
221
+ result["message"] = f"Spawned {actor_name} at ({location.x}, {location.y}, {location.z})"
222
+ else:
223
+ result["message"] = f"Failed to spawn actor from: {class_path}. Try using /Engine/BasicShapes/Cube or StaticMeshActor"
224
+ result["error"] = result["message"]
225
+ except Exception as spawn_error:
226
+ result["error"] = f"Error spawning actor: {spawn_error}"
227
+ if not result["message"]:
228
+ result["message"] = result["error"]
229
+
230
+ print('RESULT:' + json.dumps(finalize()))
278
231
  `.trim();
279
-
232
+
233
+ try {
280
234
  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 || '');
235
+ const interpreted = interpretStandardResult(response, {
236
+ successMessage: `Spawned actor ${className}`,
237
+ failureMessage: `Failed to spawn actor ${className}`
238
+ });
239
+
240
+ if (!interpreted.success) {
241
+ throw new Error(interpreted.error || interpreted.message);
300
242
  }
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
- }
243
+
244
+ const actorName = coerceString(interpreted.payload.actorName);
245
+ const resolvedClass = coerceString(interpreted.payload.resolvedClass) ?? mappedClassPath;
246
+ const requestedClass = coerceString(interpreted.payload.requestedClass) ?? className;
247
+ const locationVector = coerceVector3(interpreted.payload.location) ?? [locX, locY, locZ];
248
+ const rotationVector = coerceVector3(interpreted.payload.rotation) ?? [rotPitch, rotYaw, rotRoll];
249
+
250
+ const result: Record<string, unknown> = {
251
+ success: true,
252
+ message: interpreted.message,
253
+ actorName: actorName ?? undefined,
254
+ resolvedClass,
255
+ requestedClass,
256
+ location: { x: locationVector[0], y: locationVector[1], z: locationVector[2] },
257
+ rotation: { pitch: rotationVector[0], yaw: rotationVector[1], roll: rotationVector[2] }
258
+ };
259
+
260
+ if (interpreted.warnings?.length) {
261
+ result.warnings = interpreted.warnings;
330
262
  }
263
+ if (interpreted.details?.length) {
264
+ result.details = interpreted.details;
265
+ }
266
+
267
+ return result;
331
268
  } catch (err) {
332
269
  throw new Error(`Failed to spawn actor via Python: ${err}`);
333
270
  }
@@ -335,6 +272,7 @@ print(f"RESULT:{json.dumps(result)}")
335
272
 
336
273
  async spawnViaConsole(params: { classPath: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number } }) {
337
274
  try {
275
+ const [locX, locY, locZ] = ensureVector3(params.location ?? { x: 0, y: 0, z: 100 }, 'actor location');
338
276
  // Check if editor is in play mode first
339
277
  try {
340
278
  const pieCheckPython = `
@@ -372,8 +310,7 @@ else:
372
310
  const spawnClass = this.getConsoleClassName(params.classPath);
373
311
 
374
312
  // 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}`;
313
+ const command = `summon ${spawnClass} ${locX} ${locY} ${locZ}`;
377
314
 
378
315
  await this.bridge.httpCall('/remote/object/call', 'PUT', {
379
316
  objectPath: '/Script/Engine.Default__KismetSystemLibrary',
@@ -390,14 +327,29 @@ else:
390
327
  // We can't guarantee this actually worked, so indicate uncertainty
391
328
  return {
392
329
  success: true,
393
- message: `Actor spawn attempted via console: ${spawnClass} at ${loc.x},${loc.y},${loc.z}`,
330
+ message: `Actor spawn attempted via console: ${spawnClass} at ${locX},${locY},${locZ}`,
394
331
  note: 'Console spawn result uncertain - verify in editor'
395
332
  };
396
333
  } catch (err) {
397
334
  throw new Error(`Failed to spawn actor: ${err}`);
398
335
  }
399
336
  }
400
-
337
+ private getPythonSpawnHelper(): string {
338
+ return `
339
+ def spawn_actor_from_class(actor_class, location, rotation):
340
+ actor = None
341
+ try:
342
+ actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
343
+ if actor_subsys:
344
+ actor = actor_subsys.spawn_actor_from_class(actor_class, location, rotation)
345
+ except Exception:
346
+ actor = None
347
+ if not actor:
348
+ raise RuntimeError('EditorActorSubsystem unavailable or failed to spawn actor. Enable Editor Scripting Utilities plugin and verify class path.')
349
+ return actor
350
+ `.trim();
351
+ }
352
+
401
353
  private resolveActorClass(classPath: string): string {
402
354
  // Map common names to full Unreal class paths
403
355
  const classMap: { [key: string]: string } = {
@@ -435,6 +387,10 @@ else:
435
387
  if (classPath.startsWith('/Script/') || classPath.startsWith('/Game/')) {
436
388
  return classPath;
437
389
  }
390
+
391
+ if (classPath.startsWith('/Engine/')) {
392
+ return classPath;
393
+ }
438
394
 
439
395
  // Check for Blueprint paths
440
396
  if (classPath.includes('Blueprint') || classPath.includes('BP_')) {