unreal-engine-mcp-server 0.4.0 → 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 (135) 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 +21 -5
  5. package/dist/index.js +124 -31
  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.js +46 -62
  11. package/dist/resources/levels.d.ts +21 -3
  12. package/dist/resources/levels.js +29 -54
  13. package/dist/tools/actors.d.ts +3 -14
  14. package/dist/tools/actors.js +246 -302
  15. package/dist/tools/animation.d.ts +57 -102
  16. package/dist/tools/animation.js +429 -450
  17. package/dist/tools/assets.d.ts +13 -2
  18. package/dist/tools/assets.js +52 -44
  19. package/dist/tools/audio.d.ts +22 -13
  20. package/dist/tools/audio.js +467 -121
  21. package/dist/tools/blueprint.d.ts +32 -13
  22. package/dist/tools/blueprint.js +699 -448
  23. package/dist/tools/build_environment_advanced.d.ts +0 -1
  24. package/dist/tools/build_environment_advanced.js +190 -45
  25. package/dist/tools/consolidated-tool-definitions.js +78 -252
  26. package/dist/tools/consolidated-tool-handlers.js +506 -133
  27. package/dist/tools/debug.d.ts +72 -10
  28. package/dist/tools/debug.js +167 -31
  29. package/dist/tools/editor.d.ts +9 -2
  30. package/dist/tools/editor.js +30 -44
  31. package/dist/tools/foliage.d.ts +34 -15
  32. package/dist/tools/foliage.js +97 -107
  33. package/dist/tools/introspection.js +19 -21
  34. package/dist/tools/landscape.d.ts +1 -2
  35. package/dist/tools/landscape.js +311 -168
  36. package/dist/tools/level.d.ts +3 -28
  37. package/dist/tools/level.js +642 -192
  38. package/dist/tools/lighting.d.ts +14 -3
  39. package/dist/tools/lighting.js +236 -123
  40. package/dist/tools/materials.d.ts +25 -7
  41. package/dist/tools/materials.js +102 -79
  42. package/dist/tools/niagara.d.ts +10 -12
  43. package/dist/tools/niagara.js +74 -94
  44. package/dist/tools/performance.d.ts +12 -4
  45. package/dist/tools/performance.js +38 -79
  46. package/dist/tools/physics.d.ts +34 -10
  47. package/dist/tools/physics.js +364 -292
  48. package/dist/tools/rc.js +97 -23
  49. package/dist/tools/sequence.d.ts +1 -0
  50. package/dist/tools/sequence.js +125 -22
  51. package/dist/tools/ui.d.ts +31 -4
  52. package/dist/tools/ui.js +83 -66
  53. package/dist/tools/visual.d.ts +11 -0
  54. package/dist/tools/visual.js +245 -30
  55. package/dist/types/tool-types.d.ts +0 -6
  56. package/dist/types/tool-types.js +1 -8
  57. package/dist/unreal-bridge.d.ts +32 -2
  58. package/dist/unreal-bridge.js +621 -127
  59. package/dist/utils/elicitation.d.ts +57 -0
  60. package/dist/utils/elicitation.js +104 -0
  61. package/dist/utils/error-handler.d.ts +0 -33
  62. package/dist/utils/error-handler.js +4 -111
  63. package/dist/utils/http.d.ts +2 -22
  64. package/dist/utils/http.js +12 -75
  65. package/dist/utils/normalize.d.ts +4 -4
  66. package/dist/utils/normalize.js +15 -7
  67. package/dist/utils/python-output.d.ts +18 -0
  68. package/dist/utils/python-output.js +290 -0
  69. package/dist/utils/python.d.ts +2 -0
  70. package/dist/utils/python.js +4 -0
  71. package/dist/utils/response-validator.js +28 -2
  72. package/dist/utils/result-helpers.d.ts +27 -0
  73. package/dist/utils/result-helpers.js +147 -0
  74. package/dist/utils/safe-json.d.ts +0 -2
  75. package/dist/utils/safe-json.js +0 -43
  76. package/dist/utils/validation.d.ts +16 -0
  77. package/dist/utils/validation.js +70 -7
  78. package/mcp-config-example.json +2 -2
  79. package/package.json +10 -9
  80. package/server.json +37 -14
  81. package/src/index.ts +130 -33
  82. package/src/prompts/index.ts +211 -13
  83. package/src/resources/actors.ts +59 -44
  84. package/src/resources/assets.ts +48 -51
  85. package/src/resources/levels.ts +35 -45
  86. package/src/tools/actors.ts +269 -313
  87. package/src/tools/animation.ts +556 -539
  88. package/src/tools/assets.ts +53 -43
  89. package/src/tools/audio.ts +507 -113
  90. package/src/tools/blueprint.ts +778 -462
  91. package/src/tools/build_environment_advanced.ts +266 -64
  92. package/src/tools/consolidated-tool-definitions.ts +90 -264
  93. package/src/tools/consolidated-tool-handlers.ts +630 -121
  94. package/src/tools/debug.ts +176 -33
  95. package/src/tools/editor.ts +35 -37
  96. package/src/tools/foliage.ts +110 -104
  97. package/src/tools/introspection.ts +24 -22
  98. package/src/tools/landscape.ts +334 -181
  99. package/src/tools/level.ts +683 -182
  100. package/src/tools/lighting.ts +244 -123
  101. package/src/tools/materials.ts +114 -83
  102. package/src/tools/niagara.ts +87 -81
  103. package/src/tools/performance.ts +49 -88
  104. package/src/tools/physics.ts +393 -299
  105. package/src/tools/rc.ts +102 -24
  106. package/src/tools/sequence.ts +136 -28
  107. package/src/tools/ui.ts +101 -70
  108. package/src/tools/visual.ts +250 -29
  109. package/src/types/tool-types.ts +0 -9
  110. package/src/unreal-bridge.ts +658 -140
  111. package/src/utils/elicitation.ts +129 -0
  112. package/src/utils/error-handler.ts +4 -159
  113. package/src/utils/http.ts +16 -115
  114. package/src/utils/normalize.ts +20 -10
  115. package/src/utils/python-output.ts +351 -0
  116. package/src/utils/python.ts +3 -0
  117. package/src/utils/response-validator.ts +25 -2
  118. package/src/utils/result-helpers.ts +193 -0
  119. package/src/utils/safe-json.ts +0 -50
  120. package/src/utils/validation.ts +94 -7
  121. package/tests/run-unreal-tool-tests.mjs +720 -0
  122. package/tsconfig.json +2 -2
  123. package/dist/python-utils.d.ts +0 -29
  124. package/dist/python-utils.js +0 -54
  125. package/dist/types/index.d.ts +0 -323
  126. package/dist/types/index.js +0 -28
  127. package/dist/utils/cache-manager.d.ts +0 -64
  128. package/dist/utils/cache-manager.js +0 -176
  129. package/dist/utils/errors.d.ts +0 -133
  130. package/dist/utils/errors.js +0 -256
  131. package/src/python/editor_compat.py +0 -181
  132. package/src/python-utils.ts +0 -57
  133. package/src/types/index.ts +0 -414
  134. package/src/utils/cache-manager.ts +0 -213
  135. package/src/utils/errors.ts +0 -312
