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
gamehoster-commands/: its parameter schema, its writes manifest and its handlerA 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"
}
]
| Parameter | Type | What it is |
|---|---|---|
axis | number | The paddle input the client sends: how far, and which way, to move this tick. |
| Key | Type | Meaning |
|---|---|---|
gamehoster-command-schema-name | string | The parameter's name. |
gamehoster-command-schema-type | string | Its type. |
gamehoster-command-schema-default | optional | A 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"]
}
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"]
}
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)))
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).
| Function | What it does |
|---|---|
Gamehoster.Command.Get(param) | Read one of this command's input parameters by name. |
Gamehoster.Context.playerId · Gamehoster.Context.instanceId | The 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.