unreal-engine-mcp-server 0.3.1 → 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.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +22 -7
- package/dist/index.js +137 -46
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.d.ts +3 -2
- package/dist/resources/assets.js +117 -109
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +31 -56
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +58 -46
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +236 -87
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +124 -255
- package/dist/tools/consolidated-tool-handlers.js +749 -766
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +170 -36
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +98 -24
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +146 -24
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.d.ts +6 -1
- package/dist/utils/response-validator.js +66 -13
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +11 -10
- package/server.json +37 -14
- package/src/index.ts +146 -50
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +123 -102
- package/src/resources/levels.ts +37 -47
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +59 -45
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +312 -106
- package/src/tools/consolidated-tool-definitions.ts +136 -267
- package/src/tools/consolidated-tool-handlers.ts +871 -795
- package/src/tools/debug.ts +179 -38
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +103 -25
- package/src/tools/sequence.ts +157 -30
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +68 -17
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
|
@@ -1,10 +1,132 @@
|
|
|
1
1
|
// Consolidated tool handlers - maps 13 tools to all 36 operations
|
|
2
|
-
import { handleToolCall } from './tool-handlers.js';
|
|
3
2
|
import { cleanObject } from '../utils/safe-json.js';
|
|
4
3
|
import { Logger } from '../utils/logger.js';
|
|
5
4
|
|
|
6
5
|
const log = new Logger('ConsolidatedToolHandler');
|
|
7
6
|
|
|
7
|
+
const ACTION_REQUIRED_ERROR = 'Missing required parameter: action';
|
|
8
|
+
|
|
9
|
+
function ensureArgsPresent(args: any) {
|
|
10
|
+
if (args === null || args === undefined) {
|
|
11
|
+
throw new Error('Invalid arguments: null or undefined');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requireAction(args: any): string {
|
|
16
|
+
ensureArgsPresent(args);
|
|
17
|
+
const action = args.action;
|
|
18
|
+
if (typeof action !== 'string' || action.trim() === '') {
|
|
19
|
+
throw new Error(ACTION_REQUIRED_ERROR);
|
|
20
|
+
}
|
|
21
|
+
return action;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requireNonEmptyString(value: any, field: string, message?: string): string {
|
|
25
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
26
|
+
throw new Error(message ?? `Invalid ${field}: must be a non-empty string`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function requirePositiveNumber(value: any, field: string, message?: string): number {
|
|
32
|
+
if (typeof value !== 'number' || !isFinite(value) || value <= 0) {
|
|
33
|
+
throw new Error(message ?? `Invalid ${field}: must be a positive number`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireVector3Components(
|
|
39
|
+
vector: any,
|
|
40
|
+
message: string
|
|
41
|
+
): [number, number, number] {
|
|
42
|
+
if (
|
|
43
|
+
!vector ||
|
|
44
|
+
typeof vector.x !== 'number' ||
|
|
45
|
+
typeof vector.y !== 'number' ||
|
|
46
|
+
typeof vector.z !== 'number'
|
|
47
|
+
) {
|
|
48
|
+
throw new Error(message);
|
|
49
|
+
}
|
|
50
|
+
return [vector.x, vector.y, vector.z];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getElicitationTimeoutMs(tools: any): number | undefined {
|
|
54
|
+
if (!tools) return undefined;
|
|
55
|
+
const direct = tools.elicitationTimeoutMs;
|
|
56
|
+
if (typeof direct === 'number' && Number.isFinite(direct)) {
|
|
57
|
+
return direct;
|
|
58
|
+
}
|
|
59
|
+
if (typeof tools.getElicitationTimeoutMs === 'function') {
|
|
60
|
+
const value = tools.getElicitationTimeoutMs();
|
|
61
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function elicitMissingPrimitiveArgs(
|
|
69
|
+
tools: any,
|
|
70
|
+
args: any,
|
|
71
|
+
prompt: string,
|
|
72
|
+
fieldSchemas: Record<string, { type: 'string' | 'number' | 'integer' | 'boolean'; title?: string; description?: string; enum?: string[]; enumNames?: string[]; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; pattern?: string; format?: string; default?: unknown }>
|
|
73
|
+
) {
|
|
74
|
+
if (
|
|
75
|
+
!tools ||
|
|
76
|
+
typeof tools.supportsElicitation !== 'function' ||
|
|
77
|
+
!tools.supportsElicitation() ||
|
|
78
|
+
typeof tools.elicit !== 'function'
|
|
79
|
+
) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const properties: Record<string, any> = {};
|
|
84
|
+
const required: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const [key, schema] of Object.entries(fieldSchemas)) {
|
|
87
|
+
const value = args?.[key];
|
|
88
|
+
const missing =
|
|
89
|
+
value === undefined ||
|
|
90
|
+
value === null ||
|
|
91
|
+
(typeof value === 'string' && value.trim() === '');
|
|
92
|
+
if (missing) {
|
|
93
|
+
properties[key] = schema;
|
|
94
|
+
required.push(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (required.length === 0) return;
|
|
99
|
+
|
|
100
|
+
const timeoutMs = getElicitationTimeoutMs(tools);
|
|
101
|
+
const options: any = {
|
|
102
|
+
fallback: async () => ({ ok: false, error: 'missing-params' })
|
|
103
|
+
};
|
|
104
|
+
if (typeof timeoutMs === 'number') {
|
|
105
|
+
options.timeoutMs = timeoutMs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const elicited = await tools.elicit(
|
|
110
|
+
prompt,
|
|
111
|
+
{ type: 'object', properties, required },
|
|
112
|
+
options
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (elicited?.ok && elicited.value) {
|
|
116
|
+
for (const key of required) {
|
|
117
|
+
const value = elicited.value[key];
|
|
118
|
+
if (value === undefined || value === null) continue;
|
|
119
|
+
args[key] = typeof value === 'string' ? value.trim() : value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
log.debug('Special elicitation fallback skipped', {
|
|
124
|
+
prompt,
|
|
125
|
+
err: (err as any)?.message || String(err)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
8
130
|
export async function handleConsolidatedToolCall(
|
|
9
131
|
name: string,
|
|
10
132
|
args: any,
|
|
@@ -15,842 +137,810 @@ export async function handleConsolidatedToolCall(
|
|
|
15
137
|
log.debug(`Starting execution of ${name} at ${new Date().toISOString()}`);
|
|
16
138
|
|
|
17
139
|
try {
|
|
18
|
-
|
|
19
|
-
if (args === null || args === undefined) {
|
|
20
|
-
throw new Error('Invalid arguments: null or undefined');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let mappedName: string;
|
|
24
|
-
let mappedArgs: any = { ...args };
|
|
140
|
+
ensureArgsPresent(args);
|
|
25
141
|
|
|
26
142
|
switch (name) {
|
|
27
143
|
// 1. ASSET MANAGER
|
|
28
144
|
case 'manage_asset':
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Validate action exists
|
|
35
|
-
if (!args.action) {
|
|
36
|
-
throw new Error('Missing required parameter: action');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
switch (args.action) {
|
|
40
|
-
case 'list':
|
|
41
|
-
// Directory is optional
|
|
42
|
-
if (args.directory !== undefined && args.directory !== null) {
|
|
43
|
-
if (typeof args.directory !== 'string') {
|
|
44
|
-
throw new Error('Invalid directory: must be a string');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
mappedName = 'list_assets';
|
|
49
|
-
mappedArgs = {
|
|
50
|
-
directory: args.directory
|
|
51
|
-
// recursive removed - always false internally
|
|
52
|
-
};
|
|
53
|
-
break;
|
|
54
|
-
|
|
55
|
-
case 'import':
|
|
56
|
-
// Validate required parameters
|
|
57
|
-
if (args.sourcePath === undefined || args.sourcePath === null) {
|
|
58
|
-
throw new Error('Missing required parameter: sourcePath');
|
|
59
|
-
}
|
|
60
|
-
if (typeof args.sourcePath !== 'string') {
|
|
61
|
-
throw new Error('Invalid sourcePath: must be a string');
|
|
62
|
-
}
|
|
63
|
-
if (args.sourcePath.trim() === '') {
|
|
64
|
-
throw new Error('Invalid sourcePath: cannot be empty');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (args.destinationPath === undefined || args.destinationPath === null) {
|
|
68
|
-
throw new Error('Missing required parameter: destinationPath');
|
|
69
|
-
}
|
|
70
|
-
if (typeof args.destinationPath !== 'string') {
|
|
71
|
-
throw new Error('Invalid destinationPath: must be a string');
|
|
72
|
-
}
|
|
73
|
-
if (args.destinationPath.trim() === '') {
|
|
74
|
-
throw new Error('Invalid destinationPath: cannot be empty');
|
|
145
|
+
switch (requireAction(args)) {
|
|
146
|
+
case 'list': {
|
|
147
|
+
if (args.directory !== undefined && args.directory !== null && typeof args.directory !== 'string') {
|
|
148
|
+
throw new Error('Invalid directory: must be a string');
|
|
75
149
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
150
|
+
const res = await tools.assetResources.list(args.directory || '/Game', false);
|
|
151
|
+
return cleanObject({ success: true, ...res });
|
|
152
|
+
}
|
|
153
|
+
case 'import': {
|
|
154
|
+
let sourcePath = typeof args.sourcePath === 'string' ? args.sourcePath.trim() : '';
|
|
155
|
+
let destinationPath = typeof args.destinationPath === 'string' ? args.destinationPath.trim() : '';
|
|
156
|
+
|
|
157
|
+
if ((!sourcePath || !destinationPath) && typeof tools.supportsElicitation === 'function' && tools.supportsElicitation() && typeof tools.elicit === 'function') {
|
|
158
|
+
const schemaProps: Record<string, any> = {};
|
|
159
|
+
const required: string[] = [];
|
|
160
|
+
|
|
161
|
+
if (!sourcePath) {
|
|
162
|
+
schemaProps.sourcePath = {
|
|
163
|
+
type: 'string',
|
|
164
|
+
title: 'Source File Path',
|
|
165
|
+
description: 'Full path to the asset file on disk to import'
|
|
166
|
+
};
|
|
167
|
+
required.push('sourcePath');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!destinationPath) {
|
|
171
|
+
schemaProps.destinationPath = {
|
|
172
|
+
type: 'string',
|
|
173
|
+
title: 'Destination Path',
|
|
174
|
+
description: 'Unreal content path where the asset should be imported (e.g., /Game/MCP/Assets)'
|
|
175
|
+
};
|
|
176
|
+
required.push('destinationPath');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (required.length > 0) {
|
|
180
|
+
const timeoutMs = getElicitationTimeoutMs(tools);
|
|
181
|
+
const options: any = { fallback: async () => ({ ok: false, error: 'missing-import-params' }) };
|
|
182
|
+
if (typeof timeoutMs === 'number') {
|
|
183
|
+
options.timeoutMs = timeoutMs;
|
|
184
|
+
}
|
|
185
|
+
const elicited = await tools.elicit(
|
|
186
|
+
'Provide the missing import parameters for manage_asset.import',
|
|
187
|
+
{ type: 'object', properties: schemaProps, required },
|
|
188
|
+
options
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (elicited?.ok && elicited.value) {
|
|
192
|
+
if (typeof elicited.value.sourcePath === 'string') {
|
|
193
|
+
sourcePath = elicited.value.sourcePath.trim();
|
|
194
|
+
}
|
|
195
|
+
if (typeof elicited.value.destinationPath === 'string') {
|
|
196
|
+
destinationPath = elicited.value.destinationPath.trim();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
100
199
|
}
|
|
101
200
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
201
|
+
|
|
202
|
+
const sourcePathValidated = requireNonEmptyString(sourcePath || args.sourcePath, 'sourcePath', 'Invalid sourcePath');
|
|
203
|
+
const destinationPathValidated = requireNonEmptyString(destinationPath || args.destinationPath, 'destinationPath', 'Invalid destinationPath');
|
|
204
|
+
const res = await tools.assetTools.importAsset(sourcePathValidated, destinationPathValidated);
|
|
205
|
+
return cleanObject(res);
|
|
206
|
+
}
|
|
207
|
+
case 'create_material': {
|
|
208
|
+
await elicitMissingPrimitiveArgs(
|
|
209
|
+
tools,
|
|
210
|
+
args,
|
|
211
|
+
'Provide the material details for manage_asset.create_material',
|
|
212
|
+
{
|
|
213
|
+
name: {
|
|
214
|
+
type: 'string',
|
|
215
|
+
title: 'Material Name',
|
|
216
|
+
description: 'Name for the new material asset'
|
|
217
|
+
},
|
|
218
|
+
path: {
|
|
219
|
+
type: 'string',
|
|
220
|
+
title: 'Save Path',
|
|
221
|
+
description: 'Optional Unreal content path where the material should be saved'
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
const sanitizedName = typeof args.name === 'string' ? args.name.trim() : args.name;
|
|
226
|
+
const sanitizedPath = typeof args.path === 'string' ? args.path.trim() : args.path;
|
|
227
|
+
const name = requireNonEmptyString(sanitizedName, 'name', 'Invalid name: must be a non-empty string');
|
|
228
|
+
const res = await tools.materialTools.createMaterial(name, sanitizedPath || '/Game/Materials');
|
|
229
|
+
return cleanObject(res);
|
|
230
|
+
}
|
|
110
231
|
default:
|
|
111
232
|
throw new Error(`Unknown asset action: ${args.action}`);
|
|
112
233
|
}
|
|
113
|
-
break;
|
|
114
234
|
|
|
115
235
|
// 2. ACTOR CONTROL
|
|
116
236
|
case 'control_actor':
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
237
|
+
switch (requireAction(args)) {
|
|
238
|
+
case 'spawn': {
|
|
239
|
+
await elicitMissingPrimitiveArgs(
|
|
240
|
+
tools,
|
|
241
|
+
args,
|
|
242
|
+
'Provide the spawn parameters for control_actor.spawn',
|
|
243
|
+
{
|
|
244
|
+
classPath: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
title: 'Actor Class or Asset Path',
|
|
247
|
+
description: 'Class name (e.g., StaticMeshActor) or asset path (e.g., /Engine/BasicShapes/Cube) to spawn'
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
const classPathInput = typeof args.classPath === 'string' ? args.classPath.trim() : args.classPath;
|
|
252
|
+
const classPath = requireNonEmptyString(classPathInput, 'classPath', 'Invalid classPath: must be a non-empty string');
|
|
253
|
+
const actorNameInput = typeof args.actorName === 'string' && args.actorName.trim() !== ''
|
|
254
|
+
? args.actorName
|
|
255
|
+
: (typeof args.name === 'string' ? args.name : undefined);
|
|
256
|
+
const res = await tools.actorTools.spawn({
|
|
257
|
+
classPath,
|
|
135
258
|
location: args.location,
|
|
136
|
-
rotation: args.rotation
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
actorName
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
259
|
+
rotation: args.rotation,
|
|
260
|
+
actorName: actorNameInput
|
|
261
|
+
});
|
|
262
|
+
return cleanObject(res);
|
|
263
|
+
}
|
|
264
|
+
case 'delete': {
|
|
265
|
+
await elicitMissingPrimitiveArgs(
|
|
266
|
+
tools,
|
|
267
|
+
args,
|
|
268
|
+
'Which actor should control_actor.delete remove?',
|
|
269
|
+
{
|
|
270
|
+
actorName: {
|
|
271
|
+
type: 'string',
|
|
272
|
+
title: 'Actor Name',
|
|
273
|
+
description: 'Exact label of the actor to delete'
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
const actorNameArg = typeof args.actorName === 'string' && args.actorName.trim() !== ''
|
|
278
|
+
? args.actorName
|
|
279
|
+
: (typeof args.name === 'string' ? args.name : undefined);
|
|
280
|
+
const actorName = requireNonEmptyString(actorNameArg, 'actorName', 'Invalid actorName');
|
|
281
|
+
const res = await tools.bridge.executeEditorFunction('DELETE_ACTOR', { actor_name: actorName });
|
|
282
|
+
return cleanObject(res);
|
|
283
|
+
}
|
|
284
|
+
case 'apply_force': {
|
|
285
|
+
await elicitMissingPrimitiveArgs(
|
|
286
|
+
tools,
|
|
287
|
+
args,
|
|
288
|
+
'Provide the target actor for control_actor.apply_force',
|
|
289
|
+
{
|
|
290
|
+
actorName: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
title: 'Actor Name',
|
|
293
|
+
description: 'Physics-enabled actor that should receive the force'
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName');
|
|
298
|
+
const vector = requireVector3Components(args.force, 'Invalid force: must have numeric x,y,z');
|
|
299
|
+
const res = await tools.physicsTools.applyForce({
|
|
300
|
+
actorName,
|
|
301
|
+
forceType: 'Force',
|
|
302
|
+
vector
|
|
303
|
+
});
|
|
304
|
+
return cleanObject(res);
|
|
305
|
+
}
|
|
182
306
|
default:
|
|
183
307
|
throw new Error(`Unknown actor action: ${args.action}`);
|
|
184
308
|
}
|
|
185
|
-
break;
|
|
186
309
|
|
|
187
310
|
// 3. EDITOR CONTROL
|
|
188
311
|
case 'control_editor':
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
case 'set_camera':
|
|
238
|
-
// Allow either location or rotation or both
|
|
239
|
-
// Don't require both to be present
|
|
240
|
-
mappedName = 'set_camera';
|
|
241
|
-
mappedArgs = {
|
|
242
|
-
location: args.location,
|
|
243
|
-
rotation: args.rotation
|
|
244
|
-
};
|
|
245
|
-
break;
|
|
246
|
-
case 'set_view_mode':
|
|
247
|
-
// Validate view mode parameter
|
|
248
|
-
if (!args.viewMode) {
|
|
249
|
-
throw new Error('Missing required parameter: viewMode');
|
|
250
|
-
}
|
|
251
|
-
if (typeof args.viewMode !== 'string' || args.viewMode.trim() === '') {
|
|
252
|
-
throw new Error('Invalid viewMode: must be a non-empty string');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Normalize view mode to match what debug.ts expects
|
|
256
|
-
const validModes = ['lit', 'unlit', 'wireframe', 'detail_lighting', 'lighting_only',
|
|
257
|
-
'light_complexity', 'shader_complexity', 'lightmap_density',
|
|
258
|
-
'stationary_light_overlap', 'reflections', 'visualize_buffer',
|
|
259
|
-
'collision_pawn', 'collision_visibility', 'lod_coloration', 'quad_overdraw'];
|
|
260
|
-
const normalizedMode = args.viewMode.toLowerCase().replace(/_/g, '');
|
|
261
|
-
|
|
262
|
-
// Map to proper case for debug.ts
|
|
263
|
-
let mappedMode = '';
|
|
264
|
-
switch(normalizedMode) {
|
|
265
|
-
case 'lit': mappedMode = 'Lit'; break;
|
|
266
|
-
case 'unlit': mappedMode = 'Unlit'; break;
|
|
267
|
-
case 'wireframe': mappedMode = 'Wireframe'; break;
|
|
268
|
-
case 'detaillighting': mappedMode = 'DetailLighting'; break;
|
|
269
|
-
case 'lightingonly': mappedMode = 'LightingOnly'; break;
|
|
270
|
-
case 'lightcomplexity': mappedMode = 'LightComplexity'; break;
|
|
271
|
-
case 'shadercomplexity': mappedMode = 'ShaderComplexity'; break;
|
|
272
|
-
case 'lightmapdensity': mappedMode = 'LightmapDensity'; break;
|
|
273
|
-
case 'stationarylightoverlap': mappedMode = 'StationaryLightOverlap'; break;
|
|
274
|
-
case 'reflections': mappedMode = 'ReflectionOverride'; break;
|
|
275
|
-
case 'visualizebuffer': mappedMode = 'VisualizeBuffer'; break;
|
|
276
|
-
case 'collisionpawn': mappedMode = 'CollisionPawn'; break;
|
|
277
|
-
case 'collisionvisibility': mappedMode = 'CollisionVisibility'; break;
|
|
278
|
-
case 'lodcoloration': mappedMode = 'LODColoration'; break;
|
|
279
|
-
case 'quadoverdraw': mappedMode = 'QuadOverdraw'; break;
|
|
280
|
-
default:
|
|
281
|
-
throw new Error(`Invalid viewMode: '${args.viewMode}'. Valid modes are: ${validModes.join(', ')}`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
mappedName = 'set_view_mode';
|
|
285
|
-
mappedArgs = {
|
|
286
|
-
mode: mappedMode
|
|
287
|
-
};
|
|
288
|
-
break;
|
|
312
|
+
switch (requireAction(args)) {
|
|
313
|
+
case 'play': {
|
|
314
|
+
const res = await tools.editorTools.playInEditor();
|
|
315
|
+
return cleanObject(res);
|
|
316
|
+
}
|
|
317
|
+
case 'stop': {
|
|
318
|
+
const res = await tools.editorTools.stopPlayInEditor();
|
|
319
|
+
return cleanObject(res);
|
|
320
|
+
}
|
|
321
|
+
case 'pause': {
|
|
322
|
+
const res = await tools.editorTools.pausePlayInEditor();
|
|
323
|
+
return cleanObject(res);
|
|
324
|
+
}
|
|
325
|
+
case 'set_game_speed': {
|
|
326
|
+
const speed = requirePositiveNumber(args.speed, 'speed', 'Invalid speed: must be a positive number');
|
|
327
|
+
// Use console command via bridge
|
|
328
|
+
const res = await tools.bridge.executeConsoleCommand(`slomo ${speed}`);
|
|
329
|
+
return cleanObject(res);
|
|
330
|
+
}
|
|
331
|
+
case 'eject': {
|
|
332
|
+
const res = await tools.bridge.executeConsoleCommand('eject');
|
|
333
|
+
return cleanObject(res);
|
|
334
|
+
}
|
|
335
|
+
case 'possess': {
|
|
336
|
+
const res = await tools.bridge.executeConsoleCommand('viewself');
|
|
337
|
+
return cleanObject(res);
|
|
338
|
+
}
|
|
339
|
+
case 'set_camera': {
|
|
340
|
+
const res = await tools.editorTools.setViewportCamera(args.location, args.rotation);
|
|
341
|
+
return cleanObject(res);
|
|
342
|
+
}
|
|
343
|
+
case 'set_view_mode': {
|
|
344
|
+
await elicitMissingPrimitiveArgs(
|
|
345
|
+
tools,
|
|
346
|
+
args,
|
|
347
|
+
'Provide the view mode for control_editor.set_view_mode',
|
|
348
|
+
{
|
|
349
|
+
viewMode: {
|
|
350
|
+
type: 'string',
|
|
351
|
+
title: 'View Mode',
|
|
352
|
+
description: 'Viewport view mode (e.g., Lit, Unlit, Wireframe)'
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
const viewMode = requireNonEmptyString(args.viewMode, 'viewMode', 'Missing required parameter: viewMode');
|
|
357
|
+
const res = await tools.bridge.setSafeViewMode(viewMode);
|
|
358
|
+
return cleanObject(res);
|
|
359
|
+
}
|
|
289
360
|
default:
|
|
290
361
|
throw new Error(`Unknown editor action: ${args.action}`);
|
|
291
362
|
}
|
|
292
|
-
break;
|
|
293
363
|
|
|
294
364
|
// 4. LEVEL MANAGER
|
|
295
|
-
|
|
296
|
-
switch (args
|
|
297
|
-
case 'load':
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
mappedArgs = {
|
|
309
|
-
levelName: args.levelName
|
|
310
|
-
};
|
|
311
|
-
if (args.savePath !== undefined) {
|
|
312
|
-
mappedArgs.savePath = args.savePath;
|
|
313
|
-
}
|
|
314
|
-
break;
|
|
315
|
-
case 'stream':
|
|
316
|
-
mappedName = 'stream_level';
|
|
317
|
-
mappedArgs = {
|
|
318
|
-
levelName: args.levelName,
|
|
319
|
-
shouldBeLoaded: args.shouldBeLoaded,
|
|
320
|
-
shouldBeVisible: args.shouldBeVisible
|
|
321
|
-
};
|
|
322
|
-
break;
|
|
323
|
-
case 'create_light':
|
|
324
|
-
// Validate light type
|
|
325
|
-
if (!args.lightType) {
|
|
326
|
-
throw new Error('Missing required parameter: lightType');
|
|
327
|
-
}
|
|
328
|
-
const validLightTypes = ['directional', 'point', 'spot', 'rect', 'sky'];
|
|
329
|
-
const normalizedLightType = String(args.lightType).toLowerCase();
|
|
330
|
-
if (!validLightTypes.includes(normalizedLightType)) {
|
|
331
|
-
throw new Error(`Invalid lightType: '${args.lightType}'. Valid types are: ${validLightTypes.join(', ')}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Validate name
|
|
335
|
-
if (!args.name) {
|
|
336
|
-
throw new Error('Missing required parameter: name');
|
|
337
|
-
}
|
|
338
|
-
if (typeof args.name !== 'string' || args.name.trim() === '') {
|
|
339
|
-
throw new Error('Invalid name: must be a non-empty string');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Validate intensity if provided
|
|
343
|
-
if (args.intensity !== undefined) {
|
|
344
|
-
if (typeof args.intensity !== 'number' || !isFinite(args.intensity)) {
|
|
345
|
-
throw new Error(`Invalid intensity: must be a finite number, got ${typeof args.intensity}`);
|
|
365
|
+
case 'manage_level':
|
|
366
|
+
switch (requireAction(args)) {
|
|
367
|
+
case 'load': {
|
|
368
|
+
await elicitMissingPrimitiveArgs(
|
|
369
|
+
tools,
|
|
370
|
+
args,
|
|
371
|
+
'Select the level to load for manage_level.load',
|
|
372
|
+
{
|
|
373
|
+
levelPath: {
|
|
374
|
+
type: 'string',
|
|
375
|
+
title: 'Level Path',
|
|
376
|
+
description: 'Content path of the level asset to load (e.g., /Game/Maps/MyLevel)'
|
|
377
|
+
}
|
|
346
378
|
}
|
|
347
|
-
|
|
348
|
-
|
|
379
|
+
);
|
|
380
|
+
const levelPath = requireNonEmptyString(args.levelPath, 'levelPath', 'Missing required parameter: levelPath');
|
|
381
|
+
const res = await tools.levelTools.loadLevel({ levelPath, streaming: !!args.streaming });
|
|
382
|
+
return cleanObject(res);
|
|
383
|
+
}
|
|
384
|
+
case 'save': {
|
|
385
|
+
const res = await tools.levelTools.saveLevel({ levelName: args.levelName, savePath: args.savePath });
|
|
386
|
+
return cleanObject(res);
|
|
387
|
+
}
|
|
388
|
+
case 'stream': {
|
|
389
|
+
await elicitMissingPrimitiveArgs(
|
|
390
|
+
tools,
|
|
391
|
+
args,
|
|
392
|
+
'Provide the streaming level name for manage_level.stream',
|
|
393
|
+
{
|
|
394
|
+
levelName: {
|
|
395
|
+
type: 'string',
|
|
396
|
+
title: 'Level Name',
|
|
397
|
+
description: 'Streaming level name to toggle'
|
|
398
|
+
}
|
|
349
399
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
400
|
+
);
|
|
401
|
+
const levelName = requireNonEmptyString(args.levelName, 'levelName', 'Missing required parameter: levelName');
|
|
402
|
+
const res = await tools.levelTools.streamLevel({ levelName, shouldBeLoaded: !!args.shouldBeLoaded, shouldBeVisible: !!args.shouldBeVisible });
|
|
403
|
+
return cleanObject(res);
|
|
404
|
+
}
|
|
405
|
+
case 'create_light': {
|
|
406
|
+
await elicitMissingPrimitiveArgs(
|
|
407
|
+
tools,
|
|
408
|
+
args,
|
|
409
|
+
'Provide the light details for manage_level.create_light',
|
|
410
|
+
{
|
|
411
|
+
lightType: {
|
|
412
|
+
type: 'string',
|
|
413
|
+
title: 'Light Type',
|
|
414
|
+
description: 'Directional, Point, Spot, Rect, or Sky'
|
|
415
|
+
},
|
|
416
|
+
name: {
|
|
417
|
+
type: 'string',
|
|
418
|
+
title: 'Light Name',
|
|
419
|
+
description: 'Name for the new light actor'
|
|
420
|
+
}
|
|
356
421
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
intensity: args.intensity
|
|
365
|
-
};
|
|
366
|
-
break;
|
|
367
|
-
case 'build_lighting':
|
|
368
|
-
mappedName = 'build_lighting';
|
|
369
|
-
mappedArgs = {
|
|
370
|
-
quality: args.quality
|
|
371
|
-
};
|
|
372
|
-
break;
|
|
373
|
-
default:
|
|
374
|
-
throw new Error(`Unknown level action: ${args.action}`);
|
|
375
|
-
}
|
|
376
|
-
break;
|
|
377
|
-
|
|
378
|
-
// 5. ANIMATION & PHYSICS
|
|
379
|
-
case 'animation_physics':
|
|
380
|
-
// Validate action exists
|
|
381
|
-
if (!args.action) {
|
|
382
|
-
throw new Error('Missing required parameter: action');
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
switch (args.action) {
|
|
386
|
-
case 'create_animation_bp':
|
|
387
|
-
// Validate required parameters
|
|
388
|
-
if (args.name === undefined || args.name === null) {
|
|
389
|
-
throw new Error('Missing required parameter: name');
|
|
390
|
-
}
|
|
391
|
-
if (typeof args.name !== 'string') {
|
|
392
|
-
throw new Error('Invalid name: must be a string');
|
|
393
|
-
}
|
|
394
|
-
if (args.name.trim() === '') {
|
|
395
|
-
throw new Error('Invalid name: cannot be empty');
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (args.skeletonPath === undefined || args.skeletonPath === null) {
|
|
399
|
-
throw new Error('Missing required parameter: skeletonPath');
|
|
400
|
-
}
|
|
401
|
-
if (typeof args.skeletonPath !== 'string') {
|
|
402
|
-
throw new Error('Invalid skeletonPath: must be a string');
|
|
403
|
-
}
|
|
404
|
-
if (args.skeletonPath.trim() === '') {
|
|
405
|
-
throw new Error('Invalid skeletonPath: cannot be empty');
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Optional savePath validation
|
|
409
|
-
if (args.savePath !== undefined && args.savePath !== null) {
|
|
410
|
-
if (typeof args.savePath !== 'string') {
|
|
411
|
-
throw new Error('Invalid savePath: must be a string');
|
|
422
|
+
);
|
|
423
|
+
const lightType = requireNonEmptyString(args.lightType, 'lightType', 'Missing required parameter: lightType');
|
|
424
|
+
const name = requireNonEmptyString(args.name, 'name', 'Invalid name');
|
|
425
|
+
const typeKey = lightType.toLowerCase();
|
|
426
|
+
const toVector = (value: any, fallback: [number, number, number]): [number, number, number] => {
|
|
427
|
+
if (Array.isArray(value) && value.length === 3) {
|
|
428
|
+
return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
|
|
412
429
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
name: args.name,
|
|
418
|
-
skeletonPath: args.skeletonPath,
|
|
419
|
-
savePath: args.savePath
|
|
430
|
+
if (value && typeof value === 'object') {
|
|
431
|
+
return [Number(value.x) || 0, Number(value.y) || 0, Number(value.z) || 0];
|
|
432
|
+
}
|
|
433
|
+
return fallback;
|
|
420
434
|
};
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// Validate required parameters
|
|
425
|
-
if (args.actorName === undefined || args.actorName === null) {
|
|
426
|
-
throw new Error('Missing required parameter: actorName');
|
|
427
|
-
}
|
|
428
|
-
if (typeof args.actorName !== 'string') {
|
|
429
|
-
throw new Error('Invalid actorName: must be a string');
|
|
430
|
-
}
|
|
431
|
-
if (args.actorName.trim() === '') {
|
|
432
|
-
throw new Error('Invalid actorName: cannot be empty');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Check for montagePath or animationPath
|
|
436
|
-
const montagePath = args.montagePath || args.animationPath;
|
|
437
|
-
if (montagePath === undefined || montagePath === null) {
|
|
438
|
-
throw new Error('Missing required parameter: montagePath or animationPath');
|
|
439
|
-
}
|
|
440
|
-
if (typeof montagePath !== 'string') {
|
|
441
|
-
throw new Error('Invalid montagePath: must be a string');
|
|
442
|
-
}
|
|
443
|
-
if (montagePath.trim() === '') {
|
|
444
|
-
throw new Error('Invalid montagePath: cannot be empty');
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Optional playRate validation
|
|
448
|
-
if (args.playRate !== undefined && args.playRate !== null) {
|
|
449
|
-
if (typeof args.playRate !== 'number') {
|
|
450
|
-
throw new Error('Invalid playRate: must be a number');
|
|
435
|
+
const toRotator = (value: any, fallback: [number, number, number]): [number, number, number] => {
|
|
436
|
+
if (Array.isArray(value) && value.length === 3) {
|
|
437
|
+
return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
|
|
451
438
|
}
|
|
452
|
-
if (
|
|
453
|
-
|
|
439
|
+
if (value && typeof value === 'object') {
|
|
440
|
+
return [Number(value.pitch) || 0, Number(value.yaw) || 0, Number(value.roll) || 0];
|
|
454
441
|
}
|
|
455
|
-
|
|
456
|
-
|
|
442
|
+
return fallback;
|
|
443
|
+
};
|
|
444
|
+
const toColor = (value: any): [number, number, number] | undefined => {
|
|
445
|
+
if (Array.isArray(value) && value.length === 3) {
|
|
446
|
+
return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
|
|
457
447
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
actorName: args.actorName,
|
|
463
|
-
montagePath: montagePath,
|
|
464
|
-
playRate: args.playRate
|
|
448
|
+
if (value && typeof value === 'object') {
|
|
449
|
+
return [Number(value.r) || 0, Number(value.g) || 0, Number(value.b) || 0];
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
465
452
|
};
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
453
|
+
|
|
454
|
+
const location = toVector(args.location, [0, 0, typeKey === 'directional' ? 500 : 0]);
|
|
455
|
+
const rotation = toRotator(args.rotation, [0, 0, 0]);
|
|
456
|
+
const color = toColor(args.color);
|
|
457
|
+
const castShadows = typeof args.castShadows === 'boolean' ? args.castShadows : undefined;
|
|
458
|
+
|
|
459
|
+
if (typeKey === 'directional') {
|
|
460
|
+
return cleanObject(await tools.lightingTools.createDirectionalLight({
|
|
461
|
+
name,
|
|
462
|
+
intensity: args.intensity,
|
|
463
|
+
color,
|
|
464
|
+
rotation,
|
|
465
|
+
castShadows,
|
|
466
|
+
temperature: args.temperature
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
if (typeKey === 'point') {
|
|
470
|
+
return cleanObject(await tools.lightingTools.createPointLight({
|
|
471
|
+
name,
|
|
472
|
+
location,
|
|
473
|
+
intensity: args.intensity,
|
|
474
|
+
radius: args.radius,
|
|
475
|
+
color,
|
|
476
|
+
falloffExponent: args.falloffExponent,
|
|
477
|
+
castShadows
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
if (typeKey === 'spot') {
|
|
481
|
+
const innerCone = typeof args.innerCone === 'number' ? args.innerCone : undefined;
|
|
482
|
+
const outerCone = typeof args.outerCone === 'number' ? args.outerCone : undefined;
|
|
483
|
+
if (innerCone !== undefined && outerCone !== undefined && innerCone >= outerCone) {
|
|
484
|
+
throw new Error('innerCone must be less than outerCone');
|
|
494
485
|
}
|
|
495
|
-
|
|
496
|
-
|
|
486
|
+
return cleanObject(await tools.lightingTools.createSpotLight({
|
|
487
|
+
name,
|
|
488
|
+
location,
|
|
489
|
+
rotation,
|
|
490
|
+
intensity: args.intensity,
|
|
491
|
+
innerCone: args.innerCone,
|
|
492
|
+
outerCone: args.outerCone,
|
|
493
|
+
radius: args.radius,
|
|
494
|
+
color,
|
|
495
|
+
castShadows
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
if (typeKey === 'rect') {
|
|
499
|
+
return cleanObject(await tools.lightingTools.createRectLight({
|
|
500
|
+
name,
|
|
501
|
+
location,
|
|
502
|
+
rotation,
|
|
503
|
+
intensity: args.intensity,
|
|
504
|
+
width: args.width,
|
|
505
|
+
height: args.height,
|
|
506
|
+
color
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
if (typeKey === 'sky' || typeKey === 'skylight') {
|
|
510
|
+
return cleanObject(await tools.lightingTools.createSkyLight({
|
|
511
|
+
name,
|
|
512
|
+
sourceType: args.sourceType,
|
|
513
|
+
cubemapPath: args.cubemapPath,
|
|
514
|
+
intensity: args.intensity,
|
|
515
|
+
recapture: args.recapture
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
throw new Error(`Unknown light type: ${lightType}`);
|
|
519
|
+
}
|
|
520
|
+
case 'build_lighting': {
|
|
521
|
+
const res = await tools.lightingTools.buildLighting({ quality: args.quality || 'High', buildReflectionCaptures: true });
|
|
522
|
+
return cleanObject(res);
|
|
523
|
+
}
|
|
524
|
+
default:
|
|
525
|
+
throw new Error(`Unknown level action: ${args.action}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 5. ANIMATION & PHYSICS
|
|
529
|
+
case 'animation_physics':
|
|
530
|
+
switch (requireAction(args)) {
|
|
531
|
+
case 'create_animation_bp': {
|
|
532
|
+
await elicitMissingPrimitiveArgs(
|
|
533
|
+
tools,
|
|
534
|
+
args,
|
|
535
|
+
'Provide details for animation_physics.create_animation_bp',
|
|
536
|
+
{
|
|
537
|
+
name: {
|
|
538
|
+
type: 'string',
|
|
539
|
+
title: 'Blueprint Name',
|
|
540
|
+
description: 'Name of the Animation Blueprint to create'
|
|
541
|
+
},
|
|
542
|
+
skeletonPath: {
|
|
543
|
+
type: 'string',
|
|
544
|
+
title: 'Skeleton Path',
|
|
545
|
+
description: 'Content path of the skeleton asset to bind'
|
|
546
|
+
}
|
|
497
547
|
}
|
|
498
|
-
|
|
499
|
-
|
|
548
|
+
);
|
|
549
|
+
const name = requireNonEmptyString(args.name, 'name', 'Invalid name');
|
|
550
|
+
const skeletonPath = requireNonEmptyString(args.skeletonPath, 'skeletonPath', 'Invalid skeletonPath');
|
|
551
|
+
const res = await tools.animationTools.createAnimationBlueprint({ name, skeletonPath, savePath: args.savePath });
|
|
552
|
+
return cleanObject(res);
|
|
553
|
+
}
|
|
554
|
+
case 'play_montage': {
|
|
555
|
+
await elicitMissingPrimitiveArgs(
|
|
556
|
+
tools,
|
|
557
|
+
args,
|
|
558
|
+
'Provide playback details for animation_physics.play_montage',
|
|
559
|
+
{
|
|
560
|
+
actorName: {
|
|
561
|
+
type: 'string',
|
|
562
|
+
title: 'Actor Name',
|
|
563
|
+
description: 'Actor that should play the montage'
|
|
564
|
+
},
|
|
565
|
+
montagePath: {
|
|
566
|
+
type: 'string',
|
|
567
|
+
title: 'Montage Path',
|
|
568
|
+
description: 'Montage or animation asset path to play'
|
|
569
|
+
}
|
|
500
570
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
571
|
+
);
|
|
572
|
+
const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName');
|
|
573
|
+
const montagePath = args.montagePath || args.animationPath;
|
|
574
|
+
const validatedMontage = requireNonEmptyString(montagePath, 'montagePath', 'Invalid montagePath');
|
|
575
|
+
const res = await tools.animationTools.playAnimation({ actorName, animationType: 'Montage', animationPath: validatedMontage, playRate: args.playRate });
|
|
576
|
+
return cleanObject(res);
|
|
577
|
+
}
|
|
578
|
+
case 'setup_ragdoll': {
|
|
579
|
+
await elicitMissingPrimitiveArgs(
|
|
580
|
+
tools,
|
|
581
|
+
args,
|
|
582
|
+
'Provide setup details for animation_physics.setup_ragdoll',
|
|
583
|
+
{
|
|
584
|
+
skeletonPath: {
|
|
585
|
+
type: 'string',
|
|
586
|
+
title: 'Skeleton Path',
|
|
587
|
+
description: 'Content path for the skeleton asset'
|
|
588
|
+
},
|
|
589
|
+
physicsAssetName: {
|
|
590
|
+
type: 'string',
|
|
591
|
+
title: 'Physics Asset Name',
|
|
592
|
+
description: 'Name of the physics asset to apply'
|
|
593
|
+
}
|
|
507
594
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
blendWeight: args.blendWeight,
|
|
515
|
-
savePath: args.savePath
|
|
516
|
-
};
|
|
517
|
-
break;
|
|
518
|
-
|
|
595
|
+
);
|
|
596
|
+
const skeletonPath = requireNonEmptyString(args.skeletonPath, 'skeletonPath', 'Invalid skeletonPath');
|
|
597
|
+
const physicsAssetName = requireNonEmptyString(args.physicsAssetName, 'physicsAssetName', 'Invalid physicsAssetName');
|
|
598
|
+
const res = await tools.physicsTools.setupRagdoll({ skeletonPath, physicsAssetName, blendWeight: args.blendWeight, savePath: args.savePath });
|
|
599
|
+
return cleanObject(res);
|
|
600
|
+
}
|
|
519
601
|
default:
|
|
520
602
|
throw new Error(`Unknown animation/physics action: ${args.action}`);
|
|
521
603
|
}
|
|
522
|
-
break;
|
|
523
604
|
|
|
524
|
-
|
|
605
|
+
// 6. EFFECTS SYSTEM
|
|
525
606
|
case 'create_effect':
|
|
526
|
-
switch (args
|
|
527
|
-
case 'particle':
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
607
|
+
switch (requireAction(args)) {
|
|
608
|
+
case 'particle': {
|
|
609
|
+
await elicitMissingPrimitiveArgs(
|
|
610
|
+
tools,
|
|
611
|
+
args,
|
|
612
|
+
'Provide the particle effect details for create_effect.particle',
|
|
613
|
+
{
|
|
614
|
+
effectType: {
|
|
615
|
+
type: 'string',
|
|
616
|
+
title: 'Effect Type',
|
|
617
|
+
description: 'Preset effect type to spawn (e.g., Fire, Smoke)'
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
const res = await tools.niagaraTools.createEffect({ effectType: args.effectType, name: args.name, location: args.location, scale: args.scale, customParameters: args.customParameters });
|
|
622
|
+
return cleanObject(res);
|
|
623
|
+
}
|
|
624
|
+
case 'niagara': {
|
|
625
|
+
await elicitMissingPrimitiveArgs(
|
|
626
|
+
tools,
|
|
627
|
+
args,
|
|
628
|
+
'Provide the Niagara system path for create_effect.niagara',
|
|
629
|
+
{
|
|
630
|
+
systemPath: {
|
|
631
|
+
type: 'string',
|
|
632
|
+
title: 'Niagara System Path',
|
|
633
|
+
description: 'Asset path of the Niagara system to spawn'
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
const systemPath = requireNonEmptyString(args.systemPath, 'systemPath', 'Invalid systemPath');
|
|
638
|
+
const verifyResult = await tools.bridge.executePythonWithResult(`
|
|
639
|
+
import unreal, json
|
|
640
|
+
path = r"${systemPath}"
|
|
641
|
+
exists = unreal.EditorAssetLibrary.does_asset_exist(path)
|
|
642
|
+
print('RESULT:' + json.dumps({'success': exists, 'exists': exists, 'path': path}))
|
|
643
|
+
`.trim());
|
|
644
|
+
if (!verifyResult?.exists) {
|
|
645
|
+
return cleanObject({ success: false, error: `Niagara system not found at ${systemPath}` });
|
|
646
|
+
}
|
|
647
|
+
const loc = Array.isArray(args.location)
|
|
648
|
+
? { x: args.location[0], y: args.location[1], z: args.location[2] }
|
|
649
|
+
: args.location || { x: 0, y: 0, z: 0 };
|
|
650
|
+
const res = await tools.niagaraTools.spawnEffect({
|
|
651
|
+
systemPath,
|
|
652
|
+
location: [loc.x ?? 0, loc.y ?? 0, loc.z ?? 0],
|
|
653
|
+
rotation: Array.isArray(args.rotation) ? args.rotation : undefined,
|
|
540
654
|
scale: args.scale
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
655
|
+
});
|
|
656
|
+
return cleanObject(res);
|
|
657
|
+
}
|
|
658
|
+
case 'debug_shape': {
|
|
659
|
+
const shapeInput = args.shape ?? 'Sphere';
|
|
660
|
+
const shape = String(shapeInput).trim().toLowerCase();
|
|
661
|
+
const originalShapeLabel = String(shapeInput).trim() || 'shape';
|
|
662
|
+
const loc = args.location || { x: 0, y: 0, z: 0 };
|
|
663
|
+
const size = args.size || 100;
|
|
664
|
+
const color = args.color || [255, 0, 0, 255];
|
|
665
|
+
const duration = args.duration || 5;
|
|
666
|
+
if (shape === 'line') {
|
|
667
|
+
const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z };
|
|
668
|
+
return cleanObject(await tools.debugTools.drawDebugLine({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration }));
|
|
669
|
+
} else if (shape === 'box') {
|
|
670
|
+
const extent = [size, size, size];
|
|
671
|
+
return cleanObject(await tools.debugTools.drawDebugBox({ center: [loc.x, loc.y, loc.z], extent, color, duration }));
|
|
672
|
+
} else if (shape === 'sphere') {
|
|
673
|
+
return cleanObject(await tools.debugTools.drawDebugSphere({ center: [loc.x, loc.y, loc.z], radius: size, color, duration }));
|
|
674
|
+
} else if (shape === 'capsule') {
|
|
675
|
+
return cleanObject(await tools.debugTools.drawDebugCapsule({ center: [loc.x, loc.y, loc.z], halfHeight: size, radius: Math.max(10, size/3), color, duration }));
|
|
676
|
+
} else if (shape === 'cone') {
|
|
677
|
+
return cleanObject(await tools.debugTools.drawDebugCone({ origin: [loc.x, loc.y, loc.z], direction: [0,0,1], length: size, angleWidth: 0.5, angleHeight: 0.5, color, duration }));
|
|
678
|
+
} else if (shape === 'arrow') {
|
|
679
|
+
const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z };
|
|
680
|
+
return cleanObject(await tools.debugTools.drawDebugArrow({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration }));
|
|
681
|
+
} else if (shape === 'point') {
|
|
682
|
+
return cleanObject(await tools.debugTools.drawDebugPoint({ location: [loc.x, loc.y, loc.z], size, color, duration }));
|
|
683
|
+
} else if (shape === 'text' || shape === 'string') {
|
|
684
|
+
const text = args.text || 'Debug';
|
|
685
|
+
return cleanObject(await tools.debugTools.drawDebugString({ location: [loc.x, loc.y, loc.z], text, color, duration }));
|
|
686
|
+
}
|
|
687
|
+
// Default fallback
|
|
688
|
+
return cleanObject({ success: false, error: `Unsupported debug shape: ${originalShapeLabel}` });
|
|
689
|
+
}
|
|
555
690
|
default:
|
|
556
691
|
throw new Error(`Unknown effect action: ${args.action}`);
|
|
557
692
|
}
|
|
558
|
-
break;
|
|
559
693
|
|
|
560
|
-
|
|
694
|
+
// 7. BLUEPRINT MANAGER
|
|
561
695
|
case 'manage_blueprint':
|
|
562
|
-
switch (args
|
|
563
|
-
case 'create':
|
|
564
|
-
|
|
565
|
-
|
|
696
|
+
switch (requireAction(args)) {
|
|
697
|
+
case 'create': {
|
|
698
|
+
await elicitMissingPrimitiveArgs(
|
|
699
|
+
tools,
|
|
700
|
+
args,
|
|
701
|
+
'Provide details for manage_blueprint.create',
|
|
702
|
+
{
|
|
703
|
+
name: {
|
|
704
|
+
type: 'string',
|
|
705
|
+
title: 'Blueprint Name',
|
|
706
|
+
description: 'Name for the new Blueprint asset'
|
|
707
|
+
},
|
|
708
|
+
blueprintType: {
|
|
709
|
+
type: 'string',
|
|
710
|
+
title: 'Blueprint Type',
|
|
711
|
+
description: 'Base type such as Actor, Pawn, Character, etc.'
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
);
|
|
715
|
+
const res = await tools.blueprintTools.createBlueprint({
|
|
566
716
|
name: args.name,
|
|
567
|
-
blueprintType: args.blueprintType,
|
|
568
|
-
savePath: args.savePath
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
717
|
+
blueprintType: args.blueprintType || 'Actor',
|
|
718
|
+
savePath: args.savePath,
|
|
719
|
+
parentClass: args.parentClass
|
|
720
|
+
});
|
|
721
|
+
return cleanObject(res);
|
|
722
|
+
}
|
|
723
|
+
case 'add_component': {
|
|
724
|
+
await elicitMissingPrimitiveArgs(
|
|
725
|
+
tools,
|
|
726
|
+
args,
|
|
727
|
+
'Provide details for manage_blueprint.add_component',
|
|
728
|
+
{
|
|
729
|
+
name: {
|
|
730
|
+
type: 'string',
|
|
731
|
+
title: 'Blueprint Name',
|
|
732
|
+
description: 'Blueprint asset to modify'
|
|
733
|
+
},
|
|
734
|
+
componentType: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
title: 'Component Type',
|
|
737
|
+
description: 'Component class to add (e.g., StaticMeshComponent)'
|
|
738
|
+
},
|
|
739
|
+
componentName: {
|
|
740
|
+
type: 'string',
|
|
741
|
+
title: 'Component Name',
|
|
742
|
+
description: 'Name for the new component'
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
);
|
|
746
|
+
const res = await tools.blueprintTools.addComponent({ blueprintName: args.name, componentType: args.componentType, componentName: args.componentName });
|
|
747
|
+
return cleanObject(res);
|
|
748
|
+
}
|
|
579
749
|
default:
|
|
580
750
|
throw new Error(`Unknown blueprint action: ${args.action}`);
|
|
581
751
|
}
|
|
582
|
-
break;
|
|
583
752
|
|
|
584
|
-
|
|
753
|
+
// 8. ENVIRONMENT BUILDER
|
|
585
754
|
case 'build_environment':
|
|
586
|
-
switch (args
|
|
587
|
-
case 'create_landscape':
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
755
|
+
switch (requireAction(args)) {
|
|
756
|
+
case 'create_landscape': {
|
|
757
|
+
const res = await tools.landscapeTools.createLandscape({ name: args.name, sizeX: args.sizeX, sizeY: args.sizeY, materialPath: args.materialPath });
|
|
758
|
+
return cleanObject(res);
|
|
759
|
+
}
|
|
760
|
+
case 'sculpt': {
|
|
761
|
+
const res = await tools.landscapeTools.sculptLandscape({ landscapeName: args.name, tool: args.tool, brushSize: args.brushSize, strength: args.strength });
|
|
762
|
+
return cleanObject(res);
|
|
763
|
+
}
|
|
764
|
+
case 'add_foliage': {
|
|
765
|
+
const res = await tools.foliageTools.addFoliageType({ name: args.name, meshPath: args.meshPath, density: args.density });
|
|
766
|
+
return cleanObject(res);
|
|
767
|
+
}
|
|
768
|
+
case 'paint_foliage': {
|
|
769
|
+
const pos = args.position ? [args.position.x || 0, args.position.y || 0, args.position.z || 0] : [0,0,0];
|
|
770
|
+
const res = await tools.foliageTools.paintFoliage({ foliageType: args.foliageType, position: pos, brushSize: args.brushSize, paintDensity: args.paintDensity, eraseMode: args.eraseMode });
|
|
771
|
+
return cleanObject(res);
|
|
772
|
+
}
|
|
773
|
+
case 'create_procedural_terrain': {
|
|
774
|
+
const loc = args.location ? [args.location.x||0, args.location.y||0, args.location.z||0] : [0,0,0];
|
|
775
|
+
const res = await tools.buildEnvAdvanced.createProceduralTerrain({
|
|
776
|
+
name: args.name || 'ProceduralTerrain',
|
|
777
|
+
location: loc as [number,number,number],
|
|
591
778
|
sizeX: args.sizeX,
|
|
592
779
|
sizeY: args.sizeY,
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
780
|
+
subdivisions: args.subdivisions,
|
|
781
|
+
heightFunction: args.heightFunction,
|
|
782
|
+
material: args.materialPath
|
|
783
|
+
});
|
|
784
|
+
return cleanObject(res);
|
|
785
|
+
}
|
|
786
|
+
case 'create_procedural_foliage': {
|
|
787
|
+
if (!args.bounds || !args.bounds.location || !args.bounds.size) throw new Error('bounds.location and bounds.size are required');
|
|
788
|
+
const bounds = {
|
|
789
|
+
location: [args.bounds.location.x||0, args.bounds.location.y||0, args.bounds.location.z||0] as [number,number,number],
|
|
790
|
+
size: [args.bounds.size.x||1000, args.bounds.size.y||1000, args.bounds.size.z||100] as [number,number,number]
|
|
603
791
|
};
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (args.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
792
|
+
const res = await tools.buildEnvAdvanced.createProceduralFoliage({
|
|
793
|
+
name: args.name || 'ProceduralFoliage',
|
|
794
|
+
bounds,
|
|
795
|
+
foliageTypes: args.foliageTypes || [],
|
|
796
|
+
seed: args.seed
|
|
797
|
+
});
|
|
798
|
+
return cleanObject(res);
|
|
799
|
+
}
|
|
800
|
+
case 'add_foliage_instances': {
|
|
801
|
+
if (!args.foliageType) throw new Error('foliageType is required');
|
|
802
|
+
if (!Array.isArray(args.transforms)) throw new Error('transforms array is required');
|
|
803
|
+
const transforms = (args.transforms as any[]).map(t => ({
|
|
804
|
+
location: [t.location?.x||0, t.location?.y||0, t.location?.z||0] as [number,number,number],
|
|
805
|
+
rotation: t.rotation ? [t.rotation.pitch||0, t.rotation.yaw||0, t.rotation.roll||0] as [number,number,number] : undefined,
|
|
806
|
+
scale: t.scale ? [t.scale.x||1, t.scale.y||1, t.scale.z||1] as [number,number,number] : undefined
|
|
807
|
+
}));
|
|
808
|
+
const res = await tools.buildEnvAdvanced.addFoliageInstances({ foliageType: args.foliageType, transforms });
|
|
809
|
+
return cleanObject(res);
|
|
810
|
+
}
|
|
811
|
+
case 'create_landscape_grass_type': {
|
|
812
|
+
const res = await tools.buildEnvAdvanced.createLandscapeGrassType({
|
|
813
|
+
name: args.name || 'GrassType',
|
|
621
814
|
meshPath: args.meshPath,
|
|
622
|
-
density: args.density
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
throw new Error(`Invalid foliageType: '${args.foliageType}'`);
|
|
629
|
-
}
|
|
630
|
-
// Convert position object to array if needed
|
|
631
|
-
let positionArray;
|
|
632
|
-
if (args.position) {
|
|
633
|
-
if (Array.isArray(args.position)) {
|
|
634
|
-
positionArray = args.position;
|
|
635
|
-
} else if (typeof args.position === 'object') {
|
|
636
|
-
positionArray = [args.position.x || 0, args.position.y || 0, args.position.z || 0];
|
|
637
|
-
} else {
|
|
638
|
-
positionArray = [0, 0, 0];
|
|
639
|
-
}
|
|
640
|
-
} else {
|
|
641
|
-
positionArray = [0, 0, 0];
|
|
642
|
-
}
|
|
643
|
-
// Validate numbers in position
|
|
644
|
-
if (!Array.isArray(positionArray) || positionArray.length !== 3 || positionArray.some(v => typeof v !== 'number' || !isFinite(v))) {
|
|
645
|
-
throw new Error(`Invalid position: '${JSON.stringify(args.position)}'`);
|
|
646
|
-
}
|
|
647
|
-
if (args.brushSize !== undefined) {
|
|
648
|
-
if (typeof args.brushSize !== 'number' || !isFinite(args.brushSize) || args.brushSize < 0) {
|
|
649
|
-
throw new Error(`Invalid brushSize: '${args.brushSize}' (must be non-negative finite number)`);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
mappedName = 'paint_foliage';
|
|
653
|
-
mappedArgs = {
|
|
654
|
-
foliageType: args.foliageType,
|
|
655
|
-
position: positionArray,
|
|
656
|
-
brushSize: args.brushSize
|
|
657
|
-
};
|
|
658
|
-
break;
|
|
815
|
+
density: args.density,
|
|
816
|
+
minScale: args.minScale,
|
|
817
|
+
maxScale: args.maxScale
|
|
818
|
+
});
|
|
819
|
+
return cleanObject(res);
|
|
820
|
+
}
|
|
659
821
|
default:
|
|
660
822
|
throw new Error(`Unknown environment action: ${args.action}`);
|
|
661
823
|
}
|
|
662
|
-
break;
|
|
663
824
|
|
|
664
825
|
// 9. SYSTEM CONTROL
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
case '
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
break;
|
|
691
|
-
|
|
692
|
-
case 'show_fps':
|
|
693
|
-
// Validate enabled is boolean
|
|
694
|
-
if (args.enabled !== undefined && typeof args.enabled !== 'boolean') {
|
|
695
|
-
throw new Error(`Invalid enabled: must be boolean, got ${typeof args.enabled}`);
|
|
696
|
-
}
|
|
697
|
-
mappedName = 'show_fps';
|
|
698
|
-
mappedArgs = {
|
|
699
|
-
enabled: args.enabled,
|
|
700
|
-
verbose: args.verbose
|
|
701
|
-
};
|
|
702
|
-
break;
|
|
703
|
-
|
|
704
|
-
case 'set_quality':
|
|
705
|
-
// Validate category - normalize aliases and singular forms used by sg.*Quality
|
|
706
|
-
const validCategories = ['ViewDistance', 'AntiAliasing', 'PostProcessing', 'PostProcess',
|
|
707
|
-
'Shadows', 'Shadow', 'GlobalIllumination', 'Reflections', 'Reflection', 'Textures', 'Texture',
|
|
708
|
-
'Effects', 'Foliage', 'Shading'];
|
|
709
|
-
if (!args.category || !validCategories.includes(args.category)) {
|
|
710
|
-
throw new Error(`Invalid category: '${args.category}'. Valid categories: ${validCategories.join(', ')}`);
|
|
711
|
-
}
|
|
712
|
-
// Validate level
|
|
713
|
-
if (args.level === undefined || args.level === null) {
|
|
714
|
-
throw new Error('Missing required parameter: level');
|
|
715
|
-
}
|
|
716
|
-
if (typeof args.level !== 'number' || !Number.isInteger(args.level) || args.level < 0 || args.level > 4) {
|
|
717
|
-
throw new Error(`Invalid level: must be integer 0-4, got ${args.level}`);
|
|
718
|
-
}
|
|
719
|
-
// Normalize category to sg.<Base>Quality base (singular where needed)
|
|
720
|
-
const map: Record<string, string> = {
|
|
721
|
-
ViewDistance: 'ViewDistance',
|
|
722
|
-
AntiAliasing: 'AntiAliasing',
|
|
723
|
-
PostProcessing: 'PostProcess',
|
|
724
|
-
PostProcess: 'PostProcess',
|
|
725
|
-
Shadows: 'Shadow',
|
|
726
|
-
Shadow: 'Shadow',
|
|
727
|
-
GlobalIllumination: 'GlobalIllumination',
|
|
728
|
-
Reflections: 'Reflection',
|
|
729
|
-
Reflection: 'Reflection',
|
|
730
|
-
Textures: 'Texture',
|
|
731
|
-
Texture: 'Texture',
|
|
732
|
-
Effects: 'Effects',
|
|
733
|
-
Foliage: 'Foliage',
|
|
734
|
-
Shading: 'Shading',
|
|
735
|
-
};
|
|
736
|
-
const categoryName = map[String(args.category)] || args.category;
|
|
737
|
-
mappedName = 'set_scalability';
|
|
738
|
-
mappedArgs = {
|
|
739
|
-
category: categoryName,
|
|
740
|
-
level: args.level
|
|
741
|
-
};
|
|
742
|
-
break;
|
|
743
|
-
|
|
744
|
-
case 'play_sound':
|
|
745
|
-
// Validate sound path
|
|
746
|
-
if (!args.soundPath || typeof args.soundPath !== 'string') {
|
|
747
|
-
throw new Error('Invalid soundPath: must be a non-empty string');
|
|
748
|
-
}
|
|
749
|
-
// Validate volume if provided
|
|
750
|
-
if (args.volume !== undefined) {
|
|
751
|
-
if (typeof args.volume !== 'number' || args.volume < 0 || args.volume > 1) {
|
|
752
|
-
throw new Error(`Invalid volume: must be 0-1, got ${args.volume}`);
|
|
826
|
+
case 'system_control':
|
|
827
|
+
switch (requireAction(args)) {
|
|
828
|
+
case 'profile': {
|
|
829
|
+
const res = await tools.performanceTools.startProfiling({ type: args.profileType, duration: args.duration });
|
|
830
|
+
return cleanObject(res);
|
|
831
|
+
}
|
|
832
|
+
case 'show_fps': {
|
|
833
|
+
const res = await tools.performanceTools.showFPS({ enabled: !!args.enabled, verbose: !!args.verbose });
|
|
834
|
+
return cleanObject(res);
|
|
835
|
+
}
|
|
836
|
+
case 'set_quality': {
|
|
837
|
+
const res = await tools.performanceTools.setScalability({ category: args.category, level: args.level });
|
|
838
|
+
return cleanObject(res);
|
|
839
|
+
}
|
|
840
|
+
case 'play_sound': {
|
|
841
|
+
await elicitMissingPrimitiveArgs(
|
|
842
|
+
tools,
|
|
843
|
+
args,
|
|
844
|
+
'Provide the audio asset for system_control.play_sound',
|
|
845
|
+
{
|
|
846
|
+
soundPath: {
|
|
847
|
+
type: 'string',
|
|
848
|
+
title: 'Sound Asset Path',
|
|
849
|
+
description: 'Asset path of the sound to play'
|
|
850
|
+
}
|
|
753
851
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
location: args.
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
case 'create_widget':
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
case '
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
case 'engine_quit':
|
|
805
|
-
mappedName = 'quit_editor';
|
|
806
|
-
mappedArgs = {};
|
|
807
|
-
break;
|
|
808
|
-
|
|
852
|
+
);
|
|
853
|
+
const soundPath = requireNonEmptyString(args.soundPath, 'soundPath', 'Missing required parameter: soundPath');
|
|
854
|
+
if (args.location && typeof args.location === 'object') {
|
|
855
|
+
const loc = [args.location.x || 0, args.location.y || 0, args.location.z || 0];
|
|
856
|
+
const res = await tools.audioTools.playSoundAtLocation({ soundPath, location: loc as [number, number, number], volume: args.volume, pitch: args.pitch, startTime: args.startTime });
|
|
857
|
+
return cleanObject(res);
|
|
858
|
+
}
|
|
859
|
+
const res = await tools.audioTools.playSound2D({ soundPath, volume: args.volume, pitch: args.pitch, startTime: args.startTime });
|
|
860
|
+
return cleanObject(res);
|
|
861
|
+
}
|
|
862
|
+
case 'create_widget': {
|
|
863
|
+
await elicitMissingPrimitiveArgs(
|
|
864
|
+
tools,
|
|
865
|
+
args,
|
|
866
|
+
'Provide details for system_control.create_widget',
|
|
867
|
+
{
|
|
868
|
+
widgetName: {
|
|
869
|
+
type: 'string',
|
|
870
|
+
title: 'Widget Name',
|
|
871
|
+
description: 'Name for the new UI widget asset'
|
|
872
|
+
},
|
|
873
|
+
widgetType: {
|
|
874
|
+
type: 'string',
|
|
875
|
+
title: 'Widget Type',
|
|
876
|
+
description: 'Widget type such as HUD, Menu, Overlay, etc.'
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
);
|
|
880
|
+
const widgetName = requireNonEmptyString(args.widgetName ?? args.name, 'widgetName', 'Missing required parameter: widgetName');
|
|
881
|
+
const widgetType = requireNonEmptyString(args.widgetType, 'widgetType', 'Missing required parameter: widgetType');
|
|
882
|
+
const res = await tools.uiTools.createWidget({ name: widgetName, type: widgetType as any, savePath: args.savePath });
|
|
883
|
+
return cleanObject(res);
|
|
884
|
+
}
|
|
885
|
+
case 'show_widget': {
|
|
886
|
+
const res = await tools.uiTools.setWidgetVisibility({ widgetName: args.widgetName, visible: args.visible !== false });
|
|
887
|
+
return cleanObject(res);
|
|
888
|
+
}
|
|
889
|
+
case 'screenshot': {
|
|
890
|
+
const res = await tools.visualTools.takeScreenshot({ resolution: args.resolution });
|
|
891
|
+
return cleanObject(res);
|
|
892
|
+
}
|
|
893
|
+
case 'engine_start': {
|
|
894
|
+
const res = await tools.engineTools.launchEditor({ editorExe: args.editorExe, projectPath: args.projectPath });
|
|
895
|
+
return cleanObject(res);
|
|
896
|
+
}
|
|
897
|
+
case 'engine_quit': {
|
|
898
|
+
const res = await tools.engineTools.quitEditor();
|
|
899
|
+
return cleanObject(res);
|
|
900
|
+
}
|
|
809
901
|
default:
|
|
810
902
|
throw new Error(`Unknown system action: ${args.action}`);
|
|
811
903
|
}
|
|
812
|
-
break;
|
|
813
904
|
|
|
814
905
|
// 10. CONSOLE COMMAND - handle validation here
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
return {
|
|
819
|
-
content: [{
|
|
820
|
-
type: 'text',
|
|
821
|
-
text: 'Empty command ignored'
|
|
822
|
-
}],
|
|
823
|
-
isError: false,
|
|
824
|
-
success: true,
|
|
825
|
-
message: 'Empty command'
|
|
826
|
-
};
|
|
906
|
+
case 'console_command':
|
|
907
|
+
if (!args.command || typeof args.command !== 'string' || args.command.trim() === '') {
|
|
908
|
+
return { success: true, message: 'Empty command' } as any;
|
|
827
909
|
}
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
910
|
+
// Basic safety filter
|
|
911
|
+
const cmd = String(args.command).trim();
|
|
912
|
+
const blocked = [/\bquit\b/i, /\bexit\b/i, /debugcrash/i];
|
|
913
|
+
if (blocked.some(r => r.test(cmd))) {
|
|
914
|
+
return { success: false, error: 'Command blocked for safety' } as any;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const raw = await tools.bridge.executeConsoleCommand(cmd);
|
|
918
|
+
const summary = tools.bridge.summarizeConsoleCommand(cmd, raw);
|
|
919
|
+
const output = summary.output || '';
|
|
920
|
+
const looksInvalid = /unknown|invalid/i.test(output);
|
|
921
|
+
return cleanObject({
|
|
922
|
+
success: summary.returnValue !== false && !looksInvalid,
|
|
923
|
+
command: summary.command,
|
|
924
|
+
output: output || undefined,
|
|
925
|
+
logLines: summary.logLines?.length ? summary.logLines : undefined,
|
|
926
|
+
returnValue: summary.returnValue,
|
|
927
|
+
message: !looksInvalid
|
|
928
|
+
? (output || 'Command executed')
|
|
929
|
+
: undefined,
|
|
930
|
+
error: looksInvalid ? output : undefined,
|
|
931
|
+
raw: summary.raw
|
|
932
|
+
});
|
|
933
|
+
} catch (e: any) {
|
|
934
|
+
return cleanObject({ success: false, command: cmd, error: e?.message || String(e) });
|
|
840
935
|
}
|
|
841
936
|
|
|
842
|
-
mappedName = 'console_command';
|
|
843
|
-
mappedArgs = args;
|
|
844
|
-
break;
|
|
845
937
|
|
|
846
938
|
// 11. REMOTE CONTROL PRESETS - Direct implementation
|
|
847
939
|
case 'manage_rc':
|
|
848
|
-
if (!args.action) throw new Error('Missing required parameter: action');
|
|
849
|
-
|
|
850
940
|
// Handle RC operations directly through RcTools
|
|
851
941
|
let rcResult: any;
|
|
852
|
-
|
|
853
|
-
switch (
|
|
942
|
+
const rcAction = requireAction(args);
|
|
943
|
+
switch (rcAction) {
|
|
854
944
|
// Support both 'create_preset' and 'create' for compatibility
|
|
855
945
|
case 'create_preset':
|
|
856
946
|
case 'create':
|
|
@@ -877,8 +967,10 @@ export async function handleConsolidatedToolCall(
|
|
|
877
967
|
break;
|
|
878
968
|
|
|
879
969
|
case 'delete':
|
|
880
|
-
|
|
881
|
-
|
|
970
|
+
case 'delete_preset':
|
|
971
|
+
const presetIdentifier = args.presetId || args.presetPath;
|
|
972
|
+
if (!presetIdentifier) throw new Error('Missing required parameter: presetId');
|
|
973
|
+
rcResult = await tools.rcTools.deletePreset(presetIdentifier);
|
|
882
974
|
if (rcResult.success) {
|
|
883
975
|
rcResult.message = 'Preset deleted successfully';
|
|
884
976
|
}
|
|
@@ -975,7 +1067,7 @@ export async function handleConsolidatedToolCall(
|
|
|
975
1067
|
break;
|
|
976
1068
|
|
|
977
1069
|
default:
|
|
978
|
-
throw new Error(`Unknown RC action: ${
|
|
1070
|
+
throw new Error(`Unknown RC action: ${rcAction}. Valid actions are: create_preset, expose_actor, expose_property, list_fields, set_property, get_property, or their simplified versions: create, list, delete, expose, get_exposed, set_value, get_value, call_function`);
|
|
979
1071
|
}
|
|
980
1072
|
|
|
981
1073
|
// Return result directly - MCP formatting will be handled by response validator
|
|
@@ -984,14 +1076,13 @@ export async function handleConsolidatedToolCall(
|
|
|
984
1076
|
|
|
985
1077
|
// 12. SEQUENCER / CINEMATICS
|
|
986
1078
|
case 'manage_sequence':
|
|
987
|
-
if (!args.action) throw new Error('Missing required parameter: action');
|
|
988
|
-
|
|
989
1079
|
// Direct handling for sequence operations
|
|
990
1080
|
const seqResult = await (async () => {
|
|
991
1081
|
const sequenceTools = tools.sequenceTools;
|
|
992
1082
|
if (!sequenceTools) throw new Error('Sequence tools not available');
|
|
1083
|
+
const action = requireAction(args);
|
|
993
1084
|
|
|
994
|
-
switch (
|
|
1085
|
+
switch (action) {
|
|
995
1086
|
case 'create':
|
|
996
1087
|
return await sequenceTools.create({ name: args.name, path: args.path });
|
|
997
1088
|
case 'open':
|
|
@@ -1031,7 +1122,7 @@ export async function handleConsolidatedToolCall(
|
|
|
1031
1122
|
if (args.speed === undefined) throw new Error('Missing required parameter: speed');
|
|
1032
1123
|
return await sequenceTools.setPlaybackSpeed({ speed: args.speed });
|
|
1033
1124
|
default:
|
|
1034
|
-
throw new Error(`Unknown sequence action: ${
|
|
1125
|
+
throw new Error(`Unknown sequence action: ${action}`);
|
|
1035
1126
|
}
|
|
1036
1127
|
})();
|
|
1037
1128
|
|
|
@@ -1039,42 +1130,27 @@ export async function handleConsolidatedToolCall(
|
|
|
1039
1130
|
// Clean to prevent circular references
|
|
1040
1131
|
return cleanObject(seqResult);
|
|
1041
1132
|
// 13. INTROSPECTION
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
case 'inspect_object':
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
case 'set_property':
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1133
|
+
case 'inspect':
|
|
1134
|
+
const inspectAction = requireAction(args);
|
|
1135
|
+
switch (inspectAction) {
|
|
1136
|
+
case 'inspect_object': {
|
|
1137
|
+
const res = await tools.introspectionTools.inspectObject({ objectPath: args.objectPath, detailed: args.detailed });
|
|
1138
|
+
return cleanObject(res);
|
|
1139
|
+
}
|
|
1140
|
+
case 'set_property': {
|
|
1141
|
+
const res = await tools.introspectionTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value });
|
|
1142
|
+
return cleanObject(res);
|
|
1143
|
+
}
|
|
1053
1144
|
default:
|
|
1054
|
-
throw new Error(`Unknown inspect action: ${
|
|
1145
|
+
throw new Error(`Unknown inspect action: ${inspectAction}`);
|
|
1055
1146
|
}
|
|
1056
|
-
|
|
1147
|
+
|
|
1057
1148
|
|
|
1058
1149
|
default:
|
|
1059
1150
|
throw new Error(`Unknown consolidated tool: ${name}`);
|
|
1060
1151
|
}
|
|
1061
1152
|
|
|
1062
|
-
|
|
1063
|
-
const TOOL_TIMEOUT = 15000; // 15 seconds timeout for tool execution
|
|
1064
|
-
|
|
1065
|
-
const toolPromise = handleToolCall(mappedName, mappedArgs, tools);
|
|
1066
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
1067
|
-
setTimeout(() => {
|
|
1068
|
-
reject(new Error(`Tool execution timeout after ${TOOL_TIMEOUT}ms`));
|
|
1069
|
-
}, TOOL_TIMEOUT);
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
const result = await Promise.race([toolPromise, timeoutPromise]);
|
|
1073
|
-
const duration = Date.now() - startTime;
|
|
1074
|
-
console.log(`[ConsolidatedToolHandler] Completed execution of ${name} in ${duration}ms`);
|
|
1075
|
-
|
|
1076
|
-
// Clean the result to prevent circular reference errors
|
|
1077
|
-
return cleanObject(result);
|
|
1153
|
+
// All cases return (or throw) above; this is a type guard for exhaustiveness.
|
|
1078
1154
|
|
|
1079
1155
|
} catch (err: any) {
|
|
1080
1156
|
const duration = Date.now() - startTime;
|