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,683 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
export class IntrospectionTools {
|
|
3
|
+
bridge;
|
|
4
|
+
log = new Logger('IntrospectionTools');
|
|
5
|
+
objectCache = new Map();
|
|
6
|
+
retryAttempts = 3;
|
|
7
|
+
retryDelay = 1000;
|
|
8
|
+
constructor(bridge) {
|
|
9
|
+
this.bridge = bridge;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Execute with retry logic for transient failures
|
|
13
|
+
*/
|
|
14
|
+
async executeWithRetry(operation, operationName) {
|
|
15
|
+
let lastError;
|
|
16
|
+
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
return await operation();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
lastError = error;
|
|
22
|
+
this.log.warn(`${operationName} attempt ${attempt} failed: ${error.message || error}`);
|
|
23
|
+
if (attempt < this.retryAttempts) {
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw lastError;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse Python execution result with better error handling
|
|
32
|
+
*/
|
|
33
|
+
parsePythonResult(resp, operationName) {
|
|
34
|
+
let out = '';
|
|
35
|
+
if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
|
|
36
|
+
out = resp.LogOutput.map((l) => l.Output || '').join('');
|
|
37
|
+
}
|
|
38
|
+
else if (typeof resp === 'string') {
|
|
39
|
+
out = resp;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
out = JSON.stringify(resp);
|
|
43
|
+
}
|
|
44
|
+
const m = out.match(/RESULT:({.*})/);
|
|
45
|
+
if (m) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(m[1]);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
this.log.error(`Failed to parse ${operationName} result: ${e}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Check for common error patterns
|
|
54
|
+
if (out.includes('ModuleNotFoundError')) {
|
|
55
|
+
return { success: false, error: 'Reflection module not available.' };
|
|
56
|
+
}
|
|
57
|
+
if (out.includes('AttributeError')) {
|
|
58
|
+
return { success: false, error: 'Reflection API method not found. Check Unreal Engine version compatibility.' };
|
|
59
|
+
}
|
|
60
|
+
return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convert Unreal property value to JavaScript-friendly format
|
|
64
|
+
*/
|
|
65
|
+
convertPropertyValue(value, typeName) {
|
|
66
|
+
// Handle vectors, rotators, transforms
|
|
67
|
+
if (typeName.includes('Vector')) {
|
|
68
|
+
if (typeof value === 'object' && value !== null) {
|
|
69
|
+
return { x: value.X || 0, y: value.Y || 0, z: value.Z || 0 };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (typeName.includes('Rotator')) {
|
|
73
|
+
if (typeof value === 'object' && value !== null) {
|
|
74
|
+
return { pitch: value.Pitch || 0, yaw: value.Yaw || 0, roll: value.Roll || 0 };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (typeName.includes('Transform')) {
|
|
78
|
+
if (typeof value === 'object' && value !== null) {
|
|
79
|
+
return {
|
|
80
|
+
location: this.convertPropertyValue(value.Translation || value.Location, 'Vector'),
|
|
81
|
+
rotation: this.convertPropertyValue(value.Rotation, 'Rotator'),
|
|
82
|
+
scale: this.convertPropertyValue(value.Scale3D || value.Scale, 'Vector')
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
async inspectObject(params) {
|
|
89
|
+
// Check cache first if not requesting detailed info
|
|
90
|
+
if (!params.detailed && this.objectCache.has(params.objectPath)) {
|
|
91
|
+
const cached = this.objectCache.get(params.objectPath);
|
|
92
|
+
if (cached) {
|
|
93
|
+
return { success: true, info: cached };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const py = `
|
|
97
|
+
import unreal, json, inspect
|
|
98
|
+
path = r"${params.objectPath}"
|
|
99
|
+
detailed = ${params.detailed ? 'True' : 'False'}
|
|
100
|
+
|
|
101
|
+
def get_property_info(prop, obj=None):
|
|
102
|
+
"""Extract detailed property information"""
|
|
103
|
+
try:
|
|
104
|
+
info = {
|
|
105
|
+
'name': prop.get_name(),
|
|
106
|
+
'type': prop.get_property_class_name() if hasattr(prop, 'get_property_class_name') else 'Unknown'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Try to get property flags
|
|
110
|
+
flags = []
|
|
111
|
+
if hasattr(prop, 'has_any_property_flags'):
|
|
112
|
+
if prop.has_any_property_flags(unreal.PropertyFlags.CPF_EDIT_CONST):
|
|
113
|
+
flags.append('ReadOnly')
|
|
114
|
+
if prop.has_any_property_flags(unreal.PropertyFlags.CPF_BLUEPRINT_READ_ONLY):
|
|
115
|
+
flags.append('BlueprintReadOnly')
|
|
116
|
+
if prop.has_any_property_flags(unreal.PropertyFlags.CPF_TRANSIENT):
|
|
117
|
+
flags.append('Transient')
|
|
118
|
+
info['flags'] = flags
|
|
119
|
+
|
|
120
|
+
# Try to get metadata
|
|
121
|
+
if hasattr(prop, 'get_metadata'):
|
|
122
|
+
try:
|
|
123
|
+
info['category'] = prop.get_metadata('Category')
|
|
124
|
+
info['tooltip'] = prop.get_metadata('ToolTip')
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
# Try to get current value if object provided
|
|
129
|
+
if obj and detailed:
|
|
130
|
+
try:
|
|
131
|
+
value = getattr(obj, prop.get_name())
|
|
132
|
+
# Convert complex types to serializable format
|
|
133
|
+
if hasattr(value, '__dict__'):
|
|
134
|
+
value = str(value)
|
|
135
|
+
info['value'] = value
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return info
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return {'name': str(prop) if prop else 'Unknown', 'type': 'Unknown', 'error': str(e)}
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
obj = unreal.load_object(None, path)
|
|
145
|
+
if not obj:
|
|
146
|
+
# Try as class if object load fails
|
|
147
|
+
try:
|
|
148
|
+
obj = unreal.load_class(None, path)
|
|
149
|
+
if not obj:
|
|
150
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Object or class not found'}))
|
|
151
|
+
raise SystemExit(0)
|
|
152
|
+
except Exception:
|
|
153
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Object not found'}))
|
|
154
|
+
raise SystemExit(0)
|
|
155
|
+
|
|
156
|
+
info = {
|
|
157
|
+
'class': obj.get_class().get_name() if hasattr(obj, 'get_class') else str(type(obj)),
|
|
158
|
+
'name': obj.get_name() if hasattr(obj, 'get_name') else '',
|
|
159
|
+
'path': path,
|
|
160
|
+
'properties': [],
|
|
161
|
+
'functions': [],
|
|
162
|
+
'flags': []
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Get parent class
|
|
166
|
+
try:
|
|
167
|
+
if hasattr(obj, 'get_class'):
|
|
168
|
+
cls = obj.get_class()
|
|
169
|
+
if hasattr(cls, 'get_super_class'):
|
|
170
|
+
super_cls = cls.get_super_class()
|
|
171
|
+
if super_cls:
|
|
172
|
+
info['parent'] = super_cls.get_name()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# Get object flags
|
|
177
|
+
try:
|
|
178
|
+
if hasattr(obj, 'has_any_flags'):
|
|
179
|
+
flags = []
|
|
180
|
+
if obj.has_any_flags(unreal.ObjectFlags.RF_PUBLIC):
|
|
181
|
+
flags.append('Public')
|
|
182
|
+
if obj.has_any_flags(unreal.ObjectFlags.RF_TRANSIENT):
|
|
183
|
+
flags.append('Transient')
|
|
184
|
+
if obj.has_any_flags(unreal.ObjectFlags.RF_DEFAULT_SUB_OBJECT):
|
|
185
|
+
flags.append('DefaultSubObject')
|
|
186
|
+
info['flags'] = flags
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
# Get properties - AVOID deprecated properties completely
|
|
191
|
+
props = []
|
|
192
|
+
|
|
193
|
+
# List of deprecated properties to completely skip
|
|
194
|
+
deprecated_props = [
|
|
195
|
+
'life_span', 'on_actor_touch', 'on_actor_un_touch',
|
|
196
|
+
'get_touching_actors', 'get_touching_components',
|
|
197
|
+
'controller_class', 'look_up_scale', 'sound_wave_param',
|
|
198
|
+
'material_substitute', 'texture_object'
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
cls = obj.get_class() if hasattr(obj, 'get_class') else obj
|
|
203
|
+
|
|
204
|
+
# Try UE5 reflection API with safe property access
|
|
205
|
+
if hasattr(cls, 'get_properties'):
|
|
206
|
+
for prop in cls.get_properties():
|
|
207
|
+
prop_name = None
|
|
208
|
+
try:
|
|
209
|
+
# Safe property name extraction
|
|
210
|
+
if hasattr(prop, 'get_name'):
|
|
211
|
+
prop_name = prop.get_name()
|
|
212
|
+
elif hasattr(prop, 'name'):
|
|
213
|
+
prop_name = prop.name
|
|
214
|
+
|
|
215
|
+
# Skip if deprecated
|
|
216
|
+
if prop_name and prop_name not in deprecated_props:
|
|
217
|
+
prop_info = get_property_info(prop, obj if detailed else None)
|
|
218
|
+
if prop_info.get('name') not in deprecated_props:
|
|
219
|
+
props.append(prop_info)
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
# If reflection API didn't work, use a safe property list
|
|
224
|
+
if not props:
|
|
225
|
+
# Only access known safe properties
|
|
226
|
+
safe_properties = [
|
|
227
|
+
'actor_guid', 'actor_instance_guid', 'always_relevant',
|
|
228
|
+
'auto_destroy_when_finished', 'can_be_damaged',
|
|
229
|
+
'content_bundle_guid', 'custom_time_dilation',
|
|
230
|
+
'enable_auto_lod_generation', 'find_camera_component_when_view_target',
|
|
231
|
+
'generate_overlap_events_during_level_streaming', 'hidden',
|
|
232
|
+
'initial_life_span', # Use new name instead of life_span
|
|
233
|
+
'instigator', 'is_spatially_loaded', 'min_net_update_frequency',
|
|
234
|
+
'net_cull_distance_squared', 'net_dormancy', 'net_priority',
|
|
235
|
+
'net_update_frequency', 'net_use_owner_relevancy',
|
|
236
|
+
'only_relevant_to_owner', 'pivot_offset',
|
|
237
|
+
'replicate_using_registered_sub_object_list', 'replicates',
|
|
238
|
+
'root_component', 'runtime_grid', 'spawn_collision_handling_method',
|
|
239
|
+
'sprite_scale', 'tags', 'location', 'rotation', 'scale'
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
for prop_name in safe_properties:
|
|
243
|
+
try:
|
|
244
|
+
# Use get_editor_property for safer access
|
|
245
|
+
if hasattr(obj, 'get_editor_property'):
|
|
246
|
+
val = obj.get_editor_property(prop_name)
|
|
247
|
+
props.append({
|
|
248
|
+
'name': prop_name,
|
|
249
|
+
'type': type(val).__name__ if val is not None else 'None',
|
|
250
|
+
'value': str(val)[:100] if detailed and val is not None else None
|
|
251
|
+
})
|
|
252
|
+
elif hasattr(obj, prop_name):
|
|
253
|
+
# Direct access only for safe properties
|
|
254
|
+
val = getattr(obj, prop_name)
|
|
255
|
+
if not callable(val):
|
|
256
|
+
props.append({
|
|
257
|
+
'name': prop_name,
|
|
258
|
+
'type': type(val).__name__,
|
|
259
|
+
'value': str(val)[:100] if detailed else None
|
|
260
|
+
})
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
except Exception as e:
|
|
264
|
+
# Minimal fallback with only essential safe properties
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
info['properties'] = props
|
|
268
|
+
|
|
269
|
+
# Get functions/methods if detailed
|
|
270
|
+
if detailed:
|
|
271
|
+
funcs = []
|
|
272
|
+
try:
|
|
273
|
+
cls = obj.get_class() if hasattr(obj, 'get_class') else obj
|
|
274
|
+
|
|
275
|
+
# Try to get UFunctions
|
|
276
|
+
if hasattr(cls, 'get_functions'):
|
|
277
|
+
for func in cls.get_functions():
|
|
278
|
+
func_info = {
|
|
279
|
+
'name': func.get_name(),
|
|
280
|
+
'parameters': [],
|
|
281
|
+
'flags': []
|
|
282
|
+
}
|
|
283
|
+
# Get parameters if possible
|
|
284
|
+
if hasattr(func, 'get_params'):
|
|
285
|
+
for param in func.get_params():
|
|
286
|
+
func_info['parameters'].append({
|
|
287
|
+
'name': param.get_name() if hasattr(param, 'get_name') else str(param),
|
|
288
|
+
'type': 'Unknown'
|
|
289
|
+
})
|
|
290
|
+
funcs.append(func_info)
|
|
291
|
+
else:
|
|
292
|
+
# Fallback: use known safe function names
|
|
293
|
+
safe_functions = [
|
|
294
|
+
'get_actor_location', 'set_actor_location',
|
|
295
|
+
'get_actor_rotation', 'set_actor_rotation',
|
|
296
|
+
'get_actor_scale', 'set_actor_scale',
|
|
297
|
+
'destroy_actor', 'destroy_component',
|
|
298
|
+
'get_components', 'get_component_by_class',
|
|
299
|
+
'add_actor_component', 'add_component',
|
|
300
|
+
'get_world', 'get_name', 'get_path_name',
|
|
301
|
+
'is_valid', 'is_a', 'has_authority'
|
|
302
|
+
]
|
|
303
|
+
for func_name in safe_functions:
|
|
304
|
+
if hasattr(obj, func_name):
|
|
305
|
+
try:
|
|
306
|
+
attr_value = getattr(obj, func_name)
|
|
307
|
+
if callable(attr_value):
|
|
308
|
+
funcs.append({
|
|
309
|
+
'name': func_name,
|
|
310
|
+
'parameters': [],
|
|
311
|
+
'flags': []
|
|
312
|
+
})
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
info['functions'] = funcs
|
|
319
|
+
|
|
320
|
+
print('RESULT:' + json.dumps({'success': True, 'info': info}))
|
|
321
|
+
except Exception as e:
|
|
322
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
323
|
+
`.trim();
|
|
324
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'inspectObject');
|
|
325
|
+
const result = this.parsePythonResult(resp, 'inspectObject');
|
|
326
|
+
// Cache the result if successful and not detailed
|
|
327
|
+
if (result.success && result.info && !params.detailed) {
|
|
328
|
+
this.objectCache.set(params.objectPath, result.info);
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
async setProperty(params) {
|
|
333
|
+
return this.executeWithRetry(async () => {
|
|
334
|
+
try {
|
|
335
|
+
// Validate and convert value type if needed
|
|
336
|
+
let processedValue = params.value;
|
|
337
|
+
// Handle special Unreal types
|
|
338
|
+
if (typeof params.value === 'object' && params.value !== null) {
|
|
339
|
+
// Vector conversion
|
|
340
|
+
if ('x' in params.value || 'X' in params.value) {
|
|
341
|
+
processedValue = {
|
|
342
|
+
X: params.value.x || params.value.X || 0,
|
|
343
|
+
Y: params.value.y || params.value.Y || 0,
|
|
344
|
+
Z: params.value.z || params.value.Z || 0
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// Rotator conversion
|
|
348
|
+
else if ('pitch' in params.value || 'Pitch' in params.value) {
|
|
349
|
+
processedValue = {
|
|
350
|
+
Pitch: params.value.pitch || params.value.Pitch || 0,
|
|
351
|
+
Yaw: params.value.yaw || params.value.Yaw || 0,
|
|
352
|
+
Roll: params.value.roll || params.value.Roll || 0
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// Transform conversion
|
|
356
|
+
else if ('location' in params.value || 'Location' in params.value) {
|
|
357
|
+
processedValue = {
|
|
358
|
+
Translation: this.convertPropertyValue(params.value.location || params.value.Location, 'Vector'),
|
|
359
|
+
Rotation: this.convertPropertyValue(params.value.rotation || params.value.Rotation, 'Rotator'),
|
|
360
|
+
Scale3D: this.convertPropertyValue(params.value.scale || params.value.Scale || { x: 1, y: 1, z: 1 }, 'Vector')
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const res = await this.bridge.httpCall('/remote/object/property', 'PUT', {
|
|
365
|
+
objectPath: params.objectPath,
|
|
366
|
+
propertyName: params.propertyName,
|
|
367
|
+
propertyValue: processedValue
|
|
368
|
+
});
|
|
369
|
+
// Clear cache for this object
|
|
370
|
+
this.objectCache.delete(params.objectPath);
|
|
371
|
+
return { success: true, result: res };
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
const errorMsg = err?.message || String(err);
|
|
375
|
+
if (errorMsg.includes('404')) {
|
|
376
|
+
return { success: false, error: `Property '${params.propertyName}' not found on object '${params.objectPath}'` };
|
|
377
|
+
}
|
|
378
|
+
if (errorMsg.includes('400')) {
|
|
379
|
+
return { success: false, error: `Invalid value type for property '${params.propertyName}'` };
|
|
380
|
+
}
|
|
381
|
+
return { success: false, error: errorMsg };
|
|
382
|
+
}
|
|
383
|
+
}, 'setProperty');
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get property value of an object
|
|
387
|
+
*/
|
|
388
|
+
async getProperty(params) {
|
|
389
|
+
const py = `
|
|
390
|
+
import unreal, json
|
|
391
|
+
path = r"${params.objectPath}"
|
|
392
|
+
prop_name = r"${params.propertyName}"
|
|
393
|
+
try:
|
|
394
|
+
obj = unreal.load_object(None, path)
|
|
395
|
+
if not obj:
|
|
396
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Object not found'}))
|
|
397
|
+
else:
|
|
398
|
+
# Try different methods to get property
|
|
399
|
+
value = None
|
|
400
|
+
found = False
|
|
401
|
+
|
|
402
|
+
# Method 1: Direct attribute access
|
|
403
|
+
if hasattr(obj, prop_name):
|
|
404
|
+
try:
|
|
405
|
+
value = getattr(obj, prop_name)
|
|
406
|
+
found = True
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
# Method 2: get_editor_property (UE4/5)
|
|
411
|
+
if not found and hasattr(obj, 'get_editor_property'):
|
|
412
|
+
try:
|
|
413
|
+
value = obj.get_editor_property(prop_name)
|
|
414
|
+
found = True
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
# Method 3: Try with common property name variations
|
|
419
|
+
if not found:
|
|
420
|
+
# Try common property name variations
|
|
421
|
+
variations = [
|
|
422
|
+
prop_name,
|
|
423
|
+
prop_name.lower(),
|
|
424
|
+
prop_name.upper(),
|
|
425
|
+
prop_name.capitalize(),
|
|
426
|
+
# Convert snake_case to CamelCase
|
|
427
|
+
''.join(word.capitalize() for word in prop_name.split('_')),
|
|
428
|
+
# Convert CamelCase to snake_case
|
|
429
|
+
''.join(['_' + c.lower() if c.isupper() else c for c in prop_name]).lstrip('_')
|
|
430
|
+
]
|
|
431
|
+
for variant in variations:
|
|
432
|
+
if hasattr(obj, variant):
|
|
433
|
+
try:
|
|
434
|
+
value = getattr(obj, variant)
|
|
435
|
+
found = True
|
|
436
|
+
break
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
if found:
|
|
441
|
+
# Convert complex types to string
|
|
442
|
+
if hasattr(value, '__dict__'):
|
|
443
|
+
value = str(value)
|
|
444
|
+
elif isinstance(value, (list, tuple, dict)):
|
|
445
|
+
value = json.dumps(value)
|
|
446
|
+
|
|
447
|
+
print('RESULT:' + json.dumps({'success': True, 'value': value}))
|
|
448
|
+
else:
|
|
449
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Property {prop_name} not found'}))
|
|
450
|
+
except Exception as e:
|
|
451
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
452
|
+
`.trim();
|
|
453
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'getProperty');
|
|
454
|
+
return this.parsePythonResult(resp, 'getProperty');
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Call a function on an object
|
|
458
|
+
*/
|
|
459
|
+
async callFunction(params) {
|
|
460
|
+
const py = `
|
|
461
|
+
import unreal, json
|
|
462
|
+
path = r"${params.objectPath}"
|
|
463
|
+
func_name = r"${params.functionName}"
|
|
464
|
+
params = ${JSON.stringify(params.parameters || [])}
|
|
465
|
+
try:
|
|
466
|
+
obj = unreal.load_object(None, path)
|
|
467
|
+
if not obj:
|
|
468
|
+
# Try loading as class if object fails
|
|
469
|
+
try:
|
|
470
|
+
obj = unreal.load_class(None, path)
|
|
471
|
+
except:
|
|
472
|
+
pass
|
|
473
|
+
|
|
474
|
+
if not obj:
|
|
475
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Object not found'}))
|
|
476
|
+
else:
|
|
477
|
+
# For KismetMathLibrary or similar utility classes, use static method call
|
|
478
|
+
if 'KismetMathLibrary' in path or 'MathLibrary' in path or 'GameplayStatics' in path:
|
|
479
|
+
try:
|
|
480
|
+
# Use Unreal's MathLibrary (KismetMathLibrary is exposed as MathLibrary in Python)
|
|
481
|
+
if func_name.lower() == 'abs':
|
|
482
|
+
# Use Unreal's MathLibrary.abs function
|
|
483
|
+
result = unreal.MathLibrary.abs(float(params[0])) if params else 0
|
|
484
|
+
print('RESULT:' + json.dumps({'success': True, 'result': result}))
|
|
485
|
+
elif func_name.lower() == 'sqrt':
|
|
486
|
+
# Use Unreal's MathLibrary.sqrt function
|
|
487
|
+
result = unreal.MathLibrary.sqrt(float(params[0])) if params else 0
|
|
488
|
+
print('RESULT:' + json.dumps({'success': True, 'result': result}))
|
|
489
|
+
else:
|
|
490
|
+
# Try to call as static method
|
|
491
|
+
if hasattr(obj, func_name):
|
|
492
|
+
func = getattr(obj, func_name)
|
|
493
|
+
if callable(func):
|
|
494
|
+
result = func(*params) if params else func()
|
|
495
|
+
if hasattr(result, '__dict__'):
|
|
496
|
+
result = str(result)
|
|
497
|
+
print('RESULT:' + json.dumps({'success': True, 'result': result}))
|
|
498
|
+
else:
|
|
499
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'{func_name} is not callable'}))
|
|
500
|
+
else:
|
|
501
|
+
# Try snake_case version
|
|
502
|
+
snake_case_name = ''.join(['_' + c.lower() if c.isupper() else c for c in func_name]).lstrip('_')
|
|
503
|
+
if hasattr(obj, snake_case_name):
|
|
504
|
+
func = getattr(obj, snake_case_name)
|
|
505
|
+
result = func(*params) if params else func()
|
|
506
|
+
print('RESULT:' + json.dumps({'success': True, 'result': result}))
|
|
507
|
+
else:
|
|
508
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Function {func_name} not found'}))
|
|
509
|
+
except Exception as e:
|
|
510
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Function call failed: {str(e)}'}))
|
|
511
|
+
else:
|
|
512
|
+
# Regular object method call
|
|
513
|
+
if hasattr(obj, func_name):
|
|
514
|
+
func = getattr(obj, func_name)
|
|
515
|
+
if callable(func):
|
|
516
|
+
try:
|
|
517
|
+
result = func(*params) if params else func()
|
|
518
|
+
# Convert result to serializable format
|
|
519
|
+
if hasattr(result, '__dict__'):
|
|
520
|
+
result = str(result)
|
|
521
|
+
print('RESULT:' + json.dumps({'success': True, 'result': result}))
|
|
522
|
+
except Exception as e:
|
|
523
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Function call failed: {str(e)}'}))
|
|
524
|
+
else:
|
|
525
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'{func_name} is not callable'}))
|
|
526
|
+
else:
|
|
527
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Function {func_name} not found'}))
|
|
528
|
+
except Exception as e:
|
|
529
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
530
|
+
`.trim();
|
|
531
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'callFunction');
|
|
532
|
+
return this.parsePythonResult(resp, 'callFunction');
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Get Class Default Object (CDO) for a class
|
|
536
|
+
*/
|
|
537
|
+
async getCDO(className) {
|
|
538
|
+
const py = `
|
|
539
|
+
import unreal, json
|
|
540
|
+
class_name = r"${className}"
|
|
541
|
+
try:
|
|
542
|
+
# Try to find the class
|
|
543
|
+
cls = None
|
|
544
|
+
|
|
545
|
+
# Method 1: Direct class load
|
|
546
|
+
try:
|
|
547
|
+
cls = unreal.load_class(None, class_name)
|
|
548
|
+
except Exception:
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
# Method 2: Find class by name
|
|
552
|
+
if not cls:
|
|
553
|
+
try:
|
|
554
|
+
cls = unreal.find_class(class_name)
|
|
555
|
+
except Exception:
|
|
556
|
+
pass
|
|
557
|
+
|
|
558
|
+
# Method 3: Search in loaded classes
|
|
559
|
+
if not cls:
|
|
560
|
+
for obj in unreal.ObjectLibrary.get_all_objects():
|
|
561
|
+
if hasattr(obj, 'get_class'):
|
|
562
|
+
obj_cls = obj.get_class()
|
|
563
|
+
if obj_cls.get_name() == class_name:
|
|
564
|
+
cls = obj_cls
|
|
565
|
+
break
|
|
566
|
+
|
|
567
|
+
if not cls:
|
|
568
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Class not found'}))
|
|
569
|
+
else:
|
|
570
|
+
# Get CDO
|
|
571
|
+
cdo = cls.get_default_object() if hasattr(cls, 'get_default_object') else None
|
|
572
|
+
|
|
573
|
+
if cdo:
|
|
574
|
+
info = {
|
|
575
|
+
'className': cls.get_name(),
|
|
576
|
+
'cdoPath': cdo.get_path_name() if hasattr(cdo, 'get_path_name') else '',
|
|
577
|
+
'properties': []
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
# Get default property values using safe property list
|
|
581
|
+
safe_cdo_properties = [
|
|
582
|
+
'initial_life_span', 'hidden', 'can_be_damaged', 'replicates',
|
|
583
|
+
'always_relevant', 'net_dormancy', 'net_priority',
|
|
584
|
+
'net_update_frequency', 'replicate_movement',
|
|
585
|
+
'actor_guid', 'tags', 'root_component',
|
|
586
|
+
'auto_destroy_when_finished', 'enable_auto_lod_generation'
|
|
587
|
+
]
|
|
588
|
+
for prop_name in safe_cdo_properties:
|
|
589
|
+
try:
|
|
590
|
+
if hasattr(cdo, 'get_editor_property'):
|
|
591
|
+
value = cdo.get_editor_property(prop_name)
|
|
592
|
+
info['properties'].append({
|
|
593
|
+
'name': prop_name,
|
|
594
|
+
'defaultValue': str(value)[:100]
|
|
595
|
+
})
|
|
596
|
+
elif hasattr(cdo, prop_name):
|
|
597
|
+
value = getattr(cdo, prop_name)
|
|
598
|
+
if not callable(value):
|
|
599
|
+
info['properties'].append({
|
|
600
|
+
'name': prop_name,
|
|
601
|
+
'defaultValue': str(value)[:100]
|
|
602
|
+
})
|
|
603
|
+
except Exception:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
print('RESULT:' + json.dumps({'success': True, 'cdo': info}))
|
|
607
|
+
else:
|
|
608
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Could not get CDO'}))
|
|
609
|
+
except Exception as e:
|
|
610
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
611
|
+
`.trim();
|
|
612
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'getCDO');
|
|
613
|
+
return this.parsePythonResult(resp, 'getCDO');
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Search for objects by class
|
|
617
|
+
*/
|
|
618
|
+
async findObjectsByClass(className, limit = 100) {
|
|
619
|
+
const py = `
|
|
620
|
+
import unreal, json
|
|
621
|
+
class_name = r"${className}"
|
|
622
|
+
limit = ${limit}
|
|
623
|
+
try:
|
|
624
|
+
objects = []
|
|
625
|
+
count = 0
|
|
626
|
+
|
|
627
|
+
# Use EditorAssetLibrary to find assets
|
|
628
|
+
try:
|
|
629
|
+
all_assets = unreal.EditorAssetLibrary.list_assets("/Game", recursive=True)
|
|
630
|
+
for asset_path in all_assets:
|
|
631
|
+
if count >= limit:
|
|
632
|
+
break
|
|
633
|
+
try:
|
|
634
|
+
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
|
|
635
|
+
if asset:
|
|
636
|
+
asset_class = asset.get_class() if hasattr(asset, 'get_class') else None
|
|
637
|
+
if asset_class and class_name in asset_class.get_name():
|
|
638
|
+
objects.append({
|
|
639
|
+
'path': asset_path,
|
|
640
|
+
'name': asset.get_name() if hasattr(asset, 'get_name') else '',
|
|
641
|
+
'class': asset_class.get_name()
|
|
642
|
+
})
|
|
643
|
+
count += 1
|
|
644
|
+
except Exception:
|
|
645
|
+
pass
|
|
646
|
+
except Exception as e:
|
|
647
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Asset search failed: {str(e)}'}))
|
|
648
|
+
raise SystemExit(0)
|
|
649
|
+
|
|
650
|
+
# Also search in level actors
|
|
651
|
+
try:
|
|
652
|
+
actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
653
|
+
if actor_sub:
|
|
654
|
+
for actor in actor_sub.get_all_level_actors():
|
|
655
|
+
if count >= limit:
|
|
656
|
+
break
|
|
657
|
+
if actor:
|
|
658
|
+
actor_class = actor.get_class() if hasattr(actor, 'get_class') else None
|
|
659
|
+
if actor_class and class_name in actor_class.get_name():
|
|
660
|
+
objects.append({
|
|
661
|
+
'path': actor.get_path_name() if hasattr(actor, 'get_path_name') else '',
|
|
662
|
+
'name': actor.get_actor_label() if hasattr(actor, 'get_actor_label') else '',
|
|
663
|
+
'class': actor_class.get_name()
|
|
664
|
+
})
|
|
665
|
+
count += 1
|
|
666
|
+
except Exception:
|
|
667
|
+
pass
|
|
668
|
+
|
|
669
|
+
print('RESULT:' + json.dumps({'success': True, 'objects': objects, 'count': len(objects)}))
|
|
670
|
+
except Exception as e:
|
|
671
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
672
|
+
`.trim();
|
|
673
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'findObjectsByClass');
|
|
674
|
+
return this.parsePythonResult(resp, 'findObjectsByClass');
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Clear object cache
|
|
678
|
+
*/
|
|
679
|
+
clearCache() {
|
|
680
|
+
this.objectCache.clear();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
//# sourceMappingURL=introspection.js.map
|