Player

A player is the connected individual: the person on the other end of a session. A player is nothing more than two pieces of state: the session-fixed join data they supply on connect (a display name, say) and their per-instance state, the fields they carry inside whatever instance they are in. There is one player type, so the player files gather into a single gamehoster-player/ folder inside the game.

The join data is settled once, on connect, and carries unchanged for the whole session. The per-instance state is created when the player is routed into an instance and stepped forward each tick. The files below are the whole of a player: the two schemas that shape that state, the scripts that route the player in, tear them down and update them, one optional script that scopes what each viewer sees, and an optional client-only body that predicts your own object ahead of the server.

<gamehoster-config-contentRoot>/
  game.gamehoster.org/
    gamehoster-games/
      asteroids/
        gamehoster-player/
          gamehoster-player-join-schema.json
          gamehoster-player-schema.json
          gamehoster-player-join.js
          gamehoster-player-leave.js
          gamehoster-player-view.js
          gamehoster-player-update.js
          gamehoster-player-predict.js
The gamehoster-player/ folder inside a game: the two schemas, the lifecycle and update scripts, the optional view, and the client predict body.

Every .js file below is a raw function body: no export, no function header. It starts straight into JavaScript and reaches the world through the Gamehoster.* library. Which library calls are in scope depends on the file, and each section lists exactly what that one can call. Four are always present: Gamehoster.Time() (ms), Gamehoster.Tick() (integer), Gamehoster.Random() ([0,1), predictable, seeded by the tick and the running actor so it is deterministic), and Gamehoster.Entropy() ([0,1), unpredictable true randomness for authoritative decisions). Every body here runs on the server, so both randoms are in scope; the only difference is that an Entropy() outcome must be written into state to reach a client, since a client can never reproduce it. The one exception is the optional gamehoster-player-predict.js, which runs in the client and has its own smaller, prediction-only surface, set out in its own section below.

A body learns the ids in scope from Gamehoster.Context, a record whose slots are filled for that body's context: Gamehoster.Context.playerId (the player it acts as), Gamehoster.Context.instanceId (the instance it is in) and Gamehoster.Context.entityId (the entity it acts as); a slot that doesn't apply is undefined. Every state op names its id, Gamehoster.Player.State.Get(id, "field"), Gamehoster.Instance.State.Set(id, "field", value) and so on, so a body's first lines are usually const me = Gamehoster.Context.playerId and const here = Gamehoster.Context.instanceId.

Writes do not apply the instant you make them. Each body writes into a scratch buffer (your own reads see your own writes) and, when it returns, the engine flushes that buffer. A command handler is bounded: it may write only the fields its writes manifest lists, and a write to anything else is dropped. The other server bodies here (join, leave, view, and the update body) are the server's own code and write freely. A dropped write to a real field is a silent no-op (the authoritative value stands); a write to a field that does not exist is a contained error, logged and ignored. A field's visibility decides only what is sent to a client, never what a body may write.

Contents

The player folder holds two schemas, the lifecycle and update scripts, an optional view script, and an optional client-only predict body.

NameTypeDescription
gamehoster-player-join-schema.jsonfileThe fixed data a player supplies on connect.
gamehoster-player-schema.jsonfileThe player's per-instance state schema.
gamehoster-player-join.jsfileOn connect: route the player into an instance and set them up.
gamehoster-player-leave.jsfileClean up when a player leaves.
gamehoster-player-view.jsfileOptional: which players and entities are relevant to a viewer.
gamehoster-player-update.jsfileOptional: the player's one authoritative per-tick update, run on the server.
gamehoster-player-predict.jsfileOptional: client-only prediction of your own object, run in the browser.

gamehoster-player-join-schema.json

The fixed data a player supplies on connect: the one thing about them that is settled for the whole session, before any instance is involved. It is an array of field objects (the names are not used as keys), each with a -name, a -type, and an optional -default. Where a default is omitted the player must supply the value; where it is given, that value stands in when they don't. Join data is fixed, never state, so these entries carry no visibility. Pong asks only for a name:

[
  {
    "gamehoster-player-join-schema-name": "name",
    "gamehoster-player-join-schema-type": "string"
  }
]
pong · gamehoster-player/gamehoster-player-join-schema.json: one required field, no default
KeyTypeMeaning
gamehoster-player-join-schema-namestringThe field's name.
gamehoster-player-join-schema-typestringIts type.
gamehoster-player-join-schema-defaultoptionalA default value; omit it to force the player to supply one.

