Email copied to clipboard
Browse scripting docs

Scripting

Scripting

Write custom modules, macros, and helpers in JavaScript with the same functions that power Polinex's existing features. The pages below run from first principles through to the full API reference, read straight through, or jump to a namespace from the sidebar.

Introduction

Polinex scripting lets you write your own modules, macros, and helpers in plain JavaScript. Your scripts get the same building blocks the client itself runs on (the macro state machine, combat bots, pathfinding, rotations, failsafes, every setting type, the HUD, Visuals etc) and they show up in the in-game GUI alongside the built-in features, with settings and keybinds that look and behave exactly like native ones.

Everything runs inside a sandboxed JavaScript engine embedded in the client. There is no nothing to compile, and no rebuilding the mod drop a .js file into your scripts folder, reload it in game, and its active. If you can write a little JavaScript, you can make scripts.

Next: Getting Started

Head on to the next section which will break getting started down

Getting Started

Scripts are plain JavaScript that define real Polinex modules and macros using the client's own building blocks. This page gets a first script running, where the files live, how to edit them, and the commands that load them.

Where scripts live

Every script lives under polinex/scripts/ in your Minecraft folder. A script is one of two shapes:

A single file: drop polinex/scripts/my_script.js and that file is the whole script. Or a folder: polinex/scripts/my_script/ with an entry file, which lets you split a bigger script across files that pull each other in with require(...). The folder name is the script's name.

For a folder, Polinex resolves the entry file in this order: main.js, then index.js, then <foldername>.js, and finally the only .js file if the folder contains exactly one. The in-game editor creates every new script as a folder that starts with a single main.js.

polinex/scripts/
├── polinex.d.ts          # shipped type definitions (rewritten on launch)
├── jsconfig.json         # editor config (rewritten on launch)
├── chat_logger.js        # a single-file script
└── auto_announcer/       # a folder script
    ├── main.js           # entry file
    └── messages.js       # required by main.js

Autocomplete & the type file

On launch Polinex writes polinex.d.ts and a jsconfig.json into the scripts folder. That type file describes the API shipped with that client version for external editor hints. Keep the website and your installed client version in sync.

Autocomplete everywhere

The reference line drives IntelliSense in external IDEs. The built-in editor uses its own API completion metadata. Point VSCode, Webstorm or any trending AI slop IDE you'd like to use at the polinex/scripts/ folder and you get the same full API completion, signature help, and inline docs.

The in-game editor

I know pretty much all of you will use VSC or something similar but i find this as a nice touch for just changing 1 variable or such without tabbing out especially without dual monitors, nothing wrong with using it as your main as it was built for that but again the convenience of modern IDEs will probably outweight anything this provides

Run .scripteditor (aliases .sedit and .pnedit) to open the built-in editor without leaving the game. It has a file-tree sidebar for browsing, creating, and renaming files, JS syntax highlighting, autocomplete, and format-on-save.

Ctrl+S saves the current file; Ctrl+Enter saves and reloads every script so your change takes effect immediately; Esc goes back. It does support terminal instances so u can spin up claude / codex inside minecraft if you wish to (i know 99% of users will be vibecoding so hf)

Managing scripts from chat

The /pnxscript command manages scripts from the chat box:

pnxscript reload          reload every script from disk
pnxscript list            list all discovered scripts
pnxscript info            show script stats
pnxscript run <name>      toggle a script on or off
pnxscript stop <name>     stop a specific script

reloadis the one you'll use most editing outside the game. It re-reads every file in polinex/scripts/. Inside the built-in editor, Ctrl+Enter does the same reload for you. or you can just use the reload button on the top right :D

External edits and developer mode

Local scripts are checked against the content you approved. If either editor changes a local script, reload asks for review before running the changed code unless developer mode is enabled. To skip that review during your own development loop, right-click an already-approved local script in the script picker and choose Enable developer mode. The confirmation explains that future external edits under that script folder execute when scripts reload. The picker marks enabled scripts and lets you disable the mode from the same menu.

Only enable this for code you control

Developer mode is stored per local script and path. It does not apply to downloaded or private hub scripts, and it does not approve a new script or a clone. Review the code before enabling it; every later external edit in that script folder can run without another prompt.

Your first script

Here is a VERY simple script, it registers a module and logs every chat line it receives. Save it as polinex/scripts/chat_logger/main.js (or paste it into a new script in the editor).

/// <reference path="../polinex.d.ts" />

const mod = module.create({
  name: "Chat Logger",
  description: "Prints every chat line to the script console.",
});

on("chatReceive", (message) => {
  script.chat("chat> " + message);
});

Reload with .pnxscript reload, then find Chat Logger under the Scripts category in the ClickGUI and toggle it on. Your script is always loaded, but its hooks only fire while its main module is enabled (the first module you create is the main one by default), so nothing logs until the toggle is on. With it on, ull see it detect every chat message :)

From here, see Events for the full list of hooks, Settings for adding controls to your module, and Creating modules for the rest of the module API.

Core Concepts

Events

You interact with the game by registering handlers with on(event, handler). Each event has a name and a fixed handler shape. You can register more than one handler for the same event. they run in registration order.

// ik this doesnt apply to sb lol
on("tick", () => {
  if (player.health() < 5) player.sendMessage("low hp");
});

on("chatReceive", (message, event) => {
  script.log("chat: " + message);
});

Available events

The full set of event names, grouped by what they cover:

Ticking & rendering: tick, motion, render2d, render3d, worldRenderLast.

Packets: packetReceive, packetSend (handler gets a ScriptPacketEvent; see the Packets section).

Chat: chatReceive (message, event), chatSend (command, event), actionBar (message, formatted).

World & player: worldLoad, death (message, formatted), entitySpawn / entityDeath (entity), serverDisconnect (reason, reasonFormatted).

GUI: guiOpen (name, title), guiClose.

Skyblock: purseChange (coins, purse, reason), itemGain (itemId, amount, source).

Module toggle: enable / disable. When your script exports a module these follow that module's toggle; otherwise they fire on script load/unload.

Cancellable events

Packet and chat events hand you an event object you can no-op. Call event.cancel() to stop the packet or message from going through, and event.isCanceled() to check whether an earlier handler already did. This works for packetReceive, packetSend, the event arg of chatReceive, and the event arg of chatSend.

// hide a spammy message
on("chatReceive", (message, event) => {
  if (message.includes("Explosion")) event.cancel();
});

// block one of your own outgoing commands idfk u can cancel packets too!!!
on("chatSend", (command, event) => {
  if (command === "/dangerous") event.cancel();
});

Hooks and your main module

Your script is always loaded, but its event hooks only fire while its main module is enabled. The first module you create is the main one by default; call module.export(handle)to choose a different one. Until that module's toggle is on, none of your on(...) handlers run.

No guard needed

