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.
| Slot | What it is |
|---|---|
Gamehoster.Context.playerId | The player this body acts as: the joining or leaving player, the viewer, the player being updated, or the sender of a command. |
Gamehoster.Context.instanceId | The instance this body is running inside. |
Gamehoster.Context.entityId | The 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
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.
| Body | playerId | instanceId | entityId |
|---|---|---|---|
gamehoster-game-startup.js | — | — | — |
gamehoster-player-join.js | yes | after Instance.Join | — |
gamehoster-player-leave.js | yes | yes | — |
gamehoster-player-view.js | yes (the viewer) | yes | — |
gamehoster-player-update-*.js | yes | yes | — |
gamehoster-command-handler-*.js | yes (the sender) | yes | — |
gamehoster-instance-update-*.js | — | yes | — |
gamehoster-entity-update-*.js | — | yes | yes |
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)
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.
| Body | May 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.