unreal-engine-mcp-server 0.4.0 → 0.4.3

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 (135) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +21 -5
  5. package/dist/index.js +124 -31
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.js +46 -62
  11. package/dist/resources/levels.d.ts +21 -3
  12. package/dist/resources/levels.js +29 -54
  13. package/dist/tools/actors.d.ts +3 -14
  14. package/dist/tools/actors.js +246 -302
  15. package/dist/tools/animation.d.ts +57 -102
  16. package/dist/tools/animation.js +429 -450
  17. package/dist/tools/assets.d.ts +13 -2
  18. package/dist/tools/assets.js +52 -44
  19. package/dist/tools/audio.d.ts +22 -13
  20. package/dist/tools/audio.js +467 -121
  21. package/dist/tools/blueprint.d.ts +32 -13
  22. package/dist/tools/blueprint.js +699 -448
  23. package/dist/tools/build_environment_advanced.d.ts +0 -1
  24. package/dist/tools/build_environment_advanced.js +190 -45
  25. package/dist/tools/consolidated-tool-definitions.js +78 -252
  26. package/dist/tools/consolidated-tool-handlers.js +506 -133
  27. package/dist/tools/debug.d.ts +72 -10
  28. package/dist/tools/debug.js +167 -31
  29. package/dist/tools/editor.d.ts +9 -2
  30. package/dist/tools/editor.js +30 -44
  31. package/dist/tools/foliage.d.ts +34 -15
  32. package/dist/tools/foliage.js +97 -107
  33. package/dist/tools/introspection.js +19 -21
  34. package/dist/tools/landscape.d.ts +1 -2
  35. package/dist/tools/landscape.js +311 -168
  36. package/dist/tools/level.d.ts +3 -28
  37. package/dist/tools/level.js +642 -192
  38. package/dist/tools/lighting.d.ts +14 -3
  39. package/dist/tools/lighting.js +236 -123
  40. package/dist/tools/materials.d.ts +25 -7
  41. package/dist/tools/materials.js +102 -79
  42. package/dist/tools/niagara.d.ts +10 -12
  43. package/dist/tools/niagara.js +74 -94
  44. package/dist/tools/performance.d.ts +12 -4
  45. package/dist/tools/performance.js +38 -79
  46. package/dist/tools/physics.d.ts +34 -10
  47. package/dist/tools/physics.js +364 -292
  48. package/dist/tools/rc.js +97 -23
  49. package/dist/tools/sequence.d.ts +1 -0
  50. package/dist/tools/sequence.js +125 -22
  51. package/dist/tools/ui.d.ts +31 -4
  52. package/dist/tools/ui.js +83 -66
  53. package/dist/tools/visual.d.ts +11 -0
  54. package/dist/tools/visual.js +245 -30
  55. package/dist/types/tool-types.d.ts +0 -6
  56. package/dist/types/tool-types.js +1 -8
  57. package/dist/unreal-bridge.d.ts +32 -2
  58. package/dist/unreal-bridge.js +621 -127
  59. package/dist/utils/elicitation.d.ts +57 -0
  60. package/dist/utils/elicitation.js +104 -0
  61. package/dist/utils/error-handler.d.ts +0 -33
  62. package/dist/utils/error-handler.js +4 -111
  63. package/dist/utils/http.d.ts +2 -22
  64. package/dist/utils/http.js +12 -75
  65. package/dist/utils/normalize.d.ts +4 -4
  66. package/dist/utils/normalize.js +15 -7
  67. package/dist/utils/python-output.d.ts +18 -0
  68. package/dist/utils/python-output.js +290 -0
  69. package/dist/utils/python.d.ts +2 -0
  70. package/dist/utils/python.js +4 -0
  71. package/dist/utils/response-validator.js +28 -2
  72. package/dist/utils/result-helpers.d.ts +27 -0
  73. package/dist/utils/result-helpers.js +147 -0
  74. package/dist/utils/safe-json.d.ts +0 -2
  75. package/dist/utils/safe-json.js +0 -43
  76. package/dist/utils/validation.d.ts +16 -0
  77. package/dist/utils/validation.js +70 -7
  78. package/mcp-config-example.json +2 -2
  79. package/package.json +10 -9
  80. package/server.json +37 -14
  81. package/src/index.ts +130 -33
  82. package/src/prompts/index.ts +211 -13
  83. package/src/resources/actors.ts +59 -44
  84. package/src/resources/assets.ts +48 -51
  85. package/src/resources/levels.ts +35 -45
  86. package/src/tools/actors.ts +269 -313
  87. package/src/tools/animation.ts +556 -539
  88. package/src/tools/assets.ts +53 -43
  89. package/src/tools/audio.ts +507 -113
  90. package/src/tools/blueprint.ts +778 -462
  91. package/src/tools/build_environment_advanced.ts +266 -64
  92. package/src/tools/consolidated-tool-definitions.ts +90 -264
  93. package/src/tools/consolidated-tool-handlers.ts +630 -121
  94. package/src/tools/debug.ts +176 -33
  95. package/src/tools/editor.ts +35 -37
  96. package/src/tools/foliage.ts +110 -104
  97. package/src/tools/introspection.ts +24 -22
  98. package/src/tools/landscape.ts +334 -181
  99. package/src/tools/level.ts +683 -182
  100. package/src/tools/lighting.ts +244 -123
  101. package/src/tools/materials.ts +114 -83
  102. package/src/tools/niagara.ts +87 -81
  103. package/src/tools/performance.ts +49 -88
  104. package/src/tools/physics.ts +393 -299
  105. package/src/tools/rc.ts +102 -24
  106. package/src/tools/sequence.ts +136 -28
  107. package/src/tools/ui.ts +101 -70
  108. package/src/tools/visual.ts +250 -29
  109. package/src/types/tool-types.ts +0 -9
  110. package/src/unreal-bridge.ts +658 -140
  111. package/src/utils/elicitation.ts +129 -0
  112. package/src/utils/error-handler.ts +4 -159
  113. package/src/utils/http.ts +16 -115
  114. package/src/utils/normalize.ts +20 -10
  115. package/src/utils/python-output.ts +351 -0
  116. package/src/utils/python.ts +3 -0
  117. package/src/utils/response-validator.ts +25 -2
  118. package/src/utils/result-helpers.ts +193 -0
  119. package/src/utils/safe-json.ts +0 -50
  120. package/src/utils/validation.ts +94 -7
  121. package/tests/run-unreal-tool-tests.mjs +720 -0
  122. package/tsconfig.json +2 -2
  123. package/dist/python-utils.d.ts +0 -29
  124. package/dist/python-utils.js +0 -54
  125. package/dist/types/index.d.ts +0 -323
  126. package/dist/types/index.js +0 -28
  127. package/dist/utils/cache-manager.d.ts +0 -64
  128. package/dist/utils/cache-manager.js +0 -176
  129. package/dist/utils/errors.d.ts +0 -133
  130. package/dist/utils/errors.js +0 -256
  131. package/src/python/editor_compat.py +0 -181
  132. package/src/python-utils.ts +0 -57
  133. package/src/types/index.ts +0 -414
  134. package/src/utils/cache-manager.ts +0 -213
  135. package/src/utils/errors.ts +0 -312
