@protohax/userscript
v0.4.1
Published
TypeScript definitions for the ProtoHax userscript API.
Readme
@protohax/userscript
TypeScript definitions for the ProtoHax userscript API.
Write your own ProtoHax client modules in TypeScript. A userscript declares a module — its name and options — and wires listeners against the live game session: game events, packets, the entity/world model, the local player, inventory, and the raw packet connection. Modules you author this way appear in the client menu under the Script category, alongside the built-in ones.
This package is types-only. It ships a single
index.d.tsand no runtime code. You install it for the types; the ProtoHax client supplies the implementation when your script runs. The values you import —defineModule,moduleManager, the entity classes,nbt,utils— resolve to the host's live singletons at load time, so your script and the client share one instance of the game state.
Install
Start from the template — bundler, deploy script, and example modules included:
git clone https://github.com/hax0r31337/ProtoHax-UserScript-Template my-scriptOr add it to an existing project:
npm install --save-dev @protohax/userscripttsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true
}
}
skipLibCheck: trueis required — the published declarations vendor the host's packet definitions verbatim, which contain a harmless duplicate declaration. Your own code is still fully type-checked.
A script ships as a single ES module with @protohax/userscript left
external — that surviving import is what binds your code to the host's live
singletons. In rollup terms: external: ["@protohax/userscript"] and
output.format: "es". Drop the built file in the client's scripts folder
(%APPDATA%\ProtoHax\scripts on Windows).
Quick start
import { defineModule } from "@protohax/userscript";
defineModule(
{ name: "AutoSprint" },
{
// Declared once (shared across every session); keys become the handle
// names on ctx.options and the display names in the menu.
speed: { type: "number", def: 1, min: 0, max: 5, step: 0.1 },
rotate: { type: "boolean", def: true, child: {
smoothing: { type: "number", def: 10, min: 1, max: 20, step: 1 },
} },
},
// Runs once per session with the typed handles on ctx.options.
(ctx) => {
// "movement_tick" is the game's own tick, fired from native before it
// moves — position and motion written on the state land in that tick.
ctx.on("movement_tick", (state) => {
state.strafe(ctx.options.speed.value, 1);
});
},
);Two phases, mirroring the host's definition/instance split:
- The schema is interpreted once at registration — it declares the
module's options, nested
childoptions,groupdrawers, andmodes. - The setup function runs once per session with a fresh
ctx; the live option handles arrive typed onctx.options. Listeners wired throughctxfire only while the module is enabled and are torn down with the session automatically.
Choices that carry their own options and behavior are declared as modes; a mode with a setup function gets its own per-session listeners, gated on the module being enabled and that mode being selected:
import { defineModule, mode } from "@protohax/userscript";
defineModule(
{ name: "Velocity" },
{
mode: { type: "modes", modes: {
Vanilla: mode((ctx) => {
ctx.onPacket("set_entity_motion", (packet) => { packet.isCancelled = true; });
}),
Reversal: mode({
ticks: { type: "number", def: 2, min: 0, max: 5, step: 1 },
}, (ctx) => {
ctx.on("tick", () => { /* ctx.options.ticks.value */ });
}),
} },
},
() => { },
);The legacy builder form — defineModule(meta, (options) => (ctx) => { ... }) —
is still supported for existing scripts.
What you can reach
Everything hangs off ctx.session (a GameSession):
| Area | Entry point |
| --- | --- |
| Entities & players | ctx.session.entityState — entities, playerList, localPlayer |
| Local player actions | ctx.session.entityState.localPlayer — jump(), strafe(), interactEntity(), breakingController, rotationScheduler, … |
| World & blocks | ctx.session.levelState — getBlock(), rayTrace(), getChunk(), … |
| Items & inventory | ctx.session.itemState — controller.moveItem(), openContainer, item registry |
| Packets | ctx.session.connection — sendOutgoingPacket(), queue checkers, flushHeld() |
| Options | ctx.options (typed from your schema), each Option<T> handle, nested trees via option.child / configurable.getOption() |
| Other modules | moduleManager — inspect and observe any module |
| Math / NBT | utils (vectors, AABB, hashing, …) and nbt (patched prismarine-nbt) |
Subscribe to game events with ctx.on(name, cb) and to packets with
ctx.onPacket(name, cb) / ctx.onInjectedPacket(name, cb) — every event and
packet name and payload is typed.
Handler parameters are inferred, so you rarely need to name a packet type. When
you do — a helper that takes a payload, a field pulled out into a variable — the
whole protodef surface is exported under the packets namespace:
import type { packets } from "@protohax/userscript";
function isSprinting(p: packets.packet_player_auth_input): boolean {
return p.input_data?.includes("sprint_down") ?? false;
}
const origin: packets.vec3f = { x: 0, y: 0, z: 0 };Documentation
Full guides and API reference: userscript.protohax.net.
