Contexts

Every .js file in a game is a raw body that reaches the world through the Gamehoster.* library. A body runs in a context that pins the ids in scope, and it reads them from Gamehoster.Context. Every state operation then takes an explicit id: you always say which object you mean.

Gamehoster.Context

A record of the ids the current body may act on. A slot is present when its object is in scope and undefined otherwise, so the context both hands you the ids and tells you what kind of body you are in.

SlotWhat it is
Gamehoster.Context.playerIdThe player this body acts as: the joining or leaving player, the viewer, the player being updated, or the sender of a command.
Gamehoster.Context.instanceIdThe instance this body is running inside.
Gamehoster.Context.entityIdThe entity this body acts as, in an entity update.
Gamehoster.Context.botIdThe bot this body acts as, in a bot body. Because a bot is a player, Gamehoster.Context.playerId returns the same id there too.

The usual first lines of a body name what it needs:

const me = Gamehoster.Context.playerId       // or Gamehoster.Context.entityId in an entity body
const here = Gamehoster.Context.instanceId
Naming the ids in scope

Which ids each body gets

The context is fixed by which file the body is: the engine populates only the slots that make sense for it.

BodyplayerIdinstanceIdentityId
gamehoster-game-startup.js
gamehoster-player-join.jsyesafter Instance.Join
gamehoster-player-leave.jsyesyes
gamehoster-player-view.jsyes (the viewer)yes
gamehoster-player-update-*.jsyesyes
gamehoster-command-handler.jsyes (the sender)yes
gamehoster-instance-create.jsyes
gamehoster-instance-update-*.jsyes
gamehoster-entity-update-*.jsyesyes
gamehoster-bot-join.jsyes (the bot)yes
gamehoster-bot-update.jsyes (the bot)yes

The instance-create body runs once, at the moment an instance is created — before its first tick and before any player is in it — with only Gamehoster.Context.instanceId in scope. It is the place to set an instance up before anyone can see it: seed its server-only persistent scratch with Instance.Persistent.Set(…), spawn the opening Entity set, or write initial Instance.State.

The join body is the one that changes mid-script: Gamehoster.Context.instanceId is undefined until the body routes the player in with Gamehoster.Instance.Join (or Create then Join). Read it after that call. This is not a special rule, just the honest truth that the player has no instance until you give them one.

The two bot bodies are the one place where reading and writing part ways. A bot is a socketless player, so both its join and update bodies get Gamehoster.Context.playerId (the bot's id, also Gamehoster.Context.botId) and Gamehoster.Context.instanceId, and they read the very same surface an instance or player update does. But a bot may only write through Gamehoster.Bot.Send — a command queued as itself, applied next tick through the game's own handler — and its private Gamehoster.Bot.Persistent memory; every direct write to player, entity or instance state is firewalled off, so a bot can only ever do what a real player could.

The client predict context

One body runs on the client, not the server: the gamehoster-player-predict.js predict body. The client runtime runs it each tick for the local player's own object, so its context names Gamehoster.Context.playerId (you) and Gamehoster.Context.instanceId (your instance). It reads from the client's snapshot store rather than the live world, and it may write only its own object's predicted state: a Set to any other id is ignored. Alongside the state ops it is handed three calls no server body has — Gamehoster.Input.Get(name), the live local input the renderer feeds through conn.input; Gamehoster.Authoritative.Get(id, field), the newest server value of any field, un-interpolated; and Gamehoster.Lead(), how many ticks the present leads that newest data, for extrapolating a known position to now. Gamehoster.Tick() here is the present (leading-edge) tick.

CallWhat it does
Gamehoster.Input.Get(name)A live local input value fed through conn.input (client only).
Gamehoster.Authoritative.Get(id, field)The newest authoritative value of any object's field, straight from the server (client only).
Gamehoster.Lead()Ticks the present leads the newest authoritative data (client only).
Gamehoster.Player.State.Get(id, name)Read any object's state: your own predicted state for yourself, the newest authoritative state for anyone else.
Gamehoster.Player.State.Set(Gamehoster.Context.playerId, name, value)Write your own object's predicted state; a write to any other id is ignored.
Gamehoster.Instance.State.Get(id, name) · Gamehoster.Entity.State.Get(id, name) · Gamehoster.Tick()Read the newest authoritative instance and entity state, and the present tick.

Everything by id

Because the context only names the ids, every state op takes one explicitly. The current instance is not special: you pass Gamehoster.Context.instanceId like any other id.

Gamehoster.Instance.State.Get(instanceId, "field")
Gamehoster.Instance.State.Set(instanceId, "field", value)

Gamehoster.Player.State.Get(playerId, "field")
Gamehoster.Player.State.Set(playerId, "field", value)

Gamehoster.Entity.State.Get(entityId, "field")
Gamehoster.Entity.State.Set(entityId, "field", value)
State is always addressed by an explicit id

Ids you did not start with come from the collection calls: Gamehoster.Instance.List(), Gamehoster.Player.List() and Gamehoster.Entity.List(type) hand back ids to pass straight back in. This keeps one rule for the whole library: to touch a thing, name it.

Persistent bags: server-only scratch

A persistent bag is free-form, server-only state an object keeps between ticks. Unlike schema state it is never serialised and never sent to any client, so it can hold a Map, a class instance, or any structure you like — a spatial grid, an object pool, a velocity, a cooldown, a timer, an AI's memory. This is where state that was once a server-only schema field now lives: schema fields are for what streams; the persistent bag is for what never leaves the server. (A value that is derivable from public state, like a bullet's velocity from its angle and speed, is usually just recomputed and stored nowhere.)

