Skip to content

Script types & project layout ​

Your server's files are everything GRC's File Browser shows: its levels, images, animations, sounds and every script it runs. This page covers what goes where, which script categories exist, how a script's file name decides where it runs, and what the build does to your TypeScript before it runs.

Your server's files ​

GRC shows paths relative to the server's data folder. A new server starts out like this:

scripts/                every script, plus the typings and tsconfigs
├── tsconfig.json
├── globals.d.ts            server globals
├── npc.server.d.ts         serverside NPC `this`
├── npc.client.d.ts         clientside NPC `this`
├── login.ts                grants startWeapons on join
└── weapons/
    ├── tsconfig.json
    ├── globals.client.d.ts   client globals
    └── gui.client.d.ts       GUI controls
levels/                 start.glvl
assets/                 ganis/, images/

As you work, the server adds its own state next to these: accounts/ (per-account files, including flags and weapon grants), flags.json (the global server.* / serverr.* flags), databases/ (SQLite), and the compiled script output. Never edit the compiled output. It is regenerated from scripts/ on every build.

The .d.ts typings and the tsconfig.json files are provided by the platform and power the editors' IntelliSense. Don't edit or delete them.

Default art

A new server has no heads, bodies or other shared art of its own. Players' clients fall back to the platform's default set, so characters still look right. Upload a file with the same name to override a default.

Server configuration ​

Two more settings files sit outside the data folder, so they don't appear in the File Browser. Each has its own GRC window, and changes apply without a restart.

SettingGRC windowWhat it controlsCovered in
Server options (serveroptions.json)Server OptionsName, start level and position, maxPlayers, staff, startWeapons, custom keys. Scripts read it as serverOptions.Staff rights & server options
Folder config (foldersconfig.txt)Folder ConfigSearch folders for each asset type (gan, image, head, body, sound, file, level).Staff rights & server options

Script categories ​

Where a file lives, and whether its name ends in .client.ts, decides where it runs and which typings it's checked against.

FileRuns onTypingsTypical use
scripts/<name>.tsserverglobals.d.tsServer-wide logic: login.ts, bookkeeping on join/leave, triggerServer('script', ...) targets
scripts/weapons/<name>.tsserverglobals.d.tsA weapon's serverside half: answers its clientside half's triggerServer
scripts/weapons/<name>.client.tsevery client that holds the weaponglobals.client.d.ts + gui.client.d.tsA weapon's clientside half: input, GUI, images, HUDs
scripts/classes/<name>.ts / .client.tswherever a script join()s itsame as the joiner's sideShared behaviour joined into scripts and NPCs. See Classes
scripts/lib/<name>.ts / .client.tsinlined into each importerthe importer's sideShared helpers and types (import { x } from 'lib/name'). See Shared lib modules
NPC scriptsserver and each client in the levelglobals.d.ts + npc.server.d.ts / client globals + npc.client.d.tsStored inside the level (.glvl) or passed to putnpc. See NPCs

Plain server scripts ​

Any .ts file under scripts/ that isn't in weapons/, classes/ or lib/, and isn't a .client.ts, is a plain server script. It's loaded once at boot and receives the server-wide events: onCreated, onInitialized, onPlayerJoined, onPlayerLeft, and onActionServerSide when a client targets it with triggerServer('script', '<name>', ...). Every new server's login.ts is one:

ts
// scripts/login.ts: grants the "startWeapons" server option to every player on join.
export function onPlayerJoined(pl: Player) {
    for (const wep of serverOptions.startWeapons ?? [])
        pl.addWeapon(wep)
}

There's no client counterpart. Clients only ever run weapons (and the clientside halves of NPCs and classes).

Weapons: two halves, one name ​

A weapon is a pair of files that share a name:

  • weapons/gun.client.ts is downloaded to every player who has been granted gun, and runs in their client.
  • weapons/gun.ts is optional. It's loaded on the server once (not once per player). It handles triggerServer('weapon', 'gun', ...) from any holder, with the calling player as the first argument.

