# The C API of the kit host (`gfkits.h`) Run the logic of a GripForge game — its Game Kits, their rules, hooks, registries and saves — from C, C++ or any language that binds a C header. Readable offline: `grip-forge docs c-api`, or the MCP tool `gripforge_docs` (topic `c-api`). ## What it is for `libgfkits` is the kit host and the logic of every Game Kit as one native library. It is the same Rust logic the web player runs: a game created from the same two documents plays the same rules. You give it a game, you step it, you send it inputs, and you read what to draw from a block of memory it shares with you. It draws nothing and plays no sound: that is your engine's job. The [Godot guide](godot.md) is this API wrapped in a node. ## Prerequisites - The GripForge repository and its Rust toolchain (`rust-toolchain.toml`); a C11 compiler. - A **game file**: one JSON object `{ "project": …, "lock": … }` — the project (`gripforge.gameproject/1`) and its lockfile (`gripforge.gamekits.lock/1`), the two documents the web player is given. - Built and tested on Linux x86-64 only (see *Known limits*). ## Install ```sh # in the folder of the native libraries of the GripForge sources ./build.sh --out dist # dist//libgfkits.so the library # dist/include/gfkits.h the header cc -std=c11 -I dist/include my_host.c -L dist/linux-x86_64 -lgfkits -lm -o my_host ``` The header is generated from the source of the library: do not edit it. ## First example Create a game, press a key, step, and read where every entity is. Shortened from `host.c`, the C test of the library that runs the demonstration game through this API: the same calls, without its checks. ```c #include "gfkits.h" #include #include #define S(text) (text), strlen(text) int run(const char *game_json, size_t len) { if (gfk_abi_version() >> 16 != GFK_ABI >> 16) return 1; /* another major: refuse */ GfkGame *game = NULL; if (gfk_game_create(game_json, len, &game) != GFK_OK) { fprintf(stderr, "create: %s\n", gfk_last_error()); return 1; } printf("%s\n", gfk_out(NULL)); /* { kits, skipped, fixedDt, lockHash } */ gfk_events_watch(game, S("player.moved")); gfk_input_key(game, S("KeyW"), 1); /* the bindings are the game's */ for (int i = 0; i < 45; i++) gfk_step(game, 1.0 / 60.0, NULL); /* wall-clock seconds, fixed ticks inside */ gfk_input_key(game, S("KeyW"), 0); uint32_t moved = 0; gfk_events_take(game, &moved); /* JSON array in gfk_out */ GfkZone zone; gfk_zone(game, &zone); for (uint32_t i = 0; i < zone.capacity; i++) { const uint8_t *row = zone.base + zone.rows + i * zone.row_bytes; uint32_t id; memcpy(&id, row, 4); if (id == 0 || id == 0xffffffffu) continue; /* empty row, destroyed entity */ double x, y, z, yaw; memcpy(&x, row + 8, 8); memcpy(&y, row + 16, 8); memcpy(&z, row + 24, 8); memcpy(&yaw, row + 32, 8); printf("e%u at %f %f %f (yaw %f)\n", id, x, y, z, yaw); } gfk_game_destroy(game); return 0; } ``` ## Rules of the interface - **Every function answers a status** (`int32_t`): `GFK_OK` is 0, anything else is a named error. `gfk_status_name(status)` gives its name, `gfk_last_error()` the message of the last failed call on this thread. - **A function that answers data writes it in the output of the calling thread**: read it with `gfk_out(&len)` — UTF-8 JSON, NUL-terminated, valid until the next `gfk_*` call on that thread. Copy it if you keep it. - **Texts go in as pointer + length** (no NUL needed). - **A game belongs to the thread that created it.** Another thread is answered `GFK_WRONG_THREAD`. - **A callback must not call the game it was called by**: it is answered `GFK_REENTRANT`. - A panic of the logic is caught: the call answers `GFK_PANICKED` and the game is disabled. ## API reference Signatures are those of the header `gfkits.h` (`dist/include/gfkits.h`). ### Library ```c uint32_t gfk_abi_version(void); /* major << 16 | minor */ const char* gfk_status_name(int32_t status); /* "ok", "bad_json", "refused"… "unknown" otherwise */ const char* gfk_last_error(void); const char* gfk_out(size_t* len); int32_t gfk_info(void); /* out: { abi, contracts, physics, kits, testKits } */ int32_t gfk_compat(const char* project, size_t len); /* out: [{ path, message }], [] when the project runs */ int32_t gfk_json_at(const char* json, size_t len, const char* pointer, size_t pointer_len); ``` `gfk_json_at` reads the value at a JSON pointer (`/fixture/ops/3`) of a JSON text, for a caller without a JSON reader; `GFK_NOT_FOUND` when nothing is there. ### A game ```c int32_t gfk_game_create(const char* json, size_t len, GfkGame** game); void gfk_game_destroy(GfkGame* game); ``` `json` is `{ "project", "lock", "seed"?: 1, "fixedDt"?: 1/60, "physics"?: "rapier" | "none", "schema"?, "registries"?: { collection: [entries] }, "saves"?: { "local": { key: text } }, "assets"?: { slot: { "url" } }, "locale"?, "now"?, "tzOffset"? }`. The kits of the lock are mounted and the game starts. Output: `{ kits: [mounted], skipped: [kits without logic in this library], fixedDt, lockHash }`. - `registries` adds documents to the registries of the game before the kits mount (an item, a status, an archetype); mods and inheritance (`extends`) then apply to them as to any entry. - `saves`: with it (even `{ "local": {} }`) the game keeps one local store for the whole play; read it with `gfk_store` and give it back at the next create. - `schema` is the game schema (`gripforge.game-schema/1`) when an export computed one; without it an entity row has the fixed base only. ### Time ```c int32_t gfk_step(GfkGame* game, double dt, uint32_t* ticks); /* dt seconds of wall clock */ int32_t gfk_step_ticks(GfkGame* game, uint32_t n); /* exactly n fixed ticks */ int32_t gfk_time(GfkGame* game, uint64_t* tick, double* elapsed); int32_t gfk_clock(GfkGame* game, double ms); /* the date, ms since the Unix epoch */ ``` `gfk_clock` is only for a game created with `now` or `tzOffset`. ### Inputs ```c int32_t gfk_input_key(GfkGame* game, const char* code, size_t len, int32_t down); int32_t gfk_input_gamepad(GfkGame* game, const double* buttons, size_t button_count, const double* axes, size_t axis_count); int32_t gfk_input_axis(GfkGame* game, const char* action, size_t len, double value); int32_t gfk_input_button(GfkGame* game, const char* action, size_t len, int32_t down); int32_t gfk_input_specs(GfkGame* game); /* out: the actions the kits declared */ ``` Key codes are the web's (`KeyW`, `Space`, `Mouse0`); pad buttons (0..1) and axes (-1..1) are in the standard order. `gfk_input_axis` / `gfk_input_button` drive an action of the game directly (an on-screen stick, your engine's own input map). ### The zone and its entity rows ```c int32_t gfk_zone(GfkGame* game, GfkZone* out); int32_t gfk_column(GfkGame* game, const char* name, size_t len, GfkColumn* out); ``` ```c typedef struct GfkZone { uint32_t struct_bytes; /* bytes of this struct as the library wrote it */ uint32_t size; /* bytes of the zone */ const uint8_t* base; /* the first byte of the zone */ uint32_t input, state, events, world, regions; /* where those buffers start */ uint32_t capacity; /* rows of the table */ uint32_t row_bytes; /* bytes of a row */ uint32_t rows; /* where the first row starts */ uint32_t used; /* rows in use */ uint32_t owner; /* the world's tag: differs after a reload */ uint64_t epoch; /* unchanged: no row changed */ } GfkZone; typedef struct GfkColumn { uint32_t offset; uint32_t bytes; uint32_t kind; } GfkColumn; ``` Row `i` is at `base + rows + i * row_bytes`. In a row: `u32` id number at 0 (`e12` → 12; 0 an empty row, `0xffffffff` a row left by a destroyed entity), `u32` flags at 4 (bit 0: the entity has a transform), `f64` x, y, z at 8, `f64` yaw at 32, then the columns of the game schema at the offsets `gfk_column` answers by name (`core.position@1`, `combat.health@1`…). Column kinds: 1 u8, 2 bool, 3 u16, 4 u32, 5 i32, 6 f32, 7 atom, 8 u64, 9 f64, 10 f64x3, 11 entity, 12 bits128. Read in place, nothing is copied. **The address is valid until the next `gfk_*` call of this game** (the zone may grow): copy what you need before calling again. Offsets never change during a play. ### Reading the game as JSON ```c int32_t gfk_entity(GfkGame* game, const char* id, size_t len); /* "e12", or "player" */ int32_t gfk_snapshot(GfkGame* game); /* the whole game: tools and tests */ int32_t gfk_state(GfkGame* game, const char* path, size_t len); /* a state var; "" for all of them */ int32_t gfk_config(GfkGame* game, const char* kit, size_t len); /* a kit's config, after the mods */ int32_t gfk_data(GfkGame* game, const char* collection, size_t len); /* a registry, as the kits use it */ int32_t gfk_logs(GfkGame* game); /* [[level, line]] */ ``` A frame reads the rows; these are for decisions, UI and tools. ### Operations and services ```c int32_t gfk_apply(GfkGame* game, const char* op, size_t len); int32_t gfk_call(GfkGame* game, const char* service, size_t service_len, const char* method, size_t method_len, const char* args, size_t args_len); ``` `gfk_apply` takes one operation of the kit host as JSON, the ones a web page sends: `{"op":"step","ticks":n}`, `{"op":"idle"}`, `{"op":"key","code":"KeyW","down":true}`, `{"op":"gamepad","buttons":[],"axes":[]}`, `{"op":"virtual","action":"move_x","axis":1}`, `{"op":"emit","topic","payload"}`, `{"op":"set","path","value"}`, `{"op":"call","service","method","args":[]}`, `{"op":"spawn","spec"}`, `{"op":"create","kind","tags","components"}`, `{"op":"move","entity","to"}`, `{"op":"start_mission","id"}`, `{"op":"save"}`, `{"op":"reload"}`, `{"op":"hud"}`, `{"op":"present","needs"?}`, `{"op":"command","command"}`. Its result is in the output (`null` when it has none). `gfk_call` calls a service of a kit; `args` is a JSON array (or empty). From `host.c`: ```c gfk_apply(game, S("{\"op\":\"create\",\"kind\":\"npc\",\"tags\":[\"enemy\"],\"components\":{\"transform\":{\"x\":4,\"y\":0,\"z\":2,\"yaw\":0}}}")); /* gfk_out: the id of the new entity, a JSON text such as "e12" */ gfk_call(game, S("inventory.items"), S("give"), S("[\"coin\",3]")); /* gfk_out: 3 */ ``` The services, their methods and arguments are in each kit's API reference (`grip-forge kit api `). ### Events ```c int32_t gfk_events_watch(GfkGame* game, const char* pattern, size_t len); /* "*", "combat.*", "inventory.item.added" */ int32_t gfk_events_take(GfkGame* game, uint32_t* count); ``` `gfk_events_take` answers the events dispatched since the last take that match a watched pattern, in order: a JSON array of `{ topic, payload, seq, tick, time, source, entity?, actor? }`. ### Hooks ```c typedef int32_t (*GfkHookFn)(void* user, const char* hook, size_t hook_len, const char* payload, size_t payload_len, GfkReply* reply); int32_t gfk_hooks(GfkGame* game); int32_t gfk_hook_tap(GfkGame* game, const char* hook, size_t hook_len, const char* opts, size_t opts_len, GfkHookFn f, void* user); int32_t gfk_hook_untap(GfkGame* game, const char* id, size_t len); int32_t gfk_reply_json(GfkReply* reply, const char* json, size_t len); int32_t gfk_hooks_trace(uint32_t keep); ``` A kit asks a *hook* each time it decides something another party may change (`combat.damage`, `inventory.give`…). `gfk_hooks` lists the hooks of the game: per hook its kit, the fields a tap may change (`mutable`) and whether a tap may cancel it (`cancellable`). `gfk_hook_tap` registers your callback; `opts` is `{ "priority"?: -1000..1000, "id"?: text, "owner"?: text }` or empty; the output is the tap id, for `gfk_hook_untap`. The callback receives the payload as JSON. It answers 0, after `gfk_reply_json(reply, …)` when it changes something: `{ "set": { field: value }, "cancel"?: true, "reason"?: text }`. From `host.c` — hits on enemies count double, and one item cannot be given: ```c static int32_t double_enemy_hits(void *user, const char *hook, size_t hook_len, const char *payload, size_t payload_len, GfkReply *reply) { char team[64] = ""; if (gfk_json_at(payload, payload_len, S("/targetTeam")) == GFK_OK) snprintf(team, sizeof team, "%s", gfk_out(NULL)); if (strcmp(team, "\"enemy\"") != 0) return 0; return gfk_reply_json(reply, S("{\"set\":{\"multiply\":2},\"reason\":\"c_double\"}")); } static int32_t no_herb(void *user, const char *hook, size_t hook_len, const char *payload, size_t payload_len, GfkReply *reply) { if (gfk_json_at(payload, payload_len, S("/item")) != GFK_OK || strcmp(gfk_out(NULL), "\"herb\"") != 0) return 0; return gfk_reply_json(reply, S("{\"cancel\":true,\"reason\":\"allergic\"}")); } gfk_hook_tap(game, S("combat.damage"), S("{\"id\":\"c_double\"}"), double_enemy_hits, NULL); gfk_hook_tap(game, S("inventory.give"), NULL, 0, no_herb, NULL); ``` The rules of a hook stay those of its declaration: a field that is not `mutable`, or a cancel on a hook that is not `cancellable`, is refused and told by the event `engine.hook.refused` (`reason`: `not_mutable`, `invalid`, `not_cancellable`) — watch it while testing. `gfk_hooks_trace(keep)` keeps the last `keep` decisions (who changed or cancelled what) and answers them; 0 stops. ### Presentation: what the kits draw On the web, what a kit shows is drawn by its **presentation module**: a `.wasm` without any engine in it (`gripforge:present@1`, described in `present.wit`: `gripforge docs abi`) that reads the snapshots of the game and answers commands — meshes, materials, nodes, lights, effects, UI. The library runs those same modules and hands you their commands; your engine applies them with its own objects. | Function | Role | |---|---| | `gfk_present_native(path, len)` | Loads the library of the native presentation modules (`libgfpresent`, built with the addon: the modules of the kits compiled for the machine from the sources of their `.wasm`). Once per process, before the games register their modules; the environment variable `GFK_PRESENT_NATIVE` names it too. `GFK_REFUSED` (reason in `gfk_last_error`) when it is not loaded. | | `gfk_present_kit(game, kit, len)` | Registers the NATIVE module of a kit — no `.wasm`, no interpreter. In the output: `{ kit, version, needs, runner: "native" }`. `GFK_REFUSED` when no library is loaded or it has no module of that kit: register its `.wasm` then (`gfk_present_module`, run by the embedded interpreter — the case of a third-party module). | | `gfk_present_module(game, wasm, len)` | Registers a module of the game from the bytes of its `.wasm` (as `present-modules.json` of a web export names them). In the output: `{ kit, version, needs }`. | | `gfk_present_start(game, options, len)` | Starts. `options`: `{ "engine", "backend", "caps", "assets": { slot: { "clips": [] } }, "modelSlots": [], "textureSlots": [], "generic": {}, "settings": {}, "shared": { name: text }, "needs": { "components", "views", "events", "commands" } }`. In the output: `{ modules, needs }`. | | `gfk_present_frame(game, dt, frame, len, &commands)` | One drawn frame, after the steps: the snapshot is made (transforms interpolated between the two last ticks, the components and views the modules declared, the events since the previous frame) and every module is called. `frame`: `{ "camera": { "position", "rotation", "fovY", "viewport", "viewProj", "mode" }, "stage": { "exposure", "composed" } }`. | | `gfk_present_snapshot(game)` | The snapshot of that frame, in the output: what your own layer draws (entities, their model slot and clip state, the `camera.*` vars). | | `gfk_present_cmds(game, json)` | The commands of that frame, in the output. `json` 0: `postcard(Vec<(u32 module, Vec command)>)`, the encoding of the present ABI — read its length from `gfk_out`. `json` 1: JSON text, `[[module, { "": { fields } }]]`. | | `gfk_present_result(game, module, result, len)` | An answer to a module, as JSON: `{ "Refused": { "index": 0, "reason": "MaterialWgsl" } }` for a command you do not draw, `{ "Loaded": { "id", "ok" } }`, `{ "Bound": … }`, `{ "Bounds": … }`, `{ "Slot": … }`, `{ "Model": … }`. | | `gfk_present_command(game, command, len)` | A write of your own layer on the kit host (`{ "service", "method", "args" }` or `{ "emit", "payload" }`), allowed by `needs.commands`. | | `gfk_present_info(game)` | `{ started, frames, msPerFrame, stage, modules: [{ index, kit, version, dead, calls, slept, cmds, msPerFrame, runner: "native" \| "wasm" \| "verify" }], needs, undrawn, logs, shared }`. | What a module asks of the kit host — a service call, an event, the config of a kit, a state shared between modules, a log line — is done by the library; it never reaches you. A module that traps is left out (`dead`) and the game goes on. `gfk_events_take` still answers every event: the presentation reads them too, without taking them. The modules are run by an embedded interpreter (wasmi): no compiled runner yet. ### The window, and HUD rows without a game (1.3) ```c int32_t gfk_host_requests(GfkGame* game); /* out: { kits: { display, pointer }, requests: [{ port, call }] } */ int32_t gfk_hud_rows(const char* doc, size_t len); /* doc: { vars: [declared vars], values: { path: value } } */ ``` `gfk_host_requests` takes what the kits `display.fullscreen` and `input.pointer_lock` asked of the window since the last call (`port` `display`: `request` | `exit`; `port` `pointer`: `lock` | `unlock`). Your engine executes on its window and tells the kits what really happened through their services (`gfk_call`: `attach`, `hostChanged`, `hostRefused`, `gesture`, `hostMove`). `gfk_hud_rows` answers the HUD rows of declared state vars (the `hudVars` of a snapshot) for given values, without a game: what `{"op":"hud"}` answers for a running one. ### Saves ```c int32_t gfk_save(GfkGame* game); /* out: gripforge.save/1 */ int32_t gfk_load(GfkGame* game, const char* save, size_t len); int32_t gfk_store(GfkGame* game); /* out: { key: text }, the kits' local store */ ``` A save is in the format of the web player: it moves between the two. `gfk_load` answers `GFK_REFUSED` with the reason when a save does not fit. ## Errors | Status | Name | Meaning | |---|---|---| | `GFK_OK` 0 | `ok` | Done. | | `GFK_BAD_ARGUMENT` 1 | `bad_argument` | A null pointer, a text that is not UTF-8, a value of the wrong kind. | | `GFK_BAD_JSON` 2 | `bad_json` | A text that is not JSON. | | `GFK_BAD_PROJECT` 3 | `bad_project` | No project or no lock, or a registry entry that cannot be added. | | `GFK_NO_GAME` 4 | `no_game` | A null game. | | `GFK_WRONG_THREAD` 5 | `wrong_thread` | The game belongs to the thread that created it. | | `GFK_REENTRANT` 6 | `reentrant` | The game is running: a callback called it back. | | `GFK_PANICKED` 7 | `panicked` | The logic panicked; the game is disabled. | | `GFK_REFUSED` 8 | `refused` | The kit host or a kit refused (unknown operation or service, a save that does not fit): `gfk_last_error` says why. | | `GFK_NOT_FOUND` 9 | `not_found` | No such entity, column, kit, tap or JSON path. | | `GFK_BAD_SCHEMA` 10 | `bad_schema` | The game schema does not hold: `gfk_last_error` says what. | | `GFK_PARITY_DIFFERS` 11 | `parity_differs` | A trace differs from its reference (the test functions `gfk_trace_frame`, `gfk_parity_check`). | `host.c` checks these: `gfk_game_create("{not json")` → `GFK_BAD_JSON`; `{"project":{}}` → `GFK_BAD_PROJECT`; `gfk_step(NULL, …)` → `GFK_NO_GAME`; `{"op":"nonsense"}` and an unknown service → `GFK_REFUSED`; the config of a kit that is not in the game, an unknown column, an unknown tap → `GFK_NOT_FOUND`. ## Known limits - **Logic only.** Nothing is drawn: the views the kits declare are answered as data (`{"op":"present"}`) for your engine to draw. No HUD. - **Linux x86-64** is the only platform built and tested. No prebuilt binaries are published. - **One library holds every kit**; `gfk_info` lists them. A game whose kit has no logic in the library starts without it and names it in `skipped`: check `gfk_compat` first. - **Network kits** (`net.*`) need host services this interface does not take yet. - **Asset bytes** cannot be handed to the game: `assets` binds slots to URLs only. - One game per thread; a game is single-threaded. - JSON crosses the interface for everything that is not a row: keep per-frame reads on the rows. ## Versions `GFK_ABI` is `0x00010003` (1.3). `gfk_abi_version()` answers `major << 16 | minor`: refuse another major. A function, or a struct field added at the end (`GfkZone.struct_bytes` says how many bytes the library wrote), raises the minor. Saves are `gripforge.save/1`, game files `gripforge.gameproject/1` + `gripforge.gamekits.lock/1`.