unreal-engine-mcp-server 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +3 -2
- package/README.md +21 -5
- package/dist/index.js +124 -31
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.js +46 -62
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +29 -54
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +52 -44
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +190 -45
- package/dist/tools/consolidated-tool-definitions.js +78 -252
- package/dist/tools/consolidated-tool-handlers.js +506 -133
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +167 -31
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +97 -23
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +125 -22
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.js +28 -2
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +10 -9
- package/server.json +37 -14
- package/src/index.ts +130 -33
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +48 -51
- package/src/resources/levels.ts +35 -45
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +53 -43
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +266 -64
- package/src/tools/consolidated-tool-definitions.ts +90 -264
- package/src/tools/consolidated-tool-handlers.ts +630 -121
- package/src/tools/debug.ts +176 -33
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +102 -24
- package/src/tools/sequence.ts +136 -28
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +25 -2
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/src/tools/actors.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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 ${
|
|
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_')) {
|