Appearance
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.
| Setting | GRC window | What it controls | Covered in |
|---|---|---|---|
Server options (serveroptions.json) | Server Options | Name, start level and position, maxPlayers, staff, startWeapons, custom keys. Scripts read it as serverOptions. | Staff rights & server options |
Folder config (foldersconfig.txt) | Folder Config | Search 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.
| File | Runs on | Typings | Typical use |
|---|---|---|---|
scripts/<name>.ts | server | globals.d.ts | Server-wide logic: login.ts, bookkeeping on join/leave, triggerServer('script', ...) targets |
scripts/weapons/<name>.ts | server | globals.d.ts | A weapon's serverside half: answers its clientside half's triggerServer |
scripts/weapons/<name>.client.ts | every client that holds the weapon | globals.client.d.ts + gui.client.d.ts | A weapon's clientside half: input, GUI, images, HUDs |
scripts/classes/<name>.ts / .client.ts | wherever a script join()s it | same as the joiner's side | Shared behaviour joined into scripts and NPCs. See Classes |
scripts/lib/<name>.ts / .client.ts | inlined into each importer | the importer's side | Shared helpers and types (import { x } from 'lib/name'). See Shared lib modules |
| NPC scripts | server and each client in the level | globals.d.ts + npc.server.d.ts / client globals + npc.client.d.ts | Stored 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.tsis downloaded to every player who has been grantedgun, and runs in their client.weapons/gun.tsis optional. It's loaded on the server once (not once per player). It handlestriggerServer('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:
- On join, from
login.ts. It grants every name in thestartWeaponsarray of the server options (see the snippet above). Edit the list in GRC's Server Options window. - From any server script, with player.addWeapon / player.removeWeapon. The client downloads (or loads from cache) and starts the clientside half immediately.
- From GRC: the Players window's Grant weapon… and Revoke weapon… buttons (needs the
grantweaponsright).
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:
| Project | Includes | Excludes | Sees |
|---|---|---|---|
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:
- Type-check:
tsc --noEmitagainst the project's tsconfig. Diagnostics are reported to the server log in GRC, but they never block. A script with type errors still ships. - 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:
exportis optional. Every top-levelfunctiondeclaration is exported automatically, sofunction onCreated() {}is a handler. The built-in and example scripts writeexport, and it does no harm.thisis always the script inside top-level functions. A barehelper()call from a handler would normally getthis === undefinedin strict mode. The build makesthisfall 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
- Execution model: when your code runs, batching, timers and hot reload.
- Weapons: the full weapon lifecycle.
- Events & triggers: every handler name and the trigger round trip.