Appearance
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:
| Scope | Belongs to | Server | Owning client | Other clients | Persisted in |
|---|---|---|---|---|---|
client | one player | read/write (player.client.x) | read/write (client.x) | – | the account file |
clientr | one player | read/write (player.clientr.x) | read-only (clientr.x) | – | the account file |
serverr | the server | read/write (serverr.x) | read-only (serverr.x) | read-only | data/flags.json |
server | the server | read/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. Assigningnullorundefined(or usingdelete) removes the flag. Reading a missing flag givesundefined. - 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
TypeErrorat the assignment, on both sides. - Enumeration works:
Object.keys(serverr),{ ...client }and'x' in clientrall behave as expected. A debug command can dump a whole scope withJSON.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 = invOn 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
serverandserverrare saved todata/flags.json. Saves are debounced to at most one per second, written atomically, and flushed on a clean shutdown.clientandclientrare saved in the player's account file when they disconnect (afteronPlayerLeftruns, 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 want | Use |
|---|---|
| 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).