# Kits and your engine, both ways GripForge kits are Rust bricks that talk to each other only through **capabilities**: a kit that needs the ground asks the capability `world.ground`, it does not know which brick answers. Your engine is a brick like the others. Everything works in both directions, on every engine: | You want to… | Godot (GDScript) | Unity (C#) | Unreal (C++ / Blueprint) | Bevy (Rust) | C (`gfkits.h`) | |---|---|---|---|---|---| | call a kit | `call_service` | `CallService` | `CallService` | `call_with`, `kit_call` | `gfk_call` | | listen to a kit's events | `watch` + `game_event` | `Watch` + `gameEvent` | `Watch` + `OnGameEvent` | `watch` + `MessageReader` | `gfk_events_watch`, `gfk_events_take` | | change a kit's decision (hook) | `tap_hook` | `TapHook` | `TapHook` / `TapHookEvent` | `tap_kit_hook` | `gfk_hook_tap` | | **serve a capability yourself** | `provide_service` | `Provide` | `ProvideService` | `provide_ground`, `provide_kit_resource`, `provide_kit_service` | `gfk_provide` | The last line is what lets the choice you made in your engine — your own terrain, your own camera, your own inventory screen — keep working: the kits use your brick exactly as they would use a kit. ## Plug in your own brick (serve a capability) ### What happens 1. You give the host an object for a capability (`world.ground`). 2. When a kit calls `world.ground.groundY(x, z)`, the host calls the method `groundY` of your object with `x, z` and gives its return value back to the kit. 3. `has_service("world.ground")` answers true for every kit: the kits that only use the capability when it exists switch it on by themselves. Give it **before starting the game** when a kit needs it at mount time (the third-person kit asks `world.ground.defaultSpawn` when it places the player). Given later, it is used from the next call. It stays across a reload of the game and a stop / start of the host, until you remove it. **In place of a kit or beside it.** By default your brick **replaces** a kit that serves the same capability (your choice wins; the kit keeps running for its other capabilities). Pass `replace = false` to only fill a gap: a kit that serves the capability answers, yours answers when none does. **Rules for the provider.** It runs on the game thread, inside the step of the host: answer quickly and do not call the host back from it (the call is refused). Return a plain value: a number, a string, a boolean, an array or a dictionary. Calls whose arguments are all numbers (`groundY(x, z)`, made often) reach your engine without any JSON. ### Godot: your own terrain under the third-person kit A complete example: a Godot terrain (any mesh with collisions) and the third-person kit, without `world.terrain` and without a ray of your own. This is the scene of the test `wrappers/demo/godot/tests/provide_service_test.gd`. ```gdscript # my_terrain.gd — on the node of your terrain extends Node3D @export var heightmap: Image # or read your terrain resource, or cast into your own physics world func height_at(x: float, z: float) -> float: return 3.0 # replace with your terrain's height at (x, z) # world.ground, the methods the kits call (camelCase as in the contract; ground_y also works) func groundY(x: float, z: float) -> float: return height_at(x, z) func defaultSpawn() -> Dictionary: return {"position": [0.0, height_at(0.0, 0.0), 0.0], "yaw": 0.0} func bounds() -> Dictionary: return {"min": [-500.0, 0.0, -500.0], "max": [500.0, 50.0, 500.0]} ``` ```gdscript # main.gd @onready var host: GfGameHost = $GfGameHost @onready var terrain = $MyTerrain func _ready() -> void: host.provide_service("world.ground", terrain) # before start(): the spawn already asks it host.load_game("res://game/my.game.json") host.start() ``` The capsule of the player moves through the physics of Godot (`physics: "engine"`, the default), which already sees the collisions of your terrain; `world.ground` tells the kit where the ground is when it places the player (spawn, scene entry, respawn). In the lockfile, the capability is resolved to the host: `"capabilities": { "world.ground": { "provider": "host" } }` — no kit has to be installed for it. Other forms: ```gdscript host.provide_service("world.ground", terrain, false) # only when no kit serves world.ground host.provide_service("camera.look", func(method, args): return my_camera.call(method, args)) # a Callable host.unprovide_service("world.ground") # the kit answers again, if any print(host.provided_services()) # [{ "capability": "world.ground", "replace": true }] ``` ### Unity ```csharp using GripForge.Kits; public class MyTerrain : MonoBehaviour, IGfService, IGfNumberService { public GfGameHost host; float HeightAt(double x, double z) => Terrain.activeTerrain.SampleHeight(new Vector3((float)x, 0, (float)z)); void Awake() => host.Provide("world.ground", this); // before StartGame // Calls whose arguments are all numbers: no JSON. public bool ServeNumbers(string method, double[] args, out double result) { result = 0; if (method != "groundY") return false; result = HeightAt(args[0], args[1]); return true; } // Any other call: the arguments and the answer as JSON text. public string Serve(string method, string argsJson) => method switch { "defaultSpawn" => $"{{\"position\":[0,{HeightAt(0, 0)},0],\"yaw\":0}}", _ => throw new GfServiceRefused($"no method {method}"), }; } ``` A delegate works too: `host.Provide("world.ground", (method, args) => …)` answers any value of the `GfJson` model. `host.Unprovide("world.ground")` removes it, `host.Provided()` lists what Unity serves. ### Unreal (C++ and Blueprint) Implement the interface `IGfServiceProvider` on any object — a C++ class or a Blueprint (Class Settings → Interfaces → GfServiceProvider) — and give it to the host or the component: ```cpp // MyTerrainActor.h UCLASS() class AMyTerrainActor : public AActor, public IGfServiceProvider { GENERATED_BODY() public: virtual FString Serve_Implementation(const FString& Method, const FString& ArgsJson) override; virtual bool ServeNumbers_Implementation(const FString& Method, const TArray& Args, double& Result) override; }; // MyTerrainActor.cpp bool AMyTerrainActor::ServeNumbers_Implementation(const FString& Method, const TArray& Args, double& Result) { if (Method != TEXT("groundY") || Args.Num() < 2) return false; Result = HeightAt(Args[0], Args[1]); // your landscape's height (a line trace, a heightmap…) return true; } FString AMyTerrainActor::Serve_Implementation(const FString& Method, const FString& ArgsJson) { if (Method == TEXT("defaultSpawn")) return FString::Printf(TEXT("{\"position\":[0,%f,0],\"yaw\":0}"), HeightAt(0, 0)); return FString(); // no value: null } // Before StartGame: GameComponent->ProvideService(TEXT("world.ground"), TerrainActor); ``` In a Blueprint, implement the events **Serve** (and optionally **ServeNumbers**) of the interface, then call **Provide Service** on the GfGameComponent with your actor. `UnprovideService` removes it, `ProvidedServices` lists them. ### Bevy (Rust) A Bevy game links the kits as a Rust crate, `gripforge-kithost-bevy` (`kits/game_engine/bevy`): no C library, no text between your game and the kits. Arguments go as typed values (`Arg::Num`, `Arg::Str`, `Arg::Vec3`), answers come back typed (`Reply`, `Ret`) and are read in place through accessors (`Val`, `KitPayload`); a `to_json()` exists on the accessors for a tool or a debug print, never on the way. `groundY(x, z)` goes from the kit to your resource and back as two numbers and one number. ```rust use bevy::prelude::*; use gfkits_bevy::*; #[derive(Resource)] struct MyTerrain { /* your heightfield */ } impl GroundService for MyTerrain { fn ground_y(&self, x: f64, z: f64) -> Option { Some(self.height_at(x, z)) } fn default_spawn(&self) -> Option { Some(GroundSpawn { position: [0.0, self.height_at(0.0, 0.0), 0.0], yaw: 0.0 }) } } App::new() .add_plugins((MinimalPlugins, KitsPlugin::new(my_game).watch("player.*"))) .provide_ground::(Provide::Replace) // world.ground, read in place in your resource .add_systems(Startup, spawn_my_terrain) // the kits are mounted after Startup (KitsSet::Mount) .run(); ``` - `provide_ground::()` serves `world.ground` from a resource implementing `GroundService`; `provide_kit_resource::(capability, mode)` serves any capability from a resource implementing `KitService` (`fn serve(&self, call: &ServiceCall) -> Result`); `provide_kit_service(capability, mode, closure)` serves it from a closure. `ServiceCall` reads the arguments (`num(i)`, `text(i)`, `vec3(i)`) and the game's resources (`resource::()`) in place. - `Provide::Replace` (default) wins over a kit that serves the same capability; `Provide::Beside` only fills a gap. Given before the mount, the kits see it from their mount; given later (`KitGame::provide`), from the next call. It stays across a reload; `KitGame::unprovide` removes it, `KitGame::provided` lists them. - The game is mounted in `PostStartup` and steps in `Update` with Bevy's `Time` (`KitsSet::Mount`, `KitsSet::Step`). `KitGame` is a non-send resource (`NonSendMut`); in an exclusive system, `world.kit_call(...)` and `world.with_kits(...)` lend the world to the providers that read a resource. - Your Bevy terrain still needs a collision for the kits' physics (the kits run their own Rapier world: give it a static shape with `game.host_mut().physics.add_static_box(...)`). - Test: `kits/game_engine/bevy/tests/own_terrain.rs` (a terrain resource serves `world.ground`, the third-person player spawns on it, walks and stays grounded; events and a hook tap in the same app). ### C and other native hosts ```c static int32_t my_ground(void* user, const char* method, size_t method_len, const char* args, size_t args_len, const double* nums, size_t nums_len, GfkReply* reply) { if (method_len == 7 && memcmp(method, "groundY", 7) == 0 && nums_len == 2) { gfk_reply_number(reply, my_height(nums[0], nums[1])); return 0; } if (method_len == 12 && memcmp(method, "defaultSpawn", 12) == 0) { const char* a = "{\"position\":[0,3,0],\"yaw\":0}"; gfk_reply_json(reply, a, strlen(a)); return 0; } const char* why = "\"no such method\""; gfk_reply_json(reply, why, strlen(why)); return 1; /* refused: the kit gets the reason */ } gfk_provide(NULL, "world.ground", 12, 1 /* replace a kit */, my_ground, my_state); /* before gfk_game_create */ gfk_game_create(json, len, &game); ``` When every argument is a number, `args` is null and the numbers come in `nums`; otherwise `args` is the JSON array of the arguments and `nums` is null. `gfk_unprovide` removes a service, `gfk_provided` lists them. ## Call a kit Every service of every installed kit is callable from the engine, with the method names and arguments of the kit's API reference (`grip-forge docs `, MCP `gripforge_kit_api`): ```gdscript var hp = host.call_service("combat.health", "get", ["player"]) host.call_service("inventory.items", "give", ["herb", 1]) ``` ```csharp var pos = host.CallService("character.control", "position", new List()); ``` ```cpp FString Pos = Host->CallService(TEXT("character.control"), TEXT("position"), TEXT("[]")); ``` ```rust // Bevy: typed arguments, the answer read in place. let pos = game.call_vec3("character.control", "position", &[])?; let hp = game.call_number("combat.health", "get", &[Arg::Str("player")])?; ``` A call made from the engine reaches your own provided services too: `call_service("world.ground", "groundY", [0, 0])` answers what your terrain answers. ## Listen to a kit **Events.** Watch a pattern, then receive the matching events after each step: ```gdscript host.watch("combat.*") host.game_event.connect(func(topic, payload, event): print(topic, payload)) ``` Unity: `host.Watch("combat.*")` and the C# event `gameEvent`; Unreal: `Watch` and the Blueprint event `OnGameEvent`. Bevy: `KitsPlugin::new(game).watch("combat.*")`, then a system with `MessageReader` after `KitsSet::Step`; the payload is read in place (`event.payload.num("amount")`), moved out of the kit host, not copied. **Hooks.** A hook is a decision of a kit that the game may change before it applies (the damage of a hit, the speed of a jump). Tap it and answer the fields to change: ```gdscript host.tap_hook("combat.damage", func(payload, hook): return {"set": {"multiply": 2.0}}) ``` Unity: `TapHook(hook, (payload, hook) => …)`; Unreal: `TapHook` (C++) or `TapHookEvent` (Blueprint); Bevy: `app.tap_kit_hook("character.jump", 0.0, |call| HookReply::set("speed", Ret::Num(call.num("speed").unwrap_or(5.0) * 1.5)))`: the tap reads the payload in place and answers `HookReply::Pass`, `HookReply::set(field, value)` or `HookReply::cancel(reason)`. The hooks of a kit and their fields are listed in its API reference (`hooks`), the same ones a mod uses (contract of `game.engine`). ## Which services does a kit consume? Every kit declares the services it consumes in `services.json` next to its manifest: for each capability, whether it is **required** (listed in the kit's `requires`) or **optional** (the kit checks `has_service` and works without it), and the **methods** it calls. The same list is in the API reference of the kit (section `consumes`), which the CLI and the MCP show. Read it to know what your brick must answer. For the third-person kit: | Capability | Required | Methods called | |---|---|---| | `world.ground` | yes | `defaultSpawn()` → `{ position: [x, y, z], yaw }`, `groundY(x, z)` → height | | `input.actions` | yes | `axis`, `pressed`, `justPressed` | | `combat.ranged`, `combat.melee`, `combat.health` | no | used when installed | | `engine.hooks` | no | `run` (game.engine) | A kit that calls a capability or a method it did not declare fails the checks of the kit repository, so the list is always complete. ## When it does not do what you expect - **The kit does not use my service.** Check the capability name and the method names (case matters: `groundY`; Godot also accepts `ground_y`). Give the service before `start` when the kit reads it at mount time. - **A kit still answers instead of mine.** You passed `replace = false`; a kit serving the capability wins then. - **"the provider failed (did it call the host back?)".** The provider called the host during the step: compute the answer from your own data instead. - **The player falls through my terrain.** `world.ground` places the player; the collisions come from the physics. Keep `physics: "engine"` so the capsule moves in your engine's world, and give your terrain a collision shape. ## Limits - One provider per capability. Providing it again replaces the previous one. - The provider answers synchronously, on the game thread. - On the web, the page serves a capability with `hostServices` when it creates the game: a function fills a gap, `{ serve: fn, replace: true }` replaces a kit. ## Versions `gfk_provide`, `gfk_unprovide`, `gfk_provided` and `gfk_reply_number`: gfkits ABI 1.11 (library 0.2.22). `provide_service` (Godot), `Provide` (Unity) and `ProvideService` (Unreal) ship with the same library. Bevy: `gripforge-kithost-bevy` 0.1, typed providers over the kit host's `provide_typed`.