# Drop-in co-op (coop.session) ## What it is for `coop.session` puts one to four players in the same world, for survival, RPG, horror, platformer or any PvE game. The host (a player's browser or a dedicated process) owns the world; friends join by a room code at any time, receive the whole world on arrival and leave without ending it. Each player has their own character: inventory, needs, levels and wallet follow them and come back when they rejoin. A downed player can be revived by a teammate before they bleed out; loot is personal or shared; enemies scale with the number of players. Roles are `solo`, `host` (the authority) and `client` (a replica corrected by every snapshot). A lobby, a team HUD (health and status of everyone) and a revive prompt are drawn by the web display module. ## Turn it on Install the kit in a project and set its settings with the tools below; `gripforge kit api coop.session` lists every setting, and `gripforge docs coop-session` prints this guide offline. ```text gripforge_gamekit_install { project_id: "gkp_…", id: "coop.session" } gripforge_gamekit_configure { project_id: "gkp_…", id: "coop.session", config: { "maxPlayers": 4, "loot": "shared", "downedSeconds": 45 } } ``` ## First example A host opens a room over `net.transport`: the room code is the one configured, a client joining with the handshake gets a seat and a welcome packet then the whole world, `players()` lists it, and its departure emits `coop.left` and keeps its character. Joining without a room code does nothing. ```text coop.session.hostRoom() // true coop.session.code() // "LAN1" (the room configured on net.transport) coop.session.players() // [ host, { seat: 1, name: "Bob", … } ] once Bob joined // when Bob leaves: coop.left, his character is kept coop.session.joinRoom(" ", "Zed") // false: no room code ``` ## Settings and defaults | Setting | Default | Meaning | | --- | --- | --- | | `maxPlayers` | 4 | {{s_maxPlayers}} | | `playerName` | `Player` | {{s_playerName}} | | `snapshotHz`, `inputHz` | 10, 20 | world deltas sent to the clients per second (`snapshotHz`, 1 to 60) and inputs a client sends per second (`inputHz`, 1 to 60) | | `keyframeSeconds`, `positionStep`, `angleStep` | 0, 0.001, 0.001 | `keyframeSeconds` 0: the whole world only goes to each newcomer and to a client that asks for it; above 0 it is also resent to everyone that often. `positionStep` and `angleStep` quantize positions (m) and yaw (rad) in deltas; 0 on both sends them exactly | | `moveSpeed`, `sprint`, `snapTolerance` | 4.5, 1.6, 2 | how the host judges a client: `moveSpeed` (walk speed in m/s of a remote player moved by its inputs), `sprint` (multiplier) and `snapTolerance` (a client keeps its own predicted position while the host agrees within this distance in m) | | `playerHealth` | 100 | {{s_playerHealth}} | | `downedSeconds` | 30 | {{s_downedSeconds}} | | `reviveSeconds`, `reviveRadius`, `reviveHealth` | 3, 2, 0.3 | `reviveSeconds` (holding revive next to a downed teammate this long brings them back, 0.1 to 60), `reviveRadius` (reach in m) and `reviveHealth` (share of health they get back, 0.01 to 1) | | `respawnSeconds` | 10 | {{s_respawnSeconds}} | | `interactRadius` | 2.5 | {{s_interactRadius}} | | `dropSeconds`, `rejoinSeconds` | 4, 60 | `dropSeconds`: a client that hears nothing from the host this long counts its transport as dropped (1 to 120); `rejoinSeconds`: a room client whose transport drops joins the same code again with its rejoin token, seat and character back, retrying this long (0: never, up to 600) | | `loot`, `lootEvents` | `personal` | `personal`: what a player picks up is theirs; `shared`: every player present gets the same. `lootEvents` lists the events with `{ item, count }` that count as loot for `shared` (default `inventory.picked`, `resource.gathered`) | | `difficulty` | `{ count: 0.5, health: 0.35, damage: 0 }` | multipliers by player count, `1 + share x (players - 1)`: `count` (extra enemies per extra player, 0.5), `health` (extra enemy health, 0.35), `damage` (extra enemy damage, 0). `encounters.waves` and `enemy.ai` read the variables below | | `localKits`, `localVars` | lists | prefixes of kit ids (`localKits`) and of variables (`localVars`) whose state a client keeps from its own machine when a keyframe arrives: its view, camera, controls, inventory and needs (long default lists; extend them for your own kits) | | `autoHost` | `false` | {{s_autoHost}} | | `clickRevive` | `true` | {{s_clickRevive}} | ## Services - `coop.session` (service): `role()`, `code()`, `menu(open?)`, `players()` (a list of `{ seat, name, entity, status, host, ping, you }` with `status` of `online`, `away`, `downed` or `out`), `me()`, `token()`, `host(link, { code?, name?, seatLocal? })`, `join(link, name, token?)`, `leave()`, `hostRoom(code?, name?)` and `joinRoom(code, name, token?)` over `net.transport`, `setInput(partial | null)`, `holdRevive(on)`, `ping()`, `difficulty()` (`{ players, count, health, damage }`), `downed(entity?)`, `reviveTarget(entity?)`, `revive(target, by?)`, `characters()`, `stats()` and `memoryHub()` for in-process sessions. - Variables: `coop.role`, `coop.menu`, `coop.code`, `coop.players`, `coop.ping`, `coop.downed`, `coop.bleedout`, `coop.revive_target`, `coop.reconnecting`, `coop.revive_progress`, `difficulty.enemy_count`, `difficulty.enemy_health`, `difficulty.enemy_damage`. Inputs: `revive` (hold, key `KeyT`) and `coop_menu` (key `KeyO`). ## Events - `coop.hosting` (this machine became the host, with the `code`), `coop.joined` (a player arrived: `player`, `seat`, `name`, `rejoined` when they came back with their character), `coop.left` (a player left: `reason` of left, dropped or closed; their character is kept), and for a client `coop.connected`, `coop.reconnecting` (the transport dropped, joining the room again: `attempt`, `seconds` left), `coop.disconnected` and `coop.refused` (`reason`: full, version, closed, or mods when the host runs other mods). - `coop.downed` (a player is down: `player`, `seconds`), `coop.revived` (`player`, `by`), `coop.bledout`, `coop.respawned`, `coop.wiped` (every player present is down at once: a game over for a trigger to act on), `coop.difficulty` (`players`, `count`, `health`, `damage`) and `interaction.requested` (the interact press of a remote player on the nearest interactable in reach, as `interaction.system` emits it for the local player). It listens to `inventory.picked` and `resource.gathered`. ## Hooks and modding - `coop.down`: the host asks it once when a player at 0 health (or tagged dead) is about to be downed for `seconds`; `seconds` is mutable (0: the bleed-out comes at the next tick), and cancelling means no downed phase at all: the player is out at once (`coop.bledout`, no `coop.downed`) and the respawn clock starts. - `coop.revive`: the revive of a downed player is about to succeed (a teammate held revive for `reviveSeconds`, or `revive(target, by)`); `health` is the share given back and is mutable, and cancelling keeps the player downed, resets the progress to 0 (`revive` returns `false`) and lets the bleed-out clock run on. - `coop.bleedout`: the time of a downed player is over and they are about to bleed out; a continuous decision asked once and held: cancelled, the player stays downed and revivable while the refusal is kept, and it is released when they are revived. A mod can tune the rules the host applies: `playerHealth`, `downedSeconds`, `reviveSeconds`, `reviveRadius`, `reviveHealth`, `respawnSeconds`, `interactRadius`, `loot`, `difficulty` and `clickRevive`. What sizes or paces the session, bounds a client or says what is replicated stays the project's. ```json { "id": "last_stand", "hook": "coop.down", "set": { "seconds": 60 } } { "id": "medic", "hook": "coop.revive", "match": { "by": "e1" }, "set": { "health": 1 } } ``` ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | A client is refused on joining | Read `coop.refused`: `full` (`maxPlayers` reached), `version` (a client of an older wire protocol), `closed` (the room is closed) or `mods` (the host runs other mods than this game). | | `joinRoom` or `hostRoom` returns `false` | There is no room code, or no kit host serves the network core: install `net.transport`, or use `host` and `join` with your own link. | | A downed player is never revived | The reviver is further than `reviveRadius`, releases the key before `reviveSeconds`, or a `coop.revive` tap cancelled it; with `downedSeconds` at 0 there is no downed state at all. | ## Limits - The host owns the world; if the host leaves, the session ends for the others. - It runs on the web player and the native Rust host. A client keeps its own local player only on the web; `hostRoom` and `joinRoom` need a kit host that serves the network core. A 1.7 host refuses 1.0 to 1.6 clients (`version`). - One match per server process: to run many rooms, start one server per match. ## Versions Kit `coop.session` 1.7.0 (2026-10-12): a binary wire (protocol 2), numbered deltas with resync, no periodic keyframe by default. 1.5 added reconnection (`dropSeconds`, `rejoinSeconds`), 1.4 the hooks of the downed state, 1.6 the click revive. It needs `input.actions` and `game.engine` ^1, and `net.transport` for room codes.