unreal-engine-mcp-server 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) 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 +1 -0
  6. package/.github/workflows/release-drafter.yml +1 -1
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +71 -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/config.d.ts +0 -1
  14. package/dist/config.js +0 -1
  15. package/dist/constants.d.ts +4 -0
  16. package/dist/constants.js +4 -0
  17. package/dist/graphql/loaders.d.ts +64 -0
  18. package/dist/graphql/loaders.js +117 -0
  19. package/dist/graphql/resolvers.d.ts +3 -3
  20. package/dist/graphql/resolvers.js +33 -30
  21. package/dist/graphql/server.js +3 -1
  22. package/dist/graphql/types.d.ts +2 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +13 -2
  25. package/dist/server-setup.d.ts +0 -1
  26. package/dist/server-setup.js +0 -40
  27. package/dist/tools/actors.d.ts +40 -24
  28. package/dist/tools/actors.js +8 -2
  29. package/dist/tools/assets.d.ts +19 -71
  30. package/dist/tools/assets.js +28 -22
  31. package/dist/tools/base-tool.d.ts +4 -4
  32. package/dist/tools/base-tool.js +1 -1
  33. package/dist/tools/blueprint.d.ts +33 -61
  34. package/dist/tools/consolidated-tool-handlers.js +96 -110
  35. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  36. package/dist/tools/dynamic-handler-registry.js +17 -95
  37. package/dist/tools/editor.d.ts +19 -193
  38. package/dist/tools/editor.js +8 -0
  39. package/dist/tools/environment.d.ts +8 -14
  40. package/dist/tools/foliage.d.ts +18 -143
  41. package/dist/tools/foliage.js +4 -2
  42. package/dist/tools/handlers/actor-handlers.js +0 -5
  43. package/dist/tools/handlers/asset-handlers.js +454 -454
  44. package/dist/tools/landscape.d.ts +16 -116
  45. package/dist/tools/landscape.js +7 -3
  46. package/dist/tools/level.d.ts +22 -103
  47. package/dist/tools/level.js +24 -16
  48. package/dist/tools/lighting.js +5 -1
  49. package/dist/tools/materials.js +5 -1
  50. package/dist/tools/niagara.js +37 -2
  51. package/dist/tools/performance.d.ts +0 -1
  52. package/dist/tools/performance.js +0 -1
  53. package/dist/tools/physics.js +5 -1
  54. package/dist/tools/sequence.d.ts +24 -24
  55. package/dist/tools/sequence.js +13 -0
  56. package/dist/tools/ui.d.ts +0 -2
  57. package/dist/types/automation-responses.d.ts +115 -0
  58. package/dist/types/automation-responses.js +2 -0
  59. package/dist/types/responses.d.ts +249 -0
  60. package/dist/types/responses.js +2 -0
  61. package/dist/types/tool-interfaces.d.ts +135 -135
  62. package/dist/utils/command-validator.js +3 -2
  63. package/dist/utils/path-security.d.ts +2 -0
  64. package/dist/utils/path-security.js +24 -0
  65. package/dist/utils/response-factory.d.ts +4 -4
  66. package/dist/utils/response-factory.js +15 -21
  67. package/docs/Migration-Guide-v0.5.0.md +1 -9
  68. package/docs/testing-guide.md +2 -2
  69. package/package.json +12 -6
  70. package/scripts/run-all-tests.mjs +25 -20
  71. package/server.json +3 -2
  72. package/src/config.ts +1 -1
  73. package/src/constants.ts +7 -0
  74. package/src/graphql/loaders.ts +244 -0
  75. package/src/graphql/resolvers.ts +47 -49
  76. package/src/graphql/server.ts +3 -1
  77. package/src/graphql/types.ts +3 -0
  78. package/src/index.ts +15 -2
  79. package/src/resources/assets.ts +5 -4
  80. package/src/server-setup.ts +3 -37
  81. package/src/tools/actors.ts +36 -28
  82. package/src/tools/animation.ts +1 -0
  83. package/src/tools/assets.ts +74 -63
  84. package/src/tools/base-tool.ts +3 -3
  85. package/src/tools/blueprint.ts +59 -59
  86. package/src/tools/consolidated-tool-handlers.ts +129 -150
  87. package/src/tools/dynamic-handler-registry.ts +22 -140
  88. package/src/tools/editor.ts +39 -26
  89. package/src/tools/environment.ts +21 -27
  90. package/src/tools/foliage.ts +28 -25
  91. package/src/tools/handlers/actor-handlers.ts +2 -8
  92. package/src/tools/handlers/asset-handlers.ts +484 -484
  93. package/src/tools/handlers/sequence-handlers.ts +1 -1
  94. package/src/tools/landscape.ts +34 -28
  95. package/src/tools/level.ts +96 -76
  96. package/src/tools/lighting.ts +6 -1
  97. package/src/tools/materials.ts +8 -2
  98. package/src/tools/niagara.ts +44 -2
  99. package/src/tools/performance.ts +1 -2
  100. package/src/tools/physics.ts +7 -1
  101. package/src/tools/sequence.ts +41 -25
  102. package/src/tools/ui.ts +0 -2
  103. package/src/types/automation-responses.ts +119 -0
  104. package/src/types/responses.ts +355 -0
  105. package/src/types/tool-interfaces.ts +135 -135
  106. package/src/utils/command-validator.ts +3 -2
  107. package/src/utils/normalize.test.ts +162 -0
  108. package/src/utils/path-security.ts +43 -0
  109. package/src/utils/response-factory.ts +29 -24
  110. package/src/utils/safe-json.test.ts +90 -0
  111. package/src/utils/validation.test.ts +184 -0
  112. package/tests/test-animation.mjs +358 -33
  113. package/tests/test-asset-graph.mjs +311 -0
  114. package/tests/test-audio.mjs +314 -116
  115. package/tests/test-behavior-tree.mjs +327 -144
  116. package/tests/test-blueprint-graph.mjs +343 -12
  117. package/tests/test-control-editor.mjs +85 -53
  118. package/tests/test-graphql.mjs +58 -8
  119. package/tests/test-input.mjs +349 -0
  120. package/tests/test-inspect.mjs +291 -61
  121. package/tests/test-landscape.mjs +304 -48
  122. package/tests/test-lighting.mjs +428 -0
  123. package/tests/test-manage-level.mjs +70 -51
  124. package/tests/test-performance.mjs +539 -0
  125. package/tests/test-sequence.mjs +82 -46
  126. package/tests/test-system.mjs +72 -33
  127. package/tests/test-wasm.mjs +98 -8
  128. package/vitest.config.ts +35 -0
  129. package/.github/release-drafter.yml +0 -148
  130. package/dist/prompts/index.d.ts +0 -21
  131. package/dist/prompts/index.js +0 -217
  132. package/dist/tools/blueprint/helpers.d.ts +0 -29
  133. package/dist/tools/blueprint/helpers.js +0 -182
  134. package/src/prompts/index.ts +0 -249
  135. package/src/tools/blueprint/helpers.ts +0 -189
  136. package/tests/test-blueprint-events.mjs +0 -35
  137. package/tests/test-extra-tools.mjs +0 -38
  138. package/tests/test-render.mjs +0 -33
  139. 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 }
