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.

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-update-*.jsyes
gamehoster-entity-update-*.jsyesyes

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.

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.

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. This is why you can write shared prediction logic once and run it on both sides: the parts a client may not commit are simply dropped there.

BodyMay write
shared command handler (-frontend.js)only the fields listed in its writes manifest; a -backend.js handler writes like a backend body.
front-end body (-update-frontend.js)only the object's shared fields and its owner fields: the predicted subset.
backend / setup body (-update-backend.js, join, leave, startup)everything.

The two are the same logic at different scope: the backend runs the superset, the shared front-end body the subset. So a field's sync and audience double as an access rule, enforced here at flush rather than by hiding ids from you.