# Zones, admission and transfers for an online world (net.zones) A server-side coordinator for a game with several zones (areas, dungeons, instances): who may enter which zone, which network peer plays which character, whether a command from a client is fresh and allowed, and how a character moves from one zone to another. Readable offline: `gripforge docs net-zones`, or the MCP tool `gripforge_docs` (topic `net-zones`). ## What it is for `net.zones` runs on the trusted server, on top of the network session of the `net.*` kits ([multiplayer guide](guide:net)). The server gives it the peer id that the network layer reports, never one claimed by a client packet. It stores no positions and no physics: those stay in your game, in metres, +Y up, -Z forward. It keeps zones with a capacity, the characters (actors) each account may play, the NPC placed in each zone, and one lease per connected peer. A lease says which actor and zone a peer holds, and carries an `epoch` and a `sequence` that make old or replayed commands useless. It is not a login system: authenticate the account and grant its character (`allow`) before `bind`. `join` is a guest or demo shortcut that takes the first free admitted actor. ## Turn it on ```text gripforge_gamekit_install { project_id: "gkp_…", id: "net.zones" } gripforge_gamekit_configure { project_id: "gkp_…", id: "net.zones", config: { "maxZones": 16, "commandsPerFrame": 4 } } ``` The kit requires `game.engine`. It works on the web and in the native Rust host. ## First example Two zones (`a` for 2 actors, `b` for 1) and an admitted actor `ada`. Every call is `net.zones.request("default", command)`: ```json { "op": "register", "zone": "a", "capacity": 2 } { "op": "register", "zone": "b", "capacity": 1 } { "op": "allow", "actor": "ada", "zones": ["a", "b"] } { "op": "bind", "peer": 1, "actor": "ada", "zone": "a" } { "op": "prepare", "peer": 1, "zone": "b" } ``` `prepare` answers the lease with `pending: { zone: "b", ticket, until }` and reserves a place in `b`; the actor sends no command and is no target while it waits. When the destination is ready, `{ "op": "commit", "peer": 1, "ticket": }` moves the route once: the zone becomes `b` and the ticket is the new `epoch`. A second `commit`, or a wrong ticket, is refused. ## Settings and defaults | Setting | Default | Range | Meaning | |---|---|---|---| | `maxZones` | `64` | 1 to 256 | zones per coordinator | | `maxActors` | `1024` | 1 to 16384 | actors with a grant plus placed NPC, together | | `transferFrames` | `600` | 1 to 36000 | ticks a transfer reservation lasts | | `resumeFrames` | `3600` | 1 to 216000 | ticks a disconnected actor keeps its place | | `commandsPerFrame` | `8` | 1 to 64 | commands accepted per actor and tick | A value out of range, or an unknown key, is refused when the coordinator is created. ## Services, events, hooks - `net.zones` (service): `create(id, config)`, `request(id, command)`, `step(id?)`, `ids()`, `remove(id)`, `targetable(actor, zone)`. The coordinator `default` exists at start and survives scene changes; a game can hold up to 128 instances in all (separate worlds). - `step` advances one tick: it resets the command budgets and expires transfers and suspended actors. Call it once per authoritative tick, even with nobody connected. - `targetable(actor, zone)` answers true when the actor is in that zone, online and not in a transfer: a read-only check for server-side AI. - `net.zones.created` (event): `{ instance, preset }`; net.zones.removed (event): `{ instance }`. - `net.zones.changed` (event): `{ instance, op, result }` after any command that is not a read or a fixed step. - net.zones.refused (event): `{ instance, op, by, reason }` when a hook cancelled a command. - `net.zones.create` (hook): before an instance is built; `config` is mutable; cancellable. - `net.zones.request` (hook): before a deciding command; `command` is mutable; cancellable. The command then answers `{ ok: false, cancelled: true, by, reason }` and nothing changes. ### Commands (`op`) | Group | Ops | |---|---| | Setup (trusted) | `register { zone, capacity }`, `allow { actor, zones }`, `place { entity, zone }` for NPC targets | | Session | `bind { peer, actor, zone }`, `join { peer, zone }`, `suspend { peer }`, `resume { peer }`, `leave { peer }` | | Per command | `authorize { peer, epoch, sequence, target? }` | | Transfer | `prepare { peer, zone }`, `commit { peer, ticket }`, `cancel { peer }` | | Reads | `route { peer }`, `view { peer }`, `members { zone }`, `status`, `config`, `targetable { actor, zone }` | | Save | `save`, `load { save }`, `preview { command }` | Ids are 1 to 128 characters; a peer id is a positive integer. Actor and NPC ids are distinct. One actor has at most one peer. How to use them: 1. On connection, `bind` the authenticated character (or `join` for a guest). On a final disconnect call `leave` at once, so a reused peer id cannot inherit the lease; on a lost link call `suspend` (it cancels transfers and rotates the `epoch`), then `resume` or `join` when the player is back. 2. Before running any gameplay command from a client, call `authorize` with the epoch and sequence the client sent. It refuses an offline or transferring peer, an old `epoch`, a sequence that does not rise or jumps by more than 1024, a command over the per-tick budget, and a `target` outside the zone. The sequence is consumed even if your gameplay action then fails, so a retried cast cannot be paid twice; a command refused for budget does not use its sequence. 3. To change zone, `prepare`, stage the destination in your game, then `commit`. `cancel`, a suspension or `transferFrames` ticks without commit release the reservation. 4. Send each client only `view { peer }` (the online actors and the NPC of its zone). `status` is for the server: do not broadcast it. `save` stores zones, NPC placement and grants, never leases or session secrets. `load` validates the whole save before applying it, clears every lease, and rejects a save made with other settings; players must be admitted again. ## Hooks and modding Every key of the settings is open to mods, plus the net_zones_presets registry: `create(id, { preset: "", ...own keys })` takes the config of the preset under your own keys. The script verbs net_zones_request (action) and net_zones_exists (condition) call `request` and test an instance from content. ## Engines The coordinator is a Rust core run by the kit host on the web and natively; the same core is available as a C library and a WebAssembly module. It decides who may do what; each engine still renders the world, runs the network session and applies the transfer in its own scenes. ## When it does not do what you expect - `admission denied`: the actor was not `allow`ed in that zone, or does not exist. - `zone full`: capacity reached, a reserved transfer place counts. - `peer already bound`, `actor already bound`: one peer per actor; `leave` the old peer first. - `stale or invalid sequence`: replay, no rise, or a jump over 1024. - `inactive or stale route`: wrong `epoch`, a suspended peer, or a transfer in progress. - `command budget exceeded`: more than `commandsPerFrame` commands in one tick; call `step` each tick. - `target outside zone`: the target is not a placed NPC of that zone or an online actor there. - `invalid transfer ticket`, `no pending transfer`: wrong ticket, already committed, cancelled or expired. - `unknown peer`: the peer was never bound, or already left. - `zone limit` or `invalid zone entity`: `maxZones` or `maxActors` reached, or a duplicate id. - `save mismatch`: the save was made with other settings. ## Limits One coordinator is one authority: a transfer is a local hand-over between zones that the same server has loaded, not a distributed transaction between servers. No login, no persistence of sessions, no positions. Zones share the world of the server; they are logical areas, not streamed scenes. ## Versions Kit 1.0.0, ABI v1 of the zones library.