sbox-mcp-server 1.9.0 → 1.11.0
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/dist/index.js +2 -0
- package/dist/tools/characters.js +19 -7
- package/dist/tools/components.js +23 -0
- package/dist/tools/diagnostics.js +56 -20
- package/dist/tools/gameobjects.js +14 -6
- package/dist/tools/gameplay.js +79 -4
- package/dist/tools/inputs.d.ts +18 -0
- package/dist/tools/inputs.js +44 -0
- package/dist/tools/inspection.js +5 -2
- package/dist/tools/leveltools.js +23 -10
- package/dist/tools/npc.js +12 -6
- package/dist/tools/objecttools.js +27 -7
- package/dist/tools/physics.d.ts +0 -4
- package/dist/tools/physics.js +18 -15
- package/dist/tools/playmode.js +79 -0
- package/dist/tools/prefabs.js +9 -6
- package/dist/tools/templates.js +5 -2
- package/dist/tools/ui.js +5 -2
- package/dist/tools/visuals.js +19 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ import { registerSelfTestTools } from "./tools/selftest.js";
|
|
|
44
44
|
import { registerGameplayTools } from "./tools/gameplay.js";
|
|
45
45
|
import { registerNpcTools } from "./tools/npc.js";
|
|
46
46
|
import { registerInspectionTools } from "./tools/inspection.js";
|
|
47
|
+
import { registerInputTools } from "./tools/inputs.js";
|
|
47
48
|
// ── CLI flags ──────────────────────────────────────────────────────
|
|
48
49
|
const args = process.argv.slice(2);
|
|
49
50
|
/** Read the package version from package.json, or return "unknown" on failure. */
|
|
@@ -191,6 +192,7 @@ registerSelfTestTools(server, bridge);
|
|
|
191
192
|
registerGameplayTools(server, bridge);
|
|
192
193
|
registerNpcTools(server, bridge);
|
|
193
194
|
registerInspectionTools(server, bridge);
|
|
195
|
+
registerInputTools(server, bridge);
|
|
194
196
|
/** Start the MCP server on stdio and attempt initial Bridge connection. */
|
|
195
197
|
async function main() {
|
|
196
198
|
const transport = new StdioServerTransport();
|
package/dist/tools/characters.js
CHANGED
|
@@ -9,17 +9,28 @@ import { z } from "zod";
|
|
|
9
9
|
* change, take_screenshot and read the result. Animation *playback* is
|
|
10
10
|
* runtime, but poses preview in-editor via PlayAnimationsInEditorScene.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Colour / vector accepted as EITHER an object OR a comma string, passed
|
|
13
|
+
// through unchanged. The C# handler parses both forms (source of truth). See
|
|
14
|
+
// the cross-language vector/color contract.
|
|
15
|
+
const ColorObject = z.object({
|
|
14
16
|
r: z.number().min(0).describe("Red, 0-1"),
|
|
15
17
|
g: z.number().min(0).describe("Green, 0-1"),
|
|
16
18
|
b: z.number().min(0).describe("Blue, 0-1"),
|
|
17
19
|
a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
+
});
|
|
21
|
+
const ColorSchema = z
|
|
22
|
+
.union([
|
|
23
|
+
ColorObject,
|
|
24
|
+
z.string().describe('Comma string "r,g,b,a", e.g. "1,0,0,1"'),
|
|
25
|
+
])
|
|
26
|
+
.describe('RGBA colour — object {r,g,b,a} (0-1) OR comma string "r,g,b,a" (model tint)');
|
|
27
|
+
const Vector3Object = z.object({ x: z.number(), y: z.number(), z: z.number() });
|
|
20
28
|
const Vector3Schema = z
|
|
21
|
-
.
|
|
22
|
-
|
|
29
|
+
.union([
|
|
30
|
+
Vector3Object,
|
|
31
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
32
|
+
])
|
|
33
|
+
.describe('World vector — object {x,y,z} OR comma string "x,y,z"');
|
|
23
34
|
const RotationSchema = z
|
|
24
35
|
.object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
|
|
25
36
|
.describe("Rotation {pitch,yaw,roll} in degrees");
|
|
@@ -241,8 +252,9 @@ export function registerCharacterTools(server, bridge) {
|
|
|
241
252
|
z.number(),
|
|
242
253
|
z.boolean(),
|
|
243
254
|
z.object({ x: z.number(), y: z.number(), z: z.number() }),
|
|
255
|
+
z.string().describe('Vector as a comma string "x,y,z"'),
|
|
244
256
|
])
|
|
245
|
-
.describe(
|
|
257
|
+
.describe('Value: number (float), boolean, or a vector as {x,y,z} OR a comma string "x,y,z"'),
|
|
246
258
|
type: z
|
|
247
259
|
.enum(["float", "int", "bool", "vector"])
|
|
248
260
|
.optional()
|
package/dist/tools/components.js
CHANGED
|
@@ -74,5 +74,28 @@ export function registerComponentTools(server, bridge) {
|
|
|
74
74
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
75
75
|
};
|
|
76
76
|
});
|
|
77
|
+
// ── invoke_method ────────────────────────────────────────────────
|
|
78
|
+
server.tool("invoke_method", "Call a public method BY NAME on a component of a live scene GameObject, passing ARGUMENTS. The with-args sibling of invoke_button (which only calls parameterless [Button]/methods on a scene component). Finds a public method matching name + arg-count, coerces each JSON arg to the parameter type (primitives/enums; Color/Vector3 as comma strings '1,0,0,1'; asset refs as a path; GameObject/Component refs as a target GUID), invokes it, and returns the method's return value as a string (null for void). Returns success=false with a clear error on resolve/coerce/throw", {
|
|
79
|
+
id: z.string().describe("GUID of the GameObject"),
|
|
80
|
+
component: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Component type name to target (e.g. 'Health', 'PlayerController'). Omit to search all components on the object for a method matching name + arg-count"),
|
|
84
|
+
method: z
|
|
85
|
+
.string()
|
|
86
|
+
.describe("Name of the public method to call (e.g. 'TakeDamage', 'AddGold')"),
|
|
87
|
+
args: z
|
|
88
|
+
.array(z.unknown())
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Ordered arguments, each coerced to the matching parameter's type. Numbers/bools/strings pass through; Color/Vector3/Rotation as comma strings '1,0,0,1'; enum member names; ASSET refs as a path ('models/dev/box.vmdl'); GameObject/Component refs as a target GUID. Omit (or []) for a no-arg method"),
|
|
91
|
+
}, async (params) => {
|
|
92
|
+
const res = await bridge.send("invoke_method", params);
|
|
93
|
+
if (!res.success) {
|
|
94
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
98
|
+
};
|
|
99
|
+
});
|
|
77
100
|
}
|
|
78
101
|
//# sourceMappingURL=components.js.map
|
|
@@ -94,6 +94,15 @@ function tailLines(text, n) {
|
|
|
94
94
|
const lines = text.split(/\r?\n/);
|
|
95
95
|
return lines.slice(Math.max(0, lines.length - n));
|
|
96
96
|
}
|
|
97
|
+
// A 3D point accepted as EITHER an object {x,y,z} OR a comma string "x,y,z",
|
|
98
|
+
// passed through unchanged. The C# handler parses both forms (source of truth).
|
|
99
|
+
// See the cross-language vector/color contract.
|
|
100
|
+
const Vector3Schema = z
|
|
101
|
+
.union([
|
|
102
|
+
z.object({ x: z.number(), y: z.number(), z: z.number() }),
|
|
103
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
104
|
+
])
|
|
105
|
+
.describe('World point — object {x,y,z} OR comma string "x,y,z"');
|
|
97
106
|
export function registerDiagnosticTools(server, bridge) {
|
|
98
107
|
// ── read_log ───────────────────────────────────────────────────────
|
|
99
108
|
server.tool("read_log", "Read s&box's editor log (sbox-dev.log) so Claude can see compile errors, exceptions, and Log.Info output directly. Reads the log file (not via the bridge), so it works even when the editor has crashed. If auto-detection fails (non-Windows / non-Steam install), set the SBOX_LOG_PATH environment variable to the full log path.", {
|
|
@@ -142,7 +151,7 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
142
151
|
return { content: [{ type: "text", text: header + out.join("\n") }] };
|
|
143
152
|
});
|
|
144
153
|
// ── get_compile_errors ─────────────────────────────────────────────
|
|
145
|
-
server.tool("get_compile_errors", "Scan the recent s&box log for compile errors and exceptions — the fast way for Claude to confirm whether its last script/addon edit actually compiled. Reads sbox-dev.log directly (works even if the editor is mid-crash). Returns the
|
|
154
|
+
server.tool("get_compile_errors", "Scan the recent s&box log for compile errors and exceptions — the fast way for Claude to confirm whether its last script/addon edit actually compiled. Reads sbox-dev.log directly (works even if the editor is mid-crash). Filters out the noisy 'Broken Reference: package.local.* (the compiler failed)' cascade (which masks the real cause) and surfaces the underlying '[Generic] Error | ...CSxxxx... file:line' diagnostics. Returns the real error lines, or an all-clear.", {
|
|
146
155
|
lines: z
|
|
147
156
|
.number()
|
|
148
157
|
.int()
|
|
@@ -175,9 +184,38 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
175
184
|
};
|
|
176
185
|
}
|
|
177
186
|
const recent = tailLines(text, n);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
187
|
+
// The cascade: when a project's code fails to compile, every dependent
|
|
188
|
+
// package (including the bridge's own editor assembly) emits a
|
|
189
|
+
// "Broken Reference: package.local.<x> (the compiler failed)"
|
|
190
|
+
// line. There can be dozens of these and they MASK the real diagnostic —
|
|
191
|
+
// so we drop them and surface the actual CSxxxx / [Generic] Error lines.
|
|
192
|
+
const cascadeRe = /Broken Reference:.*\(the compiler failed\)/i;
|
|
193
|
+
// Real compile diagnostics. We accept:
|
|
194
|
+
// - any line carrying a C# error code (error CS#### or "CS#### | file:line")
|
|
195
|
+
// - the "[Generic] Error | ..." diagnostic lines s&box emits
|
|
196
|
+
// - genuine compile-failure / exception markers
|
|
197
|
+
// Whitelist (always surface, even with NO file path), e.g. a bare
|
|
198
|
+
// location like "- :352,1" that s&box prints for project-level errors.
|
|
199
|
+
const realErrorRe = /(error CS\d+|\bCS\d{3,5}\b|\[Generic\]\s*Error|Compile of .* Failed|Couldn't add project|Unhandled [Ee]xception|^\s*at Sandbox\.|StackTrace)/;
|
|
200
|
+
const noFileWhitelistRe = /^\s*-\s*:\d+,\d+/; // e.g. "- :352,1"
|
|
201
|
+
const cascadeLines = recent.filter((l) => cascadeRe.test(l));
|
|
202
|
+
const realHits = recent.filter((l) => !cascadeRe.test(l) && (realErrorRe.test(l) || noFileWhitelistRe.test(l)));
|
|
203
|
+
if (realHits.length === 0) {
|
|
204
|
+
if (cascadeLines.length > 0) {
|
|
205
|
+
// We saw the masking cascade but none of the underlying diagnostics
|
|
206
|
+
// fell within the scanned window — point Claude at the fuller log.
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: `Saw ${cascadeLines.length} "Broken Reference: package.local.* (the compiler failed)" cascade line(s), ` +
|
|
212
|
+
`but the real compile error isn't in the last ${n} lines (the cascade masks it). ` +
|
|
213
|
+
`The underlying CSxxxx / [Generic] Error line is likely just above — call read_log ` +
|
|
214
|
+
`with more lines (e.g. lines: 1000) to find it, or get_compile_errors with a larger 'lines'.\n(${path})`,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
181
219
|
return {
|
|
182
220
|
content: [
|
|
183
221
|
{
|
|
@@ -187,11 +225,14 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
187
225
|
],
|
|
188
226
|
};
|
|
189
227
|
}
|
|
228
|
+
const suffix = cascadeLines.length > 0
|
|
229
|
+
? `\n\n(Filtered out ${cascadeLines.length} "Broken Reference … (the compiler failed)" cascade line(s) that mask the real cause.)`
|
|
230
|
+
: "";
|
|
190
231
|
return {
|
|
191
232
|
content: [
|
|
192
233
|
{
|
|
193
234
|
type: "text",
|
|
194
|
-
text: `Found ${
|
|
235
|
+
text: `Found ${realHits.length} real error line(s) in the last ${n} log lines:\n\n${realHits.join("\n")}${suffix}`,
|
|
195
236
|
},
|
|
196
237
|
],
|
|
197
238
|
};
|
|
@@ -199,10 +240,9 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
199
240
|
// ── frame_camera ─────────────────────────────────────────────────── (bridge)
|
|
200
241
|
server.tool("frame_camera", "Aim the s&box EDITOR viewport camera at a GameObject (by id) or a world point (position + optional radius), then call take_screenshot to capture that view. This is how Claude points its own screenshots at what it's working on — frame a spawned object, then screenshot to verify it actually looks right.", {
|
|
201
242
|
id: z.string().optional().describe("GUID of a GameObject to frame on"),
|
|
202
|
-
position:
|
|
203
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
243
|
+
position: Vector3Schema
|
|
204
244
|
.optional()
|
|
205
|
-
.describe(
|
|
245
|
+
.describe('World point to frame on (use instead of id) — object {x,y,z} or comma string "x,y,z"'),
|
|
206
246
|
radius: z
|
|
207
247
|
.number()
|
|
208
248
|
.optional()
|
|
@@ -219,14 +259,12 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
219
259
|
// ── screenshot_from ──────────────────────────────────────────────── (bridge)
|
|
220
260
|
server.tool("screenshot_from", "Take a screenshot from a chosen angle. take_screenshot is locked to the scene's main camera; this temporarily moves that camera to frame your target, captures, then restores it — so Claude can finally AIM its own screenshots. Pass id (frame an object) OR position {x,y,z} with optional lookAt {x,y,z} or rotation {pitch,yaw,roll}. After it returns, read the newest PNG in the editor's screenshots folder.", {
|
|
221
261
|
id: z.string().optional().describe("GUID of a GameObject to frame"),
|
|
222
|
-
position:
|
|
223
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
262
|
+
position: Vector3Schema
|
|
224
263
|
.optional()
|
|
225
|
-
.describe(
|
|
226
|
-
lookAt:
|
|
227
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
264
|
+
.describe('Camera world position (use instead of id) — object {x,y,z} or comma string "x,y,z"'),
|
|
265
|
+
lookAt: Vector3Schema
|
|
228
266
|
.optional()
|
|
229
|
-
.describe(
|
|
267
|
+
.describe('World point to look at (pair with position) — object {x,y,z} or comma string "x,y,z"'),
|
|
230
268
|
rotation: z
|
|
231
269
|
.object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
|
|
232
270
|
.optional()
|
|
@@ -458,14 +496,12 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
458
496
|
// ── capture_view ──────────────────────────────────────────────────── (bridge, Batch 34)
|
|
459
497
|
server.tool("capture_view", "Capture a PNG of the scene from a camera — and crucially this WORKS IN PLAY MODE, capturing the RUNNING game (via CameraComponent.RenderToBitmap, unlike take_screenshot/screenshot_from which are edit-only). With no args it renders the live main camera = the player's POV (incl. HUD). Pass position {x,y,z} (+ lookAt or rotation) or id (a GameObject to frame) to capture from a temporary camera that never disturbs the game's own camera. Returns the saved PNG's absolute 'path' — READ it to see the result.", {
|
|
460
498
|
id: z.string().optional().describe("GUID of a GameObject to frame (uses a temp camera)"),
|
|
461
|
-
position:
|
|
462
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
499
|
+
position: Vector3Schema
|
|
463
500
|
.optional()
|
|
464
|
-
.describe(
|
|
465
|
-
lookAt:
|
|
466
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
501
|
+
.describe('Camera world position (temp camera; use instead of id) — object {x,y,z} or comma string "x,y,z"'),
|
|
502
|
+
lookAt: Vector3Schema
|
|
467
503
|
.optional()
|
|
468
|
-
.describe(
|
|
504
|
+
.describe('World point to look at (pair with position) — object {x,y,z} or comma string "x,y,z"'),
|
|
469
505
|
rotation: z
|
|
470
506
|
.object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
|
|
471
507
|
.optional()
|
|
@@ -9,13 +9,21 @@ import { z } from "zod";
|
|
|
9
9
|
* Uses shared Zod schemas (Vector3Schema, RotationSchema) for consistent
|
|
10
10
|
* Vector3 and Rotation parameter validation across multiple tools.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// A 3D vector accepted as EITHER an object {x,y,z} OR a comma string "x,y,z".
|
|
13
|
+
// The value is passed through to the bridge unchanged; the C# handler parses
|
|
14
|
+
// both forms (C# is the source of truth for parsing). See the cross-language
|
|
15
|
+
// vector/color contract.
|
|
16
|
+
const Vector3Object = z.object({
|
|
14
17
|
x: z.number().describe("X coordinate"),
|
|
15
18
|
y: z.number().describe("Y coordinate"),
|
|
16
19
|
z: z.number().describe("Z coordinate"),
|
|
17
|
-
})
|
|
18
|
-
|
|
20
|
+
});
|
|
21
|
+
const Vector3Schema = z
|
|
22
|
+
.union([
|
|
23
|
+
Vector3Object,
|
|
24
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
25
|
+
])
|
|
26
|
+
.describe('3D vector — object {x,y,z} OR comma string "x,y,z"');
|
|
19
27
|
const RotationSchema = z
|
|
20
28
|
.object({
|
|
21
29
|
pitch: z.number().describe("Pitch angle in degrees"),
|
|
@@ -35,7 +43,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
35
43
|
scale: z
|
|
36
44
|
.union([z.number(), Vector3Schema])
|
|
37
45
|
.optional()
|
|
38
|
-
.describe(
|
|
46
|
+
.describe('Uniform scale (number) or per-axis scale — object {x,y,z} or comma string "x,y,z"'),
|
|
39
47
|
parent: z
|
|
40
48
|
.string()
|
|
41
49
|
.optional()
|
|
@@ -126,7 +134,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
126
134
|
scale: z
|
|
127
135
|
.union([z.number(), Vector3Schema])
|
|
128
136
|
.optional()
|
|
129
|
-
.describe(
|
|
137
|
+
.describe('New scale — uniform number, per-axis object {x,y,z}, or comma string "x,y,z"'),
|
|
130
138
|
local: z
|
|
131
139
|
.boolean()
|
|
132
140
|
.optional()
|
package/dist/tools/gameplay.js
CHANGED
|
@@ -14,13 +14,20 @@ import { z } from "zod";
|
|
|
14
14
|
* The `sbox-scaffold-game` skill orchestrates these into a playable starter.
|
|
15
15
|
* All scene/file-mutating; refused during play mode by the bridge dispatch.
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// A 3D vector accepted as EITHER an object {x,y,z} OR a comma string "x,y,z",
|
|
18
|
+
// passed through unchanged. The C# handler parses both forms (source of truth).
|
|
19
|
+
// See the cross-language vector/color contract.
|
|
20
|
+
const Vector3Object = z.object({
|
|
19
21
|
x: z.number().describe("X coordinate"),
|
|
20
22
|
y: z.number().describe("Y coordinate"),
|
|
21
23
|
z: z.number().describe("Z coordinate"),
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
+
});
|
|
25
|
+
const Vector3Schema = z
|
|
26
|
+
.union([
|
|
27
|
+
Vector3Object,
|
|
28
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
29
|
+
])
|
|
30
|
+
.describe('3D vector — object {x,y,z} OR comma string "x,y,z"');
|
|
24
31
|
const RotationSchema = z
|
|
25
32
|
.object({
|
|
26
33
|
pitch: z.number().describe("Pitch angle in degrees"),
|
|
@@ -182,6 +189,74 @@ export function registerGameplayTools(server, bridge) {
|
|
|
182
189
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
183
190
|
};
|
|
184
191
|
});
|
|
192
|
+
// ── create_economy_wallet ─────────────────────────────────────────
|
|
193
|
+
server.tool("create_economy_wallet", "Generate a host-authoritative currency Wallet component: a [Sync(SyncFlags.FromHost)] Money balance (only the host can write it — plain [Sync] money is the classic economy exploit) with AddMoney / TrySpend / SetMoney / CanAfford and an OnMoneyChanged event. Single-player safe. Optionally attached to an existing GameObject by GUID (after a hotload). Pairs with a save system for persistence. Mined from the most-requested currency pattern across 51 games.", {
|
|
194
|
+
name: z.string().optional().describe("Class name. Defaults to 'Wallet'"),
|
|
195
|
+
directory: z
|
|
196
|
+
.string()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe("Subdirectory for the .cs file. Defaults to 'Code'"),
|
|
199
|
+
startingMoney: z
|
|
200
|
+
.number()
|
|
201
|
+
.int()
|
|
202
|
+
.optional()
|
|
203
|
+
.describe("Initial balance the host seeds on start. Defaults to 0"),
|
|
204
|
+
targetId: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("GUID of an existing GameObject to attach the Wallet to (only attaches if the type is already loaded — hotload first)"),
|
|
208
|
+
}, async (params) => {
|
|
209
|
+
const res = await bridge.send("create_economy_wallet", params);
|
|
210
|
+
if (!res.success) {
|
|
211
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
// ── create_round_phase_machine ────────────────────────────────────
|
|
218
|
+
server.tool("create_round_phase_machine", "Generate a host-authoritative round/phase machine: a [Sync(SyncFlags.FromHost)] CurrentPhase cycled through your named phases on a per-phase timer (host-only), with a static OnPhaseChanged event that fires on every machine. Great for round/match flow, match phases, or a day/night cycle. Single-player safe. Optionally attached to an existing GameObject by GUID (after a hotload). Mined from the round-flow pattern across the 51 games.", {
|
|
219
|
+
name: z.string().optional().describe("Class name. Defaults to 'GameDirector'"),
|
|
220
|
+
directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
|
|
221
|
+
phases: z
|
|
222
|
+
.array(z.string())
|
|
223
|
+
.optional()
|
|
224
|
+
.describe('Ordered phase names (become an enum), e.g. ["Lobby","Day","Night","Payout"]. Defaults to ["Lobby","Active","Ended"]'),
|
|
225
|
+
duration: z
|
|
226
|
+
.number()
|
|
227
|
+
.optional()
|
|
228
|
+
.describe("Default seconds per phase (each phase also gets its own tunable [Property]). Defaults to 60"),
|
|
229
|
+
loop: z
|
|
230
|
+
.boolean()
|
|
231
|
+
.optional()
|
|
232
|
+
.describe("Loop back to the first phase after the last (true) or hold on the last phase (false). Defaults to true"),
|
|
233
|
+
targetId: z
|
|
234
|
+
.string()
|
|
235
|
+
.optional()
|
|
236
|
+
.describe("GUID of an existing GameObject to attach to (only if the type is already loaded — hotload first)"),
|
|
237
|
+
}, async (params) => {
|
|
238
|
+
const res = await bridge.send("create_round_phase_machine", params);
|
|
239
|
+
if (!res.success) {
|
|
240
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
241
|
+
}
|
|
242
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
243
|
+
});
|
|
244
|
+
// ── create_day_night_clock ────────────────────────────────────────
|
|
245
|
+
server.tool("create_day_night_clock", "Generate a host-authoritative time-of-day clock: [Sync(SyncFlags.FromHost)] TimeOfDay (0–24) + Day advancing by Time.Delta, IsDay/IsNight from sunrise/sunset hours, and static OnNewDay / OnDayNightChanged events to drive lighting, NPC schedules, or spawns. Single-player safe. Pairs with create_round_phase_machine. Optionally attached to a GameObject by GUID (after a hotload).", {
|
|
246
|
+
name: z.string().optional().describe("Class name. Defaults to 'DayNightClock'"),
|
|
247
|
+
directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
|
|
248
|
+
dayLengthSeconds: z.number().optional().describe("Real seconds per in-game day. Defaults to 600 (10 min)"),
|
|
249
|
+
startHour: z.number().optional().describe("Hour the clock starts at (0–24). Defaults to 8"),
|
|
250
|
+
sunriseHour: z.number().optional().describe("Hour day begins. Defaults to 6"),
|
|
251
|
+
sunsetHour: z.number().optional().describe("Hour night begins. Defaults to 20"),
|
|
252
|
+
targetId: z.string().optional().describe("GUID of an existing GameObject to attach to (hotload first)"),
|
|
253
|
+
}, async (params) => {
|
|
254
|
+
const res = await bridge.send("create_day_night_clock", params);
|
|
255
|
+
if (!res.success) {
|
|
256
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
257
|
+
}
|
|
258
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
259
|
+
});
|
|
185
260
|
// ── create_pickup ─────────────────────────────────────────────────
|
|
186
261
|
server.tool("create_pickup", "Generate a trigger-based collectible component. On enter by a tagged object it raises OnCollected (wire it to your objective/score system) and despawns. Optionally builds a visible pickup GameObject with a trigger SphereCollider (+ a model) in one call", {
|
|
187
262
|
name: z.string().optional().describe("Class name. Defaults to 'Pickup'"),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Input action tools — register the custom named verbs a scaffolded game needs.
|
|
5
|
+
*
|
|
6
|
+
* s&box stores input actions in the project's `.sbproj` under
|
|
7
|
+
* `Metadata.InputSettings.Actions[]` (each: { Name, KeyboardCode, GamepadCode?,
|
|
8
|
+
* GroupName }). A game that defines NO actions is handed a default set by the
|
|
9
|
+
* engine (Forward/Back/Left/Right/Jump/Use/...). The catch: the moment a game
|
|
10
|
+
* writes its own InputSettings block, that block becomes the authoritative
|
|
11
|
+
* full list — so the handler seeds the full default set when none exists, then
|
|
12
|
+
* appends the custom action, so movement/use survive.
|
|
13
|
+
*
|
|
14
|
+
* ensure_input_action — add a named action (e.g. "interact") if missing so
|
|
15
|
+
* Input.Pressed("interact") actually fires in play mode.
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerInputTools(server: McpServer, bridge: BridgeClient): void;
|
|
18
|
+
//# sourceMappingURL=inputs.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Input action tools — register the custom named verbs a scaffolded game needs.
|
|
4
|
+
*
|
|
5
|
+
* s&box stores input actions in the project's `.sbproj` under
|
|
6
|
+
* `Metadata.InputSettings.Actions[]` (each: { Name, KeyboardCode, GamepadCode?,
|
|
7
|
+
* GroupName }). A game that defines NO actions is handed a default set by the
|
|
8
|
+
* engine (Forward/Back/Left/Right/Jump/Use/...). The catch: the moment a game
|
|
9
|
+
* writes its own InputSettings block, that block becomes the authoritative
|
|
10
|
+
* full list — so the handler seeds the full default set when none exists, then
|
|
11
|
+
* appends the custom action, so movement/use survive.
|
|
12
|
+
*
|
|
13
|
+
* ensure_input_action — add a named action (e.g. "interact") if missing so
|
|
14
|
+
* Input.Pressed("interact") actually fires in play mode.
|
|
15
|
+
*/
|
|
16
|
+
export function registerInputTools(server, bridge) {
|
|
17
|
+
// ── ensure_input_action ───────────────────────────────────────────
|
|
18
|
+
server.tool("ensure_input_action", 'Register a custom named INPUT ACTION in the project so a generated game\'s custom verbs work in play mode. Writes to <project>.sbproj → Metadata.InputSettings.Actions[]. Idempotent: if the action already exists it is left alone (pass update=true to rebind its key). If the project has no InputSettings yet, the full DEFAULT action set (Forward/Back/Left/Right/Jump/Use/attack1/...) is seeded first so player movement/use are preserved — the engine only auto-injects defaults when a game defines NONE. After adding, call it from game code with Input.Pressed("name") / Input.Down("name") / Input.Released("name"). Note: input config is read at project load, so restart_editor (or reload the project) for a new action to take effect in play mode.', {
|
|
19
|
+
name: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe('The action verb game code will call, e.g. "interact", "sprint", "drop". Matches Input.Pressed("interact").'),
|
|
22
|
+
keyboardKey: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('Default keyboard binding, e.g. "e", "f", "space", "mouse1", "shift". Omit to add the action with no default key (player can bind it).'),
|
|
26
|
+
group: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('UI group the action is listed under in the bindings menu (e.g. "Actions", "Movement", "Other"). Defaults to "Actions".'),
|
|
30
|
+
update: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("If the action already exists, rebind its keyboardKey to the provided value instead of leaving it untouched. Default false (idempotent no-op when present)."),
|
|
34
|
+
}, async (params) => {
|
|
35
|
+
const res = await bridge.send("ensure_input_action", params);
|
|
36
|
+
if (!res.success) {
|
|
37
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=inputs.js.map
|
package/dist/tools/inspection.js
CHANGED
|
@@ -87,9 +87,12 @@ export function registerInspectionTools(server, bridge) {
|
|
|
87
87
|
.default("press")
|
|
88
88
|
.describe("press = one tick (Pressed); hold = held for durationMs; release = clear a held action"),
|
|
89
89
|
analogMove: z
|
|
90
|
-
.
|
|
90
|
+
.union([
|
|
91
|
+
z.object({ x: z.number(), y: z.number(), z: z.number().default(0) }),
|
|
92
|
+
z.string().describe('Comma string "x,y,z", e.g. "1,0,0"'),
|
|
93
|
+
])
|
|
91
94
|
.optional()
|
|
92
|
-
.describe(
|
|
95
|
+
.describe('AnalogMove vector to apply (forward/left) — object {x,y,z} or comma string "x,y,z"'),
|
|
93
96
|
analogLook: z
|
|
94
97
|
.object({ pitch: z.number(), yaw: z.number(), roll: z.number().default(0) })
|
|
95
98
|
.optional()
|
package/dist/tools/leveltools.js
CHANGED
|
@@ -4,9 +4,28 @@ import { z } from "zod";
|
|
|
4
4
|
* grid-duplicate, and measure. Transform-level operations for arranging a
|
|
5
5
|
* scene — all verifiable via the editor (screenshot or hierarchy/state).
|
|
6
6
|
*/
|
|
7
|
+
// Vector / colour accepted as EITHER an object OR a comma string, passed
|
|
8
|
+
// through unchanged. The C# handler parses both forms (source of truth). See
|
|
9
|
+
// the cross-language vector/color contract.
|
|
10
|
+
const Vector3Object = z.object({ x: z.number(), y: z.number(), z: z.number() });
|
|
7
11
|
const Vector3Schema = z
|
|
8
|
-
.
|
|
9
|
-
|
|
12
|
+
.union([
|
|
13
|
+
Vector3Object,
|
|
14
|
+
z.string().describe('Comma string "x,y,z", e.g. "100,100,100"'),
|
|
15
|
+
])
|
|
16
|
+
.describe('Vector — object {x,y,z} OR comma string "x,y,z"');
|
|
17
|
+
const ColorObject = z.object({
|
|
18
|
+
r: z.number().min(0),
|
|
19
|
+
g: z.number().min(0),
|
|
20
|
+
b: z.number().min(0),
|
|
21
|
+
a: z.number().min(0).max(1).optional(),
|
|
22
|
+
});
|
|
23
|
+
const ColorSchema = z
|
|
24
|
+
.union([
|
|
25
|
+
ColorObject,
|
|
26
|
+
z.string().describe('Comma string "r,g,b,a", e.g. "1,0,0,1"'),
|
|
27
|
+
])
|
|
28
|
+
.describe('RGBA colour — object {r,g,b,a} (0-1) OR comma string "r,g,b,a"');
|
|
10
29
|
export function registerLevelTools(server, bridge) {
|
|
11
30
|
// ── snap_to_ground ─────────────────────────────────────────────────
|
|
12
31
|
server.tool("snap_to_ground", "Drop a GameObject straight down onto the surface below it (physics raycast). Works best on collider-less props (an object with its own collider may self-hit). Optional offset lifts it off the surface.", {
|
|
@@ -94,15 +113,9 @@ export function registerLevelTools(server, bridge) {
|
|
|
94
113
|
snapToGround: z.boolean().optional().describe("Raycast each onto the surface below (default true)"),
|
|
95
114
|
scaleMin: z.number().optional().describe("Min uniform scale (default 1)"),
|
|
96
115
|
scaleMax: z.number().optional().describe("Max uniform scale (default 1; set >min for size variation)"),
|
|
97
|
-
tint:
|
|
98
|
-
.object({
|
|
99
|
-
r: z.number().min(0),
|
|
100
|
-
g: z.number().min(0),
|
|
101
|
-
b: z.number().min(0),
|
|
102
|
-
a: z.number().min(0).max(1).optional(),
|
|
103
|
-
})
|
|
116
|
+
tint: ColorSchema
|
|
104
117
|
.optional()
|
|
105
|
-
.describe(
|
|
118
|
+
.describe('Tint applied to every copy — object {r,g,b,a} or comma string "r,g,b,a"'),
|
|
106
119
|
seed: z.number().int().optional().describe("PRNG seed for a reproducible layout (default 1)"),
|
|
107
120
|
group: z.boolean().optional().describe("Parent all copies under one group object (default true)"),
|
|
108
121
|
name: z.string().optional().describe("Base name for the props/group (default 'Prop')"),
|
package/dist/tools/npc.js
CHANGED
|
@@ -16,13 +16,19 @@ import { z } from "zod";
|
|
|
16
16
|
* Mirrors the templates.ts / navigation.ts module shape: zod params, one
|
|
17
17
|
* bridge.send per tool, JSON.stringify(res.data) on success.
|
|
18
18
|
*/
|
|
19
|
+
// A world-space Vector3 accepted as EITHER an object {x,y,z} OR a comma string
|
|
20
|
+
// "x,y,z", passed through unchanged. The C# handler parses both forms (source of
|
|
21
|
+
// truth). See the cross-language vector/color contract.
|
|
19
22
|
const Vec3 = z
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
.union([
|
|
24
|
+
z.object({
|
|
25
|
+
x: z.number().describe("X"),
|
|
26
|
+
y: z.number().describe("Y"),
|
|
27
|
+
z: z.number().describe("Z"),
|
|
28
|
+
}),
|
|
29
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
30
|
+
])
|
|
31
|
+
.describe('A world-space Vector3 — object {x,y,z} OR comma string "x,y,z"');
|
|
26
32
|
export function registerNpcTools(server, bridge) {
|
|
27
33
|
// ── create_npc_brain ──────────────────────────────────────────────
|
|
28
34
|
server.tool("create_npc_brain", "Generate an NpcBrain Component: a behavior state machine (Idle/Patrol/Wander/Chase/Search/Flee/Ambush) driven by occlusion-aware perception — FOV cone + sight range + a line-of-sight trace (respects walls/trees) + proximity hearing — with last-known-position memory (lose-LOS -> search -> give up -> resume). This is the decision layer on top of bake_navmesh / NavMeshAgent movement. Pick a behavior preset, then tune via the generated [Property] fields with set_property. After generating: trigger_hotload + get_compile_errors, place a route with place_patrol_route + assign_patrol_route, bake_navmesh, and verify perception in EDIT mode with simulate_npc_perception (chase/search behavior needs play mode). The component is added to a GameObject like any other; it auto-adds a NavMeshAgent in OnStart.", {
|
|
@@ -5,14 +5,22 @@ import { z } from "zod";
|
|
|
5
5
|
* the composable workhorse — query GUIDs, then feed them to align/distribute/
|
|
6
6
|
* set_tint/group/etc.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// A colour accepted as EITHER an object {r,g,b,a} OR a comma string "r,g,b,a".
|
|
9
|
+
// The value is passed through to the bridge unchanged; the C# handler parses
|
|
10
|
+
// both forms (C# is the source of truth). See the cross-language vector/color
|
|
11
|
+
// contract.
|
|
12
|
+
const ColorObject = z.object({
|
|
10
13
|
r: z.number().min(0).describe("Red, 0-1"),
|
|
11
14
|
g: z.number().min(0).describe("Green, 0-1"),
|
|
12
15
|
b: z.number().min(0).describe("Blue, 0-1"),
|
|
13
16
|
a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
|
|
14
|
-
})
|
|
15
|
-
|
|
17
|
+
});
|
|
18
|
+
const ColorSchema = z
|
|
19
|
+
.union([
|
|
20
|
+
ColorObject,
|
|
21
|
+
z.string().describe('Comma string "r,g,b,a", e.g. "1,0,0,1"'),
|
|
22
|
+
])
|
|
23
|
+
.describe('RGBA colour — object {r,g,b,a} (0-1) OR comma string "r,g,b,a"');
|
|
16
24
|
export function registerObjectTools(server, bridge) {
|
|
17
25
|
// ── find_objects ───────────────────────────────────────────────────
|
|
18
26
|
server.tool("find_objects", "Query the scene for GameObjects by name (case-insensitive substring), component type name, and/or tag — combine filters (AND). Returns {id,name} for matches (limit default 50, max 500). Read-only; works during play. Use it to get GUIDs to feed into align/distribute/set_tint/group/delete/etc.", {
|
|
@@ -33,12 +41,24 @@ export function registerObjectTools(server, bridge) {
|
|
|
33
41
|
};
|
|
34
42
|
});
|
|
35
43
|
// ── set_tint ───────────────────────────────────────────────────────
|
|
36
|
-
server.tool("set_tint",
|
|
44
|
+
server.tool("set_tint", 'Set the renderer tint colour on one object (id) or many (ids) at once. Works on any ModelRenderer/SkinnedModelRenderer. Pass the colour as "tint" (or its alias "color"); each accepts an object {r,g,b,a} OR a comma string "r,g,b,a".', {
|
|
37
45
|
id: z.string().optional().describe("Single GameObject GUID"),
|
|
38
46
|
ids: z.array(z.string()).optional().describe("Multiple GameObject GUIDs"),
|
|
39
|
-
tint: ColorSchema.describe("Tint colour to apply"),
|
|
47
|
+
tint: ColorSchema.optional().describe("Tint colour to apply (object or comma string)"),
|
|
48
|
+
color: ColorSchema.optional().describe('Alias for "tint" (object or comma string)'),
|
|
40
49
|
}, async (params) => {
|
|
41
|
-
|
|
50
|
+
// "color" is an accepted alias for "tint"; normalize to "tint" and pass
|
|
51
|
+
// the value through unchanged (C# parses object-or-string).
|
|
52
|
+
const { color, tint, ...rest } = params;
|
|
53
|
+
const tintValue = tint ?? color;
|
|
54
|
+
if (tintValue === undefined) {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{ type: "text", text: 'Error: provide "tint" (or its alias "color").' },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const res = await bridge.send("set_tint", { ...rest, tint: tintValue });
|
|
42
62
|
if (!res.success) {
|
|
43
63
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
44
64
|
}
|
package/dist/tools/physics.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
-
/**
|
|
4
|
-
* Physics tools: add_physics, add_collider, add_joint, raycast.
|
|
5
|
-
* Manages rigidbodies, colliders, physics constraints, and ray tracing.
|
|
6
|
-
*/
|
|
7
3
|
export declare function registerPhysicsTools(server: McpServer, bridge: BridgeClient): void;
|
|
8
4
|
//# sourceMappingURL=physics.d.ts.map
|
package/dist/tools/physics.js
CHANGED
|
@@ -3,6 +3,15 @@ import { z } from "zod";
|
|
|
3
3
|
* Physics tools: add_physics, add_collider, add_joint, raycast.
|
|
4
4
|
* Manages rigidbodies, colliders, physics constraints, and ray tracing.
|
|
5
5
|
*/
|
|
6
|
+
// A 3D vector accepted as EITHER an object {x,y,z} OR a comma string "x,y,z",
|
|
7
|
+
// passed through unchanged. The C# handler parses both forms (source of truth).
|
|
8
|
+
// See the cross-language vector/color contract.
|
|
9
|
+
const Vector3Schema = z
|
|
10
|
+
.union([
|
|
11
|
+
z.object({ x: z.number(), y: z.number(), z: z.number() }),
|
|
12
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
13
|
+
])
|
|
14
|
+
.describe('3D vector — object {x,y,z} OR comma string "x,y,z"');
|
|
6
15
|
export function registerPhysicsTools(server, bridge) {
|
|
7
16
|
// ── add_physics ───────────────────────────────────────────────────
|
|
8
17
|
server.tool("add_physics", "Add a Rigidbody and collider to a GameObject, making it a dynamic physics object. Auto-selects BoxCollider if no collider type specified", {
|
|
@@ -38,10 +47,9 @@ export function registerPhysicsTools(server, bridge) {
|
|
|
38
47
|
.boolean()
|
|
39
48
|
.optional()
|
|
40
49
|
.describe("If true, the collider acts as a trigger (no physics collision). Defaults to false"),
|
|
41
|
-
size:
|
|
42
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
50
|
+
size: Vector3Schema
|
|
43
51
|
.optional()
|
|
44
|
-
.describe(
|
|
52
|
+
.describe('Size for BoxCollider (x, y, z dimensions) — object {x,y,z} or comma string "x,y,z"'),
|
|
45
53
|
radius: z
|
|
46
54
|
.number()
|
|
47
55
|
.optional()
|
|
@@ -90,15 +98,12 @@ export function registerPhysicsTools(server, bridge) {
|
|
|
90
98
|
});
|
|
91
99
|
// ── raycast ───────────────────────────────────────────────────────
|
|
92
100
|
server.tool("raycast", "Perform a physics raycast (Scene.Trace.Ray) and return hit results. Useful for line-of-sight checks, object placement, and collision detection", {
|
|
93
|
-
start:
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
end: z
|
|
97
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
101
|
+
start: Vector3Schema
|
|
102
|
+
.describe('Ray start position (world space) — object {x,y,z} or comma string "x,y,z"'),
|
|
103
|
+
end: Vector3Schema
|
|
98
104
|
.optional()
|
|
99
105
|
.describe("Ray end position. Use either end or direction+maxDistance"),
|
|
100
|
-
direction:
|
|
101
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
106
|
+
direction: Vector3Schema
|
|
102
107
|
.optional()
|
|
103
108
|
.describe("Ray direction (normalized). Used with maxDistance instead of end"),
|
|
104
109
|
maxDistance: z
|
|
@@ -128,15 +133,13 @@ export function registerPhysicsTools(server, bridge) {
|
|
|
128
133
|
});
|
|
129
134
|
// ── physics_overlap ───────────────────────────────────────────────
|
|
130
135
|
server.tool("physics_overlap", "Spatial volume query: return the GameObjects whose colliders intersect a SPHERE (center + radius) or a BOX (center + size) — the volume counterpart to raycast's ray. Use it for 'what's near this point' / 'what's inside this trigger volume' checks (proximity, blast radius, spawn-clearance). Read-only.", {
|
|
131
|
-
center:
|
|
132
|
-
.
|
|
133
|
-
.describe("Center of the query volume (world space)"),
|
|
136
|
+
center: Vector3Schema
|
|
137
|
+
.describe('Center of the query volume (world space) — object {x,y,z} or comma string "x,y,z"'),
|
|
134
138
|
radius: z
|
|
135
139
|
.number()
|
|
136
140
|
.optional()
|
|
137
141
|
.describe("Sphere radius. Provide this OR size (box), not both"),
|
|
138
|
-
size:
|
|
139
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
142
|
+
size: Vector3Schema
|
|
140
143
|
.optional()
|
|
141
144
|
.describe("Full box size (not half-extents). Provide this OR radius"),
|
|
142
145
|
}, async (params) => {
|
package/dist/tools/playmode.js
CHANGED
|
@@ -121,5 +121,84 @@ export function registerPlayModeTools(server, bridge) {
|
|
|
121
121
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
122
122
|
};
|
|
123
123
|
});
|
|
124
|
+
// ── drive_player (EXPERIMENTAL) ──────────────────────────────────
|
|
125
|
+
server.tool("drive_player", "EXPERIMENTAL — Drive the active PlayerController DURING PLAY MODE across multiple frames: synthesize sustained look (EyeAngles), analog movement (wish velocity), and/or hold a named action down long enough that Input.Pressed fires its rising edge. This is the reliable alternative to simulate_input, which only sets an action for ONE frame and so MISSES edge-triggered controls (Input.Pressed) and cannot inject analog move/look at all. Requires start_play first. Runs ASYNC across editor frames and returns immediately — the job keeps applying for `frames` (or `durationMs`) frames. After it runs, confirm with drive_player_status, then verify the actual effect with capture_view / get_runtime_property. Provide at least one of look / lookDelta / move / action.", {
|
|
126
|
+
id: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("GUID of the GameObject holding the controller. Omit to auto-resolve the first PlayerController (or *Controller with EyeAngles/WishVelocity) in the play scene"),
|
|
130
|
+
component: z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe("Controller component type name to target (e.g. 'PlayerController'). Omit to auto-detect"),
|
|
134
|
+
frames: z
|
|
135
|
+
.number()
|
|
136
|
+
.int()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("How many editor frames to drive for (1–1800, ~60/sec). Default 30 (~0.5s). Takes precedence over durationMs"),
|
|
139
|
+
durationMs: z
|
|
140
|
+
.number()
|
|
141
|
+
.int()
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Duration in ms, converted at ~60fps (ignored if `frames` is given). Default ~500ms"),
|
|
144
|
+
look: z
|
|
145
|
+
.union([
|
|
146
|
+
z.object({
|
|
147
|
+
pitch: z.number().default(0),
|
|
148
|
+
yaw: z.number().default(0),
|
|
149
|
+
roll: z.number().default(0),
|
|
150
|
+
}),
|
|
151
|
+
z.array(z.number()),
|
|
152
|
+
z.string(),
|
|
153
|
+
])
|
|
154
|
+
.optional()
|
|
155
|
+
.describe("Absolute EyeAngles target {pitch,yaw,roll} held for the whole duration (aim the camera/body). Object, [pitch,yaw,roll], or 'pitch,yaw,roll'. pitch is clamped to ±89"),
|
|
156
|
+
lookDelta: z
|
|
157
|
+
.union([
|
|
158
|
+
z.object({
|
|
159
|
+
pitch: z.number().default(0),
|
|
160
|
+
yaw: z.number().default(0),
|
|
161
|
+
roll: z.number().default(0),
|
|
162
|
+
}),
|
|
163
|
+
z.array(z.number()),
|
|
164
|
+
z.string(),
|
|
165
|
+
])
|
|
166
|
+
.optional()
|
|
167
|
+
.describe("Per-frame EyeAngles delta added each frame (turn/pan over time, e.g. {yaw:2} to sweep right). Combine with `frames` to control total rotation"),
|
|
168
|
+
move: z
|
|
169
|
+
.union([
|
|
170
|
+
z.object({ x: z.number().default(0), y: z.number().default(0) }),
|
|
171
|
+
z.array(z.number()),
|
|
172
|
+
z.string(),
|
|
173
|
+
])
|
|
174
|
+
.optional()
|
|
175
|
+
.describe("Analog movement in the controller's facing frame: x = forward(+)/back(-), y = left(+)/right(-). Magnitude clamped to 1. e.g. {x:1} walks forward for the whole duration"),
|
|
176
|
+
moveSpeed: z
|
|
177
|
+
.number()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe("Units/sec used when synthesizing WishVelocity from `move` (default 160). Ignored if the controller exposes its own AnalogMove field"),
|
|
180
|
+
action: z
|
|
181
|
+
.string()
|
|
182
|
+
.optional()
|
|
183
|
+
.describe("A named input action ('jump','use','attack1',…) HELD DOWN every frame for the whole duration, so Input.Pressed catches the edge single-frame simulate_input misses. Auto-released on the final frame"),
|
|
184
|
+
}, async (params) => {
|
|
185
|
+
const res = await bridge.send("drive_player", params);
|
|
186
|
+
if (!res.success) {
|
|
187
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
// ── drive_player_status (EXPERIMENTAL) ───────────────────────────
|
|
194
|
+
server.tool("drive_player_status", "EXPERIMENTAL — Read the result of the most recently FINISHED drive_player job: which controller members were actually written (EyeAngles / WishVelocity / AnalogMove…), how many frames applied, and why it ended. Because drive_player runs across frames and returns immediately, this is how you confirm it took effect. Returns lastResult=null if no job has finished yet.", {}, async () => {
|
|
195
|
+
const res = await bridge.send("drive_player_status");
|
|
196
|
+
if (!res.success) {
|
|
197
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
201
|
+
};
|
|
202
|
+
});
|
|
124
203
|
}
|
|
125
204
|
//# sourceMappingURL=playmode.js.map
|
package/dist/tools/prefabs.js
CHANGED
|
@@ -25,13 +25,16 @@ export function registerPrefabTools(server, bridge) {
|
|
|
25
25
|
.string()
|
|
26
26
|
.describe("Path to the .prefab file (e.g. 'prefabs/enemies/grunt.prefab')"),
|
|
27
27
|
position: z
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
.union([
|
|
29
|
+
z.object({
|
|
30
|
+
x: z.number(),
|
|
31
|
+
y: z.number(),
|
|
32
|
+
z: z.number(),
|
|
33
|
+
}),
|
|
34
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,64"'),
|
|
35
|
+
])
|
|
33
36
|
.optional()
|
|
34
|
-
.describe(
|
|
37
|
+
.describe('World position to spawn at — object {x,y,z} or comma string "x,y,z". Defaults to origin'),
|
|
35
38
|
rotation: z
|
|
36
39
|
.object({
|
|
37
40
|
pitch: z.number(),
|
package/dist/tools/templates.js
CHANGED
|
@@ -42,9 +42,12 @@ export function registerTemplateTools(server, bridge) {
|
|
|
42
42
|
.optional()
|
|
43
43
|
.describe("When placeInScene is true, also create a Camera (FP/TP: child at eye/boom offset; top_down: fixed overhead). Defaults to true."),
|
|
44
44
|
spawnPosition: z
|
|
45
|
-
.
|
|
45
|
+
.union([
|
|
46
|
+
z.object({ x: z.number(), y: z.number(), z: z.number() }),
|
|
47
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,64"'),
|
|
48
|
+
])
|
|
46
49
|
.optional()
|
|
47
|
-
.describe(
|
|
50
|
+
.describe('When placeInScene is true, the world position to spawn the player rig at — object {x,y,z} or comma string "x,y,z". Defaults to the origin.'),
|
|
48
51
|
}, async (params) => {
|
|
49
52
|
const res = await bridge.send("create_player_controller", params);
|
|
50
53
|
if (!res.success) {
|
package/dist/tools/ui.js
CHANGED
|
@@ -76,9 +76,12 @@ export function registerUITools(server, bridge) {
|
|
|
76
76
|
.optional()
|
|
77
77
|
.describe("Name for the UI GameObject. Defaults to 'World UI'"),
|
|
78
78
|
position: z
|
|
79
|
-
.
|
|
79
|
+
.union([
|
|
80
|
+
z.object({ x: z.number(), y: z.number(), z: z.number() }),
|
|
81
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,64"'),
|
|
82
|
+
])
|
|
80
83
|
.optional()
|
|
81
|
-
.describe(
|
|
84
|
+
.describe('World position for the panel — object {x,y,z} or comma string "x,y,z"'),
|
|
82
85
|
rotation: z
|
|
83
86
|
.object({
|
|
84
87
|
pitch: z.number(),
|
package/dist/tools/visuals.js
CHANGED
|
@@ -8,17 +8,28 @@ import { z } from "zod";
|
|
|
8
8
|
* (which can't even set a Color). After any change, screenshot the scene and read
|
|
9
9
|
* the result — this layer is where the screenshot loop matters most.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Colour / vector accepted as EITHER an object OR a comma string, passed
|
|
12
|
+
// through unchanged. The C# handler parses both forms (source of truth). See
|
|
13
|
+
// the cross-language vector/color contract.
|
|
14
|
+
const ColorObject = z.object({
|
|
13
15
|
r: z.number().min(0).describe("Red, 0-1"),
|
|
14
16
|
g: z.number().min(0).describe("Green, 0-1"),
|
|
15
17
|
b: z.number().min(0).describe("Blue, 0-1"),
|
|
16
18
|
a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
+
});
|
|
20
|
+
const ColorSchema = z
|
|
21
|
+
.union([
|
|
22
|
+
ColorObject,
|
|
23
|
+
z.string().describe('Comma string "r,g,b,a", e.g. "1,0,0,1"'),
|
|
24
|
+
])
|
|
25
|
+
.describe('RGBA colour — object {r,g,b,a} (0-1) OR comma string "r,g,b,a"');
|
|
26
|
+
const Vector3Object = z.object({ x: z.number(), y: z.number(), z: z.number() });
|
|
19
27
|
const Vector3Schema = z
|
|
20
|
-
.
|
|
21
|
-
|
|
28
|
+
.union([
|
|
29
|
+
Vector3Object,
|
|
30
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
31
|
+
])
|
|
32
|
+
.describe('World position — object {x,y,z} OR comma string "x,y,z"');
|
|
22
33
|
const RotationSchema = z
|
|
23
34
|
.object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
|
|
24
35
|
.describe("Rotation {pitch,yaw,roll} in degrees");
|
|
@@ -81,10 +92,9 @@ export function registerVisualTools(server, bridge) {
|
|
|
81
92
|
heightWidth: z.number().optional().describe("cubemap: height-fog band width"),
|
|
82
93
|
heightExponent: z.number().optional().describe("cubemap: height-fog falloff exponent"),
|
|
83
94
|
strength: z.number().optional().describe("volumetric: fog density/strength"),
|
|
84
|
-
size:
|
|
85
|
-
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
95
|
+
size: Vector3Schema
|
|
86
96
|
.optional()
|
|
87
|
-
.describe(
|
|
97
|
+
.describe('volumetric: bounds size (units) centred on the object — object {x,y,z} or comma string "x,y,z"'),
|
|
88
98
|
}, async (params) => {
|
|
89
99
|
const res = await bridge.send("set_fog", params);
|
|
90
100
|
if (!res.success) {
|