Playing

Once you are in a match, the library streams the game to you and sends the player's input back. You subscribe to two things on the instance (its frames to draw and its state to react to), send input and commands through one call, and never touch the wire yourself. Every call takes the instance (or its id) as its first argument.

Provisional: tracks the frontend API as it settles.

Rendering frames

Instance.Frame is the render path. It returns a subscription handle; set its onChange, and the library calls it on your own free-running frame loop, as fast as the display allows: there is no fixed render rate. Between the server's sparse packets the client itself runs the game's own *-update-frontend.js shared bodies at the tick rate (the same code the backend runs), so the logical state keeps simulating and stays live between snaps, and each callback hands you a ready-to-draw, already-predicted frame view interpolated from it. You never invoke those bodies yourself; you just draw what you're handed:

const frame = Gamehoster.Instance.Frame(instance)
frame.onChange = (view) => {
  // view.players — each { id, x, y }
  // view.entities — each { id, type, x, y }
  draw(view)                       // already interpolated — just render it
}
frame.onError = (error) => showRenderError(error)
// later: frame.cancel()
Subscribing to interpolated render frames

The frame view is only the interpolated positions of everything on screen: the players and the entities (each with their type), and only the ones relevant to you. The backend streams each player just what they are allowed to see, so others appear and vanish from the view as they enter and leave your relevance. Purely cosmetic touches (grass, smoke, particles, any decorative randomness) live in this render loop too, drawn from the view but never synced anywhere.

The instance state

Instance.State is the logic path. It's a subscription too, but where Frame runs on your own free-running frame loop, this arrives at the server's send rate (the authoritative word, snapping to whatever the server last confirmed) and surfaces every logical change: a goal is scored, a player readies up, the phase advances. Use it for scoreboards, phase screens, and the player list; use Frame for drawing:

const state = Gamehoster.Instance.State(instance)
state.onChange = (state) => {
  // state.phase · state.players · plus the instance's own fields
  updateScoreboard(state)
}
state.onError = (error) => showConnectionLost(error)
// later: state.cancel()
Subscribing to the instance's current state

The instance state carries the instance's phase, so following the flow (waiting, playing, over, round again) is just reading state.phase as it changes. Its core fields, plus whatever the instance's schema declares (score, turn, and so on):

FieldTypeDescription
idstringThe instance id.
phasestringThe instance's phase: waiting, playing, over, and the like.
playersarrayThe players currently relevant to you: each an individual, with an id, their fixed join info, and the per-instance fields the game's player schema declares (like score and y). A game with per-player relevance shows you only those you may see.
Plus every field the instance's schema declares.

An instance starts in its waiting phase and moves to playing; this one subscription tracks the state throughout. While the phase is waiting, its state is the lobby (the player list and their ready flags), so you show your gathering UI straight from state.players. There is no separate lobby subscription.

Sending input and commands

Input flows the other way through Instance.Send(instance, { type, …params }). It's fire-and-forget (it returns nothing), and one call carries both high-frequency input and one-off commands. You propose; the server disposes:

Gamehoster.Instance.Send(instance, { type: "move", y })   // input
Gamehoster.Instance.Send(instance, { type: "ready" })     // lobby command
Gamehoster.Instance.Send(instance, { type: "start" })     // command
One call for inputs and lobby verbs alike

Every command is handled server-side by the game's own command body, which decides for itself whether to act. An illegal command (not your turn, wrong phase) is one the handler ignores with an early return, so it changes nothing. You can run the same handler logic on the client to decide whether to offer the control. A command that the handler ignores simply never shows up in the instance state or the frame view: that's where every command's effect appears, never as a return value from the call.

Leaving

Instance.Leave disconnects from the room and ends its subscriptions. It's an operation handle, so you can confirm you're out:

const left = Gamehoster.Instance.Leave(instance)
left.onSuccess = () => returnToMenu()
left.onError   = (error) => showError(error)
Leaving a room