@@ -1,326 +1,252 @@
1
+ import { ensureRotation, ensureVector3 } from '../utils/validation.js';
2
+ import { coerceString, coerceVector3, interpretStandardResult } from '../utils/result-helpers.js';
3
+ import { escapePythonString } from '../utils/python.js';
1
4
  export class ActorTools {
2
5
  bridge;
3
6
  constructor(bridge) {
4
7
  this.bridge = bridge;
5
8
  }
6
9
  async spawn(params) {
7
- // Validate classPath
8
- if (!params.classPath || typeof params.classPath !== '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
- // Auto-map common shape names to proper asset paths
13
+ const className = params.classPath.trim();
14
+ const requestedActorName = typeof params.actorName === 'string' ? params.actorName.trim() : undefined;
15
+ if (params.actorName !== undefined && (!requestedActorName || requestedActorName.length === 0)) {
16
+ throw new Error(`Invalid actorName: ${params.actorName}`);
17
+ }
18
+ const sanitizedActorName = requestedActorName?.replace(/[^A-Za-z0-9_-]/g, '_');
19
+ const lowerName = className.toLowerCase();
12
20
  const shapeMapping = {
13
- 'cube': '/Engine/BasicShapes/Cube',
14
- 'sphere': '/Engine/BasicShapes/Sphere',
15
- 'cylinder': '/Engine/BasicShapes/Cylinder',
16
- 'cone': '/Engine/BasicShapes/Cone',
17
- 'plane': '/Engine/BasicShapes/Plane',
18
- 'torus': '/Engine/BasicShapes/Torus',
19
- 'box': '/Engine/BasicShapes/Cube', // Common alias
20
- 'ball': '/Engine/BasicShapes/Sphere', // Common alias
21
+ cube: '/Engine/BasicShapes/Cube',
22
+ sphere: '/Engine/BasicShapes/Sphere',
23
+ cylinder: '/Engine/BasicShapes/Cylinder',
24
+ cone: '/Engine/BasicShapes/Cone',
25
+ plane: '/Engine/BasicShapes/Plane',
26
+ torus: '/Engine/BasicShapes/Torus'
21
27
  };
22
- // Check if classPath is just a simple shape name (case-insensitive)
23
- const lowerPath = params.classPath.toLowerCase();
24
- if (shapeMapping[lowerPath]) {
25
- params.classPath = shapeMapping[lowerPath];
26
- }
27
- // Auto-detect and handle asset paths (like /Engine/BasicShapes/Cube)
28
- // The Python code will automatically spawn a StaticMeshActor and assign the mesh
29
- // So we don't reject asset paths anymore - let Python handle them intelligently
30
- // Only reject obviously invalid patterns
31
- if (params.classPath === 'InvalidActorClass' ||
32
- params.classPath === 'NoSlash' ||
33
- params.classPath.startsWith('/Invalid/') ||
34
- params.classPath.startsWith('/NotExist/')) {
35
- throw new Error(`Invalid actor class: ${params.classPath}`);
36
- }
37
- // Try Python API first for better control and naming
38
- try {
39
- return await this.spawnViaPython(params);
40
- }
41
- catch (pythonErr) {
42
- // Check if this is a known failure that shouldn't fall back
43
- const errorStr = String(pythonErr).toLowerCase();
44
- if (errorStr.includes('abstract') || errorStr.includes('class not found')) {
45
- // Don't try console fallback for abstract or non-existent classes
46
- throw pythonErr;
47
- }
48
- // Check if the error is because of PIE mode
49
- if (String(pythonErr).includes('Play In Editor mode')) {
50
- // Don't fall back to console if we're in PIE mode
51
- throw pythonErr;
52
- }
53
- // Fallback to console if Python fails for other reasons
54
- // Only log if not a known/expected error
55
- if (!String(pythonErr).includes('No valid result from Python')) {
56
- console.error('Python spawn failed, falling back to console:', pythonErr);
57
- }
58
- return this.spawnViaConsole(params);
59
- }
60
- }
61
- async spawnViaPython(params) {
62
- try {
63
- // Normalize and validate location
64
- const loc = params.location ?? { x: 0, y: 0, z: 100 };
65
- if (loc === null) {
66
- throw new Error('Invalid location: null is not allowed');
67
- }
68
- if (typeof loc !== 'object' ||
69
- typeof loc.x !== 'number' ||
70
- typeof loc.y !== 'number' ||
71
- typeof loc.z !== 'number') {
72
- throw new Error('Invalid location: must have numeric x, y, z properties');
73
- }
74
- // Normalize and validate rotation
75
- const rot = params.rotation ?? { pitch: 0, yaw: 0, roll: 0 };
76
- if (rot === null) {
77
- throw new Error('Invalid rotation: null is not allowed');
78
- }
79
- if (typeof rot !== 'object' ||
80
- typeof rot.pitch !== 'number' ||
81
- typeof rot.yaw !== 'number' ||
82
- typeof rot.roll !== 'number') {
83
- throw new Error('Invalid rotation: must have numeric pitch, yaw, roll properties');
84
- }
85
- // Resolve the class path
86
- const fullClassPath = this.resolveActorClass(params.classPath);
87
- let className = params.classPath;
88
- // Extract simple class name for naming the actor
89
- if (fullClassPath.includes('.')) {
90
- className = fullClassPath.split('.').pop() || params.classPath;
91
- }
92
- const pythonCmd = `
28
+ const mappedClassPath = shapeMapping[lowerName] ?? this.resolveActorClass(className);
29
+ const [locX, locY, locZ] = ensureVector3(params.location ?? { x: 0, y: 0, z: 100 }, 'actor location');
30
+ const [rotPitch, rotYaw, rotRoll] = ensureRotation(params.rotation ?? { pitch: 0, yaw: 0, roll: 0 }, 'actor rotation');
31
+ const escapedResolvedClassPath = escapePythonString(mappedClassPath);
32
+ const escapedRequestedPath = escapePythonString(className);
33
+ const escapedRequestedActorName = sanitizedActorName ? escapePythonString(sanitizedActorName) : '';
34
+ const pythonCmd = `
93
35
  import unreal
94
36
  import json
37
+ import time
95
38
 
96
- result = {"success": False, "message": "", "actor_name": ""}
39
+ result = {
40
+ "success": False,
41
+ "message": "",
42
+ "error": "",
43
+ "actorName": "",
44
+ "requestedClass": "${escapedRequestedPath}",
45
+ "resolvedClass": "${escapedResolvedClassPath}",
46
+ "location": [${locX}, ${locY}, ${locZ}],
47
+ "rotation": [${rotPitch}, ${rotYaw}, ${rotRoll}],
48
+ "requestedActorName": "${escapedRequestedActorName}",
49
+ "warnings": [],
50
+ "details": []
51
+ }
97
52
 
98
- # Check if editor is in play mode first
99
- try:
100
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
101
- if les and les.is_in_play_in_editor():
102
- result["message"] = "Cannot spawn actors while in Play In Editor mode. Please stop PIE first."
103
- print(f"RESULT:{json.dumps(result)}")
104
- # Exit early from this script
105
- raise SystemExit(0)
106
- except SystemExit:
107
- # Re-raise the SystemExit to exit properly
108
- raise
109
- except:
110
- pass # Continue if we can't check PIE state
53
+ ${this.getPythonSpawnHelper()}
111
54
 
112
- # List of abstract classes that cannot be spawned
113
55
  abstract_classes = ['PlaneReflectionCapture', 'ReflectionCapture', 'Actor', 'Pawn', 'Character']
114
56
 
115
- # Check for abstract classes
116
- if "${params.classPath}" in abstract_classes:
117
- result["message"] = f"Cannot spawn ${params.classPath}: class is abstract and cannot be instantiated"
118
- print(f"RESULT:{json.dumps(result)}")
57
+ def finalize():
58
+ data = dict(result)
59
+ if data.get("success"):
60
+ if not data.get("message"):
61
+ data["message"] = "Actor spawned successfully"
62
+ data.pop("error", None)
63
+ else:
64
+ if not data.get("error"):
65
+ data["error"] = data.get("message") or "Failed to spawn actor"
66
+ if not data.get("message"):
67
+ data["message"] = data["error"]
68
+ if not data.get("warnings"):
69
+ data.pop("warnings", None)
70
+ if not data.get("details"):
71
+ data.pop("details", None)
72
+ return data
73
+
74
+ try:
75
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
76
+ if les and les.is_in_play_in_editor():
77
+ result["message"] = "Cannot spawn actors while in Play In Editor mode. Please stop PIE first."
78
+ result["error"] = result["message"]
79
+ result["details"].append("Play In Editor mode detected")
80
+ print('RESULT:' + json.dumps(finalize()))
81
+ raise SystemExit(0)
82
+ except SystemExit:
83
+ raise
84
+ except Exception:
85
+ result["warnings"].append("Unable to determine Play In Editor state")
86
+
87
+ if result["requestedClass"] in abstract_classes:
88
+ result["message"] = f"Cannot spawn {result['requestedClass']}: class is abstract and cannot be instantiated"
89
+ result["error"] = result["message"]
119
90
  else:
120
- try:
121
- # Get the world using the modern subsystem API
122
- editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
123
- world = editor_subsystem.get_editor_world() if hasattr(editor_subsystem, 'get_editor_world') else None
124
- if not world:
125
- # Try LevelEditorSubsystem as fallback
126
- level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
127
- if hasattr(level_subsystem, 'get_editor_world'):
128
- world = level_subsystem.get_editor_world()
129
-
130
- # Handle content paths (assets) vs class names
131
- class_path = "${params.classPath}"
132
- location = unreal.Vector(${loc.x}, ${loc.y}, ${loc.z})
133
- rotation = unreal.Rotator(${rot.pitch}, ${rot.yaw}, ${rot.roll})
134
- actor = None
135
-
136
- # Check if this is a content path (starts with /Game or /Engine)
137
- if class_path.startswith('/Game') or class_path.startswith('/Engine'):
138
- # This is a content asset path - try to load and spawn it
139
- try:
140
- # For blueprint classes or static meshes
141
- asset = unreal.EditorAssetLibrary.load_asset(class_path)
142
- if asset:
143
- # If it's a blueprint class
144
- if isinstance(asset, unreal.Blueprint):
145
- actor_class = asset.generated_class()
146
- if actor_class:
147
- # Use modern EditorActorSubsystem API
148
- actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
149
- actor = actor_subsys.spawn_actor_from_class(
150
- actor_class,
151
- location,
152
- rotation
153
- )
154
- # If it's a static mesh, spawn a StaticMeshActor and assign the mesh
155
- elif isinstance(asset, unreal.StaticMesh):
156
- # Use modern EditorActorSubsystem API
157
- actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
158
- actor = actor_subsys.spawn_actor_from_class(
159
- unreal.StaticMeshActor,
160
- location,
161
- rotation
162
- )
163
- if actor:
164
- mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
165
- if mesh_component:
166
- mesh_component.set_static_mesh(asset)
167
- # Make it movable so physics can be applied
168
- mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
169
- except Exception as load_err:
170
- # If asset loading fails, try basic shapes from Engine content
171
- shape_map = {
172
- 'cube': '/Engine/BasicShapes/Cube',
173
- 'sphere': '/Engine/BasicShapes/Sphere',
174
- 'cylinder': '/Engine/BasicShapes/Cylinder',
175
- 'cone': '/Engine/BasicShapes/Cone',
176
- 'plane': '/Engine/BasicShapes/Plane',
177
- 'torus': '/Engine/BasicShapes/Torus'
178
- }
179
-
180
- # Check if it's a basic shape name or path
181
- mesh_path = None
182
- lower_path = class_path.lower()
183
- for shape_name, shape_path in shape_map.items():
184
- if shape_name in lower_path:
185
- mesh_path = shape_path
186
- break
187
-
188
- if mesh_path:
189
- # Use Engine's basic shape
190
- shape_mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
191
- if shape_mesh:
192
- # Use modern EditorActorSubsystem API
193
- actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
194
- actor = actor_subsys.spawn_actor_from_class(
195
- unreal.StaticMeshActor,
196
- location,
197
- rotation
198
- )
199
- if actor:
200
- mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
201
- if mesh_component:
202
- mesh_component.set_static_mesh(shape_mesh)
203
- # Make it movable so physics can be applied
204
- mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
205
-
206
- # If not a content path or content spawn failed, try as a class name
207
- if not actor:
208
- if class_path == "StaticMeshActor":
209
- # Use modern EditorActorSubsystem API
210
- actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
211
- actor = actor_subsys.spawn_actor_from_class(
212
- unreal.StaticMeshActor,
213
- location,
214
- rotation
215
- )
216
-
217
- if actor:
218
- # Assign a default mesh (cube)
219
- cube_mesh = unreal.EditorAssetLibrary.load_asset('/Engine/BasicShapes/Cube')
220
- if cube_mesh:
221
- mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
222
- if mesh_component:
223
- mesh_component.set_static_mesh(cube_mesh)
224
- # Make it movable so physics can be applied
225
- mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
226
-
227
- elif class_path == "CameraActor":
228
- # Use modern EditorActorSubsystem API
229
- actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
230
- actor = actor_subsys.spawn_actor_from_class(
231
- unreal.CameraActor,
232
- location,
233
- rotation
234
- )
235
- else:
236
- # Try to get the class by name for other actors (e.g., PointLight)
237
- actor_class = None
238
- if hasattr(unreal, class_path):
239
- actor_class = getattr(unreal, class_path)
240
-
241
- if actor_class:
242
- # Use modern EditorActorSubsystem API
243
- actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
244
- actor = actor_subsys.spawn_actor_from_class(
245
- actor_class,
246
- location,
247
- rotation
248
- )
249
-
250
- # Set the actor label and return result
91
+ try:
92
+ class_path = result["resolvedClass"]
93
+ requested_path = result["requestedClass"]
94
+ location = unreal.Vector(${locX}, ${locY}, ${locZ})
95
+ rotation = unreal.Rotator(${rotPitch}, ${rotYaw}, ${rotRoll})
96
+ actor = None
97
+
98
+ simple_name = requested_path.split('/')[-1] if '/' in requested_path else requested_path
99
+ if '.' in simple_name:
100
+ simple_name = simple_name.split('.')[-1]
101
+ simple_name_lower = simple_name.lower()
102
+ class_lookup_name = class_path.split('.')[-1] if '.' in class_path else simple_name
103
+
104
+ result["details"].append(f"Attempting spawn using class path: {class_path}")
105
+
106
+ if class_path.startswith('/Game') or class_path.startswith('/Engine'):
107
+ try:
108
+ asset = unreal.EditorAssetLibrary.load_asset(class_path)
109
+ except Exception as asset_error:
110
+ asset = None
111
+ result["warnings"].append(f"Failed to load asset for {class_path}: {asset_error}")
112
+ if asset:
113
+ if isinstance(asset, unreal.Blueprint):
114
+ try:
115
+ actor_class = asset.generated_class()
116
+ except Exception as blueprint_error:
117
+ actor_class = None
118
+ result["warnings"].append(f"Failed to resolve blueprint class: {blueprint_error}")
119
+ if actor_class:
120
+ actor = spawn_actor_from_class(actor_class, location, rotation)
121
+ if actor:
122
+ result["details"].append("Spawned using Blueprint generated class")
123
+ elif isinstance(asset, unreal.StaticMesh):
124
+ actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
125
+ if actor:
126
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
127
+ if mesh_component:
128
+ mesh_component.set_static_mesh(asset)
129
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
130
+ result["details"].append("Applied static mesh to spawned StaticMeshActor")
131
+
132
+ if not actor:
133
+ shape_map = {
134
+ 'cube': '/Engine/BasicShapes/Cube',
135
+ 'sphere': '/Engine/BasicShapes/Sphere',
136
+ 'cylinder': '/Engine/BasicShapes/Cylinder',
137
+ 'cone': '/Engine/BasicShapes/Cone',
138
+ 'plane': '/Engine/BasicShapes/Plane',
139
+ 'torus': '/Engine/BasicShapes/Torus'
140
+ }
141
+ mesh_path = shape_map.get(simple_name_lower)
142
+ if not mesh_path and class_path.startswith('/Engine/BasicShapes'):
143
+ mesh_path = class_path
144
+ if mesh_path:
145
+ try:
146
+ shape_mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
147
+ except Exception as shape_error:
148
+ shape_mesh = None
149
+ result["warnings"].append(f"Failed to load shape mesh {mesh_path}: {shape_error}")
150
+ if shape_mesh:
151
+ actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
152
+ if actor:
153
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
154
+ if mesh_component:
155
+ mesh_component.set_static_mesh(shape_mesh)
156
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
157
+ result["details"].append(f"Spawned StaticMeshActor with mesh {mesh_path}")
158
+
159
+ if not actor:
160
+ if class_lookup_name == "StaticMeshActor":
161
+ actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
251
162
  if actor:
252
- import time
253
- timestamp = int(time.time() * 1000) % 10000
254
- base_name = class_path.split('/')[-1] if '/' in class_path else class_path
255
- actor_name = f"{base_name}_{timestamp}"
256
- actor.set_actor_label(actor_name)
257
- result["success"] = True
258
- result["message"] = f"Spawned {actor_name} at ({location.x}, {location.y}, {location.z})"
259
- result["actor_name"] = actor_name
260
- else:
261
- result["message"] = f"Failed to spawn actor from: {class_path}. Try using /Engine/BasicShapes/Cube or StaticMeshActor"
262
-
263
- except Exception as e:
264
- result["message"] = f"Error spawning actor: {e}"
265
-
266
- print(f"RESULT:{json.dumps(result)}")
163
+ try:
164
+ cube_mesh = unreal.EditorAssetLibrary.load_asset('/Engine/BasicShapes/Cube')
165
+ except Exception as cube_error:
166
+ cube_mesh = None
167
+ result["warnings"].append(f"Failed to load default cube mesh: {cube_error}")
168
+ if cube_mesh:
169
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
170
+ if mesh_component:
171
+ mesh_component.set_static_mesh(cube_mesh)
172
+ mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
173
+ result["details"].append("Applied default cube mesh to StaticMeshActor")
174
+ elif class_lookup_name == "CameraActor":
175
+ actor = spawn_actor_from_class(unreal.CameraActor, location, rotation)
176
+ if actor:
177
+ result["details"].append("Spawned CameraActor via reflected class lookup")
178
+ else:
179
+ actor_class = getattr(unreal, class_lookup_name, None)
180
+ if actor_class:
181
+ actor = spawn_actor_from_class(actor_class, location, rotation)
182
+ if actor:
183
+ result["details"].append(f"Spawned {class_lookup_name} via reflected class lookup")
184
+
185
+ if actor:
186
+ desired_name = (result.get("requestedActorName") or "").strip()
187
+ actor_name = ""
188
+ if desired_name:
189
+ try:
190
+ try:
191
+ actor.set_actor_label(desired_name, True)
192
+ except TypeError:
193
+ actor.set_actor_label(desired_name)
194
+ actor_name = actor.get_actor_label() or desired_name
195
+ except Exception as label_error:
196
+ result["warnings"].append(f"Failed to honor requested actor name '{desired_name}': {label_error}")
197
+ if not actor_name:
198
+ timestamp = int(time.time() * 1000) % 10000
199
+ base_name = simple_name or class_lookup_name or class_path.split('/')[-1]
200
+ fallback_name = f"{base_name}_{timestamp}"
201
+ try:
202
+ actor.set_actor_label(fallback_name)
203
+ except Exception as label_error:
204
+ result["warnings"].append(f"Failed to set actor label: {label_error}")
205
+ actor_name = actor.get_actor_label() or fallback_name
206
+ result["success"] = True
207
+ result["actorName"] = actor_name
208
+ if not result["message"]:
209
+ result["message"] = f"Spawned {actor_name} at ({location.x}, {location.y}, {location.z})"
210
+ else:
211
+ result["message"] = f"Failed to spawn actor from: {class_path}. Try using /Engine/BasicShapes/Cube or StaticMeshActor"
212
+ result["error"] = result["message"]
213
+ except Exception as spawn_error:
214
+ result["error"] = f"Error spawning actor: {spawn_error}"
215
+ if not result["message"]:
216
+ result["message"] = result["error"]
217
+
218
+ print('RESULT:' + json.dumps(finalize()))
267
219
  `.trim();
220
+ try {
268
221
  const response = await this.bridge.executePython(pythonCmd);
269
- // Extract output from Python response
270
- let outputStr = '';
271
- if (typeof response === 'object' && response !== null) {
272
- // Check if it has LogOutput (standard Python execution response)
273
- if (response.LogOutput && Array.isArray(response.LogOutput)) {
274
- // Concatenate all log outputs
275
- outputStr = response.LogOutput
276
- .map((log) => log.Output || '')
277
- .join('');
278
- }
279
- else if ('result' in response) {
280
- outputStr = String(response.result);
281
- }
282
- else if ('ReturnValue' in response && typeof response.ReturnValue === 'string') {
283
- outputStr = response.ReturnValue;
284
- }
285
- else {
286
- outputStr = JSON.stringify(response);
287
- }
288
- }
289
- else {
290
- outputStr = String(response || '');
222
+ const interpreted = interpretStandardResult(response, {
223
+ successMessage: `Spawned actor ${className}`,
224
+ failureMessage: `Failed to spawn actor ${className}`
225
+ });
226
+ if (!interpreted.success) {
227
+ throw new Error(interpreted.error || interpreted.message);
291
228
  }
292
- // Parse the result from Python output
293
- const resultMatch = outputStr.match(/RESULT:({.*})/);
294
- if (resultMatch) {
295
- try {
296
- const result = JSON.parse(resultMatch[1]);
297
- if (!result.success) {
298
- throw new Error(result.message || 'Spawn failed');
299
- }
300
- return result;
301
- }
302
- catch {
303
- // If we can't parse, check for common success patterns
304
- if (outputStr.includes('Spawned')) {
305
- return { success: true, message: outputStr };
306
- }
307
- throw new Error(`Failed to parse Python result: ${outputStr}`);
308
- }
229
+ const actorName = coerceString(interpreted.payload.actorName);
230
+ const resolvedClass = coerceString(interpreted.payload.resolvedClass) ?? mappedClassPath;
231
+ const requestedClass = coerceString(interpreted.payload.requestedClass) ?? className;
232
+ const locationVector = coerceVector3(interpreted.payload.location) ?? [locX, locY, locZ];
233
+ const rotationVector = coerceVector3(interpreted.payload.rotation) ?? [rotPitch, rotYaw, rotRoll];
234
+ const result = {
235
+ success: true,
236
+ message: interpreted.message,
237
+ actorName: actorName ?? undefined,
238
+ resolvedClass,
239
+ requestedClass,
240
+ location: { x: locationVector[0], y: locationVector[1], z: locationVector[2] },
241
+ rotation: { pitch: rotationVector[0], yaw: rotationVector[1], roll: rotationVector[2] }
242
+ };
243
+ if (interpreted.warnings?.length) {
244
+ result.warnings = interpreted.warnings;
309
245
  }
310
- else {
311
- // Check output for success/failure patterns
312
- if (outputStr.includes('Failed') || outputStr.includes('Error') || outputStr.includes('not found')) {
313
- throw new Error(outputStr || 'Spawn failed');
314
- }
315
- // Default fallback - but this shouldn't report success for failed operations
316
- // Only report success if Python execution was successful and no error markers
317
- if (response?.ReturnValue === true && !outputStr.includes('abstract')) {
318
- return { success: true, message: `Actor spawned: ${className} at ${loc.x},${loc.y},${loc.z}` };
319
- }
320
- else {
321
- throw new Error(`Failed to spawn ${className}: No valid result from Python`);
322
- }
246
+ if (interpreted.details?.length) {
247
+ result.details = interpreted.details;
323
248
  }
249
+ return result;
324
250
  }
325
251
  catch (err) {
326
252
  throw new Error(`Failed to spawn actor via Python: ${err}`);
@@ -328,6 +254,7 @@ print(f"RESULT:{json.dumps(result)}")
328
254
  }
329
255
  async spawnViaConsole(params) {
330
256
  try {
257
+ const [locX, locY, locZ] = ensureVector3(params.location ?? { x: 0, y: 0, z: 100 }, 'actor location');
331
258
  // Check if editor is in play mode first
332
259
  try {
333
260
  const pieCheckPython = `
@@ -360,8 +287,7 @@ else:
360
287
  // Get the console-friendly class name
361
288
  const spawnClass = this.getConsoleClassName(params.classPath);
362
289
  // Use summon command with location if provided
363
- const loc = params.location || { x: 0, y: 0, z: 100 };
364
- const command = `summon ${spawnClass} ${loc.x} ${loc.y} ${loc.z}`;
290
+ const command = `summon ${spawnClass} ${locX} ${locY} ${locZ}`;
365
291
  await this.bridge.httpCall('/remote/object/call', 'PUT', {
366
292
  objectPath: '/Script/Engine.Default__KismetSystemLibrary',
367
293
  functionName: 'ExecuteConsoleCommand',
@@ -376,7 +302,7 @@ else:
376
302
  // We can't guarantee this actually worked, so indicate uncertainty
377
303
  return {
378
304
  success: true,
379
- message: `Actor spawn attempted via console: ${spawnClass} at ${loc.x},${loc.y},${loc.z}`,
305
+ message: `Actor spawn attempted via console: ${spawnClass} at ${locX},${locY},${locZ}`,
380
306
  note: 'Console spawn result uncertain - verify in editor'
381
307
  };
382
308
  }
@@ -384,6 +310,21 @@ else:
384
310
  throw new Error(`Failed to spawn actor: ${err}`);
385
311
  }
386
312
  }
313
+ getPythonSpawnHelper() {
314
+ return `
315
+ def spawn_actor_from_class(actor_class, location, rotation):
316
+ actor = None
317
+ try:
318
+ actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
319
+ if actor_subsys:
320
+ actor = actor_subsys.spawn_actor_from_class(actor_class, location, rotation)
321
+ except Exception:
322
+ actor = None
323
+ if not actor:
324
+ raise RuntimeError('EditorActorSubsystem unavailable or failed to spawn actor. Enable Editor Scripting Utilities plugin and verify class path.')
325
+ return actor
326
+ `.trim();
327
+ }
387
328
  resolveActorClass(classPath) {
388
329
  // Map common names to full Unreal class paths
389
330
  const classMap = {
@@ -419,6 +360,9 @@ else:
419
360
  if (classPath.startsWith('/Script/') || classPath.startsWith('/Game/')) {
420
361
  return classPath;
421
362
  }
363
+ if (classPath.startsWith('/Engine/')) {
364
+ return classPath;
365
+ }
422
366
  // Check for Blueprint paths
423
367
  if (classPath.includes('Blueprint') || classPath.includes('BP_')) {
424
368
  // Ensure it has the proper prefix