Because hooks are gated by the main module, you DO NOT need an if (!mod.enabled()) return; at the top of every handler. If your script has no module at all, its hooks run for as long as the script is loaded. avoid this please :( i put thought into the system :(

Timers & sleep

Awaiting time

In an async function you can pause with await sleep(ms) (real milliseconds) or await sleepTicks(ticks) (20 ticks = 1 second). Both resolve on the client thread.

async function pulse() {
  player.sendMessage("start");
  await sleep(500);       // 500ms
  await sleepTicks(20);   // 1 second
  player.sendMessage("done");
}

Delayed and repeating callbacks

Use setTimeout(fn, ms?)(ms defaults to 0, with a 1 ms minimum) and setInterval(fn, ms?) (ms defaults to 50). Intervals have a 10 ms minimum. Each returns a numeric id you cancel with clearTimeout(id) or clearInterval(id)

The script. adds tick-based timers: script.after(ticks, fn) runs once after N × 50ms, script.every(ticks, fn) repeats until cleared, and both return an id you cancel with script.clearTimer(id).

const id = setInterval(() => script.log("tick"), 1000);
// later: clearInterval(id);

script.after(40, () => script.log("2s later"));   // 40 ticks
const loop = script.every(20, () => check());     // every 1s
// script.clearTimer(loop);

Pausing with your module

Timer deadlines use wall-clock time. Callbacks are deferred while the main module is off; elapsed time is not frozen. If a timer becomes due while disabled, it can run soon after re-enabling instead of waiting its original remaining delay. A pending sleep resumes when its deferred timer is allowed to run. Repeating callbacks do not replay every missed interval. Clear a timer on disable when that delayed action should be discarded rather than resumed.

Cleared on unload

Every timer is canceled when the owning script is unloaded or reloaded. Anything you want stopped sooner than that (an interval, a repeating script.every) you should clear explicitly, ideally from your module's onDisable.

Threading & performance

Tick, rendering, and script timer callbacks run on the client thread. Packet callbacks can run on the thread that posts the event, including the network thread. The script context serializes JavaScript execution; it does not move every callback onto the client thread.

Cancel packets synchronously

Call event.cancel() while handling the event, before the packet continues. An awaited sleep or timer is too late to cancel that original event. Keep packet handlers short, and copy the values you need before scheduling later work.

on("chatReceive", (message, event) => {
  if (message === "[example] hide this line") event.cancel();
  if (message.includes("[example] route ready")) {
    const text = message;
    setTimeout(() => script.log("On client thread: " + text), 0);
  }
});

Movement and path queries

Start or retarget navigation, drive Etherwarp, submit path queries, and read routes.selected() from a client-thread callback such as tick or a script timer. These APIs reject off-thread calls instead of waiting on the client while holding the script context lock. Use setTimeout to schedule from a packet or background capture callback.

Async planning is available

nav.findPathAsync and etherwarp.findPathAsync run the planner on Kotlin workers and return polling handles. JavaScript still cannot create its own threads. Scanners and world captures also have existing background implementations. See Path queries.

Synchronous path queries and large JS loops can still stall their calling thread. A native search budget excludes worker queue and mutex waits. Submit once, poll on later ticks, and reuse cached scan results instead of scanning or replanning every tick.

Path queries

nav.findPathAsync and etherwarp.findPathAsync return the same PathQuery handle. Submit on the client thread, then poll without a busy loop.

Handle methods

status(): "pending" | "found" | "no_path" | "unavailable" | "error" | "cancelled" | "stale"

pending: queued/running; found: usable result; no_path: no route result, possibly search limits/budget or native failure; unavailable: missing world/native/cache or query capacity; error: planner error; cancelled: explicitly discarded; stale: world/location/script generation changed.

finished(): boolean

True for every state except pending. Unlike nav.finished(), this describes a query, not movement.

failed(): boolean

True for terminal states other than found. False while pending.

error(): string | null

Explanation when available; cancelled/stale may have no message.

result(): PathNode[] | null

Result only while status is found. Returns null for pending, failure, cancellation, and stale handles. Array.from converts host arrays/lists for JS array operations.

cancel(): void

Discards the query result. It cannot interrupt a native call that is already executing.

At most eight queries can be in flight per script. Cancelled native calls keep their slot until they actually return. Disable, unload, world change, and disconnect invalidate old results. Keep local handles reset in lifecycle hooks so a re-enabled module can issue a fresh query.

The walking planner accepts allowEtherwarp, not budgetMs. The Etherwarp planner accepts etherOnly and budgetMs. Do not pass (target, options): the two-argument overload is (from, target); use (null, target, options) instead.

Example: replace a pending preview

// Call from tick or a script timer with a support-block goal.
let pending = null;
function previewWarpRoute(goal) {
  if (pending) pending.cancel();
  pending = etherwarp.findPathAsync(null, goal, {
    etherOnly: true,
    budgetMs: 150,
  });
}
on("tick", () => {
  if (!pending || !pending.finished()) return;
  const query = pending;
  pending = null;
  if (query.status() === "found") {
    const nodes = Array.from(query.result());
    script.log("Preview has " + nodes.length + " nodes");
    // Planning alone does not move you.
  } else {
    script.warn(query.status() + ": " + (query.error() || "no result"));
  }
});
function cancelPreview() {
  if (pending) pending.cancel();
  pending = null;
}
on("disable", cancelPreview);
on("worldLoad", cancelPreview);
on("serverDisconnect", cancelPreview);

Multi-file scripts

A script can be a single flat file, or a whole folder. Once it grows, split it into several files that import each other with require(...), just like NJS.

Folder scripts

Put your files in polinex/scripts/my_script/ and the folder name becomes the script's name. The engine picks the entry file: main.js, or index.js, or <foldername>.js, or the only .jsif there's just one.

require and module.exports

This is just basic JS you should know but. Inside the entry file, load a sibling with a relative path. require("./util.js") (or require("./util")) returns whatever that file assigned to module.exports. You can also set individual properties on exports.

// main.js  (folder entry point)
const messages = require("./messages.js");   // or "./messages"

on("chatReceive", (msg) => {
  if (msg === "!hi") player.sendMessage(messages.random());
});
// messages.js
const lines = ["hey", "yo", "hello"];

module.exports = {
  random: () => math.pick(lines),
};

Sandboxed to your folder

requireonly reads files inside the script's own folder. Relative paths can't escape it, and there's no access to the filesystem, network, or other scripts. Use it to keep a big script tidy, not to try and reach outside the sandbox :(.

Persistent store

Every script has its own small key/value store. It survives reloads and game restarts, so it's where you keep any state your script wants to remember.

The store API

script.get(key) returns the stored string or null if unset; script.getOr(key, fallback) returns the fallback instead of null. script.set(key, value) writes, script.has(key) checks existence, script.remove(key) deletes a key, and script.keys()lists every key you've stored.

script.set("greeting", "hello");
script.get("greeting");       // "hello"  (null if unset)
script.getOr("count", "0");   // fallback when the key is missing
script.has("greeting");       // true
script.keys();                // ["greeting"]
script.remove("greeting");

Values are strings

The store holds strings only, so encode anything else yourself. Parse a number back with parseInt / parseFloat, and turn an object into a string with JSON.stringify etc.

// keep a running counter across restarts
const n = parseInt(script.getOr("kills", "0"), 10) + 1;
script.set("kills", String(n));

// stash a small object as JSON (just an example, use the Settings instead section on that later)
script.set("cfg", JSON.stringify({ radius: 8, mode: "Fast" }));
const cfg = JSON.parse(script.getOr("cfg", "{}"));

Where it lives

Each script's store is persisted to polinex/scripts/data/<name>.properties, keyed by the script's NAME.
Creating Things

Modules

module.create(config) builds a toggleable module that shows up in the client GUI under the Scripts category, exactly like a built-in feature. It returns a ModuleHandle you keep around to read the module's state and settings from your event hooks.

Module config

Pass a ModuleConfig object. Only name is required.

name is the display name and default setting id prefix. description is the tooltip shown on the card. category is any Polinex category such as "MINING" or "QOL", and defaults to "SCRIPTS" this only affects it once "Show in Main GUI" is on. subcategory groups the card within that well. subcategory. onEnable / onDisable run when the toggle flips. settings is an array of setting definitions (config objects) or builder-style ScriptSetting handles; see the settings section.

Show in main GUI

Every scripted module gets a Show in main GUI toggle in the client. Turning it on also surfaces the module under its chosen category (MINING, QOL, …) with a script badge, so it sits alongside the native features instead of only in the Scripts list.

Module handle

module.create returns a ModuleHandle: enabled() reports whether the toggle is on, toggle(on) flips it from code, setting(id) reads a setting by id or name (or null when unknown), setSetting(id, value) writes one and returns false on a bad id or value, settings() lists every setting id, and addSettings(...) attaches more builder-style settings after creation.

The main module is a isEnabled check by itself

Your script is always loaded, but its event subscriptions (on("tick"), on("motion"), …) only fire while its main module is enabled. The first module you create is the main one by default; call module.export(handle) to pick a different one. Because of this you no longer need an if (!mod.enabled()) return; on every on(). Extra modules you create are independent toggles you can still read with handle.enabled() from inside your already-gated hooks.

Example

A module with settings that watches your purse and warns once when it drops below the threshold.

const mod = module.create({
    name: "Purse Alert",
    description: "Warns you when your purse drops below a threshold.",
    category: "QOL",              // also listed here once "Show in main GUI" is on
    subcategory: "Tracking",
    settings: [
        { type: "boolean", name: "Prefix",   default: true },
        { type: "slider",  name: "Threshold", min: 0, max: 1000000, default: 50000 },
    ],
    onEnable:  () => script.chat("Purse Alert on"),
    onDisable: () => script.chat("Purse Alert off"),
});

let warned = false;

on("tick", () => {   // only fires while the module toggle is on
    const low = skyblock.purse() < mod.setting("Threshold");
    const text = (mod.setting("Prefix") ? "[prefix] " : "") + "Purse is low!";
    if (low && !warned) {
        warned = true;
        script.chat(text);
    } else if (!low) {
        warned = false;
    }
});

module.export(mod);   // mark this as the main module that gates the hooks above

Macros

macro.create(config) builds a macro that shows up as a macro card under the Macros category, with a start/stop button and a keybind, grouped by the subcategory you give it. It returns a MacroHandle that drives everything below. The engine handles ticking, failsafes, the scheduler, the HUD, and stop/start for you.

The state machine

A macro is a state machine, and this is the core idea. You register a per-state handler with onState(name, fn), and the engine runs fn every client tick while the macro is in that state. You drive transitions with setState(name), and currentState() returns exactly the string you last set.

setState and onState accept any string. Built-in engine states like "TRAVEL", "MOVE", "ATTACK", "KILL", and "MINE" map to that native state and autocomplete in the editor through MacroState, but they are not a closed set. Any other name, such as "scanArea" or "moveToCrypt" yada yada becomes a custom state that works just the same. State names are matched case-insensitively!

m.onEnable(() => m.travelTo("DWARVEN_FORGE", 3000));     // -> TRAVEL state
m.onState("TRAVEL", () => { if (m.reachedDestination()) m.setState("mineVein"); });
m.onState("mineVein", () => { /* custom state, runs every tick */ });
m.hud(() => "State: " + m.currentState());

Macro config

Pass a MacroConfig: name (required), description, subcategory for grouping under the Macros category, and settings, an array of setting definitions or handles identical to a module's. Read and write(very not suggested) them at runtime with m.setting(id), m.setSetting(id, value), m.addSettings(...), and m.settings(). Call macro.export(m) to mark it as the script's main module as with the regular modules.

const m = macro.create({
    name: "lalala",
    subcategory: "Dwarven",
    settings: [
        { id: "oneTap", type: "boolean", name: "One Tap Mode", default: true },
    ],
});

// read and write settings at runtime by id
if (m.setting("oneTap")) { /* one-tap logic */ }
m.setSetting("oneTap", false);

macro.export(m);   // register this handle as the script's main module

Find and control registered macros

macro.list() returns the names of registered native and script macros. macro.get(name) looks up a name case-insensitively, and macro.active() returns the current session’s MacroRef or null. A reference exposes name(), state(), isPaused(), isRunning(), isBeta(), plus start(), stop(), and toggle(). A paused macro still owns the session, so active() can return it while isRunning() is false.

script.log("Macros: " + macro.list().join(", "));
const mining = macro.get("Gemstone Macro");
if (mining?.isBeta()) script.log(mining.name() + " is a beta macro");

// Starting/stopping is routed through the shared macro session.
const current = macro.active();
if (current?.name() === "My Script Macro") current.stop();

Beta macro confirmation

Beta macro starts initiated by a script show the normal confirmation unless the user enables General → Scripting → Allow Beta Macros. That setting defaults off and suppresses confirmation only for script starts; it does not globally disable confirmation for manual starts. Use MacroRef.isBeta() to inspect the macro before starting it.

Built-in states and locations

A macro starts in NONE. The MacroState lists the engine's built-in states purely for autocomplete: NONE, TRAVEL, HELPER, RETURN, DEATH, MINE, MOVE, ATTACK, KILL, WALK, WAIT, and many more. its open, passing a name that isn't in it is fine and just makes a custom state you can use :).

The SkyblockLocation array is the set of places you can travel, reset, or warp to: "HUB", "DWARVEN_MINES", "DWARVEN_FORGE", "CRYSTAL_HOLLOWS", "GARDEN", "CRIMSON_ISLE", and so on. Unlike states, this array is strict: unknown location names are rejected at runtime for obvious reasons!

// setState takes a built-in name...
m.setState("MINE");        // maps to the MINE state
// ...or any string you like, that just becomes a custom state
m.setState("scanArea");    // custom, ticks exactly the same

// travelTo locations are strict: only SkyblockLocation names are accepted
m.travelTo("DWARVEN_FORGE", 3000);   // ok so a known Location
m.travelTo("MyBase", 3000);          // rejected at runtime as its not a Location

Lifecycle and HUD

onEnable(fn) runs when the macro starts (a good place to kick off a travelTo) or such and onDisable(fn) runs when it stops (clean up with nav.stop() and bot.stop() here). Register HUD lines with hud(fn). Each callback returns a string that the macro HUD renders, and you can add as many as you like.

// for example on start go to hub with a 1500ms timeout
m.onEnable(() => m.travelTo("HUB", 1500));

// clean up stuff
m.onDisable(() => { nav.stop(); bot.stop(); }); 

// each hud() callback returns a line the MacroHUD renders
m.hud(() => "State: " + m.currentState());
m.hud(() => "Kills: " + kills);

Travelling and resetting

travelTo(location, delayMs) warps and pathfinds to a SkyblockLocation and puts the macro into the TRAVEL state. reachedDestination() is true once you have arrived, and travelTarget() returns the current travel target as a string (handy for stuck-check fingerprints).

reset(location, resume) is your recovery lever: it warps back to a safe location and then calls your resume callback to put the macro back into a known state. Call it from stuck checks and from onDeath to get unstuck cleanly.

// travelTo warps + pathfinds, and drops the macro into the TRAVEL state
m.onEnable(() => m.travelTo("DWARVEN_FORGE", 3000));

// sit in TRAVEL until we arrive, then advance into our own state
m.onState("TRAVEL", () => {
    if (m.reachedDestination()) m.setState("mineVein");
});

// reset() is the recovery lever: warp somewhere safe, then resume a known state
function recover() { m.setState("mineVein"); }
m.onDeath(() => m.reset("DWARVEN_FORGE", recover));

Stuck checks

addStuckCheck(check) registers a active loop for a state. The StuckCheck object has:

name is a label for the check. state is which state it watches (built-in or custom). timeoutMs fires onStuck if progress() hasn't changed within this window. minimumStateMs (optional) is a grace period after entering the state before the check actually goes into effect. active (optional) is a predicate that gates the check, so it only runs while it returns true. progress is called to track progress, as long as its return value keeps changing you are considered to be making progress. onStuck is the recovery callback, usually an m.reset(...).

// fires onStuck if progress() HASN'T CHANGED within timeoutMs while in "MOVE"
m.addStuckCheck({
    name: "route",
    state: "MOVE",
    timeoutMs: 20000,
    minimumStateMs: 3000,                    // grace period before the check arms
    progress: () => routeIndex + ":" + nav.finished(),
    onStuck: () => m.reset("DWARVEN_FORGE", () => m.setState("MOVE")),
});

Resume hooks and interruptions

When something interrupts the macro, you get a chance to put it back on track. onHelperEnd(fn) fires after a helper finishes, onReturn(fn) after a return-warp interruption ends, and onDeath(fn) after a death. onResume(fn) is the general fallback that also fires after the macro resumes from a pause caused by breaks or failsafes.

function resume() { m.setState("mineVein"); }   // put the macro back on track

m.onHelperEnd(resume);                    // after a native helper (autoSell...) finishes
m.onReturn(resume);                       // after a return-warp interruption ends
m.onDeath(() => m.reset("HUB", resume));  // warp to hub, then resume
m.onResume(resume);                       // general fallback (after break/failsafe)

Resume order

After an interruption the engine picks the most specific hook that applies (onHelperEnd, onReturn, or onDeath). If none is registered it calls onResume. If that is missing too, the macro auto-resumes the last state you setState'd (or NONE).

Helpers, combat bots, and failing

addHelper(helper, when, updateFrequent?) runs a side-task whenever when() is true; helper is a helper.create(...) handle of your own or a native helper name. The condition is checked during the macro's HELPER state, or every tick when updateFrequent is true (see helpers). createCombatBot(config) gives you the same combat engine our macros use (see combat bots). And fail(reason) stops the macro with an error message when something has gone unrecoverably wrong.

// run the native autoSell helper whenever the inventory is nearly full
m.addHelper("autoSell", () => player.getEmptyInventorySlots() <= 2);

// createCombatBot is the same targeting/attacking engine the built-in macros use
const bot = m.createCombatBot({
    areas: [[-36, 126, 129, 82, 152, 192]],
    names: ["Glacite Walker"],
    weapon: ["Drill", "Pickaxe"],
});

// fail() stops the macro with an error when something is unrecoverable
m.onEnable(() => {
    if (!skyblock.hasMiningTool()) m.fail("No drill/pickaxe found.");
});

Full example: Glacite Walker Macro

This is a script that mirrors the built-in Glacite Walker Macro, so it lands under the Macros category with its own start/stop button and keybind. It ties together everything above in one runnable file: a custom TRAVEL → MOVE → KILL flow, a createCombatBot that does the actual fighting, kill counting off the entityDeath event, three stuck checks that fingerprint progress and recover through reset, and clean start-up and tear-down. The inline comments explain why each piece is shaped the way it is, especially the bot.enabled guard that keeps KILL from restarting the bot every tick.

/// <reference path="../polinex.d.ts" />

const GLACITE_AREA = [-36, 126, 129, 82, 152, 192]; // minX,minY,minZ,maxX,maxY,maxZ
const ROUTE = [{ x: 0, y: 127, z: 137 }];

// Build the macro. The one boolean setting is exposed to the user and read later
// with m.setting("oneTap").
const m = macro.create({
    name: "Glacite Walker",
    description: "Walks the Glacite Tunnels and one-taps Glacite Walkers.",
    subcategory: "Dwarven",
    settings: [
        {
            id: "oneTap", type: "boolean", name: "One Tap Mode",
            description: "Only disable this if you can't one tap Glacite Walkers.", default: true,
        },
    ],
});

// The combat bot is the SAME targeting/attacking engine the built-in combat macros use.
// Here we only describe WHAT to hit — where, which entity types, by name, with which weapon —
// and later start() it. 
const bot = m.createCombatBot({
    areas: [GLACITE_AREA],
    entityTypes: ["RemotePlayer"],           // Glacite Walkers are player-type mobs
    names: ["Glacite Walker", "Ice Walker"],
    weapon: ["Drill", "Pickaxe"],            // equipped automatically before attacking
});

let kills = 0;
let routeIndex = 0;

// Two HUD lines
m.hud(() => "Macro State: " + m.currentState());
m.hud(() => "Glacite Walkers Killed: " + kills);

// onEnable runs once when the macro starts. Fail loudly if we have no mining tool otherwise
// jump straight into combat if we're already there, or travel to the forge + follow path first.
m.onEnable(() => {
    skyblock.findMiningTools();
    if (!skyblock.hasMiningTool()) { m.fail("No drill/pickaxe found."); return; }
    if (skyblock.location() === "DWARVEN_MINES" && skyblock.inArea(GLACITE_AREA)) beginCombat();
    else m.travelTo("DWARVEN_FORGE", 3000);  // -> puts the macro into the TRAVEL state
});

// The custom-state flow is TRAVEL -> MOVE -> KILL. Each onState handler runs every tick while
// the macro sits in that state; we advance by calling setState (via the two helpers below).

// TRAVEL: travelTo already warps and pathfinds for us, so we just wait for arrival, then walk.
m.onState("TRAVEL", () => {
    if (m.reachedDestination()) startMove();
});

// MOVE: walk the route one waypoint at a time. nav.finished() is the "am I idle?" check —
// arrive within 1.5 blocks to advance the index, otherwise (re)issue the walk.
m.onState("MOVE", () => {
    if (routeIndex >= ROUTE.length) { beginCombat(); return; }
    const wp = ROUTE[routeIndex];
    if (nav.failed()) { m.fail("Walking to the waypoint failed"); return; }
    if (nav.finished() && player.distanceFlat(wp) < 1.5) routeIndex++;
    else if (nav.finished()) nav.walkTo(wp);
});

// KILL: start the bot ONCE and let it run itself. bot.enabled is the guard without it we would
// call start() again every single tick. We push the live setting onto the bot right before starting.
m.onState("KILL", () => {
    if (!bot.enabled) {
        bot.oneTap = m.setting("oneTap");
        bot.allowSneaking = false;
        if (!bot.start()) m.fail("Combat bot failed to start.");
    }
});

// tiny transition helpers so the state names live in exactly one place.
function startMove() { routeIndex = 0; m.setState("MOVE"); }
function beginCombat() { m.setState("KILL"); }

// count kills from the global entityDeath event
on("entityDeath", (e) => {
    if ((e.name.includes("Glacite Walker") || e.name.includes("Ice Walker")) &&
        player.distance(e.pos) < 8) {
        kills++;
    }
});

// reset() warps back to the forge, then re-runs startMove to re-enter a known state.
const resetToForge = () => m.reset("DWARVEN_FORGE", startMove);

// stuck checks
m.addStuckCheck({ name: "travel", state: "TRAVEL", timeoutMs: 18000,
    progress: () => m.travelTarget() + ":" + skyblock.location(), onStuck: resetToForge });
m.addStuckCheck({ name: "route", state: "MOVE", timeoutMs: 20000,
    progress: () => routeIndex + ":" + nav.finished(), onStuck: resetToForge });
m.addStuckCheck({ name: "combat", state: "KILL", timeoutMs: 60000,
    active: () => bot.enabled, progress: () => kills + ":" + bot.snapshot(), onStuck: resetToForge });

// always clean up whatever you started, stop the bot and nav.
m.onDisable(() => { bot.stop(); nav.stop(); });

macro.export(m);

Combat bots

macro.createCombatBot(config)gives you the same targeting and attacking engine the built-in macros use. It returns a CombatBot you configure and start from inside a macro's onState, so the bot handles combat while your macro does everything around it.

Combat bot config

Pass a CombatBotConfig. Only areas is required.

areas is a list of [minX, minY, minZ, maxX, maxY, maxZ] boxes the bot hunts inside. entityTypes lists the entity class names to target, e.g. ["RemotePlayer", "ArmorStand"]. names sets name filters for valid targets. weapon is an item-name priority list for the weapon slot. ignoreAreas lists boxes never to target inside. ignoreOneTap lists names excluded from one-tap behaviour. ignoreCompletely lists names never targeted at all.

Combat bot handle

The returned CombatBot exposes: oneTap toggles one-tap behaviour, allowSneaking lets it sneak, enabled is a read-only flag for whether it is running, start() begins the bot and returns false if it couldn't start, stop() halts it, and snapshot() returns a debug string of its current state, perfect for stuck-check progress()

bot.enabled, don't restart every tick

onState handlers run every tick, but the bot drives itself once it is running. Set oneTap / allowSneaking and call start() only while bot.enabled is false, otherwise you restart it on every frame. The common shape is an early if (bot.enabled) return; at the top of the combat state. Create the bot once (at macro top level), and remember to bot.stop() in onDisable and in your stuck recovery.

Mining bots

mining.bot(type) hands you the same mining engine the built-in mining macros run. It returns a MiningBot you start from inside a macro's onState, so the bot mines while your macro does everything around it.

Bot types

Self-driving bots: "mithril", "gemstone", "ore", "oreV2", "tunnel". Route bots: "cobblestone", "crimson", "endstone", "sand", "obsidian". An unknown type throws. mining.botTypes() lists them all.

Starting and the handle

Self-driving bots take a start position: start({ x, y, z }), or start() to use the block you're standing on. Route bots take start({ waypoints }), an array of {x,y,z} or [x,y,z] points that become MINE waypoints. The handle exposes enabled, finished, start(opts) and stop().

const bot = mining.bot("mithril");

macro.onState("MINE", () => {
  if (bot.enabled) return;
  if (bot.finished) return macro.setState("DONE");
  bot.start();
});

macro.onDisable(() => bot.stop());

Coordinates are validated strictly

Every coordinate must be present and a finite number. A missing z, a string, or a bad waypoint logs an error to the script console and the bot simply doesn't start. Starting a route bot with no waypoints does nothing either. Don't run a bot while its matching native mining macro is also running, you WILL cause issues

Helpers

A helper is a side-task that runs alongside a macro: sell a full inventory, take a scheduled break, refuel, warp to safety. Each one has a lifecycle (start when it activates, tick every client tick while it runs, stop when it ends) plus a when() condition that decides when it should fire. There are three ways to get one going.

How helpers actually run: read this first

  • A helper fires only while a macro is running: one of the client's built-in macros (Mithril, Ghost, Slayer…) or a script macro. It does not fire while you're idle with no macro active.
  • Only one helper runs at a time, client-wide. When one activates it pauses whatever macro is running (that macro's state becomes HELPER) until the helper is done.
  • A helper keeps running until it calls h.finish(). Always give yourself a clear exit.
  • A 5-minute timeout force-stops a script helper that never calls finish(), so a stuck helper can't hang the client forever, but don't rely on it as your exit path.

helper.register: a toggleable helper (recommended)

helper.register(config) is the recommended path for a standalone helper. It drops a first-class toggle card into the Scripts GUI, right beside the cards module.create and macro.create make. Flip it on and the helper runs globally on its own, it polls its when() on the client tick and activates during any active macro. You don't author a macro or module to host it. The toggle is the whole feature, and its on/off state config-persists like any module.

HelperModuleConfig fields: name (required, the card's display name), description, subcategory (grouping header, default "Helpers") and category (where it lands when its injected "Show in main GUI" row is on). when / shouldActivate is required. Skip it and the card just warns and never fires. The lifecycle callbacks each accept aliases: start/onStart, tick/onTick, stop/onStop. The h they receive is the run handle, not the module. updateFrequent polls when every tick instead of ~once a second. onEnable/onDisable fire when the toggle itself flips (distinct from a run's start/stop; disabling also force-ends any live run), and settings attaches setting rows to the card.

The returned HelperModule handle lets you drive the card from code: enabled() / toggle(on) read and flip the toggle, running() reports whether a run is live, finish() ends the current run early (the toggle stays on and can fire again), and setting(id) / setSetting(id, value) / settings() / addSettings(...) read, write, list, and attach its settings.

const LOW_HP = 5;
const COOLDOWN_MS = 8000;
const WARP_TIMEOUT_MS = 5000;
let cooldownUntil = 0;
let warpDeadline = 0;

helper.register({
  name: "Low HP Warp",
  description: "Warp to the hub when your health gets dangerously low.",
  subcategory: "Safety",

  // polled while the toggle is on to decide when to run
  when: () => client.isInSkyblock()
    && player.isAlive()
    && player.health() <= LOW_HP
    && Date.now() >= cooldownUntil,
  updateFrequent: true,            // a health drop is time-critical but if using updateFrequent keep it a cheap check

  // a run begins. `h` is the RUN handle (finish/running/travelTo/...), not the toggle.
  start: (h) => {
    warpDeadline = Date.now() + WARP_TIMEOUT_MS;
    player.stopAllMovement();
    player.sendCommand("warp hub");
    script.info("Low HP Warp: warping to hub.");
  },

  // every tick while the run is active. end it once we're safe, or on the timeout.
  tick: (h) => {
    if (skyblock.inLocation("HUB") || Date.now() >= warpDeadline) h.finish();
  },

  // always runs when a run ends. arm the cooldown so we don't loop-warp.
  stop: (h) => { cooldownUntil = Date.now() + COOLDOWN_MS; },
});

helper.create + registerGlobal: a global helper object

helper.create(config) builds a helper object but doesn't wire it up on its own. Call registerGlobal() on the returned handle to make it fire globally during any active macro for as long as the script is loaded. No toggle card, just an always-registered helper. This requires a when set in the config, since that's what decides when it activates. The config accepts the same lifecycle (start/tick/stop) and updateFrequent as above. Because a global helper can re-fire the instant when is true again, gate it behind a cooldown so it doesn't tweak.

const PANIC_MS = 1500;
const COOLDOWN_MS = 5000;
let panicUntil = 0;
let cooldownUntil = 0;

// this is lowk AI'd i ran out of ideas LOL
// helper.create does nothing until you register it.
const firePanic = helper.create({
  name: "Fire Panic",
  when: () => player.isAlive() && player.burning() && Date.now() >= cooldownUntil,
  start: (h) => {
    panicUntil = Date.now() + PANIC_MS;
    player.stopAllMovement();
    player.setKey("sneak", true);
  },
  tick: (h) => {
    if (Date.now() >= panicUntil || !player.burning()) h.finish();
  },
  stop: (h) => {
    player.setKey("sneak", false);
    player.resetWASD();
    cooldownUntil = Date.now() + COOLDOWN_MS;
  },
});

firePanic.registerGlobal();  // needs `when` set above; runs during any macro while the script is loaded

macro.addHelper & helper.useNative: attach a helper

macro.addHelper(handle, when) attaches a helper to one macro. It runs only while that macro is running, whenever when() holds. The handle can be your own helper.create(...) object or a built-in helper referenced by name.

helper.useNative(name, when) drives one of the client's built-in helpers globally on your own trigger: it activates during any active macro whenever when() is true. The valid HelperName values are autoSell, scheduler, autoReconnect, interruptionFixer, playerNotifier, autoRefuel, autoForge, and slayerStarter. Both macro.addHelper and helper.useNative take an optional trailing updateFrequent to poll every tick. A script's global helpers (from register, registerGlobal, and useNative) are all removed automatically when the script unloads.

// Custom helper tied to ONE macro — runs only while that macro runs.
const restock = helper.create({
  name: "Restock",
  start: (h) => h.travelTo("HUB", 2000),
  tick:  (h) => { if (h.reachedLocation()) { buyStuff(); h.finish(); } },
});
mac.addHelper(restock, () => myItem.value < 10);

// Or drive one of the client's built-in helpers globally on your own trigger.
helper.useNative("autoSell", () => player.getEmptyInventorySlots() === 0);
API Reference

player

Read and control your own player, including position, motion, rotation, keys/clicks, inventory, and live state.

Position & motion

pos(): Vec

Current position (feet).

blockPos(): Vec

Current position floored to block coordinates.

distance(target: Vec): number

3D distance to a point.

distanceFlat(target: Vec): number

Horizontal (XZ) distance to a point, ignoring height.

x(): number; y(): number; z(): number

Current X / Y / Z coordinate.

prevX(): number; prevY(): number; prevZ(): number

Position last tick (for interpolation).

motionX(): number; motionY(): number; motionZ(): number

Velocity component this tick.

setMotionX(x: number): void; setMotionY(y: number): void; setMotionZ(z: number): void

Set a velocity component.

setPosition(x: number, y: number, z: number): void

Move the player to exact coordinates.

speed(): number

Current horizontal speed.

predictLanding(): Vec | null

Where you'll land given current velocity.

willCollide(ticks: number): boolean

Whether you'll hit something within the given number of ticks.

Rotation

yaw(): number

Current yaw.

pitch(): number

Current pitch.

setYaw(yaw: number): void

Set yaw directly.

setPitch(pitch: number): void

Set pitch directly.

rotateTo(target: Vec): void

Smoothly rotate to look at a point.

rotateTo(x: number, y: number, z: number): void

Smoothly rotate to look at scalar coordinates.

rotateToYawPitch(yaw: number, pitch: number): void

Rotate to an absolute yaw/pitch.

rotateToEntity(id: number): void

Rotate to look at an entity by id.

rotationDone(): boolean

Whether the active rotation has finished.

stopRotating(): void

Cancel the active rotation.

Input & movement

setKey(key: string, state: boolean): void

Press/release a key. key: "forward","backward","right","left","jump","sneak","sprint","attack","use".

releaseKeys(): void

Release keys tracked through this script's input helpers while preserving physically held player inputs. Also called during world changes, disconnects, and script cleanup. Example: on("disable", () => player.releaseKeys()). This is separate from global resetWASD() / stopAllMovement().

isKeyDown(key: string): boolean

Is a key held? key: same set as setKey.

resetWASD(): void

Release the movement keys.

stopAllMovement(): void

Release every input and halt movement.

setKeysTo(x: number, y: number, z: number, sneak: boolean): void

Press WASD toward a point.

setOptimalKeysTo(x: number, y: number, z: number, strafe: boolean): void

Strafe-aware walk-toward (the macros' move primitive).

setSneaking(state: boolean): void

Hold or release sneak.

setSprinting(state: boolean): void

Hold or release sprint.

jump(): void

Trigger a jump.

leftClick(): boolean

Simulate a left click.

rightClick(): boolean

Simulate a right click.

rightClickPacket(): boolean

Send a right-click via packet.

forceLeftClick(): void

Force an immediate left click.

forceRightClick(): void

Force an immediate right click.

legitLeftClick(): void

Human-timed left click.

legitRightClick(): void

Human-timed right click.

walkTo(target: Vec): boolean

Begin walking to a Vec target.

walkTo(x: number, y: number, z: number): boolean

Begin walking to scalar coordinates.

walkDone(): boolean

Whether the walk has arrived.

stopWalking(): void

Stop the current walk.

Inventory & items

setHeldItemSlot(slot: number): boolean

Select a hotbar slot (0-8).

heldItemSlot(): number

Current hotbar slot index.

getItemByName(name: string): number

Hotbar slot of an item by name, or -1.

itemByName(name: string): number

Hotbar slot of an item by name, or -1.

itemByLore(line: string): number

Hotbar slot of an item by lore substring, or -1.

fishingRodSlot(): number

Hotbar slot of your fishing rod, or -1.

etherwarpSlot(): number

Hotbar slot of your etherwarp item, or -1.

aoteSlot(): number

Hotbar slot of your Aspect of the End, or -1.

swordSlot(): number

Hotbar slot of your sword, or -1.

distanceWeaponSlot(): number

Hotbar slot of a ranged/distance weapon, or -1.

manaRegenSlot(): number

Hotbar slot of a mana-regen item, or -1.

itemByNames(names: string[]): number

Hotbar slot of the first item matching any name, or -1.

itemBySkyblockId(ids: string[]): number

Hotbar slot of the first item matching any Skyblock id, or -1.

itemCount(name: string): number

Total count of a named item across the inventory.

itemLore(slot: number): string[]

Lore lines of an inventory slot (0-35).

itemId(slot: number): string

Item id in an inventory slot (0-35).

itemInfo(slot: number): { name: string; id: string; count: number; lore: string[] } | null

Full item info for an inventory slot, or null when empty.

useItem(slot: number): void

Activate an item (swap/use/swap back).

useItemId(id: string): void

Activate an item by id (swap/use/swap back).

isHoldingItem(name: string): boolean

Whether the held item's name contains this string.

heldItemName(): string

Name of the currently held item.

swapToName(name: string): boolean

Select the hotbar slot holding a named item.

getEmptyInventorySlots(): number

Number of empty inventory slots.

getInventoryName(): string | null

Title of the open inventory/chest, or null.

State & sensing

name(): string

Player username.

health(): number

Current health.

onGround(): boolean

Standing on the ground.

airBorne(): boolean

In the air.

sneaking(): boolean

Currently sneaking.

sprinting(): boolean

Currently sprinting.

moving(): boolean

Currently moving.

isAlive(): boolean

Player is alive.

dead(): boolean

Player is dead.

hurtTime(): number

Ticks remaining in the hurt animation (0 when not hurt).

ticksExisted(): number

Ticks the player entity has existed.

fallDistance(): number

Current fall distance.

getFallDistance(): number

Current fall distance (alias).

getJumpHeight(): number

Configured jump height.

onLadder(): boolean

On a ladder/vine.

inWater(): boolean

In water.

inLava(): boolean

In lava.

eating(): boolean

Currently eating.

usingItem(): boolean

Currently using an item.

burning(): boolean

On fire.

collided(): boolean

Colliding with a block.

collidedHorizontally(): boolean

Colliding horizontally.

collidedVertically(): boolean

Colliding vertically.

isStandingStill(): boolean

Not moving.

isBlockAhead(): boolean

Solid block directly ahead.

isBlockToLeft(): boolean

Solid block to the left.

isBlockToRight(): boolean

Solid block to the right.

isPotionActive(effectId: string): boolean

Effect name or namespaced id, e.g. "speed" or "minecraft:speed".

mouseOverBlock(): Vec | null

The block under your crosshair, or null.

isOnBlockEdge(): boolean

Whether you're standing on a block edge.

shouldSneak(): boolean

Whether the client thinks you should sneak (edge protection).

Entity queries

getEntityById(id: number): number | null

Resolve an entity by id (returns the id if present, else null).

isEntityAlive(id: number): boolean

Whether an entity by id is alive.

getEntityX(id: number): number | null; getEntityY(id: number): number | null; getEntityZ(id: number): number | null

Coordinate of an entity by id, or null.

getEntityName(id: number): string | null

Name of an entity by id, or null.

distanceToEntity(id: number): number | null

Distance to an entity by id, or null.

Chat & GUI

sendMessage(msg: string): void

Send a chat message.

sendCommand(command: string): void

Run a command.

isGuiOpen(): boolean

Whether any GUI/screen is open.

isChestOpen(name: string): boolean

Whether a chest whose title contains name is open.

world

Query the world around you, including block ids, light, biomes, line-of-sight raytraces, surface sample points, and geometry helpers. Positions in / out are Vec. See World snapshots for saved captures and recorders.

Block queries

canWalkBetween(from: Vec, to: Vec): boolean

Clear straight-line walk (ignoring height)?

isOnBlock(point: Vec, range: number): boolean

Are you standing on/near this point?

isAir(pos: Vec): boolean

Is the block at pos air?

isSolid(pos: Vec): boolean

Whether the live block state is non-air; this does not guarantee collision support.

isBedrock(pos: Vec): boolean

Is the block at pos bedrock?

isPathfindable(pos: Vec): boolean

Can the player stand/path here?

blockId(pos: Vec): string

Block registry id at pos, e.g. "minecraft:stone".

blockState(pos: VecInput): { id: string; properties: Record<string, string> } | null

Loaded block state id and serialized properties; null if the position is invalid, the world is unavailable, or the block is not loaded.

blockAt(x: number, y: number, z: number): string

blockId with plain scalar coords, e.g. "minecraft:stone".

lightAt(pos: Vec): { block: number; sky: number; max: number }

Light levels 0..15 at pos.

biomeAt(pos: Vec): string

Biome registry id, e.g. "minecraft:the_end", or "".

blockHeight(pos: Vec): number

Top-surface height 0..1.

boundingBox(pos: Vec): { minX: number; minY: number; minZ: number; maxX: number; maxY: number; maxZ: number }

Collision bounding box of the block at pos.

Position helpers

center(pos: Vec): Vec

Block center.

topCenter(pos: Vec): Vec

Top-face center (stand/aim target).

groundPos(pos: Vec): Vec

Drop a position to the floor block.

groundCenter(pos: Vec): Vec

Floor-block top-center under a position.

eyePos(): Vec

The player's eye position.

Raytracing

raytrace(to: Vec): { type: "BLOCK" | "ENTITY" | "MISS"; x: number; y: number; z: number; block: Vec | null }

Raytrace from your eye to a point; reports the hit type, hit coords, and hit block (if any).

raytraceFrom(from: Vec, to: Vec): { type: string; x: number; y: number; z: number; block: Vec | null }

Raytrace between two explicit points.

raytraceEntity(to: Vec, types?: string[]): { type: string; x: number; y: number; z: number; block: Vec | null; entityId: number | null; entityName: string | null } | null

Raytrace that also reports the entity it hits (optionally filtered by types), or null.

isVisible(pos: Vec): boolean

Clear line of sight to that block?

Sampling & lines

visibleSurfacePoints(pos: Vec): Vec[]

Aim points on the block with line of sight.

surfacePoints(pos: Vec): Vec[]

All sampled face points.

boxSides(pos: Vec): Vec[]

Cheap 3-near-face sample points.

blocksBetween(from: Vec, to: Vec): Vec[]

Block positions a line passes through.

walkPointsBetween(from: Vec, to: Vec, interval: number): Vec[]

Simulated straight-walk path.

platform(maxYDeviation: number, maxDistance: number): Vec[]

Connected standable platform you're on.

const { x, y, z } = player.blockPos();
const state = world.blockState({ x, y, z });
if (state !== null) {
  script.log(state.id);
  script.log(JSON.stringify(state.properties));
}

World snapshots

The world namespace can capture or load snapshots for offline queries. Snapshots do not replace the live native pathfinding cache. Missing data is UNKNOWN, not a promise that a block is air.

World factories and saved files

world.capture(config: { name?: string; radius?: number; minY?: number; maxY?: number; blocks?: boolean } | null, onDone: ((chunks: number, error?: string) => void) | null): WorldHandle

Captures loaded chunks asynchronously; radius is in chunks, clamped 0–32 (default 4). Defaults minY=0/maxY=256, blocks=false. Returns a handle immediately; wait for the callback before using complete results.

world.blank(config: WorldConfig | null): WorldHandle

Empty snapshot; default name world. Captures nothing until capture/record is called.

world.load(name: string): WorldHandle

Loads a saved snapshot. File/format errors throw.

world.loadOrBlank(name: string, config: WorldConfig | null): WorldHandle

Loads when the named snapshot exists, otherwise calls blank(config). Set config.name to the same name if that is where the new snapshot should be saved.

world.saved(): string[]; world.savedExists(name: string): boolean; world.savedSize(name: string): number; world.deleteSaved(name: string): boolean

List, inspect, or explicitly delete saved snapshot files.

world.code(pos: Vec): number

Live-world classification in the same BlockCodes vocabulary; UNKNOWN without a valid world/position.

WorldHandle queries

name(): string; location(): string; minY(): number; maxY(): number; hasBlocks(): boolean; chunkCount(): number; blockCount(): number

Snapshot metadata. blocks:true stores registry IDs as well as coarse codes.

bounds(): { minX: number; minY: number; minZ: number; maxX: number; maxY: number; maxZ: number }

Snapshot bounds.

code(x,y,z): number; codeName(x,y,z): string; blockAt(x,y,z): string; contains(x,y,z): boolean; hasChunk(cx,cz): boolean

Snapshot reads, not live world reads. blockAt needs stored block-ID data.

isPassable(x,y,z): boolean; isSolid(x,y,z): boolean; isStandable(x,y,z): boolean

Coarse tests. isStandable uses feet coordinates with support below and clear feet/head cells.

window(x,y,z,sizeX,sizeY,sizeZ): byte[]

Dense code window, x varies fastest. Each size must be positive; maximum 16,000,000 cells. Convert once with Array.from to avoid repeated host calls.

set(x,y,z,code): boolean; clear(): void

Changes only the snapshot data, not the Minecraft world.

Capture, record, save

capture(radiusChunks: number, onDone: Function | null): number; recapture(radiusChunks: number, onDone: Function | null): number

Capture skips existing columns; recapture refreshes them. Returns scheduled chunk count, not completion. Callback receives completed count and, when supplied, an error string.

record(radiusChunks: number, autoSaveEvery: number): void; stopRecording(save: boolean): void

Walk-around recording. Radius is clamped 1–16; autoSaveEvery counts captured chunks between autosaves, with a minimum of 1. Stop explicitly on module disable.

recording(): boolean; missingNearby(): number

Recorder state and missing nearby columns.

save(onDone: ((error: string) => void) | null): void; saveAs(name: string, onDone: ((error: string) => void) | null): void

Asynchronous disk save; completion is marshalled to the client thread. Empty error string means success. saveAs changes the snapshot name only after a successful save.

Capture callbacks may originate from a worker. Schedule gameplay actions with setTimeout rather than starting movement from a capture callback. Saving on unload is attempted for an active recorder, but explicit module cleanup is clearer.

Example: record and save while enabled

const mod = module.create({ name: "Record nearby world" });
const snapshot = world.loadOrBlank("local-walk", {
  name: "local-walk", minY: -64, maxY: 320, blocks: true,
});
on("enable", () => snapshot.record(2, 30));
on("disable", () => snapshot.stopRecording(true));
module.export(mod);
// snapshot.code(x,y,z) reads the capture, not the live world.
// snapshot.save(error => script.log(error || "Saved"));

scan

Background block scanning with declarative block IDs. JavaScript predicates do not run on scanner worker threads. Results are cached and may lag world changes.

Create and classify

create(config: { name?: string; blocks: string | string[]; range?: number; height?: number }): ScanHandle

blocks is required and registry IDs must resolve. Missing minecraft: prefix is accepted. range is in chunks (default 4); height controls vertical block range (default range × 16). Creating a handle does not start it.

codes(): Record<string, number>

Named BlockCodes constants shared with world snapshots.

codeName(code: number): string

Name for a classification code.

isPassable(code: number): boolean; isSolid(code: number): boolean; blockHeight(code: number): number

Coarse classification helpers, not a full live collision or path validation.

ScanHandle

start(): void; stop(): void; running(): boolean

Manage scanning explicitly. Stop on module disable; owned scanners are disposed on unload.

invalidate(): void; version(): number

Request refresh, or read the result version to avoid rebuilding derived work every tick.

count(): number; positions(): { x: number; y: number; z: number; block: string }[]

Cached result count or a detached list of matching block coordinates and registry IDs.

nearest(pos: Vec): ScanEntry | null; within(pos: Vec, radius: number): ScanEntry[]

Queries over cached results using block-center distance. Empty results do not prove the area has finished scanning.

has(x: number, y: number, z: number): boolean; blockAt(x: number, y: number, z: number): string

Cached membership and matched block ID; blockAt returns an empty string when absent.

packed(): long[]

Packed Minecraft BlockPos keys. Do not coerce 64-bit keys to JS Number for arithmetic; use positions() for ordinary scripts.

scannedChunks(): number; pendingChunks(): number; lastScanMs(): number; scanCount(): number

Scanning progress and timing statistics.

Example: react to changed scan results

const mod = module.create({ name: "Nearby diamond blocks" });
const scanner = scan.create({ name: "diamonds", blocks: ["minecraft:diamond_ore"], range: 2, height: 32 });
let version = -1;
on("enable", () => { version = -1; scanner.start(); });
on("disable", () => scanner.stop());
on("tick", () => {
  const current = scanner.version();
  if (current === version) return;
  version = current;
  script.log("Cached blocks: " + scanner.count());
  const nearest = scanner.nearest(player.pos());
  if (nearest) script.log(nearest.block + " at " + nearest.x + ", " + nearest.y + ", " + nearest.z);
});
module.export(mod);

gui

Create script-owned panels and HUD elements using the existing render namespace. UI objects are disposed on script unload. Manage visibility explicitly for module toggles.

Panels

panel(config: PanelConfig): PanelHandle

Creates a panel handle; call open() to display it. raw defaults false for logical layout units; true uses physical pixels. pauses defaults false.

PanelConfig callbacks: onDraw(mouseX, mouseY, delta), onClick(mouseX, mouseY, button), onRelease(mouseX, mouseY, button), onDrag(mouseX, mouseY, button, dx, dy), onScroll(mouseX, mouseY, vertical, horizontal)

Use render calls in onDraw. Mouse coordinates and drag deltas use panel layout units. Input callbacks return true to consume the event.

PanelConfig callbacks: onKey(key, scanCode, modifiers), onKeyRelease(key, scanCode, modifiers), onChar(text), onOpen(), onClose()

onKey returning true also consumes Escape. Supply only the callbacks you need.

PanelHandle: open(), close(), isOpen(), width(), height(), scale(), mouse()

width/height/mouse use current layout units. scale is physical pixels per layout unit. Dimensions before opening fall back to the window.

openScreen(panel: PanelHandle): void; closeScreen(): void

openScreen aliases panel.open(). closeScreen closes a currently open script panel, including one created by another script; prefer your own handle.close().

HUD

hud(config: { id?: string; x?: number; y?: number; width?: number; height?: number; scale?: number; alwaysRender?: boolean; onDraw?: () => void; visible?: () => boolean }): HudHandle

Default id hud, x/y 960/540, size 80×20, scale 1. The HUD editor can drag/scale the element and persists its position. Draw at local coordinates.

HudHandle: show(), hide(), visible(), setSize(width, height), x(), y(), scale(), remove()

Starts hidden unless a custom visible callback says otherwise. A visible callback controls visibility instead of the show/hide flag. remove unregisters the element; hide is reversible.

Example: a navigation HUD

const mod = module.create({ name: "Navigation status HUD" });
const hud = gui.hud({
  id: "navigation", width: 220, height: 24,
  onDraw: () => {
    const text = nav.awaitingPath() ? "Planning…" : "Nodes left: " + nav.remainingNodes();
    render.drawText(text, 0, 0, "BODY_MEDIUM", 0xffffffff);
  },
});
on("enable", () => hud.show());
on("disable", () => hud.hide());
module.export(mod);

entity

Find and inspect nearby entities. Every result is an Entity ({ id, name, type, x, y, z, dist, yaw, pitch, headYaw, player, health, maxHealth, customName, nametagLines, equipment }), and lists come back nearest-first. Reading yaw/pitch/headYaw from a tick handler is the recommended way to watch another player's look (your own look is player.yaw()/player.pitch()).

Discovery (lists)

near(radius: number): Entity[]

All entities within radius.

players(radius: number): Entity[]

Other players.

mobs(radius: number): Entity[]

Living non-player entities.

named(name: string, radius: number): Entity[]

Name contains `name`.

withName(substr: string): Entity[]

Name contains `substr`, radius defaults to 48.

withName(substr: string, radius: number): Entity[]

Name contains `substr` within an explicit radius.

armorStands(radius: number): Entity[]

All armor stands (Hypixel holograms/nametags), nearest-first.

Lookup

byId(id: number): Entity | null

Resolve one entity by network id, same shape, or null.

nearest(radius: number): Entity | null

Nearest mob.

nearestNamed(name: string, radius: number): Entity | null

Nearest entity whose name contains `name`.

Interaction & aiming

interact(id: number): boolean

Right-click an NPC by id (opens menus).

canSee(id: number): boolean

Clear line of sight to the entity?

mouseOver(): Entity | null

Entity under your crosshair.

aimPointOn(id: number): Vec | null

Lead aim point on a mob.

visiblePointOn(id: number): Vec | null

Visible, raytrace-verified aim point.

standingPosOf(id: number): Vec | null

Block a mob stands on (walk/path here).

Entity shape

id: number

Network entity id.

name: string

Display name.

x: number; y: number; z: number

Entity position.

dist: number

Distance from the player.

yaw: number

Body yaw in degrees. Read each tick to watch another player's look.

pitch: number

Pitch in degrees.

headYaw: number

Head yaw in degrees (falls back to body yaw for non-living entities).

player: boolean

Whether the entity is a player.

type: string

Entity type registry id, e.g. "minecraft:armor_stand".

health: number | null; maxHealth: number | null

Health for living entities; null when the value does not apply.

customName: string | null

The entity’s own custom name, with formatting removed, or null.

nametagLines: string[]

A one-item list containing this entity’s own stripped custom name, or an empty list if it has no custom name. Nearby armor-stand labels are not attributed to mobs.

equipment: { slot: string; name: string; id: string; count: number; lore: string[] }[]

Non-empty equipment for living entities; other entities return an empty list.

for (const mob of Array.from(entity.near(24))) {
  if (mob.health !== null) {
    script.log(mob.type + " health=" + mob.health + "/" + mob.maxHealth);
  }
}

for (const stand of Array.from(entity.armorStands(24))) {
  if (stand.customName) script.log("Armor stand: " + stand.customName);
}

routes

Read existing route managers, or create a script-owned route editor with its own route folder. Existing manager reads do not change selection or files. IDs come from routes.managers(); names and folder paths are relative to that manager.

Script-owned editors

createEditor(config: ScriptRouteEditorConfig): ScriptRouteEditor

Folder scripts only. Creates a local editor under polinex/scripts/<script>/routes/<id>/. Config accepts id, optional name, mode ("file" or "folder", default "file"), files (folder mode only), and waypointTypes (defaults to WALK, WARP, MINE). Names must be unique uppercase types; ROTATION is not allowed.

editor.settings: ScriptSetting[]

Attach these settings to macro.create({ settings: editor.settings }). The native route picker then edits this editor’s files while your script owns waypoint behavior.

editor.create(name: string): boolean; editor.select(name: string): boolean

Create and select a new route, or select an existing route. Folder editors create all configured waypoint files together.

editor.load(name: string, file?: string): SavedRoute | null; editor.selected(file?: string): SavedRoute | null

Read saved route data, including branch definitions. In folder mode, file defaults to the selected waypoint file.

editor.save(name: string, waypoints: RouteWaypoint[], file?: string): boolean

Replace the main waypoint list while preserving other saved route metadata. It refuses to overwrite unreadable or branched routes; edit branches in the native picker.

editor.edit(name: string): boolean

Open the native route editor for a route. Attach editor.settings to a script macro first, and keep that macro disabled while editing.

Managers and files

managers(): RouteManager[]

Each entry has id, name, routeType, targetType, and waypointTypes. The id is the registered manager’s parent folder; do not guess it from a display name.

list(manager: string, folder?: string): string[]

Saved file names without .json, sorted. Defaults to the manager root. Missing folders return an empty list.

folders(manager: string, folder?: string): string[]

Sorted immediate subfolders. Supply folder/subfolder for deeper browsing.

load(manager: string, name: string): SavedRoute | null

Read a file such as folder/route, with optional .json suffix. Returns a detached snapshot, or null for a missing/unreadable route. Invalid manager/path arguments throw. Traversal and symlink escapes are rejected.

selected(manager: string): { name: string | null; route: SavedRoute }

Client-thread snapshot of the editor’s currently loaded runtime route. Reflects resolved coordinates and the currently chosen branch path; does not change selection. Runtime snapshot contains waypoints, location, and rotation, not the saved branch definitions or metadata.

SavedRoute data

waypoints: { x: number; y: number; z: number; type: string }[]

Original action types such as WALK, WARP, MINE, or VEIN. These are not PathNodes and cannot be passed directly to nav.followPath.

branches?: { start: RouteWaypoint; reconnect: RouteWaypoint; alternatives: RouteWaypoint[][] }[]

Optional branch definitions retained by load. Reading does not select an alternative.

location?: string; rotation?: { yaw: number; pitch: number }

Optional stored location and final camera rotation. Scripts decide how to use them.

metadata?: { name?: string; author?: string; created: number; lastModified: number; version: number }

Optional saved metadata.

grotto?: { type?: string; sourceAnchor?: Vec; templateVersion: number }

Optional grotto template information. Raw saved coordinates may be relative; the current runtime selection provides resolved coordinates.

Example: discover managers and routes

// Run once to discover the exact manager IDs and folders.
for (const manager of Array.from(routes.managers())) {
  script.log(manager.id + ": " + manager.name);
  script.log("Files: " + Array.from(routes.list(manager.id)).join(", "));
  script.log("Folders: " + Array.from(routes.folders(manager.id)).join(", "));
}

Example: create and edit a script route

A script-owned editor keeps its files separate from built-in route managers. Add custom waypoint names when creating it; the native picker stores those names unchanged, and the script decides what they mean. Open editor.edit only while the attached macro is disabled.

const editor = routes.createEditor({
  id: "leave_return",
  name: "Leave and return route",
  waypointTypes: ["WALK", "WARP", "AREA", "STOP"],
});

const m = macro.create({
  name: "Leave and return route",
  description: "Runs a script-owned route with AREA and STOP markers.",
  settings: editor.settings,
});
macro.export(m);

// Run once during setup while the macro is disabled.
if (editor.create("mine_route")) editor.edit("mine_route");

// These are custom names: the native editor preserves them, but only this
// script assigns their behavior.
function handleRouteMarker(point) {
  if (point.type === "AREA") {
    const returnAnchor = point; // leave the route for work, then walk back here
    startAreaTask(() => nav.walkTo(returnAnchor));
  } else if (point.type === "STOP") {
    nav.stop();
    etherwarp.stop();
    const active = macro.active();
    if (active?.name() === m.name()) active.stop();
  }
}

The next example executes only WALK and WARP on the saved main path. It intentionally rejects other actions and raw grotto templates. For a currently selected, resolved route, read routes.selected(id).route in tick and interpret its runtime waypoints instead.

Example: execute saved travel waypoints

// Set Manager and Route before enabling. Route is relative, e.g. folder/name.
const mod = module.create({
  name: "Saved travel route",
  settings: [
    { type: "string", name: "Manager", default: "" },
    { type: "string", name: "Route", default: "" },
    { type: "boolean", name: "Blatant", default: false },
    { type: "slider", name: "Budget", min: 1, max: 2000, default: 150, increment: 1 },
  ],
});
let points = null;
let index = 0;
let walking = false;
let done = false;
function stop() {
  nav.stop();
  etherwarp.stop();
  points = null;
  walking = false;
  done = true;
}
on("enable", () => { stop(); index = 0; done = false; });
on("disable", stop);
on("worldLoad", stop);
on("serverDisconnect", stop);
on("tick", () => {
  if (done) return;
  if (!points) {
    const id = String(mod.setting("Manager") ?? "");
    const name = String(mod.setting("Route") ?? "");
    if (!id || !name) { script.warn("Set Manager and Route first"); stop(); return; }
    let saved;
    try { saved = routes.load(id, name); }
    catch (error) { script.warn(String(error)); stop(); return; }
    if (!saved || saved.grotto) {
      script.warn("Missing route or relative grotto template"); stop(); return;
    }
    points = Array.from(saved.waypoints);
    if (points.some(p => p.type !== "WALK" && p.type !== "WARP")) {
      script.warn("This example only executes WALK and WARP"); stop(); return;
    }
    // Deliberately follows the saved main path; it does not choose branches.
  }
  if (index >= points.length) { script.log("Route finished"); stop(); return; }
  const point = points[index];
  if (point.type === "WALK") {
    if (!walking) {
      if (!nav.walkTo(point)) { script.warn("Walk rejected"); stop(); return; }
      walking = true;
    }
    if (nav.failed()) { script.warn("Walk failed"); stop(); return; }
    if (nav.finished()) {
      if (!world.isOnBlock(world.topCenter(point), 1.5)) {
        script.warn("Walk ended away from waypoint"); stop(); return;
      }
      walking = false;
      index++;
    }
  } else {
    const arrived = etherwarp.pathfindTo(point.x, point.y, point.z, {
      blatant: mod.setting("Blatant") === true, budgetMs: Number(mod.setting("Budget")),
    });
    if (arrived) index++;
    else if (etherwarp.failed()) { script.warn("Warp failed"); stop(); }
  }
});
module.export(mod);

skyblock

The skyblock namespace surfaces live Hypixel SkyBlock state: where you are, what you're holding, your economy, the scoreboard and tab list, buffs, and session skill XP.

Location & area

location(): string

Current SkyBlock location, e.g. "DWARVEN_MINES".

area(): string

Raw "Area: X" from the tab list (finer than location()), or "".

inLocation(name: string): boolean

Whether you are currently in the named location.

inArea(aabb: number[]): boolean

Whether you are inside the axis-aligned box [minX,minY,minZ,maxX,maxY,maxZ].

Detected structures

chStructures(): DetectedStructure[]

Return a snapshot of currently detected Crystal Hollows/Nucleus structures on the client thread. This reads existing detections; it does not start scanning and honors scanner toggles and route-setup detections.

DetectedStructure: { name: string; x: number; y: number; z: number; pos: Vec; bounds: [minX, minY, minZ, maxX, maxY, maxZ] | null }

bounds, when present, is the native no-bomb zone, not the exact structure footprint. Bounds may be null.

Items & tools

heldItemId(): string

SkyBlock id of the held item (e.g. "TITANIUM_DRILL_4"), or "".

findMiningTools(): void

Locate your mining tools in the hotbar.

hasMiningTool(): boolean

Whether a mining tool was found in your hotbar.

hasEtherwarp(): boolean

Whether an Etherwarp item can be found in your inventory.

Economy

purse(): number

Your current purse coins.

price(itemId: string): number

Market price of an item by SkyBlock id.

bank(): number

Your bank balance.

parseBalance(text: string): number

Parse a coin balance out of a text string.

Scoreboard & tab list

scoreboard(): string[]

Scoreboard lines (color stripped), top-first.

scoreboardColored(): string[]

Sidebar lines WITH color codes.

scoreboardContains(text: string): boolean

Whether any scoreboard line contains the given text.

tablist(): string[]

Tab-list player names (color stripped).

tablistHeader(): string

Tab-list header text.

tablistFooter(): string

Tab-list footer text (where buffs/effects live).

Buffs, slayer & skills

hasCookieBuff(): boolean

Whether the Booster Cookie buff is active.

hasGodPotion(): boolean

Whether a God Potion is active.

slayerType(): string

Current slayer quest type, or "".

skillXp(skill: string): number

Session XP gained for a skill (Combat/Mining/Farming/...).

resetSkillXp(skill?: string): void

Reset tracked session XP for a skill (or all skills).

Garden

pests(): number

Garden pest count.

visitors(): number

Garden visitor count.

Text

stripColor(text: string): string

Remove §-color codes from a string.

Use chStructures() to consume scanner results without triggering a scan. If a structure is not currently detected, it will not appear in this snapshot.

// Call on the client thread, for example from tick.
for (const structure of skyblock.chStructures()) {
  script.log(structure.name, structure.pos.x, structure.pos.y, structure.pos.z);
  if (structure.bounds) {
    const [minX, minY, minZ, maxX, maxY, maxZ] = structure.bounds;
    // Bounds describe the no-bomb zone, not the structure's footprint.
  }
}

mining

The mining namespace drives the native mining bots, exposes the low-level toolkit those bots are built from (scanning, reach, ability control, stand-spot search), and reads live mining state (fuel, heat, powder, ability, counters).

Bot factory

bot(type: MiningBotType): MiningBot

Build one of the native mining bots. Throws on an unknown type.

botTypes(): string[]

Every valid bot type.

Toolkit: build your own mining bot

scan(range: number): MineBlock[]

Mithril/titanium blocks within `range` of you (max 12).

visiblePoint(block: Vec): Vec | null

A surface point you can aim at, or null if too occluded.

isMined(block: Vec): boolean

Has the block been broken (air/bedrock).

ticksLeft(block: Vec, prev: number): number

Ticks to break; seed prev with -1, feed the result back each tick.

hardnessLeft(block: Vec, prev: number, precisionBuff: boolean): number

Remaining hardness; seed prev with -1.

speedboost(): boolean

Is your mining-speed ability buff active.

activatingAbility(): boolean

Is an ability swap currently in progress.

refreshTools(): void

Re-find your drill in the hotbar (call on enable).

refreshCache(): void

Re-read mining speed / HOTM ability (call after refreshTools).

drillSlot(): number

Hotbar slot of your drill, or -1.

holdDrill(): boolean

Select the drill slot.

ability(): boolean

Fire the drill's pickaxe ability if ready (self-gated).

fakeLookAt(block: Vec): void

Mine `block` while your head points elsewhere.

clearFakeLook(): void

Stop fake-looking.

nearbyPlayers(radius: number): { x: number; y: number; z: number; dist: number; name: string }[]

Real players near you (for avoidance).

standSpots(block: Vec): Vec[]

Reachable-ish feet positions you could mine `block` from (pair with nav.canReach/walkTo).

bestStandSpot(block: Vec): Vec | null

The single best standing spot to mine `block` from (in range, sees it, open, away from players).

Live mining state

fuel(): number

Current drill fuel.

needsRefuel(): boolean

Whether the drill needs refueling.

cold(): number

Current cold level (Glacite).

heat(): number

Current heat level.

commissions(): number

Number of active commissions.

mithrilPowder(): number

Current Mithril Powder.

gemstonePowder(): number

Current Gemstone Powder.

glacitePowder(): number

Current Glacite Powder.

miningXp(): number

Session Mining XP gained.

flowstateBlocks(): number

Flowstate block counter.

flowstateLevel(): number

Flowstate level.

betterTogetherSpeed(): number

Better Together mining-speed bonus.

abilityActive(): boolean

Is the drill ability currently active.

abilityReady(): boolean

Is the drill ability off cooldown / ready.

abilityType(): string

Name of the drill ability.

blocksMined(type: string): number

type: mithril/titanium/gemstone/umber/tungsten/glacite/ore/obsidian/...

wisdomSlot(): number

Hotbar slot of your mining-wisdom item.

crystalHollowsMobs(): Entity[]

Crystal Hollows mobs near you.

goldenGoblins(): Entity[]

Golden Goblins near you.

Types: MiningBotType / MiningBot / MineBlock

type MiningBotType = "mithril" | "gemstone" | "ore" | "oreV2" | "tunnel" | "cobblestone" | "crimson" | "endstone" | "sand" | "obsidian"

The self-driving bots (mithril/gemstone/ore/oreV2/tunnel) tick themselves once started; just give them a start position (defaults to the block under you). The route bots (cobblestone/crimson/endstone/sand/obsidian) need MINE waypoints via opts.waypoints. Don't run a bot while its matching native macro is also running.

MiningBot.enabled: boolean (readonly)

Is the bot currently mining.

MiningBot.finished: boolean (readonly)

Has the bot stopped / run out of work.

MiningBot.start(opts?: { x?: number; y?: number; z?: number; waypoints?: ({ x: number; y: number; z: number } | number[])[] }): void

Self-driving bots: opts is the start pos {x,y,z}. Route bots: opts.waypoints = [{x,y,z}, ...].

MiningBot.stop(): void

Stop the bot (also auto-stopped if the script unloads).

MineBlock { x: number; y: number; z: number; center: Vec; titanium: boolean; dist: number; turn: number; facing: number }

One mithril/titanium block from mining.scan(): block coords, its center, whether it's titanium, distance to you, and how far you'd have to rotate to face it (turn/facing), with the last two used for scoring.

combat

The combat aim engine (humanized melee rotation): aim at a point (e.g. entity.aimPointOn) then leftClick. Call track each tick to follow a moving mob.

Aiming

rotateTo(x: number, y: number, z: number): void

Aim at a fixed point with the humanized combat rotation.

track(x: number, y: number, z: number): void

Call each tick to follow a moving mob.

pitchLocked(x: number, y: number, z: number, pitchLimit: number, positive: boolean): void

Aim at the point but clamp pitch to pitchLimit (positive picks the clamp direction).

State & control

stop(): void

Stop the combat aim engine.

isAiming(): boolean

Whether the engine currently owns/holds an aim target.

rotating(): boolean

Whether a rotation is actively in progress.

etherwarp

Warp onto a support block using the configured handler or blatant follower. Travel methods must be called each tick until they return true or failed() becomes true. Pure planning never starts movement.

Travel

etherwarpTo(x: number, y: number, z: number, options?: number | EtherwarpTravelOptions): boolean

Normal configured handler and fallback behavior. Returns true while landed on the goal. The same goal can be used again after walking away.

pathfindTo(x: number, y: number, z: number, options?: number | EtherwarpTravelOptions): boolean

Requests route planning immediately instead of waiting for the normal direct-attempt fallback.

directTo(x: number, y: number, z: number, options?: number | EtherwarpTravelOptions): boolean

Direct handler attempt without route fallback. Rejects blatant:true.

blatantTo(x: number, y: number, z: number, options?: number | EtherwarpTravelOptions): boolean

Uses the existing blatant follower and the Etherwarp Engine’s configured Rotate or 1 Tick mode. May use its normal fallback when needed.

EtherwarpTravelOptions: { slot?: number; budgetMs?: number; blatant?: boolean }

Optional hotbar slot 0–8; a numeric fourth argument is shorthand for slot. budgetMs is an integer 1–2000, default 75, per planning attempt. blatant:true also selects blatant travel on etherwarpTo/pathfindTo. Unknown keys reject.

stop(): void

Cancels this binding’s owned travel and clears failure state. Call before retrying the same failed request.

failed(): boolean

Current request failed; remains set until stop() or a different goal/options request.

pathfindFailed(): boolean

Reports route failure separately from the overall handler result. Normal direct fallback may still continue.

Route planning

findPath(target: Vec): PathNode[] | null

Synchronous native route query from the player. Null means no usable result.

findPath(from: Vec | null, target: Vec, options?: EtherwarpPlanOptions): PathNode[] | null

Synchronous query with explicit support-block start, or null for the player. Prefer async for larger budgets.

findPathAsync(target: Vec): PathQuery

Background native Etherwarp route query from the player.

findPathAsync(from: Vec | null, target: Vec, options?: EtherwarpPlanOptions): PathQuery

Use null as the first argument to supply options with a player start. Returns a polling handle, not a Promise.

EtherwarpPlanOptions: { etherOnly?: boolean; budgetMs?: number }

etherOnly defaults false for mixed routes. budgetMs is an integer 1–2000, default 75. The existing native API returns positions only: mixed movement types are inferred using the same adjacency rule as EtherwarpTravel; a short warp can be mislabeled as walking. etherOnly:true makes all non-start nodes unambiguously ETHERWARP.

Geometry helpers

range(): number

Range for the held Etherwarp conduit, or 0 without one.

canReach(x: number, y: number, z: number): boolean

Whether a reachable Etherwarp aim can be found from the current position; not just a distance check.

aimPoint(x: number, y: number, z: number): Vec | null

Candidate surface aim point. Check canReach or validate the action rather than treating a non-null point alone as proof of success.

landingPos(x: number, y: number, z: number): Vec

Expected landing feet position; returns the zero vector without a world.

onBlock(x: number, y: number, z: number): boolean

Whether the player stands on the target Etherwarp block.

scan(radius: number): Vec[]

Potential Etherwarp blocks in the nearby scan; candidates still need reachability checks.

Budgets cover native search time, not worker queues, mutex waits, retries, or total travel duration. Null native output cannot distinguish budget exhaustion/no route from an internal native failure. No new native entry point is required.

Query states and cancellation

Example: configurable blatant travel

// Replace this support block before enabling.
const goal = { x: 0, y: 70, z: 0 };
const mod = module.create({
  name: "Blatant travel",
  settings: [{ type: "slider", name: "Budget", min: 1, max: 2000,
               default: 150, increment: 1 }],
});
let done = false;
function stop() { etherwarp.stop(); done = true; }
on("enable", () => { stop(); done = false; });
on("disable", stop);
on("worldLoad", stop);
on("serverDisconnect", stop);
on("tick", () => {
  if (done) return;
  const arrived = etherwarp.blatantTo(goal.x, goal.y, goal.z, {
    budgetMs: Number(mod.setting("Budget")),
  });
  if (arrived) {
    script.log("Arrived");
    done = true;
  } else if (etherwarp.failed()) {
    script.warn("Travel failed; toggle off/on to retry");
    done = true;
  }
});
module.export(mod);

esp

World box ESP (the same renderer the macros use). One verb per effect; colors are ARGB ints. The per-frame verbs draw for a single frame (call them from a tick / render3d hook); the keep* / remove verbs are persistent across frames.

Per-frame block & box effects

blockOutline(x: number, y: number, z: number, color: number): void

Outline the block at (x, y, z) for this frame.

blockFill(x: number, y: number, z: number, color: number): void

Fill the block at (x, y, z) for this frame.

blockGlow(x: number, y: number, z: number, color: number): void

Glow the block at (x, y, z) for this frame.

boxOutline(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, color: number): void

Outline an arbitrary AABB for this frame.

boxFill(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, color: number): void

Fill an arbitrary AABB for this frame.

boxGlow(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, color: number): void

Glow an arbitrary AABB for this frame.

Per-frame entity effects

entityOutline(entityId: number, color: number): void

Outline an entity by id for this frame.

entityGlow(entityId: number, color: number): void

Glow an entity by id for this frame.

entityChams(entityId: number): void

See-through chams highlight on an entity for this frame.

Persistent effects

keepBlockOutline(key: string, x: number, y: number, z: number, color: number): void

Persistent block outline stored under key; redraws every frame until removed.

keepBlockFill(key: string, x: number, y: number, z: number, color: number): void

Persistent block fill stored under key.

remove(key: string): void

Remove a persistent effect by its key.

clear(): void

Remove all persistent effects.

render

2D / HUD drawing backed by NanoVG. Call these from a render or HUD hook. Coordinates are in GUI-scaled pixels and colors are ARGB ints (createColor packs them).

Screen dimensions

getScaledWidth(): number

GUI-scaled screen width in pixels.

getScaledHeight(): number

GUI-scaled screen height in pixels.

getWindowWidth(): number

Raw framebuffer width in pixels.

getWindowHeight(): number

Raw framebuffer height in pixels.

getGuiScale(): number

The active Minecraft GUI scale factor.

Shapes

drawRect(x: number, y: number, w: number, h: number, color: number): void

Filled rectangle. color is an ARGB int.

drawRectRounded(x: number, y: number, w: number, h: number, color: number, radius: number): void

Filled rectangle with rounded corners.

drawHollowRect(x: number, y: number, w: number, h: number, thickness: number, color: number): void

Rectangle outline of the given thickness.

drawHollowRectRounded(x: number, y: number, w: number, h: number, thickness: number, color: number, radius: number): void

Rounded-corner rectangle outline.

drawCircle(x: number, y: number, radius: number, color: number): void

Filled circle centered at (x, y).

drawCircleOutline(x: number, y: number, radius: number, thickness: number, color: number): void

Circle outline of the given thickness.

drawLine(x1: number, y1: number, x2: number, y2: number, thickness: number, color: number): void

Line between two points.

drawArrow(x: number, y: number, width: number, height: number, rotation: number, color: number, thickness: number): void

Arrow glyph, rotated by rotation degrees.

Text

drawText(text: string, x: number, y: number, size: number, color: number): void

Draw text at the given pixel size.

getTextWidth(text: string, size: number): number

Rendered pixel width of text at that size.

Transform & clipping

push(): void

Save the current transform onto the stack.

pop(): void

Restore the last saved transform.

translate(x: number, y: number): void

Translate the current transform.

scale(x: number, y: number): void

Scale the current transform.

rotate(degrees: number): void

Rotate the current transform, in degrees.

pushScissor(x: number, y: number, w: number, h: number): void

Clip subsequent drawing to a rectangle.

popScissor(): void

Remove the last scissor clip.

setGlobalAlpha(alpha: number): void

Multiply the alpha of subsequent draws.

Images

renderImage(path: string, x: number, y: number, w: number, h: number): void

Draw an image file into the rect.

renderImageRounded(path: string, x: number, y: number, w: number, h: number, radius: number): void

Draw an image with rounded corners.

renderImageWithAlpha(path: string, x: number, y: number, w: number, h: number, alpha: number): void

Draw an image at the given alpha.

renderImageWithColor(path: string, x: number, y: number, w: number, h: number, color: number): void

Draw an image tinted by color.

Color

createColor(r: number, g: number, b: number, a: number): number

Pack red, green, blue and alpha channels into a single ARGB color int.

World space

worldText(text: string, x: number, y: number, z: number, color: number, fontSize?: number): void

Draw a projected world label inside worldRenderLast. Text is limited to 1024 characters and fontSize to 8–64 pixels. It is a screen overlay and does not depth-test against terrain.

worldLine(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number, color: number, width?: number, depth?: boolean): void

Draw a world-space line inside worldRenderLast. Width is 0.1–16; depth defaults false (draw through walls), true enables depth testing.

worldTracer(x: number, y: number, z: number, color: number, width?: number, depth?: boolean): void

Draw a line from the current camera toward the target. Its start is nudged 0.2 blocks toward the target to reduce first-person camera artifacts. Same limits as worldLine.

worldBeam(x: number, y: number, z: number, color: number): void

Draw a depth-tested, translucent native-style marker column from above the position to Y=319. It is not the vanilla glowing beacon texture; positions at block Y >= 318 draw nothing.

World-space example

World-space methods are available only inside worldRenderLast; calling them from tick, render2d, or render3d throws. Coordinates must be finite and within ±30,000,000, colors are signed or unsigned 32-bit ARGB values, and at most 256 draw operations are accepted per callback. The frame scope clears after each callback, so these methods do not keep persistent render objects.

on("worldRenderLast", () => {
  render.worldText("Target", target.x, target.y + 1, target.z, 0xFFFFAA00, 16);
  render.worldLine(player.x(), player.y() + 1, player.z(), target.x, target.y, target.z, 0xFF55CCFF, 2, true);
  render.worldTracer(target.x, target.y, target.z, 0xFFFF5555);
  render.worldBeam(target.x, target.y, target.z, 0xFF55AAFF);
});

rotation

The rotation namespace exposes the four rotation engines (general / mining / etherwarp / pathfind) plus planning helpers: lookTo returns an absolute {yaw,pitch}, changeTo returns the signed delta {yaw,pitch}, and distanceTo returns a scalar.

General engine

generalRotateToVec(x: number, y: number, z: number): void

Rotate toward a world point with the general engine.

generalRotateToBlock(x: number, y: number, z: number): void

Rotate toward a block with the general engine.

generalRotateToYawPitch(yaw: number, pitch: number): void

Rotate to an absolute yaw/pitch with the general engine.

generalIsDone(): boolean

Whether the general rotation has finished.

generalStopRotation(): void

Stop the general engine.

generalIsRotating(): boolean

Whether the general engine is actively rotating.

generalWithCallback(callback: () => void): void

Run a callback when the general rotation ends.

Mining engine

miningRotateToVec(x: number, y: number, z: number): void

Rotate toward a world point with the mining engine.

miningRotateToBlock(x: number, y: number, z: number): void

Rotate toward a block with the mining engine.

miningIsDone(): boolean

Whether the mining rotation has finished.

miningStopRotation(): void

Stop the mining engine.

miningIsRotating(): boolean

Whether the mining engine is actively rotating.

Etherwarp engine

etherwarpRotateToVec(x: number, y: number, z: number): void

Rotate toward a world point with the etherwarp engine.

etherwarpRotateToYawPitch(yaw: number, pitch: number, sigma: boolean): void

Rotate to an absolute yaw/pitch with the etherwarp engine.

etherwarpIsDone(): boolean

Whether the etherwarp rotation has finished.

etherwarpStopRotation(): void

Stop the etherwarp engine.

etherwarpIsRotating(): boolean

Whether the etherwarp engine is actively rotating.

etherwarpGetChangeDistance(x: number, y: number, z: number): number

Angular distance you would rotate to face this etherwarp target.

etherwarpOnEndRotation(callback: () => void): void

Run a callback when the etherwarp rotation ends.

Pathfind engine

pathFindRotateToVec(x: number, y: number, z: number): void

Rotate toward a world point with the pathfind engine.

pathFindRotateToYawPitch(yaw: number, pitch: number): void

Rotate to an absolute yaw/pitch with the pathfind engine.

pathFindIsDone(): boolean

Whether the pathfind rotation has finished.

pathFindStopRotation(): void

Stop the pathfind engine.

pathFindIsRotating(): boolean

Whether the pathfind engine is actively rotating.

Planning

lookTo(x: number, y: number, z: number): { yaw: number; pitch: number }

Absolute yaw/pitch needed to face a world point.

changeTo(x: number, y: number, z: number): { yaw: number; pitch: number }

Signed yaw/pitch delta needed to face a world point.

distanceTo(x: number, y: number, z: number): number

Scalar angular distance to face a world point.

math

JS's built-in Math is available too; this namespace adds randomness helpers and a few numeric utilities that Math lacks.

Random

random(): number

Random value in 0..1.

randomInt(min: number, max: number): number

Random integer, inclusive of both bounds.

randomDouble(min: number, max: number): number

Random double between min and max.

randomBool(): boolean

Random true / false.

chance(probability: number): boolean

True with the given probability (0..1).

gaussian(): number

Normal distribution sample (mean 0, sd 1).

randomDelay(minMs: number, maxMs: number): number

Random delay in milliseconds between the bounds.

pick<T>(array: T[]): T | null

Random element of the array, or null when empty.

Numeric helpers

clamp(value: number, min: number, max: number): number

Constrain value to the range [min, max].

lerp(a: number, b: number, t: number): number

Linear interpolation from a to b by t.

round(value: number, decimals: number): number

Round value to the given number of decimals.

distance(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number

3D Euclidean distance between two points.

distance2d(x1: number, z1: number, x2: number, z2: number): number

Horizontal (XZ) distance between two points.

toRadians(degrees: number): number

Convert degrees to radians.

toDegrees(radians: number): number

Convert radians to degrees.

util

Miscellaneous helpers: a pausable stopwatch, number and duration formatting, color stripping, and OS-level notifications.

util

stopwatch(): Stopwatch

Create a pausable real-millisecond timer (handle below).

formatNumber(n: number): string

Compact human formatting, e.g. 1500000 -> "1.50M".

formatDecimal(n: number, digits: number): number

Round n to the given number of decimal places.

extractSeconds(text: string): number

Parse a duration like "1h 20m 5s" into total seconds.

stripColor(text: string): string

Remove Minecraft section-sign (§) color codes.

desktopNotify(title: string, message: string): void

Show an OS desktop notification (vs the in-game notification API).

Stopwatch handle (util.stopwatch())

elapsed(): number

Milliseconds elapsed since the last reset.

hasReached(ms: number): boolean

True once elapsed has reached ms.

reset(): void

Reset elapsed time back to zero.

setElapsed(ms: number): void

Force the elapsed time to a specific value.

pause(): void

Pause counting.

resume(): void

Resume counting.

paused(): boolean

Whether the timer is currently paused.

inventory

Open-container automation. The usual flow: open a menu (entity.interact or use an item), read openItems(), clickSlot(...), then close().

inventory

isOpen(): boolean

Whether a container GUI is currently open.

name(): string

Container title, color-stripped.

openItems(): { slot: number; name: string; id: string; count: number; lore: string[] }[]

Every item currently in the open container.

items(): { slot: number; name: string; id: string; count: number; lore: string[] }[]

Non-empty items in your inventory slots 0–35.

item(slot: number): { slot: number; name: string; id: string; count: number; lore: string[] } | null

One non-empty inventory item, or null for an empty or out-of-range slot.

itemData(slot: number): object | null

Parsed `custom_data` for an inventory item in slots 0–35, or null for an empty/out-of-range slot.

openItemData(menuSlot: number): object | null

Parsed `custom_data` for a non-empty item in the open container, addressed by menu slot id; null if unavailable.

clickSlot(slotId: number, button: number, action: "PICKUP" | "QUICK_MOVE" | "SHIFT" | "SWAP" | "CLONE" | "THROW" | "DROP" | "QUICK_CRAFT" | "PICKUP_ALL"): void

Click a slot. action is one of PICKUP, QUICK_MOVE, SHIFT, SWAP, CLONE, THROW, DROP, QUICK_CRAFT or PICKUP_ALL (SHIFT aliases QUICK_MOVE and DROP aliases THROW).

close(): void

Close the open container.

for (const item of Array.from(inventory.items())) {
  const data = inventory.itemData(item.slot);
  script.log(item.name + " x" + item.count);
  if (data?.ExtraAttributes) {
    script.log(JSON.stringify(data.ExtraAttributes));
  }
}

client

Client-level state: framerate, smoothed server TPS, raw mouse / keyboard, the player’s saved key mappings, the current server, and Skyblock location checks.

client

scriptMessage(text: string): void

Print a client message to chat.

fps(): number

Current client framerate.

tps(): number

Smoothed server TPS (20 = perfect).

leftMouseButtonDown(): boolean

Whether the left mouse button is currently held.

rightMouseButtonDown(): boolean

Whether the right mouse button is currently held.

isKeyDown(key: number): boolean

Whether the given key code is currently held.

keybinds(): { name: string; category: string; key: string; down: boolean }[]

Snapshot of the current vanilla/client key mappings. `name` and `category` are ids; `key` is the current serialized key, such as `key.keyboard.w`. The returned objects are detached and cannot change a mapping.

getServerIP(): string

IP / hostname of the connected server.

isOnHypixel(): boolean

Whether you are connected to Hypixel.

isInSkyblock(): boolean

Whether you are currently in Skyblock.

getCurrentLocation(): string

Current Skyblock location name.

isInLocation(name: string): boolean

Whether the current location matches name.

keybind

Create script-owned keyboard and mouse bindings without registering vanilla KeyMappings. A binding stays with its script and is removed when that script unloads.

keybind

create(name: string, keyCode: number, onPress: () => void): ScriptKeybind

Create one of at most 32 bindings for this script. Names must be non-blank and unique within the script.

ScriptKeybind handle

key(): number

Current key code, or 0 when unbound.

setKey(keyCode: number): void

Change the key code; 0 unbinds it.

isDown(): boolean

Whether this binding’s key is currently held.

name(): string

The name passed to create().

remove(): void

Detach this binding. Safe to call more than once.

Call create and setKey on the client thread.

Pass a GLFW keyboard code from 32 through 348, or a mouse button code from 1 through 7. Code 0 means unbound, so the left mouse button (button code 0) cannot be assigned through this API. The callback runs once on a press; repeats are ignored.

const pingKey = keybind.create("ping", 66, () => {
  script.chat("B pressed");
});

pingKey.key();       // 66
pingKey.isDown();    // currently held?
pingKey.setKey(0);   // unbind
pingKey.setKey(66);
pingKey.remove();

Input gates

Bindings use Polinex's existing custom-keybind dispatch: input is ignored while a screen is open or the client is unauthenticated. They remain active when the script's main module is off. Use client.keybinds()to inspect copied metadata for the player's current mappings; that method does not return editable mapping handles.

notification

In-client toast notifications. Use push with a style constant for full control, or one of the convenience methods for a preset style.

Style constants

SUCCESS: number

Success style id.

ERROR: number

Error style id.

WARNING: number

Warning style id.

INFO: number

Info style id.

Methods

push(durationMs: number, title: string, description: string, style: number): void

Show a notification for durationMs; style is one of the constants above.

success(title: string, description: string): void

Show a success-styled notification.

error(title: string, description: string): void

Show an error-styled notification.

warning(title: string, description: string): void

Show a warning-styled notification.

info(title: string, description: string): void

Show an info-styled notification.

action

A read-only constants proxy of raw protocol action ids (numbers). Pass or compare these where a low-level packet / interaction helper expects an action id.

Player-command actions

START_SNEAKING: number

Begin sneaking.

STOP_SNEAKING: number

Stop sneaking.

STOP_SLEEPING: number

Leave a bed.

START_SPRINTING: number

Begin sprinting.

STOP_SPRINTING: number

Stop sprinting.

RIDING_JUMP: number

Jump while riding a mount.

OPEN_INVENTORY: number

Open the vehicle inventory.

Interaction types

INTERACT: number

Interact (right-click) with an entity.

ATTACK: number

Attack (left-click) an entity.

INTERACT_AT: number

Interact with an entity at a precise hit vector.

Block-break actions

START_DESTROY_BLOCK: number

Begin breaking a block.

ABORT_DESTROY_BLOCK: number

Cancel breaking a block.

STOP_DESTROY_BLOCK: number

Finish breaking a block.

Item actions

DROP_ALL_ITEMS: number

Drop the entire held stack.

DROP_ITEM: number

Drop a single held item.

RELEASE_USE_ITEM: number

Release a charged / in-use item.

Client commands

PERFORM_RESPAWN: number

Respawn after death.

REQUEST_STATS: number

Request statistics from the server.

OPEN_INVENTORY_ACHIEVEMENT: number

Open-inventory achievement trigger.

script

Per-script logging, scoped JSON reads, a persistent key/value store, and tick-based timers. Terse reference below. The narrative guides (logging, persistent store, timing) live under Core Concepts.

Logging & chat

name(): string

This script’s name.

log(message: string): void

Script console + latest.log.

info(message: string): void

Console / log + a local info message in game chat.

warn(message: string): void

Console / log + a local warning in game chat.

error(message: string): void

Console / log + a local error in game chat.

chat(message: string): void

Local game chat only; never sent to the server.

Persistent store

get(key: string): string | null

Read a stored value, or null when absent.

getOr(key: string, fallback: string): string

Read a stored value, or fallback when absent.

set(key: string, value: string): void

Store a value; persisted to polinex/scripts/data/<name>.properties.

has(key: string): boolean

Whether a key is stored.

remove(key: string): void

Delete a stored key.

keys(): string[]

All stored keys.

Local JSON

readJson(path: string): unknown | null

For local folder scripts, parse a relative `.json` file inside that script folder into a detached JS value. Returns null when the file is missing or the JSON value is null. Example: `script.readJson("settings.json") ?? {}`. Limits: 512 KiB, depth 32, and 4096 JSON nodes. Symlinks/hardlinks are rejected; unsupported link-count checks fail closed. Read-only; in-memory bundle JSON is supported.

Timers

after(ticks: number, fn: () => void): number

Wall-clock; run fn once after N * 50ms. Returns a timer id.

every(ticks: number, fn: () => void): number

Wall-clock; run fn every N * 50ms until cleared or unload. Returns a timer id.

clearTimer(id: number): void

Cancel a timer created by after / every.

const settings = script.readJson("settings.json") ?? {};
const radius = settings.radius ?? 6;
script.log("Using scan radius " + radius);

Packets

Register a handler with on("packetReceive", handler) or on("packetSend", handler) to see raw protocol traffic before the client (or server) acts on it. Both hooks hand you a single ScriptPacketEvent: read its stable name, pull curated fields out of data(), and optionally cancel() the packet. See the Events section for how hooks are gated by your module.

The ScriptPacketEvent

Every packetReceive / packetSend handler (and the second argument to chatReceive / chatSend) receives this object:

Identity

event.name

Stable, obfuscation-proof packet id, e.g. "CLIENTBOUND_SYSTEM_CHAT" (or "UNKNOWN" if unmapped). This is the value you compare against packet.* constants.

event.type

Raw packet class simpleName, obfuscated in production builds. Use it for debug logging only; never compare it.

Control

event.cancel()

Drop the packet before the client or server handles it.

event.isCanceled()

Whether an earlier handler already canceled this packet.

Payload

event.data()

Lazily-computed field map for curated high-value packets; returns {} for everything else. The shapes are listed below.

event.formatted()

Chat packets only: the message as a legacy §-code string. Empty string for non-chat packets.

event.json()

Chat packets only: the message as vanilla component JSON. Empty string for non-chat packets.

Always compare event.name, never event.type

name is stable across obfuscation and equals the packet.* constant values. type is the raw class name and is obfuscated in production. A check like event.type === "ClientboundSetHealthPacket" WONT WORK! Always write event.name === packet.CLIENTBOUND_SET_HEALTH.
on("packetReceive", (e) => {
  // compare the STABLE name to packet.* constants — never e.type
  if (e.name === packet.CLIENTBOUND_SET_HEALTH) {
    const d = e.data();            // { health, food, saturation }
    script.log("health " + d.health + " / food " + d.food);
    return;
  }

  if (e.name === packet.CLIENTBOUND_SOUND) {
    const d = e.data();            // { sound, source, x, y, z, volume, pitch }
    if (d.sound === "minecraft:block.note_block.pling") {
      e.cancel();                  // stop it before the client plays it
    }
  }
});

// outgoing packets work exactly the same on packetSend
on("packetSend", (e) => {
  if (e.name === packet.SERVERBOUND_SWING) script.debug("swing");
});

The packet.* constants

packet is a read-only object holding a constant for every packet in the modern protocol, all clientbound and serverbound, roughly 229 of them. It also carries a set of 1.8-era name aliases (for example C03PacketPlayer and S2FPacketSetSlot) that map one-to-one onto their modern equivalents, for about 264 constants in all. Each value equals the matching event.name. You do not need to memorize them. The script editor autocompletes the full list as you type packet.. A representative slice:

packet.CLIENTBOUND_SYSTEM_CHAT
packet.CLIENTBOUND_SET_HEALTH
packet.CLIENTBOUND_SOUND
packet.CLIENTBOUND_CONTAINER_SET_SLOT
packet.CLIENTBOUND_OPEN_SCREEN
packet.CLIENTBOUND_DISCONNECT
packet.CLIENTBOUND_ADD_ENTITY
packet.CLIENTBOUND_BLOCK_UPDATE

packet.SERVERBOUND_SWING
packet.SERVERBOUND_USE_ITEM
packet.SERVERBOUND_PLAYER_ACTION
packet.SERVERBOUND_CONTAINER_CLICK

// 1.8-era aliases — same value as the modern name
packet.C03PacketPlayer
packet.S2FPacketSetSlot

Packet delivery: three tiers

Not every packet reaches packetReceive. The receive hook runs synchronously on the network thread, so the very high-rate types are held back. Delivery falls into three tiers.

1. Default-delivered: arrive with no opt-in

Everything not named in the two tiers below reaches your handler as-is. That now includes CLIENTBOUND_BLOCK_UPDATE, SET_EQUIPMENT, UPDATE_ATTRIBUTES, ADD_ENTITY, REMOVE_ENTITIES, TELEPORT_ENTITY, SET_TIME, AWARD_STATS, your own CLIENTBOUND_PLAYER_POSITION, and your own CLIENTBOUND_SET_ENTITY_MOTION(the local player's velocity).

2. Auto-detected: reference the packet.X constant to opt in

These arrive only if your script's source references the matching packet.* constant. At load the engine statically scans your script and every .js in its folder for packet.X references and opts you into exactly those: CLIENTBOUND_MOVE_ENTITY_POSITION, MOVE_ENTITY_ROTATION, MOVE_ENTITY_POSITION_ROTATION, ROTATE_HEAD, ENTITY_POSITION_SYNC, ENTITY_EVENT, ANIMATE, SET_PLAYER_TEAM, CUSTOM_PAYLOAD, and other entities' SET_ENTITY_MOTION. Just naming the constant anywhere in your code is the switch, with no API call, e.g. if (e.name === packet.CLIENTBOUND_ROTATE_HEAD). The scan is static: a bare string literal (e.name === "CLIENTBOUND_ROTATE_HEAD") or a name you assemble at runtime is not detected. You must reference the packet.X constant.

3. Hard-dropped: never arrive, even if referenced

CLIENTBOUND_LEVEL_PARTICLES, CLIENTBOUND_SECTION_BLOCKS_UPDATE, and CLIENTBOUND_SET_ENTITY_DATA are far too high-rate for the synchronous hook and are dropped unconditionally. Referencing the constant changes nothing. Read that state from the Entity API in a tick handler instead (entity.near() / entity.byId() / entity.withName(), which now expose yaw, pitch, and headYaw).

data() shapes: clientbound

Roughly three dozen high-value packets have curated extractors. Calling data() on any other packet returns an empty object {}, so guard on the fields you expect. Incoming packets:

CLIENTBOUND_SYSTEM_CHAT            { text, overlay }
CLIENTBOUND_PLAYER_CHAT            { sender, text }                      // sender = UUID string
CLIENTBOUND_SET_HEALTH            { health, food, saturation }
CLIENTBOUND_SET_EXPERIENCE        { progress, level, total }
CLIENTBOUND_SET_ENTITY_MOTION     { id, x, y, z }                       // own player = default; OTHER entities = auto-detect (reference packet.CLIENTBOUND_SET_ENTITY_MOTION)
CLIENTBOUND_TELEPORT_ENTITY       { id, x, y, z, yaw, pitch, onGround }  // default-delivered
CLIENTBOUND_ENTITY_POSITION_SYNC  { id, x, y, z, yaw, pitch, onGround }  // auto-detect (reference packet.CLIENTBOUND_ENTITY_POSITION_SYNC)
CLIENTBOUND_PLAYER_POSITION       { id, x, y, z, yaw, pitch }            // your own absolute position — default-delivered
CLIENTBOUND_ADD_ENTITY            { id, type, x, y, z }                  // default-delivered; type = "minecraft:zombie"
CLIENTBOUND_REMOVE_ENTITIES       { ids }                               // default-delivered; ids: number[]
CLIENTBOUND_UPDATE_MOB_EFFECT     { id, effect, amplifier, duration }   // effect = "minecraft:speed"
CLIENTBOUND_REMOVE_MOB_EFFECT     { id, effect }
CLIENTBOUND_SET_EQUIPMENT         { id, slots }                         // default-delivered; slots: {slot, item:{name,id,count}|null}[]
CLIENTBOUND_UPDATE_ATTRIBUTES     { id, count }                         // default-delivered
CLIENTBOUND_DAMAGE_EVENT          { id, sourceType }
CLIENTBOUND_RESPAWN               { dimension }
CLIENTBOUND_GAME_EVENT            { event, param }                      // event = "START_RAINING" / "UNKNOWN"
CLIENTBOUND_SET_TITLE_TEXT        { text }
CLIENTBOUND_SET_SUBTITLE_TEXT     { text }
CLIENTBOUND_SET_ACTION_BAR_TEXT   { text }
CLIENTBOUND_BOSS_EVENT            { operation }                         // ADD / REMOVE / UPDATE_PROGRESS / ...
CLIENTBOUND_PLAYER_INFO_UPDATE    { count }
CLIENTBOUND_SET_OBJECTIVE         { name, displayName }
CLIENTBOUND_SET_SCORE             { owner, objective, value }
CLIENTBOUND_SET_DISPLAY_OBJECTIVE { slot, objective }
CLIENTBOUND_BLOCK_UPDATE          { x, y, z, block }                    // default-delivered; block = "minecraft:stone"
CLIENTBOUND_SOUND                 { sound, source, x, y, z, volume, pitch }
CLIENTBOUND_CONTAINER_SET_SLOT    { containerId, slot, stateId, item }  // item: {name,id,count} | null
CLIENTBOUND_OPEN_SCREEN           { containerId, menuType, title, titleFormatted }
CLIENTBOUND_CONTAINER_CLOSE       { containerId }
CLIENTBOUND_DISCONNECT            { reason, reasonFormatted }

Which of these shapes need an opt-in

Every curated shape above is default-delivered except two, which are auto-detected: they only reach packetReceive if your script references their packet.* constant (see Packet delivery): CLIENTBOUND_ENTITY_POSITION_SYNC, and CLIENTBOUND_SET_ENTITY_MOTIONfor entities other than yourself (your own player's velocity is default-delivered). None of the shapes above are hard-dropped, so once delivered each shape is exactly as listed. To follow other entities tick-by-tick, read the Entity API from a tick handler instead.

data() shapes: serverbound

Outgoing packets you can read (and cancel) on packetSend:

SERVERBOUND_SWING            { hand }                          // "MAIN_HAND" / "OFF_HAND"
SERVERBOUND_USE_ITEM         { hand }
SERVERBOUND_PLAYER_ACTION    { action, x, y, z, direction }
SERVERBOUND_CHAT             { message }
SERVERBOUND_CHAT_COMMAND     { command }
SERVERBOUND_CONTAINER_CLICK  { containerId, slot, button }
SERVERBOUND_SET_CARRIED_ITEM { slot }

Cancel a packet inside its handler, before it continues. See Synchronous cancellation for an example and how to schedule follow-up work on the client thread.

Watching entities and other players' look

Remember the delivery tiers (Packet delivery): the hard-dropped packets (LEVEL_PARTICLES, SECTION_BLOCKS_UPDATE, SET_ENTITY_DATA) never arrive, and the entity-movement packets (MOVE_ENTITY_*, ROTATE_HEAD, ENTITY_POSITION_SYNC, other entities' SET_ENTITY_MOTION) only arrive when you opt in by referencing their packet.*constant. For tick-accurate entity tracking, including another player's yaw / pitch / headYaw, read the Entity API from a tick handler instead of watching packets.

Settings

Modules, macros, and helpers all take the same settings: [...]array on their config object. Each entry uses one of the client's own setting types, so a script's settings look and behave exactly like the ones the client ships with.

There are two ways to author an entry, and you can mix them freely in the same array: config objects (a plain { type, name, ... } literal) and builder constructors (SliderSetting(...), BooleanSetting(...), …) that return a live handle you can read and write from anywhere.

Callbacks fire on a user change only

Every setting callback (onChange, onEnable, onDisable, onClick, and a builder's .onChange(fn)) runs only when a user changes the control in the GUI. They never fire while the config file loads at startup, so side effects (chat, warps, toggling other features) placed in them will not replay on load.

Config-object settings

Every entry shares a small base: a required name, an optional id (defaults to name, which is the key you read and write by), an optional description, and an optional visible (alias shouldRender) predicate that hides the row when it returns false. The remaining keys depend on the type.

const h = module.create({
  name: "Demo",
  category: "QOL",
  settings: [
    { type: "boolean",       name: "Enabled",   default: true,
        onChange: (v) => script.log("enabled = " + v) },
    { type: "slider",        name: "Radius",    min: 1, max: 20, default: 8, increment: 1,
        onChange: (v) => {} },
    { type: "decimalSlider", name: "Speed",     min: 0, max: 1,  default: 0.5, increment: 0.05 },
    { type: "plusMinus",     name: "Count",     min: 0, max: 9,  default: 3 },
    { type: "range",         name: "Levels",    min: 0, max: 100, defaultMin: 10, defaultMax: 60,
        onChange: (lo, hi) => {} },
    { type: "decimalRange",  name: "Window",    min: 0, max: 5,   defaultMin: 0.5, defaultMax: 2.5 },
    { type: "dropdown",      name: "Mode",      options: ["Fast", "Safe"], default: "Safe",
        onChange: (opt, idx) => {} },
    { type: "color",         name: "Highlight", default: 0xFF00FFAA, allowAlpha: true },  // 0xAARRGGBB
    { type: "string",        name: "Label",     default: "hi",
        onChange: (text) => {} },
    { type: "button",        name: "Reset",     buttonText: "Reset Now", onClick: () => doReset() },
    { type: "multiBool",     name: "Filters",   options: ["Ore", "Gem"], defaults: [true, false],
        onChange: (opt, on) => {} },
  ],
});

Builder-style settings

Each type also has a global constructor (the same names the client uses internally). The first two arguments are always the display name and description; the name doubles as the id. Each returns a ScriptSetting handle with a read/write .value and a chainable .onChange(fn). Pass the handles straight into settings: [...] (mixed with config objects) or attach them later with addSettings(...).

const interval    = SliderSetting("Interval (s)", "How often to check", 1, 30, 5);
const onlyChanges = BooleanSetting("Only On Change", "Only report changed values", true);
onlyChanges.onChange(v => script.log("now " + v));   // chainable change hook

const mod = module.create({ name: "Purse Tracker", settings: [interval, onlyChanges] });

// attach more after creation (works on module / macro / helper handles):
const mode = DropdownSetting("Mode", "How the tracker runs", ["Fast", "Safe"], "Safe");
mod.addSettings(mode);

on("tick", () => {
  if (interval.value >= 5) onlyChanges.value = false;   // read AND write via .value
});

Setting types

Every type, with its constructor signature, the extra keys its config object accepts (defaults in parentheses), and its onChange signature.

One per setting type

BooleanSetting(name, description, def?)

type: "boolean" · default? (false). A toggle. onChange(value: boolean), plus optional onEnable() / onDisable().

SliderSetting(name, description, min, max, def, inc?)

type: "slider" · min, max, default, increment? (1). Integer slider. onChange(value: number).

DecimalSliderSetting(name, description, min, max, def, inc?)

type: "decimalSlider" · min, max, default, increment? (0.01). Floating-point slider. onChange(value: number).

PlusMinusSetting(name, description, min, max, def, inc?)

type: "plusMinus" · min, max, default, increment? (1). Integer +/- stepper. onChange(value: number).

RangeSetting(name, description, min, max, defMin, defMax)

type: "range" · min, max, defaultMin?, defaultMax?. Integer min/max range. onChange(min, max).

DecimalRangeSetting(name, description, min, max, defMin, defMax)

type: "decimalRange" · min, max, defaultMin?, defaultMax?. Floating-point range. onChange(min, max).

DropdownSetting(name, description, options[], def?)

type: "dropdown" · options, default? (option label or index) or defaultIndex?. onChange(option, index).

ColorSetting(name, description, def?)

type: "color" · default? (0xAARRGGBB, 0xFFFFFFFF), allowAlpha? (true). onChange(color: number). Always shown.

StringSetting(name, description, def?)

type: "string" · default? (""). Text input. onChange(text: string).

MultiBoolSetting(name, description, options[], defaults?)

type: "multiBool" · options, defaults? (alias default?) is the per-option initial state. Independent toggles. onChange(option, enabled).

ButtonSetting(name, description, onClick?)

type: "button" · buttonText? (alias label?), onClick? (alias onChange). A clickable action row with no stored value. onClick(). Always shown.

color and button are native final types: they are always shown, so their visible / shouldRender (and builder visibleWhen / dependsOn) predicates are ignored.

The setting handle & runtime access

A builder constructor returns a ScriptSetting handle you can hold onto and read or write live. Separately, the handle returned by module.create / macro.create / helper.register lets you reach any setting (config-object or builder) by its id.

ScriptSetting handle (builder settings)

setting.value

Read AND write the current value.

setting.get()

Same as reading .value.

setting.set(value)

Same as writing .value; returns false on an unusable value.

setting.onChange(fn)

Register a change hook (fired on user changes); chainable.

setting.isEnabled()

True when the value is truthy (e.g. a toggle is on).

setting.name()

The display name, which also doubles as the default id.

From the module / macro / helper handle

handle.setting(id)

Read a setting by id or name; null when unknown.

handle.setSetting(id, value)

Write a setting; fires its normal onChange. Returns false on an unknown id or a bad value.

handle.settings()

Array of every setting id on the handle.

handle.addSettings(...settings)

Attach builder-style settings after creation.

What setting() / setSetting() read & write, per kind

boolean

boolean

slider / plusMinus

number (integer)

decimalSlider

number

dropdown

the selected option string (setSetting accepts a string or an index)

color

number (0xAARRGGBB)

string

string

range / decimalRange

{ min, max } (setSetting accepts { min, max } or [min, max])

multiBool

string[] of enabled options (setSetting accepts string[] or { [option]: boolean })

const h = module.create({ name: "Demo", settings: [
  { id: "radius", type: "slider",   name: "Radius", min: 1, max: 20, default: 8 },
  { id: "mode",   type: "dropdown", name: "Mode",   options: ["Fast", "Safe"] },
]});

h.setting("radius");          // -> 8 (current value)
h.setSetting("radius", 12);   // set it; runs the setting's normal onChange
h.setSetting("mode", "Fast"); // dropdown accepts an option string or an index
h.settings();                 // -> ["radius", "mode"]

Conditional visibility

A setting can hide its row until a condition holds. The predicate is re-checked every frame while the GUI is open, so keep it cheap, just read another setting. Builder handles offer three chainable options: .visibleWhen(fn) (any predicate), .dependsOn(other) (visible while other is truthy) or .dependsOn(other, value) (visible only when it equals value), and .if(condition) which accepts another setting, a () => boolean, or a plain boolean.

const advanced = BooleanSetting("Advanced", "Show extra controls", false);
const tuning   = SliderSetting("Tuning", "Advanced tuning amount", 0, 10, 5).dependsOn(advanced);

const mode  = DropdownSetting("Mode", "Execution mode", ["Fast", "Safe"], "Safe");
const speed = DecimalSliderSetting("Speed", "Fast-mode speed", 0, 1, 0.5).dependsOn(mode, "Fast");

const fancy = BooleanSetting("Fancy", "Fancy look", false);
const color = ColorSetting("Color", "Highlight", 0x55FFFFFF).visibleWhen(() => fancy.value);
// .if(fancy) would do the same as .visibleWhen(() => fancy.value)

module.create({ name: "Demo", settings: [advanced, tuning, mode, speed, fancy, color] });

Config objects use a visible function instead (alias shouldRender), returning a boolean:

const h = module.create({ name: "Demo", settings: [
  { id: "advanced", type: "boolean", name: "Advanced", default: false },
  { type: "slider", name: "Tuning", min: 0, max: 10, default: 5,
      visible: () => h.setting("advanced") === true },   // hidden until Advanced is on
]});
Limits & Gotchas

Limits & gotchas

The scripting engine is deliberately locked down and shares the client with everything else running in the game. Most of these rules exist to keep a misbehaving script from taking the whole client down with it. Read them before you ship anything nontrivial.

The sandbox is sealed

No filesystem, network, reflection, or threads

The script context runs with I/O, native access, thread creation, process creation, environment access, and polyglot access all switched off, and Reflect, Proxy, load, and eval stripped from the global scope. You cannot perform arbitrary filesystem I/O, open sockets, spawn threads, or reflect your way around the host-access filter. This is by design and not configurable.

Anything heavy lives in the kotlin (player, nav, world, skyblock, and friends). Your script orchestrates them; it does not reach outside the game on its own. If a script needs to persist a little data across reloads, use the persistent store on script. The routes and world facades provide specific saved-route and snapshot operations without granting arbitrary filesystem access.

Runaway and blocking loops

Statement limits are not a time budget

Each outer script entry resets a 5,000,000-statement limit. It can abort runaway JavaScript, but it is not a wall-clock, native-work, or heap limit. A slow host call or busy loop can still stall the event thread. Never busy-wait for a path or a player-state change.

The engine runs interpreter-only inside Minecraft (no JIT), so per-tick JavaScript needs to stay light: orchestrate the facades, don't loop over the world in JS. When you need to wait, use the async timing helpers (sleep, sleepTicks, setTimeout) or the tick hook, which return control to the client between turns.

// wrong — busy-waits on the event thread
// while (!player.onGround()) {}

// right — yields back to the game each tick
on("tick", () => {
  if (player.onGround()) doThing();
});

Packet delivery is tiered

Not every packet reaches packetReceive

Because the receive hook runs synchronously on the network thread, delivery splits into three tiers. Default-delivered: most packets arrive with no opt-in, including BLOCK_UPDATE, ADD_ENTITY, REMOVE_ENTITIES, TELEPORT_ENTITY, SET_EQUIPMENT, UPDATE_ATTRIBUTES, SET_TIME, AWARD_STATS, your own PLAYER_POSITION, and your own player's SET_ENTITY_MOTION. Auto-detected: the entity-movement types (MOVE_ENTITY_*, ROTATE_HEAD, ENTITY_POSITION_SYNC, ENTITY_EVENT, ANIMATE, SET_PLAYER_TEAM, CUSTOM_PAYLOAD, and other entities' SET_ENTITY_MOTION) arrive only if your script's source references their packet.* constant. The opt-in scan is static, so a bare string literal or a name built at runtime is not detected. Hard-dropped: LEVEL_PARTICLES, SECTION_BLOCKS_UPDATE, and SET_ENTITY_DATA never arrive at all; read that state from the world/entity in a tick handler instead.

So a packet whose data() shape the type file advertises may still need an opt-in (or never arrive at all). See Packet delivery for the exact tier of each packet.

For the packets you do receive, always compare event.name against a packet.* constant. Never compare event.type. That is the packet's raw runtime class name, which is obfuscated in production builds and only kept around for debugging. See Packets for the full list of mapped names and their curated data shapes.

Timer callbacks wait for their module

Timer callbacks and pending sleeps are deferred while the main module is off, but their deadlines keep using wall-clock time. An overdue callback can run soon after re-enabling; it does not preserve its old remaining delay. Unload/reload cancels timers outright. Clear timers on disable if the delayed action should not resume. See Timer lifecycle.