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. |
Gamehoster.Context.botId | The 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
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-create.js | — | yes | — |
gamehoster-instance-update-*.js | — | yes | — |
gamehoster-entity-update-*.js | — | yes | yes |
gamehoster-bot-join.js | yes (the bot) | yes | — |
gamehoster-bot-update.js | yes (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.
| Call | What 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)
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.
| Call | Returns | Description |
|---|---|---|
Gamehoster.Instance.Persistent.Get() / .Set(value) | the live value | The current instance's bag — no id, addressed implicitly. |
Gamehoster.Player.Persistent(id).Get() / .Set(value) | the live value | One player's bag, by id. New. |
Gamehoster.Entity.Persistent(id).Get() / .Set(value) | the live value | One entity's bag, by id. New. |
Gamehoster.Bot.Persistent.Get() / .Set(value) | the live value | A 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:
| Call | Returns | Description |
|---|---|---|
Gamehoster.Instance.Persistent.Get() | the current value | The 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)
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.
| Body | May write |
|---|---|
| command handler | only 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-create | everything: 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.