Skip to main content

Getting Started

Every script module is created with a single call:

import { defineModule, ModuleCategory } from "@protohax/userscript";

defineModule(meta, build);
  • meta — the module's static identity (name, category, …). See Modules.
  • build — a function that declares the module's options and returns a setup function.

The two phases

Authoring mirrors the host's own definition/instance split. There are two distinct phases, and understanding the boundary between them is the key to writing correct modules.

defineModule(
{ name: "Example", category: ModuleCategory.Client },

// ── Phase 1: build(options) — runs ONCE at registration ──
(options) => {
// Declare options here. These are shared by every session.
const enabled = options.boolean("Feature", true);

// ── Phase 2: setup(ctx) — runs PER SESSION ──
return (ctx) => {
// Wire listeners against `ctx`. A fresh ctx is created for each
// session the module runs in.
ctx.on("tick", () => {
if (enabled.value) doSomething(ctx);
});
};
},
);

Phase 1 — build(options)

Runs once, when the module is registered with the client. Its job is to declare the options the module exposes in the menu. Because it runs once, the option handles it returns are shared across every session — your listeners can close over them, and reading option.value inside a listener always sees the current menu value.

build must return the setup function.

Phase 2 — setup(ctx)

Runs once per session (a session is one connection to a server). It receives a fresh ModuleContextctx — whose session is that session. Everything you subscribe through ctx is automatically bound and torn down with the session; you never unsubscribe by hand.

The context (ctx)

ctx is how a module observes and reacts. Its members:

MemberPurpose
ctx.sessionThe live GameSession — entities, world, items, connection.
ctx.on(event, cb)Subscribe to a game event.
ctx.onPacket(name, cb)Subscribe to an inbound packet.
ctx.onInjectedPacket(name, cb)Subscribe to a locally-injected packet.
ctx.onEnable(cb)Run cb each time the module becomes enabled.
ctx.onDisable(cb)Run cb each time the module becomes disabled.

Listeners only fire while enabled

A listener wired through ctx.on / ctx.onPacket / ctx.onInjectedPacket only fires while the module is toggled on. You do not need to guard event handlers with an "am I enabled?" check — being disabled silences them.

onEnable / onDisable fire on the transitions, which is where you put setup/teardown that must happen each time the user flips the toggle (resetting state, sending a one-shot packet, etc.).

return (ctx) => {
const player = ctx.session.entityState.localPlayer;

ctx.onEnable(() => console.log("turned on"));
ctx.onDisable(() => console.log("turned off"));

// Fires every tick, but only while enabled.
ctx.on("tick", () => player.jump());
};

Errors are contained

If your setup function throws, or a lifecycle hook throws, the error is logged to the console and does not break the session for other modules. A broken script degrades gracefully rather than taking the client down.

Next