@protohax/userscript
v0.5.0
Published
TypeScript definitions for the ProtoHax userscript API.
Readme
@protohax/userscript
TypeScript definitions for the ProtoHax userscript API.
With a userscript, you write your own ProtoHax client modules in TypeScript. You give each module a name and options, then react to what happens in the game:
- game events and packets
- entities and the world
- the local player and their inventory
- the raw packet connection
Your modules show up in the client menu under the Script category, next to the built-in ones.
This package only contains types: a single
index.d.ts, with no runtime code. The ProtoHax client provides the real implementation when your script runs. Imports likedefineModule,moduleManager, the entity classes,nbtandutilsresolve to the client's own objects at load time, so your script and the client share the same game state.
Install
The easiest way to start is the template. It comes with a bundler, a deploy script and an example module:
git clone https://github.com/hax0r31337/ProtoHax-UserScript-Template my-scriptOr add the types to an existing project:
npm install --save-dev @protohax/userscripttsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true
}
}
skipLibCheck: trueis recommended. Versions before 0.5 need it, because their declarations include a harmless duplicate declaration from the client's packet definitions. Your own code is fully type-checked either way.
Building
Your script must build to a single ES module that keeps
@protohax/userscript as an external import. That import is how your code
connects to the client's objects, so don't bundle it. With rollup, that means:
external: ["@protohax/userscript"]output.format: "es"
Put 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 and shared by every session. Each key is both the name on
// ctx.options and the label shown 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 typed options on ctx.options.
(ctx) => {
// The game's own movement tick, fired natively before the player moves,
// so position and motion you set here apply to this tick.
ctx.on("movement_tick", (state) => {
state.strafe(ctx.options.speed.value, 1);
});
},
);A module has two parts:
- The schema is read once, when the module is registered. It declares
the options, including nested
childoptions,groupdrawers andmodes. - The setup function runs once per session with a fresh
ctx, and your options arrive typed onctx.options. Listeners you add throughctxonly fire while the module is enabled, and they're removed automatically when the session ends.
When a choice needs its own options or behavior, declare it as a mode. A mode with a setup function gets its own per-session listeners, which only fire while the module is enabled and that mode is 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 older builder form, defineModule(meta, (options) => (ctx) => { ... }),
still works for existing scripts.
What you can access
Everything starts from ctx.session (a GameSession):
| Area | Entry point |
| --- | --- |
| Entities & players | ctx.session.entityState — entities, playerList, localPlayer |
| Local player actions | ctx.session.entityState.localPlayer — interactEntity(), interactBlock(), swingItem(), breakingController, rotationScheduler, … |
| Movement | ctx.on("movement_tick", …) hands you a MovementState — strafe(), jump(), setMotion(), and SprintIntent / SneakIntent requests (docs) |
| Input | ctx.on("input_tick", …) hands you an InputState — override the move keys (MoveKey), stick and sneak (docs) |
| World & blocks | ctx.session.levelState — getBlock(), rayTrace(), getChunk(), chunkStorage, … |
| 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 |
| Audio | registerAudio(bytes) hands you an AudioClip — play(), pause(), stop(), volume, looping, played by the client UI (docs) |
| Math / NBT | utils (vectors, AABB, A* pathfinding, hashing, …) and nbt (patched prismarine-nbt) |
Listen to game events with ctx.on(name, cb), and to packets with
ctx.onPacket(name, cb) or ctx.onInjectedPacket(name, cb). Every event name,
packet name and payload is typed.
Handler parameters are inferred, so you rarely need to write a packet type
yourself. When you do (say, for a helper that takes a payload, or a field stored
in a variable), all the protodef types are 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");
}
const origin: packets.vec3f = { x: 0, y: 0, z: 0 };Documentation
Full guides and API reference: userscript.protohax.net.
