unreal-engine-mcp-server 0.5.0 → 0.5.2

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 (188) hide show
  1. package/.env.example +1 -1
  2. package/.github/release-drafter-config.yml +51 -0
  3. package/.github/workflows/greetings.yml +5 -1
  4. package/.github/workflows/labeler.yml +2 -1
  5. package/.github/workflows/publish-mcp.yml +2 -4
  6. package/.github/workflows/release-drafter.yml +3 -2
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +109 -0
  9. package/CONTRIBUTING.md +1 -1
  10. package/GEMINI.md +115 -0
  11. package/Public/Plugin_setup_guide.mp4 +0 -0
  12. package/README.md +166 -200
  13. package/dist/automation/bridge.d.ts +1 -2
  14. package/dist/automation/bridge.js +24 -23
  15. package/dist/automation/connection-manager.d.ts +1 -0
  16. package/dist/automation/connection-manager.js +10 -0
  17. package/dist/automation/message-handler.js +5 -4
  18. package/dist/automation/request-tracker.d.ts +4 -0
  19. package/dist/automation/request-tracker.js +11 -3
  20. package/dist/config.d.ts +0 -1
  21. package/dist/config.js +0 -1
  22. package/dist/constants.d.ts +4 -0
  23. package/dist/constants.js +4 -0
  24. package/dist/graphql/loaders.d.ts +64 -0
  25. package/dist/graphql/loaders.js +117 -0
  26. package/dist/graphql/resolvers.d.ts +3 -3
  27. package/dist/graphql/resolvers.js +33 -30
  28. package/dist/graphql/server.js +3 -1
  29. package/dist/graphql/types.d.ts +2 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +13 -2
  32. package/dist/server-setup.d.ts +0 -1
  33. package/dist/server-setup.js +0 -40
  34. package/dist/tools/actors.d.ts +58 -24
  35. package/dist/tools/actors.js +22 -6
  36. package/dist/tools/assets.d.ts +19 -71
  37. package/dist/tools/assets.js +28 -22
  38. package/dist/tools/base-tool.d.ts +4 -4
  39. package/dist/tools/base-tool.js +1 -1
  40. package/dist/tools/blueprint.d.ts +45 -61
  41. package/dist/tools/blueprint.js +43 -14
  42. package/dist/tools/consolidated-tool-definitions.js +2 -1
  43. package/dist/tools/consolidated-tool-handlers.js +96 -110
  44. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  45. package/dist/tools/dynamic-handler-registry.js +17 -95
  46. package/dist/tools/editor.d.ts +19 -193
  47. package/dist/tools/editor.js +11 -2
  48. package/dist/tools/environment.d.ts +8 -14
  49. package/dist/tools/foliage.d.ts +18 -143
  50. package/dist/tools/foliage.js +4 -2
  51. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  52. package/dist/tools/handlers/actor-handlers.js +14 -13
  53. package/dist/tools/handlers/asset-handlers.js +454 -454
  54. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  55. package/dist/tools/handlers/sequence-handlers.js +24 -13
  56. package/dist/tools/introspection.d.ts +1 -1
  57. package/dist/tools/introspection.js +1 -1
  58. package/dist/tools/landscape.d.ts +16 -116
  59. package/dist/tools/landscape.js +7 -3
  60. package/dist/tools/level.d.ts +22 -103
  61. package/dist/tools/level.js +26 -18
  62. package/dist/tools/lighting.d.ts +54 -7
  63. package/dist/tools/lighting.js +9 -5
  64. package/dist/tools/materials.d.ts +1 -1
  65. package/dist/tools/materials.js +5 -1
  66. package/dist/tools/niagara.js +37 -2
  67. package/dist/tools/performance.d.ts +0 -1
  68. package/dist/tools/performance.js +0 -1
  69. package/dist/tools/physics.js +5 -1
  70. package/dist/tools/sequence.d.ts +24 -24
  71. package/dist/tools/sequence.js +13 -0
  72. package/dist/tools/ui.d.ts +0 -2
  73. package/dist/types/automation-responses.d.ts +115 -0
  74. package/dist/types/automation-responses.js +2 -0
  75. package/dist/types/responses.d.ts +249 -0
  76. package/dist/types/responses.js +2 -0
  77. package/dist/types/tool-interfaces.d.ts +135 -135
  78. package/dist/types/tool-types.d.ts +2 -0
  79. package/dist/unreal-bridge.js +4 -4
  80. package/dist/utils/command-validator.js +7 -5
  81. package/dist/utils/error-handler.d.ts +24 -2
  82. package/dist/utils/error-handler.js +58 -23
  83. package/dist/utils/normalize.d.ts +7 -4
  84. package/dist/utils/normalize.js +12 -10
  85. package/dist/utils/path-security.d.ts +2 -0
  86. package/dist/utils/path-security.js +24 -0
  87. package/dist/utils/response-factory.d.ts +4 -4
  88. package/dist/utils/response-factory.js +15 -21
  89. package/dist/utils/response-validator.js +88 -73
  90. package/dist/utils/unreal-command-queue.d.ts +2 -0
  91. package/dist/utils/unreal-command-queue.js +8 -1
  92. package/docs/Migration-Guide-v0.5.0.md +1 -9
  93. package/docs/handler-mapping.md +4 -2
  94. package/docs/testing-guide.md +2 -2
  95. package/package.json +12 -6
  96. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  97. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  98. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  99. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  100. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  101. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  102. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  103. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  104. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  105. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  106. package/scripts/run-all-tests.mjs +25 -20
  107. package/server.json +3 -2
  108. package/src/automation/bridge.ts +27 -25
  109. package/src/automation/connection-manager.ts +18 -0
  110. package/src/automation/message-handler.ts +33 -8
  111. package/src/automation/request-tracker.ts +39 -7
  112. package/src/config.ts +1 -1
  113. package/src/constants.ts +7 -0
  114. package/src/graphql/loaders.ts +244 -0
  115. package/src/graphql/resolvers.ts +47 -49
  116. package/src/graphql/server.ts +3 -1
  117. package/src/graphql/types.ts +3 -0
  118. package/src/index.ts +15 -2
  119. package/src/resources/assets.ts +5 -4
  120. package/src/server/tool-registry.ts +3 -3
  121. package/src/server-setup.ts +3 -37
  122. package/src/tools/actors.ts +77 -44
  123. package/src/tools/animation.ts +1 -0
  124. package/src/tools/assets.ts +76 -65
  125. package/src/tools/base-tool.ts +3 -3
  126. package/src/tools/blueprint.ts +170 -104
  127. package/src/tools/consolidated-tool-definitions.ts +2 -1
  128. package/src/tools/consolidated-tool-handlers.ts +129 -150
  129. package/src/tools/dynamic-handler-registry.ts +22 -140
  130. package/src/tools/editor.ts +43 -29
  131. package/src/tools/environment.ts +21 -27
  132. package/src/tools/foliage.ts +28 -25
  133. package/src/tools/handlers/actor-handlers.ts +16 -17
  134. package/src/tools/handlers/asset-handlers.ts +484 -484
  135. package/src/tools/handlers/sequence-handlers.ts +85 -62
  136. package/src/tools/introspection.ts +7 -7
  137. package/src/tools/landscape.ts +34 -28
  138. package/src/tools/level.ts +100 -80
  139. package/src/tools/lighting.ts +25 -20
  140. package/src/tools/materials.ts +9 -3
  141. package/src/tools/niagara.ts +44 -2
  142. package/src/tools/performance.ts +1 -2
  143. package/src/tools/physics.ts +7 -1
  144. package/src/tools/sequence.ts +42 -26
  145. package/src/tools/ui.ts +1 -3
  146. package/src/types/automation-responses.ts +119 -0
  147. package/src/types/responses.ts +355 -0
  148. package/src/types/tool-interfaces.ts +135 -135
  149. package/src/types/tool-types.ts +4 -0
  150. package/src/unreal-bridge.ts +71 -26
  151. package/src/utils/command-validator.ts +47 -5
  152. package/src/utils/error-handler.ts +128 -45
  153. package/src/utils/normalize.test.ts +162 -0
  154. package/src/utils/normalize.ts +38 -16
  155. package/src/utils/path-security.ts +43 -0
  156. package/src/utils/response-factory.ts +29 -24
  157. package/src/utils/response-validator.ts +103 -87
  158. package/src/utils/safe-json.test.ts +90 -0
  159. package/src/utils/unreal-command-queue.ts +13 -1
  160. package/src/utils/validation.test.ts +184 -0
  161. package/tests/test-animation.mjs +358 -33
  162. package/tests/test-asset-graph.mjs +311 -0
  163. package/tests/test-audio.mjs +314 -116
  164. package/tests/test-behavior-tree.mjs +327 -144
  165. package/tests/test-blueprint-graph.mjs +343 -12
  166. package/tests/test-control-editor.mjs +85 -53
  167. package/tests/test-graphql.mjs +58 -8
  168. package/tests/test-input.mjs +349 -0
  169. package/tests/test-inspect.mjs +291 -61
  170. package/tests/test-landscape.mjs +304 -48
  171. package/tests/test-lighting.mjs +428 -0
  172. package/tests/test-manage-level.mjs +70 -51
  173. package/tests/test-performance.mjs +539 -0
  174. package/tests/test-sequence.mjs +82 -46
  175. package/tests/test-system.mjs +72 -33
  176. package/tests/test-wasm.mjs +98 -8
  177. package/vitest.config.ts +35 -0
  178. package/.github/release-drafter.yml +0 -148
  179. package/dist/prompts/index.d.ts +0 -21
  180. package/dist/prompts/index.js +0 -217
  181. package/dist/tools/blueprint/helpers.d.ts +0 -29
  182. package/dist/tools/blueprint/helpers.js +0 -182
  183. package/src/prompts/index.ts +0 -249
  184. package/src/tools/blueprint/helpers.ts +0 -189
  185. package/tests/test-blueprint-events.mjs +0 -35
  186. package/tests/test-extra-tools.mjs +0 -38
  187. package/tests/test-render.mjs +0 -33
  188. package/tests/test-search-assets.mjs +0 -66
