Online zones

Usage guide of net.zones: a trusted server coordinator that admits players into zones, binds them to a network peer, checks every command (epoch, sequence, budget, target) and moves players between zones by ticket.

Source: module-kits:kits/net_zones/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs net-zonesMCP tool: gripforge_docs { "topic": "net-zones" }

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). 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 #

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 #

<!-- test: kits/net_zones/tests/net_zones.rs::a_transfer_is_prepared_then_committed_once_with_its_ticket --> Two zones (a for 2 actors, b for 1) and an admitted actor ada. Every call is net.zones.request("default", command):

{ "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": <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 #

SettingDefaultRangeMeaning
maxZones641 to 256zones per coordinator
maxActors10241 to 16384actors with a grant plus placed NPC, together
transferFrames6001 to 36000ticks a transfer reservation lasts
resumeFrames36001 to 216000ticks a disconnected actor keeps its place
commandsPerFrame81 to 64commands 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) #

GroupOps
Setup (trusted)register { zone, capacity }, allow { actor, zones }, place { entity, zone } for NPC targets
Sessionbind { peer, actor, zone }, join { peer, zone }, suspend { peer }, resume { peer }, leave { peer }
Per commandauthorize { peer, epoch, sequence, target? }
Transferprepare { peer, zone }, commit { peer, ticket }, cancel { peer }
Readsroute { peer }, view { peer }, members { zone }, status, config, targetable { actor, zone }
Savesave, 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: "<id>", ...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 allowed 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.