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