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