Imported from coffeegrind123/claude-sbox-setup (
skill/SKILL.md). Install upstream withnpx skills add coffeegrind123/claude-sbox-setup --skill skill. Copyright stays with the author.
name: sbox-live
description: Use when working in an s&box (Facepunch's Source 2 game engine) project: writing or editing C# components/Razor panels/scenes, animation graphs, debugging editor behavior, or interacting with the live editor through the claude-sbox in-editor MCP server. Trigger on using Sandbox;, : Component, [Property], [Sync], [Rpc.Broadcast], PanelComponent, *.razor, *.scene, *.prefab, *.sbproj, *.vanmgrph/*.vsubgrph (animation graph KV3 source), and animgraph concepts (anim nodes, state machines, blend spaces, IK, anim parameters). Also trigger when editing files under ~/sbox-public/ or any folder named addons/. Replaces and supersedes the third-party sbox skill.
sbox-live: live s&box integration skill
This skill is one half of the ClaudeSbox ↔ s&box deep integration. The other half is the in-editor tool addon (ghage/claude-sbox on sbox.game) that hosts an MCP server on port 6790 and exposes editor introspection / control to you.
Two endpoints, one registry. The same ~730-tool Dispatcher is reachable two ways: (1) the addon's own host on :6790 — the established path (sbox-mcp-bridge, claude mcp add … :6790/mcp); (2) the engine's built-in MCP server on :7269 (Facepunch ships one), through which the addon exposes a gateway. Same Dispatcher, same AutoToast/telemetry either way. Discover tools on demand instead of paging the whole list — the way Facepunch's built-in server works: on :6790 use search_tools(query) → call_tool(name, args) (plus list_toolsets/describe_toolset, and set_tool_list_mode listed for a tiny staleness-proof tools/list); on :7269 use sbox_search_tools(query) → sbox_call_tool(name, args) / sbox_call_tools(calls). Reach for list_tools only when you deliberately want the full inventory.
When you're in an s&box context you have ground-truth docs pipelines (live, no snapshots) plus live editor introspection + drive. The full capability map — all 13 buckets with their exact tool surfaces (schema, first-party + community + hosted docs, scene/inspector, UI drive, spotlight, preferences, reflection, compile/hotload + execute_csharp REPL, asset/cloud/event-bus, gizmo/convars/Qt, filesystem reads) — lives in references/mcp-tools.md § Capability map. The essentials you should keep in mind on every prompt:
Docs ground-truth, in layering order (default; deviate only when the user pinned a layer):
- Ground first with
schema_search_members(query)/schema_lookup_type(fqn)— confirms the symbol exists in this build and pins the canonical FQN, per-parameter types, and return type. The local schema (Facepunch.AssemblySchemaover the editor's loaded DLLs) is stricter than any CDN snapshot. Ifconnected=false, skip to step 2 (local prose still works). - Narrative with
docs_search(query)→docs_get(path)for first-party Facepunch prose (lifecycle, RPC semantics, networking visibility). - Community fallback with
learn_search(query)/learn_search(topic:"…", difficulty:"Beginner")when official docs don't cover it — faceted (difficulty/topic/tags/rating). - Real-world usage with
codesearch_search(symbol)→codesearch_get_file(hit.source_url)— searches the source of every open-source package on sbox.game for actual call sites (not signatures/prose). Plain REST (public.facepunch.com/sbox/code/search/1/) — no driver/install. Queries leave the machine — skip for private-source identifiers. Seereferences/mcp-tools.md§ Code search.
Shortcut: a one-line concept ("RPC visibility rules") → start at 2; verifying a call you're about to write → run 1 alone (schema is ground truth); "how does anyone actually use this?" → 4.
Live editor drive (editor running + bridge connected): start with editor_state (one-call mode/scene/project/capability snapshot) and gate mode-dependent tools on its result; doctor is the session-opener readiness roll-up. Everything user-facing is reachable. The principle: anything the user can do in the editor, you can do — if you can't find a tool, try list_menus/list_shortcuts (almost everything is registered there), then run_console_command as the universal escape hatch. Reach for references/tool-families.md to answer "is there a tool for X?" across ~730 tools, and references/mcp-tools.md for per-subsystem detail.
If sbox_status reports connected=false, the editor isn't running or the bridge can't reach host.docker.internal:6790. Fall back to the schema/docs pipelines (which still work without the editor) and the references below.
How to respond to common asks
- "where is X?" → don't lecture. Resolve via
find_widgetorlist_docks/list_menus, then callspotlightwith a one-sentence message. The user sees the highlight; you confirm in chat with one line. For multi-step answers ("how do I find the Asset Browser AND open a vmat from there"), usespotlight's tour mode (sequence: [...]). - "do X for me" → if X has a
[Menu]entry, preferinvoke_menu. If[Shortcut], preferinvoke_shortcut. They go through the same code path the user's manual click would, so notifications/undo behave naturally. Reach for low-level scene mutations only when there's no menu/shortcut. - "how do I X?" →
docs_searchfor the prose explanation,spotlightfor the visual answer (tour mode if multi-step), then offer to do it for them. - "set my editor to X" →
list_preferencesto find the property,set_preferenceto apply. - "is there a tool for Y?" → grep
references/tool-families.mdfirst (curated one-liner index over ~730 bridge tools). Reach forlist_toolsonly if the family isn't there. - session opener → call
doctoronce. It returns a structured pass/warn/fail roll-up plus a singlenext_suggested_actionso you don't have to ping/sbox_status/compile_check_build_state/list_unsaved_scenes individually. - "do four things in a row" → use
dispatcher_batch. Each op runs through the normal dispatcher (own LogCapture window); refer to earlier results via{"$ref": "alias.path"}. Saves agent turns and roundtrips. - runtime bug you can't reproduce (no WASD/mouse injection) → have the user reproduce and freeze in the bad state, then read component fields with
get_property(ground truth beats guessing). Read a per-tick value likeVelocitytwice — if it's byte-identical, that component isn't ticking (disabled / inactive / proxy / paused);get_componentsshows theenabledflags. Seereferences/gotchas.md"Live runtime debugging".
Routing: when to read which file
| If the user is asking about… | Open this reference file |
|---|---|
| Translating a Unity pattern to s&box | references/unity-translation.md |
| The Ten Rules of s&box (lifecycle, networking, async) | references/ten-rules.md |
| Common gotchas (namespace surprises, signature traps, set_property coercion — bool/Vector3/float/asset-handles now work, runtime-only props no-op in edit mode, bone GET=world vs SET=model space, Components.Get skips disabled, live runtime debugging, screenshot location, codesearch/news REST + privacy (queries leave the machine) and the forum-only Chromium driver lifecycle, auto_* naming, widget_drag rejections, game-code reflection whitelist vs full-trust editor addons, [Event] handler arg-count signature rule) | references/gotchas.md |
| Bodygroups: hiding/showing body parts on models (e.g. citizen) | references/bodygroups.md |
Porting a self-contained / foreign viewmodel (a Rust/CS gun+hands mesh on its own skeleton) into an FP viewmodel system, and adding more of them: sourcing sibling models (the rust org on sbox.game — all share one skeleton, so one bone map + one animgraph drive them all), making arm tuning + bone map per-weapon (the second-gun refactor), retargeting the Facepunch arms onto a skeleton with 0 shared bone names, killing the 1-frame arm jitter (read rendered bone objects, not the animation pose), hiding baked hands by an invisible material override, authoring/name-swapping the weapon animgraph (no baked graph; auto-reset params, full-auto fire re-trigger), viewmodel lighting/darkness (tagged-light trap, metal complex vs skin), placement of a model authored for another camera (pivot-target math), muzzle/eject attachments-or-child effects, and the give-a-weapon test/repro + screenshot loop |
references/self-contained-viewmodel-arms.md |
| Making store/marketing art for a package — the sbox.game thumbnails (Square 512², Wide 910×512, Tall 512×910) or a Discord WAYWO showcase (1200×800, 3:2) | references/publish-art.md — hand-write one HTML per canvas (uses /claude-design), screenshot via the browser MCP, crop with PIL. Same pipeline for achievement-icon sets. |
| Find &/or "watch" a tutorial video (a YouTube link, or "find a video on X") | references/watch-video.md. youtube_search(query) to discover (keyless), then youtube_watch(input) (MCP): transcribe + frame-per-caption → a viewing package in the game folder. First time → youtube_install. Then Read watch.md + frames/ from the game folder (use the returned output_dir_game_relative). |
Inspecting/editing an animation graph (.vanmgrph): find what drives an animation, disable a state-machine transition, change a node's sequence, add/connect nodes |
Don't read a file: animgraph_source_inspect(path) to map nodes/connections/parameters/state-machines, then the animgraph_edit_* session tools (_load → mutate → _verify → _save) + animgraph_set_node_property / animgraph_set_transition_disabled / animgraph_connect / animgraph_add_node. Operates on the KV3 source — not nodegraph_* (that's ActionGraph/ShaderGraph only). See references/tool-families.md § Animation. |
The .vanmgrph KV3 file format itself: what a node/condition/parameter class means, its fields, ID/connection conventions, how to read/interpret animgraph_source_inspect output, or hand-edit the source when the editor is offline |
references/animgraph-format.md — full spec: root CAnimationGraph layout, every C*AnimNode class + fields, state machines & CAnimState/CAnimStateTransition, the condition classes (CParameterAnimCondition, CTagCondition, CFinishedCondition, …), parameter types, tags/movement/settings, and driving params from C# via Model.Set(...)/CitizenAnimationHelper. |
| Live MCP tools you can call (curated, with usage stories) | references/mcp-tools.md |
| "Is there a tool for X?": discovery index across all ~730 bridge tools | references/tool-families.md |
| Which s&box doc area + which tool families cover a domain (Scene/Code/Editor/Assets/Graphics/UI/Gameplay/Networking/Services), and the non-negotiable engineering rules / quality gates | references/nine-categories.md |
| Driving an open file dialog or spawning a modal file picker | Call pick_file (modal blocking) or file_dialog_* (drive an already-open dialog) |
| Driving a tree widget (asset browser tree, scene hierarchy, etc.) | Call tree_list_items to discover paths, then tree_select_item / _expand_node / _activate_item |
| Driving a tab page widget | Call tab_list_pages then tab_select |
Filling an inspector input that doesn't have a [Property] (custom widget, popup dialog) |
Call set_input_text / set_color / set_checkbox / set_slider_value / select_dropdown_option; set_widget_value for universal "I don't know the type"; inspect_widget to discover surface first |
| The exact signature of a method, property, field | Don't read a file: call schema_signature |
| Researching a symbol or API you're about to call (full pipeline) | Layer the queries: schema_search_members / schema_signature → docs_search → learn_search as a fallback → codesearch_search for real call sites. See top-of-file recipe. |
| How to use a system (RPC, Razor, Editor Tools, Hammer) | Don't read a file: call docs_search then docs_get; fall back to learn_search for community walkthroughs |
| "Show me how someone built X" / a beginner walkthrough / a community tutorial | Don't read a file: call learn_search(query) or learn_search(difficulty: "Beginner", topic: "UI"). Faceted; tutorials carry difficulty / topic / tags / rating. Pair with learn_get(path) for the body. |
| Find a tutorial video by topic, or "watch" a YouTube link | Don't read a file (until details needed): youtube_search(query: "<topic>") (keyless discovery → ranked videos), then youtube_watch(input: "<url>") — transcribes (yapsnap) + a frame per caption → a viewing package in the game folder; first use → youtube_install, diagnose with youtube_status. Then Read watch.md + frames/*.jpg from the game folder (the result's output_dir_game_relative). Full detail: references/watch-video.md. |
| "How does anyone actually use this API?" / find real call sites across published code | Don't read a file: call codesearch_search(symbol) (every open-source package's source), then codesearch_get_file(hit.source_url) for full context. Plain REST — no install. Queries leave the machine. See references/mcp-tools.md § Code search. |
| Browse / read a specific open-source package's source tree | Don't read a file: codesearch_list_files(org, package) to enumerate, then codesearch_get_file(org, package, file) for any file. |
| "What's the community saying about X" / find a forum thread / read a bug report or announcement | Don't read a file: forum_search(query) for the site's own index (older/specific threads), or forum_list_categories → forum_browse_category(slug) for recent threads, then forum_read_thread(url) for the posts. Forum is the only Chromium-driver family — first use → codesearch_install_driver (forum_driver_outdated → force:true + restart). Queries leave the machine. See references/mcp-tools.md § Community forum. |
| "What changed in the last update" / when did an API land or change / is this a known issue | Don't read a file: call release_notes(limit?) for recent update posts (each with titled sections), or release_notes(version: "26.05") to filter. Plain REST (/news/platform) — no driver. See references/mcp-tools.md § Release notes. |
| Disambiguating a short type name to a fully-qualified one | Don't read a file: call schema_search_members (local) |
| Discovery: "which types implement X", "every method tagged Y" | Don't read a file: call reflection_find_types_with_attribute / reflection_find_methods_with_attribute / reflection_get_type_hierarchy |
| Synchronizing with editor lifecycle (compile finished? scene saved? hotload happened?) | Don't read a file: call wait_for_<event> (or composites: wait_for_scene_state, wait_for_asset_ready, wait_for_editor_ready) |
| "Is the editor ready? What should I do first?": single-call readiness probe | Don't read a file: call doctor. It rolls up listener / dispatcher / active scene / compile state / playing tabs / unsaved scenes / recent engine errors and returns next_suggested_action. |
| Chaining multiple tool calls in one turn (create → reparent → add component → set property, etc.) | Don't read a file: call dispatcher_batch with operations: [{ key, tool, args }] and {"$ref": "alias.path"} substitutions. Up to 50 ops, sequential. |
Placing a .vmdl upright (the model's "up" axis is wrong) |
Set the orientation once with orientation_override_set; subsequent drop_asset_into_scene calls auto-apply it. orientation_override_get/_list to inspect. |
"What does <expression> evaluate to in the editor right now?": live C# REPL |
Don't read a file: call execute_csharp(code). System / System.Linq / Sandbox / Editor are auto-imported; pass imports for extras. Useful for "what's the active scene's GameObject count", "does this LINQ query return what I expect", "what's the current value of EditorPreferences.<X>". |
| "Find me a model/material/sound from the s&box library": cloud asset workflow | asset_search(query) → pick an ident → asset_fetch(ident) for full metadata → asset_mount(ident) to install. The default pin_to_project:true appends the ident to .sbproj PackageReferences so it auto-mounts on next session; pass false for one-session-only. asset_unpin(ident) removes the pin without unmounting the running session. |
| Shipping game code that uses cloud assets — mount, don't vendor | Prefer Package.FetchAsync(ident,false) → await package.MountAsync() → Model.Load( package.GetMeta("PrimaryAsset","") ). The alternative, Cloud.Model("facepunch.v_mp5"), resolves at compile time and ships a COPY of every asset inside your package — it bloats the download and an upstream fix stays broken until you republish. Mounting keeps the asset the author's, fetched on demand and engine-cached. ⚠️ A model path only resolves AFTER its package is mounted — before that Model.Load returns an error model, which looks exactly like a wrong path and isn't. Mount from Component.OnLoad (the scene loader awaits it, so every consumer downstream is already past it — no "ready" flag to gate on), and mount in parallel with GameTask.WhenAll (dozens of small downloads one-by-one turns seconds into a minute of loading screen). Never guess a path — GetMeta("PrimaryAsset") is truth (facepunch.vmagnum → v_magnum.vmdl, not vmagnum.vmdl). See references/gotchas.md § Cloud assets. |
Anti-hallucination rule
Before you write a method call, attribute, or component lifecycle method that you haven't seen verified in this session, call schema_signature or schema_lookup_type to confirm it exists. Hallucinated APIs is the #1 failure mode in s&box code generation; the live schema fixes it for free.
For attribute-driven discovery (e.g. "which types are [GameResource]s?", "every method tagged [Menu]", "what does [Range] actually store?"), reach for reflection_find_types_with_attribute / reflection_find_methods_with_attribute / reflection_parse_attribute_metadata: these are richer than schema_* because they walk relationships, not just signatures.
If sbox_status reports connected=false and you can't call schema_* / reflection_*, fall back to references/unity-translation.md and references/ten-rules.md for the highest-frequency anti-patterns.
Editing live state
When the user asks you to change a value in the inspector ("set the player speed to 250", "disable the second component"), prefer the MCP tools over editing the .scene JSON by hand:
get_selectionto see what's selected.get_components(id)to enumerate.set_property(id, component_index, name, value): runs through the editor's undo scope, so the user sees a normal undoable change. Handles every common type —bool(true/false), numbers,Vector3/Color({x,y,z}/{r,g,b,a}), enum-by-name, and asset handles (pass a content-path string for aModel/Material, e.g."models/dev/box.vmdl"). Always verify: the response includespreviousandcurrent. If they're equal, either the write missed or the property is runtime-only (e.g.Rigidbody.Velocityno-ops in edit mode) — confirm withget_propertyand, for runtime props, test in Play.
When the user asks you to write code, prefer Read/Edit/Write against the bind-mounted source tree (your cwd is the s&box project root).
Applying C# edits made from outside the editor (the WSL/9p gotcha) — use recompile_project. The editor picks up code edits via a FileSystemWatcher (Compiler.Watch.cs → OnFileChanged → MarkForRecompile), but on a WSL/9p-mounted tree the OS never delivers change events, so the editor keeps compiling a stale in-memory snapshot of your files until a full restart — your edit silently never takes, and compile_get_diagnostics reports errors at impossible line numbers (it's reading the old cached compiler state). Two tools, know the difference:
recompile_project(added to this addon for exactly this) — the one to use after editing.cs/.razorfrom outside the editor. It marks the project's real compilers dirty (bypassing the change-detection that never fired), rebuilds them reading source fresh from disk (Compiler.SyntaxTree.cs: CollectFromFilesystem, content-hash change detection), and hotloads into the running editor/game — no restart needed. Returnsok/error_count/errors. The final package-reload flush restarts the MCP bridge one tick after the response is sent, so the connection may drop right after — that's expected; the operation completed, justpingand continue. If the editor is in PLAY mode, it stops play FIRST (returns to edit mode) before the package reload — hotloading into a live play scene makes the engine migrate runtime component instances across assembly contexts (oldIsolatedAssemblyContext→ new), and component-typed[Property]refs (e.g. aSoundEmitter) can't cast across contexts, so the scene throwsInvalidCastException/TargetExceptionon deserialize and components "don't load even though they're there". Stopping play first makes the reload re-deserialize the edit scene from disk in a single context (the robust path), and also stops the play session pinning the outgoing context (repeated mid-play hotloads otherwise leak a growing pile of staleIsolatedAssemblyContexts — only a full process restart clears them, via therestart_editortool documented just below). The response carriesstopped_play:truewhen it did so; press Play afterwards for a clean single-context session. Passkeep_playing:trueto opt out and hotload into the live session anyway (the old, error-prone behaviour).compile_project— a throwaway publish-style validate compile (freshCompileGroup, reads disk). Good for a yes/no "does it compile" gate, but it does not hotload, so it won't apply your edit to the running session.If the bridge tools aren't loaded yet (e.g. you just edited the addon itself), drive the real compile inline with
execute_csharpvia reflection: getSandbox.Project.Current, call the publicCompiler.MarkForRecompile()on its internalCompiler+EditorCompiler,awaitthe internal staticProject.CompileAsync(), thenawait EditorUtility.Projects.WaitForCompiles(). A greenCompiler.BuildSuccessis ground truth (the in-editor compiler's.Diagnosticsarray is the Roslyn diagnostics for the fresh build). If you can't hotload at all, C# also loads on a Play restart (the play tab does a fresh compile).
Hotload assembly-context accumulation — the clean-slate fix is restart_editor. Every hotload (recompile_project, or a normal in-editor save) spawns a new collectible IsolatedAssemblyContext for the package. The engine does unload the old ones (they're removed from LoadContext.Children + PackageLoader.Loaded and Unload()'d), but they stay resident because they're rooted OUTSIDE every managed collection — native GCHandles / interop pins that a managed addon can't null and a forced GC can't collect (verified: scanning 86k+ static fields + 16k static delegates finds zero managed roots). So they pile up across a long session (10s of versions), and eventually Play breaks: TypeLibrary could not find GunGame.Cinder.MainMenu (and friends) on serialize, struct-array hotload-upgrade errors (Source/Destination array is too small), and cross-context cast failures. A hotload can't fix this and neither can GC — only a process restart clears it.
restart_editor(added to this addon for exactly this) — relaunches the editor process on the same project (Editor.EditorUtility.RestartEditor(): closes the window, starts a freshsbox-dev.exe -project <current>). Defers the relaunch one tick so the response flushes. Slower than a hotload and it resets play state, so use it when accumulation bites, not per-edit.- ⚠️ SAVE HAZARD — this can silently destroy scenes. Pass
save:falsewhen accumulation has already bitten.restart_editorsaves every open edit-session scene first by default, and once contexts have accumulated the very components that are failing to resolve (TypeLibrary could not find GunGame.Cinder.MainMenu) are sitting in those scenes asMissingComponents — and s&box DROPS MissingComponents on save. So the pre-restart save serializes each open scene minus its unresolved components, permanently overwriting the good on-disk file. (Observed: arestart_editorwith the main-menu scene open strippedGunGame.Cinder.MainMenu+ theSoundEmitter/MainMenuStemMixer/TemporaryEffectobjects — the menu "completely disappeared"; the file halved in size.) Therefore: the moment you see context-accumulation symptoms (missing-type / struct-array / cross-context errors), any open scene is already degraded — restart withsave:false, never the default. Belt-and-suspenders for a long editing session: back up theAssets/scenes/+Assets/prefabs/source.scene/.prefabfiles up front (they're small), and after any restart diff each scene's size/component-list against the backup — a scene that shrank was stripped; restore the source file (and its.scene_c) from the backup, thenrecompile/refresh. Detecting stripped scenes:python3 -c "import json; d=json.load(open('Assets/scenes/x.scene')); ..."and walk__types, or just compare byte size to a known-good copy.- 🚨
restart_editorMAY NOT RELAUNCH — it can end your session. Confirmed twice, identically (2026-07-16/17, WSL/Docker, agent in a Linux container). The tool returnsok/restarting:true, the editor closes, and nothing comes back: port dead, and zero file activity undergame/for minutes (so the process is gone, not busy compiling — that's the check that tells the two apart). A human had to relaunchgame/sbox-dev.exe -project <project>by hand both times. This is a catch-22, because accumulation (below) has no other fix: hotload enough →TypeLibrarywedges → the only cure strands you. Before calling it: tell the user you may need them to relaunch, and make sure the work is saved/committed. If a human isn't watching, prefer batching your edits over more recompiles — unless a supervisor is running (next bullet), which removes the catch-22 entirely.- ✅ THE FIX: an out-of-process supervisor. A dead editor is recoverable without a human.
game/addons/claude-sbox-setup/supervisor/is a Windows-side PowerShell process that outlives the editor, so it can always start one. A human runsstart-supervisor.batonce (shortcut inshell:startupmakes it permanent); after that, drive it with plain curl from Bash — which always works, even when every MCP tool is gone:Proven 2026-07-17: editor killed → relaunched → booted → MCP bound → tools reconnected, no human. Notes that cost real time to learn: it is deliberately not an MCP server (an MCP-only launcher is unreachable in exactly the situation you need it — tools register at session start and don't come back mid-session);curl -s http://host.docker.internal:6791/status curl -sX POST http://host.docker.internal:6791/launch -d '{"project":"projects/<name>"}' curl -sX POST http://host.docker.internal:6791/restart -d '{"project":"projects/<name>"}'/statusreportseditor.runningandmcp.aliveSEPARATELY andmcp.alivelags the process by ~70 seconds on boot, so pollmcp.alive, never the pid; and a POST with no body never arrives (http.sysanswers411 Length Requiredfirst) — always send-d '{}'.- ⚠️ The agent CAN execute Windows processes — through the editor. The claim that it can't (no WSL interop, no
/mnt/c) is only true of Bash. The editor is itself a Windows process, soexecute_csharpcanSystem.Diagnostics.Process.Start(...)— that's how you runpowershell.exe, parse-check a.ps1([System.Management.Automation.Language.Parser]::ParseFile), or launch anything else on the host. It turns "I can't verify this Windows-side thing" into a real test loop. Two catches: it only helps while the editor is alive (hence the supervisor for when it isn't), and never redirect stdout/stderr without draining the pipe — an undrained pipe blocks the editor's main thread and every subsequent callTimeoutExceptions for a minute. UseUseShellExecute = true, or read the streams.- Detect accumulation early: count the loaded contexts with
execute_csharp—AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "package.local.<game>").Count(). One is healthy; a steadily-climbing number (5+, 10+) means the accumulation is building and arestart_editor(withsave:falseif scenes are open) is due before the errors — and before the save-hazard — start.- Reconnect + main-thread timeouts (both
restart_editorandrecompile_project). After either, the MCP bridge drops and the editor is busy (arestart_editorre-globs + recompiles the whole project on boot — 30–60s+ for a large project, e.g. a de-monolithed tree of hundreds of files). Poll a cheap read (editor_state,ping, ordoctor) until it answers — expect severalUnable to connectin a row, and sometimes a connect-then-immediately-drop flicker on the first success (retry once more). While the editor is mid-compile, main-thread-bound calls (recompile_project,auto_Editor_EditorScene_TogglePlay, scene mutations) return aTimeoutException: the editor main thread didn't pick this call up within 30s— the note says it stays QUEUED and runs when the editor frees up. Do not spam-retry these — each retry queues another run, and queuedTogglePlays can double-toggle play state (end up back in edit, or stuck). Wait on a cheap read going green first, then issue the heavy call once. The socket can also drop on a normalrecompile_project(the bridge restarts one tick after the response) — that single retry is expected.
Editing assets the editor must recompile (.shader, .vmat, .vmdl, …): the editor's file-watch frequently does not see edits written from outside the editor (e.g. an agent writing into a WSL/9p-mounted tree), so it keeps the stale compiled *_c and your change silently never takes. Deleting the *_c to force a rebuild makes it WORSE — the engine's on-demand recompile path (Asset.Compile) errors (Invalid Dependency Information) and you get the pink/checkerboard error material. The fix differs by asset type:
.shader:Asset.Compileis broken for shaders on this setup, so the bridge routes shader recompiles straight through the engine compiler,Sandbox.Engine.Shaders.ShaderCompile.Compile(absPath, relPath, {ForceRecompile=true}, token). Callshader_compile_and_check(path)(orasset_recompile(path)— same path for.shader): it reads the source fresh from disk (so it sees your external/WSL edit), writes a fresh.shader_c, and returnssuccess+ per-program (VFX_PROGRAM_VS/_PS) results plus the engine's compile error lines (e.g.*** Error! "g_tGround" can only use Default1!— the error text only reaches the engine log, not the compile Results, so the bridge captures it for you). A clean compile + valid.shader_cis what you want.- other assets (
.vmat/.vmdl/…):asset_recompile(path [, full=true])runs the normalAsset.Compilepipeline and writes a fresh*_c.Then Play-restart to pick it up in the running game (a newly written
.shader_cdoes not hot-load mid-session — see the runtime-shader quirks below). Custom runtimeMaterial.Create("name","shaders/x.shader")shaders are especially bitten by the file-watch staleness — always recompile the.shaderafter editing it.
Fallback (the bridge tools aren't reloaded yet, e.g. you just edited the addon): run the engine compiler inline with
execute_csharpvia reflection (the staticShaderCompile.Compileisinternal, so a direct call won't bind — useMethodInfo.Invoke):var sct = typeof(Sandbox.Engine.Shaders.ShaderCompile); var mi = sct.GetMethods((System.Reflection.BindingFlags)0x3E).First(m => m.Name=="Compile" && m.GetParameters().Length==4); var opt = Activator.CreateInstance(sct.Assembly.GetType("Sandbox.Engine.Shaders.ShaderCompileOptions")); opt.GetType().GetProperty("ForceRecompile").SetValue(opt, true); var asset = AssetSystem.FindByPath("shaders/x.shader"); var t = (System.Threading.Tasks.Task)mi.Invoke(null, new object[]{ asset.AbsolutePath, asset.Path, opt, System.Threading.CancellationToken.None }); t.GetAwaiter().GetResult(); // writes shaders/x.shader_cDo not use
AssetSystem.FindByPath(...).Compile(true)for a.shader— that's the broken on-demand path (Invalid Dependency Information, writes no_c). It still works for non-shader assets. Verify either way by checking the*_cfile's mtime/size updated.
Runtime custom-shader quirks (Material.Create("name","shaders/x.shader"))
Hard-won, all true together — design around them:
- Render-attribute binding is partial. Setting per-object params via
renderer.SceneObject.Attributes.Set(name, value)(read in the shader as< Attribute("name"); >) works for textures (Texture2D) andfloat2, but silently fails forfloatandfloat4/colour params — those keep their shaderDefault. (You can tell because heightmap + UV-mapping bind fine while a colour/scale won't.) Fix: don't push scalars/colours as attributes — bake them as shaderDefaults and make a separate.shadervariant per look (e.g.snow_deformwhite-default vssand_deformsand-default). Defaults always render. - Recompiling an already-loaded shader doesn't hot-swap it.
Asset.Compile(true)rewrites the*_con disk, but the editor keeps the previously-loaded shader program in memory; even a Play restart (and sometimes a full editor restart) won't pick up the change for a shader that was already loaded this session. A brand-new.shaderfilename has nothing cached, so it loads fresh on first use — prefer creating a new variant file over editing-in-place when you need the change to actually take. Material::Init()+ manualShadingModelStandard::Shade(i, m)is the reliable opaque-lit path (setm.Albedo/Normal/Roughness/Metalness/AmbientOcclusion/Opacity, optionallym.Emission). Read world position in the pixel stage via a custom interpolant (o.vWp = o.vPositionWs.xyin VS) —i.vPositionWsis a VS-only field;i.vTextureCoords.xyis the valid PS UV.g_flTimeis available for animation.- For an emissive look on an existing material, prefer using the material's own authored emissive map (most stock ground/lava
.vmats ship one) + a low-threshold camera Bloom post-process, rather than a custom shader — far less fragile than runtime shader plumbing.
Believe the user about what's on screen
When the user tells you what they observe in the running game — "there is no HUD",
"the font isn't showing", "the digits are cut off", "it works now" — treat it as ground
truth and act on it directly. Do NOT take screenshots to verify or contradict them. The
bridge generally cannot capture the in-game UI reliably (screenshot_scene_to_file
excludes screen-space panels; widget capture of the 3D viewport returns blank), so a
screenshot is more likely to mislead than confirm — and re-checking what the user just told
you wastes their time and erodes trust. Reason about the cause from their description, make
the fix, and ask them to confirm. Use screenshots only when the user hasn't said what they
see and you have no other signal — never to second-guess an explicit statement.
What you should NOT do
- Don't suggest
MonoBehaviour,Awake,Start,Update,[SerializeField]. Seereferences/unity-translation.mdfor the s&box equivalents. - Don't use
System.IO.File,System.Console,System.Net.Http.HttpClient, raw sockets: they're sandbox-blocked. UseFileSystem.Data,Log,Http. - Don't write
Physics.Raycast(...). UseScene.Trace.Ray(...)builder. (Rule 6.) - Don't write coroutines (
IEnumerator,yield return). Useasync Task+await Task.DelaySeconds(n). (Rule 8.) - Don't
usinga namespace you haven't verified exists. The sandbox blocks unknown namespaces at compile time.
What you absolutely should do
- Mark gameplay classes
sealed. (Rule 1.) - Use
protected override void On*()for all lifecycle. (Rule 2.) - Tag inspector fields
[Property]. (Rule 3.) - Tag networked state
[Sync], guard withif ( IsProxy ) return;. (Rules 4 to 5.) - Run a
schema_search_memberswhenever you're tempted to guess. The schema is ground truth.