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
package/dist/tools/physics.js
CHANGED
|
@@ -14,7 +14,7 @@ const Vector3Schema = z
|
|
|
14
14
|
.describe('3D vector — object {x,y,z} OR comma string "x,y,z"');
|
|
15
15
|
export function registerPhysicsTools(server, bridge) {
|
|
16
16
|
// ── add_physics ───────────────────────────────────────────────────
|
|
17
|
-
server.tool("add_physics", "Add a Rigidbody and collider to a GameObject, making it a dynamic physics object. Auto-selects BoxCollider if no collider type specified", {
|
|
17
|
+
server.tool("add_physics", "Add a Rigidbody and collider to a GameObject, making it a dynamic physics object. Auto-selects BoxCollider if no collider type specified. Returns { physicsAdded, id, components } listing exactly which components were added (e.g. Rigidbody + BoxCollider) — enter play mode (start_play) to see it simulate.", {
|
|
18
18
|
id: z.string().describe("GUID of the GameObject"),
|
|
19
19
|
collider: z
|
|
20
20
|
.enum(["box", "sphere", "capsule", "mesh"])
|
|
@@ -38,7 +38,7 @@ export function registerPhysicsTools(server, bridge) {
|
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
// ── add_collider ──────────────────────────────────────────────────
|
|
41
|
-
server.tool("add_collider", "Add a specific collider component to a GameObject.
|
|
41
|
+
server.tool("add_collider", "Add a specific collider component to a GameObject (no Rigidbody — use add_physics for a dynamic body). Can be configured as a trigger. Returns { added, id, collider, isTrigger } where collider is the actual component type added — note 'mesh' maps to HullCollider (s&box has no MeshCollider) and unrecognized types fall back to BoxCollider.", {
|
|
42
42
|
id: z.string().describe("GUID of the GameObject"),
|
|
43
43
|
type: z
|
|
44
44
|
.enum(["box", "sphere", "capsule", "mesh", "hull"])
|
|
@@ -68,7 +68,7 @@ export function registerPhysicsTools(server, bridge) {
|
|
|
68
68
|
};
|
|
69
69
|
});
|
|
70
70
|
// ── add_joint ─────────────────────────────────────────────────────
|
|
71
|
-
server.tool("add_joint", "Add a physics joint/constraint
|
|
71
|
+
server.tool("add_joint", "Add a physics joint/constraint component to a GameObject, optionally connected to a target body (targetId). Returns { added, id, joint, targetId } — joint is the component type added (FixedJoint/SpringJoint/SliderJoint). If targetId is omitted the joint is added unconnected; wire it later via set_property. Both objects need physics (add_physics) for the constraint to simulate in play mode.", {
|
|
72
72
|
id: z
|
|
73
73
|
.string()
|
|
74
74
|
.describe("GUID of the GameObject to add the joint to"),
|
|
@@ -97,7 +97,7 @@ export function registerPhysicsTools(server, bridge) {
|
|
|
97
97
|
};
|
|
98
98
|
});
|
|
99
99
|
// ── raycast ───────────────────────────────────────────────────────
|
|
100
|
-
server.tool("raycast", "Perform a physics raycast (Scene.Trace.Ray)
|
|
100
|
+
server.tool("raycast", "Perform a physics raycast (Scene.Trace.Ray) from start to end — pass both; the handler traces the start→end segment. Useful for line-of-sight checks, object placement, and collision detection. Returns { hit, hitPosition, normal, distance, gameObjectId, gameObjectName } — feed gameObjectId into get_all_properties/set_transform, or visualize the result with debug_draw_ray. A 'Default Surface not found' error is a known transient; call restart_editor and retry.", {
|
|
101
101
|
start: Vector3Schema
|
|
102
102
|
.describe('Ray start position (world space) — object {x,y,z} or comma string "x,y,z"'),
|
|
103
103
|
end: Vector3Schema
|
package/dist/tools/playmode.js
CHANGED
|
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
*/
|
|
9
9
|
export function registerPlayModeTools(server, bridge) {
|
|
10
10
|
// ── start_play ───────────────────────────────────────────────────
|
|
11
|
-
server.tool("start_play", "Enter play mode — starts running the game in the editor. Scripts execute, physics simulate, everything goes live", {}, async () => {
|
|
11
|
+
server.tool("start_play", "Enter play mode — starts running the game in the editor. Scripts execute, physics simulate, everything goes live. Returns { started, method } (method 'EditorScene.Play', or 'SetPlaying (fallback)' with editorErrorSkipped when the safe path failed). While playing, scene-mutating tools refuse — use get/set_runtime_property, capture_view, and playtest, then stop_play to edit again.", {}, async () => {
|
|
12
12
|
const res = await bridge.send("start_play");
|
|
13
13
|
if (!res.success) {
|
|
14
14
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
@@ -41,7 +41,7 @@ export function registerPlayModeTools(server, bridge) {
|
|
|
41
41
|
};
|
|
42
42
|
});
|
|
43
43
|
// ── take_screenshot ──────────────────────────────────────────────
|
|
44
|
-
server.tool("take_screenshot", "Capture the
|
|
44
|
+
server.tool("take_screenshot", "Capture a 1920x1080 PNG from the scene's Main Camera (ONE fixed angle — the camera may not face what you changed; use screenshot_from to aim at a target object/point). Returns { taken, note, path }, but the engine saves to its default screenshots folder regardless of the path param (<sbox install>/screenshots/sbox.<timestamp>.png) — list the newest file there and READ the image to verify.", {
|
|
45
45
|
path: z
|
|
46
46
|
.string()
|
|
47
47
|
.optional()
|
|
@@ -56,7 +56,7 @@ export function registerPlayModeTools(server, bridge) {
|
|
|
56
56
|
};
|
|
57
57
|
});
|
|
58
58
|
// ── undo ─────────────────────────────────────────────────────────
|
|
59
|
-
server.tool("undo", "Undo the last editor action. Safety net for when a change goes wrong", {}, async () => {
|
|
59
|
+
server.tool("undo", "Undo the last editor action via the editor's UndoSystem. Safety net for when a change goes wrong. Returns { undone: true } — it does not report WHAT was undone, so re-check state with get_scene_hierarchy or get_all_properties. Reverse it with redo. Note: file writes (write_file/create_script) are not editor undo steps.", {}, async () => {
|
|
60
60
|
const res = await bridge.send("undo");
|
|
61
61
|
if (!res.success) {
|
|
62
62
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
@@ -66,7 +66,7 @@ export function registerPlayModeTools(server, bridge) {
|
|
|
66
66
|
};
|
|
67
67
|
});
|
|
68
68
|
// ── redo ─────────────────────────────────────────────────────────
|
|
69
|
-
server.tool("redo", "
|
|
69
|
+
server.tool("redo", "Re-apply the editor action most recently reverted by undo (editor UndoSystem redo — the counterpart to the undo tool). Returns { redone: true } without details of what was redone, so verify the resulting state with get_scene_hierarchy or get_all_properties.", {}, async () => {
|
|
70
70
|
const res = await bridge.send("redo");
|
|
71
71
|
if (!res.success) {
|
|
72
72
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
@@ -107,7 +107,7 @@ export function registerPlayModeTools(server, bridge) {
|
|
|
107
107
|
};
|
|
108
108
|
});
|
|
109
109
|
// ── set_runtime_property ─────────────────────────────────────────
|
|
110
|
-
server.tool("set_runtime_property", "Set a component property value during play mode — tweak values live while the game runs", {
|
|
110
|
+
server.tool("set_runtime_property", "Set a component property value during play mode — tweak values live while the game runs. Errors if the game is not playing (start_play first). Returns { set, id, component, property, value } like set_property; read it back with get_runtime_property. Runtime changes are DISCARDED when play mode stops — use set_property in edit mode to persist.", {
|
|
111
111
|
id: z.string().describe("GUID of the GameObject"),
|
|
112
112
|
component: z.string().describe("Component type name"),
|
|
113
113
|
property: z.string().describe("Property name to set"),
|
package/dist/tools/playtest.js
CHANGED
|
@@ -34,6 +34,7 @@ export function registerPlaytestTools(server, bridge) {
|
|
|
34
34
|
id: z.string().optional().describe("GUID of the player/controller GameObject. Omit to auto-resolve the first PlayerController."),
|
|
35
35
|
component: z.string().optional().describe("Controller component type to target (e.g. 'PlayerController'). Omit to auto-detect."),
|
|
36
36
|
}, async (p) => reply(await bridge.send("playtest", p)));
|
|
37
|
-
server.tool("playtest_status", "Poll the
|
|
37
|
+
server.tool("playtest_status", "Poll the playtest started by the playtest tool. While running: { active:true, step, totalSteps, passed, failed }. When done: { finished:true, reason, verdict:'PASS'|'FAIL', passed, failed, stepsRun, totalSteps, controller, controllerResolved, transcript:[...] } — the full per-step pass/fail record, including any capture PNG paths; the finished summary stays readable until the next playtest starts. If none has run yet: { active:false, finished:false }. Stop a stuck/wrong run early with playtest_abort.", {}, async () => reply(await bridge.send("playtest_status", {})));
|
|
38
|
+
server.tool("playtest_abort", "Stop the RUNNING playtest immediately: releases held input actions, restores UseInputControls, and finalizes the partial transcript (reason 'aborted'). Returns { aborted, stepsRun, passed, failed } — or aborted:false if no job is running. The partial transcript stays readable via playtest_status. Use when a step list is clearly stuck or targeting the wrong object", {}, async () => reply(await bridge.send("playtest_abort", {})));
|
|
38
39
|
}
|
|
39
40
|
//# sourceMappingURL=playtest.js.map
|
package/dist/tools/prefabs.js
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
*/
|
|
6
6
|
export function registerPrefabTools(server, bridge) {
|
|
7
7
|
// ── create_prefab ─────────────────────────────────────────────────
|
|
8
|
-
server.tool("create_prefab", "Save an existing GameObject as a
|
|
8
|
+
server.tool("create_prefab", "Save an existing GameObject as a real .prefab file — FULL engine serialization: every component with its property values, and all children, in the same JSON format the editor writes. Returns { created, path, sourceId, components, children } — pass path to instantiate_prefab to spawn copies or get_prefab_info to inspect. Errors if the source GameObject is missing; overwrites an existing file at path.", {
|
|
9
9
|
id: z.string().describe("GUID of the GameObject to save as prefab"),
|
|
10
10
|
path: z
|
|
11
11
|
.string()
|
|
@@ -20,10 +20,14 @@ export function registerPrefabTools(server, bridge) {
|
|
|
20
20
|
};
|
|
21
21
|
});
|
|
22
22
|
// ── instantiate_prefab ────────────────────────────────────────────
|
|
23
|
-
server.tool("instantiate_prefab", "Spawn a prefab instance into the active scene
|
|
23
|
+
server.tool("instantiate_prefab", "Spawn a FULL prefab instance into the active scene — components and children recreated. Uses the engine's GameObject.Clone for registered prefabs, with a guid-remapped deserialize fallback for freshly-written files (repeat instantiations never collide). Returns { instantiated, prefab, method, gameObject, components, childCount } — gameObject.id is the new GUID for set_transform/set_property follow-ups. Optional name/position/rotation override the spawned root.", {
|
|
24
24
|
path: z
|
|
25
25
|
.string()
|
|
26
26
|
.describe("Path to the .prefab file (e.g. 'prefabs/enemies/grunt.prefab')"),
|
|
27
|
+
name: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Rename the spawned root (defaults to the prefab's root name)"),
|
|
27
31
|
position: z
|
|
28
32
|
.union([
|
|
29
33
|
z.object({
|
|
@@ -80,7 +84,7 @@ export function registerPrefabTools(server, bridge) {
|
|
|
80
84
|
};
|
|
81
85
|
});
|
|
82
86
|
// ── get_prefab_info ───────────────────────────────────────────────
|
|
83
|
-
server.tool("get_prefab_info", "
|
|
87
|
+
server.tool("get_prefab_info", "Inspect a prefab file as a structured summary: { path, name, size, modified, totalObjects, maxDepth, referencedPrefabs, tree } — tree is the object hierarchy with per-node component type lists (children capped at 8 per node with a truncation count). referencedPrefabs lists other .prefab files this one links to. Use before instantiate_prefab; find prefabs with list_prefabs; raw JSON via read_file if needed.", {
|
|
84
88
|
path: z
|
|
85
89
|
.string()
|
|
86
90
|
.describe("Path to the .prefab file (e.g. 'prefabs/enemies/grunt.prefab')"),
|
package/dist/tools/project.js
CHANGED
|
@@ -21,7 +21,7 @@ export function registerProjectTools(server, bridge) {
|
|
|
21
21
|
};
|
|
22
22
|
});
|
|
23
23
|
// ── list_project_files ───────────────────────────────────────────
|
|
24
|
-
server.tool("list_project_files", "Browse the project file tree. Optionally filter by directory path and/or file extension (e.g. '.cs', '.scene')", {
|
|
24
|
+
server.tool("list_project_files", "Browse the project file tree. Optionally filter by directory path and/or file extension (e.g. '.cs', '.scene'). Returns { path, count, files } as project-root-relative paths — CAPPED AT 500 files (count reflects the truncated list, with no marker that more exist), so on large projects narrow with path/extension or use find_in_project. Recursive by default.", {
|
|
25
25
|
path: z
|
|
26
26
|
.string()
|
|
27
27
|
.optional()
|
|
@@ -67,7 +67,7 @@ export function registerProjectTools(server, bridge) {
|
|
|
67
67
|
};
|
|
68
68
|
});
|
|
69
69
|
// ── write_file ───────────────────────────────────────────────────
|
|
70
|
-
server.tool("write_file", "Write or overwrite a file in the s&box project. Creates parent directories as needed", {
|
|
70
|
+
server.tool("write_file", "Write or overwrite a file in the s&box project (SILENTLY replaces existing content — read_file first if you need to preserve it). Creates parent directories as needed; paths are confined to the project root (traversal outside it is denied). Returns a confirmation with the path — for C# follow with trigger_hotload so it compiles; for assets (.vmat etc.) follow with recompile_asset.", {
|
|
71
71
|
path: z
|
|
72
72
|
.string()
|
|
73
73
|
.describe("Relative path for the file (e.g. 'code/Components/Health.cs')"),
|
package/dist/tools/publishing.js
CHANGED
|
@@ -22,7 +22,7 @@ export function registerPublishingTools(server, bridge) {
|
|
|
22
22
|
};
|
|
23
23
|
});
|
|
24
24
|
// ── set_project_config ───────────────────────────────────────────
|
|
25
|
-
server.tool("set_project_config", "Update project configuration fields for publishing: title, description, version, type, package ident, summary, visibility. Only provided fields are changed", {
|
|
25
|
+
server.tool("set_project_config", "Update project configuration fields for publishing: title, description, version, type, package ident, summary, visibility. Only provided fields are changed — edits string values in the .sbproj file in place. Returns { updated, path } (the .sbproj path); read the result back with get_project_config to confirm what actually changed.", {
|
|
26
26
|
title: z.string().optional().describe("Project display title"),
|
|
27
27
|
description: z
|
|
28
28
|
.string()
|
|
@@ -58,7 +58,7 @@ export function registerPublishingTools(server, bridge) {
|
|
|
58
58
|
};
|
|
59
59
|
});
|
|
60
60
|
// ── validate_project ─────────────────────────────────────────────
|
|
61
|
-
server.tool("validate_project", "Validate that the project is ready for publishing.
|
|
61
|
+
server.tool("validate_project", "Validate that the project is ready for publishing. Runs four checks: .sbproj exists, at least one scene, project Ident set, project Title set. Returns { valid, issueCount, issues, checks } — issues are human-readable problems and each checks entry has { check, pass, detail }; fix metadata gaps with set_project_config (it does NOT check compile errors — use get_compile_errors for that).", {}, async (params) => {
|
|
62
62
|
const res = await bridge.send("validate_project", params);
|
|
63
63
|
if (!res.success) {
|
|
64
64
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
@@ -93,7 +93,7 @@ export function registerPublishingTools(server, bridge) {
|
|
|
93
93
|
};
|
|
94
94
|
});
|
|
95
95
|
// ── get_package_details ──────────────────────────────────────────
|
|
96
|
-
server.tool("get_package_details", "Fetch
|
|
96
|
+
server.tool("get_package_details", "Fetch package information from the s&box package backend (Package.FetchAsync) by ident. Returns { fullIdent, title, summary, description, org } — no download/rating/dependency data is included. Use it to confirm a package exists and what it is before install_asset.", {
|
|
97
97
|
ident: z
|
|
98
98
|
.string()
|
|
99
99
|
.describe("Package identifier (e.g. 'facepunch.flatgrass', 'myorg.mygame')"),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Round-flow & UI pack — three tools:
|
|
5
|
+
*
|
|
6
|
+
* - create_round_timer_hud Razor screen HUD showing round/phase time remaining;
|
|
7
|
+
* binds at runtime (TypeLibrary reflection) to whichever
|
|
8
|
+
* shipped round machine exists — no code coupling
|
|
9
|
+
* - scaffold_map_vote_flow end-of-round map vote: [Rpc.Host] ballots, [Sync]
|
|
10
|
+
* NetList tallies, countdown, deterministic tie-break,
|
|
11
|
+
* winner -> Scene.LoadFromFile, plus a Razor vote panel
|
|
12
|
+
* - add_panel_buildhash FILE-EDIT tool (not a scaffold): patches an existing
|
|
13
|
+
* .razor to add the BuildHash() override razor_lint
|
|
14
|
+
* flags as missing
|
|
15
|
+
*
|
|
16
|
+
* All are file-mutating (refused during play mode by the bridge dispatch). The
|
|
17
|
+
* Razor output is razor_lint-safe by construction (BuildHash override, no
|
|
18
|
+
* switch-expressions or non-ASCII in @code, class-selector SCSS roots), modeled
|
|
19
|
+
* on create_leaderboard_panel / the UI-feedback pack.
|
|
20
|
+
*/
|
|
21
|
+
export declare function registerRoundUiTools(server: McpServer, bridge: BridgeClient): void;
|
|
22
|
+
//# sourceMappingURL=roundui.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Round-flow & UI pack — three tools:
|
|
4
|
+
*
|
|
5
|
+
* - create_round_timer_hud Razor screen HUD showing round/phase time remaining;
|
|
6
|
+
* binds at runtime (TypeLibrary reflection) to whichever
|
|
7
|
+
* shipped round machine exists — no code coupling
|
|
8
|
+
* - scaffold_map_vote_flow end-of-round map vote: [Rpc.Host] ballots, [Sync]
|
|
9
|
+
* NetList tallies, countdown, deterministic tie-break,
|
|
10
|
+
* winner -> Scene.LoadFromFile, plus a Razor vote panel
|
|
11
|
+
* - add_panel_buildhash FILE-EDIT tool (not a scaffold): patches an existing
|
|
12
|
+
* .razor to add the BuildHash() override razor_lint
|
|
13
|
+
* flags as missing
|
|
14
|
+
*
|
|
15
|
+
* All are file-mutating (refused during play mode by the bridge dispatch). The
|
|
16
|
+
* Razor output is razor_lint-safe by construction (BuildHash override, no
|
|
17
|
+
* switch-expressions or non-ASCII in @code, class-selector SCSS roots), modeled
|
|
18
|
+
* on create_leaderboard_panel / the UI-feedback pack.
|
|
19
|
+
*/
|
|
20
|
+
export function registerRoundUiTools(server, bridge) {
|
|
21
|
+
// ── create_round_timer_hud ────────────────────────────────────────
|
|
22
|
+
server.tool("create_round_timer_hud", "Generate a Razor screen-panel HUD (.razor + .razor.scss) showing the active round/phase time remaining as mm:ss with the phase/state name above it. NO code coupling to your round machine: at runtime it discovers one by TypeLibrary property reflection — same-GameObject components first, then the whole scene, re-scanning every 2s while unbound — matching either shipped machine shape: create_round_phase_machine output (a [Sync] TimeUntil 'PhaseTimer' + 'CurrentPhase' enum for the label) or create_round_state_machine output (a manager with 'StateIndex' + 'Current', whose active state carries a [Sync] TimeUntil 'TimeLeft' + 'Identifier'). First matching component wins; any hand-written machine exposing those member names also binds. Adaptive BuildHash folds the WHOLE second remaining so the panel re-renders at 1 Hz, not every frame. Optional low-time warning: at/below lowTimeSeconds the clock gets the 'low' CSS class (red by default). Shows '--:--' (and fades out via the 'unbound' class) until a machine exists. Returns { created, razorPath, scssPath, className, lowTimeSeconds, note, nextSteps }. Renders NOTHING without a ScreenPanel host: follow with trigger_hotload, then add_screen_panel, then add_component_with_properties (component=className) on the same object; verify with capture_view (renderUI=true) in play mode.", {
|
|
23
|
+
name: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Class name for the generated panel. Defaults to 'RoundTimerHud'"),
|
|
27
|
+
directory: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Subdirectory for the generated .razor + .razor.scss. Defaults to 'Code/UI'"),
|
|
31
|
+
lowTimeSeconds: z
|
|
32
|
+
.number()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Remaining-seconds threshold at/below which the clock gets the 'low' warning class (clamped to >= 0; 0 disables). Editable per-instance via the LowTimeSeconds [Property]. Defaults to 10"),
|
|
35
|
+
}, async (params) => {
|
|
36
|
+
const res = await bridge.send("create_round_timer_hud", params);
|
|
37
|
+
if (!res.success) {
|
|
38
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
39
|
+
}
|
|
40
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
41
|
+
});
|
|
42
|
+
// ── scaffold_map_vote_flow ────────────────────────────────────────
|
|
43
|
+
server.tool("scaffold_map_vote_flow", "Scaffold an end-of-round map vote. Three files: <Name>.cs (sealed host-authoritative controller) + <Name>Panel.razor + <Name>Panel.razor.scss (vote UI: one button per map, live tallies, countdown, own-pick highlight, winner banner). Flow: host calls StartVote() (usually from a post-round phase/state, or set the AutoStart [Property]) -> clients click -> votes route client-to-host via [Rpc.Host] SubmitVote with the caller re-resolved HOST-SIDE from Rpc.Caller (null-checked — Connection has no IsValid on this SDK) and the map index re-validated (re-votes overwrite, keyed by SteamId) -> tallies replicate via [Sync(FromHost)] NetList<int> -> when the [Sync] TimeUntil countdown expires the host picks the winner (most votes; ties break deterministically via one LCG scramble of a time seed — no System.Random) -> after resultLingerSeconds the HOST calls Scene.LoadFromFile(winner) (API verified live on this SDK; clients follow via the scene networking layer — verify the client hand-off in a real multi-client session). Static event OnVoteFinished(sceneFile) fires on every machine. Returns { created, componentPath, razorPath, scssPath, className, panelClassName, maps, voteDurationSeconds, resultLingerSeconds, autoStart, note, nextSteps }. REQUIREMENTS: the controller must sit on a NETWORK-SPAWNED object in multiplayer or [Sync] never replicates; if maps is omitted the MapScenes list is generated EMPTY and StartVote() refuses with a warning until you fill it in the inspector. Follow with trigger_hotload, attach via add_component_with_properties, host the panel under add_screen_panel.", {
|
|
44
|
+
name: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Class name for the controller; the panel is generated as <Name>Panel. Defaults to 'MapVote'"),
|
|
48
|
+
directory: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Subdirectory for the generated .cs + .razor + .razor.scss. Defaults to 'Code'"),
|
|
52
|
+
maps: z
|
|
53
|
+
.array(z.string())
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Scene files to vote between, e.g. [\"scenes/arena.scene\", \"scenes/docks.scene\"] (find them with list_scenes). Baked into the MapScenes [Property] list, editable later in the inspector. Defaults to an EMPTY list (StartVote() then refuses until it's filled)"),
|
|
56
|
+
voteDurationSeconds: z
|
|
57
|
+
.number()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Seconds the vote stays open once StartVote() is called (clamped to >= 3). Defaults to 20"),
|
|
60
|
+
resultLingerSeconds: z
|
|
61
|
+
.number()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Seconds the winner banner shows before the host loads the winning scene (clamped to >= 0). Defaults to 4"),
|
|
64
|
+
autoStart: z
|
|
65
|
+
.boolean()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Start the vote automatically on spawn (host only). Usually false — call StartVote() from your round machine's post-round state instead. Defaults to false"),
|
|
68
|
+
}, async (params) => {
|
|
69
|
+
const res = await bridge.send("scaffold_map_vote_flow", params);
|
|
70
|
+
if (!res.success) {
|
|
71
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
72
|
+
}
|
|
73
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
74
|
+
});
|
|
75
|
+
// ── add_panel_buildhash ───────────────────────────────────────────
|
|
76
|
+
server.tool("add_panel_buildhash", "PATCH an existing .razor file (razor_lint's companion FIXER — razor_lint flags 'PanelComponent without BuildHash', this adds it). NOT a scaffold: it EDITS the file in place, inserting 'protected override int BuildHash() => System.HashCode.Combine( ... )' at the end of the first @code block, folding the fields and auto-properties declared there. HEURISTIC string/regex parsing — review the diff: computed (=>) properties, statics, consts, events and Action/Func members are skipped; reference-typed members fold by REFERENCE (a mutated List re-renders only if Count-style state is also folded); TimeSince/TimeUntil/RealTimeSince/RealTimeUntil members fold as whole seconds (1 Hz re-render, not every frame); braces inside @code string literals can confuse the block parser. If no members are found a '=> 0' stub with a TODO is inserted (silences the lint but does NOT re-render — replace it). Returns { patched, path, hashedMembers[], buildHash, note } on success, { alreadyPresent: true, path } when a BuildHash already exists (file left UNCHANGED — extend it by hand), or { error } for non-PanelComponent files, missing @code, or a path outside the project. Follow with trigger_hotload, then razor_lint to confirm the finding cleared.", {
|
|
77
|
+
path: z
|
|
78
|
+
.string()
|
|
79
|
+
.describe("Project-relative path to the .razor file to patch, e.g. 'Code/UI/MyHud.razor' (from razor_lint's finding.file or list_project_files). Must '@inherits PanelComponent'; .razor.scss files are rejected"),
|
|
80
|
+
}, async (params) => {
|
|
81
|
+
const res = await bridge.send("add_panel_buildhash", params);
|
|
82
|
+
if (!res.success) {
|
|
83
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
84
|
+
}
|
|
85
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=roundui.js.map
|
package/dist/tools/scenes.js
CHANGED
|
@@ -33,7 +33,7 @@ export function registerSceneTools(server, bridge) {
|
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
35
|
// ── save_scene ───────────────────────────────────────────────────
|
|
36
|
-
server.tool("save_scene", "Save the currently open scene in the s&box editor", {
|
|
36
|
+
server.tool("save_scene", "Save the currently open scene in the s&box editor (EditorScene.SaveSession) to its existing .scene file — a provided path does NOT redirect the save. Returns a plain 'Scene saved' confirmation. Call after a batch of scene edits so work survives an editor restart.", {
|
|
37
37
|
path: z
|
|
38
38
|
.string()
|
|
39
39
|
.optional()
|
|
@@ -48,7 +48,7 @@ export function registerSceneTools(server, bridge) {
|
|
|
48
48
|
};
|
|
49
49
|
});
|
|
50
50
|
// ── create_scene ─────────────────────────────────────────────────
|
|
51
|
-
server.tool("create_scene", "Create a new
|
|
51
|
+
server.tool("create_scene", "Create a new EMPTY .scene file under the project's Assets folder and register it with the AssetSystem so load_scene works immediately. Errors if the scene already exists. NOTE: includeDefaults is not yet honored — the scene is always created empty, so after load_scene add a camera/light/ground yourself (create_gameobject, add_light). Returns a confirmation with the path — pass it to load_scene next.", {
|
|
52
52
|
path: z
|
|
53
53
|
.string()
|
|
54
54
|
.describe("Relative path for the new scene (e.g. 'scenes/level_01.scene')"),
|
package/dist/tools/scripts.js
CHANGED
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
*/
|
|
7
7
|
export function registerScriptTools(server, bridge) {
|
|
8
8
|
// ── create_script ────────────────────────────────────────────────
|
|
9
|
-
server.tool("create_script", "Create a new C# component script in the project
|
|
9
|
+
server.tool("create_script", "Create a new C# component script in the project — a minimal s&box Component class (name is sanitized to a valid identifier), or your exact code when content is provided. Errors if the file already exists. Returns { path, created, className } — the new type is NOT live until a recompile, so call trigger_hotload, then attach it with add_component_with_properties (component=className).", {
|
|
10
10
|
name: z
|
|
11
11
|
.string()
|
|
12
12
|
.describe("Class name for the component (e.g. 'PlayerController'). Will also be the filename"),
|
|
@@ -52,7 +52,7 @@ export function registerScriptTools(server, bridge) {
|
|
|
52
52
|
};
|
|
53
53
|
});
|
|
54
54
|
// ── edit_script ──────────────────────────────────────────────────
|
|
55
|
-
server.tool("edit_script", "Edit an existing C# script.
|
|
55
|
+
server.tool("edit_script", "Edit an existing C# script in place via exact-text find/replace or a full-content overwrite. Errors if the file or the find text isn't found (find/replace replaces ALL occurrences). Returns { path, edited, operation } where operation is 'find_replace' or 'overwrite' — follow with trigger_hotload so the change compiles, then get_compile_errors if in doubt.", {
|
|
56
56
|
path: z
|
|
57
57
|
.string()
|
|
58
58
|
.describe("Relative path to the script file (e.g. 'code/PlayerController.cs')"),
|
|
@@ -98,7 +98,7 @@ export function registerScriptTools(server, bridge) {
|
|
|
98
98
|
};
|
|
99
99
|
});
|
|
100
100
|
// ── delete_script ────────────────────────────────────────────────
|
|
101
|
-
server.tool("delete_script", "
|
|
101
|
+
server.tool("delete_script", "Permanently delete a file from the project by its project-relative path (built for C# scripts, but removes any file; no recycle bin, and editor undo cannot restore it). Errors if the file doesn't exist. Returns a confirmation with the path — follow with trigger_hotload so the removed class actually leaves the compiled assembly.", {
|
|
102
102
|
path: z
|
|
103
103
|
.string()
|
|
104
104
|
.describe("Relative path to the script file to delete"),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
export declare function registerStatsAchievementsTools(server: McpServer, bridge: BridgeClient): void;
|
|
4
|
+
//# sourceMappingURL=statsachievements.d.ts.map
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Stats & Achievements pack — five scaffolds:
|
|
4
|
+
*
|
|
5
|
+
* - add_leaderboard_stat batched write-side stat reporter (Stats.Increment)
|
|
6
|
+
* - create_achievement_set achievement engine + unlock toast HUD (razor)
|
|
7
|
+
* - add_achievement_trigger trigger zone firing Progress/Unlock (creates a GO)
|
|
8
|
+
* - create_speedrun_leaderboard timer + min-aggregation submit + friends-filter panel
|
|
9
|
+
* - create_elo_rating_system host-authoritative elo math + persisted ratings
|
|
10
|
+
*
|
|
11
|
+
* The write-side partners of create_leaderboard_panel (the read side). All are
|
|
12
|
+
* file/scene-mutating and refused during play mode by the bridge dispatch.
|
|
13
|
+
* Sandbox.Services APIs used by the generated code were verified live via
|
|
14
|
+
* describe_type (Stats.Increment/SetValue/Flush, Leaderboards.GetFromStat ->
|
|
15
|
+
* Board2.SetFriendsOnly/SetAggregationMin/SetSortAscending/Refresh(token)).
|
|
16
|
+
*/
|
|
17
|
+
// A 3D vector accepted as EITHER an object {x,y,z} OR a comma string "x,y,z".
|
|
18
|
+
// The value is passed through to the bridge unchanged; the C# handler parses
|
|
19
|
+
// both forms (C# is the source of truth for parsing).
|
|
20
|
+
const Vector3Object = z.object({
|
|
21
|
+
x: z.number().describe("X coordinate"),
|
|
22
|
+
y: z.number().describe("Y coordinate"),
|
|
23
|
+
z: z.number().describe("Z coordinate"),
|
|
24
|
+
});
|
|
25
|
+
const Vector3Schema = z
|
|
26
|
+
.union([
|
|
27
|
+
Vector3Object,
|
|
28
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
29
|
+
])
|
|
30
|
+
.describe('3D vector — object {x,y,z} OR comma string "x,y,z"');
|
|
31
|
+
export function registerStatsAchievementsTools(server, bridge) {
|
|
32
|
+
// ── add_leaderboard_stat ──────────────────────────────────────────
|
|
33
|
+
server.tool("add_leaderboard_stat", "Generate a batched write-side stat reporter component for Sandbox.Services.Stats — the write partner of create_leaderboard_panel. Gameplay code calls the static <Name>.Report(\"kills\", 1) from anywhere; amounts accumulate locally and flush as Stats.Increment deltas on a timer (default every 12 s, also on disable/destroy). Baseline-delta bookkeeping means a partial flush retries the un-sent remainder instead of double-counting, and deltas larger than maxChunk are sent in chunks. Returns { created, path, className, placedOn, note, nextSteps }. Place ONE in the scene after trigger_hotload (add_component_to_new_object), or pass targetId to attach immediately when the type is already compiled. Stats are PER LOCAL PLAYER (each client reports its own) and only exist on leaderboards once the stat is registered for the project ident on sbox.game. Fails if the file already exists.", {
|
|
34
|
+
name: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Class name for the generated component. Defaults to 'StatReporter'"),
|
|
38
|
+
directory: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
42
|
+
flushIntervalSeconds: z
|
|
43
|
+
.number()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Seconds between batched flushes to the backend. Defaults to 12, clamped to >= 1"),
|
|
46
|
+
maxChunk: z
|
|
47
|
+
.number()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Largest amount sent in a single Stats.Increment call; bigger deltas are chunked. Defaults to 1000, clamped to >= 1"),
|
|
50
|
+
targetId: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
54
|
+
}, async (params) => {
|
|
55
|
+
const res = await bridge.send("add_leaderboard_stat", params);
|
|
56
|
+
if (!res.success) {
|
|
57
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
58
|
+
}
|
|
59
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
60
|
+
});
|
|
61
|
+
// ── create_achievement_set ────────────────────────────────────────
|
|
62
|
+
server.tool("create_achievement_set", "Generate an achievement engine: a component with an AchievementDef list (id/title/description/target), per-achievement progress persisted via FileSystem.Data JSON (survives restarts), Progress(id, amount) / Unlock(id) API on a static Instance, a static OnAchievementUnlocked event, and an optional Stats.Increment mirror ('ach-<id>' += 1) on unlock. Also emits a Razor unlock-toast HUD (<Name>Toast.razor + .razor.scss, razor_lint clean) unless makeToast=false. Returns { created, path, className, toastRazorPath, toastScssPath, toastClassName, achievements, placedOn, note, nextSteps }. Ids are sanitized to [a-z0-9_-]; omitting achievements bakes 3 editable samples. After trigger_hotload: place ONE set in the scene, and host the toast under a ScreenPanel (add_screen_panel). Pair with add_achievement_trigger for world-trigger unlocks. LOCAL-only: achievements belong to each client's local player. Fails if the .cs or toast .razor already exists.", {
|
|
63
|
+
name: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Class name for the generated engine component (toast panel becomes <name>Toast). Defaults to 'AchievementSet'"),
|
|
67
|
+
directory: z
|
|
68
|
+
.string()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Subdirectory for all generated files. Defaults to 'Code'"),
|
|
71
|
+
achievements: z
|
|
72
|
+
.array(z.object({
|
|
73
|
+
id: z.string().describe("Stable id, sanitized to [a-z0-9_-] (used in the save file and the mirrored stat name)"),
|
|
74
|
+
title: z.string().describe("Display title shown in the toast"),
|
|
75
|
+
description: z.string().optional().describe("Short description shown in the toast"),
|
|
76
|
+
target: z.number().optional().describe("Progress value that unlocks it. Defaults to 1, clamped to >= 1"),
|
|
77
|
+
}))
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Achievement definitions baked into the component. Omit for 3 editable samples (first_steps, collector, veteran)"),
|
|
80
|
+
fileName: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Save file name inside FileSystem.Data. Defaults to 'achievements.json'"),
|
|
84
|
+
mirrorToStats: z
|
|
85
|
+
.boolean()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Mirror each unlock into Sandbox.Services.Stats as 'ach-<id>' += 1. Defaults to true"),
|
|
88
|
+
makeToast: z
|
|
89
|
+
.boolean()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Also emit the <name>Toast.razor + .razor.scss unlock toast HUD. Defaults to true"),
|
|
92
|
+
toastSeconds: z
|
|
93
|
+
.number()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("Seconds each unlock toast stays on screen. Defaults to 4, clamped to >= 0.5"),
|
|
96
|
+
targetId: z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("GUID of a GameObject to attach the engine to (only attaches if the type is already loaded — hotload first)"),
|
|
100
|
+
}, async (params) => {
|
|
101
|
+
const res = await bridge.send("create_achievement_set", params);
|
|
102
|
+
if (!res.success) {
|
|
103
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
104
|
+
}
|
|
105
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
106
|
+
});
|
|
107
|
+
// ── add_achievement_trigger ───────────────────────────────────────
|
|
108
|
+
server.tool("add_achievement_trigger", "SCENE-MUTATING: generate a data-driven achievement trigger-zone component AND create its GameObject now (named zone with a sized BoxCollider, IsTrigger=true, at the given position). When an object tagged triggerTag enters, the component calls <achievementSetClass>.Instance.Progress(achievementId, amount) — or Unlock() when unlock=true — with a once-only latch and optional destroy-after-fire. Returns { created, path, className, achievementSetClass, achievementId, gameObject, attached, note, nextSteps }. The generated component only attaches to the zone after trigger_hotload — until then `attached` is false and nextSteps carries the exact add_component_with_properties follow-up. The generated code references the set class BY NAME: run create_achievement_set first or the project will not compile (the result warns via `note`). Re-running with the same name fails unless reuseClass=true, which skips codegen and just places another zone (attaching + configuring immediately since the class is already compiled). Refused during play mode.", {
|
|
109
|
+
name: z
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Class name for the generated trigger component. Defaults to 'AchievementTrigger'"),
|
|
113
|
+
directory: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
117
|
+
achievementSetClass: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe("Class name of the achievement set the zone reports to (from create_achievement_set). Defaults to 'AchievementSet'"),
|
|
121
|
+
achievementId: z
|
|
122
|
+
.string()
|
|
123
|
+
.describe("Id of the achievement to progress/unlock (sanitized to [a-z0-9_-])"),
|
|
124
|
+
amount: z
|
|
125
|
+
.number()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe("Progress amount added per fire (ignored when unlock=true). Defaults to 1"),
|
|
128
|
+
unlock: z
|
|
129
|
+
.boolean()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Call Unlock() instead of Progress(). Defaults to false"),
|
|
132
|
+
triggerTag: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("Tag the entering object must carry (put it on the player via set_tags). Defaults to 'player'"),
|
|
136
|
+
onceOnly: z
|
|
137
|
+
.boolean()
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("Only the first tagged entry fires. Defaults to true"),
|
|
140
|
+
destroyAfterFire: z
|
|
141
|
+
.boolean()
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Destroy the zone GameObject after firing. Defaults to false"),
|
|
144
|
+
createObject: z
|
|
145
|
+
.boolean()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe("Create the zone GameObject now (with BoxCollider). Defaults to true; false = code-gen only"),
|
|
148
|
+
objectName: z
|
|
149
|
+
.string()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe("Name for the zone GameObject. Defaults to '<name>Zone'"),
|
|
152
|
+
position: Vector3Schema.optional().describe("World position of the zone GameObject"),
|
|
153
|
+
scale: z
|
|
154
|
+
.union([z.number(), Vector3Schema])
|
|
155
|
+
.optional()
|
|
156
|
+
.describe('BoxCollider size — uniform number, object {x,y,z}, or comma string "x,y,z". Defaults to 100,100,100'),
|
|
157
|
+
reuseClass: z
|
|
158
|
+
.boolean()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("If the .cs already exists, skip codegen and just place another zone with the existing class. Defaults to false"),
|
|
161
|
+
}, async (params) => {
|
|
162
|
+
const res = await bridge.send("add_achievement_trigger", params);
|
|
163
|
+
if (!res.success) {
|
|
164
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
165
|
+
}
|
|
166
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
167
|
+
});
|
|
168
|
+
// ── create_speedrun_leaderboard ───────────────────────────────────
|
|
169
|
+
server.tool("create_speedrun_leaderboard", "Generate a speedrun timer component plus a leaderboard display panel. The timer (<Name>.cs) is TimeSince-based with a static Instance: StartTimer() at run start, StopTimer() at the finish (pairs with a trigger zone), ResetTimer() to abort. StopTimer persists the local best via FileSystem.Data and submits Stats.SetValue(statName, seconds) ONLY when the run beats it — configure the stat with MIN aggregation on sbox.game so the global board keeps best times. The panel (<Name>Panel.razor + .razor.scss, razor_lint clean) fetches via Leaderboards.GetFromStat with min aggregation + ascending sort, has a clickable Friends-only filter button, and overlays a local-best row read from the same save file. Returns { created, path, className, panelRazorPath, panelScssPath, panelClassName, statName, placedOn, note, nextSteps }. After trigger_hotload: place ONE timer (add_component_to_new_object or targetId) and host the panel under a ScreenPanel/WorldPanel (add_screen_panel). maxRows clamps to 1..50; makePanel=false skips the panel files. Fails if the .cs or panel .razor already exists.", {
|
|
170
|
+
name: z
|
|
171
|
+
.string()
|
|
172
|
+
.optional()
|
|
173
|
+
.describe("Class name for the generated timer component (panel becomes <name>Panel). Defaults to 'SpeedrunTimer'"),
|
|
174
|
+
directory: z
|
|
175
|
+
.string()
|
|
176
|
+
.optional()
|
|
177
|
+
.describe("Subdirectory for all generated files. Defaults to 'Code'"),
|
|
178
|
+
statName: z
|
|
179
|
+
.string()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("Sandbox.Services stat the best time is written to (sanitized to [a-z0-9_-]). Defaults to 'best_time'"),
|
|
182
|
+
fileName: z
|
|
183
|
+
.string()
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("Save file name inside FileSystem.Data for the local best. Defaults to 'speedrun.json'"),
|
|
186
|
+
title: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("Panel title text. Defaults to 'Best Times'"),
|
|
190
|
+
maxRows: z
|
|
191
|
+
.number()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Leaderboard rows fetched/shown. Defaults to 10, clamped to 1..50"),
|
|
194
|
+
makePanel: z
|
|
195
|
+
.boolean()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Also emit the <name>Panel.razor + .razor.scss display panel. Defaults to true"),
|
|
198
|
+
targetId: z
|
|
199
|
+
.string()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("GUID of a GameObject to attach the timer to (only attaches if the type is already loaded — hotload first)"),
|
|
202
|
+
}, async (params) => {
|
|
203
|
+
const res = await bridge.send("create_speedrun_leaderboard", params);
|
|
204
|
+
if (!res.success) {
|
|
205
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
206
|
+
}
|
|
207
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
208
|
+
});
|
|
209
|
+
// ── create_elo_rating_system ──────────────────────────────────────
|
|
210
|
+
server.tool("create_elo_rating_system", "Generate a self-contained, host-authoritative elo rating component: standard elo math (expected = 1/(1+10^((Rb-Ra)/400)), delta = K * (score - expected)) with a [Property] K-factor, ratings in a [Sync(SyncFlags.FromHost)] NetDictionary<long,float> keyed by SteamId, and host-side persistence via FileSystem.Data JSON. API on a static Instance: ReportMatch(winnerSteamId, loserSteamId) for 1v1 and ReportTeamMatch(winnerIds, loserIds) for teams (team-average elo, uniform delta per member) — both are IsProxy-guarded no-ops on clients; GetRating(steamId) works anywhere (unknown players = defaultRating); the static OnRatingChanged(steamId, newRating) fires on EVERY machine via an [Rpc.Broadcast]. Returns { created, path, className, kFactor, defaultRating, placedOn, note, nextSteps }. After trigger_hotload: place ONE in the scene and network its GameObject (network_spawn) or the [Sync] never replicates. Only the HOST's disk holds the ratings ledger. Fails if the file already exists.", {
|
|
211
|
+
name: z
|
|
212
|
+
.string()
|
|
213
|
+
.optional()
|
|
214
|
+
.describe("Class name for the generated component. Defaults to 'EloRatingSystem'"),
|
|
215
|
+
directory: z
|
|
216
|
+
.string()
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
219
|
+
kFactor: z
|
|
220
|
+
.number()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe("Elo K-factor — how far one result moves ratings (32 = fast, 16 = stable). Defaults to 32, clamped to >= 1"),
|
|
223
|
+
defaultRating: z
|
|
224
|
+
.number()
|
|
225
|
+
.optional()
|
|
226
|
+
.describe("Rating assigned to players with no recorded matches. Defaults to 1000"),
|
|
227
|
+
fileName: z
|
|
228
|
+
.string()
|
|
229
|
+
.optional()
|
|
230
|
+
.describe("Save file name inside FileSystem.Data (host-side ledger). Defaults to 'elo_ratings.json'"),
|
|
231
|
+
targetId: z
|
|
232
|
+
.string()
|
|
233
|
+
.optional()
|
|
234
|
+
.describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
235
|
+
}, async (params) => {
|
|
236
|
+
const res = await bridge.send("create_elo_rating_system", params);
|
|
237
|
+
if (!res.success) {
|
|
238
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
239
|
+
}
|
|
240
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=statsachievements.js.map
|
package/dist/tools/status.js
CHANGED
|
@@ -19,7 +19,7 @@ catch {
|
|
|
19
19
|
*/
|
|
20
20
|
export function registerStatusTools(server, bridge) {
|
|
21
21
|
// ── get_bridge_status ────────────────────────────────────────────
|
|
22
|
-
server.tool("get_bridge_status", "Check the connection
|
|
22
|
+
server.tool("get_bridge_status", "Check the s&box Bridge connection — call this FIRST in a session. Returns a human summary plus JSON: connected, roundTripOk (heartbeat can be fresh while the editor's request loop is stalled — trust roundTripOk), bridgeVersion vs mcpServerVersion + versionsAligned (a mismatch means restart Claude Code / republish the addon), handlerCount, heartbeatAgeMs, latencyMs, and ipcDir (transport is file IPC; host/port are legacy fields).", {}, async () => {
|
|
23
23
|
const connected = bridge.isConnected();
|
|
24
24
|
const ipcDir = bridge.getIpcDir();
|
|
25
25
|
const heartbeatAgeMs = bridge.getHeartbeatAgeMs();
|