The Context (ctx)
ctx is how your module watches the game and reacts to it. Your
setup function gets a
fresh one for each session. Here's what's on it:
| Member | Purpose |
|---|---|
ctx.session | The live GameSession: entities, world, items, connection. |
ctx.options | The live option handles, typed from the schema. |
ctx.on(event, cb) | Subscribe to a game event. |
ctx.onPacket(name, cb) | Subscribe to a packet on the real connection, in either direction. |
ctx.onInjectedPacket(name, cb) | Subscribe to a locally-injected packet. |
ctx.onEnable(cb) | Run cb each time the module is enabled. |
ctx.onDisable(cb) | Run cb each time the module is disabled. |
Listeners only fire while enabled
Listeners added with ctx.on, ctx.onPacket, or ctx.onInjectedPacket
only fire while the module is toggled on. You don't need an "am I enabled?"
check in your handlers; disabling the module silences them.
onEnable and onDisable fire on the switch itself. Use them for anything
that should happen each time the user flips the toggle, like resetting state or
sending a one-off packet.
(ctx) => {
ctx.onEnable(() => console.log("turned on"));
ctx.onDisable(() => console.log("turned off"));
// Every movement tick, but only while enabled.
ctx.on("movement_tick", (state) => state.jump());
};
The same rules apply inside an executable mode's setup, with one addition: its listeners and hooks also only fire while that mode is selected.
Per-session state
Setup runs fresh for each session, so a plain variable inside it already is per-session state. That's the default, and it needs nothing extra:
(ctx) => {
let ticks = 0; // one per session
ctx.on("tick", () => { ticks++; });
};
What a closure can't do is share state across several setup functions. For example, a module's own setup and its modes' setups each get their own closure.
For that, use utils.sessionLocal. It creates a typed store keyed by session;
every setup that calls it with ctx.session gets the same object:
import { utils } from "@protohax/userscript";
const combat = utils.sessionLocal(() => ({ ticks: 0, target: null as string | null }));
// in the module's setup AND any mode's setup:
(ctx) => {
ctx.on("tick", () => { combat(ctx.session).ticks++; });
};
Values are dropped automatically when the session ends. Keep in mind:
- It's per-session, not per-enable. State survives the module being toggled
off and on again. Reset it in
ctx.onEnableif that's not what you want. - Don't use a module-level
letinstead. That single variable would be shared by every session (and every copy of an Atomic module) at once.
Errors are contained
If your setup function or a lifecycle hook throws, the error is logged and doesn't break the session for other modules. A broken script fails gracefully instead of taking the client down. See Debugging for where the logs go on each platform.