There is one bag per object. Gamehoster.Player.Persistent(id) and Gamehoster.Entity.Persistent(id) are new: they take an explicit id like the object's state ops, and each returns a { Get, Set } handle for that object's bag. A bot has Gamehoster.Bot.Persistent (its own, no id). The instance's own bag, Gamehoster.Instance.Persistent, is the one that breaks the by-id rule on purpose: because there is exactly one per instance and the body already runs inside that instance, it takes no id, acting on Gamehoster.Context.instanceId implicitly.

CallReturnsDescription
Gamehoster.Instance.Persistent.Get() / .Set(value)the live valueThe current instance's bag — no id, addressed implicitly.
Gamehoster.Player.Persistent(id).Get() / .Set(value)the live valueOne player's bag, by id. New.
Gamehoster.Entity.Persistent(id).Get() / .Set(value)the live valueOne entity's bag, by id. New.
Gamehoster.Bot.Persistent.Get() / .Set(value)the live valueA bot's own private memory (in a bot body).

In every case Get returns the live object, so you may mutate whatever it hands back in place without calling Set again; Set replaces the value. It is undefined until something sets it. The instance bag, addressed with no id, in detail:

CallReturnsDescription
Gamehoster.Instance.Persistent.Get()the current valueThe live persistent value for the instance in context. undefined until something sets it. Persists across ticks; never sent to clients.
Gamehoster.Instance.Persistent.Set(value)Replaces the persistent value. It is mutable in place — you may mutate whatever Get() returns without calling Set again — and is never serialised, so it may contain Maps or any structure.
// initialise once, in gamehoster-instance-create.js
Gamehoster.Instance.Persistent.Set(new Map())

// later, in an update body — mutate in place, no Set needed
const grid = Gamehoster.Instance.Persistent.Get()
grid.set(key, value)
Server-only scratch for the current instance, addressed with no id

The write firewall

A Set does not take effect immediately. Each body writes into a buffer (reads within the body see your own writes), and when the body returns the engine flushes that buffer against the body's allowed-write set: it applies each write the body was allowed to make, and whatever is left over was a write it was not permitted here. A leftover 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 dropped, so a stray write can only ever hurt itself, never the instance.

BodyMay write
command handleronly the fields listed in its writes manifest (the sender's own player.<field> and named instance.<field>s).
update body (-update.js, or the legacy -update-frontend.js / -update-backend.js pair), join, leave, view, startup, instance-createeverything: these are trusted server code.

Only commands are bounded. Every other body is the server's own trusted code and writes freely, so the firewall's real job is to hold each command to the fields it declared and to contain a write to a field that does not exist. It is an access rule enforced here at flush, rather than by hiding ids from you.