Skip to content

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 fromOutput goes to
Server scripts, weapon server halves, classes, serverside NPCsThe 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.

ButtonFolderReload button rebuilds
Scriptsscripts/ (plain server scripts)server scripts
Weaponsscripts/weapons/ (both halves side by side)weapons
Classesscripts/classes/classes
Libscripts/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:

  1. Uploads it to the server.
  2. Recompiles just that entry. For a lib/ file, that means every script that imports it.
  3. 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):

CommandUse
/scriptscan <weapons|classes|lib|scripts|all> <text>Find text across script sources
/whoList 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.

ToolDoes
get_overviewSession status and a scripting overview for this server
list_scripts, read_script, search_scriptsBrowse scripts/
write_script, rename_script, delete_scriptChange files (and by default reload the affected family)
reload_scriptsTargeted (by path) or full (by kind) recompile and reload, returning diagnostics
tail_logThe 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 lineCauseFix
Script 'x' failed handling onFoo: …The handler threwRead the message. Other scripts still received the event
[script] triggerClient called outside a player context; dropped.triggerClient after an await or in a timerSee the current player
[script] <acct> triggered weapon 'x' they don't have; dropped.triggerServer('weapon', 'x') from a player without the grantGrant 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 halfCheck 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 loadedThe 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-serializableSee Flags

Traps checklist ​

When something "does nothing", run down this list:

  • Arrow-function handlers (export const onCreated = () => …) run, but this isn't the script. Use function declarations. See this binding.
  • Mutating a flag in place (client.inv.push(x)) stores nothing. Reads return copies, so reassign the value. See Flags.
  • onInitialized after a save doesn't re-run. Only onCreated does.
  • A clientside file outside weapons/ or classes/ 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 a let cache in a lib is per importing script.
  • findweapon() on the client returns null until 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.