@@ -62,10 +64,10 @@ export class EditorTools extends BaseTool implements IEditorTools {
62
64
  }
63
65
  }
64
66
 
65
- async stopPlayInEditor() {
67
+ async stopPlayInEditor(): Promise<StandardActionResponse> {
66
68
  try {
67
69
  try {
68
- const response = await this.sendAutomationRequest(
70
+ const response = await this.sendAutomationRequest<EditorResponse>(
69
71
  'control_editor',
70
72
  { action: 'stop' },
71
73
  { timeoutMs: 30000 }
@@ -92,7 +94,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
92
94
  }
93
95
  }
94
96
 
95
- async pausePlayInEditor() {
97
+ async pausePlayInEditor(): Promise<StandardActionResponse> {
96
98
  try {
97
99
  // Pause/Resume PIE
98
100
  await this.bridge.executeConsoleCommand('pause');
@@ -103,11 +105,11 @@ export class EditorTools extends BaseTool implements IEditorTools {
103
105
  }
104
106
 
105
107
  // Alias for consistency with naming convention
106
- async pauseInEditor() {
108
+ async pauseInEditor(): Promise<StandardActionResponse> {
107
109
  return this.pausePlayInEditor();
108
110
  }
109
111
 
110
- async buildLighting() {
112
+ async buildLighting(): Promise<StandardActionResponse> {
111
113
  try {
112
114
  // Use console command to build lighting
113
115
  await this.bridge.executeConsoleCommand('BuildLighting');
@@ -125,12 +127,12 @@ export class EditorTools extends BaseTool implements IEditorTools {
125
127
  message?: string;
126
128
  }> {
127
129
  try {
128
- const resp = await this.sendAutomationRequest(
130
+ const resp = await this.sendAutomationRequest<EditorResponse>(
129
131
  'control_editor',
130
132
  { action: 'get_camera' },
131
133
  { timeoutMs: 3000 }
132
134
  );
133
- const result = resp?.result ?? resp;
135
+ const result: any = resp?.result ?? resp;
134
136
  const loc = result?.location ?? result?.camera?.location;
135
137
  const rot = result?.rotation ?? result?.camera?.rotation;
136
138
  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 +146,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
144
146
  }
145
147
  }
146
148
 
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) {
149
+ 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
150
  // Special handling for when both location and rotation are missing/invalid
149
151
  // Allow rotation-only updates
150
152
  if (location === null) {
@@ -182,7 +184,18 @@ export class EditorTools extends BaseTool implements IEditorTools {
182
184
 
183
185
  // Use native control_editor.set_camera when available
184
186
  try {
185
- const resp = await this.sendAutomationRequest('control_editor', {
187
+ // Use WASM composeTransform for camera transform calculation
188
+ const locArray: [number, number, number] = location
189
+ ? [((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)]
190
+ : [0, 0, 0];
191
+ const rotArray: [number, number, number] = rotation
192
+ ? [((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)]
193
+ : [0, 0, 0];
194
+ // Compose transform to validate and process camera positioning via WASM
195
+ wasmIntegration.composeTransform(locArray, rotArray, [1, 1, 1]);
196
+ // console.error('[WASM] Using composeTransform for camera positioning');
197
+
198
+ const resp = await this.sendAutomationRequest<EditorResponse>('control_editor', {
186
199
  action: 'set_camera',
187
200
  location: location as any,
188
201
  rotation: rotation as any
@@ -196,7 +209,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
196
209
  }
197
210
  }
198
211
 
199
- async setCameraSpeed(speed: number) {
212
+ async setCameraSpeed(speed: number): Promise<StandardActionResponse> {
200
213
  try {
201
214
  await this.bridge.executeConsoleCommand(`camspeed ${speed}`);
202
215
  return { success: true, message: `Camera speed set to ${speed}` };
@@ -205,7 +218,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
205
218
  }
206
219
  }
207
220
 
208
- async setFOV(fov: number) {
221
+ async setFOV(fov: number): Promise<StandardActionResponse> {
209
222
  try {
210
223
  await this.bridge.executeConsoleCommand(`fov ${fov}`);
211
224
  return { success: true, message: `FOV set to ${fov}` };
@@ -214,7 +227,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
214
227
  }
215
228
  }
216
229
 
217
- async takeScreenshot(filename?: string, resolution?: string) {
230
+ async takeScreenshot(filename?: string, resolution?: string): Promise<StandardActionResponse> {
218
231
  try {
219
232
  if (resolution && !/^\d+x\d+$/.test(resolution)) {
220
233
  return { success: false, error: 'Invalid resolution format. Use WxH (e.g. 1920x1080)' };
@@ -237,7 +250,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
237
250
  }
238
251
  }
239
252
 
240
- async resumePlayInEditor() {
253
+ async resumePlayInEditor(): Promise<StandardActionResponse> {
241
254
  try {
242
255
  // Use console command to toggle pause (resumes if paused)
243
256
  await this.bridge.executeConsoleCommand('pause');
@@ -250,7 +263,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
250
263
  }
251
264
  }
252
265
 
253
- async stepPIEFrame(steps: number = 1) {
266
+ async stepPIEFrame(steps: number = 1): Promise<StandardActionResponse> {
254
267
  const clampedSteps = Number.isFinite(steps) ? Math.max(1, Math.floor(steps)) : 1;
255
268
  try {
256
269
  // Use console command to step frames
@@ -267,7 +280,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
267
280
  }
268
281
  }
269
282
 
270
- async startRecording(options?: { filename?: string; frameRate?: number; durationSeconds?: number; metadata?: Record<string, unknown> }) {
283
+ async startRecording(options?: { filename?: string; frameRate?: number; durationSeconds?: number; metadata?: Record<string, unknown> }): Promise<StandardActionResponse> {
271
284
  const startedAt = Date.now();
272
285
  this.activeRecording = {
273
286
  name: typeof options?.filename === 'string' ? options.filename.trim() : undefined,
@@ -286,7 +299,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
286
299
  };
287
300
  }
288
301
 
289
- async stopRecording() {
302
+ async stopRecording(): Promise<StandardActionResponse> {
290
303
  if (!this.activeRecording) {
291
304
  return {
292
305
  success: true as const,
@@ -304,7 +317,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
304
317
  };
305
318
  }
306
319
 
307
- async createCameraBookmark(name: string) {
320
+ async createCameraBookmark(name: string): Promise<StandardActionResponse> {
308
321
  const trimmedName = name.trim();
309
322
  if (!trimmedName) {
310
323
  return { success: false as const, error: 'bookmarkName is required' };
@@ -335,7 +348,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
335
348
  };
336
349
  }
337
350
 
338
- async jumpToCameraBookmark(name: string) {
351
+ async jumpToCameraBookmark(name: string): Promise<StandardActionResponse> {
339
352
  const trimmedName = name.trim();
340
353
  if (!trimmedName) {
341
354
  return { success: false as const, error: 'bookmarkName is required' };
@@ -360,7 +373,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
360
373
  };
361
374
  }
362
375
 
363
- async setEditorPreferences(category: string | undefined, preferences: Record<string, unknown>) {
376
+ async setEditorPreferences(category: string | undefined, preferences: Record<string, unknown>): Promise<StandardActionResponse> {
364
377
  const resolvedCategory = typeof category === 'string' && category.trim().length > 0 ? category.trim() : 'General';
365
378
  const existing = this.editorPreferences.get(resolvedCategory) ?? {};
366
379
  this.editorPreferences.set(resolvedCategory, { ...existing, ...preferences });
@@ -372,7 +385,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
372
385
  };
373
386
  }
374
387
 
375
- async setViewportResolution(width: number, height: number) {
388
+ async setViewportResolution(width: number, height: number): Promise<StandardActionResponse> {
376
389
  try {
377
390
  // Clamp to reasonable limits
378
391
  const clampedWidth = Math.max(320, Math.min(7680, width));
@@ -393,7 +406,7 @@ export class EditorTools extends BaseTool implements IEditorTools {
393
406
  }
394
407
  }
395
408
 
396
- async executeConsoleCommand(command: string) {
409
+ async executeConsoleCommand(command: string): Promise<StandardActionResponse> {
397
410
  try {
398
411
  // Sanitize and validate command
399
412
  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) {
@@ -1,13 +1,10 @@
1
1
  import { cleanObject } from '../../utils/safe-json.js';
2
2
  import { ITools } from '../../types/tool-interfaces.js';
3
3
  import { executeAutomationRequest } from './common-handlers.js';
4
- import { Logger } from '../../utils/logger.js';
5
4
  import { normalizeArgs } from './argument-helper.js';
6
5
 
7
6
  type ActorActionHandler = (args: any, tools: ITools) => Promise<any>;
8
7
 
9
- const logger = new Logger('ActorHandlers');
10
-
11
8
  const handlers: Record<string, ActorActionHandler> = {
12
9
  spawn: async (args, tools) => {
13
10
  // Class name aliases for user-friendly names
@@ -35,7 +32,7 @@ const handlers: Record<string, ActorActionHandler> = {
35
32
  { key: 'actorName', aliases: ['name'] },
36
33
  { key: 'timeoutMs', default: undefined }
37
34
  ]);
38
-
35
+
39
36
  const timeoutMs = typeof params.timeoutMs === 'number' ? params.timeoutMs : undefined;
40
37
 
41
38
  // Extremely small timeouts are treated as an immediate timeout-style
@@ -113,7 +110,6 @@ const handlers: Record<string, ActorActionHandler> = {
113
110
  // Auto-enable physics logic
114
111
  const compsResult = await tools.actorTools.getComponents(params.actorName);
115
112
  if (compsResult && compsResult.success && Array.isArray(compsResult.components)) {
116
- logger.debug('Components found:', JSON.stringify(compsResult.components));
117
113
  const meshComp = compsResult.components.find((c: any) => {
118
114
  const name = c.name || c;
119
115
  const match = typeof name === 'string' && (
@@ -121,13 +117,11 @@ const handlers: Record<string, ActorActionHandler> = {
121
117
  name.toLowerCase().includes('mesh') ||
122
118
  name.toLowerCase().includes('primitive')
123
119
  );
124
- logger.debug(`Checking component '${name}' matches? ${match}`);
125
120
  return match;
126
121
  });
127
122
 
128
123
  if (meshComp) {
129
124
  const compName = meshComp.name || meshComp;
130
- logger.debug(`Auto-enabling physics for component: ${compName}`); // Debug log
131
125
  await tools.actorTools.setComponentProperties({
132
126
  actorName: params.actorName,
133
127
  componentName: compName,
@@ -253,7 +247,7 @@ const handlers: Record<string, ActorActionHandler> = {
253
247
  const params = normalizeArgs(args, [
254
248
  { key: 'name', aliases: ['actorName', 'query'], required: true }
255
249
  ]);
256
-
250
+
257
251
  // Use the plugin's fuzzy query endpoint (contains-match) instead of the
258
252
  // exact lookup endpoint. This improves "spawn then find" reliability.
259
253
  return tools.actorTools.findByName(params.name);