# 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](glossary.md). 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 · 5. 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](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 [--section config,hooks] [--json]` - MCP: `gripforge_gamekit_api { id, sections? }`, or `gripforge_gamekit_get { id, section: "api" }` - HTTP: `GET /api/v1/gamekits//api` (`?format=md`, `?compact=1`) - Catalogue: the **API** tab of the kit page. How the reference is made and checked: [kit-api.md](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: ```json { "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..` merges an object key by key and **replaces** anything else — an array is replaced whole. To change ONE element of a config list, use `configPatch` (game.engine 1.3): `"configPatch": { "survival.needs": { "update": { "needs": [{ "match": { "id": "hunger" }, "set": { "drain": 0.3 } }] } } }` — operations `set` (by dotted path), `append`, `update` (`match` + `set`) and `remove` on 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.` with `remove: [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: , then: [] }` on events; verbs are listed per kit. - **What validation does not check**: `applied` means 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 as `mutable`, or a value of the wrong type, is dropped at run time (event `engine.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 watch `engine.hook.refused` while 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.engine` config `mods.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 ```rust 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:` | `&["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` 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 ```sh # in the modules workspace of the GripForge sources (one folder per kit) cargo build --release --target wasm32-unknown-unknown -p gf-present- node build.mjs # 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 ```rust #[test] fn trace() { gripforge_present_sdk::harness::check::("tests/storm.player.json", "tests/storm.trace"); } ``` `cargo test -p gf-present-` 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`. ```rust 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, )`. ### 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:` 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:` 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) or `PATCH …/data` (`mods` collection). 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.mjs` and 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 in `ui.webui` `fragments`. ## 8. ABI compatibility rules - `gf_abi()` returns `1`: the `gripforge:present@1` contract. **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`… — marked `v1.1` / `v1.2` in `present_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() != 1` is 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 `). - [ ] Mod: only moddable keys and collections; validated with `gripforge_game_mods action=validate`. - [ ] Module: needs declared, caps checked, WGSL dialect only, `cargo test` trace green, wasm32 release builds, no Bevy in `cargo tree`. - [ ] UI: `emit:` declared for every `dispatch`; works without the theme. - [ ] Both WebGPU and WebGL2 checked.