@@ -1,7 +1,9 @@
1
1
  import { BaseTool } from './base-tool.js';
2
- import { IEditorTools } from '../types/tool-interfaces.js';
2
+ import { IEditorTools, StandardActionResponse } from '../types/tool-interfaces.js';
3
3
  import { toVec3Object, toRotObject } from '../utils/normalize.js';
4
4
  import { DEFAULT_SCREENSHOT_RESOLUTION } from '../constants.js';
5
+ import { EditorResponse } from '../types/automation-responses.js';
6
+ import { wasmIntegration } from '../wasm/index.js';
5
7
 
6
8
  export class EditorTools extends BaseTool implements IEditorTools {
7
9
  private cameraBookmarks = new Map<string, { location: [number, number, number]; rotation: [number, number, number]; savedAt: number }>();
@@ -10,14 +12,14 @@ export class EditorTools extends BaseTool implements IEditorTools {
10
12
 
11
13
  async isInPIE(): Promise<boolean> {
12
14
  try {
13
- const response = await this.sendAutomationRequest(
15
+ const response = await this.sendAutomationRequest<EditorResponse>(
14
16
  'check_pie_state',
15
17
  {},
16
18
  { timeoutMs: 5000 }
17
19
  );
18
20
 
19
21
  if (response && response.success !== false) {
20
- return response.isInPIE === true || response.result?.isInPIE === true;
22
+ return response.isInPIE === true || (response.result as any)?.isInPIE === true;
21
23
  }
22
24
 
23
25
  return false;
@@ -34,10 +36,10 @@ export class EditorTools extends BaseTool implements IEditorTools {
34
36
  }
35
37
  }
36
38
 
37
- async playInEditor(timeoutMs: number = 30000) {
39
+ async playInEditor(timeoutMs: number = 30000): Promise<StandardActionResponse> {
38
40
  try {
39
41
  try {
40
- const response = await this.sendAutomationRequest(
42
+ const response = await this.sendAutomationRequest<EditorResponse>(
41
43
  'control_editor',
42
44
  { action: 'play' },
43
45
  { timeoutMs }
@@ -46,10 +48,11 @@ export class EditorTools extends BaseTool implements IEditorTools {
46
48
  return { success: true, message: response.message || 'PIE started' };
47
49
  }
48
50
  return { success: false, error: response?.error || response?.message || 'Failed to start PIE' };
49
- } catch (err: any) {
51
+ } catch (err: unknown) {
50
52
  // If it's a timeout, return error instead of falling back
51
- if (err.message && /time.*out/i.test(err.message)) {
52
- return { success: false, error: `Timeout waiting for PIE to start: ${err.message}` };
53
+ const errMsg = err instanceof Error ? err.message : String(err);
54
+ if (errMsg && /time.*out/i.test(errMsg)) {
55
+ return { success: false, error: `Timeout waiting for PIE to start: ${errMsg}` };
53
56
  }
54
57
 
55
58
  // Fallback to console commands if automation bridge is unavailable or fails (non-timeout)
@@ -62,10 +65,10 @@ export class EditorTools extends BaseTool implements IEditorTools {
62
65
  }
63
66
  }
64
67
 
65
- async stopPlayInEditor() {
68
+ async stopPlayInEditor(): Promise<StandardActionResponse> {
66
69
  try {
67
70
  try {
68
- const response = await this.sendAutomationRequest(
71
+ const response = await this.sendAutomationRequest<EditorResponse>(
69
72
  'control_editor',
70
73
  { action: 'stop' },
71
74
  { timeoutMs: 30000 }
@@ -92,7 +95,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
92
95
  }
93
96
  }
94
97
 
95
- async pausePlayInEditor() {
98
+ async pausePlayInEditor(): Promise<StandardActionResponse> {
96
99
  try {
97
100
  // Pause/Resume PIE
98
101
  await this.bridge.executeConsoleCommand('pause');
@@ -103,11 +106,11 @@ export class EditorTools extends BaseTool implements IEditorTools {
103
106
  }
104
107
 
105
108
  // Alias for consistency with naming convention
106
- async pauseInEditor() {
109
+ async pauseInEditor(): Promise<StandardActionResponse> {
107
110
  return this.pausePlayInEditor();
108
111
  }
109
112
 
110
- async buildLighting() {
113
+ async buildLighting(): Promise<StandardActionResponse> {
111
114
  try {
112
115
  // Use console command to build lighting
113
116
  await this.bridge.executeConsoleCommand('BuildLighting');
@@ -125,12 +128,12 @@ export class EditorTools extends BaseTool implements IEditorTools {
125
128
  message?: string;
126
129
  }> {
127
130
  try {
128
- const resp = await this.sendAutomationRequest(
131
+ const resp = await this.sendAutomationRequest<EditorResponse>(
129
132
  'control_editor',
130
133
  { action: 'get_camera' },
131
134
  { timeoutMs: 3000 }
132
135
  );
133
- const result = resp?.result ?? resp;
136
+ const result: any = resp?.result ?? resp;
134
137
  const loc = result?.location ?? result?.camera?.location;
135
138
  const rot = result?.rotation ?? result?.camera?.rotation;
136
139
  const locArr: [number, number, number] | undefined = Array.isArray(loc) && loc.length === 3 ? [Number(loc[0]) || 0, Number(loc[1]) || 0, Number(loc[2]) || 0] : undefined;
@@ -144,7 +147,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
144
147
  }
145
148
  }
146
149
 
147
- async setViewportCamera(location?: { x: number; y: number; z: number } | [number, number, number] | null | undefined, rotation?: { pitch: number; yaw: number; roll: number } | [number, number, number] | null | undefined) {
150
+ async setViewportCamera(location?: { x: number; y: number; z: number } | [number, number, number] | null | undefined, rotation?: { pitch: number; yaw: number; roll: number } | [number, number, number] | null | undefined): Promise<StandardActionResponse> {
148
151
  // Special handling for when both location and rotation are missing/invalid
149
152
  // Allow rotation-only updates
150
153
  if (location === null) {
@@ -182,7 +185,18 @@ export class EditorTools extends BaseTool implements IEditorTools {
182
185
 
183
186
  // Use native control_editor.set_camera when available
184
187
  try {
185
- const resp = await this.sendAutomationRequest('control_editor', {
188
+ // Use WASM composeTransform for camera transform calculation
189
+ const locArray: [number, number, number] = location
190
+ ? [((location as any).x ?? (location as any)[0] ?? 0), ((location as any).y ?? (location as any)[1] ?? 0), ((location as any).z ?? (location as any)[2] ?? 0)]
191
+ : [0, 0, 0];
192
+ const rotArray: [number, number, number] = rotation
193
+ ? [((rotation as any).pitch ?? (rotation as any)[0] ?? 0), ((rotation as any).yaw ?? (rotation as any)[1] ?? 0), ((rotation as any).roll ?? (rotation as any)[2] ?? 0)]
194
+ : [0, 0, 0];
195
+ // Compose transform to validate and process camera positioning via WASM
196
+ wasmIntegration.composeTransform(locArray, rotArray, [1, 1, 1]);
197
+ // console.error('[WASM] Using composeTransform for camera positioning');
198
+
199
+ const resp = await this.sendAutomationRequest<EditorResponse>('control_editor', {
186
200
  action: 'set_camera',
187
201
  location: location as any,
188
202
  rotation: rotation as any
@@ -196,7 +210,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
196
210
  }
197
211
  }
198
212
 
199
- async setCameraSpeed(speed: number) {
213
+ async setCameraSpeed(speed: number): Promise<StandardActionResponse> {
200
214
  try {
201
215
  await this.bridge.executeConsoleCommand(`camspeed ${speed}`);
202
216
  return { success: true, message: `Camera speed set to ${speed}` };
@@ -205,7 +219,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
205
219
  }
206
220
  }
207
221
 
208
- async setFOV(fov: number) {
222
+ async setFOV(fov: number): Promise<StandardActionResponse> {
209
223
  try {
210
224
  await this.bridge.executeConsoleCommand(`fov ${fov}`);
211
225
  return { success: true, message: `FOV set to ${fov}` };
@@ -214,7 +228,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
214
228
  }
215
229
  }
216
230
 
217
- async takeScreenshot(filename?: string, resolution?: string) {
231
+ async takeScreenshot(filename?: string, resolution?: string): Promise<StandardActionResponse> {
218
232
  try {
219
233
  if (resolution && !/^\d+x\d+$/.test(resolution)) {
220
234
  return { success: false, error: 'Invalid resolution format. Use WxH (e.g. 1920x1080)' };
@@ -237,7 +251,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
237
251
  }
238
252
  }
239
253
 
240
- async resumePlayInEditor() {
254
+ async resumePlayInEditor(): Promise<StandardActionResponse> {
241
255
  try {
242
256
  // Use console command to toggle pause (resumes if paused)
243
257
  await this.bridge.executeConsoleCommand('pause');
@@ -250,7 +264,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
250
264
  }
251
265
  }
252
266
 
253
- async stepPIEFrame(steps: number = 1) {
267
+ async stepPIEFrame(steps: number = 1): Promise<StandardActionResponse> {
254
268
  const clampedSteps = Number.isFinite(steps) ? Math.max(1, Math.floor(steps)) : 1;
255
269
  try {
256
270
  // Use console command to step frames
@@ -267,7 +281,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
267
281
  }
268
282
  }
269
283
 
270
- async startRecording(options?: { filename?: string; frameRate?: number; durationSeconds?: number; metadata?: Record<string, unknown> }) {
284
+ async startRecording(options?: { filename?: string; frameRate?: number; durationSeconds?: number; metadata?: Record<string, unknown> }): Promise<StandardActionResponse> {
271
285
  const startedAt = Date.now();
272
286
  this.activeRecording = {
273
287
  name: typeof options?.filename === 'string' ? options.filename.trim() : undefined,
@@ -286,7 +300,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
286
300
  };
287
301
  }
288
302
 
289
- async stopRecording() {
303
+ async stopRecording(): Promise<StandardActionResponse> {
290
304
  if (!this.activeRecording) {
291
305
  return {
292
306
  success: true as const,
@@ -304,7 +318,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
304
318
  };
305
319
  }
306
320
 
307
- async createCameraBookmark(name: string) {
321
+ async createCameraBookmark(name: string): Promise<StandardActionResponse> {
308
322
  const trimmedName = name.trim();
309
323
  if (!trimmedName) {
310
324
  return { success: false as const, error: 'bookmarkName is required' };
@@ -335,7 +349,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
335
349
  };
336
350
  }
337
351
 
338
- async jumpToCameraBookmark(name: string) {
352
+ async jumpToCameraBookmark(name: string): Promise<StandardActionResponse> {
339
353
  const trimmedName = name.trim();
340
354
  if (!trimmedName) {
341
355
  return { success: false as const, error: 'bookmarkName is required' };
@@ -360,7 +374,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
360
374
  };
361
375
  }
362
376
 
363
- async setEditorPreferences(category: string | undefined, preferences: Record<string, unknown>) {
377
+ async setEditorPreferences(category: string | undefined, preferences: Record<string, unknown>): Promise<StandardActionResponse> {
364
378
  const resolvedCategory = typeof category === 'string' && category.trim().length > 0 ? category.trim() : 'General';
365
379
  const existing = this.editorPreferences.get(resolvedCategory) ?? {};
366
380
  this.editorPreferences.set(resolvedCategory, { ...existing, ...preferences });
@@ -372,7 +386,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
372
386
  };
373
387
  }
374
388
 
375
- async setViewportResolution(width: number, height: number) {
389
+ async setViewportResolution(width: number, height: number): Promise<StandardActionResponse> {
376
390
  try {
377
391
  // Clamp to reasonable limits
378
392
  const clampedWidth = Math.max(320, Math.min(7680, width));
@@ -393,7 +407,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
393
407
  }
394
408
  }
395
409
 
396
- async executeConsoleCommand(command: string) {
410
+ async executeConsoleCommand(command: string): Promise<StandardActionResponse> {
397
411
  try {
398
412
  // Sanitize and validate command
399
413
  if (!command || typeof command !== 'string') {
@@ -3,15 +3,9 @@ import path from 'node:path';
3
3
  import { AutomationBridge } from '../automation/index.js';
4
4
  import { UnrealBridge } from '../unreal-bridge.js';
5
5
  import { DEFAULT_SKYLIGHT_INTENSITY, DEFAULT_SUN_INTENSITY, DEFAULT_TIME_OF_DAY } from '../constants.js';
6
+ import { IEnvironmentTools, StandardActionResponse } from '../types/tool-interfaces.js';
6
7
 
7
- interface EnvironmentResult {
8
- success: boolean;
9
- message?: string;
10
- error?: string;
11
- details?: Record<string, unknown>;
12
- }
13
-
14
- export class EnvironmentTools {
8
+ export class EnvironmentTools implements IEnvironmentTools {
15
9
  constructor(_bridge: UnrealBridge, private automationBridge?: AutomationBridge) { }
16
10
 
17
11
  setAutomationBridge(automationBridge?: AutomationBridge) {
@@ -69,7 +63,7 @@ export class EnvironmentTools {
69
63
  return num;
70
64
  }
71
65
 
72
- private async invoke(action: string, payload: Record<string, unknown>): Promise<EnvironmentResult> {
66
+ private async invoke(action: string, payload: Record<string, unknown>): Promise<StandardActionResponse> {
73
67
  try {
74
68
  const bridge = this.ensureAutomationBridge();
75
69
  const response = await bridge.sendAutomationRequest(action, payload, { timeoutMs: 40000 });
@@ -79,42 +73,42 @@ export class EnvironmentTools {
79
73
  error: typeof response.error === 'string' && response.error.length > 0 ? response.error : 'ENVIRONMENT_CONTROL_FAILED',
80
74
  message: typeof response.message === 'string' ? response.message : undefined,
81
75
  details: typeof response.result === 'object' && response.result ? response.result as Record<string, unknown> : undefined
82
- };
76
+ } as StandardActionResponse;
83
77
  }
84
78
  const resultPayload = typeof response.result === 'object' && response.result ? response.result as Record<string, unknown> : undefined;
85
79
  return {
86
80
  success: true,
87
81
  message: typeof response.message === 'string' ? response.message : 'Environment control action succeeded',
88
82
  details: resultPayload
89
- };
83
+ } as StandardActionResponse;
90
84
  } catch (error) {
91
85
  const message = error instanceof Error ? error.message : String(error);
92
86
  return {
93
87
  success: false,
94
88
  error: message || 'ENVIRONMENT_CONTROL_FAILED'
95
- };
89
+ } as StandardActionResponse;
96
90
  }
97
91
  }
98
92
 
99
- async setTimeOfDay(hour: unknown): Promise<EnvironmentResult> {
93
+ async setTimeOfDay(hour: unknown): Promise<StandardActionResponse> {
100
94
  const normalizedHour = this.normalizeNumber(hour, 'hour', this.getDefaultTimeOfDay());
101
95
  const clampedHour = Math.min(Math.max(normalizedHour, 0), 24);
102
96
  return this.invoke('set_time_of_day', { hour: clampedHour });
103
97
  }
104
98
 
105
- async setSunIntensity(intensity: unknown): Promise<EnvironmentResult> {
99
+ async setSunIntensity(intensity: unknown): Promise<StandardActionResponse> {
106
100
  const normalized = this.normalizeNumber(intensity, 'intensity', this.getDefaultSunIntensity());
107
101
  const finalValue = Math.max(normalized, 0);
108
102
  return this.invoke('set_sun_intensity', { intensity: finalValue });
109
103
  }
110
104
 
111
- async setSkylightIntensity(intensity: unknown): Promise<EnvironmentResult> {
105
+ async setSkylightIntensity(intensity: unknown): Promise<StandardActionResponse> {
112
106
  const normalized = this.normalizeNumber(intensity, 'intensity', this.getDefaultSkylightIntensity());
113
107
  const finalValue = Math.max(normalized, 0);
114
108
  return this.invoke('set_skylight_intensity', { intensity: finalValue });
115
109
  }
116
110
 
117
- async exportSnapshot(params: { path?: unknown; filename?: unknown }): Promise<EnvironmentResult> {
111
+ async exportSnapshot(params: { path?: unknown; filename?: unknown }): Promise<StandardActionResponse> {
118
112
  try {
119
113
  const rawPath = typeof params?.path === 'string' && params.path.trim().length > 0
120
114
  ? params.path.trim()
@@ -157,17 +151,17 @@ export class EnvironmentTools {
157
151
  success: true,
158
152
  message: `Environment snapshot exported to ${targetPath}`,
159
153
  details: { path: targetPath }
160
- };
154
+ } as StandardActionResponse;
161
155
  } catch (error) {
162
156
  const message = error instanceof Error ? error.message : String(error);
163
157
  return {
164
158
  success: false,
165
159
  error: `Failed to export environment snapshot: ${message}`
166
- };
160
+ } as StandardActionResponse;
167
161
  }
168
162
  }
169
163
 
170
- async importSnapshot(params: { path?: unknown; filename?: unknown }): Promise<EnvironmentResult> {
164
+ async importSnapshot(params: { path?: unknown; filename?: unknown }): Promise<StandardActionResponse> {
171
165
  const rawPath = typeof params?.path === 'string' && params.path.trim().length > 0
172
166
  ? params.path.trim()
173
167
  : './tests/reports/env_snapshot.json';
@@ -210,7 +204,7 @@ export class EnvironmentTools {
210
204
  return {
211
205
  success: true,
212
206
  message: `Environment snapshot file not found at ${targetPath}; import treated as no-op`
213
- };
207
+ } as StandardActionResponse;
214
208
  }
215
209
  throw err;
216
210
  }
@@ -219,17 +213,17 @@ export class EnvironmentTools {
219
213
  success: true,
220
214
  message: `Environment snapshot import handled from ${targetPath}`,
221
215
  details: parsed && typeof parsed === 'object' ? parsed as Record<string, unknown> : undefined
222
- };
216
+ } as StandardActionResponse;
223
217
  } catch (error) {
224
218
  const message = error instanceof Error ? error.message : String(error);
225
219
  return {
226
220
  success: false,
227
221
  error: `Failed to import environment snapshot: ${message}`
228
- };
222
+ } as StandardActionResponse;
229
223
  }
230
224
  }
231
225
 
232
- async cleanup(params?: { names?: unknown; name?: unknown }): Promise<EnvironmentResult> {
226
+ async cleanup(params?: { names?: unknown; name?: unknown }): Promise<StandardActionResponse> {
233
227
  try {
234
228
  const rawNames = Array.isArray(params?.names) ? params.names : [];
235
229
  if (typeof params?.name === 'string' && params.name.trim().length > 0) {
@@ -244,7 +238,7 @@ export class EnvironmentTools {
244
238
  return {
245
239
  success: true,
246
240
  message: 'No environment actor names provided for cleanup; no-op'
247
- };
241
+ } as StandardActionResponse;
248
242
  }
249
243
 
250
244
  const bridge = this.ensureAutomationBridge();
@@ -264,7 +258,7 @@ export class EnvironmentTools {
264
258
  : 'Failed to delete environment actors'),
265
259
  message: typeof resp?.message === 'string' ? resp.message : undefined,
266
260
  details: result
267
- };
261
+ } as StandardActionResponse;
268
262
  }
269
263
 
270
264
  const result = resp && typeof resp.result === 'object' ? resp.result as Record<string, unknown> : undefined;
@@ -275,13 +269,13 @@ export class EnvironmentTools {
275
269
  ? resp.message
276
270
  : `Environment actors deleted: ${cleaned.join(', ')}`,
277
271
  details: result ?? { names: cleaned }
278
- };
272
+ } as StandardActionResponse;
279
273
  } catch (error) {
280
274
  const message = error instanceof Error ? error.message : String(error);
281
275
  return {
282
276
  success: false,
283
277
  error: `Failed to cleanup environment actors: ${message}`
284
- };
278
+ } as StandardActionResponse;
285
279
  }
286
280
  }
287
281
  }
@@ -2,8 +2,9 @@
2
2
  import { UnrealBridge } from '../unreal-bridge.js';
3
3
  import { AutomationBridge } from '../automation/index.js';
4
4
  import { coerceBoolean, coerceNumber, coerceString } from '../utils/result-helpers.js';
5
+ import { IFoliageTools, StandardActionResponse } from '../types/tool-interfaces.js';
5
6
 
6
- export class FoliageTools {
7
+ export class FoliageTools implements IFoliageTools {
7
8
  constructor(private bridge: UnrealBridge, private automationBridge?: AutomationBridge) { }
8
9
 
9
10
  setAutomationBridge(automationBridge?: AutomationBridge) { this.automationBridge = automationBridge; }
@@ -24,7 +25,7 @@ export class FoliageTools {
24
25
  alignToNormal?: boolean;
25
26
  randomYaw?: boolean;
26
27
  groundSlope?: number;
27
- }) {
28
+ }): Promise<StandardActionResponse> {
28
29
  // Basic validation to prevent bad inputs like 'undefined' and empty strings
29
30
  const errors: string[] = [];
30
31
  const name = String(params?.name ?? '').trim();
@@ -99,7 +100,7 @@ export class FoliageTools {
99
100
  message: exists
100
101
  ? `Foliage type '${name}' ready (${method})`
101
102
  : `Created foliage '${name}' but verification did not find it yet`
102
- };
103
+ } as StandardActionResponse;
103
104
  } catch (error) {
104
105
  return {
105
106
  success: false,
@@ -115,7 +116,7 @@ export class FoliageTools {
115
116
  brushSize?: number;
116
117
  paintDensity?: number;
117
118
  eraseMode?: boolean;
118
- }) {
119
+ }): Promise<StandardActionResponse> {
119
120
  const errors: string[] = [];
120
121
  const foliageType = String(params?.foliageType ?? '').trim();
121
122
  const pos = Array.isArray(params?.position) ? params.position : [0, 0, 0];
@@ -174,7 +175,7 @@ export class FoliageTools {
174
175
  added,
175
176
  note,
176
177
  message: `Painted ${added} instances for '${foliageType}' around (${pos[0]}, ${pos[1]}, ${pos[2]})`
177
- };
178
+ } as StandardActionResponse;
178
179
  } catch (error) {
179
180
  return {
180
181
  success: false,
@@ -184,7 +185,7 @@ export class FoliageTools {
184
185
  }
185
186
 
186
187
  // Query foliage instances (plugin-native)
187
- async getFoliageInstances(params: { foliageType?: string }) {
188
+ async getFoliageInstances(params: { foliageType?: string }): Promise<StandardActionResponse> {
188
189
  if (!this.automationBridge) {
189
190
  throw new Error('Automation Bridge not available. Foliage operations require plugin support.');
190
191
  }
@@ -200,15 +201,16 @@ export class FoliageTools {
200
201
  return {
201
202
  success: true,
202
203
  count: coerceNumber(payload.count) ?? 0,
203
- instances: (payload.instances as any[]) ?? []
204
- };
204
+ instances: (payload.instances as any[]) ?? [],
205
+ message: 'Foliage instances retrieved'
206
+ } as StandardActionResponse;
205
207
  } catch (error) {
206
208
  return { success: false, error: `Failed to get foliage instances: ${error instanceof Error ? error.message : String(error)}` };
207
209
  }
208
210
  }
209
211
 
210
212
  // Remove foliage (plugin-native)
211
- async removeFoliage(params: { foliageType?: string; removeAll?: boolean }) {
213
+ async removeFoliage(params: { foliageType?: string; removeAll?: boolean }): Promise<StandardActionResponse> {
212
214
  if (!this.automationBridge) {
213
215
  throw new Error('Automation Bridge not available. Foliage operations require plugin support.');
214
216
  }
@@ -224,8 +226,9 @@ export class FoliageTools {
224
226
  const payload = response.result as Record<string, unknown>;
225
227
  return {
226
228
  success: true,
227
- instancesRemoved: coerceNumber(payload.instancesRemoved) ?? 0
228
- };
229
+ instancesRemoved: coerceNumber(payload.instancesRemoved) ?? 0,
230
+ message: 'Foliage removed'
231
+ } as StandardActionResponse;
229
232
  } catch (error) {
230
233
  return { success: false, error: `Failed to remove foliage: ${error instanceof Error ? error.message : String(error)}` };
231
234
  }
@@ -242,7 +245,7 @@ export class FoliageTools {
242
245
  }>;
243
246
  enableCulling?: boolean;
244
247
  cullDistance?: number;
245
- }) {
248
+ }): Promise<StandardActionResponse> {
246
249
  const commands: string[] = [];
247
250
 
248
251
  commands.push(`CreateInstancedStaticMesh ${params.name} ${params.meshPath}`);
@@ -271,7 +274,7 @@ export class FoliageTools {
271
274
  foliageType: string;
272
275
  lodDistances?: number[];
273
276
  screenSize?: number[];
274
- }) {
277
+ }): Promise<StandardActionResponse> {
275
278
  const commands: string[] = [];
276
279
 
277
280
  if (params.lodDistances) {
@@ -288,7 +291,7 @@ export class FoliageTools {
288
291
  }
289
292
 
290
293
  // Alias for addFoliageType to match interface/handler usage
291
- async addFoliage(params: { foliageType: string; locations: Array<{ x: number; y: number; z: number }> }) {
294
+ async addFoliage(params: { foliageType: string; locations: Array<{ x: number; y: number; z: number }> }): Promise<StandardActionResponse> {
292
295
  // Delegate to paintFoliage which handles placing instances at locations
293
296
  if (params.locations && params.locations.length > 0) {
294
297
  if (!this.automationBridge) {
@@ -331,7 +334,7 @@ export class FoliageTools {
331
334
  size?: [number, number, number];
332
335
  seed?: number;
333
336
  tileSize?: number;
334
- }) {
337
+ }): Promise<StandardActionResponse> {
335
338
  if (!this.automationBridge) {
336
339
  throw new Error('Automation Bridge not available.');
337
340
  }
@@ -376,7 +379,7 @@ export class FoliageTools {
376
379
  volumeActor: result?.volume_actor,
377
380
  spawnerPath: result?.spawner_path,
378
381
  foliageTypesCount: result?.foliage_types_count
379
- };
382
+ } as StandardActionResponse;
380
383
  }
381
384
 
382
385
  /**
@@ -390,7 +393,7 @@ export class FoliageTools {
390
393
  rotation?: [number, number, number];
391
394
  scale?: [number, number, number];
392
395
  }>;
393
- }) {
396
+ }): Promise<StandardActionResponse> {
394
397
  if (!this.automationBridge) {
395
398
  throw new Error('Automation Bridge not available. Foliage instance placement requires plugin support.');
396
399
  }
@@ -417,7 +420,7 @@ export class FoliageTools {
417
420
  success: true,
418
421
  message: response.message || `Added ${result?.instances_count || params.transforms.length} foliage instances`,
419
422
  instancesCount: result?.instances_count
420
- };
423
+ } as StandardActionResponse;
421
424
  } catch (error) {
422
425
  return {
423
426
  success: false,
@@ -432,7 +435,7 @@ export class FoliageTools {
432
435
  collisionEnabled?: boolean;
433
436
  collisionProfile?: string;
434
437
  generateOverlapEvents?: boolean;
435
- }) {
438
+ }): Promise<StandardActionResponse> {
436
439
  const commands: string[] = [];
437
440
 
438
441
  if (params.collisionEnabled !== undefined) {
@@ -463,7 +466,7 @@ export class FoliageTools {
463
466
  }>;
464
467
  windStrength?: number;
465
468
  windSpeed?: number;
466
- }) {
469
+ }): Promise<StandardActionResponse> {
467
470
  const commands: string[] = [];
468
471
 
469
472
  commands.push(`CreateGrassSystem ${params.name}`);
@@ -492,7 +495,7 @@ export class FoliageTools {
492
495
  foliageType: string;
493
496
  position: [number, number, number];
494
497
  radius: number;
495
- }) {
498
+ }): Promise<StandardActionResponse> {
496
499
  const command = `RemoveFoliageInRadius ${params.foliageType} ${params.position.join(' ')} ${params.radius}`;
497
500
  return this.bridge.executeConsoleCommand(command);
498
501
  }
@@ -503,7 +506,7 @@ export class FoliageTools {
503
506
  position?: [number, number, number];
504
507
  radius?: number;
505
508
  selectAll?: boolean;
506
- }) {
509
+ }): Promise<StandardActionResponse> {
507
510
  let command: string;
508
511
 
509
512
  if (params.selectAll) {
@@ -523,7 +526,7 @@ export class FoliageTools {
523
526
  updateTransforms?: boolean;
524
527
  updateMesh?: boolean;
525
528
  newMeshPath?: string;
526
- }) {
529
+ }): Promise<StandardActionResponse> {
527
530
  const commands: string[] = [];
528
531
 
529
532
  if (params.updateTransforms) {
@@ -546,7 +549,7 @@ export class FoliageTools {
546
549
  name: string;
547
550
  spawnArea: 'Landscape' | 'StaticMesh' | 'BSP' | 'Foliage' | 'All';
548
551
  excludeAreas?: Array<[number, number, number, number]>; // [x, y, z, radius]
549
- }) {
552
+ }): Promise<StandardActionResponse> {
550
553
  const commands: string[] = [];
551
554
 
552
555
  commands.push(`CreateFoliageSpawner ${params.name} ${params.spawnArea}`);
@@ -568,7 +571,7 @@ export class FoliageTools {
568
571
  generateClusters?: boolean;
569
572
  clusterSize?: number;
570
573
  reduceDrawCalls?: boolean;
571
- }) {
574
+ }): Promise<StandardActionResponse> {
572
575
  const commands = [];
573
576
 
574
577
  if (params.mergeInstances) {