Third-party developer guide
Modules, mods, UI and games made outside GripForge: SDK, needs, commands, WGSL, caps, assets, fonts, ui.webui, publishing, ABI compatibility.
Third-party developer guide #
For developers outside GripForge who build presentation modules, mods, UI or games on GripForge kits. Everything here is a public contract: what you may rely on, how to build, test, ship and install. Words in italics are in the glossary. Readable offline: grip-forge docs third-party, or the MCP tool gripforge_docs (topic third-party).
Contents: 1. What you can build · 2. Reading a kit's API · 3. Writing a mod · 4. Writing a presentation module ·
- Host capabilities (caps) · 6. Your own assets, fonts and UI · 7. Publishing and installing · 8. ABI compatibility rules · 9. Checklist.
1. What you can build #
| You want to… | Build | Runs as | Trust |
|---|---|---|---|
| change rules, numbers, content of a game | a mod (gripforge.mod/1 JSON) | data applied before the kits mount | untrusted data: only what kits declare moddable |
| react to events, intercept decisions | hooks and scripts documents (in a mod or in the project) | data, run by game.engine | same as a mod |
| draw a kit differently, or draw a new kit | a presentation module (Rust → .wasm) | sandboxed WebAssembly, no imports | can only send ABI commands |
| add a rule of game logic no kit and no mod can express | a kit logic module (Rust → .wasm, built alone): kit-third-party.md, grip-forge docs kit-third-party | its own WebAssembly beside the core, as every kit of the game (the kit modules are the Rust logic of the page) | sandboxed: it only calls the kithost.* interfaces of the core |
| add a HUD or a menu | a ui.webui fragment (React components or HTML templates) | code in the page | the trust of mod scripts (see §6.3) |
| a whole game | a project made of kits (CLI / MCP) | the kits | — |
A mod never carries code that runs in the game logic. A module never touches the game state: it reads what it declared and answers commands.
2. Reading a kit's API #
Every kit has a generated API reference: config keys (type, default, unit, limits, mod / live flags), hooks (payload, mutable fields, cancellable), events (payload), views, services, script verbs, asset slots, data collections, mod points, its presentation module (needs, ABI families, web UI), target parity, changelog and examples of a config and of a mod. Read it before configuring a kit, writing a mod or a module for it:
- CLI:
grip-forge kit api <id> [--section config,hooks] [--json] - MCP:
gripforge_gamekit_api { id, sections? }, orgripforge_gamekit_get { id, section: "api" } - HTTP:
GET /api/v1/gamekits/<id>/api(?format=md,?compact=1) - Catalogue: the API tab of the kit page.
How the reference is made and checked: kit-api.md.
3. Writing a mod #
A mod is one JSON document (gripforge.mod/1), stored in the mods collection of game.engine in a project:
{
"schema": "gripforge.mod/1",
"id": "hard_mode", "name": "Hard mode", "version": "1.0.0",
"requires": { "game.engine": "^1.2", "combat.core": "^1" },
"config": { "combat.core": { "playerHealth": 60, "friendlyFire": true } },
"data": {
"hooks": { "add": [
{ "id": "harder_hits", "hook": "combat.damage", "mul": { "amount": 1.5 } },
{ "id": "no_heal_in_boss", "hook": "combat.heal", "if": [{ "var": "boss.active", "op": "==", "value": true }], "cancel": true, "reason": "boss" }
] }
}
}
- Config:
config.<kit>.<key>merges an object key by key and replaces anything else — an array is replaced whole. To change ONE element of a config list, useconfigPatch(game.engine 1.3):"configPatch": { "survival.needs": { "update": { "needs": [{ "match": { "id": "hunger" }, "set": { "drain": 0.3 } }] } } }— operationsset(by dotted path),append,update(match+set) andremoveon the lists at a path. - What you may touch: only config keys, collections and live settings the kit lists in its mod points (
engine.moddable, section "Mod points" of its API reference). Anything else refuses the whole mod. - Content:
data.<collection>withremove: [ids],patch: [{ id, set?, remove?, update?, append? }],add: [documents], applied in that order. A registry collection accepts{ "id": "fire_imp", "extends": "imp" }. - Hooks (a tap):
{ id, hook, priority?, match?, if?, set?, add?, mul?, min?, max?, cancel?, reason? }— change the mutable fields of a decision or cancel it. The hook list and their payloads are in the kit's API reference. - Scripts: rules
{ when: <condition>, then: [<actions>] }on events; verbs are listed per kit. - What validation does not check:
appliedmeans the mod's config and documents were accepted. A tap is checked when the decision runs: a tap that writes a field the hook does not list asmutable, or a value of the wrong type, is dropped at run time (eventengine.hook.refused); a hook name no kit declares is only a warning (hook_unknown). Take hook names and mutable fields from the kit's API reference, and watchengine.hook.refusedwhile testing. A hook runs only when the kit asks that decision: its API reference says when. - Order and failure: mods apply in the game's order (
mods.order), after the mods they depend on; one error refuses the whole mod (readable codes:config_not_moddable,collection_not_moddable,kit_incompatible…). - The game decides:
game.engineconfigmods.enabled,mods.disabled,mods.deny: { kits, collections, hooks }.
Install, validate, order: MCP gripforge_game_mods { project_id, action: validate | install | enable | disable | order | remove, mod }. Full format and every rule: grip-forge docs modding (§3 is the mod format, §4 hooks, §13 game.engine 1.3).
4. Writing a presentation module #
A module is the presentation of one kit, in plain Rust (no Bevy, no engine), compiled to wasm32-unknown-unknown. The recipe with every detail is grip-forge docs porting; the contract and its engine-neutral description are grip-forge docs abi (the MCP tool gripforge_docs serves both topics).
4.1 The SDK #
use gripforge_present_sdk::{Module, Needs, Out, World, abi::*, export_module};
#[derive(Default)]
pub struct Storm { built: bool }
impl Module for Storm {
const KIT: &'static str = "zone.shrink"; // the kit it draws
const VERSION: &'static str = "1.0.0"; // the kit version it follows
fn needs() -> Needs { Needs::new().polled("zone.shrink", "zone.storm", "view", 1) }
fn init(&mut self, world: &World, init: &Init, out: &mut Out) {}
fn frame(&mut self, world: &World, out: &mut Out) { /* read world, write out */ }
}
export_module!(Storm);
4.2 Needs: declare what you read #
The host sends a module only what it declared, as deltas:
Needs:: | Gives | Example |
|---|---|---|
components(&[…]) | entities carrying one of them (transform: every entity) | &["duel.fighter"] |
events(&[…]) | events of these topics, new this frame (duel.*, *) | &["fx.effect"] |
data(&[…]) | project collections, in Init::data | &["boons"] |
polled(name, service, method, every) | a service view read every N snapshots | ("sky","world.atmosphere","sky",3) |
view(name, service, method, &on) | a view read at start and on these events | &["engine.scene.entered"] |
grid(name, service, method, half, step, &on) | a view sampled around the player | ground heights |
commands(&[…]) | what you may ask the kit host: capability.method, emit:<topic> | &["emit:hud.action.choose"] |
.0.shared / .0.settings | shared keys of other modules; settings (graphics.*, project.platform) | — |
A module of a kit already lists its needs in that kit's API reference (section "Presentation module").
4.3 Commands #
out.* helpers, one per ABI command family: mesh, material, node, scene, fx, ui, skeleton, camera / post / audio, plus kit host requests (kit_call, kit_emit), share, log, undrawn. Ids are yours, one space per kind. Conventions: right handed, Y up, metres, seconds, linear RGBA colours (UI: sRGB), quaternions [x, y, z, w].
4.4 WGSL #
Custom shading is plain WGSL functions — no #import, no binding, no storage buffer, no compute, nothing WebGPU-only. You write gf_shade(GfIn) -> vec4<f32> or gf_surface(GfIn, GfSurface) -> GfSurface (optional gf_displace), a post pass gf_post(uv, color), a surface layer gf_layer, a UI shader gf_ui(uv, size, time). The host provides gf_param, gf_tex, gf_tex_layer, gf_time (and gf_scene_depth with the SCENE_DEPTH cap), validates with naga and translates to GLSL ES 3.0 for WebGL2. Lighting, fog and tone mapping are the host's. Full dialect: the end of present.wit.
4.5 Build #
# in the modules workspace of the GripForge sources (one folder per kit)
cargo build --release --target wasm32-unknown-unknown -p gf-present-<kit-with-dashes>
node build.mjs <dir> # fingerprinted build + present-modules.json entry
The same .wasm serves WebGPU, WebGL2 and native hosts. Native libraries (.so / .dll / .dylib): the crate is cdylib + rlib, but export_module! exports the ABI symbols only on wasm32 today; native Bevy will run the .wasm under wasmtime (ahead-of-time .cwasm), and the Godot / Unity / Unreal hosts (planned) will load either the .wasm or a native library exposing the same gf_* functions. Ship the .wasm: it is the portable form.
Calling other modules: a module may call functions of another module (a kit's logic, a service) and expose its own, through the player, with versioned interfaces (.gfi) and no recompilation of the other side — gripforge_present_sdk::calls. The contract is grip-forge docs module-calls.
4.6 Test without a GPU #
#[test]
fn trace() { gripforge_present_sdk::harness::check::<Storm>("tests/storm.player.json", "tests/storm.trace"); }
cargo test -p gf-present-<kit> replays snapshots exactly as a host would (postcard included) and compares the command trace. Visual check: the before/after board (gripforge docs player, §6).
5. Host capabilities (caps) #
Init::host gives engine (bevy, later godot, unity, unreal), backend (webgpu, webgl2, vulkan…) and caps, a bit set of what the host draws beyond the baseline (meshes, nodes, StdMaterial). Degrade when a cap is missing — never assume one.
| Cap | Bit | Meaning |
|---|---|---|
WGSL_MATERIAL | 0 | MaterialWgsl |
POST_PASS | 1 | full-screen passes of the module |
SHADOWS | 2 | shadow maps |
HDR | 3 | HDR target (bloom-ready colours) |
AUDIO | 4 | AudioPlay |
UI | 5 | UI trees |
BONES | 6 | bone overrides, IK |
DECALS | 7 | projected decals |
FLOAT_TEXTURES | 8 | float texture formats |
TEXTURE_ARRAYS | 9 | 2D texture arrays (gf_tex_layer) |
WGSL_SURFACE | 10 | gf_surface (lit by the host) |
SCENE_DEPTH | 11 | gf_scene_depth(uv) in materials |
WEB_UI | 12 | React / template fragments (UiWeb): the web only |
RENDER_TARGETS | 13 | RenderTarget textures |
What the Bevy web player announces today (player/src/module_host.rs): SHADOWS | UI | WGSL_MATERIAL | FLOAT_TEXTURES, the same on WebGPU and on WebGL2 (half and full float textures are stored as Rgba16Float, filterable on WebGL2 too). Everything of the WGSL dialect works on both backends: the WebGL2 path is mandatory, so a host never needs a "WebGL2-safe" cap — the dialect itself is the WebGL2-safe subset. The few differences are in the dialect description: under WebGL2 gf_depth answers 1e9 (no depth texture). Init::host.backend (webgpu or webgl2) tells which one runs if you want to choose a cheaper path. A feature without its cap bit may still be applied by the host; when it is not, the command comes back Refused.
fn init(&mut self, _w: &World, init: &Init, out: &mut Out) {
self.fancy = init.host.caps & caps::WGSL_MATERIAL != 0;
}
Test both sides: the harness fixture sets host.caps; write one trace with the cap and one without. In a browser, ?renderer=webgl2 and ?renderer=webgpu force a backend. A command the host does not apply answers HostResult::Refused (read it in world.results) and is listed in kithost.undrawn.
6. Your own assets, fonts and UI #
6.1 Assets #
Kits draw what the project binds to their asset slots (bindings: slot → Library item). A module draws a slot's model with ModelInstances / ModelSpawn and a slot's texture with TextureSlot; it never fetches a URL. To bring your own model or texture: upload it to the Library (MCP / site), bind it to the slot. The slots of a kit, with the kinds they accept and the animation clips a creature slot needs, are in its API reference.
6.2 Fonts and theme #
The game's look is the ui.theme kit: colours, typography.font (a family), typography.fontFile (TTF / OTF / WOFF2: a URL on the web, res:// on Godot, a Resources path on Unity, an asset path on Unreal), logo and background. Fragments get it as props (theme) and as --gf-* CSS variables: write var(--gf-accent, <your colour>).
6.3 UI with ui.webui #
A module ships web/ui.json + web/ui.mjs (React components and / or HTML templates); a game or a mod can declare a fragment in the fragments config of ui.webui. Props: kit, views, vars, shared, theme, config, lang, dispatch(event, payload). dispatch sends only events declared as emit:<event> in the module's needs; a UI never feeds game input. Templates bind with data-gf-text, data-gf-show, data-gf-width / data-gf-of, data-gf-on-click. Trust: a mod:<id> UI runs in the page with the trust of mod scripts; it is not loaded when the game says mods.enabled: false, disables the mod, or denies the kit. Details: PORTING.md §8.
7. Publishing and installing #
- A mod: validate then install it in a project with
gripforge_game_mods(MCP) orPATCH …/data(modscollection). Players' mods are merged by the application into the project before the host mounts it. - A module: today the published modules are those of the GripForge sources (one folder per kit, contributed by pull request), built by
build.mjsand shipped as the modules package (gripforge docs player, §4). A module enters the published list when its kit's before/after board is green. A module for a kit of your own game goes with that game's export (--modules game). - A UI fragment: inside its module (
web/), or declared by the game or a mod inui.webuifragments.
8. ABI compatibility rules #
gf_abi()returns1: thegripforge:present@1contract. v1 is frozen: no field is ever added to an existing struct (postcard has no optional field).- Evolutions are appended enum variants only — of
Cmd,HostResult,Ext,Parent,Fog… — markedv1.1/v1.2inpresent_abi.rs. v1.1: shaders, clips, entity animation, sampled texture slots,NodeNode, fog reach. v1.2: node meshes / materials / textures owned by the module,ReadConfig, web UI (UiWeb,UiProps), render targets, UI images, shaders and gradients, entity materials,ModelSpawn, material options and clearcoat,SceneSet,ClaimOccupants. - Every command is encoded on its own: a host that does not know a command skips and reports it, the others apply. So an older host runs a newer module (degraded), a newer host runs an older module unchanged.
- A module built for
gf_abi() != 1is refused at load. A change that cannot be appended is a new ABI (@2). - Settings keys (
FrameIn::settings) and shared keys may be added freely.
9. Checklist #
- Read the kit's API reference (
grip-forge kit api <id>). - Mod: only moddable keys and collections; validated with
gripforge_game_mods action=validate. - Module: needs declared, caps checked, WGSL dialect only,
cargo testtrace green, wasm32 release builds, no Bevy incargo tree. - UI:
emit:<event>declared for everydispatch; works without the theme. - Both WebGPU and WebGL2 checked.