sbox-mcp-server 2.0.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 +18 -0
- package/dist/tools/aisystems.d.ts +4 -0
- package/dist/tools/aisystems.js +236 -0
- package/dist/tools/cinematicrecording.d.ts +31 -0
- package/dist/tools/cinematicrecording.js +107 -0
- package/dist/tools/dialoguefx.d.ts +4 -0
- package/dist/tools/dialoguefx.js +144 -0
- package/dist/tools/economysave.d.ts +27 -0
- package/dist/tools/economysave.js +231 -0
- package/dist/tools/gameplayrecorder.d.ts +18 -0
- package/dist/tools/gameplayrecorder.js +73 -0
- package/dist/tools/movieauthoring.d.ts +31 -0
- package/dist/tools/movieauthoring.js +84 -0
- package/dist/tools/multiplayertest.d.ts +23 -0
- package/dist/tools/multiplayertest.js +62 -0
- package/dist/tools/roundui.d.ts +22 -0
- package/dist/tools/roundui.js +88 -0
- package/dist/tools/statsachievements.d.ts +4 -0
- package/dist/tools/statsachievements.js +243 -0
- package/dist/tools/worldrender.d.ts +4 -0
- package/dist/tools/worldrender.js +183 -0
- package/package.json +1 -1
|
@@ -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
|
|
@@ -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
|
|
@@ -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 registerWorldRenderTools(server: McpServer, bridge: BridgeClient): void;
|
|
4
|
+
//# sourceMappingURL=worldrender.d.ts.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* World & Render family — three tools:
|
|
4
|
+
*
|
|
5
|
+
* - add_water_body WaterVolume physics volume + trigger BoxCollider
|
|
6
|
+
* footprint + optional tinted visual surface
|
|
7
|
+
* - add_render_target_camera secondary camera → render-target texture →
|
|
8
|
+
* display surface (CCTV / mirror / portal)
|
|
9
|
+
* - add_daynight_sun codegen sun/sky driver bound to the component
|
|
10
|
+
* generated by create_day_night_clock
|
|
11
|
+
*
|
|
12
|
+
* add_water_body is scene-direct; the other two generate a sealed game-code
|
|
13
|
+
* component (hotload to compile). All three are scene/file-mutating and are
|
|
14
|
+
* refused during play mode by the bridge dispatch.
|
|
15
|
+
*/
|
|
16
|
+
// Colour / vector accepted as EITHER an object OR a comma string, passed
|
|
17
|
+
// through unchanged. The C# handler parses both forms (source of truth). See
|
|
18
|
+
// the cross-language vector/color contract.
|
|
19
|
+
const ColorObject = z.object({
|
|
20
|
+
r: z.number().min(0).describe("Red, 0-1"),
|
|
21
|
+
g: z.number().min(0).describe("Green, 0-1"),
|
|
22
|
+
b: z.number().min(0).describe("Blue, 0-1"),
|
|
23
|
+
a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
|
|
24
|
+
});
|
|
25
|
+
const ColorSchema = z
|
|
26
|
+
.union([
|
|
27
|
+
ColorObject,
|
|
28
|
+
z.string().describe('Comma string "r,g,b,a", e.g. "1,0,0,1"'),
|
|
29
|
+
])
|
|
30
|
+
.describe('RGBA colour — object {r,g,b,a} (0-1) OR comma string "r,g,b,a"');
|
|
31
|
+
const Vector3Object = z.object({ x: z.number(), y: z.number(), z: z.number() });
|
|
32
|
+
const Vector3Schema = z
|
|
33
|
+
.union([
|
|
34
|
+
Vector3Object,
|
|
35
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
36
|
+
])
|
|
37
|
+
.describe('World position — object {x,y,z} OR comma string "x,y,z"');
|
|
38
|
+
const RotationSchema = z
|
|
39
|
+
.object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
|
|
40
|
+
.describe("Rotation {pitch,yaw,roll} in degrees");
|
|
41
|
+
export function registerWorldRenderTools(server, bridge) {
|
|
42
|
+
// ── add_water_body ───────────────────────────────────────────────
|
|
43
|
+
server.tool("add_water_body", "Create a swimmable/buoyant water region: a GameObject with a WaterVolume (s&box's buoyancy + drag physics volume, region defined by a trigger BoxCollider sized to `size`) plus, by default, a simple flat translucent-blue surface box at the water line so it's visible. HONEST LIMIT: the visual is a tinted dev-box stand-in, NOT a Gerstner/water shader — the dev material may render the tint opaque; swap in a real water material later. Only the WaterVolume properties you pass are set (engine defaults otherwise). Returns {created, gameObject{id,...}, size, appliedProperties[], surface{id,...}|null, note, nextSteps[]}. Next: start_play and drop a physics prop in to verify buoyancy; tune FluidDensity/LinearDrag/WaveAmplitude etc. with set_property on the WaterVolume. Scene-mutating — refused during play mode.", {
|
|
44
|
+
name: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("GameObject name. Defaults to 'Water'"),
|
|
48
|
+
position: Vector3Schema.optional().describe("World position of the volume's CENTRE (the surface sits at z + size.z/2 + surfaceOffset)"),
|
|
49
|
+
size: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Full extents of the volume as 'x,y,z' world units. Defaults to '512,512,128'"),
|
|
53
|
+
density: z
|
|
54
|
+
.number()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("WaterVolume.FluidDensity — higher = more buoyant. Engine default when omitted"),
|
|
57
|
+
linearDrag: z
|
|
58
|
+
.number()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("WaterVolume.LinearDrag on submerged bodies. Engine default when omitted"),
|
|
61
|
+
angularDrag: z
|
|
62
|
+
.number()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("WaterVolume.AngularDrag on submerged bodies. Engine default when omitted"),
|
|
65
|
+
waveAmplitude: z
|
|
66
|
+
.number()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("WaterVolume.WaveAmplitude — physics wave height (does NOT animate the visual surface)"),
|
|
69
|
+
waveFrequency: z
|
|
70
|
+
.number()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("WaterVolume.WaveFrequency — physics wave frequency"),
|
|
73
|
+
fluidVelocity: Vector3Schema.optional().describe("WaterVolume.FluidVelocity — current/flow direction and speed, world units/s"),
|
|
74
|
+
surfaceOffset: z
|
|
75
|
+
.number()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("WaterVolume.SurfaceOffset — shifts the water line relative to the top of the box"),
|
|
78
|
+
visualSurface: z
|
|
79
|
+
.boolean()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Create the tinted surface box child. Defaults to true"),
|
|
82
|
+
surfaceColor: ColorSchema.optional().describe("Tint of the visual surface. Defaults to translucent blue (0.15,0.35,0.55,0.6)"),
|
|
83
|
+
parentId: z.string().optional().describe("GUID of a parent GameObject"),
|
|
84
|
+
}, async (params) => {
|
|
85
|
+
const res = await bridge.send("add_water_body", params);
|
|
86
|
+
if (!res.success) {
|
|
87
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
88
|
+
}
|
|
89
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
90
|
+
});
|
|
91
|
+
// ── add_render_target_camera ─────────────────────────────────────
|
|
92
|
+
server.tool("add_render_target_camera", "CCTV / security monitor / mirror / simple portal: creates a secondary camera (IsMainCamera=false, Priority param) positioned/rotated or aimed at a target object, a display surface (scaled box with a ModelRenderer), and GENERATES a sealed wiring component that at runtime creates the render-target Texture (Texture.CreateRenderTarget builder), assigns camera.RenderTarget and sets the texture onto an anonymous complex-shader material on the display (attribute 'Color' = albedo; the screen is scene-lit, not emissive/unlit). Returns {created, camera{id,...}, display{id,...}|null, className, path, generatedCode, attachedAndWired, resolution, nextSteps[]}. FIRST call generates the .cs → follow nextSteps: trigger_hotload, then add_component_with_properties on the display with properties {SourceCamera: <camera GUID>}; a REPEAT call reusing the same `name` after the hotload attaches AND wires everything automatically (attachedAndWired=true). The feed only renders in play mode — verify with start_play + capture_view. If `name`'s .cs file exists but isn't compiled yet, the call errors — hotload first. Scene/file-mutating — refused during play mode.", {
|
|
93
|
+
name: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Class name for the generated wiring component. Defaults to 'RenderTargetDisplay'. Reuse the same name for extra cameras — after a hotload the class is reused and wired in one shot"),
|
|
97
|
+
directory: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
101
|
+
cameraName: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("GameObject name for the camera. Defaults to 'RT Camera'"),
|
|
105
|
+
position: Vector3Schema.optional().describe("World position of the camera"),
|
|
106
|
+
rotation: RotationSchema.optional().describe("World rotation of the camera"),
|
|
107
|
+
lookAtId: z
|
|
108
|
+
.string()
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("GUID of a GameObject to aim the camera at (overrides rotation; from get_scene_hierarchy / find_objects)"),
|
|
111
|
+
priority: z
|
|
112
|
+
.number()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("CameraComponent.Priority (integer). Defaults to 1"),
|
|
115
|
+
fieldOfView: z
|
|
116
|
+
.number()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Camera field of view in degrees (clamped 5-170). Engine default when omitted"),
|
|
119
|
+
resolution: z
|
|
120
|
+
.string()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Render-target size as 'width,height' pixels (min 16). Defaults to '512,512'"),
|
|
123
|
+
backgroundColor: ColorSchema.optional().describe("Camera background colour. Engine default when omitted"),
|
|
124
|
+
createDisplay: z
|
|
125
|
+
.boolean()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe("Create the display surface GameObject. Defaults to true. Pass false to wire your own surface later"),
|
|
128
|
+
displayPosition: Vector3Schema.optional().describe("World position of the display surface"),
|
|
129
|
+
displayRotation: RotationSchema.optional().describe("World rotation of the display surface"),
|
|
130
|
+
displaySize: z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe("Display box extents as 'x,y,z' world units (thin x = a wall screen facing ±x). Defaults to '4,128,72'"),
|
|
134
|
+
attribute: z
|
|
135
|
+
.string()
|
|
136
|
+
.optional()
|
|
137
|
+
.describe("Material attribute that receives the texture. Defaults to 'Color' (albedo on the complex shader). Sanitized to [A-Za-z0-9_]"),
|
|
138
|
+
}, async (params) => {
|
|
139
|
+
const res = await bridge.send("add_render_target_camera", params);
|
|
140
|
+
if (!res.success) {
|
|
141
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
142
|
+
}
|
|
143
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
144
|
+
});
|
|
145
|
+
// ── add_daynight_sun ─────────────────────────────────────────────
|
|
146
|
+
server.tool("add_daynight_sun", "Generate a sun/sky driver component for the clock made by create_day_night_clock — a COMPANION, not a replacement: the clock owns TimeOfDay, this renders it. Every frame it reads TimeOfDay/SunriseHour/SunsetHour off the clock (sibling first, else first in scene, retrying 1/s) and drives a DirectionalLight: sunrise→sunset maps to a 0-180° pitch arc (continuing below the horizon at night), LightColor runs a hardcoded gradient (warm dawn → white noon → orange dusk → dim blue night, HDR-scaled by Intensity since s&box lights have no Brightness field), and optionally lerps the SkyBox2D Tint between night blue and day white. The clock CLASS NAME is baked in as a typed reference — the call ERRORS if `clockClass` isn't found in the TypeLibrary or any project .cs (run create_day_night_clock first; a bad token would break the whole game-assembly compile). Returns {created, path, className, clockClass, sunYaw, intensity, driveSkybox, placedOn, note, nextSteps[]}. Next: trigger_hotload → attach to the DirectionalLight's GameObject → start_play, set_runtime_property TimeOfDay on the clock (6/12/20/0) + capture_view to verify the looks. File/scene-mutating — refused during play mode.", {
|
|
147
|
+
name: z
|
|
148
|
+
.string()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe("Class name for the generated component. Defaults to 'DayNightSun'"),
|
|
151
|
+
directory: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
155
|
+
clockClass: z
|
|
156
|
+
.string()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Class name of the day-night clock to bind to (the `name` you gave create_day_night_clock). Defaults to 'DayNightClock'. Must already exist as a compiled type or a project .cs file"),
|
|
159
|
+
sunYaw: z
|
|
160
|
+
.number()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("Compass heading of the sun's arc in degrees. Defaults to 30"),
|
|
163
|
+
intensity: z
|
|
164
|
+
.number()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("Brightness multiplier on the colour gradient (HDR, >1 valid; intensity = colour magnitude). Defaults to 1"),
|
|
167
|
+
driveSkybox: z
|
|
168
|
+
.boolean()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe("Also tint the scene's SkyBox2D between night blue and day white. Defaults to true"),
|
|
171
|
+
targetId: z
|
|
172
|
+
.string()
|
|
173
|
+
.optional()
|
|
174
|
+
.describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
175
|
+
}, async (params) => {
|
|
176
|
+
const res = await bridge.send("add_daynight_sun", params);
|
|
177
|
+
if (!res.success) {
|
|
178
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
179
|
+
}
|
|
180
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=worldrender.js.map
|