Command
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 and a
handler that applies it, split by trust into a shared -frontend.js part and an optional backend -backend.js part. 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 — the dependency graph
gamehoster-command-handler-frontend.js applies it — runs both sides, ctx-filtered
ready/
gamehoster-command-schema.json
gamehoster-command.json
gamehoster-command-handler-frontend.js
gamehoster-commands/, its parameter schema and its handlerA command runs at step 1 of the tick, before any update. The shared handler
applies the input to the sender's own player state and/or the shared
instance state. Each .js handler is a
raw function body (no export, no header) reaching the world through
Gamehoster.*, with Gamehoster.Time() (ms), Gamehoster.Tick() (integer) and
the predictable Gamehoster.Random() ([0,1), seeded by the tick and
context so it returns the same value on server and client).
The shared handler (-frontend.js) runs on both sides, so it stays deterministic:
it has no Gamehoster.Entropy() and writes only fields a client can reproduce, the sender's
shared and owner fields. When acting on a command has a private or authoritative
effect, a hidden roll, a spawn, a write to a sent field, the
command adds an optional backend handler (-backend.js) that runs on the server
alone, after the shared one, and there Gamehoster.Entropy() is available. Most commands, like pong's
moveY and ready, are wholly shared and need only the front-end file.
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
sync/audience/smooth: none of the 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 shared 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 an authoritative change (nothing written directly to a
synced player or instance field) declares an empty list, { "writes": [] }.
This is the game's declarative dependency graph, and it feeds the
deploy-time predictability check. Nothing more. A field's
sync is fixed in its schema, the same for every viewer; it never splits
shared-vs-sent per player at runtime. From writes the engine proves, once, that each
shared field is genuinely reproducible on every client that can see it: that its inputs are all
owner-driven and in view. If that closure holds the field may be shared; if it could ever depend
on out-of-view state it must be sent instead, and the check rejects the definition until it is.
See relevance and the
state model on Updates.
{
"writes": ["player.ready"]
}
gamehoster-command-handler-frontend.js
The shared handler is the raw function body that applies the command on both sides. 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/or the shared 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-frontend.js
// Move the sending player's paddle by their axis input. `y` is a `shared` field,
// so this runs on the owner's front end immediately (prediction) and on the server
// (authority). 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 shared instance state. Those permitted
fields 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). It runs on both sides, filtered by context: because the paddle's y is a
shared field, the sender's own client applies it immediately while the server confirms it.
| 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 shared 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() | Clock (ms), tick number, and the predictable random in [0,1) (seeded by tick + context, matching the server). The shared handler has no Gamehoster.Entropy() and must stay deterministic; the backend handler below may use it. |
gamehoster-command-handler-backend.js
Optional, and rare. A command needs a backend handler only when acting on it has a private or
authoritative effect a client must not compute: a hidden roll, an entity
spawn, a write to a sent or server field. It is a
raw function body like the shared one, with the same Gamehoster.* reads and the sender in
Gamehoster.Context, and it runs on the server only, right after the shared
handler. Because it never runs on a client it may use Gamehoster.Entropy() for true randomness, and
it may write the fields the shared handler cannot.
Pong has none: its two commands are wholly shared. A game with a boost that burns a hidden fuel gauge splits
the work. The shared handler pushes the ship, a shared field every client reproduces; the backend
handler decrements the private fuel, an owner or server field the others
never see. The relayed command lets other clients reproduce the visible push while the private effect stays on
the server. See the state model for which fields each half may write.