gamehoster-player-schema.json

The player's per-instance state: the fields they carry inside an instance. Like the join schema it is an array of field objects (a name is never used as an object key) each carrying a fully-qualified -name and -type. State fields additionally declare -visibility (public, streamed to everyone who can see the object, or private, streamed only to the player it belongs to), with an optional -default; a bounded numeric field may also carry the -min/-max/-decimals range hints. In pong a player is their name and side, whether they're ready, their score, and the paddle position y:

[
  {
    "gamehoster-player-schema-name": "name",
    "gamehoster-player-schema-type": "string",
    "gamehoster-player-schema-visibility": "public"
  },
  {
    "gamehoster-player-schema-name": "side",
    "gamehoster-player-schema-type": "string",
    "gamehoster-player-schema-visibility": "public"
  },
  {
    "gamehoster-player-schema-name": "ready",
    "gamehoster-player-schema-type": "bool",
    "gamehoster-player-schema-default": false,
    "gamehoster-player-schema-visibility": "public"
  },
  {
    "gamehoster-player-schema-name": "score",
    "gamehoster-player-schema-type": "int",
    "gamehoster-player-schema-default": 0,
    "gamehoster-player-schema-visibility": "public"
  },
  {
    "gamehoster-player-schema-name": "y",
    "gamehoster-player-schema-type": "number",
    "gamehoster-player-schema-default": 240,
    "gamehoster-player-schema-visibility": "public",
    "gamehoster-player-schema-min": 0,
    "gamehoster-player-schema-max": 480,
    "gamehoster-player-schema-decimals": 1
  }
]
pong · gamehoster-player/gamehoster-player-schema.json
FieldTypevisibilityWhat it is
namestringpublicDisplay name, copied from the player's join data when they enter.
sidestringpublicWhich paddle they hold: "left" or "right", assigned to whichever side is free.
readyboolpublicWhether they've readied up; starts false.
scoreintpublicPoints this player has scored; starts 0.
ynumberpublicPaddle position, streamed to the opponent; the client eases it between updates so it stays smooth. Bounded 0480 to one decimal place with -min/-max/-decimals.

A field's visibility settles the whole of its networking: whether the value is sent to clients, and to whom. There are exactly two values.

visibilityHow the client gets it
publicThe default. Computed on the server and sent to every viewer the object is currently relevant to, whenever it changes. The client holds it and eases numeric fields between updates.
privateOwner-only: on a player, sent to that player alone and to nobody else. A private hand of cards, a personal timer only its holder should see.

private is only meaningful on a player, the one object with an owner; an instance or entity has none, so every one of their streamed fields is public. Pong's paddle y is public, so each player sees the other's paddle; a hidden hand would be private and reach only its holder.

