sbox-mcp-server 1.20.0 → 2.1.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 +125 -82
- package/dist/tools/aisystems.d.ts +4 -0
- package/dist/tools/aisystems.js +236 -0
- package/dist/tools/assets.js +5 -5
- package/dist/tools/audio.js +12 -19
- package/dist/tools/audit.d.ts +8 -0
- package/dist/tools/audit.js +65 -0
- package/dist/tools/batch.d.ts +8 -0
- package/dist/tools/batch.js +77 -0
- package/dist/tools/characters.js +6 -6
- package/dist/tools/cinematicrecording.d.ts +31 -0
- package/dist/tools/cinematicrecording.js +107 -0
- package/dist/tools/cinematics.js +2 -2
- package/dist/tools/components.js +2 -2
- package/dist/tools/debugdraw.js +4 -4
- package/dist/tools/diagnostics.js +7 -5
- package/dist/tools/dialoguefx.d.ts +4 -0
- package/dist/tools/dialoguefx.js +144 -0
- package/dist/tools/discovery.js +6 -6
- package/dist/tools/economysave.d.ts +27 -0
- package/dist/tools/economysave.js +231 -0
- package/dist/tools/gameobjects.js +7 -7
- package/dist/tools/gameplay.js +4 -4
- package/dist/tools/gameplayrecorder.d.ts +18 -0
- package/dist/tools/gameplayrecorder.js +73 -0
- package/dist/tools/inspection.js +6 -6
- package/dist/tools/leveltools.js +5 -5
- package/dist/tools/materials.js +4 -4
- package/dist/tools/movieauthoring.d.ts +31 -0
- package/dist/tools/movieauthoring.js +84 -0
- package/dist/tools/moviemaker.js +1 -1
- package/dist/tools/multiplayertest.d.ts +23 -0
- package/dist/tools/multiplayertest.js +62 -0
- package/dist/tools/networking.js +22 -22
- package/dist/tools/physics.js +4 -4
- package/dist/tools/playmode.js +5 -5
- package/dist/tools/playtest.js +2 -1
- package/dist/tools/prefabs.js +7 -3
- package/dist/tools/project.js +2 -2
- package/dist/tools/publishing.js +3 -3
- package/dist/tools/roundui.d.ts +22 -0
- package/dist/tools/roundui.js +88 -0
- package/dist/tools/scenes.js +2 -2
- package/dist/tools/scripts.js +3 -3
- package/dist/tools/statsachievements.d.ts +4 -0
- package/dist/tools/statsachievements.js +243 -0
- package/dist/tools/status.js +1 -1
- package/dist/tools/templates.js +7 -7
- package/dist/tools/ui.js +5 -5
- package/dist/tools/uifeedback.js +1 -1
- package/dist/tools/vehicles.d.ts +8 -0
- package/dist/tools/vehicles.js +98 -0
- package/dist/tools/visuals.js +8 -8
- package/dist/tools/workflow.d.ts +8 -0
- package/dist/tools/workflow.js +107 -0
- package/dist/tools/world.js +61 -58
- package/dist/tools/worldrender.d.ts +4 -0
- package/dist/tools/worldrender.js +183 -0
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Project audit & batch operation tools (v2 relaunch wave 1, Batch 51):
|
|
4
|
+
* find_broken_references, batch_set_property, describe_project.
|
|
5
|
+
*/
|
|
6
|
+
export function registerAuditTools(server, bridge) {
|
|
7
|
+
// ── find_broken_references ───────────────────────────────────────
|
|
8
|
+
server.tool("find_broken_references", "Scan the project for broken references, two layers in one call: (1) every GameObject in the open scene — renderers with no Model (missing_model), component properties pointing at DESTROYED GameObjects/Components (dead_gameobject_ref / dead_component_ref), null component entries whose type no longer exists (missing_component); (2) every .scene/.prefab FILE — prefab references to deleted/renamed files (missing_prefab_file). Returns { total, showing, truncated, objectsScanned, filesScanned, issues } — each issue has { id, name, component, kind, detail } (file-level issues carry the file path in name). Fix missing models with assign_model, dead refs with set_property/set_component_reference, missing prefab files by fixing the path or recreating via create_prefab. Read-only; safe any time. Results cap at `limit` (default 100, max 500)", {
|
|
9
|
+
limit: z
|
|
10
|
+
.number()
|
|
11
|
+
.int()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Max issues to return (default 100, max 500). total still counts everything"),
|
|
14
|
+
scanFiles: z
|
|
15
|
+
.boolean()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Include the .scene/.prefab file scan for missing prefab references. Default true"),
|
|
18
|
+
}, async (params) => {
|
|
19
|
+
const res = await bridge.send("find_broken_references", params);
|
|
20
|
+
if (!res.success) {
|
|
21
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
// ── batch_set_property ───────────────────────────────────────────
|
|
28
|
+
server.tool("batch_set_property", "Set ONE component property to the same value across MANY GameObjects in a single call (e.g. Tint on 40 props, Enabled on every light). Pass dryRun:true first to validate — it reports each object's current value and what would change WITHOUT applying anything. Returns { total, succeeded, failed, dryRun, results } with per-object ok/error and the previous value on success. Get ids from find_objects or get_selected_objects. Scene-mutating: refused during play mode. Value coercion matches set_property (numbers, bools, enums, 'x,y,z' vectors, colors, asset paths)", {
|
|
29
|
+
ids: z
|
|
30
|
+
.array(z.string())
|
|
31
|
+
.describe("GUIDs of the GameObjects to modify (from find_objects, get_scene_hierarchy, or get_selected_objects)"),
|
|
32
|
+
component: z
|
|
33
|
+
.string()
|
|
34
|
+
.describe("Component type name present on each object, e.g. 'ModelRenderer', 'PointLight'"),
|
|
35
|
+
property: z
|
|
36
|
+
.string()
|
|
37
|
+
.describe("Property name to set, e.g. 'Tint', 'Enabled', 'LightColor' (see get_all_properties)"),
|
|
38
|
+
value: z
|
|
39
|
+
.any()
|
|
40
|
+
.describe("New value — number, bool, string, enum name, 'x,y,z' vector, color, or asset path"),
|
|
41
|
+
dryRun: z
|
|
42
|
+
.boolean()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("true = validate and report current values without changing anything. Default false"),
|
|
45
|
+
}, async (params) => {
|
|
46
|
+
const res = await bridge.send("batch_set_property", params);
|
|
47
|
+
if (!res.success) {
|
|
48
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
// ── describe_project ─────────────────────────────────────────────
|
|
55
|
+
server.tool("describe_project", "One-call project orientation: identity (name/ident/org/type), the open scene with object count, scene and prefab file lists (capped at 50 each, Libraries/.sbox excluded), code footprint (.cs/.razor counts), custom Component types (up to 100, engine types excluded), and installed libraries. Returns a structured summary — orient here first, then get_scene_hierarchy for the scene, describe_type for components, find_broken_references for project health. Read-only", {}, async (params) => {
|
|
56
|
+
const res = await bridge.send("describe_project", params);
|
|
57
|
+
if (!res.success) {
|
|
58
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Batch operation tools (Batch 52): batch_delete, batch_add_component,
|
|
5
|
+
* batch_reparent. All follow the dryRun convention from batch_set_property.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerBatchTools(server: McpServer, bridge: BridgeClient): void;
|
|
8
|
+
//# sourceMappingURL=batch.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Batch operation tools (Batch 52): batch_delete, batch_add_component,
|
|
4
|
+
* batch_reparent. All follow the dryRun convention from batch_set_property.
|
|
5
|
+
*/
|
|
6
|
+
export function registerBatchTools(server, bridge) {
|
|
7
|
+
// ── batch_delete ─────────────────────────────────────────────────
|
|
8
|
+
server.tool("batch_delete", "Delete MANY GameObjects (and their entire subtrees) in one call. ALWAYS pass dryRun:true first — it reports each object's name and child count WITHOUT deleting, so you can confirm the target list. Returns { total, succeeded, failed, dryRun, results } with per-object ok/error. Destructive and NOT undoable — get ids from find_objects or get_selected_objects and verify the dry-run before applying. Scene-mutating: refused during play mode", {
|
|
9
|
+
ids: z
|
|
10
|
+
.array(z.string())
|
|
11
|
+
.describe("GUIDs of the GameObjects to delete (subtrees included)"),
|
|
12
|
+
dryRun: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("true = report names/child counts without deleting anything. Default false — but run a dry pass first"),
|
|
16
|
+
}, async (params) => {
|
|
17
|
+
const res = await bridge.send("batch_delete", params);
|
|
18
|
+
if (!res.success) {
|
|
19
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
// ── batch_add_component ──────────────────────────────────────────
|
|
26
|
+
server.tool("batch_add_component", "Add one component type to MANY GameObjects in one call (e.g. BoxCollider on every crate). Skips objects that already have the component unless skipExisting:false. dryRun:true validates the type and reports per-object alreadyHas WITHOUT adding. Returns { total, succeeded, failed, skipped, dryRun, results }. Follow with batch_set_property to configure the new components. Scene-mutating: refused during play mode", {
|
|
27
|
+
ids: z
|
|
28
|
+
.array(z.string())
|
|
29
|
+
.describe("GUIDs of the target GameObjects (from find_objects or get_selected_objects)"),
|
|
30
|
+
component: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe("Component type name to add, e.g. 'BoxCollider', 'PointLight' (search with list_available_components)"),
|
|
33
|
+
skipExisting: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Skip objects that already have this component type. Default true"),
|
|
37
|
+
dryRun: z
|
|
38
|
+
.boolean()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("true = validate and report without adding anything. Default false"),
|
|
41
|
+
}, async (params) => {
|
|
42
|
+
const res = await bridge.send("batch_add_component", params);
|
|
43
|
+
if (!res.success) {
|
|
44
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
// ── batch_reparent ───────────────────────────────────────────────
|
|
51
|
+
server.tool("batch_reparent", "Move MANY GameObjects under a new parent (or to the scene root) in one call — organize scattered props into a folder object, or regroup a level section. dryRun:true reports each object's current parent and destination WITHOUT moving. Returns { total, succeeded, failed, dryRun, keepWorldPosition, results }. Errors if parent is among ids (no cycles). Scene-mutating: refused during play mode", {
|
|
52
|
+
ids: z
|
|
53
|
+
.array(z.string())
|
|
54
|
+
.describe("GUIDs of the GameObjects to move"),
|
|
55
|
+
parent: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("GUID of the new parent GameObject. Omit (or empty) to move to the scene root"),
|
|
59
|
+
keepWorldPosition: z
|
|
60
|
+
.boolean()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Preserve each object's world position while reparenting. Default true"),
|
|
63
|
+
dryRun: z
|
|
64
|
+
.boolean()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("true = report current parents and the destination without moving anything. Default false"),
|
|
67
|
+
}, async (params) => {
|
|
68
|
+
const res = await bridge.send("batch_reparent", params);
|
|
69
|
+
if (!res.success) {
|
|
70
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=batch.js.map
|
package/dist/tools/characters.js
CHANGED
|
@@ -56,7 +56,7 @@ export function registerCharacterTools(server, bridge) {
|
|
|
56
56
|
};
|
|
57
57
|
});
|
|
58
58
|
// ── spawn_citizen ──────────────────────────────────────────────────
|
|
59
|
-
server.tool("spawn_citizen", "Spawn an animated Citizen character: a SkinnedModelRenderer with the Citizen model, plus (by default) a CitizenAnimationHelper so it idles. PlayAnimationsInEditorScene is enabled so the idle pose shows in the editor view (screenshot-verifiable).
|
|
59
|
+
server.tool("spawn_citizen", "Spawn an animated Citizen character: a SkinnedModelRenderer with the Citizen model, plus (by default) a CitizenAnimationHelper so it idles. PlayAnimationsInEditorScene is enabled so the idle pose shows in the editor view (screenshot-verifiable). Returns { created, hasAnimator, gameObject:{ id, name, position, components, ... } } — pass gameObject.id to the follow-ups: dress_citizen, pose_citizen, set_expression, equip_model.", {
|
|
60
60
|
name: z.string().optional().describe("GameObject name (default 'Citizen')"),
|
|
61
61
|
model: z
|
|
62
62
|
.string()
|
|
@@ -110,7 +110,7 @@ export function registerCharacterTools(server, bridge) {
|
|
|
110
110
|
};
|
|
111
111
|
});
|
|
112
112
|
// ── set_bodygroup ──────────────────────────────────────────────────
|
|
113
|
-
server.tool("set_bodygroup", "Show/hide a bodygroup on a SkinnedModelRenderer (e.g. hide hands when holding a tool, swap head variants). Provide value (int index) or choice (string name).", {
|
|
113
|
+
server.tool("set_bodygroup", "Show/hide a bodygroup on a SkinnedModelRenderer (e.g. hide hands when holding a tool, swap head variants). Provide value (int index) or choice (string name). Returns { set, bodygroup } on success (errors if the object has no SkinnedModelRenderer or neither value nor choice is given); take_screenshot to verify the visual change.", {
|
|
114
114
|
id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
|
|
115
115
|
name: z.string().describe("Bodygroup name"),
|
|
116
116
|
value: z.number().int().optional().describe("Bodygroup choice index"),
|
|
@@ -125,7 +125,7 @@ export function registerCharacterTools(server, bridge) {
|
|
|
125
125
|
};
|
|
126
126
|
});
|
|
127
127
|
// ── pose_citizen ───────────────────────────────────────────────────
|
|
128
|
-
server.tool("pose_citizen", "Pose a Citizen by setting CitizenAnimationHelper params (enables PlayAnimationsInEditorScene so the pose shows in-editor). Set holdType (None/Pistol/Rifle/Shotgun/HoldItem/Punch/Swing), moveStyle (Auto/Walk/Run), specialMove, sitting (bool), and/or duckLevel (0-1).", {
|
|
128
|
+
server.tool("pose_citizen", "Pose a Citizen by setting CitizenAnimationHelper params (enables PlayAnimationsInEditorScene so the pose shows in-editor). Set holdType (None/Pistol/Rifle/Shotgun/HoldItem/Punch/Swing), moveStyle (Auto/Walk/Run), specialMove, sitting (bool), and/or duckLevel (0-1). Returns { posed, changed:[...], gameObject } — changed lists which helper params were applied; take_screenshot to verify the pose.", {
|
|
129
129
|
id: z.string().describe("GUID of the Citizen GameObject (must have a CitizenAnimationHelper)"),
|
|
130
130
|
holdType: z.string().optional().describe("Hold pose, e.g. None, Pistol, Rifle, Shotgun, HoldItem"),
|
|
131
131
|
moveStyle: z.string().optional().describe("Movement style, e.g. Auto, Walk, Run"),
|
|
@@ -184,7 +184,7 @@ export function registerCharacterTools(server, bridge) {
|
|
|
184
184
|
};
|
|
185
185
|
});
|
|
186
186
|
// ── add_ragdoll ────────────────────────────────────────────────────
|
|
187
|
-
server.tool("add_ragdoll", "Add ModelPhysics to a skinned model so it becomes a ragdoll (physics-driven bones). NOTE: the ragdoll only flops in PLAY mode — it won't move in the static editor view, so this one is verified structurally, not by screenshot.", {
|
|
187
|
+
server.tool("add_ragdoll", "Add ModelPhysics to a skinned model so it becomes a ragdoll (physics-driven bones). NOTE: the ragdoll only flops in PLAY mode — it won't move in the static editor view, so this one is verified structurally, not by screenshot. Returns { ragdoll, note, gameObject } — check gameObject.components for ModelPhysics, then verify at runtime with start_play + capture_view.", {
|
|
188
188
|
id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
|
|
189
189
|
motionEnabled: z
|
|
190
190
|
.boolean()
|
|
@@ -200,7 +200,7 @@ export function registerCharacterTools(server, bridge) {
|
|
|
200
200
|
};
|
|
201
201
|
});
|
|
202
202
|
// ── set_expression ─────────────────────────────────────────────────
|
|
203
|
-
server.tool("set_expression", "Set a facial morph (blendshape) on a skinned model — e.g. smile, frown, blink. Call with NO morph to list the model's available morph names (returned as availableMorphs). weight is typically 0-1.", {
|
|
203
|
+
server.tool("set_expression", "Set a facial morph (blendshape) on a skinned model — e.g. smile, frown, blink. Call with NO morph to list the model's available morph names (returned as availableMorphs). weight is typically 0-1. Returns { set, morph, weight, availableMorphs } — availableMorphs is included in every response, so a wrong name can be corrected without an extra listing call.", {
|
|
204
204
|
id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
|
|
205
205
|
morph: z
|
|
206
206
|
.string()
|
|
@@ -244,7 +244,7 @@ export function registerCharacterTools(server, bridge) {
|
|
|
244
244
|
};
|
|
245
245
|
});
|
|
246
246
|
// ── set_animgraph_param ────────────────────────────────────────────
|
|
247
|
-
server.tool("set_animgraph_param", "Set an AnimationGraph parameter on a GameObject's SkinnedModelRenderer (calls Set). This drives Citizen/animgraph motion — e.g. 'move_x'/'move_y' (float), 'b_grounded'/'b_ducked' (bool), or a Vector3. Pose previews in-editor when PlayAnimationsInEditorScene is on; screenshot to verify. Param names are defined by the model's animation graph.", {
|
|
247
|
+
server.tool("set_animgraph_param", "Set an AnimationGraph parameter on a GameObject's SkinnedModelRenderer (calls Set). This drives Citizen/animgraph motion — e.g. 'move_x'/'move_y' (float), 'b_grounded'/'b_ducked' (bool), or a Vector3. Pose previews in-editor when PlayAnimationsInEditorScene is on; screenshot to verify. Param names are defined by the model's animation graph — use list_animations to check whether the model is animgraph-driven. Returns { set, param, kind, note } — kind is the type actually applied (float/int/bool/vector).", {
|
|
248
248
|
id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
|
|
249
249
|
param: z.string().describe("Animgraph parameter name, e.g. 'move_x', 'b_grounded'"),
|
|
250
250
|
value: z
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cinematic Recording pair — scripted-run capture + killcam scaffold:
|
|
5
|
+
*
|
|
6
|
+
* - record_playtest one call = scripted playtest + gameplay recording of the
|
|
7
|
+
* SAME run. Composition of the playtest harness and the
|
|
8
|
+
* Batch 60 MovieRecorder stack: automated regression
|
|
9
|
+
* footage — a failing playtest comes with a replayable
|
|
10
|
+
* clip of exactly what happened.
|
|
11
|
+
* - create_killcam codegen scaffold (sealed Component): rolling-buffer
|
|
12
|
+
* recording + on-death replay via MoviePlayer + a
|
|
13
|
+
* chase-camera takeover of Scene.Camera.
|
|
14
|
+
*
|
|
15
|
+
* Live-verified (2026-07-13, Gravehold editor): sandboxed GAME code can
|
|
16
|
+
* construct + drive MovieRecorder/MoviePlayer at runtime, and
|
|
17
|
+
* MovieRecorderOptions.BufferDuration is a true rolling buffer — ~8.7s of
|
|
18
|
+
* recording with a 3s buffer compiled to a clip with Duration exactly 3.00s,
|
|
19
|
+
* re-based to start at 0. The official recording-api doc
|
|
20
|
+
* (Facepunch/sbox-docs docs/movie-maker/recording-api.md) confirms game-code
|
|
21
|
+
* recording is the supported use case and documents
|
|
22
|
+
* MovieRecorderOptions.Default (a static, invisible to describe_type) +
|
|
23
|
+
* record `with` syntax — the whole-scene idiom create_killcam's
|
|
24
|
+
* RecordWholeScene mode generates.
|
|
25
|
+
*
|
|
26
|
+
* record_playtest is NOT scene-mutating (it only exists in play mode — the
|
|
27
|
+
* Batch 60 recording precedent). create_killcam IS scene-mutating (writes a
|
|
28
|
+
* .cs scaffold to disk, like every create_* scaffold).
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerCinematicRecordingTools(server: McpServer, bridge: BridgeClient): void;
|
|
31
|
+
//# sourceMappingURL=cinematicrecording.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Cinematic Recording pair — scripted-run capture + killcam scaffold:
|
|
4
|
+
*
|
|
5
|
+
* - record_playtest one call = scripted playtest + gameplay recording of the
|
|
6
|
+
* SAME run. Composition of the playtest harness and the
|
|
7
|
+
* Batch 60 MovieRecorder stack: automated regression
|
|
8
|
+
* footage — a failing playtest comes with a replayable
|
|
9
|
+
* clip of exactly what happened.
|
|
10
|
+
* - create_killcam codegen scaffold (sealed Component): rolling-buffer
|
|
11
|
+
* recording + on-death replay via MoviePlayer + a
|
|
12
|
+
* chase-camera takeover of Scene.Camera.
|
|
13
|
+
*
|
|
14
|
+
* Live-verified (2026-07-13, Gravehold editor): sandboxed GAME code can
|
|
15
|
+
* construct + drive MovieRecorder/MoviePlayer at runtime, and
|
|
16
|
+
* MovieRecorderOptions.BufferDuration is a true rolling buffer — ~8.7s of
|
|
17
|
+
* recording with a 3s buffer compiled to a clip with Duration exactly 3.00s,
|
|
18
|
+
* re-based to start at 0. The official recording-api doc
|
|
19
|
+
* (Facepunch/sbox-docs docs/movie-maker/recording-api.md) confirms game-code
|
|
20
|
+
* recording is the supported use case and documents
|
|
21
|
+
* MovieRecorderOptions.Default (a static, invisible to describe_type) +
|
|
22
|
+
* record `with` syntax — the whole-scene idiom create_killcam's
|
|
23
|
+
* RecordWholeScene mode generates.
|
|
24
|
+
*
|
|
25
|
+
* record_playtest is NOT scene-mutating (it only exists in play mode — the
|
|
26
|
+
* Batch 60 recording precedent). create_killcam IS scene-mutating (writes a
|
|
27
|
+
* .cs scaffold to disk, like every create_* scaffold).
|
|
28
|
+
*/
|
|
29
|
+
export function registerCinematicRecordingTools(server, bridge) {
|
|
30
|
+
const reply = (res) => res.success
|
|
31
|
+
? { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }
|
|
32
|
+
: { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
33
|
+
// ── record_playtest ───────────────────────────────────────────────
|
|
34
|
+
server.tool("record_playtest", [
|
|
35
|
+
"Run a scripted playtest AND record the same run to a .movie clip in ONE call — automated regression footage: a failing playtest comes with a replayable clip of exactly what happened. REQUIRES play mode (start_play first).",
|
|
36
|
+
"steps uses the EXACT playtest schema (one verb per step: move / look / lookDelta / action / jump / set / wait / capture / assert — see the playtest tool for the full verb reference). The recording defaults to the playtest's resolved player hierarchy; pass ids to record other objects, or nothing resolvable falls back to whole-scene capture (heavy).",
|
|
37
|
+
"Returns { started, steps, recordingJobId, capture, sampleRate, clipName, folder, recorderCapSeconds, note } immediately; both jobs run ASYNC in the editor frame loop and the clip AUTO-SAVES the moment the playtest finishes (a failing or aborted run still saves its footage; play mode ending early is also saved).",
|
|
38
|
+
"THE POLL CHAIN: 1) playtest_status until finished:true → the per-step pass/fail transcript. 2) gameplay_recording_status → the saved clip summary { saved, assetPath, durationSeconds, trackCount } (if it still says pendingSave, the save is a frame away — poll again; a save error there means name collision: call stop_gameplay_recording yourself with a new name).",
|
|
39
|
+
"Replay the footage with add_movie_player + play_movie. Errors if a playtest or gameplay recording is already active. Only one at a time.",
|
|
40
|
+
].join("\n"), {
|
|
41
|
+
steps: z
|
|
42
|
+
.array(z.record(z.string(), z.any()))
|
|
43
|
+
.describe("Ordered playtest step objects — identical schema to the playtest tool (move/look/lookDelta/action/jump/set/wait/capture/assert). Runs top-to-bottom in the frame loop."),
|
|
44
|
+
id: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("GUID of the player/controller GameObject the playtest drives. Omit to auto-resolve the first PlayerController."),
|
|
48
|
+
component: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Controller component type to target (e.g. 'PlayerController'). Omit to auto-detect."),
|
|
52
|
+
ids: z
|
|
53
|
+
.array(z.string())
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("GameObject GUIDs to RECORD (from get_scene_hierarchy WHILE PLAYING). Omit to record the playtest's player hierarchy (the default and usually what you want)."),
|
|
56
|
+
sampleRate: z
|
|
57
|
+
.number()
|
|
58
|
+
.int()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Recording samples per second (default 30, clamped 1-120)"),
|
|
61
|
+
clipName: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Saved .movie asset name without extension (default playtest_<UTC timestamp>; sanitized to [A-Za-z0-9_-])"),
|
|
65
|
+
folder: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('Assets subfolder to save the clip into (default "recordings")'),
|
|
69
|
+
}, async (params) => reply(await bridge.send("record_playtest", params)));
|
|
70
|
+
// ── create_killcam ────────────────────────────────────────────────
|
|
71
|
+
server.tool("create_killcam", [
|
|
72
|
+
"Generate a sealed killcam Component: a rolling-buffer MovieRecorder keeps ONLY the last MaxBufferSeconds of a target's gameplay (BufferDuration verified live: the compiled clip's Duration equals the buffer, re-based to 0), and TriggerReplay() plays that history back through a MoviePlayer while the main camera chase-follows the target (Scene.Camera takeover in OnPreRender, restored exactly afterwards; static OnReplayFinished event + IsReplaying flag).",
|
|
73
|
+
"Sandbox-safe: live-verified that GAME code can construct and drive MovieRecorder/MoviePlayer at runtime, and killcams/replays are the official recording-api use case — this is the real MovieMaker path, not a transform-history approximation. The replay REWINDS THE LIVE TARGET through its recorded past (classic killcam — the target is dead/inactive when it runs; disable a still-alive controller for the duration). wholeScene:true makes the generated component default to MovieRecorderOptions.Default (all renderers/cameras/sound points/particles — the replay rewinds everything, killer included; heavy in dense scenes), and it stays toggleable per-instance via the RecordWholeScene property.",
|
|
74
|
+
"Returns { created, path, className, bufferSeconds, sampleRate, cameraDistance, cameraHeight, nextSteps }. Then: trigger_hotload → attach to a MANAGER object → set_component_reference Target to the player → arm via WatchOnStart or StartWatching() from spawn code → call TriggerReplay() from death code (pairs with create_health_system). LOCAL/visual-only — wrap in an [Rpc.Broadcast] for all clients. Refuses if the file already exists.",
|
|
75
|
+
].join("\n"), {
|
|
76
|
+
name: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe('Component class/file name (default "Killcam")'),
|
|
80
|
+
directory: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('Project folder for the .cs file (default "Code")'),
|
|
84
|
+
bufferSeconds: z
|
|
85
|
+
.number()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Rolling-buffer length in seconds — the replay shows at most this much history (default 10, clamped 2-120)"),
|
|
88
|
+
sampleRate: z
|
|
89
|
+
.number()
|
|
90
|
+
.int()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Recorder samples per second (default 30, clamped 1-120)"),
|
|
93
|
+
cameraDistance: z
|
|
94
|
+
.number()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Replay chase-camera distance behind the target (default 150, clamped 10-2000)"),
|
|
97
|
+
cameraHeight: z
|
|
98
|
+
.number()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Replay chase-camera height above the target (default 60, clamped 0-2000)"),
|
|
101
|
+
wholeScene: z
|
|
102
|
+
.boolean()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Generated default for RecordWholeScene: true = buffer the WHOLE scene via MovieRecorderOptions.Default (replay rewinds everything; heavy in dense scenes), false = only the Target hierarchy (default)"),
|
|
105
|
+
}, async (params) => reply(await bridge.send("create_killcam", params)));
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=cinematicrecording.js.map
|
package/dist/tools/cinematics.js
CHANGED
|
@@ -16,7 +16,7 @@ import { z } from "zod";
|
|
|
16
16
|
*/
|
|
17
17
|
export function registerCinematicsTools(server, bridge) {
|
|
18
18
|
// ── create_cutscene_director ──────────────────────────────────────
|
|
19
|
-
server.tool("create_cutscene_director", "Generate a hand-authored cutscene player component that needs NO .movie asset — the zero-asset alternative to the MovieMaker family (add_movie_player/play_movie, which play keyframed .movie clips authored in the editor's Movie Maker dock). You author the shots directly in the inspector as parallel lists: ShotPositions (Vector3), ShotAngles (pitch/yaw/roll — Angles, not raw quaternions), ShotHoldSeconds, ShotBlendSeconds, and an optional per-shot ShotLookAt GameObject (aim at a target instead of using ShotAngles). At runtime it takes over the main camera (Scene.Camera) in OnPreRender ONLY while playing — smoothstep-eased Vector3.Lerp + Rotation.Slerp between shots — captures the camera's prior transform and restores it exactly when finished (same un-apply discipline as create_camera_shake). Play from any game code via the static <Name>.Play() (first director) or <Name>.Play(\"name\") (matches CutsceneName); subscribe the static <Name>.OnCutsceneFinished, or gate game logic on the static <Name>.IsCutscenePlaying. LockInput freezes player input each frame via Input.ClearActions() while still reading the SkipAction press first so the cutscene stays skippable. Optional letterbox generates a razor_lint-safe black-bars overlay panel (host under a ScreenPanel) shown while IsCutscenePlaying. LOCAL-only (each client renders its own view) — trigger inside an [Rpc.Broadcast] for all clients. Attach to ANY GameObject; it drives the camera itself and does not need to sit on the camera.
|
|
19
|
+
server.tool("create_cutscene_director", "Generate a hand-authored cutscene player component that needs NO .movie asset — the zero-asset alternative to the MovieMaker family (add_movie_player/play_movie, which play keyframed .movie clips authored in the editor's Movie Maker dock). You author the shots directly in the inspector as parallel lists: ShotPositions (Vector3), ShotAngles (pitch/yaw/roll — Angles, not raw quaternions), ShotHoldSeconds, ShotBlendSeconds, and an optional per-shot ShotLookAt GameObject (aim at a target instead of using ShotAngles). At runtime it takes over the main camera (Scene.Camera) in OnPreRender ONLY while playing — smoothstep-eased Vector3.Lerp + Rotation.Slerp between shots — captures the camera's prior transform and restores it exactly when finished (same un-apply discipline as create_camera_shake). Play from any game code via the static <Name>.Play() (first director) or <Name>.Play(\"name\") (matches CutsceneName); subscribe the static <Name>.OnCutsceneFinished, or gate game logic on the static <Name>.IsCutscenePlaying. LockInput freezes player input each frame via Input.ClearActions() while still reading the SkipAction press first so the cutscene stays skippable. Optional letterbox generates a razor_lint-safe black-bars overlay panel (host under a ScreenPanel) shown while IsCutscenePlaying. LOCAL-only (each client renders its own view) — trigger inside an [Rpc.Broadcast] for all clients. Attach to ANY GameObject; it drives the camera itself and does not need to sit on the camera. Returns { created, path, className, skipAction, lockInput, letterbox, nextSteps } — path is the generated .cs (letterbox lists the overlay files when enabled). Follow with trigger_hotload, then get_compile_errors, then attach via add_component_with_properties (component=className).", {
|
|
20
20
|
name: z
|
|
21
21
|
.string()
|
|
22
22
|
.optional()
|
|
@@ -45,7 +45,7 @@ export function registerCinematicsTools(server, bridge) {
|
|
|
45
45
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
46
46
|
});
|
|
47
47
|
// ── create_dialogue_system ────────────────────────────────────────
|
|
48
|
-
server.tool("create_dialogue_system", "Generate an NPC/story dialogue system — a sealed state+data Component paired with a razor_lint-safe Razor HUD panel. Lines are authored in the inspector as a List<string> using the 'Speaker: text' convention (the part before the first colon is the speaker). The generated HUD panel binds to <Name>.Current automatically (no wiring) and renders the current line with a TimeSince-driven typewriter reveal at CharsPerSecond, folding the visible substring into BuildHash so it re-renders as characters appear. Press the AdvanceAction once to snap the whole line into view instantly, again to move to the next line; dismissing the last line ends the conversation. Start from any game code via the static <Name>.StartDialogue(string[] lines) or set Lines and call the instance Begin(). Static events for hooks: OnLineShown(index, speaker) — pair with add_lipsync to drive facial morphs / audio per line — and OnDialogueFinished when the conversation ends. Pairs with create_interactable to trigger dialogue on use. LOCAL-only (per-client HUD) — call StartDialogue inside an [Rpc.Broadcast] if every client should see it. Attach the panel under a ScreenPanel (add_screen_panel) so the HUD renders.
|
|
48
|
+
server.tool("create_dialogue_system", "Generate an NPC/story dialogue system — a sealed state+data Component paired with a razor_lint-safe Razor HUD panel. Lines are authored in the inspector as a List<string> using the 'Speaker: text' convention (the part before the first colon is the speaker). The generated HUD panel binds to <Name>.Current automatically (no wiring) and renders the current line with a TimeSince-driven typewriter reveal at CharsPerSecond, folding the visible substring into BuildHash so it re-renders as characters appear. Press the AdvanceAction once to snap the whole line into view instantly, again to move to the next line; dismissing the last line ends the conversation. Start from any game code via the static <Name>.StartDialogue(string[] lines) or set Lines and call the instance Begin(). Static events for hooks: OnLineShown(index, speaker) — pair with add_lipsync to drive facial morphs / audio per line — and OnDialogueFinished when the conversation ends. Pairs with create_interactable to trigger dialogue on use. LOCAL-only (per-client HUD) — call StartDialogue inside an [Rpc.Broadcast] if every client should see it. Attach the panel under a ScreenPanel (add_screen_panel) so the HUD renders. Returns { created, path, className, charsPerSecond, advanceAction, panel, nextSteps } — panel lists the generated '<Name>Panel' HUD files. Follow with trigger_hotload, then get_compile_errors, then attach both components via add_component_with_properties.", {
|
|
49
49
|
name: z
|
|
50
50
|
.string()
|
|
51
51
|
.optional()
|
package/dist/tools/components.js
CHANGED
|
@@ -24,7 +24,7 @@ export function registerComponentTools(server, bridge) {
|
|
|
24
24
|
};
|
|
25
25
|
});
|
|
26
26
|
// ── get_all_properties ───────────────────────────────────────────
|
|
27
|
-
server.tool("get_all_properties", "Dump all public properties of every component on a GameObject
|
|
27
|
+
server.tool("get_all_properties", "Dump all public properties of every component on a GameObject. Returns { id, components } where each entry is { component, properties: [{ name, type, value }] } — values are stringified (unreadable ones show '<error>'). Use the exact component/property names it reports with set_property or get_property; can be large on component-heavy objects.", {
|
|
28
28
|
id: z.string().describe("GUID of the GameObject"),
|
|
29
29
|
}, async (params) => {
|
|
30
30
|
const res = await bridge.send("get_all_properties", params);
|
|
@@ -36,7 +36,7 @@ export function registerComponentTools(server, bridge) {
|
|
|
36
36
|
};
|
|
37
37
|
});
|
|
38
38
|
// ── list_available_components ────────────────────────────────────
|
|
39
|
-
server.tool("list_available_components", "List all component types
|
|
39
|
+
server.tool("list_available_components", "List all instantiable component types in the TypeLibrary — built-in AND your project's custom components (abstract types excluded); filter does a substring match on the type name. Returns { count, components } with { name, title, description, fullName } per type, sorted by name — the unfiltered list is LARGE, so pass filter. Use the returned name with add_component_with_properties, and describe_type for a type's full property list.", {
|
|
40
40
|
filter: z
|
|
41
41
|
.string()
|
|
42
42
|
.optional()
|
package/dist/tools/debugdraw.js
CHANGED
|
@@ -28,25 +28,25 @@ export function registerDebugDrawTools(server, bridge) {
|
|
|
28
28
|
color,
|
|
29
29
|
thickness,
|
|
30
30
|
}, async (p) => reply(await bridge.send("debug_draw_line", p)));
|
|
31
|
-
server.tool("debug_draw_ray", "Draw a debug ray (drawn as an arrow) from an origin along a direction for a given length. Renders in editor and play. Ideal for visualizing a raycast result or a facing/normal direction. Accumulates until debug_clear.", {
|
|
31
|
+
server.tool("debug_draw_ray", "Draw a debug ray (drawn as an arrow) from an origin along a direction for a given length. Renders in editor and play. Ideal for visualizing a raycast result or a facing/normal direction. Accumulates until debug_clear. Returns { drawn: 'ray', count, mode } — count is the total accumulated primitives, mode is 'edit' or 'play' (edit-mode gizmos are NOT in take_screenshot; use capture_view in play mode).", {
|
|
32
32
|
origin: z.string().describe('Ray origin, world-space "x,y,z"'),
|
|
33
33
|
direction: z.string().describe('Direction vector "x,y,z" (normalized internally)'),
|
|
34
34
|
length: z.number().optional().describe("Ray length in units (default 64)"),
|
|
35
35
|
color,
|
|
36
36
|
thickness,
|
|
37
37
|
}, async (p) => reply(await bridge.send("debug_draw_ray", p)));
|
|
38
|
-
server.tool("debug_draw_box", "Draw a wireframe debug box centered at a point. Renders in editor and play. Ideal for visualizing a trigger_zone's bounds or a physics_overlap box volume. Accumulates until debug_clear.", {
|
|
38
|
+
server.tool("debug_draw_box", "Draw a wireframe debug box centered at a point. Renders in editor and play. Ideal for visualizing a trigger_zone's bounds or a physics_overlap box volume. Accumulates until debug_clear. Returns { drawn: 'box', count, mode } — count is the total accumulated primitives, mode is 'edit' or 'play' (edit-mode gizmos are NOT in take_screenshot; use capture_view in play mode).", {
|
|
39
39
|
center: z.string().describe('Box center, world-space "x,y,z"'),
|
|
40
40
|
size: z.string().optional().describe('Full size "x,y,z" in units (default "32,32,32")'),
|
|
41
41
|
color,
|
|
42
42
|
thickness,
|
|
43
43
|
}, async (p) => reply(await bridge.send("debug_draw_box", p)));
|
|
44
|
-
server.tool("debug_draw_sphere", "Draw a wireframe debug sphere at a point. Renders in editor and play. Ideal for visualizing a physics_overlap radius or an NPC's hearing/sight range. Accumulates until debug_clear.", {
|
|
44
|
+
server.tool("debug_draw_sphere", "Draw a wireframe debug sphere at a point. Renders in editor and play. Ideal for visualizing a physics_overlap radius or an NPC's hearing/sight range. Accumulates until debug_clear. Returns { drawn: 'sphere', count, mode } — count is the total accumulated primitives, mode is 'edit' or 'play' (edit-mode gizmos are NOT in take_screenshot; use capture_view in play mode).", {
|
|
45
45
|
center: z.string().describe('Sphere center, world-space "x,y,z"'),
|
|
46
46
|
radius: z.number().optional().describe("Radius in units (default 32)"),
|
|
47
47
|
color,
|
|
48
48
|
thickness,
|
|
49
49
|
}, async (p) => reply(await bridge.send("debug_draw_sphere", p)));
|
|
50
|
-
server.tool("debug_clear", "Remove
|
|
50
|
+
server.tool("debug_clear", "Remove ALL debug-draw primitives at once by destroying the debug holder for the current scene (edit or play) — there is no way to remove a single shape. Call before redrawing a fresh frame of debug shapes. Returns { cleared, removed } where removed is how many primitives were destroyed.", {}, async (p) => reply(await bridge.send("debug_clear", p)));
|
|
51
51
|
}
|
|
52
52
|
//# sourceMappingURL=debugdraw.js.map
|
|
@@ -281,7 +281,7 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
281
281
|
};
|
|
282
282
|
});
|
|
283
283
|
// ── console_run ──────────────────────────────────────────────────── (bridge)
|
|
284
|
-
server.tool("console_run", "Run an s&box console command / ConCmd via Sandbox.ConsoleSystem.Run — e.g. a cvar ('sv_cheats 1') or a registered command. Also the invocation primitive behind execute_csharp.", {
|
|
284
|
+
server.tool("console_run", "Run an s&box console command / ConCmd via Sandbox.ConsoleSystem.Run — e.g. a cvar ('sv_cheats 1') or a registered command. Also the invocation primitive behind execute_csharp. Fire-and-forget: returns only { ran, command } and does NOT capture console output — follow with read_log to see what the command printed.", {
|
|
285
285
|
command: z.string().describe("The console command line to run"),
|
|
286
286
|
}, async (params) => {
|
|
287
287
|
const res = await bridge.send("console_run", params);
|
|
@@ -310,14 +310,16 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
310
310
|
const cmd = `claude_exec_${id}`;
|
|
311
311
|
const filePath = `Editor/__Exec_${id}.cs`;
|
|
312
312
|
const marker = `[EXEC ${id}]`;
|
|
313
|
+
// Bare `Log` (via `using Sandbox;`), NOT `Sandbox.Log` — the latter stopped
|
|
314
|
+
// resolving in project editor assemblies on engine 26.07.08b and broke every call.
|
|
313
315
|
const inner = params.expression
|
|
314
|
-
? `var __r = (${params.code});\n\t\t\
|
|
315
|
-
: `${params.code}\n\t\t\
|
|
316
|
+
? `var __r = (${params.code});\n\t\t\tLog.Info( "${marker} RESULT=" + System.Text.Json.JsonSerializer.Serialize( __r ) );`
|
|
317
|
+
: `${params.code}\n\t\t\tLog.Info( "${marker} DONE" );`;
|
|
316
318
|
const cs = `using Editor;\nusing Sandbox;\nusing System;\n\n` +
|
|
317
319
|
`public static class __Exec_${id}\n{\n` +
|
|
318
320
|
`\t[ConCmd( "${cmd}" )]\n\tpublic static void Run()\n\t{\n` +
|
|
319
321
|
`\t\ttry\n\t\t{\n\t\t\t${inner}\n\t\t}\n` +
|
|
320
|
-
`\t\tcatch ( System.Exception __e ) {
|
|
322
|
+
`\t\tcatch ( System.Exception __e ) { Log.Error( "${marker} ERROR=" + __e.Message ); }\n` +
|
|
321
323
|
`\t}\n}\n`;
|
|
322
324
|
const timeout = params.timeoutMs ?? 20000;
|
|
323
325
|
const wr = await bridge.send("write_file", { path: filePath, content: cs });
|
|
@@ -390,7 +392,7 @@ export function registerDiagnosticTools(server, bridge) {
|
|
|
390
392
|
};
|
|
391
393
|
});
|
|
392
394
|
// ── get_bounds ─────────────────────────────────────────────────────── (bridge, Batch 33)
|
|
393
|
-
server.tool("get_bounds", "Get a GameObject's world-space bounding box
|
|
395
|
+
server.tool("get_bounds", "Get a GameObject's world-space bounding box. Returns { id, name, center, size, extents, mins, maxs, radius, position, empty } — objects with no renderer report empty:true (bounds collapse to the world position) plus an explanatory note. Feed center/radius into screenshot_from or frame_camera to frame the object, or use mins/maxs for placement math (screenshot_orbit calls this internally).", {
|
|
394
396
|
id: z.string().describe("GUID of the GameObject to measure"),
|
|
395
397
|
}, async (params) => {
|
|
396
398
|
const res = await bridge.send("get_bounds", params);
|