Both halves see this.name === 'gun', so triggerServer('weapon', this.name, ...) and triggerClient('weapon', this.name, ...) always reach the other half. A weapon only exists (can be granted) if it has a .client.ts half. A weapons/foo.ts with no client half loads like any server script, but player.addWeapon('foo') returns false. See Weapons.

Weapon server halves hear every player

The serverside half is an ordinary server script, so onPlayerJoined / onPlayerLeft fire on it for every player, whether or not they hold the weapon. A playerlist weapon that tracks who's online can rely on this.

How weapons reach players ​

Players get weapons only through a grant:

  1. On join, from login.ts. It grants every name in the startWeapons array of the server options (see the snippet above). Edit the list in GRC's Server Options window.
  2. From any server script, with player.addWeapon / player.removeWeapon. The client downloads (or loads from cache) and starts the clientside half immediately.
  3. From GRC: the Players window's Grant weapon… and Revoke weapon… buttons (needs the grantweapons right).

Grants are saved in the account file, so a player keeps their weapons across logins.

The .client.ts split and the two tsconfigs ​

scripts/ contains two TypeScript projects, and the .client.ts suffix decides which one a file belongs to:

ProjectIncludesExcludesSees
scripts/tsconfig.json**/*.ts**/*.client.ts, classes/, lib/globals.d.ts (server API)
scripts/weapons/tsconfig.json**/*.client.ts in weapons/–globals.client.d.ts + gui.client.d.ts (client API)

This split is why findimg is a type error in gun.ts, and triggerClient is one in gun.client.ts. The server and client APIs are different globals, and each side only sees its own. Classes have their own pair of tsconfigs under classes/, and NPC scripts are compiled in memory against npc.server.d.ts / npc.client.d.ts on top of the matching globals.

Both projects map lib/* imports. The client project tries lib/<name>.client.ts first and falls back to lib/<name>.ts, so one import path ('lib/inventory') resolves to the correct half on each side. lib/ is excluded from the server project so lib files are only type-checked through the scripts that import them.

.client.ts only means "client" under weapons/ and classes/

A scripts/foo.client.ts next to login.ts is excluded from the server project, and it's not in any client project either, so it never runs. Clientside code goes in weapons/, classes/ (as a class's client half) or lib/ (as a helper).

What the build does ​

Each save in GRC (or each boot) runs two steps for the affected scripts:

  1. Type-check: tsc --noEmit against the project's tsconfig. Diagnostics are reported to the server log in GRC, but they never block. A script with type errors still ships.
  2. Bundle: esbuild bundles each entry script into a single self-contained ES module. lib/ imports are inlined into each importer. If one entry fails to bundle (a syntax error, a bad import), the others are retried one by one so a broken script doesn't stop the rest from shipping.

The build also rewrites your source in two ways, so scripts read like Graal scripts:

  • export is optional. Every top-level function declaration is exported automatically, so function onCreated() {} is a handler. The built-in and example scripts write export, and it does no harm.
  • this is always the script inside top-level functions. A bare helper() call from a handler would normally get this === undefined in strict mode. The build makes this fall back to the running script's own object. Details are in Execution model.

Because of the auto-export, every script is compiled as a module, even one with no import/export of its own. The tsconfigs set "moduleDetection": "force" for this reason. Without it, two export-less scripts that both define onCreated would be treated as global scripts and collide. The tsconfigs also turn off noImplicitThis, so handlers can use this without an annotation. Annotate with this: ScriptThis / this: WeaponThis when you want it typed.

Why tsc and esbuild disagree sometimes

tsc checks your original source. The auto-export and this rewrite only exist in the bundle. esbuild also strips comments, so a comment-only edit produces byte-identical output, and the reload reports that nothing changed. See Debugging & tooling.

Where to go next ​