Integration Guide
Cenosis exposes exactly two integration surfaces: a C ABI for native engines (Unity / Unreal / any C-compatible host) and a WebAssembly surface for browsers. Both share the same underlying engine — the WASM build is an SS-safe browser branch of the same codebase.
The Two-Pane Mental Model
There are exactly two things a host does:
- Ingress — push events from the game into the engine:
npc_engine_record_event(event_json). Calling from the game thread is safe — returns in microseconds. - Egress — pull structured actions from the engine:
peek_action_size→pop_action. The host drains on its own schedule.
Everything else — ticking, LLM calls, social updates, WAL projection, budget governance, storylet evaluation, scene management, bundle save/load — is internal to the engine.
Native (C ABI) — Unity / Unreal
Building
.\build-ffi.ps1
Produces a platform-native cdylib (DLL on Windows, .so on Linux, .dylib on macOS) via cargo-zigbuild. Six targets are built in CI: x86_64-windows, aarch64-windows, x86_64-linux, aarch64-linux, x86_64-macos, aarch64-macos.
Lifecycle
npc_engine.hEngineHandle* npc_engine_init(const char* config_json);
void npc_engine_shutdown(EngineHandle* handle);
EngineHandle is opaque — a boxed Engine with no global state. Multiple engines can coexist in the same process (the test suite relies on this).
Config JSON (key fields)
| Field | Type | Description |
|---|---|---|
storage_path | string | Directory for the WAL and SQLite projection store. |
rng_seed | u64 | Determinism seed for SS social encounters and storylets. |
primary_channel_capacity | u32 | Ingress channel buffer size (default: 4096). |
budget | object? | Per-agent, session, and hourly LLM spend caps. |
cloud | object? | Cloud provider config. Omit for SS-only (no narrative). |
max_full_sim_agents | u32 | Hard cap on concurrent FS agents (default: 8). |
Ingress — record_event
int npc_engine_record_event(EngineHandle*, const char* event_json);
// Returns: 0 = enqueued on primary channel
// 1 = primary saturated, enqueued on overflow
// -1 = JSON parse / null pointer error
// -2 = engine not initialized
Call this from your game thread. It must return in microseconds — internally it does JSON parse → crossbeam-channel::try_send → return. Never block.
Tick
int npc_engine_tick(EngineHandle*, double now_seconds);
now_seconds is real wall time in seconds. The engine converts to game-time delta internally. Call once per game tick (or at your desired simulation frequency).
Egress — peek-then-pop
uint32_t npc_engine_peek_action_size(EngineHandle*);
int npc_engine_pop_action(EngineHandle*, char* buf, uint32_t buf_len);
// Returns: >0 = bytes written
// -2 = buf_len too small (message NOT dropped)
// -3 = queue empty
Always peek first. An undersized buffer returns -2 without dropping the message — this is the only way to bound your allocation cost. Pattern:
uint32_t sz = npc_engine_peek_action_size(handle);
while (sz > 0) {
char* buf = malloc(sz);
npc_engine_pop_action(handle, buf, sz);
handle_action(buf);
free(buf);
sz = npc_engine_peek_action_size(handle);
}
Proximity — tier transitions
int npc_engine_set_proximity(EngineHandle*, const char* agent_id_json, float distance, double now_seconds);
Call whenever agent-to-player distance changes. The engine handles hysteresis (enter FS at 20m, exit at 30m by default). FS promotion is capped at max_full_sim_agents.
Save / Load
// Export world state to a NPCB v17 bundle
int npc_engine_export_bundle(EngineHandle*, const char* path);
// Import a bundle into a fresh engine handle
int npc_engine_import_bundle(EngineHandle*, const char* path);
// Error codes: -5 = I/O error, -6 = corrupt bundle, -7 = unsupported version
Generated Bindings
npc-codegen generates host bindings from the frozen ABI surface. Committed artifacts:
npc-ffi/bindings/NpcEngineV40.cs— Unity C# (P/Invoke wrappers)npc-ffi/bindings/NpcEngineV40.hpp— Unreal C++ (RAII handle)npc-ffi/bindings/NpcEngineV40.ts— TypeScript (for non-WASM native Node)
Binding drift is gated by npc-tests::v40_codegen_hosts in CI — generated output must be byte-identical to committed artifacts.
WebAssembly (Browser / Three.js)
Building
.\build-wasm.ps1
Produces cenosis_wasm_bg.wasm + cenosis_wasm.js glue via wasm-pack.
Required HTTP Headers
COOP / COEP Required
The WASM build uses SharedArrayBuffer for main↔worker communication over OPFS. Your server must send these headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Web Worker Setup
The engine must run in a dedicated Web Worker — OPFS sync handles require workers. Main thread → worker communication is via SharedArrayBuffer (requires the headers above) or standard postMessage.
import { EngineWasm } from './cenosis_wasm.js';
const engine = new EngineWasm(configJson);
await engine.init_store('my_world.db'); // OPFS-backed SQLite
self.onmessage = ({ data }) => {
switch (data.type) {
case 'tick':
engine.tick(data.nowSeconds);
const actions = engine.pop_actions_json();
if (actions) self.postMessage({ type: 'actions', actions });
break;
case 'event':
engine.record_event(data.eventJson);
break;
case 'proximity':
engine.set_proximity(data.agentId, data.distance, data.nowSeconds);
break;
}
};
WASM API Surface
| Method | Description |
|---|---|
new EngineWasm(configJson) | Construct and configure the engine. |
await init_store(dbName) | Initialize OPFS-backed SQLite storage. |
record_event(eventJson) | Ingest a game event (non-blocking). |
tick(nowSeconds) | Advance simulation by one world tick. |
pop_actions_json() | Drain all pending NPC actions as a JSON array string. |
peek_metrics() | Return engine metrics JSON (budget, circuit breaker state, etc.). |
set_proximity(agentId, distance, now) | Update agent-to-player distance for tier promotion. |
export_bundle(dbName) | Export world state bundle to OPFS. |
import_bundle(dbName) | Import a bundle from OPFS. |
register_decision_hook(seam, fn) | Register a JS function to override a policy seam decision. |
unregister_decision_hook(seam) | Remove a decision hook (restores engine default). |
WASM streaming
Provider streaming (LLM token-by-token output) is deferred on wasm32. Structured conversation, subscriptions, relationship/snapshot queries, and bundle import/export have full parity with native.