sbox-mcp-server 1.19.0 → 2.0.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.
Files changed (48) hide show
  1. package/dist/index.js +119 -82
  2. package/dist/tools/assets.js +5 -5
  3. package/dist/tools/audio.js +12 -19
  4. package/dist/tools/audit.d.ts +8 -0
  5. package/dist/tools/audit.js +65 -0
  6. package/dist/tools/batch.d.ts +8 -0
  7. package/dist/tools/batch.js +77 -0
  8. package/dist/tools/characters.js +6 -6
  9. package/dist/tools/cinematics.d.ts +19 -0
  10. package/dist/tools/cinematics.js +73 -0
  11. package/dist/tools/components.js +2 -2
  12. package/dist/tools/debugdraw.js +4 -4
  13. package/dist/tools/diagnostics.js +7 -5
  14. package/dist/tools/discovery.js +6 -6
  15. package/dist/tools/gameobjects.js +7 -7
  16. package/dist/tools/gameplay.js +4 -4
  17. package/dist/tools/inspection.js +6 -6
  18. package/dist/tools/interactionpack.d.ts +16 -0
  19. package/dist/tools/interactionpack.js +107 -0
  20. package/dist/tools/leveltools.js +5 -5
  21. package/dist/tools/looteconomy.d.ts +18 -0
  22. package/dist/tools/looteconomy.js +107 -0
  23. package/dist/tools/materials.js +4 -4
  24. package/dist/tools/moviemaker.d.ts +17 -0
  25. package/dist/tools/moviemaker.js +98 -0
  26. package/dist/tools/netprimitives.d.ts +18 -0
  27. package/dist/tools/netprimitives.js +110 -0
  28. package/dist/tools/networking.js +22 -22
  29. package/dist/tools/physics.js +4 -4
  30. package/dist/tools/playmode.js +5 -5
  31. package/dist/tools/playtest.js +2 -1
  32. package/dist/tools/prefabs.js +7 -3
  33. package/dist/tools/project.js +2 -2
  34. package/dist/tools/publishing.js +3 -3
  35. package/dist/tools/scenes.js +2 -2
  36. package/dist/tools/scripts.js +3 -3
  37. package/dist/tools/status.js +1 -1
  38. package/dist/tools/templates.js +7 -7
  39. package/dist/tools/ui.js +5 -5
  40. package/dist/tools/uifeedback.d.ts +16 -0
  41. package/dist/tools/uifeedback.js +83 -0
  42. package/dist/tools/vehicles.d.ts +8 -0
  43. package/dist/tools/vehicles.js +98 -0
  44. package/dist/tools/visuals.js +8 -8
  45. package/dist/tools/workflow.d.ts +8 -0
  46. package/dist/tools/workflow.js +107 -0
  47. package/dist/tools/world.js +61 -58
  48. package/package.json +55 -55
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Loot / Economy variants pack — three Tier-2 scaffolds (v1.20.0, Track D):
4
+ *
5
+ * - create_gacha_drop_table per-rarity roll + pity counter + duplicate detection,
6
+ * host-authoritative ([Rpc.Host] roll → [Rpc.Broadcast])
7
+ * - create_currency_pickup networked coin: optional magnet + host-validated grant
8
+ * into create_economy_wallet's AddMoney + replicated despawn
9
+ * - create_offline_progress DateTime delta on enable + clamp + deterministic tick replay
10
+ *
11
+ * All generate a clean, self-contained sealed Component .cs; file/scene-mutating, refused
12
+ * during play mode by the bridge dispatch. create_currency_pickup + create_offline_progress
13
+ * emit host/owner-authoritative code ([Sync]-free state stays host-side); create_gacha_drop_table
14
+ * routes the roll through [Rpc.Host] and announces via [Rpc.Broadcast].
15
+ */
16
+ export function registerLootEconomyTools(server, bridge) {
17
+ // ── create_gacha_drop_table ───────────────────────────────────────
18
+ server.tool("create_gacha_drop_table", "Generate a host-authoritative gacha / loot-box roller component. Two-level pick: parallel [Property] lists RarityNames + RarityWeights select a RARITY by cumulative weight (the create_weighted_loot_table shape), then a flat 'Rarity:Item' [Property] list (e.g. 'Legendary:Dragon Fang') picks an ITEM uniformly within that rarity — simple and inspector-editable. A pity counter (PityAfter, default 50) guarantees the rarest tier (the LAST entry in RarityNames) after N rolls without it and resets on a hit. Duplicate detection against an owned-items set fires a host-side OnDuplicate hook (marked TODO: convert dupes to shards/currency). Roll() routes to the host via an [Rpc.Host] RequestRoll (Rpc.Caller re-validated — NetFlags is not security) and the result fans out via [Rpc.Broadcast] so every machine fires the static OnRolled(rarity, item, isDuplicate) event; single-player safe (RPCs run locally). Use create_weighted_loot_table instead for a simpler single-tier weighted pick with no pity/dupe/networking. Pairs with create_economy_wallet (spend currency to roll) and create_inventory (store the pulls).", {
19
+ name: z
20
+ .string()
21
+ .optional()
22
+ .describe("Class name for the generated component. Defaults to 'GachaDropTable'"),
23
+ directory: z
24
+ .string()
25
+ .optional()
26
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
27
+ pityAfter: z
28
+ .number()
29
+ .int()
30
+ .optional()
31
+ .describe("Rolls without a rarest-tier hit before the next roll is guaranteed rarest. 0 disables pity. Defaults to 50"),
32
+ targetId: z
33
+ .string()
34
+ .optional()
35
+ .describe("GUID of a per-player/manager GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
36
+ }, async (params) => {
37
+ const res = await bridge.send("create_gacha_drop_table", params);
38
+ if (!res.success) {
39
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
40
+ }
41
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
42
+ });
43
+ // ── create_currency_pickup ────────────────────────────────────────
44
+ server.tool("create_currency_pickup", "Generate a networked coin / currency pickup component (sealed, Component.ITriggerListener). Host-spawned; when a GameObject carrying PlayerTag ('player') enters its trigger the HOST validates and grants Value (default 1) into a wallet on the player, then destroys the pickup network-wide (the host Destroy() replicates — there is no NetworkDestroy on this SDK). Optional magnet: while MagnetRadius (default 0 = off) is > 0 the coin accelerates toward the nearest player each FixedUpdate (host-side, capped by MaxMagnetSpeed). IsProxy guards keep the grant + despawn host-only in multiplayer (NetworkSpawn the coin on the host); single-player works with no networking. The deposit is reflection-free and dependency-free: a static Grant seam is wired ONCE to the direct typed call — player.Components.Get<EconomyWallet>()?.AddMoney(amount) — so the component compiles with NO hard reference to a specific wallet class (rename the wallet type if yours differs; mirrors create_pickup's self-contained convention). WalletComponentName (default 'EconomyWallet') is used to locate the wallet and name the fix if Grant is left unwired (never silent). Pairs with create_economy_wallet (AddMoney/TrySpend/CanAfford) and create_floating_combat_text (spawn a '+N' popup from OnCollected).", {
45
+ name: z
46
+ .string()
47
+ .optional()
48
+ .describe("Class name for the generated component. Defaults to 'CurrencyPickup'"),
49
+ directory: z
50
+ .string()
51
+ .optional()
52
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
53
+ value: z
54
+ .number()
55
+ .int()
56
+ .optional()
57
+ .describe("How much currency the pickup grants into the wallet. Defaults to 1"),
58
+ magnetRadius: z
59
+ .number()
60
+ .optional()
61
+ .describe("Magnet range in world units — within it the coin flies to the nearest player each FixedUpdate. 0 = magnet off. Defaults to 0"),
62
+ walletComponentName: z
63
+ .string()
64
+ .optional()
65
+ .describe("Type name of the wallet component to deposit into (used to locate it and to name the fix if the Grant seam is left unwired). Defaults to 'EconomyWallet'"),
66
+ targetId: z
67
+ .string()
68
+ .optional()
69
+ .describe("GUID of a coin GameObject to attach to — give it a trigger Collider (SphereCollider, IsTrigger=true). Only attaches if the type is already loaded — hotload first"),
70
+ }, async (params) => {
71
+ const res = await bridge.send("create_currency_pickup", params);
72
+ if (!res.success) {
73
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
74
+ }
75
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
76
+ });
77
+ // ── create_offline_progress ───────────────────────────────────────
78
+ server.tool("create_offline_progress", "Generate an offline / idle-progress component (sealed, owner/host-only) — the idle-game staple. Persists LastSeenUtc (DateTime) to FileSystem.Data JSON on a dirty-flag autosave heartbeat (AutosaveSeconds) and on OnDisabled, copying create_save_system's persistence patterns. On enable it computes elapsed = now − LastSeenUtc, guards a clock rollback (negative → 0), clamps to MaxOfflineHours (default 8), then replays that time through a SimulateOffline(double seconds) TODO hook in fixed TickSeconds chunks (default 1) so idle accumulation is deterministic (frame-rate independent), and fires the static OnOfflineProgressApplied(seconds) event (drive a 'welcome back, you earned X' screen). IsProxy-guarded so a client can't author their own offline earnings. Fill in the SimulateOffline hook with your idle math (e.g. wallet.AddMoney(rate*seconds)). Pairs with create_economy_wallet / create_save_system / create_stat_modifier_system.", {
79
+ name: z
80
+ .string()
81
+ .optional()
82
+ .describe("Class name for the generated component. Defaults to 'OfflineProgress'"),
83
+ directory: z
84
+ .string()
85
+ .optional()
86
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
87
+ maxOfflineHours: z
88
+ .number()
89
+ .optional()
90
+ .describe("Offline time is clamped to this many hours (stops a week-away paying out a week). Defaults to 8"),
91
+ tickSeconds: z
92
+ .number()
93
+ .optional()
94
+ .describe("SimulateOffline chunk size in seconds — smaller = finer-grained deterministic replay (floored at 0.1). Defaults to 1"),
95
+ targetId: z
96
+ .string()
97
+ .optional()
98
+ .describe("GUID of an idle/save-manager GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
99
+ }, async (params) => {
100
+ const res = await bridge.send("create_offline_progress", params);
101
+ if (!res.success) {
102
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
103
+ }
104
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
105
+ });
106
+ }
107
+ //# sourceMappingURL=looteconomy.js.map
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  */
6
6
  export function registerMaterialTools(server, bridge) {
7
7
  // ── assign_model ─────────────────────────────────────────────────
8
- server.tool("assign_model", "Set a 3D model on a GameObject's ModelRenderer. Creates the renderer component if it doesn't exist", {
8
+ server.tool("assign_model", "Set a 3D model on a GameObject's ModelRenderer. Creates the renderer component if it doesn't exist; errors if the model path can't be loaded. Returns { assigned, id, model } — follow with assign_material / set_material_property to style it, or take a screenshot to verify.", {
9
9
  id: z.string().describe("GUID of the GameObject"),
10
10
  model: z
11
11
  .string()
@@ -20,7 +20,7 @@ export function registerMaterialTools(server, bridge) {
20
20
  };
21
21
  });
22
22
  // ── create_material ──────────────────────────────────────────────
23
- server.tool("create_material", "Create a new material file (.vmat) with a shader and properties like color, roughness, metallic, texture", {
23
+ server.tool("create_material", "Create a new material file (.vmat, KV1 format) with a shader and properties like color, roughness, metallic, texture. Errors if the file already exists; when no properties are given it writes sensible PBR defaults (g_flMetalness 0, g_flRoughness 1). Returns { created, path, shader, propertiesWritten } — pass the returned path to recompile_asset (so the editor compiles it) and then assign_material.", {
24
24
  path: z
25
25
  .string()
26
26
  .describe("Relative path for the material (e.g. 'materials/walls/brick.vmat')"),
@@ -42,7 +42,7 @@ export function registerMaterialTools(server, bridge) {
42
42
  };
43
43
  });
44
44
  // ── assign_material ──────────────────────────────────────────────
45
- server.tool("assign_material", "Apply a material to a ModelRenderer on a GameObject. Optionally target a specific material slot", {
45
+ server.tool("assign_material", "Apply a material to a GameObject by setting its ModelRenderer's MaterialOverride (overrides the whole model's material). Requires an existing ModelRenderer (assign_model first) and errors if the material path can't be loaded. Returns { assigned, id, material } — tweak values afterwards with set_material_property.", {
46
46
  id: z.string().describe("GUID of the GameObject"),
47
47
  material: z
48
48
  .string()
@@ -61,7 +61,7 @@ export function registerMaterialTools(server, bridge) {
61
61
  };
62
62
  });
63
63
  // ── set_material_property ────────────────────────────────────────
64
- server.tool("set_material_property", "Change a property on the material assigned to a GameObject — color, roughness, metallic, texture, etc.", {
64
+ server.tool("set_material_property", "Change a property on the material assigned to a GameObject — color, roughness, metallic, texture, etc. Operates on the ModelRenderer's MaterialOverride; if none is assigned it auto-creates one from the default complex shader (no separate assign_material step needed). Returns { set, id, property, autoCreatedMaterial } — screenshot to verify the visual change.", {
65
65
  id: z.string().describe("GUID of the GameObject"),
66
66
  property: z
67
67
  .string()
@@ -0,0 +1,17 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * MovieMaker / cutscene family (v1.20.0) — first bridge coverage of
5
+ * Sandbox.MovieMaker (landed in the shipping build; verified live 2026-07-08):
6
+ *
7
+ * - list_movies enumerate the project's .movie resources
8
+ * - add_movie_player wire a MoviePlayer component + MovieResource
9
+ * - play_movie start playback (real playback advances in play mode)
10
+ * - stop_movie stop playback (optionally rewind)
11
+ *
12
+ * Movies are AUTHORED in the editor's Movie Maker dock — the bridge wires and
13
+ * plays them; it does not author keyframes. Only add_movie_player is
14
+ * scene-mutating; play/stop stay callable during play mode.
15
+ */
16
+ export declare function registerMovieMakerTools(server: McpServer, bridge: BridgeClient): void;
17
+ //# sourceMappingURL=moviemaker.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * MovieMaker / cutscene family (v1.20.0) — first bridge coverage of
4
+ * Sandbox.MovieMaker (landed in the shipping build; verified live 2026-07-08):
5
+ *
6
+ * - list_movies enumerate the project's .movie resources
7
+ * - add_movie_player wire a MoviePlayer component + MovieResource
8
+ * - play_movie start playback (real playback advances in play mode)
9
+ * - stop_movie stop playback (optionally rewind)
10
+ *
11
+ * Movies are AUTHORED in the editor's Movie Maker dock — the bridge wires and
12
+ * plays them; it does not author keyframes. Only add_movie_player is
13
+ * scene-mutating; play/stop stay callable during play mode.
14
+ */
15
+ export function registerMovieMakerTools(server, bridge) {
16
+ // ── list_movies ───────────────────────────────────────────────────
17
+ server.tool("list_movies", "List the project's .movie resources (Sandbox.MovieMaker clips authored in the editor's Movie Maker dock: Window → Movie Maker). Scans the ENTIRE Assets folder recursively and returns every .movie found — no limit or paging. Returns { count, movies, note } where each movie has { path (asset-relative — the form add_movie_player/play_movie expect), name, loadable (resolves via ResourceLibrary), hasCompiledClip }. Start here before add_movie_player / play_movie — if the list is empty, the movie has to be authored in the dock first (the bridge plays movies; it doesn't author keyframes).", {}, async () => {
18
+ const res = await bridge.send("list_movies", {});
19
+ if (!res.success) {
20
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
21
+ }
22
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23
+ });
24
+ // ── add_movie_player ──────────────────────────────────────────────
25
+ server.tool("add_movie_player", "Add a Sandbox.MovieMaker.MoviePlayer component and optionally wire a .movie resource into it — the cutscene playback primitive. Creates a new 'Movie Player' GameObject when no id is given. Set playOnStart to begin playback the moment play mode starts (intro cinematics), or leave it and trigger via play_movie (scripted cutscenes — call it from a trigger zone or dialogue beat). isLooping + timeScale map straight onto the component. Movies must already exist as .movie assets (list_movies; author in the Movie Maker dock). Scene-mutating — refused during play mode.", {
26
+ id: z
27
+ .string()
28
+ .optional()
29
+ .describe("GameObject GUID to attach to. Omit to create a new 'Movie Player' object"),
30
+ moviePath: z
31
+ .string()
32
+ .optional()
33
+ .describe("Asset-relative path of the .movie resource to wire (see list_movies)"),
34
+ isLooping: z.boolean().optional().describe("Loop playback"),
35
+ timeScale: z
36
+ .number()
37
+ .optional()
38
+ .describe("Playback speed multiplier (1 = normal)"),
39
+ createTargets: z
40
+ .boolean()
41
+ .optional()
42
+ .describe("Let the player create missing track-target objects on play"),
43
+ playOnStart: z
44
+ .boolean()
45
+ .optional()
46
+ .describe("Begin playing as soon as play mode starts (intro cinematic)"),
47
+ }, async (params) => {
48
+ const res = await bridge.send("add_movie_player", params);
49
+ if (!res.success) {
50
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
51
+ }
52
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
53
+ });
54
+ // ── play_movie ────────────────────────────────────────────────────
55
+ server.tool("play_movie", "Start MoviePlayer playback. Targets the MoviePlayer on the given GameObject, or the first MoviePlayer in the scene when id is omitted. Pass moviePath to load-and-play a different .movie on the same player; positionSeconds seeks before playing; isLooping/timeScale apply immediately. Clips genuinely advance in PLAY MODE (start_play first, then verify with capture_view) — in edit mode this only sets state, which the response calls out. NOT scene-mutating, so it works during play mode.", {
56
+ id: z
57
+ .string()
58
+ .optional()
59
+ .describe("GameObject GUID holding the MoviePlayer. Omit to use the first MoviePlayer in the scene"),
60
+ moviePath: z
61
+ .string()
62
+ .optional()
63
+ .describe("Asset-relative .movie path to load and play (otherwise plays the wired Resource)"),
64
+ positionSeconds: z
65
+ .number()
66
+ .optional()
67
+ .describe("Seek to this time (seconds) before playing"),
68
+ timeScale: z
69
+ .number()
70
+ .optional()
71
+ .describe("Playback speed multiplier (1 = normal)"),
72
+ isLooping: z.boolean().optional().describe("Loop playback"),
73
+ }, async (params) => {
74
+ const res = await bridge.send("play_movie", params);
75
+ if (!res.success) {
76
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
77
+ }
78
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
79
+ });
80
+ // ── stop_movie ────────────────────────────────────────────────────
81
+ server.tool("stop_movie", "Stop MoviePlayer playback (the counterpart to play_movie). Targets the MoviePlayer on the given GameObject, or the first MoviePlayer in the scene when id is omitted. Pass rewind to also reset the playhead to 0 so the next play_movie starts from the top. Works during play mode.", {
82
+ id: z
83
+ .string()
84
+ .optional()
85
+ .describe("GameObject GUID holding the MoviePlayer. Omit to use the first MoviePlayer in the scene"),
86
+ rewind: z
87
+ .boolean()
88
+ .optional()
89
+ .describe("Also reset the playhead to 0"),
90
+ }, async (params) => {
91
+ const res = await bridge.send("stop_movie", params);
92
+ if (!res.success) {
93
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
94
+ }
95
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
96
+ });
97
+ }
98
+ //# sourceMappingURL=moviemaker.js.map
@@ -0,0 +1,18 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Networking primitives pack — four multiplayer scaffolds (v1.20.0, Track B):
5
+ *
6
+ * - create_host_rpc_action validated + rate-limited [Rpc.Host] action skeleton
7
+ * - add_targeted_rpc Rpc.FilterInclude single-client (unicast) side-effect
8
+ * - create_local_player_resolver proxy-safe "who is MY player" resolver (online + offline)
9
+ * - add_host_migration_recovery proxy→authority transition detector + OnBecameHost hook
10
+ *
11
+ * Each generates ONE self-contained sealed Component .cs into the project via
12
+ * ScaffoldHelpers. These are the correctness primitives every networked s&box
13
+ * game hand-rolls (and usually gets wrong): host-authoritative validated actions,
14
+ * unicast RPCs, proxy-safe local-player resolution, and host-migration recovery.
15
+ * All are file/scene-mutating and refused during play mode by the bridge dispatch.
16
+ */
17
+ export declare function registerNetPrimitivesTools(server: McpServer, bridge: BridgeClient): void;
18
+ //# sourceMappingURL=netprimitives.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Networking primitives pack — four multiplayer scaffolds (v1.20.0, Track B):
4
+ *
5
+ * - create_host_rpc_action validated + rate-limited [Rpc.Host] action skeleton
6
+ * - add_targeted_rpc Rpc.FilterInclude single-client (unicast) side-effect
7
+ * - create_local_player_resolver proxy-safe "who is MY player" resolver (online + offline)
8
+ * - add_host_migration_recovery proxy→authority transition detector + OnBecameHost hook
9
+ *
10
+ * Each generates ONE self-contained sealed Component .cs into the project via
11
+ * ScaffoldHelpers. These are the correctness primitives every networked s&box
12
+ * game hand-rolls (and usually gets wrong): host-authoritative validated actions,
13
+ * unicast RPCs, proxy-safe local-player resolution, and host-migration recovery.
14
+ * All are file/scene-mutating and refused during play mode by the bridge dispatch.
15
+ */
16
+ export function registerNetPrimitivesTools(server, bridge) {
17
+ // ── create_host_rpc_action ────────────────────────────────────────
18
+ server.tool("create_host_rpc_action", "Generate a validated, rate-limited host-action component — the SAFE skeleton for 'a client asks the host to DO something' (buy, use, vote, interact, spend). The generated sealed Component exposes a client-callable Request() that forwards to an [Rpc.Host] SubmitRequest() which runs ON THE HOST: it re-resolves WHO called it via Rpc.Caller (never trusting client args for identity), enforces a per-SteamId cooldown backed by a Dictionary<ulong, TimeSince>, runs a clearly-marked TODO block for your authoritative action, and fires the static OnActionExecuted(Connection) event. This is the correct answer to the #1 multiplayer exploit class: [Rpc.Host] is callable by ANY client with forged args — NetFlags restrict who may INVOKE, which is not security — so identity + rate-limit + validation all live inside the host body. Single-player safe (no session → the RPC runs locally, caller falls back to Connection.Local). Attach it to the object that owns the action (a player, a station, or your game manager). Next steps: hotload, tag/attach, call Request() from input or a UI button, fill the TODO host block, and subscribe to OnActionExecuted. Covers the backlog's add_rate_limited_rpc. Optionally attached to an existing GameObject by GUID (after a hotload).", {
19
+ name: z
20
+ .string()
21
+ .optional()
22
+ .describe("Class name for the generated component. Defaults to 'HostRpcAction'"),
23
+ directory: z
24
+ .string()
25
+ .optional()
26
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
27
+ cooldownSeconds: z
28
+ .number()
29
+ .optional()
30
+ .describe("Minimum seconds between accepted requests, per calling player (the per-SteamId rate limit). Defaults to 1"),
31
+ targetId: z
32
+ .string()
33
+ .optional()
34
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
35
+ }, async (params) => {
36
+ const res = await bridge.send("create_host_rpc_action", 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
+ // ── add_targeted_rpc ──────────────────────────────────────────────
43
+ server.tool("add_targeted_rpc", "Generate a component demonstrating the unicast (single-client) RPC pattern via Rpc.FilterInclude. A normal [Rpc.Broadcast] runs on EVERY machine; the generated component's host-side SendTo(Connection target, string message) wraps its [Rpc.Broadcast] call in `using ( Rpc.FilterInclude( target ) )` so ONLY the target connection executes the body, which raises the static OnReceived(string) event. This is the RIGHT way to send something to one player — a private prompt, a personal reward toast, a per-player cutscene cue — instead of broadcasting to everyone and filtering on the client (which leaks data and wastes bandwidth). Call SendTo on the host (guarded behind Networking.IsActive so Networking.IsHost can't throw with no session; runs locally in solo). Attach to a networked manager object that is NetworkSpawn'd. Next steps: hotload, attach, call SendTo(player.Network.Owner, \"...\") from the host, and subscribe to OnReceived on the target client. Optionally attached to an existing GameObject by GUID (after a hotload).", {
44
+ name: z
45
+ .string()
46
+ .optional()
47
+ .describe("Class name for the generated component. Defaults to 'TargetedRpc'"),
48
+ directory: z
49
+ .string()
50
+ .optional()
51
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
52
+ targetId: z
53
+ .string()
54
+ .optional()
55
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
56
+ }, async (params) => {
57
+ const res = await bridge.send("add_targeted_rpc", params);
58
+ if (!res.success) {
59
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
60
+ }
61
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
62
+ });
63
+ // ── create_local_player_resolver ──────────────────────────────────
64
+ server.tool("create_local_player_resolver", "Generate a proxy-safe 'who is MY player?' resolver — the corpus footgun-killer that stops you running local-player logic against a proxy of someone else's player. The generated sealed Component exposes a static Local property that lazily finds the player GameObject belonging to THIS machine: online it's the tagged object whose Network.Owner is the local connection (Network.Owner == Connection.Local, or Network.IsOwner); offline/solo (no session) it's the first/only tagged player. The result is cached and revalidated with IsValid() so a destroyed or respawned player is re-resolved automatically. Also exposes a static IsLocal(GameObject) helper for filtering events (e.g. only open a station overlay for your own player). Works identically online and offline — no `if (Network.IsOwner)` guard that silently disables everything in a solo playtest. Attach ONE to a persistent object (your game manager) so PlayerTag is configurable in the inspector; the resolver is static and callable from anywhere. Next steps: hotload, attach, tag each player GameObject with the PlayerTag, then read <Name>.Local / filter with <Name>.IsLocal(go). Optionally attached to an existing GameObject by GUID (after a hotload).", {
65
+ name: z
66
+ .string()
67
+ .optional()
68
+ .describe("Class name for the generated component. Defaults to 'LocalPlayerResolver'"),
69
+ directory: z
70
+ .string()
71
+ .optional()
72
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
73
+ playerTag: z
74
+ .string()
75
+ .optional()
76
+ .describe("Tag that marks a player GameObject (players must carry this tag). Baked as the default PlayerTag, editable per-instance in the inspector. Defaults to 'player'"),
77
+ targetId: z
78
+ .string()
79
+ .optional()
80
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
81
+ }, async (params) => {
82
+ const res = await bridge.send("create_local_player_resolver", params);
83
+ if (!res.success) {
84
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
85
+ }
86
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
87
+ });
88
+ // ── add_host_migration_recovery ───────────────────────────────────
89
+ server.tool("add_host_migration_recovery", "Generate a host-migration recovery component — it detects when THIS machine takes authority over its GameObject (the proxy→authority transition that promotes a client to host when the old host leaves) and gives you a clean hook to rebuild host-only state. It tracks IsProxy each frame; when it flips from true (someone else was the authority) to false (now it's us), it fires the static OnBecameHost(GameObject) event and runs a virtual-style TODO rebuild region, then — after a tunable SettleSeconds delay so in-flight packets can land — runs a deferred validation region. s&box has a known bug where networked objects are often destroyed during host migration and the new host inherits stale transient state, so the recommended shape is: detect becoming host, aggressively rebuild (re-arm host-only loops/TimeUntil timers against your clock, TakeOwnership of orphans, rebuild handle maps by world position, reconcile the [Sync] registry against the real scene), and defer the sanity check ~1s. Inert offline (IsProxy is always false with no session). Attach to your host-authoritative manager object (NetworkSpawn'd). Next steps: hotload, attach, subscribe to OnBecameHost, and fill the two TODO regions. Requires a real host migration (a second client that becomes host) to fire — it can't be exercised in a solo playtest. Optionally attached to an existing GameObject by GUID (after a hotload).", {
90
+ name: z
91
+ .string()
92
+ .optional()
93
+ .describe("Class name for the generated component. Defaults to 'HostMigrationRecovery'"),
94
+ directory: z
95
+ .string()
96
+ .optional()
97
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
98
+ targetId: z
99
+ .string()
100
+ .optional()
101
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
102
+ }, async (params) => {
103
+ const res = await bridge.send("add_host_migration_recovery", params);
104
+ if (!res.success) {
105
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
106
+ }
107
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
108
+ });
109
+ }
110
+ //# sourceMappingURL=netprimitives.js.map
@@ -8,23 +8,23 @@ import { z } from "zod";
8
8
  */
9
9
  export function registerNetworkingTools(server, bridge) {
10
10
  // ── add_network_helper ────────────────────────────────────────────
11
- server.tool("add_network_helper", "Add a NetworkHelper component to the scene for quick multiplayer setup. Handles lobby creation and player prefab spawning", {
11
+ server.tool("add_network_helper", "Add a NetworkHelper component (with StartServer=true) to an existing GameObject for quick multiplayer setup at runtime it creates the lobby and spawns the player prefab per connection. Returns { added, id, component:'NetworkHelper' }. NOTE: the current handler requires id (create a holder with create_gameobject first) and does not apply maxPlayers/playerPrefab — wire PlayerPrefab afterward with set_prefab_ref/set_property", {
12
12
  id: z
13
13
  .string()
14
14
  .optional()
15
- .describe("GUID of existing GameObject. Creates new if omitted"),
15
+ .describe("GUID of the GameObject to attach to. Required in practice — the current handler errors when omitted (create a holder with create_gameobject first)"),
16
16
  name: z
17
17
  .string()
18
18
  .optional()
19
- .describe("Name for the network manager object. Defaults to 'Network Manager'"),
19
+ .describe("Rename the target GameObject to this. Omit to keep its current name"),
20
20
  maxPlayers: z
21
21
  .number()
22
22
  .optional()
23
- .describe("Maximum number of players in the lobby"),
23
+ .describe("Maximum number of players in the lobby (currently not applied by the handler)"),
24
24
  playerPrefab: z
25
25
  .string()
26
26
  .optional()
27
- .describe("Path to the player prefab to spawn for each connection"),
27
+ .describe("Path to the player prefab to spawn for each connection (currently not applied by the handler — set the NetworkHelper's PlayerPrefab afterward with set_prefab_ref)"),
28
28
  }, async (params) => {
29
29
  const res = await bridge.send("add_network_helper", params);
30
30
  if (!res.success) {
@@ -35,23 +35,23 @@ export function registerNetworkingTools(server, bridge) {
35
35
  };
36
36
  });
37
37
  // ── configure_network ─────────────────────────────────────────────
38
- server.tool("configure_network", "Configure networking settings on the existing NetworkHelper: max players, lobby name, player prefab, start server", {
38
+ server.tool("configure_network", "Configure lobby settings on Sandbox.Networking. Currently only lobbyName is applied (sets Networking.ServerName) — Networking.MaxPlayers is read-only on this SDK, and playerPrefab/startServer are not applied by the handler (use add_network_helper + set_prefab_ref for those). Returns { configured, maxPlayers, serverName } reflecting the live Networking values", {
39
39
  maxPlayers: z
40
40
  .number()
41
41
  .optional()
42
- .describe("Maximum number of players"),
42
+ .describe("Maximum number of players (currently not applied — Networking.MaxPlayers is read-only on this SDK; the live value is echoed in the response)"),
43
43
  lobbyName: z
44
44
  .string()
45
45
  .optional()
46
- .describe("Display name for the lobby"),
46
+ .describe("Display name for the lobby (sets Networking.ServerName — the only setting this handler applies)"),
47
47
  playerPrefab: z
48
48
  .string()
49
49
  .optional()
50
- .describe("Path to the player prefab"),
50
+ .describe("Path to the player prefab (currently not applied by the handler — set the NetworkHelper's PlayerPrefab via set_prefab_ref instead)"),
51
51
  startServer: z
52
52
  .boolean()
53
53
  .optional()
54
- .describe("Start the server/lobby immediately"),
54
+ .describe("Start the server/lobby immediately (currently not applied by the handler — add_network_helper sets StartServer=true on the NetworkHelper)"),
55
55
  }, async (params) => {
56
56
  const res = await bridge.send("configure_network", params);
57
57
  if (!res.success) {
@@ -62,7 +62,7 @@ export function registerNetworkingTools(server, bridge) {
62
62
  };
63
63
  });
64
64
  // ── get_network_status ────────────────────────────────────────────
65
- server.tool("get_network_status", "Check the current multiplayer status: connection state, player count, lobby info, networked objects", {}, async (params) => {
65
+ server.tool("get_network_status", "Check the current multiplayer status. Returns { isActive, isHost, isClient, isConnecting, maxPlayers, serverName } read from Sandbox.Networking — meaningful mostly in play mode with networking active. It does NOT return a player list or networked-object dump; use inspect_networked_object for a specific object's Network/[Sync] state", {}, async (params) => {
66
66
  const res = await bridge.send("get_network_status", params);
67
67
  if (!res.success) {
68
68
  return { content: [{ type: "text", text: `Error: ${res.error}` }] };
@@ -72,7 +72,7 @@ export function registerNetworkingTools(server, bridge) {
72
72
  };
73
73
  });
74
74
  // ── network_spawn ─────────────────────────────────────────────────
75
- server.tool("network_spawn", "Network-enable a GameObject so it is synchronized across all connected clients. Calls NetworkSpawn()", {
75
+ server.tool("network_spawn", "Network-enable a GameObject so it is synchronized across all connected clients. Calls NetworkSpawn(). Returns { spawned, id } on success — follow with inspect_networked_object to confirm the Network state, or set_ownership to hand it to a connection", {
76
76
  id: z.string().describe("GUID of the GameObject to network"),
77
77
  }, async (params) => {
78
78
  const res = await bridge.send("network_spawn", params);
@@ -84,12 +84,12 @@ export function registerNetworkingTools(server, bridge) {
84
84
  };
85
85
  });
86
86
  // ── set_ownership ─────────────────────────────────────────────────
87
- server.tool("set_ownership", "Transfer network ownership of a GameObject to a different connection, or take/drop ownership", {
87
+ server.tool("set_ownership", "Assign network ownership of a GameObject to a connection, or drop ownership. Omitting connectionId (or passing an empty string) calls Network.DropOwnership(); passing a connection Id or SteamId calls Network.AssignOwnership() on the matching live Connection. Returns { ownershipAssigned, id, connectionId } or { ownershipDropped, id }; errors if no connection matches (connections only exist while networking is active)", {
88
88
  id: z.string().describe("GUID of the networked GameObject"),
89
89
  connectionId: z
90
90
  .string()
91
91
  .optional()
92
- .describe("GUID of the target connection. Empty string = drop ownership. Omit = take ownership"),
92
+ .describe("Connection Id GUID or SteamId of the target connection. Omit OR pass an empty string to DROP ownership (there is no take-ownership mode in the current handler)"),
93
93
  }, async (params) => {
94
94
  const res = await bridge.send("set_ownership", params);
95
95
  if (!res.success) {
@@ -100,7 +100,7 @@ export function registerNetworkingTools(server, bridge) {
100
100
  };
101
101
  });
102
102
  // ── add_sync_property ─────────────────────────────────────────────
103
- server.tool("add_sync_property", "Annotate an EXISTING public property in a C# script with the [Sync] attribute so s&box replicates it across the network. This does NOT create a new property — the property named by `propertyName` must already be declared in the file; the tool only inserts the [Sync] attribute above it", {
103
+ server.tool("add_sync_property", "Annotate an EXISTING public property in a C# script with the [Sync] attribute so s&box replicates it across the network. This does NOT create a new property — the property named by `propertyName` must already be declared in the file; the tool only inserts the [Sync] attribute above it. Returns { added, path, property, attribute } — attribute echoes the exact [Sync...] emitted; errors if the property already has [Sync] or isn't found. Follow with trigger_hotload, then get_compile_errors", {
104
104
  path: z
105
105
  .string()
106
106
  .describe("Relative path to the script file (e.g. 'code/Player.cs')"),
@@ -156,7 +156,7 @@ export function registerNetworkingTools(server, bridge) {
156
156
  };
157
157
  });
158
158
  // ── create_networked_player ───────────────────────────────────────
159
- server.tool("create_networked_player", "Generate a network-aware player controller with [Sync] properties, owner-only input, and [Rpc.Broadcast] actions", {
159
+ server.tool("create_networked_player", "Generate a network-aware player controller with [Sync] properties, owner-only input, and [Rpc.Broadcast] actions. Writes <name>.cs and returns { created, path, className }. The generated Component syncs PlayerName/Health, moves a CharacterController from Input.AnalogMove behind an IsProxy guard, and exposes a [Property] MoveSpeed plus an [Rpc.Broadcast] TakeDamage(int). Follow with trigger_hotload, then get_compile_errors, then attach + network_spawn", {
160
160
  name: z
161
161
  .string()
162
162
  .optional()
@@ -165,11 +165,11 @@ export function registerNetworkingTools(server, bridge) {
165
165
  .string()
166
166
  .optional()
167
167
  .describe("Subdirectory under code/"),
168
- moveSpeed: z.number().optional().describe("Movement speed. Defaults to 300"),
168
+ moveSpeed: z.number().optional().describe("Movement speed (generated MoveSpeed [Property]). Defaults to 200"),
169
169
  includeHealth: z
170
170
  .boolean()
171
171
  .optional()
172
- .describe("Include health/damage system with host-authoritative TakeDamage. Defaults to true"),
172
+ .describe("Currently not applied by the handler — the [Sync] Health and [Rpc.Broadcast] TakeDamage are always generated"),
173
173
  }, async (params) => {
174
174
  const res = await bridge.send("create_networked_player", params);
175
175
  if (!res.success) {
@@ -180,7 +180,7 @@ export function registerNetworkingTools(server, bridge) {
180
180
  };
181
181
  });
182
182
  // ── create_lobby_manager ──────────────────────────────────────────
183
- server.tool("create_lobby_manager", "Generate a lobby manager script with create/join/leave lobby, player spawning, and connection cleanup", {
183
+ server.tool("create_lobby_manager", "Generate a lobby manager Component implementing Component.INetworkListener: a static Instance singleton, a [Sync] PlayerCount maintained in OnActive/OnDisconnected, and a LobbyState [Property] that flips to 'playing' when the lobby fills. Writes <name>.cs and returns { created, path, className }. Follow with trigger_hotload, then get_compile_errors, then place via add_component_to_new_object", {
184
184
  name: z
185
185
  .string()
186
186
  .optional()
@@ -192,7 +192,7 @@ export function registerNetworkingTools(server, bridge) {
192
192
  maxPlayers: z
193
193
  .number()
194
194
  .optional()
195
- .describe("Default max players. Defaults to 8"),
195
+ .describe("Currently not applied by the handler — the generated MaxPlayers [Property] defaults to 16; tune it per-instance with set_property"),
196
196
  }, async (params) => {
197
197
  const res = await bridge.send("create_lobby_manager", params);
198
198
  if (!res.success) {
@@ -203,7 +203,7 @@ export function registerNetworkingTools(server, bridge) {
203
203
  };
204
204
  });
205
205
  // ── create_network_events ─────────────────────────────────────────
206
- server.tool("create_network_events", "Generate a network event handler script implementing INetworkListener for connect/disconnect/chat events", {
206
+ server.tool("create_network_events", "Generate a network event relay Component: [Rpc.Broadcast] SendEvent(eventName, payload) to all clients and [Rpc.Host] SendEventToHost(...), both dispatching into a local OnNetworkEvent switch you extend. It does NOT implement INetworkListener — use create_lobby_manager for connect/disconnect hooks. Writes <name>.cs and returns { created, path, className }. Follow with trigger_hotload, then get_compile_errors", {
207
207
  name: z
208
208
  .string()
209
209
  .optional()
@@ -215,7 +215,7 @@ export function registerNetworkingTools(server, bridge) {
215
215
  includeChat: z
216
216
  .boolean()
217
217
  .optional()
218
- .describe("Include a chat message broadcast system. Defaults to false"),
218
+ .describe("Currently not applied by the handler — no chat system is generated"),
219
219
  }, async (params) => {
220
220
  const res = await bridge.send("create_network_events", params);
221
221
  if (!res.success) {
@@ -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. Supports box, sphere, capsule, mesh, and hull types. Can be configured as trigger", {
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 between two GameObjects. Supports fixed, spring, and slider joint types", {
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) and return hit results. Useful for line-of-sight checks, object placement, and collision detection", {
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