sbox-mcp-server 1.3.1 → 1.4.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/README.md +119 -113
- package/dist/index.d.ts +2 -2
- package/dist/index.js +25 -6
- package/dist/tools/characters.d.ts +4 -0
- package/dist/tools/characters.js +209 -0
- package/dist/tools/leveltools.d.ts +4 -0
- package/dist/tools/leveltools.js +148 -0
- package/dist/tools/objecttools.d.ts +4 -0
- package/dist/tools/objecttools.js +80 -0
- package/dist/tools/status.js +23 -8
- package/dist/tools/visuals.d.ts +4 -0
- package/dist/tools/visuals.js +259 -0
- package/dist/transport/bridge-client.d.ts +44 -7
- package/dist/transport/bridge-client.js +139 -52
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,113 +1,119 @@
|
|
|
1
|
-
# sbox-mcp-server
|
|
2
|
-
|
|
3
|
-
MCP Server for the s&box game engine. Lets Claude Code build s&box games through conversation —
|
|
4
|
-
|
|
5
|
-
## Fastest install — the Claude Code plugin
|
|
6
|
-
|
|
7
|
-
If you use Claude Code, the easiest install is the companion plugin. It registers this MCP server automatically, ships a workflow skill, and includes the `sbox-game-dev` specialist agent.
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
/plugin marketplace add LouSputthole/Sbox-Claude
|
|
11
|
-
/plugin install sbox-claude
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
You still need to install the s&box-side **bridge addon** into your project's `Libraries/` folder (see step 1 below). The plugin handles the Claude side; the addon handles the s&box side.
|
|
15
|
-
|
|
16
|
-
## Manual install — three steps
|
|
17
|
-
|
|
18
|
-
### 1. Install the bridge addon in s&box
|
|
19
|
-
|
|
20
|
-
The bridge addon runs inside the s&box editor and receives commands from this MCP server. It MUST live inside a project's `Libraries/` folder — putting it in s&box's global `addons/` will silently fail to compile.
|
|
21
|
-
|
|
22
|
-
```powershell
|
|
23
|
-
git clone https://github.com/LouSputthole/Sbox-Claude.git
|
|
24
|
-
cd Sbox-Claude
|
|
25
|
-
.\install.ps1 -RemoveStaleAddons # Windows, auto-detects your s&box project
|
|
26
|
-
./install.sh --remove-stale # Linux/Mac/WSL
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
See [INSTALL.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/INSTALL.md) for the full guide and manual fallback.
|
|
30
|
-
|
|
31
|
-
### 2. Register the MCP server with Claude Code
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
claude mcp add sbox -- npx sbox-mcp-server
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
This is the bare command — equivalent to what the plugin's `.mcp.json` does for you.
|
|
38
|
-
|
|
39
|
-
### 3. Open s&box
|
|
40
|
-
|
|
41
|
-
Open your project. The bridge starts automatically. Verify with:
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
Check the bridge status.
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
You should see `connected: true, handlerCount: 99`.
|
|
48
|
-
|
|
49
|
-
## How it works
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
Claude Code → (stdio) → sbox-mcp-server → (file IPC) → bridge addon → s&box editor
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Communication uses file-based IPC through `%TEMP%/sbox-bridge-ipc/`. The MCP server writes request JSON files, the bridge addon (running inside s&box) polls and processes on the main editor thread, then writes response files back. WebSocket is not used — s&box's sandboxed C# environment blocks `System.Net`.
|
|
56
|
-
|
|
57
|
-
## Tools (
|
|
58
|
-
|
|
59
|
-
| Category | Tools |
|
|
60
|
-
|----------|-------|
|
|
61
|
-
| **Project** | get_project_info, list_project_files, read_file, write_file |
|
|
62
|
-
| **Scripts** | create_script, edit_script, delete_script, trigger_hotload |
|
|
63
|
-
| **Scenes** | list_scenes, load_scene, save_scene, create_scene |
|
|
64
|
-
| **GameObjects** | create/delete/duplicate/rename, set_parent/enabled/transform |
|
|
65
|
-
| **Components** | get/set_property, get_all_properties, list_available, add_component, set_prefab_ref |
|
|
66
|
-
| **Hierarchy** | get_scene_hierarchy (with `maxDepth` + `rootId`), get/select/focus_object |
|
|
67
|
-
| **Assets** | search_assets, list_asset_library, install_asset, get_asset_info |
|
|
68
|
-
| **Materials** | assign_model, create/assign_material, set_material_property |
|
|
69
|
-
| **Audio** | list_sounds, create_sound_event, assign_sound, play_sound_preview |
|
|
70
|
-
| **Play Mode** | start/stop_play, is_playing |
|
|
71
|
-
| **Runtime** | get/set_runtime_property, take_screenshot |
|
|
72
|
-
| **Editor** | undo, redo |
|
|
73
|
-
| **Prefabs** | create/instantiate_prefab, list_prefabs, get_prefab_info |
|
|
74
|
-
| **Physics** | add_physics, add_collider, add_joint, raycast |
|
|
75
|
-
| **UI** | create_razor_ui, add_screen_panel, add_world_panel |
|
|
76
|
-
| **Templates** | create_player/npc_controller, create_game_manager, create_trigger_zone |
|
|
77
|
-
| **Networking** | network_helper, configure/status, spawn, ownership, sync, RPCs, lobby/event templates |
|
|
78
|
-
| **Publishing** | project_config, validate, thumbnail, package_details |
|
|
79
|
-
| **World gen** | invoke_button, list_component_buttons, raycast_terrain, build_terrain_mesh |
|
|
80
|
-
| **Map edit** | add_terrain_hill/clearing/trail, clear_terrain_features, sculpt_terrain |
|
|
81
|
-
| **Caves / Forest** | add_cave_waypoint, clear_cave_path, add_forest_poi/trail, set_forest_seed, clear_forest_pois, paint_forest_density |
|
|
82
|
-
| **Placement** | place_along_path |
|
|
83
|
-
| **Discovery** | describe_type, search_types, get_method_signature, find_in_project |
|
|
84
|
-
| **Status** | get_bridge_status |
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
##
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
# sbox-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP Server for the s&box game engine. Lets Claude Code build s&box games through conversation — 131 working tools for scenes, scripts, GameObjects, components, assets, materials, audio, physics, UI, networking, publishing, world-gen, lighting & atmosphere, characters, scene layout, and type discovery.
|
|
4
|
+
|
|
5
|
+
## Fastest install — the Claude Code plugin
|
|
6
|
+
|
|
7
|
+
If you use Claude Code, the easiest install is the companion plugin. It registers this MCP server automatically, ships a workflow skill, and includes the `sbox-game-dev` specialist agent.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/plugin marketplace add LouSputthole/Sbox-Claude
|
|
11
|
+
/plugin install sbox-claude
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
You still need to install the s&box-side **bridge addon** into your project's `Libraries/` folder (see step 1 below). The plugin handles the Claude side; the addon handles the s&box side.
|
|
15
|
+
|
|
16
|
+
## Manual install — three steps
|
|
17
|
+
|
|
18
|
+
### 1. Install the bridge addon in s&box
|
|
19
|
+
|
|
20
|
+
The bridge addon runs inside the s&box editor and receives commands from this MCP server. It MUST live inside a project's `Libraries/` folder — putting it in s&box's global `addons/` will silently fail to compile.
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
git clone https://github.com/LouSputthole/Sbox-Claude.git
|
|
24
|
+
cd Sbox-Claude
|
|
25
|
+
.\install.ps1 -RemoveStaleAddons # Windows, auto-detects your s&box project
|
|
26
|
+
./install.sh --remove-stale # Linux/Mac/WSL
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
See [INSTALL.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/INSTALL.md) for the full guide and manual fallback.
|
|
30
|
+
|
|
31
|
+
### 2. Register the MCP server with Claude Code
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
claude mcp add sbox -- npx sbox-mcp-server
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This is the bare command — equivalent to what the plugin's `.mcp.json` does for you.
|
|
38
|
+
|
|
39
|
+
### 3. Open s&box
|
|
40
|
+
|
|
41
|
+
Open your project. The bridge starts automatically. Verify with:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Check the bridge status.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You should see `connected: true, handlerCount: 99`.
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Claude Code → (stdio) → sbox-mcp-server → (file IPC) → bridge addon → s&box editor
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Communication uses file-based IPC through `%TEMP%/sbox-bridge-ipc/`. The MCP server writes request JSON files, the bridge addon (running inside s&box) polls and processes on the main editor thread, then writes response files back. WebSocket is not used — s&box's sandboxed C# environment blocks `System.Net`.
|
|
56
|
+
|
|
57
|
+
## Tools (131 — v1.4.0)
|
|
58
|
+
|
|
59
|
+
| Category | Tools |
|
|
60
|
+
|----------|-------|
|
|
61
|
+
| **Project** | get_project_info, list_project_files, read_file, write_file |
|
|
62
|
+
| **Scripts** | create_script, edit_script, delete_script, trigger_hotload |
|
|
63
|
+
| **Scenes** | list_scenes, load_scene, save_scene, create_scene |
|
|
64
|
+
| **GameObjects** | create/delete/duplicate/rename, set_parent/enabled/transform |
|
|
65
|
+
| **Components** | get/set_property, get_all_properties, list_available, add_component, set_prefab_ref |
|
|
66
|
+
| **Hierarchy** | get_scene_hierarchy (with `maxDepth` + `rootId`), get/select/focus_object |
|
|
67
|
+
| **Assets** | search_assets, list_asset_library, install_asset, get_asset_info |
|
|
68
|
+
| **Materials** | assign_model, create/assign_material, set_material_property |
|
|
69
|
+
| **Audio** | list_sounds, create_sound_event, assign_sound, play_sound_preview |
|
|
70
|
+
| **Play Mode** | start/stop_play, is_playing |
|
|
71
|
+
| **Runtime** | get/set_runtime_property, take_screenshot |
|
|
72
|
+
| **Editor** | undo, redo |
|
|
73
|
+
| **Prefabs** | create/instantiate_prefab, list_prefabs, get_prefab_info |
|
|
74
|
+
| **Physics** | add_physics, add_collider, add_joint, raycast |
|
|
75
|
+
| **UI** | create_razor_ui, add_screen_panel, add_world_panel |
|
|
76
|
+
| **Templates** | create_player/npc_controller, create_game_manager, create_trigger_zone |
|
|
77
|
+
| **Networking** | network_helper, configure/status, spawn, ownership, sync, RPCs, lobby/event templates |
|
|
78
|
+
| **Publishing** | project_config, validate, thumbnail, package_details |
|
|
79
|
+
| **World gen** | invoke_button, list_component_buttons, raycast_terrain, build_terrain_mesh |
|
|
80
|
+
| **Map edit** | add_terrain_hill/clearing/trail, clear_terrain_features, sculpt_terrain |
|
|
81
|
+
| **Caves / Forest** | add_cave_waypoint, clear_cave_path, add_forest_poi/trail, set_forest_seed, clear_forest_pois, paint_forest_density |
|
|
82
|
+
| **Placement** | place_along_path |
|
|
83
|
+
| **Discovery** | describe_type, search_types, get_method_signature, find_in_project |
|
|
84
|
+
| **Status** | get_bridge_status |
|
|
85
|
+
| **Visual & atmosphere** *(v1.4.0)* | add_light, set_fog, add_post_process, set_skybox, add_envmap_probe, apply_atmosphere, apply_post_fx_look |
|
|
86
|
+
| **Characters** *(v1.4.0)* | spawn_model, spawn_citizen, dress_citizen, set_bodygroup, pose_citizen, equip_model, set_look_at, add_ragdoll, set_expression |
|
|
87
|
+
| **Scene & level** *(v1.4.0)* | snap_to_ground, align_objects, distribute_objects, grid_duplicate, measure_distance |
|
|
88
|
+
| **Environment** *(v1.4.0)* | scatter_props, randomize_transforms, group_objects |
|
|
89
|
+
| **Object utilities** *(v1.4.0)* | find_objects, set_tint, replace_model, set_tags |
|
|
90
|
+
| **VFX** *(v1.4.0, experimental)* | spawn_particle, create_particle_effect, add_trail, add_beam — compile but runtime rendering unverified through the bridge; use a legacy `.vpcf` for visible particles |
|
|
91
|
+
|
|
92
|
+
## Working with Claude effectively
|
|
93
|
+
|
|
94
|
+
Two disciplines prevent the iteration-loop trap:
|
|
95
|
+
|
|
96
|
+
1. **After visual changes, call `take_screenshot` and read the PNG.** Claude is a multimodal model — it can see the result. Guessing about visual outcomes from code alone produces long iteration loops.
|
|
97
|
+
2. **Before writing code that touches an unfamiliar s&box type, call `describe_type` or `search_types`.** Reflection is the source of truth; training data goes stale across SDK versions.
|
|
98
|
+
|
|
99
|
+
The companion plugin's `sbox-build-feature` skill encodes this workflow plus the common gotchas. If you're not using the plugin, the same rules apply manually.
|
|
100
|
+
|
|
101
|
+
## Requirements
|
|
102
|
+
|
|
103
|
+
- **Node.js 18+**
|
|
104
|
+
- **s&box** with the bridge addon installed in your project's `Libraries/` folder
|
|
105
|
+
- **Claude Code**
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
- [Main README](https://github.com/LouSputthole/Sbox-Claude/blob/main/README.md) — full project overview
|
|
110
|
+
- [INSTALL.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/INSTALL.md) — install + manual fallback
|
|
111
|
+
- [TROUBLESHOOTING.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/TROUBLESHOOTING.md) — 10 most common failures
|
|
112
|
+
- [CHANGELOG.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/CHANGELOG.md) — release history
|
|
113
|
+
- [Plugin README](https://github.com/LouSputthole/Sbox-Claude/blob/main/plugins/sbox-claude/README.md) — Claude Code plugin docs
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
**GPL-3.0** — see [LICENSE](../LICENSE) for details.
|
|
118
|
+
|
|
119
|
+
Copyright (c) 2026 [sboxskins.gg](https://sboxskins.gg)
|
package/dist/index.d.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Entry point for the sbox-mcp MCP server.
|
|
4
4
|
*
|
|
5
5
|
* Creates an MCP server (stdio transport), connects to the s&box Bridge Addon
|
|
6
|
-
* via
|
|
6
|
+
* via file-based IPC (a shared temp dir), and registers all tool handlers. Each tool domain (project,
|
|
7
7
|
* scripts, console, scenes, etc.) has its own register function in src/tools/.
|
|
8
8
|
*
|
|
9
9
|
* CLI flags: --version / -v, --help / -h
|
|
10
|
-
* Environment: SBOX_BRIDGE_HOST,
|
|
10
|
+
* Environment: SBOX_BRIDGE_IPC_DIR (the real knob); SBOX_BRIDGE_HOST / SBOX_BRIDGE_PORT (legacy, cosmetic)
|
|
11
11
|
*/
|
|
12
12
|
export {};
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Entry point for the sbox-mcp MCP server.
|
|
4
4
|
*
|
|
5
5
|
* Creates an MCP server (stdio transport), connects to the s&box Bridge Addon
|
|
6
|
-
* via
|
|
6
|
+
* via file-based IPC (a shared temp dir), and registers all tool handlers. Each tool domain (project,
|
|
7
7
|
* scripts, console, scenes, etc.) has its own register function in src/tools/.
|
|
8
8
|
*
|
|
9
9
|
* CLI flags: --version / -v, --help / -h
|
|
10
|
-
* Environment: SBOX_BRIDGE_HOST,
|
|
10
|
+
* Environment: SBOX_BRIDGE_IPC_DIR (the real knob); SBOX_BRIDGE_HOST / SBOX_BRIDGE_PORT (legacy, cosmetic)
|
|
11
11
|
*/
|
|
12
12
|
import { readFileSync } from "fs";
|
|
13
13
|
import { fileURLToPath } from "url";
|
|
@@ -33,6 +33,10 @@ import { registerNetworkingTools } from "./tools/networking.js";
|
|
|
33
33
|
import { registerPublishingTools } from "./tools/publishing.js";
|
|
34
34
|
import { registerWorldTools } from "./tools/world.js";
|
|
35
35
|
import { registerDiscoveryTools } from "./tools/discovery.js";
|
|
36
|
+
import { registerVisualTools } from "./tools/visuals.js";
|
|
37
|
+
import { registerCharacterTools } from "./tools/characters.js";
|
|
38
|
+
import { registerLevelTools } from "./tools/leveltools.js";
|
|
39
|
+
import { registerObjectTools } from "./tools/objecttools.js";
|
|
36
40
|
// ── CLI flags ──────────────────────────────────────────────────────
|
|
37
41
|
const args = process.argv.slice(2);
|
|
38
42
|
/** Read the package version from package.json, or return "unknown" on failure. */
|
|
@@ -60,13 +64,15 @@ USAGE
|
|
|
60
64
|
node dist/index.js --version Show version
|
|
61
65
|
|
|
62
66
|
ENVIRONMENT VARIABLES
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
SBOX_BRIDGE_IPC_DIR IPC directory — MUST match the s&box addon's dir.
|
|
68
|
+
Default: <os tmpdir>/sbox-bridge-ipc
|
|
69
|
+
SBOX_BRIDGE_HOST Legacy/cosmetic — shown in get_bridge_status only
|
|
70
|
+
SBOX_BRIDGE_PORT Legacy/cosmetic — shown in get_bridge_status only
|
|
65
71
|
|
|
66
72
|
CONNECT TO CLAUDE CODE
|
|
67
73
|
claude mcp add sbox -- node /path/to/sbox-mcp-server/dist/index.js
|
|
68
74
|
|
|
69
|
-
TOOLS (
|
|
75
|
+
TOOLS (131 — +32 in v1.4.0: visual, characters, scene, environment, utilities)
|
|
70
76
|
Project: get_project_info, list_project_files, read_file, write_file
|
|
71
77
|
Scripts: create_script, edit_script, delete_script, trigger_hotload
|
|
72
78
|
Scenes: list_scenes, load_scene, save_scene, create_scene
|
|
@@ -93,6 +99,14 @@ TOOLS (99 working — was 109; 10 unimplementable tools removed in v1.3.0)
|
|
|
93
99
|
Placement: place_along_path
|
|
94
100
|
Discovery: describe_type, search_types, get_method_signature, find_in_project
|
|
95
101
|
Status: get_bridge_status
|
|
102
|
+
|
|
103
|
+
── New in v1.4.0 ───────────────────────────────────
|
|
104
|
+
Visual: add_light, set_fog, add_post_process, set_skybox, add_envmap_probe, apply_atmosphere, apply_post_fx_look
|
|
105
|
+
Characters: spawn_model, spawn_citizen, dress_citizen, set_bodygroup, pose_citizen, equip_model, set_look_at, add_ragdoll, set_expression
|
|
106
|
+
Scene: snap_to_ground, align_objects, distribute_objects, grid_duplicate, measure_distance
|
|
107
|
+
Environment: scatter_props, randomize_transforms, group_objects
|
|
108
|
+
Utilities: find_objects, set_tint, replace_model, set_tags
|
|
109
|
+
VFX (exp): spawn_particle, create_particle_effect, add_trail, add_beam
|
|
96
110
|
`);
|
|
97
111
|
process.exit(0);
|
|
98
112
|
}
|
|
@@ -125,7 +139,7 @@ If you're running inside Claude Code, install the companion plugin for the full
|
|
|
125
139
|
|
|
126
140
|
The plugin ships an \`sbox-build-feature\` skill that codifies the workflow above plus a list of common s&box gotchas (MathF not available in sandbox, Cloud assets ephemeral, head bone case-sensitive, CitizenAnimationHelper.IkRightHand works at runtime, etc.). Read its SKILL.md before starting non-trivial features.`,
|
|
127
141
|
});
|
|
128
|
-
// Bridge client
|
|
142
|
+
// Bridge client talks to the s&box editor via file IPC. host/port are cosmetic.
|
|
129
143
|
const bridge = new BridgeClient(process.env.SBOX_BRIDGE_HOST ?? "127.0.0.1", parseInt(process.env.SBOX_BRIDGE_PORT ?? "29015", 10));
|
|
130
144
|
// Register all tools
|
|
131
145
|
registerProjectTools(server, bridge);
|
|
@@ -146,6 +160,10 @@ registerNetworkingTools(server, bridge);
|
|
|
146
160
|
registerPublishingTools(server, bridge);
|
|
147
161
|
registerWorldTools(server, bridge);
|
|
148
162
|
registerDiscoveryTools(server, bridge);
|
|
163
|
+
registerVisualTools(server, bridge);
|
|
164
|
+
registerCharacterTools(server, bridge);
|
|
165
|
+
registerLevelTools(server, bridge);
|
|
166
|
+
registerObjectTools(server, bridge);
|
|
149
167
|
/** Start the MCP server on stdio and attempt initial Bridge connection. */
|
|
150
168
|
async function main() {
|
|
151
169
|
const transport = new StdioServerTransport();
|
|
@@ -159,6 +177,7 @@ async function main() {
|
|
|
159
177
|
console.error(" ║ https://sboxskins.gg ║");
|
|
160
178
|
console.error(" ╚═══════════════════════════════════════════════════╝");
|
|
161
179
|
console.error("");
|
|
180
|
+
console.error(`[sbox-mcp] IPC directory: ${bridge.getIpcDir()}`);
|
|
162
181
|
// Attempt initial connection to s&box (non-fatal if it fails)
|
|
163
182
|
try {
|
|
164
183
|
await bridge.connect();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Character & model tools (Batch 19): spawn world models, spawn animated
|
|
4
|
+
* Citizen characters, dress them with clothing, toggle bodygroups, and pose
|
|
5
|
+
* them via CitizenAnimationHelper.
|
|
6
|
+
*
|
|
7
|
+
* Unlike particles (Batch 18), everything here is a STATIC mesh/pose that
|
|
8
|
+
* renders in the editor viewport — so the screenshot loop works: after a
|
|
9
|
+
* change, take_screenshot and read the result. Animation *playback* is
|
|
10
|
+
* runtime, but poses preview in-editor via PlayAnimationsInEditorScene.
|
|
11
|
+
*/
|
|
12
|
+
const ColorSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
r: z.number().min(0).describe("Red, 0-1"),
|
|
15
|
+
g: z.number().min(0).describe("Green, 0-1"),
|
|
16
|
+
b: z.number().min(0).describe("Blue, 0-1"),
|
|
17
|
+
a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
|
|
18
|
+
})
|
|
19
|
+
.describe("RGBA colour as 0-1 floats (model tint)");
|
|
20
|
+
const Vector3Schema = z
|
|
21
|
+
.object({ x: z.number(), y: z.number(), z: z.number() })
|
|
22
|
+
.describe("World position {x,y,z}");
|
|
23
|
+
const RotationSchema = z
|
|
24
|
+
.object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
|
|
25
|
+
.describe("Rotation {pitch,yaw,roll} in degrees");
|
|
26
|
+
export function registerCharacterTools(server, bridge) {
|
|
27
|
+
// ── spawn_model ────────────────────────────────────────────────────
|
|
28
|
+
server.tool("spawn_model", "Spawn a GameObject with a ModelRenderer showing a model — the quick way to place a prop. Pass an engine/project model path (e.g. 'models/citizen/citizen.vmdl', 'models/dev/box.vmdl'). Cloud (sbox.game) models must be installed first via install_asset, then pass the resulting path. Add physics separately with add_collider/add_physics if needed.", {
|
|
29
|
+
model: z
|
|
30
|
+
.string()
|
|
31
|
+
.describe("Model path, e.g. 'models/dev/box.vmdl' or an installed model path"),
|
|
32
|
+
name: z.string().optional().describe("GameObject name"),
|
|
33
|
+
position: Vector3Schema.optional().describe("World position"),
|
|
34
|
+
rotation: RotationSchema.optional().describe("World rotation"),
|
|
35
|
+
scale: Vector3Schema.optional().describe("World scale (default 1,1,1)"),
|
|
36
|
+
tint: ColorSchema.optional().describe("Model tint colour"),
|
|
37
|
+
parentId: z.string().optional().describe("GUID of a parent GameObject"),
|
|
38
|
+
}, async (params) => {
|
|
39
|
+
const res = await bridge.send("spawn_model", params);
|
|
40
|
+
if (!res.success) {
|
|
41
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
// ── spawn_citizen ──────────────────────────────────────────────────
|
|
48
|
+
server.tool("spawn_citizen", "Spawn an animated Citizen character: a SkinnedModelRenderer with the Citizen model, plus (by default) a CitizenAnimationHelper so it idles. PlayAnimationsInEditorScene is enabled so the idle pose shows in the editor view (screenshot-verifiable). Dress it afterward with dress_citizen, pose it with pose_citizen.", {
|
|
49
|
+
name: z.string().optional().describe("GameObject name (default 'Citizen')"),
|
|
50
|
+
model: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Override the skinned model (default 'models/citizen/citizen.vmdl')"),
|
|
54
|
+
position: Vector3Schema.optional().describe("World position"),
|
|
55
|
+
rotation: RotationSchema.optional().describe("World rotation"),
|
|
56
|
+
scale: Vector3Schema.optional().describe("World scale (default 1,1,1)"),
|
|
57
|
+
tint: ColorSchema.optional().describe("Body tint colour"),
|
|
58
|
+
animator: z
|
|
59
|
+
.boolean()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Add a CitizenAnimationHelper for idle/pose (default true)"),
|
|
62
|
+
holdType: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Initial hold pose, e.g. None, Pistol, Rifle, Shotgun, HoldItem, Punch, Swing"),
|
|
66
|
+
moveStyle: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Movement style, e.g. Auto, Walk, Run"),
|
|
70
|
+
parentId: z.string().optional().describe("GUID of a parent GameObject"),
|
|
71
|
+
}, async (params) => {
|
|
72
|
+
const res = await bridge.send("spawn_citizen", params);
|
|
73
|
+
if (!res.success) {
|
|
74
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
// ── dress_citizen ──────────────────────────────────────────────────
|
|
81
|
+
server.tool("dress_citizen", "Dress a spawned Citizen (or any GameObject with a SkinnedModelRenderer) by applying .clothing resources. Pass an array of clothing resource paths; they're loaded, added to a ClothingContainer, and applied to the body. Returns which paths applied vs were not found.", {
|
|
82
|
+
id: z.string().describe("GUID of the Citizen GameObject"),
|
|
83
|
+
clothing: z
|
|
84
|
+
.array(z.string())
|
|
85
|
+
.describe("Clothing resource paths, e.g. ['models/citizen_clothes/jacket/jacket.clothing']"),
|
|
86
|
+
tint: z
|
|
87
|
+
.number()
|
|
88
|
+
.min(0)
|
|
89
|
+
.max(1)
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("ClothingContainer tint position 0-1 (skin/clothing colour variation)"),
|
|
92
|
+
}, async (params) => {
|
|
93
|
+
const res = await bridge.send("dress_citizen", params);
|
|
94
|
+
if (!res.success) {
|
|
95
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
// ── set_bodygroup ──────────────────────────────────────────────────
|
|
102
|
+
server.tool("set_bodygroup", "Show/hide a bodygroup on a SkinnedModelRenderer (e.g. hide hands when holding a tool, swap head variants). Provide value (int index) or choice (string name).", {
|
|
103
|
+
id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
|
|
104
|
+
name: z.string().describe("Bodygroup name"),
|
|
105
|
+
value: z.number().int().optional().describe("Bodygroup choice index"),
|
|
106
|
+
choice: z.string().optional().describe("Bodygroup choice by name (alternative to value)"),
|
|
107
|
+
}, async (params) => {
|
|
108
|
+
const res = await bridge.send("set_bodygroup", params);
|
|
109
|
+
if (!res.success) {
|
|
110
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
// ── pose_citizen ───────────────────────────────────────────────────
|
|
117
|
+
server.tool("pose_citizen", "Pose a Citizen by setting CitizenAnimationHelper params (enables PlayAnimationsInEditorScene so the pose shows in-editor). Set holdType (None/Pistol/Rifle/Shotgun/HoldItem/Punch/Swing), moveStyle (Auto/Walk/Run), specialMove, sitting (bool), and/or duckLevel (0-1).", {
|
|
118
|
+
id: z.string().describe("GUID of the Citizen GameObject (must have a CitizenAnimationHelper)"),
|
|
119
|
+
holdType: z.string().optional().describe("Hold pose, e.g. None, Pistol, Rifle, Shotgun, HoldItem"),
|
|
120
|
+
moveStyle: z.string().optional().describe("Movement style, e.g. Auto, Walk, Run"),
|
|
121
|
+
specialMove: z.string().optional().describe("Special move style, e.g. None, LedgeGrab, Roll"),
|
|
122
|
+
sitting: z.boolean().optional().describe("Sit the character (IsSitting)"),
|
|
123
|
+
duckLevel: z.number().min(0).max(1).optional().describe("Crouch amount, 0-1"),
|
|
124
|
+
}, async (params) => {
|
|
125
|
+
const res = await bridge.send("pose_citizen", params);
|
|
126
|
+
if (!res.success) {
|
|
127
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
// ── equip_model ────────────────────────────────────────────────────
|
|
134
|
+
server.tool("equip_model", "Attach a prop model (weapon, hat, tool) to a Citizen's bone or attachment point — the prop is parented so it follows that point. Tries the point as an attachment (hand_R, hand_L, eyes, hat) then as a bone. Great for arming NPCs or adding accessories.", {
|
|
135
|
+
id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
|
|
136
|
+
model: z.string().describe("Prop model path, e.g. 'models/dev/box.vmdl'"),
|
|
137
|
+
point: z
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Attachment or bone name (default 'hand_R'; try hand_L, eyes, hat, head)"),
|
|
141
|
+
offset: Vector3Schema.optional().describe("Local position offset from the point"),
|
|
142
|
+
rotation: RotationSchema.optional().describe("Local rotation offset"),
|
|
143
|
+
tint: ColorSchema.optional().describe("Prop tint colour"),
|
|
144
|
+
name: z.string().optional().describe("Name for the prop GameObject"),
|
|
145
|
+
}, async (params) => {
|
|
146
|
+
const res = await bridge.send("equip_model", params);
|
|
147
|
+
if (!res.success) {
|
|
148
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
// ── set_look_at ────────────────────────────────────────────────────
|
|
155
|
+
server.tool("set_look_at", "Aim a Citizen's gaze. Pass target {x,y,z} (spawns a LookTarget) or targetId (existing GameObject) and the head/eyes track it. Pass enabled:false to turn gaze tracking off. Tune eyesWeight/headWeight/bodyWeight (0-1).", {
|
|
156
|
+
id: z.string().describe("GUID of the Citizen (must have a CitizenAnimationHelper)"),
|
|
157
|
+
target: Vector3Schema.optional().describe("World point to look at"),
|
|
158
|
+
targetId: z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe("GUID of a GameObject to look at (overrides target)"),
|
|
162
|
+
enabled: z.boolean().optional().describe("false to disable gaze tracking"),
|
|
163
|
+
eyesWeight: z.number().min(0).max(1).optional().describe("Eye look weight 0-1"),
|
|
164
|
+
headWeight: z.number().min(0).max(1).optional().describe("Head turn weight 0-1"),
|
|
165
|
+
bodyWeight: z.number().min(0).max(1).optional().describe("Body turn weight 0-1"),
|
|
166
|
+
}, async (params) => {
|
|
167
|
+
const res = await bridge.send("set_look_at", params);
|
|
168
|
+
if (!res.success) {
|
|
169
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
// ── add_ragdoll ────────────────────────────────────────────────────
|
|
176
|
+
server.tool("add_ragdoll", "Add ModelPhysics to a skinned model so it becomes a ragdoll (physics-driven bones). NOTE: the ragdoll only flops in PLAY mode — it won't move in the static editor view, so this one is verified structurally, not by screenshot.", {
|
|
177
|
+
id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
|
|
178
|
+
motionEnabled: z
|
|
179
|
+
.boolean()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("Whether physics bodies start with motion enabled"),
|
|
182
|
+
}, async (params) => {
|
|
183
|
+
const res = await bridge.send("add_ragdoll", params);
|
|
184
|
+
if (!res.success) {
|
|
185
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
// ── set_expression ─────────────────────────────────────────────────
|
|
192
|
+
server.tool("set_expression", "Set a facial morph (blendshape) on a skinned model — e.g. smile, frown, blink. Call with NO morph to list the model's available morph names (returned as availableMorphs). weight is typically 0-1.", {
|
|
193
|
+
id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
|
|
194
|
+
morph: z
|
|
195
|
+
.string()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Morph/blendshape name (omit to list available morphs)"),
|
|
198
|
+
weight: z.number().optional().describe("Morph weight, typically 0-1 (default 1)"),
|
|
199
|
+
}, async (params) => {
|
|
200
|
+
const res = await bridge.send("set_expression", params);
|
|
201
|
+
if (!res.success) {
|
|
202
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=characters.js.map
|