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