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
package/src/tools/rc.ts
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { UnrealBridge } from '../unreal-bridge.js';
|
|
2
|
+
import { Logger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
export interface RCPreset {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
path: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
exposedEntities?: RCExposedEntity[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RCExposedEntity {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
type: 'property' | 'function' | 'actor';
|
|
16
|
+
objectPath?: string;
|
|
17
|
+
propertyName?: string;
|
|
18
|
+
functionName?: string;
|
|
19
|
+
metadata?: Record<string, any>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class RcTools {
|
|
23
|
+
private log = new Logger('RcTools');
|
|
24
|
+
private presetCache = new Map<string, RCPreset>();
|
|
25
|
+
private retryAttempts = 3;
|
|
26
|
+
private retryDelay = 1000;
|
|
27
|
+
|
|
28
|
+
constructor(private bridge: UnrealBridge) {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute with retry logic for transient failures
|
|
32
|
+
*/
|
|
33
|
+
private async executeWithRetry<T>(
|
|
34
|
+
operation: () => Promise<T>,
|
|
35
|
+
operationName: string
|
|
36
|
+
): Promise<T> {
|
|
37
|
+
let lastError: any;
|
|
38
|
+
|
|
39
|
+
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
40
|
+
try {
|
|
41
|
+
return await operation();
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
lastError = error;
|
|
44
|
+
this.log.warn(`${operationName} attempt ${attempt} failed: ${error.message || error}`);
|
|
45
|
+
|
|
46
|
+
if (attempt < this.retryAttempts) {
|
|
47
|
+
await new Promise(resolve =>
|
|
48
|
+
setTimeout(resolve, this.retryDelay * attempt)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw lastError;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse Python execution result with better error handling
|
|
59
|
+
*/
|
|
60
|
+
private parsePythonResult(resp: any, operationName: string): any {
|
|
61
|
+
let out = '';
|
|
62
|
+
if (resp?.LogOutput && Array.isArray((resp as any).LogOutput)) {
|
|
63
|
+
out = (resp as any).LogOutput.map((l: any) => l.Output || '').join('');
|
|
64
|
+
} else if (typeof resp === 'string') {
|
|
65
|
+
out = resp;
|
|
66
|
+
} else {
|
|
67
|
+
out = JSON.stringify(resp);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const m = out.match(/RESULT:({.*})/);
|
|
71
|
+
if (m) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(m[1]);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
this.log.error(`Failed to parse ${operationName} result: ${e}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for common error patterns
|
|
80
|
+
if (out.includes('ModuleNotFoundError')) {
|
|
81
|
+
return { success: false, error: 'Remote Control module not available. Ensure Remote Control plugin is enabled.' };
|
|
82
|
+
}
|
|
83
|
+
if (out.includes('AttributeError')) {
|
|
84
|
+
return { success: false, error: 'Remote Control API method not found. Check Unreal Engine version compatibility.' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create a Remote Control Preset asset
|
|
91
|
+
async createPreset(params: { name: string; path?: string }) {
|
|
92
|
+
const name = params.name?.trim();
|
|
93
|
+
const path = (params.path || '/Game/RCPresets').replace(/\/$/, '');
|
|
94
|
+
if (!name) return { success: false, error: 'Preset name is required' };
|
|
95
|
+
const python = `
|
|
96
|
+
import unreal, json
|
|
97
|
+
import time
|
|
98
|
+
name = r"${name}"
|
|
99
|
+
base_path = r"${path}"
|
|
100
|
+
full_path = f"{base_path}/{name}"
|
|
101
|
+
try:
|
|
102
|
+
# Check if asset already exists
|
|
103
|
+
if unreal.EditorAssetLibrary.does_asset_exist(full_path):
|
|
104
|
+
# If it exists, add a timestamp suffix to create a unique name
|
|
105
|
+
timestamp = str(int(time.time() * 1000))
|
|
106
|
+
unique_name = f"{name}_{timestamp}"
|
|
107
|
+
full_path = f"{base_path}/{unique_name}"
|
|
108
|
+
# Check again to ensure uniqueness
|
|
109
|
+
if unreal.EditorAssetLibrary.does_asset_exist(full_path):
|
|
110
|
+
print('RESULT:' + json.dumps({'success': True, 'presetPath': full_path, 'existing': True}))
|
|
111
|
+
else:
|
|
112
|
+
# Continue with creation using unique name
|
|
113
|
+
name = unique_name
|
|
114
|
+
# Now create the preset if it doesn't exist
|
|
115
|
+
if not unreal.EditorAssetLibrary.does_asset_exist(full_path):
|
|
116
|
+
# Ensure directory exists
|
|
117
|
+
if not unreal.EditorAssetLibrary.does_directory_exist(base_path):
|
|
118
|
+
unreal.EditorAssetLibrary.make_directory(base_path)
|
|
119
|
+
|
|
120
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
121
|
+
factory = None
|
|
122
|
+
try:
|
|
123
|
+
factory = unreal.RemoteControlPresetFactory()
|
|
124
|
+
except Exception:
|
|
125
|
+
# Factory might not be available in older versions
|
|
126
|
+
factory = None
|
|
127
|
+
|
|
128
|
+
asset = None
|
|
129
|
+
try:
|
|
130
|
+
if factory is not None:
|
|
131
|
+
asset = asset_tools.create_asset(asset_name=name, package_path=base_path, asset_class=unreal.RemoteControlPreset, factory=factory)
|
|
132
|
+
else:
|
|
133
|
+
# Try alternative creation method
|
|
134
|
+
asset = asset_tools.create_asset(asset_name=name, package_path=base_path, asset_class=unreal.RemoteControlPreset, factory=None)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
# If creation fails, try to provide helpful error
|
|
137
|
+
if "RemoteControlPreset" in str(e):
|
|
138
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'RemoteControlPreset class not available. Ensure Remote Control plugin is enabled.'}))
|
|
139
|
+
else:
|
|
140
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Create asset failed: {str(e)}'}))
|
|
141
|
+
raise SystemExit(0)
|
|
142
|
+
|
|
143
|
+
if asset:
|
|
144
|
+
# Save with suppressed validation warnings
|
|
145
|
+
try:
|
|
146
|
+
unreal.EditorAssetLibrary.save_asset(full_path, only_if_is_dirty=False)
|
|
147
|
+
print('RESULT:' + json.dumps({'success': True, 'presetPath': full_path}))
|
|
148
|
+
except Exception as save_err:
|
|
149
|
+
# Asset was created but save had warnings - still consider success
|
|
150
|
+
print('RESULT:' + json.dumps({'success': True, 'presetPath': full_path, 'warning': 'Asset created with validation warnings'}))
|
|
151
|
+
else:
|
|
152
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Preset creation returned None'}))
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
155
|
+
`.trim();
|
|
156
|
+
const resp = await this.executeWithRetry(
|
|
157
|
+
() => this.bridge.executePython(python),
|
|
158
|
+
'createPreset'
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const result = this.parsePythonResult(resp, 'createPreset');
|
|
162
|
+
|
|
163
|
+
// Cache the preset if successful
|
|
164
|
+
if (result.success && result.presetPath) {
|
|
165
|
+
const preset: RCPreset = {
|
|
166
|
+
id: result.presetPath,
|
|
167
|
+
name: name,
|
|
168
|
+
path: result.presetPath,
|
|
169
|
+
description: `Created at ${new Date().toISOString()}`
|
|
170
|
+
};
|
|
171
|
+
this.presetCache.set(preset.id, preset);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Expose an actor by label/name into a preset
|
|
178
|
+
async exposeActor(params: { presetPath: string; actorName: string }) {
|
|
179
|
+
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, None)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
|
|
180
|
+
const resp = await this.executeWithRetry(
|
|
181
|
+
() => this.bridge.executePython(python),
|
|
182
|
+
'exposeActor'
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const result = this.parsePythonResult(resp, 'exposeActor');
|
|
186
|
+
|
|
187
|
+
// Clear cache for this preset to force refresh
|
|
188
|
+
if (result.success) {
|
|
189
|
+
this.presetCache.delete(params.presetPath);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Expose a property on an object into a preset
|
|
196
|
+
async exposeProperty(params: { presetPath: string; objectPath: string; propertyName: string }) {
|
|
197
|
+
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name, None)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
|
|
198
|
+
const resp = await this.executeWithRetry(
|
|
199
|
+
() => this.bridge.executePython(python),
|
|
200
|
+
'exposeProperty'
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const result = this.parsePythonResult(resp, 'exposeProperty');
|
|
204
|
+
|
|
205
|
+
// Clear cache for this preset to force refresh
|
|
206
|
+
if (result.success) {
|
|
207
|
+
this.presetCache.delete(params.presetPath);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// List exposed fields (best-effort)
|
|
214
|
+
async listFields(params: { presetPath: string }) {
|
|
215
|
+
const python = `
|
|
216
|
+
import unreal, json
|
|
217
|
+
preset_path = r"${params.presetPath}"
|
|
218
|
+
try:
|
|
219
|
+
# First check if the asset exists
|
|
220
|
+
if not preset_path or not preset_path.startswith('/Game/'):
|
|
221
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Invalid preset path. Must start with /Game/'}))
|
|
222
|
+
elif not unreal.EditorAssetLibrary.does_asset_exist(preset_path):
|
|
223
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found at path: ' + preset_path}))
|
|
224
|
+
else:
|
|
225
|
+
preset = unreal.EditorAssetLibrary.load_asset(preset_path)
|
|
226
|
+
if not preset:
|
|
227
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to load preset'}))
|
|
228
|
+
else:
|
|
229
|
+
fields = []
|
|
230
|
+
try:
|
|
231
|
+
# Try to get exposed entities
|
|
232
|
+
if hasattr(preset, 'get_exposed_entities'):
|
|
233
|
+
for entity in preset.get_exposed_entities():
|
|
234
|
+
try:
|
|
235
|
+
fields.append({
|
|
236
|
+
'id': str(entity.id) if hasattr(entity, 'id') else '',
|
|
237
|
+
'label': str(entity.label) if hasattr(entity, 'label') else '',
|
|
238
|
+
'path': str(getattr(entity, 'path', ''))
|
|
239
|
+
})
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
except Exception as e:
|
|
243
|
+
# Method might not exist or be accessible
|
|
244
|
+
pass
|
|
245
|
+
print('RESULT:' + json.dumps({'success': True, 'fields': fields}))
|
|
246
|
+
except Exception as e:
|
|
247
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
248
|
+
`.trim();
|
|
249
|
+
const resp = await this.executeWithRetry(
|
|
250
|
+
() => this.bridge.executePython(python),
|
|
251
|
+
'listFields'
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
return this.parsePythonResult(resp, 'listFields');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Set a property value via Remote Control property endpoint
|
|
258
|
+
async setProperty(params: { objectPath: string; propertyName: string; value: any }) {
|
|
259
|
+
return this.executeWithRetry(async () => {
|
|
260
|
+
try {
|
|
261
|
+
// Validate value type and convert if needed
|
|
262
|
+
let processedValue = params.value;
|
|
263
|
+
|
|
264
|
+
// Handle special types
|
|
265
|
+
if (typeof params.value === 'object' && params.value !== null) {
|
|
266
|
+
// Check if it's a vector/rotator/transform
|
|
267
|
+
if ('x' in params.value || 'X' in params.value) {
|
|
268
|
+
processedValue = {
|
|
269
|
+
X: params.value.x || params.value.X || 0,
|
|
270
|
+
Y: params.value.y || params.value.Y || 0,
|
|
271
|
+
Z: params.value.z || params.value.Z || 0
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const res = await this.bridge.httpCall('/remote/object/property', 'PUT', {
|
|
277
|
+
objectPath: params.objectPath,
|
|
278
|
+
propertyName: params.propertyName,
|
|
279
|
+
propertyValue: processedValue
|
|
280
|
+
});
|
|
281
|
+
return { success: true, result: res };
|
|
282
|
+
} catch (err: any) {
|
|
283
|
+
// Check for specific error types
|
|
284
|
+
const errorMsg = err?.message || String(err);
|
|
285
|
+
if (errorMsg.includes('404')) {
|
|
286
|
+
return { success: false, error: `Property '${params.propertyName}' not found on object '${params.objectPath}'` };
|
|
287
|
+
}
|
|
288
|
+
if (errorMsg.includes('400')) {
|
|
289
|
+
return { success: false, error: `Invalid value type for property '${params.propertyName}'` };
|
|
290
|
+
}
|
|
291
|
+
return { success: false, error: errorMsg };
|
|
292
|
+
}
|
|
293
|
+
}, 'setProperty');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Get a property value via Remote Control property endpoint
|
|
297
|
+
async getProperty(params: { objectPath: string; propertyName: string }) {
|
|
298
|
+
return this.executeWithRetry(async () => {
|
|
299
|
+
try {
|
|
300
|
+
const res = await this.bridge.httpCall('/remote/object/property', 'GET', {
|
|
301
|
+
objectPath: params.objectPath,
|
|
302
|
+
propertyName: params.propertyName
|
|
303
|
+
});
|
|
304
|
+
return { success: true, value: res };
|
|
305
|
+
} catch (err: any) {
|
|
306
|
+
const errorMsg = err?.message || String(err);
|
|
307
|
+
if (errorMsg.includes('404')) {
|
|
308
|
+
return { success: false, error: `Property '${params.propertyName}' not found on object '${params.objectPath}'` };
|
|
309
|
+
}
|
|
310
|
+
return { success: false, error: errorMsg };
|
|
311
|
+
}
|
|
312
|
+
}, 'getProperty');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* List all available Remote Control presets
|
|
317
|
+
*/
|
|
318
|
+
async listPresets(): Promise<{ success: boolean; presets?: RCPreset[]; error?: string }> {
|
|
319
|
+
const python = `
|
|
320
|
+
import unreal, json
|
|
321
|
+
try:
|
|
322
|
+
presets = []
|
|
323
|
+
# Try to list assets in common RC preset locations
|
|
324
|
+
for path in ["/Game/RCPresets", "/Game/RemoteControl", "/Game"]:
|
|
325
|
+
try:
|
|
326
|
+
assets = unreal.EditorAssetLibrary.list_assets(path, recursive=True)
|
|
327
|
+
for asset in assets:
|
|
328
|
+
if "RemoteControlPreset" in asset:
|
|
329
|
+
try:
|
|
330
|
+
preset = unreal.EditorAssetLibrary.load_asset(asset)
|
|
331
|
+
if preset:
|
|
332
|
+
presets.append({
|
|
333
|
+
"id": asset,
|
|
334
|
+
"name": preset.get_name(),
|
|
335
|
+
"path": asset,
|
|
336
|
+
"description": getattr(preset, 'description', '')
|
|
337
|
+
})
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
except Exception:
|
|
341
|
+
pass
|
|
342
|
+
print('RESULT:' + json.dumps({'success': True, 'presets': presets}))
|
|
343
|
+
except Exception as e:
|
|
344
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
345
|
+
`.trim();
|
|
346
|
+
|
|
347
|
+
const resp = await this.executeWithRetry(
|
|
348
|
+
() => this.bridge.executePython(python),
|
|
349
|
+
'listPresets'
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const result = this.parsePythonResult(resp, 'listPresets');
|
|
353
|
+
|
|
354
|
+
// Update cache
|
|
355
|
+
if (result.success && result.presets) {
|
|
356
|
+
result.presets.forEach((p: RCPreset) => {
|
|
357
|
+
this.presetCache.set(p.id, p);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Delete a Remote Control preset
|
|
366
|
+
*/
|
|
367
|
+
async deletePreset(presetId: string): Promise<{ success: boolean; error?: string }> {
|
|
368
|
+
const python = `
|
|
369
|
+
import unreal, json
|
|
370
|
+
preset_id = r"${presetId}"
|
|
371
|
+
try:
|
|
372
|
+
if unreal.EditorAssetLibrary.does_asset_exist(preset_id):
|
|
373
|
+
success = unreal.EditorAssetLibrary.delete_asset(preset_id)
|
|
374
|
+
if success:
|
|
375
|
+
print('RESULT:' + json.dumps({'success': True}))
|
|
376
|
+
else:
|
|
377
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to delete preset'}))
|
|
378
|
+
else:
|
|
379
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))
|
|
380
|
+
except Exception as e:
|
|
381
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
382
|
+
`.trim();
|
|
383
|
+
|
|
384
|
+
const resp = await this.executeWithRetry(
|
|
385
|
+
() => this.bridge.executePython(python),
|
|
386
|
+
'deletePreset'
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const result = this.parsePythonResult(resp, 'deletePreset');
|
|
390
|
+
|
|
391
|
+
// Remove from cache if successful
|
|
392
|
+
if (result.success) {
|
|
393
|
+
this.presetCache.delete(presetId);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Call an exposed function through Remote Control
|
|
401
|
+
*/
|
|
402
|
+
async callFunction(params: {
|
|
403
|
+
presetPath: string;
|
|
404
|
+
functionName: string;
|
|
405
|
+
parameters?: Record<string, any>
|
|
406
|
+
}): Promise<{ success: boolean; result?: any; error?: string }> {
|
|
407
|
+
try {
|
|
408
|
+
const res = await this.bridge.httpCall('/remote/object/call', 'PUT', {
|
|
409
|
+
objectPath: params.presetPath,
|
|
410
|
+
functionName: params.functionName,
|
|
411
|
+
parameters: params.parameters || {}
|
|
412
|
+
});
|
|
413
|
+
return { success: true, result: res };
|
|
414
|
+
} catch (err: any) {
|
|
415
|
+
return { success: false, error: String(err?.message || err) };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Validate connection to Remote Control
|
|
421
|
+
*/
|
|
422
|
+
async validateConnection(): Promise<boolean> {
|
|
423
|
+
try {
|
|
424
|
+
await this.bridge.httpCall('/remote/info', 'GET', {});
|
|
425
|
+
return true;
|
|
426
|
+
} catch {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Clear preset cache
|
|
433
|
+
*/
|
|
434
|
+
clearCache(): void {
|
|
435
|
+
this.presetCache.clear();
|
|
436
|
+
}
|
|
437
|
+
}
|