Skip to content

Flags ​

Flags are named, persistent, JSON-valued variables that the engine stores and syncs for you. They're the simplest way to keep state across relogs and restarts, and to share a value between the server and clients without writing trigger code. There are four scopes:

ScopeBelongs toServerOwning clientOther clientsPersisted in
clientone playerread/write (player.client.x)read/write (client.x)–the account file
clientrone playerread/write (player.clientr.x)read-only (clientr.x)–the account file
serverrthe serverread/write (serverr.x)read-only (serverr.x)read-onlydata/flags.json
serverthe serverread/write (server.x)––data/flags.json

The r stands for read-only on the client. The server can always write every scope.

Reading and writing ​

Flags behave like properties on a plain object:

ts
// Server
server.eventActive = true
serverr.motd = 'Double XP this weekend!'
player.clientr.money = (player.clientr.money ?? 0) + 50
delete player.client.tutorialStep       // same as assigning null or undefined

// Client
client.volume = 0.8
echo(`You have ${clientr.money ?? 0} gold. ${serverr.motd ?? ''}`)
  • Values can be anything JSON-serializable: strings, numbers, booleans, null, arrays, plain objects. Assigning null or undefined (or using delete) removes the flag. Reading a missing flag gives undefined.
  • Limits: names are 1–128 characters, values up to 8192 characters of JSON. There's no cap on the number of flags. A write that breaks a limit, or isn't serializable, throws a TypeError at the assignment, on both sides.
  • Enumeration works: Object.keys(serverr), { ...client } and 'x' in clientr all behave as expected. A debug command can dump a whole scope with JSON.stringify({ ...client }).

Reads return copies: reassign to write ​

Every read returns a fresh copy of the stored JSON. Mutating a nested value changes only your copy, and nothing is stored:

ts
player.clientr.inventory.push('sword')          // ✘ modifies a temporary copy

const inv = player.clientr.inventory ?? []      // ✔ read, change, write back
inv.push('sword')
player.clientr.inventory = inv

On the client, clientr and serverr values are deep-frozen, so an accidental mutation fails loudly instead of silently. Since every server read re-parses the JSON, read a big flag into a local once rather than in a loop.

Timing and sync ​

Server writes are direct. A server write is stored immediately and readable back on the next line. Then:

  • player.client.* / player.clientr.* writes are sent to that player's client right away.
  • serverr.* writes (and deletes) are broadcast to every client right away.
  • server.* never leaves the server.

Client writes are coalesced. A client.* write on the client is validated at the assignment, readable back immediately from the local mirror, and each changed flag is sent to the server once per flush window (the end of the current handler). The server doesn't echo it back. Client flag writes count toward the per-client rate limit, so don't write a flag every frame.

At login, the client receives its own client and clientr flags and every serverr flag, so they're readable from the first onCreated. After that, clientr and serverr update live as the server writes them. Writing clientr/serverr on the client logs a warning and is ignored.

Persistence ​

  • server and serverr are saved to data/flags.json. Saves are debounced to at most one per second, written atomically, and flushed on a clean shutdown.
  • client and clientr are saved in the player's account file when they disconnect (after onPlayerLeft runs, so writes there are kept) and on a clean server shutdown.
  • Each server has its own accounts and flags. A player's flags on one server are unrelated to their flags on another.

A crash loses session-only player flags

Player flags reach disk at logout or clean shutdown. If the server process is killed, changes made during the current sessions are lost. For values that must never be lost (purchases, trades), use SQLite or a collection, which persist as you write.

Choosing a scope ​

You wantUse
A player's preference the client sets itself (volume, UI layout, tutorial progress)client
Authoritative per-player state the client must display (money, level, equipped items)clientr
A value every client should see (MOTD, event flags, day/night phase)serverr
Server bookkeeping clients must never see (counters, admin notes, secrets)server

Never trust client flags

client is writable by the player's own client, so a modified client can set any value it likes. Anything that matters for game balance or security (money, items, permissions, cooldowns) belongs in clientr (or a database), written only by server code after validating the request.

Patterns ​

Server-authoritative display. The client asks, the server decides and writes clientr, and the client's UI reads clientr every frame or on change. No reply trigger is needed:

ts
// weapons/bank.ts (server)
function onActionServerSide(player: Player, action: string, amount: number) {
    if (action !== 'withdraw') return
    const bank = player.clientr.bank ?? 0
    const n = Math.floor(Number(amount))
    if (!(n > 0 && n <= bank)) return
    player.clientr.bank = bank - n
    player.clientr.money = (player.clientr.money ?? 0) + n
}
ts
// weapons/bank.client.ts (client). `label` is a GuiTextCtrl built in onCreated.
function onUpdate() {
    label.text = `Gold: ${clientr.money ?? 0}   Bank: ${clientr.bank ?? 0}`
}

Reaching a player outside a player context. triggerClient only works synchronously inside a player-scoped handler (why), but a flag write on a Player you kept works any time. After an await, write the result to player.clientr.* and let the client pick it up.

Global announcements. A server script sets serverr.announcement, and every client's HUD shows it. Players who log in later see it too, because serverr is part of the login snapshot.

Flags vs other storage ​

Flags suit small, per-player or global values. For many records, queries or partial sync, use replicated collections (keyed records, delta-synced to exactly the right audience) or SQLite (server-only relational data).