Co-op

Usage guide of coop.session: drop-in, drop-out co-op for 1 to 4 players in one world, room codes, personal or shared loot, downed state and revive, difficulty by player count, settings and defaults, service, events, hooks and variables.

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

Also read offline — CLI: grip-forge docs coop-sessionMCP tool: gripforge_docs { "topic": "coop-session" }

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.

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 #

<!-- test: kits/game_engine/host/tests/net_kits.rs::a_coop_room_travels_through_the_relay_of_the_core --> 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.

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 #

SettingDefaultMeaning
maxPlayers4{{s_maxPlayers}}
playerNamePlayer{{s_playerName}}
snapshotHz, inputHz10, 20world deltas sent to the clients per second (snapshotHz, 1 to 60) and inputs a client sends per second (inputHz, 1 to 60)
keyframeSeconds, positionStep, angleStep0, 0.001, 0.001keyframeSeconds 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, snapTolerance4.5, 1.6, 2how 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)
playerHealth100{{s_playerHealth}}
downedSeconds30{{s_downedSeconds}}
reviveSeconds, reviveRadius, reviveHealth3, 2, 0.3reviveSeconds (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)
respawnSeconds10{{s_respawnSeconds}}
interactRadius2.5{{s_interactRadius}}
dropSeconds, rejoinSeconds4, 60dropSeconds: 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, lootEventspersonalpersonal: 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, localVarslistsprefixes 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)
autoHostfalse{{s_autoHost}}
clickRevivetrue{{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.

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

SymptomCause and fix
A client is refused on joiningRead 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 falseThere 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 revivedThe 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.