Use shared static gameplay data
import { Tabs, TabItem } from ‘@astrojs/starlight/components’;
Static data is for versioned, operator-owned constants that both a game server and its clients need: collision volumes, hitbox offsets, tuning values, and rules. It is not a player-data store and it is not a general filesystem API.
-
Put the selected game-runtime entrypoint and shared data in separate directories. The example layout is deliberately simple; choose one entrypoint extension:
main.lua,main.py, ormain.js.game/
main.
2. Configure the two roots independently. The data root must already exist;Citadel never creates or writes it. Choose a per-file limit suitable forsmall gameplay definitions, not for arbitrary content blobs.
<Tabs syncKey="runtime-lang"> <TabItem label="Lua">
```toml[runtime]language = "lua"scripts_dir = "./game"static_data_dir = "./common"static_data_max_file_bytes = 65536hot_reload = truehot_reload_poll_ms = 250 </TabItem> <TabItem label="Python">[runtime]language = "python"scripts_dir = "./game"static_data_dir = "./common"static_data_max_file_bytes = 65536hot_reload = truehot_reload_poll_ms = 250 </TabItem> <TabItem label="JavaScript">[runtime]language = "js"scripts_dir = "./game"static_data_dir = "./common"static_data_max_file_bytes = 65536hot_reload = truehot_reload_poll_ms = 250 </TabItem>If another local process can edit server files, make common/ read-only with
your platform’s filesystem permissions or mount it read-only. Citadel itself
only reads this tree.
-
Load every data file while the entrypoint initializes and keep the returned values in runtime memory. Paths are always relative to
static_data_dir, use/separators, and must end in the matching extension.local collision = citadel.static_data.load_json("gameplay/collision.json")local attacks = citadel.static_data.load_csv("gameplay/attacks.csv")local knight = collision.characters.knight.hitboxlocal balloon = collision.characters.balloon.hitboxcitadel.on_message(80, function(ctx, body)-- Calculate distance from authoritative actor state. Clients may render-- the same data, but they never decide whether this hit is accepted.local allowed_cm = knight.radius_cm + balloon.radius_cmif #body <= allowed_cm thencitadel.broadcast(81, "authoritative_hit", false)endend)import citadelcollision = citadel.static_data.load_json("gameplay/collision.json")attacks = citadel.static_data.load_csv("gameplay/attacks.csv")knight = collision["characters"]["knight"]["hitbox"]balloon = collision["characters"]["balloon"]["hitbox"]@citadel.on_message(80)def hit(ctx, body):allowed_cm = knight["radius_cm"] + balloon["radius_cm"]if len(body) <= allowed_cm:citadel.broadcast(81, b"authoritative_hit")const collision = citadel.static_data.load_json("gameplay/collision.json");const attacks = citadel.static_data.load_csv("gameplay/attacks.csv");const knight = collision.characters.knight.hitbox;const balloon = collision.characters.balloon.hitbox;citadel.on_message(80, (ctx, body) => {const allowedCm = knight.radius_cm + balloon.radius_cm;if (body.length <= allowedCm) {citadel.broadcast(81, "authoritative_hit");}});JSON must have an object or array at its root. CSV must be UTF-8 with a non-empty, unique header row and a consistent number of columns; returned CSV rows are tables keyed by header.
true/falseand finite numbers are converted, while other cells remain strings. -
Run
citadel check --config citadel.tomlbefore starting the node. A missing root, bad configuration, absent data file, size-limit violation, malformed JSON/CSV, or invalid CSV/JSON schema reports a clear error during script initialization. Errors name only the relative requested path, never the server’s data-root path. -
Deploy the same versioned
common/tree alongside each client for UI or presentation. Citadel does not serve these files to clients automatically; package them through your game’s normal content pipeline. The server’s in-memory copy remains authoritative for collision and balance validation. -
With hot reload enabled, changing a data file successfully loaded during initialization causes Citadel to build a replacement VM and data catalog off the dispatch path. A fully valid replacement swaps atomically. If the new data, script, or registration is invalid, the prior VM and parsed catalog keep serving. In-VM state resets on a successful reload, so put durable state in the appropriate Citadel service rather than a global.
The static-data capability only exposes citadel.static_data.load_json and
citadel.static_data.load_csv; it never hands the script a data-root path,
directory, or raw file handle. Absolute paths, Windows drive paths, backslashes,
./.., non-data extensions, and symbolic links that resolve outside the
configured root are denied. A loader call made after initialization can return
an already-cached file, but a cache miss is denied so message and tick handlers
cannot trigger filesystem I/O. This capability does not change the independent
trusted-tier permissions of a language runtime; use it instead of direct file
reads when game code needs this bounded, reload-aware catalog.
The runnable example is in examples/static-data-game in a source checkout.
See the individual Lua,
Python, and
JavaScript
runtime references for the API contracts and the
configuration reference for all
runtime options.