@@ -2,50 +2,248 @@ export interface PromptArgument {
2
2
  type: string;
3
3
  description?: string;
4
4
  enum?: string[];
5
- default?: any;
5
+ default?: unknown;
6
6
  required?: boolean;
7
7
  }
8
8
 
9
- export interface Prompt {
9
+ export interface PromptTemplate {
10
10
  name: string;
11
11
  description: string;
12
12
  arguments?: Record<string, PromptArgument>;
13
+ build: (args: Record<string, unknown>) => Array<{
14
+ role: 'user' | 'assistant';
15
+ content: { type: 'text'; text: string };
16
+ }>;
13
17
  }
14
18
 
15
- export const prompts: Prompt[] = [
19
+ function clampChoice(value: unknown, choices: string[], fallback: string): string {
20
+ if (typeof value === 'string') {
21
+ const normalized = value.toLowerCase();
22
+ if (choices.includes(normalized)) {
23
+ return normalized;
24
+ }
25
+ }
26
+ return fallback;
27
+ }
28
+
29
+ function coerceNumber(value: unknown, fallback: number, min?: number, max?: number): number {
30
+ const num = typeof value === 'number' ? value : Number(value);
31
+ if (!Number.isFinite(num)) {
32
+ return fallback;
33
+ }
34
+ if (min !== undefined && num < min) {
35
+ return min;
36
+ }
37
+ if (max !== undefined && num > max) {
38
+ return max;
39
+ }
40
+ return num;
41
+ }
42
+
43
+ function formatVector(value: unknown): string | null {
44
+ if (!value || typeof value !== 'object') {
45
+ return null;
46
+ }
47
+ const vector = value as Record<string, unknown>;
48
+ const x = typeof vector.x === 'number' ? vector.x : Number(vector.x);
49
+ const y = typeof vector.y === 'number' ? vector.y : Number(vector.y);
50
+ const z = typeof vector.z === 'number' ? vector.z : Number(vector.z);
51
+ if ([x, y, z].some((component) => !Number.isFinite(component))) {
52
+ return null;
53
+ }
54
+ return `${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)}`;
55
+ }
56
+
57
+ export const prompts: PromptTemplate[] = [
16
58
  {
17
59
  name: 'setup_three_point_lighting',
18
- description: 'Set up a basic three-point lighting rig around the current camera focus',
60
+ description: 'Author a cinematic three-point lighting rig aligned to the active camera focus.',
19
61
  arguments: {
20
- intensity: {
21
- type: 'string',
22
- enum: ['low', 'medium', 'high'],
62
+ intensity: {
63
+ type: 'string',
64
+ enum: ['low', 'medium', 'high'],
23
65
  default: 'medium',
24
- description: 'Light intensity level'
66
+ description: 'Overall lighting mood. Low = dramatic contrast, high = bright key light.'
25
67
  }
68
+ },
69
+ build: (args) => {
70
+ const intensity = clampChoice(args.intensity, ['low', 'medium', 'high'], 'medium');
71
+ const moodHints: Record<string, string> = {
72
+ low: 'gentle key with strong contrast and subtle rim highlights',
73
+ medium: 'balanced key/fill ratio for natural coverage',
74
+ high: 'bright key with energetic fill and crisp rim separation'
75
+ };
76
+
77
+ const text = `Configure a three-point lighting rig around the current cinematic focus.
78
+
79
+ Tasks:
80
+ - Position a key light roughly 45° off-axis at eye level. Target the subject center and tune intensity for ${intensity} output (${moodHints[intensity]}).
81
+ - Add a fill light on the opposite side with wider spread and softened shadows to control contrast.
82
+ - Place a rim/back light to outline silhouettes and separate the subject from the background.
83
+ - Ensure all lights use physically plausible color temperature, enable shadow casting where helpful, and adjust attenuation to avoid spill.
84
+ - Once balanced, report the final intensity values, color temperatures, and any blockers encountered.`;
85
+
86
+ return [{
87
+ role: 'user',
88
+ content: { type: 'text', text }
89
+ }];
26
90
  }
27
91
  },
28
92
  {
29
93
  name: 'create_fps_controller',
30
- description: 'Create a first-person shooter character controller',
94
+ description: 'Spin up a first-person controller blueprint with input mappings, collision, and starter movement.',
31
95
  arguments: {
32
96
  spawnLocation: {
33
- type: 'object',
34
- description: 'Location to spawn the controller',
97
+ type: 'vector',
98
+ description: 'Optional XYZ spawn position for the player pawn.',
35
99
  required: false
36
100
  }
101
+ },
102
+ build: (args) => {
103
+ const spawnVector = formatVector(args.spawnLocation);
104
+ const spawnLine = spawnVector ? `Spawn the pawn at world coordinates (${spawnVector}).` : 'Spawn the pawn at a safe default player start or the origin.';
105
+
106
+ const text = `Build a First Person Character blueprint with:
107
+ - Camera + arms mesh, basic WASD input, jump, crouch, and sprint bindings using Enhanced Input.
108
+ - Proper collision capsule sizing for a 180cm tall human.
109
+ - Momentum-preserving air control with configurable acceleration and friction.
110
+ - A configurable base turn rate with mouse sensitivity scaling.
111
+ - Serialized defaults for walking speed (600 uu/s) and sprint speed (900 uu/s).
112
+ - Expose key movement settings as editable defaults.
113
+ - ${spawnLine}
114
+
115
+ Finish by compiling, saving, and summarizing the created blueprint path plus the mapped input actions.`;
116
+
117
+ return [{
118
+ role: 'user',
119
+ content: { type: 'text', text }
120
+ }];
37
121
  }
38
122
  },
39
123
  {
40
124
  name: 'setup_post_processing',
41
- description: 'Configure post-processing volume with cinematic settings',
125
+ description: 'Author a post-process volume tuned to a named cinematic grade.',
42
126
  arguments: {
43
127
  style: {
44
128
  type: 'string',
45
129
  enum: ['cinematic', 'realistic', 'stylized', 'noir'],
46
130
  default: 'cinematic',
47
- description: 'Visual style preset'
131
+ description: 'Look preset to emphasize color grading and tone-mapping style.'
132
+ }
133
+ },
134
+ build: (args) => {
135
+ const style = clampChoice(args.style, ['cinematic', 'realistic', 'stylized', 'noir'], 'cinematic');
136
+ const styleNotes: Record<string, string> = {
137
+ cinematic: 'filmic tonemapper, gentle bloom, warm highlights, cool shadows, slight vignette',
138
+ realistic: 'minimal grading, accurate white balance, restrained bloom, detail-preserving sharpening',
139
+ stylized: 'bold saturation shifts, custom color LUT, exaggerated contrast, selective bloom',
140
+ noir: 'monochrome conversion, strong contrast curve, subtle film grain, heavy vignette'
141
+ };
142
+
143
+ const text = `Create a global post-process volume with priority over level defaults.
144
+ - Apply the "${style}" look: ${styleNotes[style]}.
145
+ - Configure tone mapping, exposure, bloom, chromatic aberration, and LUTs as required.
146
+ - Ensure the volume is unbound unless level-specific constraints apply.
147
+ - Provide sanity checks for HDR output and keep auto-exposure transitions smooth.
148
+ - Summarize all modified settings with their final numeric values or asset references.`;
149
+
150
+ return [{
151
+ role: 'user',
152
+ content: { type: 'text', text }
153
+ }];
154
+ }
155
+ },
156
+ {
157
+ name: 'setup_dynamic_day_night_cycle',
158
+ description: 'Create or update a Blueprint to drive a dynamic day/night cycle with optional weather hooks.',
159
+ arguments: {
160
+ startTime: {
161
+ type: 'string',
162
+ enum: ['dawn', 'noon', 'dusk', 'midnight'],
163
+ default: 'dawn',
164
+ description: 'Initial lighting state for the cycle.'
165
+ },
166
+ transitionMinutes: {
167
+ type: 'number',
168
+ default: 5,
169
+ description: 'Game-time minutes to blend between major lighting states.'
170
+ },
171
+ enableWeather: {
172
+ type: 'boolean',
173
+ default: false,
174
+ description: 'Whether to expose hooks for weather-driven sky adjustments.'
48
175
  }
176
+ },
177
+ build: (args) => {
178
+ const startTime = clampChoice(args.startTime, ['dawn', 'noon', 'dusk', 'midnight'], 'dawn');
179
+ const transitionMinutes = coerceNumber(args.transitionMinutes, 5, 1, 60);
180
+ const enableWeather = Boolean(args.enableWeather);
181
+
182
+ const weatherLine = enableWeather
183
+ ? '- Expose interfaces for cloud opacity, precipitation-driven skylight updates, and lightning flashes.'
184
+ : '- Weather hooks are disabled; keep the blueprint lean';
185
+
186
+ const text = `Implement a Blueprint-based day/night cycle manager.
187
+ - Start the sequence at ${startTime} lighting.
188
+ - Advance sun rotation, skylight captures, fog, and sky atmosphere continuously with ${transitionMinutes} minute blends between key states.
189
+ - Sync directional light intensity/color with real-world sun elevation and inject moonlight at night.
190
+ - ${weatherLine}.
191
+ - Provide editor controls for time-of-day multiplier and manual overrides.
192
+ - Document the generated blueprint path and exposed parameters.`;
193
+
194
+ return [{
195
+ role: 'user',
196
+ content: { type: 'text', text }
197
+ }];
198
+ }
199
+ },
200
+ {
201
+ name: 'design_cinematic_camera_move',
202
+ description: 'Author a sequencer shot with a polished camera move and easing markers.',
203
+ arguments: {
204
+ durationSeconds: {
205
+ type: 'number',
206
+ default: 6,
207
+ description: 'Shot duration in seconds.'
208
+ },
209
+ moveStyle: {
210
+ type: 'string',
211
+ enum: ['push_in', 'orbit', 'tracking', 'crane'],
212
+ default: 'push_in',
213
+ description: 'Camera move archetype to emphasize.'
214
+ },
215
+ focusTarget: {
216
+ type: 'string',
217
+ description: 'Optional actor or component name to keep in focus.',
218
+ required: false
219
+ }
220
+ },
221
+ build: (args) => {
222
+ const duration = coerceNumber(args.durationSeconds, 6, 2, 30);
223
+ const moveStyle = clampChoice(args.moveStyle, ['push_in', 'orbit', 'tracking', 'crane'], 'push_in');
224
+ const focusLine = typeof args.focusTarget === 'string' && args.focusTarget.trim().length > 0
225
+ ? `Lock focus distance on "${args.focusTarget}" and animate depth of field pulls if necessary.`
226
+ : 'Pick the most prominent subject in frame and maintain crisp focus throughout the move.';
227
+
228
+ const moveHints: Record<string, string> = {
229
+ push_in: 'Ease-in push toward the subject with gentle camera roll stabilization.',
230
+ orbit: '360° orbit with consistent parallax and a tracked look-at target.',
231
+ tracking: 'Match the subject velocity along a spline with smoothed acceleration.',
232
+ crane: 'Combine vertical rise with lateral drift for a reveal shot.'
233
+ };
234
+
235
+ const text = `In Sequencer, author a ${duration.toFixed(1)} second cinematic shot.
236
+ - Movement style: ${moveStyle} (${moveHints[moveStyle]}).
237
+ - Key auto-exposure, camera focal length, and focal distance for a premium look.
238
+ - Add ease-in/ease-out tangents at shot boundaries to avoid abrupt starts/stops.
239
+ - ${focusLine}
240
+ - Annotate the timeline with intent markers (intro beat, climax, resolve).
241
+ - Render a preview range and summarize the created assets.`;
242
+
243
+ return [{
244
+ role: 'user',
245
+ content: { type: 'text', text }
246
+ }];
49
247
  }
50
248
  }
51
249
  ];
@@ -1,4 +1,5 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { bestEffortInterpretedText, coerceNumber, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
2
3
 
3
4
  interface CacheEntry {
4
5
  data: any;
@@ -33,7 +34,7 @@ export class ActorResources {
33
34
 
34
35
  // Use Python to get actors via EditorActorSubsystem
35
36
  try {
36
- const pythonCode = `
37
+ const pythonCode = `
37
38
  import unreal, json
38
39
  actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
39
40
  actors = actor_subsystem.get_all_level_actors() if actor_subsystem else []
@@ -51,44 +52,29 @@ for actor in actors:
51
52
  print('RESULT:' + json.dumps({'success': True, 'count': len(actor_list), 'actors': actor_list}))
52
53
  `.trim();
53
54
 
54
- const resp = await this.bridge.executePythonWithResult(pythonCode);
55
- if (resp && typeof resp === 'object' && resp.success === true && Array.isArray((resp as any).actors)) {
56
- this.setCache('listActors', resp);
57
- return resp;
58
- }
55
+ const response = await this.bridge.executePython(pythonCode);
56
+ const interpreted = interpretStandardResult(response, {
57
+ successMessage: 'Retrieved actor list',
58
+ failureMessage: 'Failed to retrieve actor list'
59
+ });
59
60
 
60
- // Fallback manual extraction with bracket matching
61
- const raw = await this.bridge.executePython(pythonCode);
62
- let output = '';
63
- if (raw?.LogOutput && Array.isArray(raw.LogOutput)) output = raw.LogOutput.map((l: any) => l.Output || '').join('');
64
- else if (typeof raw === 'string') output = raw; else output = JSON.stringify(raw);
65
- const marker = 'RESULT:';
66
- const idx = output.lastIndexOf(marker);
67
- if (idx !== -1) {
68
- let i = idx + marker.length;
69
- while (i < output.length && output[i] !== '{') i++;
70
- if (i < output.length) {
71
- let depth = 0, inStr = false, esc = false, j = i;
72
- for (; j < output.length; j++) {
73
- const ch = output[j];
74
- if (esc) { esc = false; continue; }
75
- if (ch === '\\') { esc = true; continue; }
76
- if (ch === '"') { inStr = !inStr; continue; }
77
- if (!inStr) {
78
- if (ch === '{') depth++;
79
- else if (ch === '}') { depth--; if (depth === 0) { j++; break; } }
80
- }
81
- }
82
- const jsonStr = output.slice(i, j);
83
- try {
84
- const parsed = JSON.parse(jsonStr);
85
- this.setCache('listActors', parsed);
86
- return parsed;
87
- } catch {}
88
- }
61
+ if (interpreted.success && Array.isArray(interpreted.payload.actors)) {
62
+ const actors = interpreted.payload.actors as any[];
63
+ const count = coerceNumber(interpreted.payload.count) ?? actors.length;
64
+ const payload = {
65
+ success: true as const,
66
+ count,
67
+ actors
68
+ };
69
+ this.setCache('listActors', payload);
70
+ return payload;
89
71
  }
90
72
 
91
- return { success: false, error: 'Failed to parse actors list' };
73
+ return {
74
+ success: false,
75
+ error: coerceString(interpreted.payload.error) ?? interpreted.error ?? 'Failed to parse actors list',
76
+ note: bestEffortInterpretedText(interpreted)
77
+ };
92
78
  } catch (err) {
93
79
  return { success: false, error: `Failed to list actors: ${err}` };
94
80
  }
@@ -99,18 +85,47 @@ print('RESULT:' + json.dumps({'success': True, 'count': len(actor_list), 'actors
99
85
  try {
100
86
  const pythonCode = `
101
87
  import unreal
88
+ import json
89
+
102
90
  actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
103
- actors = actor_subsystem.get_all_level_actors()
91
+ actors = actor_subsystem.get_all_level_actors() if actor_subsystem else []
92
+
93
+ found = None
104
94
  for actor in actors:
105
- if actor and actor.get_name() == "${actorName}":
106
- print(f"Found actor: {actor.get_path_name()}")
95
+ if actor and actor.get_name() == ${JSON.stringify(actorName)}:
96
+ found = {
97
+ 'success': True,
98
+ 'name': actor.get_name(),
99
+ 'path': actor.get_path_name(),
100
+ 'class': actor.get_class().get_name()
101
+ }
107
102
  break
108
- else:
109
- print(f"Actor not found: ${actorName}")
103
+
104
+ if not found:
105
+ found = {'success': False, 'error': f"Actor not found: {actorName}"}
106
+
107
+ print('RESULT:' + json.dumps(found))
110
108
  `.trim();
111
-
112
- const result = await this.bridge.executePython(pythonCode);
113
- return result;
109
+
110
+ const response = await this.bridge.executePython(pythonCode);
111
+ const interpreted = interpretStandardResult(response, {
112
+ successMessage: `Actor resolved: ${actorName}`,
113
+ failureMessage: `Actor not found: ${actorName}`
114
+ });
115
+
116
+ if (interpreted.success) {
117
+ return {
118
+ success: true as const,
119
+ name: coerceString(interpreted.payload.name) ?? actorName,
120
+ path: coerceString(interpreted.payload.path),
121
+ class: coerceString(interpreted.payload.class)
122
+ };
123
+ }
124
+
125
+ return {
126
+ success: false as const,
127
+ error: coerceString(interpreted.payload.error) ?? interpreted.error ?? `Actor not found: ${actorName}`
128
+ };
114
129
  } catch (err) {
115
130
  return { error: `Failed to get actor: ${err}` };
116
131
  }
@@ -1,4 +1,5 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { coerceBoolean, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
2
3
 
3
4
  export class AssetResources {
4
5
  constructor(private bridge: UnrealBridge) {}
@@ -177,54 +178,51 @@ except Exception as e:
177
178
  `.trim();
178
179
 
179
180
  const resp = await this.bridge.executePython(py);
180
- let output = '';
181
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
182
- output = resp.LogOutput.map((l: any) => l.Output || '').join('');
183
- } else if (typeof resp === 'string') {
184
- output = resp;
185
- } else {
186
- output = JSON.stringify(resp);
187
- }
188
-
189
- const m = output.match(/RESULT:({.*})/);
190
- if (m) {
191
- try {
192
- const parsed = JSON.parse(m[1]);
193
- if (parsed.success) {
194
- // Map folders and assets to a clear response
195
- const foldersArr = Array.isArray(parsed.folders_list) ? parsed.folders_list.map((f: any) => ({
196
- Name: f.n,
197
- Path: f.p,
181
+ const interpreted = interpretStandardResult(resp, {
182
+ successMessage: 'Directory contents retrieved',
183
+ failureMessage: 'Failed to list directory contents'
184
+ });
185
+
186
+ if (interpreted.success) {
187
+ const payload = interpreted.payload as Record<string, unknown>;
188
+
189
+ const foldersArr = Array.isArray(payload.folders_list)
190
+ ? payload.folders_list.map((f: any) => ({
191
+ Name: coerceString(f?.n) ?? '',
192
+ Path: coerceString(f?.p) ?? '',
198
193
  Class: 'Folder',
199
194
  isFolder: true
200
- })) : [];
195
+ }))
196
+ : [];
201
197
 
202
- const assetsArr = Array.isArray(parsed.assets) ? parsed.assets.map((a: any) => ({
203
- Name: a.n,
204
- Path: a.p,
205
- Class: a.c || 'Asset',
198
+ const assetsArr = Array.isArray(payload.assets)
199
+ ? payload.assets.map((a: any) => ({
200
+ Name: coerceString(a?.n) ?? '',
201
+ Path: coerceString(a?.p) ?? '',
202
+ Class: coerceString(a?.c) ?? 'Asset',
206
203
  isFolder: false
207
- })) : [];
204
+ }))
205
+ : [];
208
206
 
209
- const total = foldersArr.length + assetsArr.length;
210
- const summary = {
211
- total,
212
- folders: foldersArr.length,
213
- assets: assetsArr.length
214
- };
207
+ const total = foldersArr.length + assetsArr.length;
208
+ const summary = {
209
+ total,
210
+ folders: foldersArr.length,
211
+ assets: assetsArr.length
212
+ };
215
213
 
216
- return {
217
- success: true,
218
- path: parsed.path || this.normalizeDir(dir),
219
- summary,
220
- foldersList: foldersArr,
221
- assets: assetsArr,
222
- count: total,
223
- note: `Immediate children of ${parsed.path || this.normalizeDir(dir)}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
224
- method: 'asset_registry_listing'
225
- };
226
- }
227
- } catch {}
214
+ const resolvedPath = coerceString(payload.path) ?? this.normalizeDir(dir);
215
+
216
+ return {
217
+ success: true,
218
+ path: resolvedPath,
219
+ summary,
220
+ foldersList: foldersArr,
221
+ assets: assetsArr,
222
+ count: total,
223
+ note: `Immediate children of ${resolvedPath}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
224
+ method: 'asset_registry_listing'
225
+ };
228
226
  }
229
227
  } catch (err: any) {
230
228
  console.warn('Engine asset listing failed:', err.message);
@@ -260,16 +258,15 @@ except Exception as e:
260
258
  print("RESULT:{'success': False, 'error': '" + str(e) + "'}")
261
259
  `.trim();
262
260
  const resp = await this.bridge.executePython(py);
263
- let output = '';
264
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) output = resp.LogOutput.map((l: any) => l.Output || '').join('');
265
- else if (typeof resp === 'string') output = resp; else output = JSON.stringify(resp);
266
- const m = output.match(/RESULT:({.*})/);
267
- if (m) {
268
- try {
269
- const parsed = JSON.parse(m[1].replace(/'/g, '"'));
270
- if (parsed.success) return !!parsed.exists;
271
- } catch {}
261
+ const interpreted = interpretStandardResult(resp, {
262
+ successMessage: 'Asset existence verified',
263
+ failureMessage: 'Failed to verify asset existence'
264
+ });
265
+
266
+ if (interpreted.success) {
267
+ return coerceBoolean(interpreted.payload.exists, false) ?? false;
272
268
  }
269
+
273
270
  return false;
274
271
  }
275
272
  }
@@ -1,4 +1,5 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { coerceString, interpretStandardResult } from '../utils/result-helpers.js';
2
3
 
3
4
  export class LevelResources {
4
5
  constructor(private bridge: UnrealBridge) {}
@@ -8,22 +9,20 @@ export class LevelResources {
8
9
  try {
9
10
  const py = '\nimport unreal, json\ntry:\n # Use UnrealEditorSubsystem instead of deprecated EditorLevelLibrary\n editor_subsys = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)\n world = editor_subsys.get_editor_world()\n name = world.get_name() if world else \'None\'\n path = world.get_path_name() if world else \'None\'\n print(\'RESULT:\' + json.dumps({\'success\': True, \'name\': name, \'path\': path}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
10
11
  const resp: any = await this.bridge.executePython(py);
11
- // Handle LogOutput format from executePython
12
- let out = '';
13
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
14
- out = resp.LogOutput.map((log: any) => log.Output || '').join('');
15
- } else if (typeof resp === 'string') {
16
- out = resp;
17
- } else {
18
- out = JSON.stringify(resp);
19
- }
20
- const m = out.match(/RESULT:({.*})/);
21
- if (m) {
22
- const parsed = JSON.parse(m[1]);
23
- if (parsed.success) return parsed;
12
+ const interpreted = interpretStandardResult(resp, {
13
+ successMessage: 'Retrieved current level',
14
+ failureMessage: 'Failed to get current level'
15
+ });
16
+
17
+ if (interpreted.success) {
18
+ return {
19
+ success: true,
20
+ name: coerceString(interpreted.payload.name) ?? coerceString(interpreted.payload.level_name) ?? 'None',
21
+ path: coerceString(interpreted.payload.path) ?? 'None'
22
+ };
24
23
  }
25
- // If Python failed, return error
26
- return { error: 'Failed to get current level', success: false };
24
+
25
+ return { success: false, error: interpreted.error ?? interpreted.message };
27
26
  } catch (err) {
28
27
  return { error: `Failed to get current level: ${err}`, success: false };
29
28
  }
@@ -34,22 +33,19 @@ export class LevelResources {
34
33
  try {
35
34
  const py = '\nimport unreal, json\ntry:\n # Use UnrealEditorSubsystem instead of deprecated EditorLevelLibrary\n editor_subsys = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)\n world = editor_subsys.get_editor_world()\n path = world.get_path_name() if world else \'\'\n print(\'RESULT:\' + json.dumps({\'success\': True, \'path\': path}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
36
35
  const resp: any = await this.bridge.executePython(py);
37
- // Handle LogOutput format from executePython
38
- let out = '';
39
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
40
- out = resp.LogOutput.map((log: any) => log.Output || '').join('');
41
- } else if (typeof resp === 'string') {
42
- out = resp;
43
- } else {
44
- out = JSON.stringify(resp);
45
- }
46
- const m = out.match(/RESULT:({.*})/);
47
- if (m) {
48
- const parsed = JSON.parse(m[1]);
49
- if (parsed.success) return parsed;
36
+ const interpreted = interpretStandardResult(resp, {
37
+ successMessage: 'Retrieved level path',
38
+ failureMessage: 'Failed to get level name'
39
+ });
40
+
41
+ if (interpreted.success) {
42
+ return {
43
+ success: true,
44
+ path: coerceString(interpreted.payload.path) ?? ''
45
+ };
50
46
  }
51
- // If Python failed, return error
52
- return { error: 'Failed to get level name', success: false };
47
+
48
+ return { success: false, error: interpreted.error ?? interpreted.message };
53
49
  } catch (err) {
54
50
  return { error: `Failed to get level name: ${err}`, success: false };
55
51
  }
@@ -60,22 +56,16 @@ export class LevelResources {
60
56
  try {
61
57
  const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if not les:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': \'LevelEditorSubsystem not available\'}))\n else:\n les.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
62
58
  const resp: any = await this.bridge.executePython(py);
63
- // Handle LogOutput format from executePython
64
- let out = '';
65
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
66
- out = resp.LogOutput.map((log: any) => log.Output || '').join('');
67
- } else if (typeof resp === 'string') {
68
- out = resp;
69
- } else {
70
- out = JSON.stringify(resp);
71
- }
72
- const m = out.match(/RESULT:({.*})/);
73
- if (m) {
74
- const parsed = JSON.parse(m[1]);
75
- if (parsed.success) return { success: true, message: 'Level saved' };
59
+ const interpreted = interpretStandardResult(resp, {
60
+ successMessage: 'Level saved',
61
+ failureMessage: 'Failed to save level'
62
+ });
63
+
64
+ if (interpreted.success) {
65
+ return { success: true, message: interpreted.message };
76
66
  }
77
- // If Python failed, return error
78
- return { error: 'Failed to save level', success: false };
67
+
68
+ return { success: false, error: interpreted.error ?? interpreted.message };
79
69
  } catch (err) {
80
70
  return { error: `Failed to save level: ${err}`, success: false };
81
71
  }