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,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Gameplay Recording family — record live PLAY-MODE gameplay to a .movie clip
|
|
4
|
+
* via Sandbox.MovieMaker.MovieRecorder (shipped in the current editor build,
|
|
5
|
+
* verified live 2026-07-12; closes engine-watch #2 in docs/TOOL_BACKLOG.md):
|
|
6
|
+
*
|
|
7
|
+
* - record_gameplay_clip start an async frame-loop recording job
|
|
8
|
+
* - stop_gameplay_recording stop → persist as a project .movie asset
|
|
9
|
+
* - gameplay_recording_status poll the job
|
|
10
|
+
*
|
|
11
|
+
* The counterpart to the MovieMaker playback family (list_movies /
|
|
12
|
+
* add_movie_player / play_movie / stop_movie): those play clips, this CREATES
|
|
13
|
+
* them from live gameplay. None of the three are scene-mutating — they must
|
|
14
|
+
* stay callable during play mode, where recording lives.
|
|
15
|
+
*/
|
|
16
|
+
export function registerGameplayRecorderTools(server, bridge) {
|
|
17
|
+
// ── record_gameplay_clip ──────────────────────────────────────────
|
|
18
|
+
server.tool("record_gameplay_clip", "Start recording live play-mode gameplay into a Sandbox.MovieMaker clip — REQUIRES play mode (start_play first; errors otherwise). Captures the given GameObjects (ids — recommended: small focused clips) or, when ids is omitted, the WHOLE scene (heavy: every object becomes tracks). Returns { started, jobId, sampleRate, maxSeconds, capture, discarded, note } immediately; recording runs ASYNC in the editor frame loop until stop_gameplay_recording or the maxSeconds safety cap (default 60s of clip time, max 600s). Only one recording at a time (a second call errors while active; a stopped-but-unsaved clip is discarded by a new start, reported in 'discarded'). Combine with playtest or drive_player to record a SCRIPTED run, then stop_gameplay_recording to save the .movie and play_movie to replay it.", {
|
|
19
|
+
ids: z
|
|
20
|
+
.array(z.string())
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("GameObject GUIDs to capture (from get_scene_hierarchy WHILE PLAYING — play-mode ids can differ from editor ids). Omit to capture the whole scene (heavy)"),
|
|
23
|
+
sampleRate: z
|
|
24
|
+
.number()
|
|
25
|
+
.int()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Samples per second (default 30, clamped 1-120)"),
|
|
28
|
+
maxSeconds: z
|
|
29
|
+
.number()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Safety cap — auto-stops the recording once the clip timeline reaches this many seconds (default 60, clamped 1-600). The clip stays in memory until stop_gameplay_recording saves it"),
|
|
32
|
+
}, async (params) => {
|
|
33
|
+
const res = await bridge.send("record_gameplay_clip", params);
|
|
34
|
+
if (!res.success) {
|
|
35
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
36
|
+
}
|
|
37
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
38
|
+
});
|
|
39
|
+
// ── stop_gameplay_recording ───────────────────────────────────────
|
|
40
|
+
server.tool("stop_gameplay_recording", "Stop the active gameplay recording and persist it as a project .movie asset the editor can load (written to Assets/<folder>/<name>.movie, registered + compiled — list_movies then shows it with hasCompiledClip). Also saves a job that already auto-stopped (maxSeconds cap, or play mode ended). Returns { saved, assetPath, durationSeconds, trackCount, sampleRate, compiled, stopReason, wired, note } — a trackCount of 0 means nothing was captured and the response warns about it. Pass wireToId to auto-wire a MoviePlayer on that GameObject pointed at the new clip (during play mode that wiring is RUNTIME-ONLY and discarded on stop_play; the .movie asset itself always persists). Errors if the target file already exists (the clip stays in memory — retry with another name). discard:true throws the recording away instead. Replay: add_movie_player + play_movie in play mode.", {
|
|
41
|
+
name: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Asset name without extension (default recording_<UTC timestamp>; sanitized to [A-Za-z0-9_-])"),
|
|
45
|
+
folder: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Assets subfolder to save into (default "recordings")'),
|
|
49
|
+
wireToId: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("GameObject GUID to auto-wire a MoviePlayer at the new clip (runtime-only if done during play mode)"),
|
|
53
|
+
discard: z
|
|
54
|
+
.boolean()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Throw the recording away instead of saving it"),
|
|
57
|
+
}, async (params) => {
|
|
58
|
+
const res = await bridge.send("stop_gameplay_recording", params);
|
|
59
|
+
if (!res.success) {
|
|
60
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
61
|
+
}
|
|
62
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
63
|
+
});
|
|
64
|
+
// ── gameplay_recording_status ─────────────────────────────────────
|
|
65
|
+
server.tool("gameplay_recording_status", "Poll the gameplay recording job. While recording returns { recording:true, jobId, elapsedSeconds (clip-timeline seconds), framesWithData, maxSeconds, sampleRate, capture, trackedObjectCount } (trackedObjectCount is -1 for whole-scene capture). After an auto-stop (maxSeconds cap / play mode ended) returns { stopped:true, pendingSave:true, reason } — the clip is in memory awaiting stop_gameplay_recording. After a save/discard returns that last summary (assetPath etc.). Read-only; works during play. No params.", {}, async () => {
|
|
66
|
+
const res = await bridge.send("gameplay_recording_status", {});
|
|
67
|
+
if (!res.success) {
|
|
68
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
69
|
+
}
|
|
70
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=gameplayrecorder.js.map
|
package/dist/tools/inspection.js
CHANGED
|
@@ -14,7 +14,7 @@ import { z } from "zod";
|
|
|
14
14
|
*/
|
|
15
15
|
export function registerInspectionTools(server, bridge) {
|
|
16
16
|
// ── inspect_networked_object ──────────────────────────────────────
|
|
17
|
-
server.tool("inspect_networked_object", "Inspect the live networking contract of a GameObject:
|
|
17
|
+
server.tool("inspect_networked_object", "Inspect the live networking contract of a GameObject. Returns {id, name, network: {active, isProxy, isOwner, isCreator, ownerId, ownerSteamId, ownerTransfer, orphaned, flags}, components: [{component, fields: [{name, type, isSync, syncFlags, value}]}]} — by default only [Sync]-marked fields are listed (components with none are omitted). Unlike get_network_status (session-only), this is per-object — the way to verify a host-authoritative or ownership change actually replicated; works in edit or play mode. Follow up with set_ownership to change the owner, or networking_lint to find the code-level cause of a bad [Sync] value.", {
|
|
18
18
|
id: z.string().describe("GUID of the GameObject to inspect"),
|
|
19
19
|
allProps: z
|
|
20
20
|
.boolean()
|
|
@@ -70,8 +70,8 @@ export function registerInspectionTools(server, bridge) {
|
|
|
70
70
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
71
71
|
});
|
|
72
72
|
// ── save_inspect ──────────────────────────────────────────────────
|
|
73
|
-
server.tool("save_inspect", "Inspect the game's FileSystem.Data save files — the assistant is otherwise blind to persisted state. action='list'
|
|
74
|
-
action: z.enum(["list", "read", "diff"]).default("list"),
|
|
73
|
+
server.tool("save_inspect", "Inspect the game's FileSystem.Data save files — the assistant is otherwise blind to persisted state. action='list' (default) returns `directories` and `files` [{name, path, size}] under `path` (omit path for the Data root); action='read' returns {path, length, content}, truncating content at 60,000 chars; action='diff' compares two save files key-by-key, returning `diffCount` and up to 200 `diffs` [{key, change: added|removed|changed}]. Use to verify a save actually wrote, debug a load/migration, or confirm a sanitize/clamp ran.", {
|
|
74
|
+
action: z.enum(["list", "read", "diff"]).default("list").describe("'list' (default) enumerates a folder; 'read' dumps one file's JSON; 'diff' compares `path` vs `pathB`"),
|
|
75
75
|
path: z
|
|
76
76
|
.string()
|
|
77
77
|
.optional()
|
|
@@ -87,8 +87,8 @@ export function registerInspectionTools(server, bridge) {
|
|
|
87
87
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
88
88
|
});
|
|
89
89
|
// ── services_query ────────────────────────────────────────────────
|
|
90
|
-
server.tool("services_query", "Read from Sandbox.Services — the cloud stats/leaderboard layer many games use as their real DB. action='stats'
|
|
91
|
-
action: z.enum(["stats", "leaderboard"]).default("stats"),
|
|
90
|
+
server.tool("services_query", "Read from Sandbox.Services — the cloud stats/leaderboard layer many games use as their real DB. action='stats' with `name` returns the local player's stat {ident, value, sum, min, max, lastValue, valueString}; without `name` it returns only the package ident plus a usage note (it does NOT list stat definitions). action='leaderboard' (name required) returns {board, displayName, totalEntries, count, entries} with at most `limit` entries (default 10). Read-only; use to verify a Stats.Increment/SetValue path or a leaderboard wired correctly.", {
|
|
91
|
+
action: z.enum(["stats", "leaderboard"]).default("stats").describe("'stats' (default) reads a local-player stat by `name`; 'leaderboard' fetches a board's top entries"),
|
|
92
92
|
name: z
|
|
93
93
|
.string()
|
|
94
94
|
.optional()
|
|
@@ -101,7 +101,7 @@ export function registerInspectionTools(server, bridge) {
|
|
|
101
101
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
102
102
|
});
|
|
103
103
|
// ── simulate_input ────────────────────────────────────────────────
|
|
104
|
-
server.tool("simulate_input", "
|
|
104
|
+
server.tool("simulate_input", "Press or release a named input action during PLAY mode (via Sandbox.Input.SetAction) so input-driven behavior — jump, attack1, use, reload, IPressable, weapon fire — can be verified without a human at the keyboard. REQUIRES play mode and a named `action`; returns {action, state: 'down'|'up', note}. Caveats: SetAction applies to the current input frame ('press' and 'hold' both set the action down; 'release' clears it), and analogMove/analogLook/durationMs are accepted but IGNORED — Sandbox.Input has no analog injection API, so for movement use drive_player or the playtest harness instead.", {
|
|
105
105
|
action: z
|
|
106
106
|
.string()
|
|
107
107
|
.optional()
|
package/dist/tools/leveltools.js
CHANGED
|
@@ -28,7 +28,7 @@ const ColorSchema = z
|
|
|
28
28
|
.describe('RGBA colour — object {r,g,b,a} (0-1) OR comma string "r,g,b,a"');
|
|
29
29
|
export function registerLevelTools(server, bridge) {
|
|
30
30
|
// ── snap_to_ground ─────────────────────────────────────────────────
|
|
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.", {
|
|
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. Returns { snapped, groundZ, gameObject } with the object's updated transform — or { snapped: false, reason } (not an error) when no ground was hit below.", {
|
|
32
32
|
id: z.string().describe("GUID of the GameObject to snap"),
|
|
33
33
|
offset: z.number().optional().describe("Height above the surface to place it (default 0)"),
|
|
34
34
|
startHeight: z.number().optional().describe("How far above the object to start the trace (default 2000)"),
|
|
@@ -43,7 +43,7 @@ export function registerLevelTools(server, bridge) {
|
|
|
43
43
|
};
|
|
44
44
|
});
|
|
45
45
|
// ── align_objects ──────────────────────────────────────────────────
|
|
46
|
-
server.tool("align_objects", "Align several GameObjects on one axis so they share a coordinate. mode = first (match the first object), min, max, or average.", {
|
|
46
|
+
server.tool("align_objects", "Align several GameObjects on one axis so they share a coordinate. mode = first (match the first object), min, max, or average; defaults to first. Returns { aligned, axis, mode, target } — aligned is the object count and target the shared coordinate; verify positions with get_scene_hierarchy or a screenshot.", {
|
|
47
47
|
ids: z.array(z.string()).describe("GUIDs of the GameObjects to align (>= 2)"),
|
|
48
48
|
axis: z.enum(["x", "y", "z"]).describe("Axis to align on"),
|
|
49
49
|
mode: z
|
|
@@ -60,7 +60,7 @@ export function registerLevelTools(server, bridge) {
|
|
|
60
60
|
};
|
|
61
61
|
});
|
|
62
62
|
// ── distribute_objects ─────────────────────────────────────────────
|
|
63
|
-
server.tool("distribute_objects", "Evenly space GameObjects along an axis between the lowest and highest (keeps the two ends fixed, spreads the rest evenly).", {
|
|
63
|
+
server.tool("distribute_objects", "Evenly space GameObjects along an axis between the lowest and highest (keeps the two ends fixed, spreads the rest evenly). Returns { distributed, axis, from, to } — the object count and the fixed end coordinates the rest were spread between.", {
|
|
64
64
|
ids: z.array(z.string()).describe("GUIDs of the GameObjects to distribute (>= 3)"),
|
|
65
65
|
axis: z.enum(["x", "y", "z"]).describe("Axis to distribute along"),
|
|
66
66
|
}, async (params) => {
|
|
@@ -104,7 +104,7 @@ export function registerLevelTools(server, bridge) {
|
|
|
104
104
|
};
|
|
105
105
|
});
|
|
106
106
|
// ── scatter_props ──────────────────────────────────────────────────
|
|
107
|
-
server.tool("scatter_props", "Scatter N copies of a model randomly within a radius around a center point — instant foliage, rocks, debris. Each copy gets a random yaw and (by default) is snapped to the ground. Seeded for reproducibility; copies are grouped under one parent by default. Count capped at 300.", {
|
|
107
|
+
server.tool("scatter_props", "Scatter N copies of a model randomly within a radius around a center point — instant foliage, rocks, debris. Each copy gets a random yaw and (by default) is snapped to the ground. Seeded for reproducibility; copies are grouped under one parent by default. Count capped at 300. Returns { scattered, groupId, seed } — individual prop GUIDs are not returned, so use groupId with get_scene_hierarchy (rootId) to enumerate them, or with set_transform/delete_gameobject to move/remove the whole batch.", {
|
|
108
108
|
model: z.string().describe("Model path to scatter, e.g. 'models/dev/box.vmdl'"),
|
|
109
109
|
center: Vector3Schema.optional().describe("Centre of the scatter area (default origin)"),
|
|
110
110
|
radius: z.number().optional().describe("Scatter radius in units (default 256)"),
|
|
@@ -129,7 +129,7 @@ export function registerLevelTools(server, bridge) {
|
|
|
129
129
|
};
|
|
130
130
|
});
|
|
131
131
|
// ── randomize_transforms ───────────────────────────────────────────
|
|
132
|
-
server.tool("randomize_transforms", "Add natural variation to existing objects: random yaw and/or random uniform scale within a range. Great for breaking up repetition in placed foliage/rocks/crates. Seeded.", {
|
|
132
|
+
server.tool("randomize_transforms", "Add natural variation to existing objects: random yaw and/or random uniform scale within a range. Great for breaking up repetition in placed foliage/rocks/crates. Seeded — the same seed reproduces the same layout. Returns { randomized, seed } (the count of objects changed); scale only varies when scaleMax > scaleMin.", {
|
|
133
133
|
ids: z.array(z.string()).describe("GUIDs of the GameObjects to randomize"),
|
|
134
134
|
randomYaw: z.boolean().optional().describe("Randomize Z rotation (default true)"),
|
|
135
135
|
scaleMin: z.number().optional().describe("Min uniform scale (default 1)"),
|
package/dist/tools/materials.js
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
*/
|
|
6
6
|
export function registerMaterialTools(server, bridge) {
|
|
7
7
|
// ── assign_model ─────────────────────────────────────────────────
|
|
8
|
-
server.tool("assign_model", "Set a 3D model on a GameObject's ModelRenderer. Creates the renderer component if it doesn't exist", {
|
|
8
|
+
server.tool("assign_model", "Set a 3D model on a GameObject's ModelRenderer. Creates the renderer component if it doesn't exist; errors if the model path can't be loaded. Returns { assigned, id, model } — follow with assign_material / set_material_property to style it, or take a screenshot to verify.", {
|
|
9
9
|
id: z.string().describe("GUID of the GameObject"),
|
|
10
10
|
model: z
|
|
11
11
|
.string()
|
|
@@ -20,7 +20,7 @@ export function registerMaterialTools(server, bridge) {
|
|
|
20
20
|
};
|
|
21
21
|
});
|
|
22
22
|
// ── create_material ──────────────────────────────────────────────
|
|
23
|
-
server.tool("create_material", "Create a new material file (.vmat) with a shader and properties like color, roughness, metallic, texture", {
|
|
23
|
+
server.tool("create_material", "Create a new material file (.vmat, KV1 format) with a shader and properties like color, roughness, metallic, texture. Errors if the file already exists; when no properties are given it writes sensible PBR defaults (g_flMetalness 0, g_flRoughness 1). Returns { created, path, shader, propertiesWritten } — pass the returned path to recompile_asset (so the editor compiles it) and then assign_material.", {
|
|
24
24
|
path: z
|
|
25
25
|
.string()
|
|
26
26
|
.describe("Relative path for the material (e.g. 'materials/walls/brick.vmat')"),
|
|
@@ -42,7 +42,7 @@ export function registerMaterialTools(server, bridge) {
|
|
|
42
42
|
};
|
|
43
43
|
});
|
|
44
44
|
// ── assign_material ──────────────────────────────────────────────
|
|
45
|
-
server.tool("assign_material", "Apply a material to a ModelRenderer
|
|
45
|
+
server.tool("assign_material", "Apply a material to a GameObject by setting its ModelRenderer's MaterialOverride (overrides the whole model's material). Requires an existing ModelRenderer (assign_model first) and errors if the material path can't be loaded. Returns { assigned, id, material } — tweak values afterwards with set_material_property.", {
|
|
46
46
|
id: z.string().describe("GUID of the GameObject"),
|
|
47
47
|
material: z
|
|
48
48
|
.string()
|
|
@@ -61,7 +61,7 @@ export function registerMaterialTools(server, bridge) {
|
|
|
61
61
|
};
|
|
62
62
|
});
|
|
63
63
|
// ── set_material_property ────────────────────────────────────────
|
|
64
|
-
server.tool("set_material_property", "Change a property on the material assigned to a GameObject — color, roughness, metallic, texture, etc.", {
|
|
64
|
+
server.tool("set_material_property", "Change a property on the material assigned to a GameObject — color, roughness, metallic, texture, etc. Operates on the ModelRenderer's MaterialOverride; if none is assigned it auto-creates one from the default complex shader (no separate assign_material step needed). Returns { set, id, property, autoCreatedMaterial } — screenshot to verify the visual change.", {
|
|
65
65
|
id: z.string().describe("GUID of the GameObject"),
|
|
66
66
|
property: z
|
|
67
67
|
.string()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Movie Authoring family — author a .movie cutscene clip from a declarative
|
|
5
|
+
* shot list, deterministically, in EDIT mode. No Movie Maker dock, no play
|
|
6
|
+
* mode, no real-time waiting: the whole clip bakes inside one handler call.
|
|
7
|
+
*
|
|
8
|
+
* - author_movie_clip shot list → keyframe timeline → baked .movie asset
|
|
9
|
+
*
|
|
10
|
+
* The mechanism (docs/BRIDGE_GOTCHAS.md §13, flipped around): in EDIT mode
|
|
11
|
+
* MovieRecorder does NOT auto-advance, so manual Advance(1/sampleRate) +
|
|
12
|
+
* Capture() per synthetic frame produces EXACT manual durations. This is the
|
|
13
|
+
* OFFICIAL in-editor recording idiom (sbox-docs movie-maker/recording-api.md:
|
|
14
|
+
* "Instead of Start and Stop, call Advance ... and Capture to record a
|
|
15
|
+
* frame"). The handler steps a camera (temp or borrowed) through hold+blend
|
|
16
|
+
* segments and pumps the recorder one frame at a time. Proven live
|
|
17
|
+
* 2026-07-13: positions decode back exactly (x = 100·t), and FOV IS captured
|
|
18
|
+
* when the CameraComponent is explicitly targeted (WithCaptureComponent) —
|
|
19
|
+
* so fovDegrees is real. E2E: a 3-shot 3.5s bake took 6ms, listed loadable,
|
|
20
|
+
* played to positionSeconds=3.5 in play mode, and add_movie_player
|
|
21
|
+
* createTargets:true recreated the destroyed temp camera.
|
|
22
|
+
*
|
|
23
|
+
* Completes the MovieMaker triangle: list/add_movie_player/play/stop PLAY
|
|
24
|
+
* clips, record_gameplay_clip CAPTURES live play-mode gameplay, and this
|
|
25
|
+
* AUTHORS clips from data. Scene-mutating (creates/borrows-and-moves a camera
|
|
26
|
+
* and writes a project asset) — registered in _sceneMutatingCommands; it also
|
|
27
|
+
* refuses play mode itself, because the recorder auto-advances there and a
|
|
28
|
+
* manual bake would double-count.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerMovieAuthoringTools(server: McpServer, bridge: BridgeClient): void;
|
|
31
|
+
//# sourceMappingURL=movieauthoring.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Movie Authoring family — author a .movie cutscene clip from a declarative
|
|
4
|
+
* shot list, deterministically, in EDIT mode. No Movie Maker dock, no play
|
|
5
|
+
* mode, no real-time waiting: the whole clip bakes inside one handler call.
|
|
6
|
+
*
|
|
7
|
+
* - author_movie_clip shot list → keyframe timeline → baked .movie asset
|
|
8
|
+
*
|
|
9
|
+
* The mechanism (docs/BRIDGE_GOTCHAS.md §13, flipped around): in EDIT mode
|
|
10
|
+
* MovieRecorder does NOT auto-advance, so manual Advance(1/sampleRate) +
|
|
11
|
+
* Capture() per synthetic frame produces EXACT manual durations. This is the
|
|
12
|
+
* OFFICIAL in-editor recording idiom (sbox-docs movie-maker/recording-api.md:
|
|
13
|
+
* "Instead of Start and Stop, call Advance ... and Capture to record a
|
|
14
|
+
* frame"). The handler steps a camera (temp or borrowed) through hold+blend
|
|
15
|
+
* segments and pumps the recorder one frame at a time. Proven live
|
|
16
|
+
* 2026-07-13: positions decode back exactly (x = 100·t), and FOV IS captured
|
|
17
|
+
* when the CameraComponent is explicitly targeted (WithCaptureComponent) —
|
|
18
|
+
* so fovDegrees is real. E2E: a 3-shot 3.5s bake took 6ms, listed loadable,
|
|
19
|
+
* played to positionSeconds=3.5 in play mode, and add_movie_player
|
|
20
|
+
* createTargets:true recreated the destroyed temp camera.
|
|
21
|
+
*
|
|
22
|
+
* Completes the MovieMaker triangle: list/add_movie_player/play/stop PLAY
|
|
23
|
+
* clips, record_gameplay_clip CAPTURES live play-mode gameplay, and this
|
|
24
|
+
* AUTHORS clips from data. Scene-mutating (creates/borrows-and-moves a camera
|
|
25
|
+
* and writes a project asset) — registered in _sceneMutatingCommands; it also
|
|
26
|
+
* refuses play mode itself, because the recorder auto-advances there and a
|
|
27
|
+
* manual bake would double-count.
|
|
28
|
+
*/
|
|
29
|
+
export function registerMovieAuthoringTools(server, bridge) {
|
|
30
|
+
// ── author_movie_clip ─────────────────────────────────────────────
|
|
31
|
+
server.tool("author_movie_clip", "Author a MovieMaker .movie cutscene clip from a declarative shot list — EDIT MODE ONLY, no Movie Maker dock, no play mode, no real-time waiting (a 30s clip bakes in one call, typically <1s). Builds a hold+blend keyframe timeline from the shots (smoothstep ease by default), steps a camera through it, and hand-pumps MovieRecorder Advance/Capture per synthetic frame, then saves Assets/<folder>/<clipName>.movie (registered + compiled; errors if the file exists — the scene itself is NOT saved). Returns { authored, path, name, durationSeconds, frames, sampleRate, shots, tracks, bakeMs, compiled, loadable, camera, nextSteps }. Camera: omit cameraId for a temp camera (destroyed after the bake — play back with add_movie_player createTargets:true so the missing target is recreated), or pass cameraId of an existing camera GameObject (transform + FOV restored EXACTLY afterwards; the clip then animates THAT object on playback). fovDegrees is baked for real (the clip carries a FieldOfView track). Authored clips animate ONLY the camera the bake moves — other scene objects don't move in edit mode (that's what record_gameplay_clip is for). Total timeline capped at 120s, max 32 shots. Errors during play mode (stop_play first). Verify with list_movies; play via add_movie_player + play_movie in play mode.", {
|
|
32
|
+
shots: z
|
|
33
|
+
.array(z.object({
|
|
34
|
+
position: z
|
|
35
|
+
.string()
|
|
36
|
+
.describe("Camera position for this shot as 'x,y,z'"),
|
|
37
|
+
lookAt: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Aim target: a GameObject GUID (resolved to its world position at bake time) or a world point 'x,y,z'. Omit to keep the previous shot's rotation (first shot: the camera's starting rotation)"),
|
|
41
|
+
fovDegrees: z
|
|
42
|
+
.number()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Field of view in degrees (clamped 5-170), baked into the clip as a FieldOfView track. Omit to carry the previous shot's FOV (first shot: the camera's current FOV)"),
|
|
45
|
+
holdSeconds: z
|
|
46
|
+
.number()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("How long to hold this shot's pose (default 1, clamped 0-120)"),
|
|
49
|
+
blendSeconds: z
|
|
50
|
+
.number()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Blend time from the previous shot into this one (default 1, clamped 0-120; ignored on the first shot)"),
|
|
53
|
+
ease: z
|
|
54
|
+
.enum(["linear", "smoothstep"])
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Easing of the blend INTO this shot (default smoothstep)"),
|
|
57
|
+
}))
|
|
58
|
+
.describe("The shot list in order (1-32 shots). Timeline = hold₀, then blendᵢ + holdᵢ per following shot"),
|
|
59
|
+
clipName: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Asset name without extension (default authored_<UTC timestamp>; sanitized to [A-Za-z0-9_-])"),
|
|
63
|
+
folder: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Assets subfolder to save into (default "movies")'),
|
|
67
|
+
sampleRate: z
|
|
68
|
+
.number()
|
|
69
|
+
.int()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Clip samples per second (default 30, clamped 1-120)"),
|
|
72
|
+
cameraId: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("GUID of an existing camera GameObject (must have a CameraComponent) to bake through — restored EXACTLY afterwards, and playback then animates that object. Omit for a temp camera that is destroyed after the bake"),
|
|
76
|
+
}, async (params) => {
|
|
77
|
+
const res = await bridge.send("author_movie_clip", params);
|
|
78
|
+
if (!res.success) {
|
|
79
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
80
|
+
}
|
|
81
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=movieauthoring.js.map
|
package/dist/tools/moviemaker.js
CHANGED
|
@@ -14,7 +14,7 @@ import { z } from "zod";
|
|
|
14
14
|
*/
|
|
15
15
|
export function registerMovieMakerTools(server, bridge) {
|
|
16
16
|
// ── list_movies ───────────────────────────────────────────────────
|
|
17
|
-
server.tool("list_movies", "List the project's .movie resources (Sandbox.MovieMaker clips authored in the editor's Movie Maker dock: Window → Movie Maker). Returns each movie
|
|
17
|
+
server.tool("list_movies", "List the project's .movie resources (Sandbox.MovieMaker clips authored in the editor's Movie Maker dock: Window → Movie Maker). Scans the ENTIRE Assets folder recursively and returns every .movie found — no limit or paging. Returns { count, movies, note } where each movie has { path (asset-relative — the form add_movie_player/play_movie expect), name, loadable (resolves via ResourceLibrary), hasCompiledClip }. Start here before add_movie_player / play_movie — if the list is empty, the movie has to be authored in the dock first (the bridge plays movies; it doesn't author keyframes).", {}, async () => {
|
|
18
18
|
const res = await bridge.send("list_movies", {});
|
|
19
19
|
if (!res.success) {
|
|
20
20
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Multiplayer test harness — host a lobby on the RUNNING game and boot real
|
|
5
|
+
* sbox.exe clients that auto-join it (the engine's official test path:
|
|
6
|
+
* networking/testing-multiplayer.md "Join via new instance" / `connect local`,
|
|
7
|
+
* live-verified through the bridge 2026-07-13):
|
|
8
|
+
*
|
|
9
|
+
* - start_multiplayer_test CreateLobby (if needed) + spawn N `sbox.exe -joinlocal`
|
|
10
|
+
* - multiplayer_test_status read-only: networking, connections, client PIDs, join-wait
|
|
11
|
+
* - stop_multiplayer_test kill tracked clients (+ optional disconnect / stray sweep)
|
|
12
|
+
*
|
|
13
|
+
* The composition this unlocks: start_multiplayer_test → poll status until the guest
|
|
14
|
+
* joins (~80 s boot) → drive the HOST player (drive_player / playtest) → assert
|
|
15
|
+
* replication host-side (inspect_networked_object's [Sync] dump, get_runtime_property)
|
|
16
|
+
* → stop_multiplayer_test. HONEST LIMIT: the bridge cannot see or drive the CLIENT
|
|
17
|
+
* instance (no bridge runs inside it) — a human can eyeball the client window.
|
|
18
|
+
*
|
|
19
|
+
* None of the three are scene-mutating — they must stay callable during play mode,
|
|
20
|
+
* where the session lives (record_playtest precedent).
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerMultiplayerTestTools(server: McpServer, bridge: BridgeClient): void;
|
|
23
|
+
//# sourceMappingURL=multiplayertest.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Multiplayer test harness — host a lobby on the RUNNING game and boot real
|
|
4
|
+
* sbox.exe clients that auto-join it (the engine's official test path:
|
|
5
|
+
* networking/testing-multiplayer.md "Join via new instance" / `connect local`,
|
|
6
|
+
* live-verified through the bridge 2026-07-13):
|
|
7
|
+
*
|
|
8
|
+
* - start_multiplayer_test CreateLobby (if needed) + spawn N `sbox.exe -joinlocal`
|
|
9
|
+
* - multiplayer_test_status read-only: networking, connections, client PIDs, join-wait
|
|
10
|
+
* - stop_multiplayer_test kill tracked clients (+ optional disconnect / stray sweep)
|
|
11
|
+
*
|
|
12
|
+
* The composition this unlocks: start_multiplayer_test → poll status until the guest
|
|
13
|
+
* joins (~80 s boot) → drive the HOST player (drive_player / playtest) → assert
|
|
14
|
+
* replication host-side (inspect_networked_object's [Sync] dump, get_runtime_property)
|
|
15
|
+
* → stop_multiplayer_test. HONEST LIMIT: the bridge cannot see or drive the CLIENT
|
|
16
|
+
* instance (no bridge runs inside it) — a human can eyeball the client window.
|
|
17
|
+
*
|
|
18
|
+
* None of the three are scene-mutating — they must stay callable during play mode,
|
|
19
|
+
* where the session lives (record_playtest precedent).
|
|
20
|
+
*/
|
|
21
|
+
export function registerMultiplayerTestTools(server, bridge) {
|
|
22
|
+
const reply = (res) => res.success
|
|
23
|
+
? { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }
|
|
24
|
+
: { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
25
|
+
// ── start_multiplayer_test ────────────────────────────────────────
|
|
26
|
+
server.tool("start_multiplayer_test", [
|
|
27
|
+
"Spin up a REAL multiplayer session for the running game — REQUIRES play mode (start_play first; errors otherwise). Hosts a lobby on the current session if none is active (play mode does NOT host by default) and launches N real sbox.exe game clients with -joinlocal that boot and auto-join — the engine's official multiplayer test path ('Join via new instance' / connect local). The sbox.exe path is auto-resolved (next to the editor process, else Steam library scan) — never hardcoded.",
|
|
28
|
+
"Returns { hosting, isHost, lobbyCreated, maxPlayers, spawned:[pids], sboxExe, resolvedVia, expectedConnections, jobId?, waitForJoinSeconds, note } IMMEDIATELY. With waitForJoinSeconds > 0 an async frame-loop job (jobId) polls Connection.All.Count until it reaches expectedConnections (host + every tracked client) or times out — poll multiplayer_test_status until joinWait.state is 'complete'. A client takes ~80 SECONDS to boot and join, and uses ~4.5 GB RAM (default clients=1, hard cap 2 total).",
|
|
29
|
+
"What to do next: once joined, drive the HOST player via drive_player or playtest, and assert replication HOST-SIDE — inspect_networked_object dumps [Sync] state, get_runtime_property reads live values, multiplayer_test_status shows the connection list. Then stop_multiplayer_test to clean up.",
|
|
30
|
+
"HONEST LIMIT: the bridge CANNOT see or drive the CLIENT instance — no bridge runs inside it. All assertions are host-side; a human can eyeball the client window for visual confirmation.",
|
|
31
|
+
].join("\n"), {
|
|
32
|
+
clients: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("How many sbox.exe client instances to launch (default 1, hard cap 2 total tracked alive — each uses ~4.5 GB RAM)"),
|
|
37
|
+
maxPlayers: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Lobby capacity (default 4). Only applied when THIS call creates the lobby (via LobbyConfig — Networking.MaxPlayers is read-only); ignored if a session is already active"),
|
|
42
|
+
waitForJoinSeconds: z
|
|
43
|
+
.number()
|
|
44
|
+
.int()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("How long the async join-wait job polls for all clients to connect (default 120, clamped 0-600). 0 = don't wait, return right after spawning. Clients boot in ~80 s — don't set this below ~100 unless polling manually"),
|
|
47
|
+
}, async (params) => reply(await bridge.send("start_multiplayer_test", params)));
|
|
48
|
+
// ── multiplayer_test_status ───────────────────────────────────────
|
|
49
|
+
server.tool("multiplayer_test_status", "Read-only snapshot of the multiplayer test harness — works whether or not a test is running (no test = honest empty state). Returns { networkingActive, isHost, connectionCount, connections:[{name,id,isHost}], clients:[{pid,alive,uptimeSeconds}], joinWait:{state:'none'|'pending'|'complete'|'timeout', jobId, expectedConnections, currentConnections, elapsedSeconds, note}, playing }. The success signal for a join is connectionCount going 1 → N+1 (host + guests; guests appear with the Steam guest account name). 'timeout' is not fatal — clients boot in ~80 s, so a late join can still land: keep polling. Also your host-side assertion surface: pair it with inspect_networked_object / get_runtime_property to verify replication while the session runs.", {}, async () => reply(await bridge.send("multiplayer_test_status", {})));
|
|
50
|
+
// ── stop_multiplayer_test ─────────────────────────────────────────
|
|
51
|
+
server.tool("stop_multiplayer_test", "Kill every sbox.exe test client spawned by start_multiplayer_test (entire process tree) and clear the harness registry. Returns { stopped, clients:[{pid,killed,note?}], disconnected, strays, note } — a PID that's already dead (crashed / closed by hand) is reported per-PID, not an error. Pass disconnect:true to also Networking.Disconnect() the host session on the running game (default false: the session keeps running for another round). Pass alsoStrays:true as a safety net to kill ANY process named 'sbox' that isn't the editor (never touches sbox-dev / the editor's own PID). Killing a connected client leaves benign TcpChannel.SendThread traces in the editor log — not an error. Safe to call with nothing running.", {
|
|
52
|
+
disconnect: z
|
|
53
|
+
.boolean()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Also Networking.Disconnect() the host session on the running game (default false — leaves the session up)"),
|
|
56
|
+
alsoStrays: z
|
|
57
|
+
.boolean()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Safety net: also kill ANY process named 'sbox' that isn't the editor or a tracked client (default false). Never touches sbox-dev or the editor's own PID"),
|
|
60
|
+
}, async (params) => reply(await bridge.send("stop_multiplayer_test", params)));
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=multiplayertest.js.map
|
package/dist/tools/networking.js
CHANGED
|
@@ -8,23 +8,23 @@ import { z } from "zod";
|
|
|
8
8
|
*/
|
|
9
9
|
export function registerNetworkingTools(server, bridge) {
|
|
10
10
|
// ── add_network_helper ────────────────────────────────────────────
|
|
11
|
-
server.tool("add_network_helper", "Add a NetworkHelper component to
|
|
11
|
+
server.tool("add_network_helper", "Add a NetworkHelper component (with StartServer=true) to an existing GameObject for quick multiplayer setup — at runtime it creates the lobby and spawns the player prefab per connection. Returns { added, id, component:'NetworkHelper' }. NOTE: the current handler requires id (create a holder with create_gameobject first) and does not apply maxPlayers/playerPrefab — wire PlayerPrefab afterward with set_prefab_ref/set_property", {
|
|
12
12
|
id: z
|
|
13
13
|
.string()
|
|
14
14
|
.optional()
|
|
15
|
-
.describe("GUID of
|
|
15
|
+
.describe("GUID of the GameObject to attach to. Required in practice — the current handler errors when omitted (create a holder with create_gameobject first)"),
|
|
16
16
|
name: z
|
|
17
17
|
.string()
|
|
18
18
|
.optional()
|
|
19
|
-
.describe("
|
|
19
|
+
.describe("Rename the target GameObject to this. Omit to keep its current name"),
|
|
20
20
|
maxPlayers: z
|
|
21
21
|
.number()
|
|
22
22
|
.optional()
|
|
23
|
-
.describe("Maximum number of players in the lobby"),
|
|
23
|
+
.describe("Maximum number of players in the lobby (currently not applied by the handler)"),
|
|
24
24
|
playerPrefab: z
|
|
25
25
|
.string()
|
|
26
26
|
.optional()
|
|
27
|
-
.describe("Path to the player prefab to spawn for each connection"),
|
|
27
|
+
.describe("Path to the player prefab to spawn for each connection (currently not applied by the handler — set the NetworkHelper's PlayerPrefab afterward with set_prefab_ref)"),
|
|
28
28
|
}, async (params) => {
|
|
29
29
|
const res = await bridge.send("add_network_helper", params);
|
|
30
30
|
if (!res.success) {
|
|
@@ -35,23 +35,23 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
35
35
|
};
|
|
36
36
|
});
|
|
37
37
|
// ── configure_network ─────────────────────────────────────────────
|
|
38
|
-
server.tool("configure_network", "Configure
|
|
38
|
+
server.tool("configure_network", "Configure lobby settings on Sandbox.Networking. Currently only lobbyName is applied (sets Networking.ServerName) — Networking.MaxPlayers is read-only on this SDK, and playerPrefab/startServer are not applied by the handler (use add_network_helper + set_prefab_ref for those). Returns { configured, maxPlayers, serverName } reflecting the live Networking values", {
|
|
39
39
|
maxPlayers: z
|
|
40
40
|
.number()
|
|
41
41
|
.optional()
|
|
42
|
-
.describe("Maximum number of players"),
|
|
42
|
+
.describe("Maximum number of players (currently not applied — Networking.MaxPlayers is read-only on this SDK; the live value is echoed in the response)"),
|
|
43
43
|
lobbyName: z
|
|
44
44
|
.string()
|
|
45
45
|
.optional()
|
|
46
|
-
.describe("Display name for the lobby"),
|
|
46
|
+
.describe("Display name for the lobby (sets Networking.ServerName — the only setting this handler applies)"),
|
|
47
47
|
playerPrefab: z
|
|
48
48
|
.string()
|
|
49
49
|
.optional()
|
|
50
|
-
.describe("Path to the player prefab"),
|
|
50
|
+
.describe("Path to the player prefab (currently not applied by the handler — set the NetworkHelper's PlayerPrefab via set_prefab_ref instead)"),
|
|
51
51
|
startServer: z
|
|
52
52
|
.boolean()
|
|
53
53
|
.optional()
|
|
54
|
-
.describe("Start the server/lobby immediately"),
|
|
54
|
+
.describe("Start the server/lobby immediately (currently not applied by the handler — add_network_helper sets StartServer=true on the NetworkHelper)"),
|
|
55
55
|
}, async (params) => {
|
|
56
56
|
const res = await bridge.send("configure_network", params);
|
|
57
57
|
if (!res.success) {
|
|
@@ -62,7 +62,7 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
62
62
|
};
|
|
63
63
|
});
|
|
64
64
|
// ── get_network_status ────────────────────────────────────────────
|
|
65
|
-
server.tool("get_network_status", "Check the current multiplayer status
|
|
65
|
+
server.tool("get_network_status", "Check the current multiplayer status. Returns { isActive, isHost, isClient, isConnecting, maxPlayers, serverName } read from Sandbox.Networking — meaningful mostly in play mode with networking active. It does NOT return a player list or networked-object dump; use inspect_networked_object for a specific object's Network/[Sync] state", {}, async (params) => {
|
|
66
66
|
const res = await bridge.send("get_network_status", params);
|
|
67
67
|
if (!res.success) {
|
|
68
68
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
@@ -72,7 +72,7 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
72
72
|
};
|
|
73
73
|
});
|
|
74
74
|
// ── network_spawn ─────────────────────────────────────────────────
|
|
75
|
-
server.tool("network_spawn", "Network-enable a GameObject so it is synchronized across all connected clients. Calls NetworkSpawn()", {
|
|
75
|
+
server.tool("network_spawn", "Network-enable a GameObject so it is synchronized across all connected clients. Calls NetworkSpawn(). Returns { spawned, id } on success — follow with inspect_networked_object to confirm the Network state, or set_ownership to hand it to a connection", {
|
|
76
76
|
id: z.string().describe("GUID of the GameObject to network"),
|
|
77
77
|
}, async (params) => {
|
|
78
78
|
const res = await bridge.send("network_spawn", params);
|
|
@@ -84,12 +84,12 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
84
84
|
};
|
|
85
85
|
});
|
|
86
86
|
// ── set_ownership ─────────────────────────────────────────────────
|
|
87
|
-
server.tool("set_ownership", "
|
|
87
|
+
server.tool("set_ownership", "Assign network ownership of a GameObject to a connection, or drop ownership. Omitting connectionId (or passing an empty string) calls Network.DropOwnership(); passing a connection Id or SteamId calls Network.AssignOwnership() on the matching live Connection. Returns { ownershipAssigned, id, connectionId } or { ownershipDropped, id }; errors if no connection matches (connections only exist while networking is active)", {
|
|
88
88
|
id: z.string().describe("GUID of the networked GameObject"),
|
|
89
89
|
connectionId: z
|
|
90
90
|
.string()
|
|
91
91
|
.optional()
|
|
92
|
-
.describe("GUID of the target connection.
|
|
92
|
+
.describe("Connection Id GUID or SteamId of the target connection. Omit OR pass an empty string to DROP ownership (there is no take-ownership mode in the current handler)"),
|
|
93
93
|
}, async (params) => {
|
|
94
94
|
const res = await bridge.send("set_ownership", params);
|
|
95
95
|
if (!res.success) {
|
|
@@ -100,7 +100,7 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
100
100
|
};
|
|
101
101
|
});
|
|
102
102
|
// ── add_sync_property ─────────────────────────────────────────────
|
|
103
|
-
server.tool("add_sync_property", "Annotate an EXISTING public property in a C# script with the [Sync] attribute so s&box replicates it across the network. This does NOT create a new property — the property named by `propertyName` must already be declared in the file; the tool only inserts the [Sync] attribute above it", {
|
|
103
|
+
server.tool("add_sync_property", "Annotate an EXISTING public property in a C# script with the [Sync] attribute so s&box replicates it across the network. This does NOT create a new property — the property named by `propertyName` must already be declared in the file; the tool only inserts the [Sync] attribute above it. Returns { added, path, property, attribute } — attribute echoes the exact [Sync...] emitted; errors if the property already has [Sync] or isn't found. Follow with trigger_hotload, then get_compile_errors", {
|
|
104
104
|
path: z
|
|
105
105
|
.string()
|
|
106
106
|
.describe("Relative path to the script file (e.g. 'code/Player.cs')"),
|
|
@@ -156,7 +156,7 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
156
156
|
};
|
|
157
157
|
});
|
|
158
158
|
// ── create_networked_player ───────────────────────────────────────
|
|
159
|
-
server.tool("create_networked_player", "Generate a network-aware player controller with [Sync] properties, owner-only input, and [Rpc.Broadcast] actions", {
|
|
159
|
+
server.tool("create_networked_player", "Generate a network-aware player controller with [Sync] properties, owner-only input, and [Rpc.Broadcast] actions. Writes <name>.cs and returns { created, path, className }. The generated Component syncs PlayerName/Health, moves a CharacterController from Input.AnalogMove behind an IsProxy guard, and exposes a [Property] MoveSpeed plus an [Rpc.Broadcast] TakeDamage(int). Follow with trigger_hotload, then get_compile_errors, then attach + network_spawn", {
|
|
160
160
|
name: z
|
|
161
161
|
.string()
|
|
162
162
|
.optional()
|
|
@@ -165,11 +165,11 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
165
165
|
.string()
|
|
166
166
|
.optional()
|
|
167
167
|
.describe("Subdirectory under code/"),
|
|
168
|
-
moveSpeed: z.number().optional().describe("Movement speed. Defaults to
|
|
168
|
+
moveSpeed: z.number().optional().describe("Movement speed (generated MoveSpeed [Property]). Defaults to 200"),
|
|
169
169
|
includeHealth: z
|
|
170
170
|
.boolean()
|
|
171
171
|
.optional()
|
|
172
|
-
.describe("
|
|
172
|
+
.describe("Currently not applied by the handler — the [Sync] Health and [Rpc.Broadcast] TakeDamage are always generated"),
|
|
173
173
|
}, async (params) => {
|
|
174
174
|
const res = await bridge.send("create_networked_player", params);
|
|
175
175
|
if (!res.success) {
|
|
@@ -180,7 +180,7 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
180
180
|
};
|
|
181
181
|
});
|
|
182
182
|
// ── create_lobby_manager ──────────────────────────────────────────
|
|
183
|
-
server.tool("create_lobby_manager", "Generate a lobby manager
|
|
183
|
+
server.tool("create_lobby_manager", "Generate a lobby manager Component implementing Component.INetworkListener: a static Instance singleton, a [Sync] PlayerCount maintained in OnActive/OnDisconnected, and a LobbyState [Property] that flips to 'playing' when the lobby fills. Writes <name>.cs and returns { created, path, className }. Follow with trigger_hotload, then get_compile_errors, then place via add_component_to_new_object", {
|
|
184
184
|
name: z
|
|
185
185
|
.string()
|
|
186
186
|
.optional()
|
|
@@ -192,7 +192,7 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
192
192
|
maxPlayers: z
|
|
193
193
|
.number()
|
|
194
194
|
.optional()
|
|
195
|
-
.describe("
|
|
195
|
+
.describe("Currently not applied by the handler — the generated MaxPlayers [Property] defaults to 16; tune it per-instance with set_property"),
|
|
196
196
|
}, async (params) => {
|
|
197
197
|
const res = await bridge.send("create_lobby_manager", params);
|
|
198
198
|
if (!res.success) {
|
|
@@ -203,7 +203,7 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
203
203
|
};
|
|
204
204
|
});
|
|
205
205
|
// ── create_network_events ─────────────────────────────────────────
|
|
206
|
-
server.tool("create_network_events", "Generate a network event
|
|
206
|
+
server.tool("create_network_events", "Generate a network event relay Component: [Rpc.Broadcast] SendEvent(eventName, payload) to all clients and [Rpc.Host] SendEventToHost(...), both dispatching into a local OnNetworkEvent switch you extend. It does NOT implement INetworkListener — use create_lobby_manager for connect/disconnect hooks. Writes <name>.cs and returns { created, path, className }. Follow with trigger_hotload, then get_compile_errors", {
|
|
207
207
|
name: z
|
|
208
208
|
.string()
|
|
209
209
|
.optional()
|
|
@@ -215,7 +215,7 @@ export function registerNetworkingTools(server, bridge) {
|
|
|
215
215
|
includeChat: z
|
|
216
216
|
.boolean()
|
|
217
217
|
.optional()
|
|
218
|
-
.describe("
|
|
218
|
+
.describe("Currently not applied by the handler — no chat system is generated"),
|
|
219
219
|
}, async (params) => {
|
|
220
220
|
const res = await bridge.send("create_network_events", params);
|
|
221
221
|
if (!res.success) {
|