sbox-mcp-server 1.20.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +125 -82
- package/dist/tools/aisystems.d.ts +4 -0
- package/dist/tools/aisystems.js +236 -0
- package/dist/tools/assets.js +5 -5
- package/dist/tools/audio.js +12 -19
- package/dist/tools/audit.d.ts +8 -0
- package/dist/tools/audit.js +65 -0
- package/dist/tools/batch.d.ts +8 -0
- package/dist/tools/batch.js +77 -0
- package/dist/tools/characters.js +6 -6
- package/dist/tools/cinematicrecording.d.ts +31 -0
- package/dist/tools/cinematicrecording.js +107 -0
- package/dist/tools/cinematics.js +2 -2
- package/dist/tools/components.js +2 -2
- package/dist/tools/debugdraw.js +4 -4
- package/dist/tools/diagnostics.js +7 -5
- package/dist/tools/dialoguefx.d.ts +4 -0
- package/dist/tools/dialoguefx.js +144 -0
- package/dist/tools/discovery.js +6 -6
- package/dist/tools/economysave.d.ts +27 -0
- package/dist/tools/economysave.js +231 -0
- package/dist/tools/gameobjects.js +7 -7
- package/dist/tools/gameplay.js +4 -4
- package/dist/tools/gameplayrecorder.d.ts +18 -0
- package/dist/tools/gameplayrecorder.js +73 -0
- package/dist/tools/inspection.js +6 -6
- package/dist/tools/leveltools.js +5 -5
- package/dist/tools/materials.js +4 -4
- package/dist/tools/movieauthoring.d.ts +31 -0
- package/dist/tools/movieauthoring.js +84 -0
- package/dist/tools/moviemaker.js +1 -1
- package/dist/tools/multiplayertest.d.ts +23 -0
- package/dist/tools/multiplayertest.js +62 -0
- package/dist/tools/networking.js +22 -22
- package/dist/tools/physics.js +4 -4
- package/dist/tools/playmode.js +5 -5
- package/dist/tools/playtest.js +2 -1
- package/dist/tools/prefabs.js +7 -3
- package/dist/tools/project.js +2 -2
- package/dist/tools/publishing.js +3 -3
- package/dist/tools/roundui.d.ts +22 -0
- package/dist/tools/roundui.js +88 -0
- package/dist/tools/scenes.js +2 -2
- package/dist/tools/scripts.js +3 -3
- package/dist/tools/statsachievements.d.ts +4 -0
- package/dist/tools/statsachievements.js +243 -0
- package/dist/tools/status.js +1 -1
- package/dist/tools/templates.js +7 -7
- package/dist/tools/ui.js +5 -5
- package/dist/tools/uifeedback.js +1 -1
- package/dist/tools/vehicles.d.ts +8 -0
- package/dist/tools/vehicles.js +98 -0
- package/dist/tools/visuals.js +8 -8
- package/dist/tools/workflow.d.ts +8 -0
- package/dist/tools/workflow.js +107 -0
- package/dist/tools/world.js +61 -58
- package/dist/tools/worldrender.d.ts +4 -0
- package/dist/tools/worldrender.js +183 -0
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Dialogue & Camera FX pair — two scaffold generators:
|
|
4
|
+
*
|
|
5
|
+
* - generate_lipsync_dialogue NPCs speak their lines with moving mouths
|
|
6
|
+
* (Synthesizer TTS + viseme-driven morphs + loose
|
|
7
|
+
* create_dialogue_system HUD bind)
|
|
8
|
+
* - create_camera_effects static conveniences over the SDK's built-in
|
|
9
|
+
* CameraComponent.AddShake/AddPunch/AddTilt
|
|
10
|
+
*
|
|
11
|
+
* Both write a .cs file into the project (scene/file-mutating; refused during
|
|
12
|
+
* play mode by the bridge dispatch). Both templates were live-verified on
|
|
13
|
+
* 2026-07-13: default render written into the live project, hotloaded, compile
|
|
14
|
+
* clean, TypeLibrary-load confirmed, then deleted. Key SDK facts verified the
|
|
15
|
+
* same day: Handle.LipSync.Visemes is IReadOnlyList<float> in the engine's
|
|
16
|
+
* 15-viseme order (read live from Sandbox.LipSync.VisemeNames), the viseme->
|
|
17
|
+
* morph mapping is baked into models (Model.GetVisemeMorph — nonzero on
|
|
18
|
+
* Citizen), and the camera built-ins are whitelist-callable from game code.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the aisystems.ts module shape: zod params, one bridge.send per tool,
|
|
21
|
+
* JSON.stringify(res.data) on success.
|
|
22
|
+
*/
|
|
23
|
+
// A world-space Vector3 accepted as EITHER an object {x,y,z} OR a comma string
|
|
24
|
+
// "x,y,z", passed through unchanged. The C# handler parses both forms.
|
|
25
|
+
const Vec3 = z
|
|
26
|
+
.union([
|
|
27
|
+
z.object({
|
|
28
|
+
x: z.number().describe("X"),
|
|
29
|
+
y: z.number().describe("Y"),
|
|
30
|
+
z: z.number().describe("Z"),
|
|
31
|
+
}),
|
|
32
|
+
z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
|
|
33
|
+
])
|
|
34
|
+
.describe('A world-space Vector3 — object {x,y,z} OR comma string "x,y,z"');
|
|
35
|
+
export function registerDialogueFxTools(server, bridge) {
|
|
36
|
+
// ── generate_lipsync_dialogue ─────────────────────────────────────
|
|
37
|
+
server.tool("generate_lipsync_dialogue", "Generate a lipsync dialogue performer — NPCs SPEAK their lines with MOVING MOUTHS: a sealed Component holding a [Property] line list (speaker GameObject name + text) that, per line, (a) mirrors the line into a generated create_dialogue_system HUD when one exists (loose TypeLibrary capability bind: List<string> Lines + Begin() + bool IsActive — neither system references the other), (b) speaks the text via Sandbox.Speech.Synthesizer positionally AT the speaker (per-speaker voice name/gender/age/rate via the Voices list), (c) drives the speaker's SkinnedModelRenderer mouth morphs from the live viseme stream — Handle.LipSync.Visemes (IReadOnlyList<float> in the engine's 15-viseme order, read live from Sandbox.LipSync.VisemeNames 2026-07-13) multiplied through the model's own baked viseme->morph table (Model.GetVisemeMorph — verified nonzero on Citizen, e.g. viseme_AA -> openjawL/R; NOT a hand-guessed morph map), with MorphScale + smoothing, and (d) advances when the audio handle stops (LineGapSeconds pause, LineTimeoutSeconds safety-skip, Skip() to cut a line short, StopDialogue() to abort). Static events: OnLineStarted(dialogue, lineIndex, speaker) + OnDialogueFinished(dialogue). Returns {created, path, className, lineCount, voiceCount, propertyNames[], note}. Next: trigger_hotload + get_compile_errors, attach (targetId re-call or add_component_with_properties), fill Lines/Voices in the inspector or bake them via params, call Begin() from game code (or autoStart:true; pair with create_interactable). Limits & honesty: the editor cannot playtest audio, so the LIVE viseme stream is RUNTIME-UNVERIFIED — the generated LogVisemes() helper + DebugLogVisemes property confirm it in seconds in play mode (the API surface and mapping data ARE live-verified); models without baked viseme data log a warning and stay audio-only (Citizen has it); voices are machine/OS-specific and TrySetVoice is best-effort; LOCAL-only — call Begin() inside an [Rpc.Broadcast] for everyone; a bound HUD's own advance input stays active (it only ends that HUD's display, not the audio). Refused during play mode; refuses to overwrite an existing file.", {
|
|
38
|
+
name: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Class/file name. Defaults to 'LipsyncDialogue'. Sanitized to a valid C# identifier."),
|
|
42
|
+
directory: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Subdirectory under the project root for the .cs file. Defaults to 'Code'."),
|
|
46
|
+
lines: z
|
|
47
|
+
.array(z.object({
|
|
48
|
+
speaker: z.string().optional().describe("Scene GameObject name to speak from (case-insensitive; drives THAT object's mouth). Empty/omitted = the component's own GameObject."),
|
|
49
|
+
text: z.string().describe("The spoken line."),
|
|
50
|
+
}))
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Dialogue lines baked as inspector-editable defaults. Defaults to a two-line demo on the own GameObject."),
|
|
53
|
+
voices: z
|
|
54
|
+
.array(z.object({
|
|
55
|
+
speaker: z.string().describe("Speaker name these voice settings apply to (matches lines[].speaker, case-insensitive)."),
|
|
56
|
+
voiceName: z.string().optional().describe("Exact installed OS voice name (machine-specific — the generated component's Voices list is inspector-editable). Empty = use gender/age hints."),
|
|
57
|
+
voiceGender: z.string().optional().describe("Voice gender hint used when voiceName is empty (e.g. 'Female', 'Male'). Pair with voiceAge. Passed through unvalidated."),
|
|
58
|
+
voiceAge: z.string().optional().describe("Voice age hint paired with voiceGender (e.g. 'Adult', 'Child', 'Senior')."),
|
|
59
|
+
rate: z.number().optional().describe("Speaking rate offset (integer): negative = slower, positive = faster. Defaults to 0."),
|
|
60
|
+
}))
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Per-speaker voice settings baked as defaults. Defaults to empty (every speaker uses the OS default voice)."),
|
|
63
|
+
volume: z.number().optional().describe("Playback volume for spoken lines. Defaults to 1."),
|
|
64
|
+
positional: z
|
|
65
|
+
.boolean()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("true (default): 3D audio parented to the speaker GameObject. false: flat 2D narrator voice."),
|
|
68
|
+
driveMouth: z
|
|
69
|
+
.boolean()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("true (default): drive the speaker's SkinnedModelRenderer mouth morphs from the viseme stream. false: audio-only."),
|
|
72
|
+
morphScale: z
|
|
73
|
+
.number()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Multiplier on viseme-derived morph weights (same idea as Sandbox.LipSync.MorphScale). Defaults to 1."),
|
|
76
|
+
mouthSmoothSeconds: z
|
|
77
|
+
.number()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Seconds of exponential smoothing on mouth morphs (0 = raw viseme weights). Defaults to 0.05."),
|
|
80
|
+
lineGapSeconds: z
|
|
81
|
+
.number()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Pause between a line's audio ending and the next line starting. Defaults to 0.2."),
|
|
84
|
+
lineTimeoutSeconds: z
|
|
85
|
+
.number()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Safety: a line whose audio never starts (synthesis pending/failed) is skipped after this many seconds. Defaults to 20."),
|
|
88
|
+
bindHud: z
|
|
89
|
+
.boolean()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("true (default): loosely bind a create_dialogue_system HUD in the scene (TypeLibrary capability match) and mirror each line into it. false: no HUD mirroring."),
|
|
92
|
+
autoStart: z
|
|
93
|
+
.boolean()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("true: Begin() fires in OnStart. Defaults to false (call Begin() from game code)."),
|
|
96
|
+
debugLogVisemes: z
|
|
97
|
+
.boolean()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("true: log the live viseme stream ~4x/second while speaking — the fast way to runtime-verify the mouth drive. Defaults to false."),
|
|
100
|
+
targetId: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("GUID of a GameObject to attach the component to (only attaches if the type is already in the TypeLibrary — hotload first, then re-call or use add_component_with_properties)."),
|
|
104
|
+
}, async (params) => {
|
|
105
|
+
const res = await bridge.send("generate_lipsync_dialogue", params);
|
|
106
|
+
if (!res.success) {
|
|
107
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
108
|
+
}
|
|
109
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
110
|
+
});
|
|
111
|
+
// ── create_camera_effects ─────────────────────────────────────────
|
|
112
|
+
server.tool("create_camera_effects", "Generate static conveniences over the SDK's BUILT-IN camera effects — CameraComponent.AddShake(amplitude, frequency, duration), AddPunch(Vector3 direction, amplitude, frequency, duration, fovAmplitude), AddPunch(Angles, ...) and AddTilt(Angles, duration, easeTime), all fire-and-forget, self-expiring, whitelist-verified in sandboxed game code 2026-07-13: a sealed Component exposing {name}.Shake/ShakeAt/Punch/PunchAngles/Tilt statics that resolve the main camera (Scene.Camera, else IsMainCamera search, else first camera; warn + return null when the scene has none) and return the live Sandbox.CameraEffectSystem.BaseEffect (Stop()/IsDone; ShakeAt sets Epicenter+Radius for distance falloff), plus one-word preset triggers — HitPunch() / ExplosionShake() / ExplosionShakeAt(position, radius) / LandingTilt() — driven by [Property] tunables. Statics work with NO instance placed; place the component only to tune presets in the inspector. RELATIONSHIP: create_camera_shake is the CONTINUOUS trauma model (AddTrauma accumulates and decays); these built-ins are ONE-SHOT engine effects — they compose safely, but don't fire both for the same event or hits feel doubled. Returns {created, path, className, staticApi[], presetTriggers[], propertyNames[], note}. Next: trigger_hotload + get_compile_errors, call the statics from game code (e.g. {name}.Shake(4, 25, 0.8) on explosion) or attach via targetId and trigger presets. Limits & honesty: compile + camera resolution verified; the editor cannot judge FEEL — tune amplitudes in a human playtest; effects are LOCAL visuals (wrap in [Rpc.Broadcast] for everyone). Refused during play mode; refuses to overwrite an existing file.", {
|
|
113
|
+
name: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe("Class/file name. Defaults to 'CameraFx'. Sanitized to a valid C# identifier."),
|
|
117
|
+
directory: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe("Subdirectory under the project root for the .cs file. Defaults to 'Code'."),
|
|
121
|
+
hitPunchDirection: Vec3.optional().describe("HitPunch preset: punch direction. Defaults to Vector3.Backward (camera kicks back)."),
|
|
122
|
+
hitPunchAmplitude: z.number().optional().describe("HitPunch preset: positional kick strength. Defaults to 8."),
|
|
123
|
+
hitPunchFrequency: z.number().optional().describe("HitPunch preset: oscillation frequency. Defaults to 20."),
|
|
124
|
+
hitPunchDuration: z.number().optional().describe("HitPunch preset: seconds. Defaults to 0.25."),
|
|
125
|
+
hitPunchFovAmplitude: z.number().optional().describe("HitPunch preset: FOV kick amount (0 = none). Defaults to 3."),
|
|
126
|
+
explosionShakeAmplitude: z.number().optional().describe("ExplosionShake preset: shake strength. Defaults to 5."),
|
|
127
|
+
explosionShakeFrequency: z.number().optional().describe("ExplosionShake preset: oscillation frequency. Defaults to 25."),
|
|
128
|
+
explosionShakeDuration: z.number().optional().describe("ExplosionShake preset: seconds. Defaults to 0.8."),
|
|
129
|
+
landingTiltAngles: Vec3.optional().describe("LandingTilt preset as {x: pitch, y: yaw, z: roll} degrees (or 'p,y,r'). Defaults to 5 pitch / 0 yaw / 2 roll."),
|
|
130
|
+
landingTiltDuration: z.number().optional().describe("LandingTilt preset: seconds the tilt lasts. Defaults to 0.35."),
|
|
131
|
+
landingTiltEase: z.number().optional().describe("LandingTilt preset: ease-in/out time within the duration. Defaults to 0.15."),
|
|
132
|
+
targetId: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("GUID of a GameObject to attach the component to — only needed to tune presets in the inspector; the statics work with no instance (only attaches if the type is already in the TypeLibrary — hotload first)."),
|
|
136
|
+
}, async (params) => {
|
|
137
|
+
const res = await bridge.send("create_camera_effects", params);
|
|
138
|
+
if (!res.success) {
|
|
139
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
140
|
+
}
|
|
141
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=dialoguefx.js.map
|
package/dist/tools/discovery.js
CHANGED
|
@@ -19,21 +19,21 @@ export function registerDiscoveryTools(server, bridge) {
|
|
|
19
19
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
20
20
|
});
|
|
21
21
|
// ── list_libraries ───────────────────────────────────────────────
|
|
22
|
-
server.tool("list_libraries", "List the s&box libraries/addons installed in this project (reads Libraries/ + each .sbproj). Discovers what's available to build ON — e.g. character controllers (fish.scc = Shrimple Character Controller, facepunch.playercontroller), world/spline/road tools — so you can leverage an installed library (add its components via add_component_with_properties, or generate code against its API) instead of writing from scratch. Returns ident
|
|
22
|
+
server.tool("list_libraries", "List the s&box libraries/addons installed in this project (reads Libraries/ + each .sbproj). Discovers what's available to build ON — e.g. character controllers (fish.scc = Shrimple Character Controller, facepunch.playercontroller), world/spline/road tools — so you can leverage an installed library (add its components via add_component_with_properties, or generate code against its API) instead of writing from scratch. Returns `count` and `libraries` [{folder, ident, org, title, type, enabled}] — ALL libraries, no limit or pagination; `enabled` is false when the library's .sbproj has been disabled (renamed .sbproj.disabled). Read-only.", {}, async () => {
|
|
23
23
|
const res = await bridge.send("list_libraries", {});
|
|
24
24
|
if (!res.success)
|
|
25
25
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
26
26
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
27
27
|
});
|
|
28
28
|
// ── search_types ─────────────────────────────────────────────────
|
|
29
|
-
server.tool("search_types", "Find types matching a name pattern.
|
|
29
|
+
server.tool("search_types", "Find loaded types matching a name pattern. Useful for discovering 'is there a built-in X for this?'. Returns `count` and `matches` with each type's name, fullName, isComponent, and isAbstract — results are silently truncated at `limit` (default 50), so narrow the pattern if you hit the cap. Pass a match's name to describe_type for its full member surface.", {
|
|
30
30
|
pattern: z.string().describe("Substring to match against type name (case-insensitive)"),
|
|
31
31
|
namespace: z
|
|
32
32
|
.string()
|
|
33
33
|
.optional()
|
|
34
34
|
.describe("Optional namespace filter (case-insensitive substring)"),
|
|
35
|
-
components_only: z.boolean().default(false),
|
|
36
|
-
limit: z.number().int().default(50),
|
|
35
|
+
components_only: z.boolean().default(false).describe("Only return Component subclasses (default false)"),
|
|
36
|
+
limit: z.number().int().default(50).describe("Maximum matches to return (default 50); the search stops silently at this cap"),
|
|
37
37
|
}, async (params) => {
|
|
38
38
|
const res = await bridge.send("search_types", params);
|
|
39
39
|
if (!res.success)
|
|
@@ -51,10 +51,10 @@ export function registerDiscoveryTools(server, bridge) {
|
|
|
51
51
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
52
52
|
});
|
|
53
53
|
// ── find_in_project ──────────────────────────────────────────────
|
|
54
|
-
server.tool("find_in_project", "Grep the user's s&box project for a symbol
|
|
54
|
+
server.tool("find_in_project", "Grep the user's s&box project for a symbol (case-sensitive substring; skips .git/bin/obj). Useful for finding usage examples of an API or seeing how the project already does something. Returns `symbol`, `count`, and `results` [{file, line, text}], capped at `max_results` (default 25) — raise it if you may be missing hits. Follow up with read_file on a result's file path.", {
|
|
55
55
|
symbol: z.string().describe("Substring or symbol to search for"),
|
|
56
56
|
extension: z.string().default(".cs").describe("File extension filter"),
|
|
57
|
-
max_results: z.number().int().default(25),
|
|
57
|
+
max_results: z.number().int().default(25).describe("Maximum hits to return (default 25); the search stops once reached"),
|
|
58
58
|
}, async (params) => {
|
|
59
59
|
const res = await bridge.send("find_in_project", params);
|
|
60
60
|
if (!res.success)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Economy & Save family — six Tier-2 scaffolds (Track E):
|
|
5
|
+
*
|
|
6
|
+
* - create_currency_account audited host-authoritative ledger: [Sync(FromHost)]
|
|
7
|
+
* balance + Deposit/Withdraw/TryTransfer + a fixed-size
|
|
8
|
+
* transaction ring buffer with GetRecentTransactions()
|
|
9
|
+
* - create_idle_economy geometric bulk-buy purchasing (BaseCost * Growth^Owned,
|
|
10
|
+
* closed-form Buy 1 / N / Max) + income auto-wired to a
|
|
11
|
+
* sibling wallet via TypeLibrary reflection
|
|
12
|
+
* - create_signed_save tamper-evident save: FNV-1a signature over payload+salt,
|
|
13
|
+
* verify-on-load, Sanitize() clamp hook, forced reset on
|
|
14
|
+
* mismatch, versioned
|
|
15
|
+
* - create_meta_progression between-runs roguelite meta: persistent meta-currency +
|
|
16
|
+
* unlock flags, BankRun(int) run-end seam, OnUnlocked event
|
|
17
|
+
* - add_steam_stat_currency currency persisted over Sandbox.Services.Stats
|
|
18
|
+
* (Steam-cloud, per account, per package ident)
|
|
19
|
+
* - create_loot_table_resource GameResource-based loot table assets ([AssetType],
|
|
20
|
+
* .loot files, nested tables, depth cap) + a resolver component
|
|
21
|
+
*
|
|
22
|
+
* All generate clean, self-contained sealed game code (.cs) into the project; file/scene
|
|
23
|
+
* mutating, refused during play mode by the bridge dispatch. Host-authoritative state uses
|
|
24
|
+
* [Sync(SyncFlags.FromHost)] + IsProxy guards throughout.
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerEconomySaveTools(server: McpServer, bridge: BridgeClient): void;
|
|
27
|
+
//# sourceMappingURL=economysave.d.ts.map
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Economy & Save family — six Tier-2 scaffolds (Track E):
|
|
4
|
+
*
|
|
5
|
+
* - create_currency_account audited host-authoritative ledger: [Sync(FromHost)]
|
|
6
|
+
* balance + Deposit/Withdraw/TryTransfer + a fixed-size
|
|
7
|
+
* transaction ring buffer with GetRecentTransactions()
|
|
8
|
+
* - create_idle_economy geometric bulk-buy purchasing (BaseCost * Growth^Owned,
|
|
9
|
+
* closed-form Buy 1 / N / Max) + income auto-wired to a
|
|
10
|
+
* sibling wallet via TypeLibrary reflection
|
|
11
|
+
* - create_signed_save tamper-evident save: FNV-1a signature over payload+salt,
|
|
12
|
+
* verify-on-load, Sanitize() clamp hook, forced reset on
|
|
13
|
+
* mismatch, versioned
|
|
14
|
+
* - create_meta_progression between-runs roguelite meta: persistent meta-currency +
|
|
15
|
+
* unlock flags, BankRun(int) run-end seam, OnUnlocked event
|
|
16
|
+
* - add_steam_stat_currency currency persisted over Sandbox.Services.Stats
|
|
17
|
+
* (Steam-cloud, per account, per package ident)
|
|
18
|
+
* - create_loot_table_resource GameResource-based loot table assets ([AssetType],
|
|
19
|
+
* .loot files, nested tables, depth cap) + a resolver component
|
|
20
|
+
*
|
|
21
|
+
* All generate clean, self-contained sealed game code (.cs) into the project; file/scene
|
|
22
|
+
* mutating, refused during play mode by the bridge dispatch. Host-authoritative state uses
|
|
23
|
+
* [Sync(SyncFlags.FromHost)] + IsProxy guards throughout.
|
|
24
|
+
*/
|
|
25
|
+
export function registerEconomySaveTools(server, bridge) {
|
|
26
|
+
// ── create_currency_account ───────────────────────────────────────
|
|
27
|
+
server.tool("create_currency_account", "Generate a host-authoritative currency ACCOUNT component (sealed) — the audited sibling of create_economy_wallet (wallet = simple money, account = money + a ledger). Balance is [Sync(SyncFlags.FromHost)] so clients can't author their own money; host-guarded Deposit(amount, reason), Withdraw(amount, reason) -> bool, and TryTransfer(otherAccount, amount, reason) -> bool each record a Transaction { Time (Time.Now), signed Amount, Reason, BalanceAfter } into a fixed-size ring buffer (historySize, default 32; oldest entries overwritten SILENTLY). GetRecentTransactions(max) returns them NEWEST FIRST — the ledger is HOST-SIDE ONLY and does not replicate (Balance does); proxies get an empty list. Bind the instance OnBalanceChanged(long) for HUD labels. Single-player safe. Returns { created, path, className, startingBalance, historySize, placedOn, note, nextSteps }. Next: trigger_hotload, then attach via targetId re-run or add_component_to_new_object. Refuses if the file already exists; refused during play mode. Use create_economy_wallet when you don't need the audit trail; pair with create_idle_economy (it auto-wires this account's Money/TrySpend).", {
|
|
28
|
+
name: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Class name for the generated component. Defaults to 'CurrencyAccount'"),
|
|
32
|
+
directory: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
36
|
+
startingBalance: z
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Balance the account opens with (host seeds it in OnStart). Defaults to 0"),
|
|
41
|
+
historySize: z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Transaction ring-buffer capacity (clamped 1..4096); fixed once the first transaction is recorded, oldest overwritten silently after that. Defaults to 32"),
|
|
46
|
+
targetId: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("GUID of a per-player/bank GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
50
|
+
}, async (params) => {
|
|
51
|
+
const res = await bridge.send("create_currency_account", params);
|
|
52
|
+
if (!res.success) {
|
|
53
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
54
|
+
}
|
|
55
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
56
|
+
});
|
|
57
|
+
// ── create_idle_economy ───────────────────────────────────────────
|
|
58
|
+
server.tool("create_idle_economy", "Generate a geometric idle-economy component (sealed): generators on the classic BaseCost * Growth^Owned cost curve with Buy 1 / Buy N / Buy Max — CostOf(index, count), MaxAffordable(index), TryBuy(index, count) and BuyMax(index) all use the CLOSED-FORM geometric series (cost = c0*(g^n-1)/(g-1), buyMax = floor(log_g(funds*(g-1)/c0+1))) — no per-copy loops, Buy 1000 is the same math as Buy 1. Wallet wiring is TypeLibrary reflection with NO compile-time wallet dependency (the shipped create_idle_income pattern): each income tick invokes AddMoney(long|int) on the first sibling component that has one, purchases invoke TrySpend(long|int), Buy Max reads the sibling's Money (or Balance) property — works out of the box next to create_economy_wallet or create_currency_account; with NO wallet sibling, purchases are refused with a Log.Warning (never silent) while TotalEarned still accumulates. Host-authoritative: mutations IsProxy-guarded; owned counts are HOST-SIDE state (not replicated); TotalEarned is [Sync(FromHost)]. Static events OnPurchased(index, count, cost) and OnIncomeTick(amount, total). BuyMax steps down once past a whole-currency rounding edge rather than failing. Returns { created, path, className, generators, tickSeconds, placedOn, note, nextSteps }. Next: trigger_hotload, place it NEXT TO a wallet on the same GameObject, tune the parallel GeneratorNames/BaseCosts/Growths/IncomesPerSecond lists with set_property. Refused during play mode. Pair with create_offline_progress for away-time earnings; use create_idle_income for a bare income ticker with no purchasing.", {
|
|
59
|
+
name: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Class name for the generated component. Defaults to 'IdleEconomy'"),
|
|
63
|
+
directory: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
67
|
+
tickSeconds: z
|
|
68
|
+
.number()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Seconds between income grants (floored at 0.1). Defaults to 1"),
|
|
71
|
+
generators: z
|
|
72
|
+
.array(z.object({
|
|
73
|
+
name: z.string().optional().describe("Generator display name"),
|
|
74
|
+
baseCost: z.number().optional().describe("Cost of the first copy. Defaults to 15"),
|
|
75
|
+
growth: z.number().optional().describe("Per-copy cost multiplier, min 1 (1.15 = classic curve). Defaults to 1.15"),
|
|
76
|
+
incomePerSecond: z.number().optional().describe("Income each owned copy produces per second. Defaults to 0.5"),
|
|
77
|
+
}))
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Baked-in generator defaults (inspector-tunable after generation). Omit for a starter trio: Cursor 15/1.15/0.5, Farm 200/1.15/4, Factory 3000/1.12/30"),
|
|
80
|
+
targetId: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("GUID of the GameObject to attach to — put it on the SAME GameObject as the wallet so the reflection wiring finds it (only attaches if the type is already loaded — hotload first)"),
|
|
84
|
+
}, async (params) => {
|
|
85
|
+
const res = await bridge.send("create_idle_economy", 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
|
+
// ── create_signed_save ────────────────────────────────────────────
|
|
92
|
+
server.tool("create_signed_save", "Generate a tamper-evident, versioned save-system component (sealed, owner-only). The SaveData payload POCO is serialized to JSON (Sandbox.Json), FNV-1a-64 hashed over payload + version + salt, and written as a signed envelope { Version, Payload, Signature } to FileSystem.Data. Load() re-verifies: a signature mismatch (hand-edited/corrupt file) triggers a FORCED RESET — the save file is DELETED, defaults are used, and the static OnTampered(reason) event fires (destructive and deliberate; tell the player). A version mismatch starts fresh without the tamper event (add migrations in Load). Loaded values pass a Sanitize() clamp hook so even a re-signed save can't smuggle absurd values. Dirty-flag autosave (autosaveSeconds, default 10; MarkDirty() to arm) + a final save in OnDestroy. HONEST LIMIT: the salt ships inside the game assembly, so this is tamper-EVIDENT (stops notepad edits), NOT cryptographically secure. If you omit salt, a unique random one is baked into the generated file — changing it later invalidates existing saves. Returns { created, path, className, fileName, version, autosaveSeconds, placedOn, note, nextSteps }. Next: trigger_hotload, attach, add your fields to SaveData + clamps to Sanitize(), bump version on shape changes. Refused during play mode. Use create_save_system for a plain unsigned save, create_save_slots for multi-slot UI flows, create_meta_progression for roguelite meta-state.", {
|
|
93
|
+
name: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Class name for the generated component. Defaults to 'SignedSave'"),
|
|
97
|
+
directory: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
101
|
+
fileName: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("FileSystem.Data path the signed envelope is written to. Defaults to 'save_signed.json'"),
|
|
105
|
+
version: z
|
|
106
|
+
.number()
|
|
107
|
+
.int()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe("Save-shape version baked into the file and the signature; mismatched files start fresh. Defaults to 1"),
|
|
110
|
+
salt: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("Signing salt baked into the generated code. Omit to bake a unique random salt (recommended); changing it later invalidates existing saves"),
|
|
114
|
+
autosaveSeconds: z
|
|
115
|
+
.number()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("Dirty-flag autosave cadence in seconds; 0 disables the heartbeat (OnDestroy still saves). Defaults to 10"),
|
|
118
|
+
targetId: z
|
|
119
|
+
.string()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe("GUID of a save-manager GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
122
|
+
}, async (params) => {
|
|
123
|
+
const res = await bridge.send("create_signed_save", params);
|
|
124
|
+
if (!res.success) {
|
|
125
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
126
|
+
}
|
|
127
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
128
|
+
});
|
|
129
|
+
// ── create_meta_progression ───────────────────────────────────────
|
|
130
|
+
server.tool("create_meta_progression", "Generate a between-runs roguelite meta-progression component (sealed, owner-only): persistent meta-currency + an unlock-flag dictionary saved to FileSystem.Data JSON (dirty-flag autosave + OnDestroy, the create_save_system shape). API: Grant(long), TrySpend(long) -> bool, Unlock(key) (idempotent — the static OnUnlocked(key) event fires only on the FIRST unlock, and unlocks write through to disk immediately), IsUnlocked(key) -> bool, and the run-end seam BankRun(int earned) which converts a finished run's earnings into meta-currency, bumps RunsBanked, and saves immediately — call it from your round machine's end-of-run transition (create_round_state_machine / create_round_phase_machine). Instance OnCurrencyChanged(long) drives meta-shop balance labels. Versioned payload: old-version files start fresh. IsProxy-guarded — in multiplayer each machine banks only its own local meta file (this is per-machine persistence, not a server economy). Returns { created, path, className, fileName, version, placedOn, note, nextSteps }. Next: trigger_hotload, attach to a persistent hub/menu-scene manager GameObject, gate content with IsUnlocked when building the player. Refused during play mode. Pair with create_currency_account (in-run money) and create_signed_save (if the meta file needs tamper evidence — this one is unsigned).", {
|
|
131
|
+
name: z
|
|
132
|
+
.string()
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Class name for the generated component. Defaults to 'MetaProgression'"),
|
|
135
|
+
directory: z
|
|
136
|
+
.string()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
139
|
+
fileName: z
|
|
140
|
+
.string()
|
|
141
|
+
.optional()
|
|
142
|
+
.describe("FileSystem.Data path the meta state is written to. Defaults to 'meta.json'"),
|
|
143
|
+
version: z
|
|
144
|
+
.number()
|
|
145
|
+
.int()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe("Payload version; mismatched files start fresh. Defaults to 1"),
|
|
148
|
+
autosaveSeconds: z
|
|
149
|
+
.number()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe("Dirty-flag autosave cadence in seconds; 0 disables the heartbeat (unlocks and BankRun still write through immediately). Defaults to 10"),
|
|
152
|
+
targetId: z
|
|
153
|
+
.string()
|
|
154
|
+
.optional()
|
|
155
|
+
.describe("GUID of a persistent manager GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
156
|
+
}, async (params) => {
|
|
157
|
+
const res = await bridge.send("create_meta_progression", params);
|
|
158
|
+
if (!res.success) {
|
|
159
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
160
|
+
}
|
|
161
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
162
|
+
});
|
|
163
|
+
// ── add_steam_stat_currency ───────────────────────────────────────
|
|
164
|
+
server.tool("add_steam_stat_currency", "Generate a currency component (sealed) persisted over Sandbox.Services.Stats — Steam-cloud persistence, per Steam account, per package ident, with NO local save file. The stat stores the ABSOLUTE balance: every Add(double)/TrySpend(double) pushes Stats.SetValue(statName, balance); Flush() (and OnDestroy) pushes the buffered writes. On start it reads the balance back asynchronously via Stats.GetLocalPlayerStats(ident) -> Refresh() -> Get(statName).Value and fires the static OnBalanceLoaded(double); wait for IsLoaded before showing the balance. CLOUD SEMANTICS (surprising): stat writes are buffered/rate-limited by the backend and apply ONLY to the LOCAL Steam user — calling this for another player silently does nothing, so attach it to the LOCAL player's GameObject (IsProxy guards keep remote copies inert); read-back is eventually consistent and can lag minutes behind writes — the in-session Balance property is the runtime truth. Dev sessions without a real published package ident may read back nothing (balance starts 0 with a log line). packageIdent defaults to the running package (Game.Ident). Returns { created, path, className, statName, packageIdent, flushEveryChange, placedOn, note, nextSteps }. Next: trigger_hotload, attach to the local player, bind OnBalanceChanged for the HUD. Refused during play mode. Use create_economy_wallet/create_currency_account for in-run networked money, create_signed_save for offline local persistence; pair with create_leaderboard_panel (the same stat can back a leaderboard).", {
|
|
165
|
+
name: z
|
|
166
|
+
.string()
|
|
167
|
+
.optional()
|
|
168
|
+
.describe("Class name for the generated component. Defaults to 'SteamStatCurrency'"),
|
|
169
|
+
directory: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
173
|
+
statName: z
|
|
174
|
+
.string()
|
|
175
|
+
.optional()
|
|
176
|
+
.describe("Sandbox.Services stat that stores the balance (the stat-name string is the contract between write and read-back). Defaults to 'currency'"),
|
|
177
|
+
packageIdent: z
|
|
178
|
+
.string()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("Package ident to read stats from. Omit/empty = the running package (Game.Ident)"),
|
|
181
|
+
flushEveryChange: z
|
|
182
|
+
.boolean()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe("Call Stats.Flush() after every balance change instead of relying on the buffered flush + OnDestroy flush (the backend rate-limits flushes). Defaults to false"),
|
|
185
|
+
targetId: z
|
|
186
|
+
.string()
|
|
187
|
+
.optional()
|
|
188
|
+
.describe("GUID of the LOCAL player's GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
189
|
+
}, async (params) => {
|
|
190
|
+
const res = await bridge.send("add_steam_stat_currency", params);
|
|
191
|
+
if (!res.success) {
|
|
192
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
193
|
+
}
|
|
194
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
195
|
+
});
|
|
196
|
+
// ── create_loot_table_resource ────────────────────────────────────
|
|
197
|
+
server.tool("create_loot_table_resource", "Generate GameResource-based loot tables — the data-asset sibling of create_weighted_loot_table. One .cs file containing THREE types: an entry POCO { Name, Weight, optional NestedTable reference }, a [AssetType]-registered GameResource loot-table class (designers author '.loot' files in the editor asset browser — New > Loot Table — after the hotload; NOTE: [AssetType(Name=..., Extension=..., Category=...)] is used because GameResourceAttribute is [Obsolete] on this SDK), and a '<name>Resolver' Component that rolls an assigned table by cumulative weight. Nested tables: an entry with a NestedTable rolls INTO that table instead of dropping its Name, capped at maxDepth (default 4) with a self-reference guard so cycles terminate (at the cap the deepest entry's Name is returned). Resolver.Roll() returns the item name (null + warning when no Table is assigned or the table is empty; entries with weight <= 0 never win; all-zero weights fall back to the first entry) and fires the static OnLoot(GameObject, item) event; roll HOST-SIDE and replicate the result yourself. targetId attaches the RESOLVER (the resource is an asset type, not a component). SURPRISING: pick an extension that is NOT a suffix of a built-in one (e.g. avoid 'cfg') or ResourceLibrary picks up engine files as phantom instances. Returns { created, path, className, resolverClass, extension, maxDepth, placedOn, note, nextSteps }. Next: trigger_hotload -> author .loot assets in the editor -> assign the resolver's Table (set_property with the asset path). Refused during play mode. Use create_weighted_loot_table for a single inline component with no asset files; create_gacha_drop_table for pity + duplicate mechanics.", {
|
|
198
|
+
name: z
|
|
199
|
+
.string()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("Class name for the generated GameResource (the resolver becomes '<name>Resolver'). Defaults to 'LootTableResource'"),
|
|
202
|
+
directory: z
|
|
203
|
+
.string()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
206
|
+
extension: z
|
|
207
|
+
.string()
|
|
208
|
+
.optional()
|
|
209
|
+
.describe("Asset file extension (lowercase alphanumerics; avoid suffixes of built-in extensions like 'cfg'). Defaults to 'loot'"),
|
|
210
|
+
title: z
|
|
211
|
+
.string()
|
|
212
|
+
.optional()
|
|
213
|
+
.describe("Display name of the asset type in the editor's New-asset menu. Defaults to 'Loot Table'"),
|
|
214
|
+
maxDepth: z
|
|
215
|
+
.number()
|
|
216
|
+
.int()
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("Default nested-table resolve depth cap baked into the resolver (clamped 0..16; also a [Property]). Defaults to 4"),
|
|
219
|
+
targetId: z
|
|
220
|
+
.string()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe("GUID of a GameObject to attach the RESOLVER component to (only attaches if the type is already loaded — hotload first)"),
|
|
223
|
+
}, async (params) => {
|
|
224
|
+
const res = await bridge.send("create_loot_table_resource", params);
|
|
225
|
+
if (!res.success) {
|
|
226
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
227
|
+
}
|
|
228
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=economysave.js.map
|
|
@@ -70,7 +70,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
70
70
|
};
|
|
71
71
|
});
|
|
72
72
|
// ── duplicate_gameobject ─────────────────────────────────────────
|
|
73
|
-
server.tool("duplicate_gameobject", "Clone a GameObject with all its components.
|
|
73
|
+
server.tool("duplicate_gameobject", "Clone a GameObject with all its components. Returns { duplicated, original, gameObject } — gameObject.id is the clone's new GUID; pass it to set_transform / add_component_with_properties. If offset is omitted the clone lands exactly on top of the original.", {
|
|
74
74
|
id: z.string().describe("GUID of the GameObject to duplicate"),
|
|
75
75
|
name: z.string().optional().describe("New name for the clone"),
|
|
76
76
|
offset: Vector3Schema.optional().describe("Position offset from original so the clone doesn't overlap"),
|
|
@@ -84,7 +84,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
84
84
|
};
|
|
85
85
|
});
|
|
86
86
|
// ── rename_gameobject ────────────────────────────────────────────
|
|
87
|
-
server.tool("rename_gameobject", "Change the display name of a GameObject", {
|
|
87
|
+
server.tool("rename_gameobject", "Change the display name of a GameObject identified by its GUID (the GUID itself never changes, so existing references stay valid). Returns { renamed, id, oldName, newName }. Name-based lookups (e.g. find_objects) will see the new name immediately.", {
|
|
88
88
|
id: z.string().describe("GUID of the GameObject"),
|
|
89
89
|
name: z.string().describe("New display name"),
|
|
90
90
|
}, async (params) => {
|
|
@@ -114,7 +114,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
114
114
|
};
|
|
115
115
|
});
|
|
116
116
|
// ── set_enabled ──────────────────────────────────────────────────
|
|
117
|
-
server.tool("set_enabled", "Enable or disable a GameObject (disabled objects are invisible and inactive)", {
|
|
117
|
+
server.tool("set_enabled", "Enable or disable a GameObject (disabled objects are invisible and inactive, including their components and children). Returns { id, enabled } confirming the new state.", {
|
|
118
118
|
id: z.string().describe("GUID of the GameObject"),
|
|
119
119
|
enabled: z.boolean().describe("true to enable, false to disable"),
|
|
120
120
|
}, async (params) => {
|
|
@@ -127,7 +127,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
127
127
|
};
|
|
128
128
|
});
|
|
129
129
|
// ── set_transform ────────────────────────────────────────────────
|
|
130
|
-
server.tool("set_transform", "Set position, rotation, and/or scale on a GameObject. Only provided values are changed", {
|
|
130
|
+
server.tool("set_transform", "Set position, rotation, and/or scale on a GameObject. Only provided values are changed; values apply in world space unless local=true. Returns { transformed, gameObject } with the full serialized object (id, name, position, rotation, scale, components) so you can verify what actually applied.", {
|
|
131
131
|
id: z.string().describe("GUID of the GameObject"),
|
|
132
132
|
position: Vector3Schema.optional().describe("New position"),
|
|
133
133
|
rotation: RotationSchema.optional().describe("New rotation"),
|
|
@@ -170,7 +170,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
170
170
|
};
|
|
171
171
|
});
|
|
172
172
|
// ── get_selected_objects ─────────────────────────────────────────
|
|
173
|
-
server.tool("get_selected_objects", "Get the GameObjects currently selected by the user in the s&box editor", {}, async () => {
|
|
173
|
+
server.tool("get_selected_objects", "Get the GameObjects currently selected by the user in the s&box editor. Returns { count, selected } where each entry is a serialized GameObject (id, name, enabled, position, rotation, scale, components, childCount) — use the ids with set_transform, add_component_with_properties, etc. Handy for 'do X to what I have selected' requests.", {}, async () => {
|
|
174
174
|
const res = await bridge.send("get_selected_objects");
|
|
175
175
|
if (!res.success) {
|
|
176
176
|
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
@@ -180,7 +180,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
180
180
|
};
|
|
181
181
|
});
|
|
182
182
|
// ── select_object ────────────────────────────────────────────────
|
|
183
|
-
server.tool("select_object", "Select a GameObject in the editor (highlights it in the hierarchy and scene view)", {
|
|
183
|
+
server.tool("select_object", "Select a GameObject in the editor (highlights it in the hierarchy and scene view). Replaces the current selection unless addToSelection=true. Returns { selected, id }; confirm the result with get_selected_objects.", {
|
|
184
184
|
id: z.string().describe("GUID of the GameObject to select"),
|
|
185
185
|
addToSelection: z
|
|
186
186
|
.boolean()
|
|
@@ -196,7 +196,7 @@ export function registerGameObjectTools(server, bridge) {
|
|
|
196
196
|
};
|
|
197
197
|
});
|
|
198
198
|
// ── focus_object ─────────────────────────────────────────────────
|
|
199
|
-
server.tool("focus_object", "
|
|
199
|
+
server.tool("focus_object", "Highlight a GameObject by selecting it in the editor. NOTE: s&box exposes no dedicated focus API, so this only sets the selection — it does NOT move any camera (returns { focused, id, note } saying so). To actually point the viewport at an object use frame_camera; to aim a screenshot use screenshot_from.", {
|
|
200
200
|
id: z.string().describe("GUID of the GameObject to focus"),
|
|
201
201
|
}, async (params) => {
|
|
202
202
|
const res = await bridge.send("focus_object", params);
|
package/dist/tools/gameplay.js
CHANGED
|
@@ -109,7 +109,7 @@ export function registerGameplayTools(server, bridge) {
|
|
|
109
109
|
});
|
|
110
110
|
// ── create_objective_system ───────────────────────────────────────
|
|
111
111
|
// The win/lose primitive — turns "objects in a scene" into "a game with a goal".
|
|
112
|
-
server.tool("create_objective_system", "Generate an ObjectiveManager component — the win/lose brain of a game. Tracks an objective (collect_all / reach_goal / survive_time / eliminate_all), fires a win, and handles a lose condition (fall below kill-Z / timer / out of lives). Self-contained C#; other systems call ObjectiveManager.Instance. Optionally placed as a scene singleton", {
|
|
112
|
+
server.tool("create_objective_system", "Generate an ObjectiveManager component — the win/lose brain of a game. Tracks an objective (collect_all / reach_goal / survive_time / eliminate_all), fires a win, and handles a lose condition (fall below kill-Z / timer / out of lives). Self-contained C#; other systems call ObjectiveManager.Instance. Optionally placed as a scene singleton. Returns { created, path, className, gameObject, note } — gameObject is the placed singleton, or null with a note when the fresh type isn't in the TypeLibrary yet. Follow with trigger_hotload, then get_compile_errors; if placement was skipped, place with add_component_to_new_object after the hotload", {
|
|
113
113
|
name: z
|
|
114
114
|
.string()
|
|
115
115
|
.optional()
|
|
@@ -310,7 +310,7 @@ export function registerGameplayTools(server, bridge) {
|
|
|
310
310
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
311
311
|
});
|
|
312
312
|
// -- create_pickup -----------------------------------------------------
|
|
313
|
-
server.tool("create_pickup", "Generate a trigger-based collectible component. On enter by a tagged object it raises OnCollected (wire it to your objective/score system) and despawns. Optionally builds a visible pickup GameObject with a trigger SphereCollider (+ a model) in one call", {
|
|
313
|
+
server.tool("create_pickup", "Generate a trigger-based collectible component. On enter by a tagged object it raises OnCollected (wire it to your objective/score system) and despawns. Optionally builds a visible pickup GameObject with a trigger SphereCollider (+ a model) in one call. Returns { created, path, className, gameObject, note } — gameObject is the placed pickup (null unless placeInScene=true); a note flags when the component couldn't attach because the fresh type needs a hotload. Follow with trigger_hotload, then get_compile_errors", {
|
|
314
314
|
name: z.string().optional().describe("Class name. Defaults to 'Pickup'"),
|
|
315
315
|
directory: z
|
|
316
316
|
.string()
|
|
@@ -379,7 +379,7 @@ export function registerGameplayTools(server, bridge) {
|
|
|
379
379
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
380
380
|
});
|
|
381
381
|
// -- create_leaderboard_panel ------------------------------------------
|
|
382
|
-
server.tool("create_leaderboard_panel", "Generate a Razor PanelComponent that fetches and displays a Sandbox.Services leaderboard derived from a stat name. Produces TWO files: {name}.razor and {name}.razor.scss. The panel auto-refreshes every 30 s, shows rank/displayName/value rows, handles loading state, and includes a BuildHash() override (razor-lint clean). Must be hosted under a ScreenPanel or WorldPanel. Stats must be configured for the project ident on sbox.game. Uses Leaderboards.Get(statName) + board.Refresh() -- the exact API from ServicesQueryHandler.", {
|
|
382
|
+
server.tool("create_leaderboard_panel", "Generate a Razor PanelComponent that fetches and displays a Sandbox.Services leaderboard derived from a stat name. Produces TWO files: {name}.razor and {name}.razor.scss. The panel auto-refreshes every 30 s, shows rank/displayName/value rows, handles loading state, and includes a BuildHash() override (razor-lint clean). Must be hosted under a ScreenPanel or WorldPanel. Stats must be configured for the project ident on sbox.game. Uses Leaderboards.Get(statName) + board.Refresh() -- the exact API from ServicesQueryHandler. Returns { created, razorPath, scssPath, className, note }. Follow with trigger_hotload, then get_compile_errors, then host it via add_screen_panel (panelComponent=className).", {
|
|
383
383
|
name: z.string().optional().describe("Class name for the panel component. Defaults to 'LeaderboardPanel'"),
|
|
384
384
|
directory: z.string().optional().describe("Subdirectory for the generated files. Defaults to 'Code/UI'"),
|
|
385
385
|
statName: z.string().optional().describe("Sandbox.Services stat name the leaderboard is derived from. Defaults to 'score'"),
|
|
@@ -407,7 +407,7 @@ export function registerGameplayTools(server, bridge) {
|
|
|
407
407
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
408
408
|
});
|
|
409
409
|
// -- create_stat_modifier_system ---------------------------------------
|
|
410
|
-
server.tool("create_stat_modifier_system", "Generate an enum-keyed stat modifier system with three modifier layers: SET (highest-priority-wins hard override), ADD (summed bonuses), MULT (multiplied factors applied last). Modifier storage uses parallel private Lists of primitive types (serialization-safe). RemoveModifiersFrom(source) cleans up all mods from a buff/debuff source by reference. Static OnStatChanged(stat, value) event fires after every add/remove. Mined from RPG/buff/debuff patterns across shipped s&box games.", {
|
|
410
|
+
server.tool("create_stat_modifier_system", "Generate an enum-keyed stat modifier system with three modifier layers: SET (highest-priority-wins hard override), ADD (summed bonuses), MULT (multiplied factors applied last). Modifier storage uses parallel private Lists of primitive types (serialization-safe). RemoveModifiersFrom(source) cleans up all mods from a buff/debuff source by reference. Static OnStatChanged(stat, value) event fires after every add/remove. Mined from RPG/buff/debuff patterns across shipped s&box games. Returns { created, path, className, stats, placedOn, note } — stats echoes the sanitized stat names ({name}Stat enum values); placedOn is the target GameObject when attached (needs the type hotloaded). Follow with trigger_hotload, then get_compile_errors.", {
|
|
411
411
|
name: z.string().optional().describe("Class name prefix -- generates {name}Stat enum + {name} Component. Defaults to 'StatSystem'"),
|
|
412
412
|
directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
|
|
413
413
|
stats: z.union([z.array(z.string()), z.string()]).optional().describe("Stat names as a JSON array or comma-separated string. Defaults to 'Health,Speed,Damage'"),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Gameplay Recording family — record live PLAY-MODE gameplay to a .movie clip
|
|
5
|
+
* via Sandbox.MovieMaker.MovieRecorder (shipped in the current editor build,
|
|
6
|
+
* verified live 2026-07-12; closes engine-watch #2 in docs/TOOL_BACKLOG.md):
|
|
7
|
+
*
|
|
8
|
+
* - record_gameplay_clip start an async frame-loop recording job
|
|
9
|
+
* - stop_gameplay_recording stop → persist as a project .movie asset
|
|
10
|
+
* - gameplay_recording_status poll the job
|
|
11
|
+
*
|
|
12
|
+
* The counterpart to the MovieMaker playback family (list_movies /
|
|
13
|
+
* add_movie_player / play_movie / stop_movie): those play clips, this CREATES
|
|
14
|
+
* them from live gameplay. None of the three are scene-mutating — they must
|
|
15
|
+
* stay callable during play mode, where recording lives.
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerGameplayRecorderTools(server: McpServer, bridge: BridgeClient): void;
|
|
18
|
+
//# sourceMappingURL=gameplayrecorder.d.ts.map
|