State that no client should ever see is not a schema field at all. Per-player server-only data — a cooldown, an AI's target, matchmaking scratch — lives in the player's persistent bag, Gamehoster.Player.Persistent(id), with .Get() and .Set(value); .Get() returns the live object, so a body may mutate it in place. It is never typed, tracked, serialised or streamed, the private counterpart to the streamed schema state, and the same bag exists on the instance, each entity and each bot. Values you can recompute from public state (a bullet's velocity from its angle and speed) need not be stored at all.

A bounded numeric field may declare -min and -max for its range and, on a number, -decimals (a non-negative integer) for its significant decimal places. The engine uses them to validate writes and, in future, to encode the field compactly on the wire. An unbounded field — a tick counter, a running score — simply omits them.

These declarations govern only what crosses the wire. They are not a write rule: every server body may read any field it can name, and only a command is bounded in what it writes, by its writes manifest. How a drawn field is eased or snapped is finally the front end's choice; the Types reference lists the default per type, and a per-object -smooth.js body may override it.

gamehoster-player-join.js

Runs on the server the moment a player connects, and does two jobs. First it routes the player into an instance: it lists the live instances, filters them (pong wants one still waiting for an opponent that has room), and Joins that one, or Creates a fresh instance when none fits. Then it sets the player up: it reads their fixed join data, gives them whichever side is free, copies over their name, and centres their paddle.

// route: join a waiting game that has room, else start a new one
const open = Gamehoster.Instance.List().find(id =>
  Gamehoster.Instance.Info(id).players < Gamehoster.Instance.Info(id).capacity &&
  Gamehoster.Instance.State.Get(id, "phase") === "waiting")

Gamehoster.Instance.Join(open !== undefined ? open : Gamehoster.Instance.Create())

// set up: whichever side is free. Now that we have joined, Context.playerId names
// us and Context.instanceId our court (both undefined until the Join above).
const me = Gamehoster.Context.playerId
const here = Gamehoster.Context.instanceId
const taken = Gamehoster.Player.List()
  .filter(id => id !== me)
  .map(id => Gamehoster.Player.State.Get(id, "side"))

Gamehoster.Player.State.Set(me, "side", taken.includes("left") ? "right" : "left")
Gamehoster.Player.State.Set(me, "name", Gamehoster.Join.Get("name"))
Gamehoster.Player.State.Set(me, "score", 0)
Gamehoster.Player.State.Set(me, "ready", false)
Gamehoster.Player.State.Set(me, "y", Gamehoster.Instance.State.Get(here, "courtH") / 2)
pong · gamehoster-player/gamehoster-player-join.js: route the player into an instance, then fill their state
FunctionWhat it does
Gamehoster.Context.playerId · Gamehoster.Context.instanceIdThe ids in scope: the joining player, and their instance (which is undefined until the Instance.Join above, so read it after).
Gamehoster.Join.Get(name)A value the player supplied against the join schema.
Gamehoster.Instance.List()Ids of the live instances, to route between.
Gamehoster.Instance.Info(id){players, capacity, …} for an instance, to choose between them.
Gamehoster.Instance.State.Get(id, name)Read an instance's state field: e.g. to filter by phase.
Gamehoster.Instance.Join(id)Place this player into that instance.
Gamehoster.Instance.Create()Make a fresh instance and return its id.
Gamehoster.Player.List()Ids of the players in the instance.
Gamehoster.Player.State.Get(id, name)Read another player's state field.
Gamehoster.Player.State.Set(Gamehoster.Context.playerId, name, value)Fill the joining player's per-instance state.
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy()Clock (ms), tick number, predictable random in [0,1) (seeded by tick + actor), and unpredictable Entropy() for authoritative choices such as picking a starting side.

gamehoster-player-leave.js

Runs on the server when a player leaves, to clean up. Pong keeps no per-player resources beyond the paddle the engine reclaims, so it only drops the instance back to waiting if a match was in play.

const here = Gamehoster.Context.instanceId
if (Gamehoster.Instance.State.Get(here, "phase") === "playing")
  Gamehoster.Instance.State.Set(here, "phase", "waiting")
pong · gamehoster-player/gamehoster-player-leave.js: pausing the match when an opponent drops
FunctionWhat it does
Gamehoster.Context.playerId · Gamehoster.Context.instanceIdThe ids in scope: the leaving player and their instance.
Gamehoster.Instance.State.Get(id, name) · .Set(id, name, value)Read or write the instance's state field.
Gamehoster.Player.List()Ids of the players still in the instance.
Gamehoster.Player.State.Get(id, name) · .Set(id, name, value)Read or write a remaining player's state field: e.g. hand a leaver's resource to whoever is left.
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.Entity.Destroy(id)Remove an entity: e.g. a resource this player owned.
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy()Clock (ms), tick number, predictable random in [0,1) (seeded by tick + actor), and unpredictable Entropy().

gamehoster-player-view.js

An optional script that decides relevance: which players and entities a given viewer should receive at all. It is the outer gate in front of every field's visibility: if an object is not relevant to you, you receive none of its fields; inside a relevant object, each field still streams by its own visibility. This is how partial visibility (areas of interest, fog of war) is expressed.

The script is viewer-centric: Gamehoster.Context.playerId is the viewer, and the body runs on the backend at the send rate, once per viewer. It calls Gamehoster.View.Include(id) for each player or entity that should be relevant to that viewer, including ones about to become relevant, so they appear just before they're needed and nothing pops in late. If the file is absent, everything is relevant to everyone: full visibility. Pong, with two paddles and one ball in a single court, has nothing to hide, so it omits the file.

// A viewer sees players near them, plus any about to come into range (pre-fetch).
const me = Gamehoster.Context.playerId
const [mx, my] = [Gamehoster.Player.State.Get(me, "x"), Gamehoster.Player.State.Get(me, "y")]
const near = (x, y, r) => (x - mx) ** 2 + (y - my) ** 2 < r * r

for (const id of Gamehoster.Player.List()) {
  const x = Gamehoster.Player.State.Get(id, "x")
  const y = Gamehoster.Player.State.Get(id, "y")
  if (near(x, y, 600)) Gamehoster.View.Include(id)   // visible now, or soon (pre-fetch margin)
}

for (const id of Gamehoster.Entity.List("prop"))
  if (near(Gamehoster.Entity.State.Get(id, "x"), Gamehoster.Entity.State.Get(id, "y"), 600))
    Gamehoster.View.Include(id)
gamehoster-player-view.js: mark the players and entities relevant to this viewer (illustrative; pong has no view)
FunctionWhat it does
Gamehoster.Context.playerId · Gamehoster.Context.instanceIdThe ids in scope: the viewer this pass is scoping the world for, and their instance.
Gamehoster.View.Include(id)Mark a player or entity relevant to the viewer, so its fields sync to them.
Gamehoster.Player.List()Ids of the players in the instance, to test against the viewer.
Gamehoster.Player.State.Get(id, name)Read a player's state field: e.g. their position.
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.Instance.State.Get(id, name)Read the instance's state field: always relevant, so useful for tuning the test.
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy()Clock (ms), tick number, predictable random, and unpredictable random.

gamehoster-player-update.js

The player's own per-tick update: step 2 of the tick, where a player reads the previous state and writes its own. It is one authoritative body, run on the server, the only simulator. It reads any state it can name through Gamehoster.* and writes the player's own fields; reading an object outside this instance simply finds nothing, and a write to a field that does not exist is a contained error, never a throw. The client never runs it: the front end runs no game logic, it draws the public state the server streams it.

The body is optional, with no empty stub. A pong paddle has no autonomous per-tick behaviour (it moves only in response to the moveY command) so it ships no update file at all. A game whose ship moves itself each tick, like the drifting, thrusting ship in asteroids, puts that step here; its own client-side responsiveness lives separately in the predict body below.

A game that has not been converted may instead ship the legacy pair, gamehoster-player-update-frontend.js then gamehoster-player-update-backend.js: the server runs them front then back, both authoritative server code writing the player's own state. The single body above is the standard shape; the pair is still accepted for older games.

FunctionWhat it does
Gamehoster.Context.playerId · Gamehoster.Context.instanceIdThe ids in scope: this player and their instance.
Gamehoster.Player.State.Get(Gamehoster.Context.playerId, name) · .Set(Gamehoster.Context.playerId, name, value)Read and write this player's own state.
Gamehoster.Instance.State.Get(id, name)Read an instance state field.
Gamehoster.Player.List()Ids of the players in the 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, predictable random in [0,1) (seeded by tick + actor), and unpredictable Entropy() for authoritative choices.

gamehoster-player-predict.js

An optional, client-only body that predicts the local player's own object ahead of the server. Everything the client draws is normally interpolated a fixed buffer behind the server, which would add a round trip of lag to your own moves. The predict body closes that gap: the client runtime runs it once per tick for your own object, fed your live input, and in one body it simulates the object forward from that input and reconciles it toward the authoritative state, so your object renders at the present. It is opt-in and only for your own object; everything else stays interpolated, and a game without a predict body simply has no prediction. Only asteroids ships one.

It runs in the browser, not on the server, so it has a smaller, prediction-only surface: it owns just its own predicted copy of the object and never writes real game state, and it has no Entropy(), no spawn and no destroy. The renderer feeds it the live input each frame with conn.input(name, value) and reads its own object, already predicted, straight from conn.frame(); it writes no prediction of its own. The full asteroids body is on the asteroids player page.

FunctionWhat it does
Gamehoster.Context.playerId · Gamehoster.Context.instanceIdThe ids in scope: your own object and its instance.
Gamehoster.Input.Get(name)The live local input the renderer feeds this frame (thrust held, aim angle), the thing you simulate forward from.
Gamehoster.Authoritative.Get(id, field)The newest authoritative server value of a field: the target to reconcile toward.
Gamehoster.Lead()How many ticks the client clock leads the newest data, to extrapolate that target to the present.
Gamehoster.Player.State.Get(me, field) · .Set(me, field, value)Read and write your own predicted state: the client's own copy, never the server's.
Gamehoster.Instance.State.Get(here, field)Read a public instance state field: the tuning constants your motion needs.
Gamehoster.Tick()The present tick, the leading edge the prediction runs at.