Commands

A command is a thing a player may send: a paddle nudge, a ready toggle, a move. The whole set a game accepts lives in gamehoster-commands/, one directory per command, each holding a schema of the command's input parameters, a writes manifest, and a single handler the server runs to apply it. There is one set for all players: no per-role or per-phase lists. Legality is not a gate the engine enforces; the handler decides for itself what is appropriate from the sender's state, and ignoring is how a command is illegal: an out-of-turn or out-of-phase command simply returns without doing anything.

game.gamehoster.org/
  pong/
    gamehoster-commands/                       one directory per command
      moveY/                                   a command (you name it)
        gamehoster-command-schema.json         the input parameters this command takes
        gamehoster-command.json                the fields it writes
        gamehoster-command-handler.js          the server body that applies it
      ready/
        gamehoster-command-schema.json
        gamehoster-command.json
        gamehoster-command-handler.js
A command is a directory under gamehoster-commands/: its parameter schema, its writes manifest and its handler

A command runs at step 1 of the tick, before any update, and applies on the next tick after the player sends it. Its handler runs on the server and applies the input to the sender's own player state and the instance state. The handler is a raw function body (no export, no header) reaching the world through Gamehoster.*, with Gamehoster.Time() (ms), Gamehoster.Tick() (integer), the predictable Gamehoster.Random() ([0,1), seeded by the tick and context) and, since it runs only on the server, Gamehoster.Entropy() for true randomness.

gamehoster-command-schema.json

The command's input parameters: the payload the client sends with it. It is an array of parameter objects in the same typed form as the other schemas (a parameter name is never used as an object key), but with one difference: a command's parameters carry no visibility, and none of the other state-field keys. They are transient input, not stored state, so each entry is just a fully-qualified -name and -type (with an optional -default). Pong's moveY takes a single axis value:

[
  {
    "gamehoster-command-schema-name": "axis",
    "gamehoster-command-schema-type": "number"
  }
]
pong · gamehoster-commands/moveY/gamehoster-command-schema.json
ParameterTypeWhat it is
axisnumberThe paddle input the client sends: how far, and which way, to move this tick.
KeyTypeMeaning
gamehoster-command-schema-namestringThe parameter's name.
gamehoster-command-schema-typestringIts type.
gamehoster-command-schema-defaultoptionalA default value; omit it to require the sender to supply the parameter.

A command with no input has an empty schema: pong's ready toggle carries nothing, so its gamehoster-command-schema.json is just []. The handler reads each parameter back by name with Gamehoster.Command.Get(name).

gamehoster-command.json

This file declares the fields the command writes. Nothing more. Each entry is a dotted field path: player.<field> (the sender's own player state) or instance.<field> (the instance state). Pong's moveY writes the sender's paddle position; ready writes the sender's ready flag:

{
  "writes": ["player.y"]
}
pong · gamehoster-commands/moveY/gamehoster-command.json: the fields this command touches

A command whose only effect is a server-side spawn or a write the handler makes some other way declares an empty list, { "writes": [] }.

The manifest is the handler's allowed-write set. The engine buffers everything the handler writes and, when it returns, commits only the fields the manifest lists: a write to any field outside it is a silent no-op, and a write to a field that does not exist a contained error. So a command can touch only the state it declares, and that declaration is checked against real fields at deploy. See the write firewall.

{
  "writes": ["player.ready"]
}
pong · gamehoster-commands/ready/gamehoster-command.json: the ready toggle writes one field

gamehoster-command-handler.js

The handler is the raw function body that applies the command on the server. It reads its parameters with Gamehoster.Command.Get(param) and takes the sender as Gamehoster.Context.playerId (its instance is Gamehoster.Context.instanceId). From there it decides for itself whether the command is appropriate and, if so, writes the sender's own player state and the instance state. Pong's moveY ignores anything outside the playing phase, then moves the sender's paddle, clamped to the court:

// gamehoster-commands/moveY/gamehoster-command-handler.js
// Move the sending player's paddle by their axis input, on the server. `y` is a public
// field, so the move streams back to every viewer. Ignored outside the playing phase.

const me   = Gamehoster.Context.playerId
const here = Gamehoster.Context.instanceId
if (Gamehoster.Instance.State.Get(here, "phase") !== "playing") return

const speed = Gamehoster.Instance.State.Get(here, "paddleSpeed")
const half  = Gamehoster.Instance.State.Get(here, "paddleH") / 2
const court = Gamehoster.Instance.State.Get(here, "courtH")

const y = Gamehoster.Player.State.Get(me, "y") + Gamehoster.Command.Get("axis") * speed
Gamehoster.Player.State.Set(me, "y", Math.max(half, Math.min(court - half, y)))
pong · gamehoster-commands/moveY/gamehoster-command-handler.js: clamp the sender's paddle to the court

The first line is the whole of the command's "legality": outside playing it just returns and nothing happens. The handler only ever writes its own sender's player state (Gamehoster.Context.playerId) and the instance state. The fields it may commit are exactly the ones its gamehoster-command.json writes manifest lists: that manifest is the handler's allowed-write set. A handler's writes are buffered and, when it returns, flushed against that set; a write to any other real field is a silent no-op and a write to a field that does not exist a contained error (the write firewall, in full on the Players page).

FunctionWhat it does
Gamehoster.Command.Get(param)Read one of this command's input parameters by name.
Gamehoster.Context.playerId · Gamehoster.Context.instanceIdThe ids in scope: the player who sent the command and their instance.
Gamehoster.Player.State.Get(Gamehoster.Context.playerId, name) · .Set(Gamehoster.Context.playerId, name, value)Read and write the sender's own player state.
Gamehoster.Instance.State.Get(id, name) · .Set(id, name, value)Read and write the instance state.
Gamehoster.Player.List()Ids of the players in this instance.
Gamehoster.Player.State.Get(id, name)Read another player's state field.
Gamehoster.Entity.List(type)Ids of the live entities of a type.
Gamehoster.Entity.State.Get(id, name)Read an entity's state field.
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy()Clock (ms), tick number, the predictable random in [0,1) (seeded by tick and context) and true randomness. The handler runs on the server, so both randoms are available.

A command needs nothing more than this one handler. When acting on it has a server-only effect, a hidden roll or an entity spawn, the same handler does it: it can use Gamehoster.Entropy() and, because it runs on the server alone, write freely into the persistent bags (Gamehoster.Player.Persistent(id), Gamehoster.Instance.Persistent) that no client ever sees — those writes are not schema fields, so the writes manifest does not gate them. Most commands, like pong's moveY and ready, only touch a public field or two.