Runner lifecycle & the tick pump¶
A runner (lattice_runner) is one networked simulation instance. The lifecycle is the
same for every role; the role is chosen by lattice_runner_start.
The standard sequence¶
flowchart TD
C["lattice_runner_create(cfg)"] --> SC["lattice_runner_set_callbacks(cb)"]
SC --> S["lattice_runner_start(MODE)"]
S -->|SERVER / HOST / SHARED_HOST| L["lattice_runner_listen(port)"]
S -->|CLIENT| CO["lattice_runner_connect(addr, port, token)"]
L --> RT["register_type ×N — identically on every peer"]
CO --> RT
RT --> T["loop: lattice_runner_tick(dt) — recv → tick → send; callbacks fire here"]
T --> D["lattice_runner_destroy(r)"]
lattice_runner_create¶
Create a runner. Role: both.
| Param | Meaning |
|---|---|
cfg |
Optional lattice_runner_config. May be NULL for all defaults. Deep-copied; need not outlive the call. |
Returns: an owning lattice_runner*, or NULL on allocation failure. Destroy with
lattice_runner_destroy.
lattice_runner_config¶
typedef struct {
uint32_t tick_rate_hz; /* fixed simulation rate; default 60 if 0 */
uint32_t max_objects; /* soft hint; 0 => unbounded in this skeleton */
uint32_t reserved;
uint32_t worker_threads; /* async job pool size; 0 => a small default */
} lattice_runner_config;
tick_rate_hz— fixed simulation rate (default 60 Hz).max_objectsis a soft capacity hint (0 ⇒ unbounded in the reference). Leavereservedzeroed.worker_threadsnew — the number of worker threads the runner spins up for the off-the-main-thread job system and the mediated web fetch.0⇒ a small default pool. Additive: appended afterreserved, so a caller that zero-inits the struct (the documented pattern) gets the default pool with no behaviour change.
lattice_runner_config cfg = {0};
cfg.tick_rate_hz = 60;
cfg.max_objects = 4096;
cfg.worker_threads = 4; /* optional; 0 is fine */
lattice_runner* r = lattice_runner_create(&cfg);
if (!r) { /* OOM */ }
lattice_runner_set_callbacks¶
LATTICE_API lattice_result lattice_runner_set_callbacks(lattice_runner* r,
const lattice_callbacks* cb);
Install the event callback table (lattice_callbacks).
Role: both. Set this before start/listen/connect so no event is missed. The struct
(including user_data) is copied; the function pointers must remain valid for the runner's
lifetime.
Returns: LATTICE_OK, or LATTICE_ERR_INVALID_ARG if r is NULL.
lattice_runner_start¶
Start the runner in a role. Role: both. The mode is the one knob that distinguishes a
dedicated server, a P2P host, a client, and a shared-authority host — the rest of your module
code is byte-for-byte identical across them.
LATTICE_MODE_SERVER = 0 /* dedicated server: authority, typically headless */
LATTICE_MODE_HOST = 1 /* P2P host: authority + a local participant */
LATTICE_MODE_CLIENT = 2 /* predicts/converges; holds no server-auth authority */
LATTICE_MODE_SHARED_HOST = 3 /* shared/distributed authority arbiter */
Returns: LATTICE_OK, or LATTICE_ERR_INVALID_ARG.
lattice_runner_listen¶
Open the authority's session on port. Role: authority (SERVER / HOST / SHARED_HOST).
Returns: LATTICE_OK or an error.
lattice_runner_connect¶
LATTICE_API lattice_result lattice_runner_connect(lattice_runner* r, const char* addr,
uint16_t port,
const uint8_t* token, uint32_t token_len);
Connect a client to an authority. Role: CLIENT. token/token_len is the connect token
(a director-minted session token in production); pass the bytes
your auth flow produced. The buffer is caller-owned and copied.
Returns: LATTICE_OK or an error (e.g. LATTICE_ERR_INVALID_ARG).
lattice_runner_tick¶
Pump one or more fixed steps. Role: both. The per-step order is recv → tick → send, and
all callbacks fire synchronously on the calling thread inside this call — including
on_spawned, on_state_updated, on_rpc, on_event, on_authority_changed, on_violation,
on_http_result, and on_store_result. Do no blocking work in a callback.
A headless server typically runs it in a fixed-timestep accumulator:
const double dt = 1.0 / 60.0;
while (running) {
/* sample inputs / run game logic that mutates state + marks dirty ... */
lattice_runner_tick(r, dt);
/* sleep to pace to the tick rate */
}
Returns: LATTICE_OK or an error.
lattice_runner_state¶
The runner's current connection state — one of LATTICE_CONN_DISCONNECTED,
LATTICE_CONN_CONNECTING, LATTICE_CONN_CONNECTED, LATTICE_CONN_DISCONNECTING. Role: both.
lattice_runner_destroy¶
Destroy the runner and free everything it owns. Role: both. Safe on NULL. After this the
handle is dangling; do not reuse it.
lattice_module_version new¶
The build-time version id baked into this binary, of the form
<versionnumber>_<YYYYMMDD>_<epoch_millis> — e.g. "1.2.0_20260623_1782227746626". It is
generated once per build invocation and embedded by the build script (the generated
lattice_version.c). Role: both; takes no runner. The string never enters the
wire/snapshot format, so it cannot affect determinism (conformance stays byte-identical).
Returns: a static string owned by the library — do not free it. Returns the literal
"0.0.0_00000000_0" if a build somehow did not stamp an id (the symbol is always defined, so
callers need no fallback of their own). The host/director reports this at runtime so the live
module version is dashboard-verifiable.