Appearance
Debugging & tooling
Mytharyn has no step debugger for scripts. You debug with logging, the compiler's diagnostics and a fast save-and-reload loop. This page covers where output goes, how the GRC script editors compile and reload, how to connect an AI coding assistant through GRC's MCP server, and the error messages and traps you'll run into most.
echo: printing from scripts
echo is the scripting console.log. There is no console object on either side, so console.log(...) throws a ReferenceError.
| Called from | Output goes to |
|---|---|
| Server scripts, weapon server halves, classes, serverside NPCs | The server log, shown live in the log pane of GRC's main window. |
| Clientside weapons, clientside NPCs (client echo) | The game client's internal console output. It does not appear in-game or in GRC, so in practice you won't see it. Use the relay below instead. |
echo is a batched call: its argument must be JSON-serializable, and the line appears when the current handler returns. Pass strings. Stringify objects yourself:
ts
echo(`[${this.name}] ${player.account} sent ${JSON.stringify(params)}`)Tag your lines
Server output mixes every script with engine messages like [script], [weapon], [flags] and [npc]. Prefix your own lines with the script name ([${this.name}]) so you can filter them. The MCP tail_log tool's filter works better with a tag, too.
Seeing clientside output
Since clientside echo output isn't visible, send debug lines from the client to the server and echo them there, where they show up in GRC's log. Add a tiny relay to the weapon you're working on:
ts
// weapons/mything.client.ts
function debug(msg: string) {
triggerServer('weapon', this.name, 'debug', msg)
}ts
// weapons/mything.ts
export function onActionServerSide(player: Player, action: string, ...params: any[]) {
if (action === 'debug') {
echo(`[${this.name}:${player.account}] ${params[0]}`)
return
}
// ... the weapon's real actions
}Triggers count against the rate limit (400 per second per client), so don't call debug every frame. For values that change every frame, draw them on screen instead: set player.chat, or show a screen-space text image with findimg.
The GRC script editors
GRC's toolbar has four script windows, each rooted at one folder of scripts/. All of them need the scripting staff right.
| Button | Folder | Reload button rebuilds |
|---|---|---|
| Scripts | scripts/ (plain server scripts) | server scripts |
| Weapons | scripts/weapons/ (both halves side by side) | weapons |
| Classes | scripts/classes/ | classes |
| Lib | scripts/lib/ | everything (lib code is inlined into every importer) |
The editor is Monaco with the server's own typings loaded, so completions and type errors match what the server's tsc will report.
Save & reload
With Reload on save checked (the default), saving a file:
- Uploads it to the server.
- Recompiles just that entry. For a
lib/file, that means every script that imports it. - Hot-swaps every script whose compiled output changed. Clientside weapon changes are pushed to every player holding the weapon.
The bar at the top then reads either Compiled cleanly or Compile errors (see below / log), followed by swapped: … (the scripts that were replaced) or nothing changed. The Recompile & reload … button forces a full rebuild of that window's whole family. Use it when you want a clean rebuild, for example after deleting a weapon, which a single-file save doesn't pick up.
What a reload keeps and loses is covered in Execution model → Hot reload. In short, onCreated re-runs and onInitialized doesn't.
Compile diagnostics
Each compile runs tsc (type errors) and esbuild (syntax and import errors). The diagnostics are shown under the reload bar and printed to the server log.
- Type errors never block. The script still bundles and ships. A red squiggle is a warning, not a gate.
- Bundle errors block that one script. A syntax error or an unresolvable import keeps the broken entry from building, but other scripts still build and reload.
"nothing changed" after an edit
esbuild strips comments, so a comment-only or whitespace-only edit compiles to byte-identical output. The reload reports nothing changed and nothing is swapped. That's expected.
Useful RC commands
Type these in the input box of GRC's main window (try /help):
| Command | Use |
|---|---|
/scriptscan <weapons|classes|lib|scripts|all> <text> | Find text across script sources |
/who | List online players with level and position |
/warp <account> <level> [x y] | Move a test account (warpplayer right) |
/clearnpcs <level> | Remove stray putnpc NPCs (clearnpcs right) |
/openrights <account> | Open the staff rights editor. See Staff rights |
AI assistants: GRC's MCP server
GRC hosts a local MCP server so an AI coding assistant (for example Claude Code) can read, search, write and reload the server's scripts through your logged-in RC session. Open it from the toolbar's AI Assistant button. The window shows the endpoint (http://127.0.0.1:14901/mcp by default, port editable) and a ready-made registration command:
sh
claude mcp add --transport http grc http://127.0.0.1:14901/mcp --header "Authorization: Bearer <token>"Run it once. The token is stored in GRC's settings and stays valid across restarts until you press Regenerate token.
| Tool | Does |
|---|---|
get_overview | Session status and a scripting overview for this server |
list_scripts, read_script, search_scripts | Browse scripts/ |
write_script, rename_script, delete_script | Change files (and by default reload the affected family) |
reload_scripts | Targeted (by path) or full (by kind) recompile and reload, returning diagnostics |
tail_log | The latest server log lines GRC has received, including echo output. Filter by substring or /regex/ |
The tools only reach scripts/, only work while GRC is connected, and are subject to your account's staff rights. The server enforces them, so the assistant can't do anything you couldn't. Editor tabs open on a file the assistant changed reload themselves, or are marked stale if you have unsaved edits.
Treat the token like a password
Anyone on your machine with the token can act as your staff account while GRC is logged in.
Common errors and what they mean
These lines appear in the server log (and in GRC) when a script does something the engine refuses:
| Log line | Cause | Fix |
|---|---|---|
Script 'x' failed handling onFoo: … | The handler threw | Read the message. Other scripts still received the event |
[script] triggerClient called outside a player context; dropped. | triggerClient after an await or in a timer | See the current player |
[script] <acct> triggered weapon 'x' they don't have; dropped. | triggerServer('weapon', 'x') from a player without the grant | Grant it, or target a 'script' |
[script] triggerServer from <acct>: no loaded script 'x'. | No such server script, or the weapon has no weapons/x.ts half | Check the name (no extension, no weapons/ prefix) |
[script] <acct> tried to reach 'weapons/x' via type "script"; dropped. | triggerServer('script', 'weapons/x') | Use type 'weapon' |
[script] <acct> exceeded 400 triggers/sec; dropped. | A client is flooding triggers (see rate limits) | Throttle, or batch several values into one trigger |
[weapon] triggerClient: 'x' is not loaded. (game client console) | The client got onActionClientSide for a weapon it hasn't loaded | The weapon isn't granted, or its script is still downloading |
TypeError: flag x: … | A flag write broke the name/size rules or wasn't JSON-serializable | See Flags |
Traps checklist
When something "does nothing", run down this list:
- Arrow-function handlers (
export const onCreated = () => …) run, butthisisn't the script. Usefunctiondeclarations. Seethisbinding. - Mutating a flag in place (
client.inv.push(x)) stores nothing. Reads return copies, so reassign the value. See Flags. onInitializedafter a save doesn't re-run. OnlyonCreateddoes.- A clientside file outside
weapons/orclasses/is never compiled for the client. See Project layout. - Lib module state isn't shared. Each importer gets its own inlined copy of a
lib/module, so alet cachein a lib is per importing script. findweapon()on the client returnsnulluntil that weapon's script has arrived. Look it up per call instead of caching the handle.- Deleting a script file doesn't necessarily unload it. A full Weapons reload retires deleted weapons, but a deleted server script can keep running until the server next restarts. Before deleting a server script, empty its handlers and save, so it stops doing anything.