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
A command is a directory under gamehoster-commands/, its parameter schema and its handler

A 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"
  }
]
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 shared 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 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"]
}
pong · gamehoster-commands/ready/gamehoster-command.json: the ready toggle writes one field

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)))
pong · gamehoster-commands/moveY/gamehoster-command-handler-frontend.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 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.

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 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.