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
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.jsAutocomplete & 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
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
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.
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
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
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(): booleanTrue for every state except pending. Unlike nav.finished(), this describes a query, not movement.
failed(): booleanTrue for terminal states other than found. False while pending.
error(): string | nullExplanation when available; cancelled/stale may have no message.
result(): PathNode[] | nullResult 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(): voidDiscards 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
polinex/scripts/data/<name>.properties, keyed by the script's NAME.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
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
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 aboveMacros
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 moduleFind 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
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 LocationLifecycle 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
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
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 issuesHelpers
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 loadedmacro.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);player
Read and control your own player, including position, motion, rotation, keys/clicks, inventory, and live state.
Position & motion
pos(): VecCurrent position (feet).
blockPos(): VecCurrent position floored to block coordinates.
distance(target: Vec): number3D distance to a point.
distanceFlat(target: Vec): numberHorizontal (XZ) distance to a point, ignoring height.
x(): number; y(): number; z(): numberCurrent X / Y / Z coordinate.
prevX(): number; prevY(): number; prevZ(): numberPosition last tick (for interpolation).
motionX(): number; motionY(): number; motionZ(): numberVelocity component this tick.
setMotionX(x: number): void; setMotionY(y: number): void; setMotionZ(z: number): voidSet a velocity component.
setPosition(x: number, y: number, z: number): voidMove the player to exact coordinates.
speed(): numberCurrent horizontal speed.
predictLanding(): Vec | nullWhere you'll land given current velocity.
willCollide(ticks: number): booleanWhether you'll hit something within the given number of ticks.
Rotation
yaw(): numberCurrent yaw.
pitch(): numberCurrent pitch.
setYaw(yaw: number): voidSet yaw directly.
setPitch(pitch: number): voidSet pitch directly.
rotateTo(target: Vec): voidSmoothly rotate to look at a point.
rotateTo(x: number, y: number, z: number): voidSmoothly rotate to look at scalar coordinates.
rotateToYawPitch(yaw: number, pitch: number): voidRotate to an absolute yaw/pitch.
rotateToEntity(id: number): voidRotate to look at an entity by id.
rotationDone(): booleanWhether the active rotation has finished.
stopRotating(): voidCancel the active rotation.
Input & movement
setKey(key: string, state: boolean): voidPress/release a key. key: "forward","backward","right","left","jump","sneak","sprint","attack","use".
releaseKeys(): voidRelease 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): booleanIs a key held? key: same set as setKey.
resetWASD(): voidRelease the movement keys.
stopAllMovement(): voidRelease every input and halt movement.
setKeysTo(x: number, y: number, z: number, sneak: boolean): voidPress WASD toward a point.
setOptimalKeysTo(x: number, y: number, z: number, strafe: boolean): voidStrafe-aware walk-toward (the macros' move primitive).
setSneaking(state: boolean): voidHold or release sneak.
setSprinting(state: boolean): voidHold or release sprint.
jump(): voidTrigger a jump.
leftClick(): booleanSimulate a left click.
rightClick(): booleanSimulate a right click.
rightClickPacket(): booleanSend a right-click via packet.
forceLeftClick(): voidForce an immediate left click.
forceRightClick(): voidForce an immediate right click.
legitLeftClick(): voidHuman-timed left click.
legitRightClick(): voidHuman-timed right click.
walkTo(target: Vec): booleanBegin walking to a Vec target.
walkTo(x: number, y: number, z: number): booleanBegin walking to scalar coordinates.
walkDone(): booleanWhether the walk has arrived.
stopWalking(): voidStop the current walk.
Inventory & items
setHeldItemSlot(slot: number): booleanSelect a hotbar slot (0-8).
heldItemSlot(): numberCurrent hotbar slot index.
getItemByName(name: string): numberHotbar slot of an item by name, or -1.
itemByName(name: string): numberHotbar slot of an item by name, or -1.
itemByLore(line: string): numberHotbar slot of an item by lore substring, or -1.
fishingRodSlot(): numberHotbar slot of your fishing rod, or -1.
etherwarpSlot(): numberHotbar slot of your etherwarp item, or -1.
aoteSlot(): numberHotbar slot of your Aspect of the End, or -1.
swordSlot(): numberHotbar slot of your sword, or -1.
distanceWeaponSlot(): numberHotbar slot of a ranged/distance weapon, or -1.
manaRegenSlot(): numberHotbar slot of a mana-regen item, or -1.
itemByNames(names: string[]): numberHotbar slot of the first item matching any name, or -1.
itemBySkyblockId(ids: string[]): numberHotbar slot of the first item matching any Skyblock id, or -1.
itemCount(name: string): numberTotal count of a named item across the inventory.
itemLore(slot: number): string[]Lore lines of an inventory slot (0-35).
itemId(slot: number): stringItem id in an inventory slot (0-35).
itemInfo(slot: number): { name: string; id: string; count: number; lore: string[] } | nullFull item info for an inventory slot, or null when empty.
useItem(slot: number): voidActivate an item (swap/use/swap back).
useItemId(id: string): voidActivate an item by id (swap/use/swap back).
isHoldingItem(name: string): booleanWhether the held item's name contains this string.
heldItemName(): stringName of the currently held item.
swapToName(name: string): booleanSelect the hotbar slot holding a named item.
getEmptyInventorySlots(): numberNumber of empty inventory slots.
getInventoryName(): string | nullTitle of the open inventory/chest, or null.
State & sensing
name(): stringPlayer username.
health(): numberCurrent health.
onGround(): booleanStanding on the ground.
airBorne(): booleanIn the air.
sneaking(): booleanCurrently sneaking.
sprinting(): booleanCurrently sprinting.
moving(): booleanCurrently moving.
isAlive(): booleanPlayer is alive.
dead(): booleanPlayer is dead.
hurtTime(): numberTicks remaining in the hurt animation (0 when not hurt).
ticksExisted(): numberTicks the player entity has existed.
fallDistance(): numberCurrent fall distance.
getFallDistance(): numberCurrent fall distance (alias).
getJumpHeight(): numberConfigured jump height.
onLadder(): booleanOn a ladder/vine.
inWater(): booleanIn water.
inLava(): booleanIn lava.
eating(): booleanCurrently eating.
usingItem(): booleanCurrently using an item.
burning(): booleanOn fire.
collided(): booleanColliding with a block.
collidedHorizontally(): booleanColliding horizontally.
collidedVertically(): booleanColliding vertically.
isStandingStill(): booleanNot moving.
isBlockAhead(): booleanSolid block directly ahead.
isBlockToLeft(): booleanSolid block to the left.
isBlockToRight(): booleanSolid block to the right.
isPotionActive(effectId: string): booleanEffect name or namespaced id, e.g. "speed" or "minecraft:speed".
mouseOverBlock(): Vec | nullThe block under your crosshair, or null.
isOnBlockEdge(): booleanWhether you're standing on a block edge.
shouldSneak(): booleanWhether the client thinks you should sneak (edge protection).
Entity queries
getEntityById(id: number): number | nullResolve an entity by id (returns the id if present, else null).
isEntityAlive(id: number): booleanWhether an entity by id is alive.
getEntityX(id: number): number | null; getEntityY(id: number): number | null; getEntityZ(id: number): number | nullCoordinate of an entity by id, or null.
getEntityName(id: number): string | nullName of an entity by id, or null.
distanceToEntity(id: number): number | nullDistance to an entity by id, or null.
Chat & GUI
sendMessage(msg: string): voidSend a chat message.
sendCommand(command: string): voidRun a command.
isGuiOpen(): booleanWhether any GUI/screen is open.
isChestOpen(name: string): booleanWhether 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): booleanClear straight-line walk (ignoring height)?
isOnBlock(point: Vec, range: number): booleanAre you standing on/near this point?
isAir(pos: Vec): booleanIs the block at pos air?
isSolid(pos: Vec): booleanWhether the live block state is non-air; this does not guarantee collision support.
isBedrock(pos: Vec): booleanIs the block at pos bedrock?
isPathfindable(pos: Vec): booleanCan the player stand/path here?
blockId(pos: Vec): stringBlock registry id at pos, e.g. "minecraft:stone".
blockState(pos: VecInput): { id: string; properties: Record<string, string> } | nullLoaded 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): stringblockId 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): stringBiome registry id, e.g. "minecraft:the_end", or "".
blockHeight(pos: Vec): numberTop-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): VecBlock center.
topCenter(pos: Vec): VecTop-face center (stand/aim target).
groundPos(pos: Vec): VecDrop a position to the floor block.
groundCenter(pos: Vec): VecFloor-block top-center under a position.
eyePos(): VecThe 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 } | nullRaytrace that also reports the entity it hits (optionally filtered by types), or null.
isVisible(pos: Vec): booleanClear 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): WorldHandleCaptures 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): WorldHandleEmpty snapshot; default name world. Captures nothing until capture/record is called.
world.load(name: string): WorldHandleLoads a saved snapshot. File/format errors throw.
world.loadOrBlank(name: string, config: WorldConfig | null): WorldHandleLoads 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): booleanList, inspect, or explicitly delete saved snapshot files.
world.code(pos: Vec): numberLive-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(): numberSnapshot 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): booleanSnapshot 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): booleanCoarse 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(): voidChanges only the snapshot data, not the Minecraft world.
Capture, record, save
capture(radiusChunks: number, onDone: Function | null): number; recapture(radiusChunks: number, onDone: Function | null): numberCapture 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): voidWalk-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(): numberRecorder state and missing nearby columns.
save(onDone: ((error: string) => void) | null): void; saveAs(name: string, onDone: ((error: string) => void) | null): voidAsynchronous 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 }): ScanHandleblocks 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): stringName for a classification code.
isPassable(code: number): boolean; isSolid(code: number): boolean; blockHeight(code: number): numberCoarse classification helpers, not a full live collision or path validation.
ScanHandle
start(): void; stop(): void; running(): booleanManage scanning explicitly. Stop on module disable; owned scanners are disposed on unload.
invalidate(): void; version(): numberRequest 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): stringCached 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(): numberScanning 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): PanelHandleCreates 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(): voidopenScreen 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 }): HudHandleDefault 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 | nullResolve one entity by network id, same shape, or null.
nearest(radius: number): Entity | nullNearest mob.
nearestNamed(name: string, radius: number): Entity | nullNearest entity whose name contains `name`.
Interaction & aiming
interact(id: number): booleanRight-click an NPC by id (opens menus).
canSee(id: number): booleanClear line of sight to the entity?
mouseOver(): Entity | nullEntity under your crosshair.
aimPointOn(id: number): Vec | nullLead aim point on a mob.
visiblePointOn(id: number): Vec | nullVisible, raytrace-verified aim point.
standingPosOf(id: number): Vec | nullBlock a mob stands on (walk/path here).
Entity shape
id: numberNetwork entity id.
name: stringDisplay name.
x: number; y: number; z: numberEntity position.
dist: numberDistance from the player.
yaw: numberBody yaw in degrees. Read each tick to watch another player's look.
pitch: numberPitch in degrees.
headYaw: numberHead yaw in degrees (falls back to body yaw for non-living entities).
player: booleanWhether the entity is a player.
type: stringEntity type registry id, e.g. "minecraft:armor_stand".
health: number | null; maxHealth: number | nullHealth for living entities; null when the value does not apply.
customName: string | nullThe 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): ScriptRouteEditorFolder 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): booleanCreate 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 | nullRead saved route data, including branch definitions. In folder mode, file defaults to the selected waypoint file.
editor.save(name: string, waypoints: RouteWaypoint[], file?: string): booleanReplace 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): booleanOpen 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 | nullRead 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(): stringCurrent SkyBlock location, e.g. "DWARVEN_MINES".
area(): stringRaw "Area: X" from the tab list (finer than location()), or "".
inLocation(name: string): booleanWhether you are currently in the named location.
inArea(aabb: number[]): booleanWhether 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(): stringSkyBlock id of the held item (e.g. "TITANIUM_DRILL_4"), or "".
findMiningTools(): voidLocate your mining tools in the hotbar.
hasMiningTool(): booleanWhether a mining tool was found in your hotbar.
hasEtherwarp(): booleanWhether an Etherwarp item can be found in your inventory.
Economy
purse(): numberYour current purse coins.
price(itemId: string): numberMarket price of an item by SkyBlock id.
bank(): numberYour bank balance.
parseBalance(text: string): numberParse 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): booleanWhether any scoreboard line contains the given text.
tablist(): string[]Tab-list player names (color stripped).
tablistHeader(): stringTab-list header text.
tablistFooter(): stringTab-list footer text (where buffs/effects live).
Buffs, slayer & skills
hasCookieBuff(): booleanWhether the Booster Cookie buff is active.
hasGodPotion(): booleanWhether a God Potion is active.
slayerType(): stringCurrent slayer quest type, or "".
skillXp(skill: string): numberSession XP gained for a skill (Combat/Mining/Farming/...).
resetSkillXp(skill?: string): voidReset tracked session XP for a skill (or all skills).
Garden
pests(): numberGarden pest count.
visitors(): numberGarden visitor count.
Text
stripColor(text: string): stringRemove §-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): MiningBotBuild 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 | nullA surface point you can aim at, or null if too occluded.
isMined(block: Vec): booleanHas the block been broken (air/bedrock).
ticksLeft(block: Vec, prev: number): numberTicks to break; seed prev with -1, feed the result back each tick.
hardnessLeft(block: Vec, prev: number, precisionBuff: boolean): numberRemaining hardness; seed prev with -1.
speedboost(): booleanIs your mining-speed ability buff active.
activatingAbility(): booleanIs an ability swap currently in progress.
refreshTools(): voidRe-find your drill in the hotbar (call on enable).
refreshCache(): voidRe-read mining speed / HOTM ability (call after refreshTools).
drillSlot(): numberHotbar slot of your drill, or -1.
holdDrill(): booleanSelect the drill slot.
ability(): booleanFire the drill's pickaxe ability if ready (self-gated).
fakeLookAt(block: Vec): voidMine `block` while your head points elsewhere.
clearFakeLook(): voidStop 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 | nullThe single best standing spot to mine `block` from (in range, sees it, open, away from players).
Live mining state
fuel(): numberCurrent drill fuel.
needsRefuel(): booleanWhether the drill needs refueling.
cold(): numberCurrent cold level (Glacite).
heat(): numberCurrent heat level.
commissions(): numberNumber of active commissions.
mithrilPowder(): numberCurrent Mithril Powder.
gemstonePowder(): numberCurrent Gemstone Powder.
glacitePowder(): numberCurrent Glacite Powder.
miningXp(): numberSession Mining XP gained.
flowstateBlocks(): numberFlowstate block counter.
flowstateLevel(): numberFlowstate level.
betterTogetherSpeed(): numberBetter Together mining-speed bonus.
abilityActive(): booleanIs the drill ability currently active.
abilityReady(): booleanIs the drill ability off cooldown / ready.
abilityType(): stringName of the drill ability.
blocksMined(type: string): numbertype: mithril/titanium/gemstone/umber/tungsten/glacite/ore/obsidian/...
wisdomSlot(): numberHotbar 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[])[] }): voidSelf-driving bots: opts is the start pos {x,y,z}. Route bots: opts.waypoints = [{x,y,z}, ...].
MiningBot.stop(): voidStop 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): voidAim at a fixed point with the humanized combat rotation.
track(x: number, y: number, z: number): voidCall each tick to follow a moving mob.
pitchLocked(x: number, y: number, z: number, pitchLimit: number, positive: boolean): voidAim at the point but clamp pitch to pitchLimit (positive picks the clamp direction).
State & control
stop(): voidStop the combat aim engine.
isAiming(): booleanWhether the engine currently owns/holds an aim target.
rotating(): booleanWhether 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): booleanNormal 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): booleanRequests route planning immediately instead of waiting for the normal direct-attempt fallback.
directTo(x: number, y: number, z: number, options?: number | EtherwarpTravelOptions): booleanDirect handler attempt without route fallback. Rejects blatant:true.
blatantTo(x: number, y: number, z: number, options?: number | EtherwarpTravelOptions): booleanUses 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(): voidCancels this binding’s owned travel and clears failure state. Call before retrying the same failed request.
failed(): booleanCurrent request failed; remains set until stop() or a different goal/options request.
pathfindFailed(): booleanReports route failure separately from the overall handler result. Normal direct fallback may still continue.
Route planning
findPath(target: Vec): PathNode[] | nullSynchronous native route query from the player. Null means no usable result.
findPath(from: Vec | null, target: Vec, options?: EtherwarpPlanOptions): PathNode[] | nullSynchronous query with explicit support-block start, or null for the player. Prefer async for larger budgets.
findPathAsync(target: Vec): PathQueryBackground native Etherwarp route query from the player.
findPathAsync(from: Vec | null, target: Vec, options?: EtherwarpPlanOptions): PathQueryUse 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(): numberRange for the held Etherwarp conduit, or 0 without one.
canReach(x: number, y: number, z: number): booleanWhether a reachable Etherwarp aim can be found from the current position; not just a distance check.
aimPoint(x: number, y: number, z: number): Vec | nullCandidate 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): VecExpected landing feet position; returns the zero vector without a world.
onBlock(x: number, y: number, z: number): booleanWhether 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 cancellationExample: 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): voidOutline the block at (x, y, z) for this frame.
blockFill(x: number, y: number, z: number, color: number): voidFill the block at (x, y, z) for this frame.
blockGlow(x: number, y: number, z: number, color: number): voidGlow the block at (x, y, z) for this frame.
boxOutline(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, color: number): voidOutline an arbitrary AABB for this frame.
boxFill(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, color: number): voidFill an arbitrary AABB for this frame.
boxGlow(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, color: number): voidGlow an arbitrary AABB for this frame.
Per-frame entity effects
entityOutline(entityId: number, color: number): voidOutline an entity by id for this frame.
entityGlow(entityId: number, color: number): voidGlow an entity by id for this frame.
entityChams(entityId: number): voidSee-through chams highlight on an entity for this frame.
Persistent effects
keepBlockOutline(key: string, x: number, y: number, z: number, color: number): voidPersistent block outline stored under key; redraws every frame until removed.
keepBlockFill(key: string, x: number, y: number, z: number, color: number): voidPersistent block fill stored under key.
remove(key: string): voidRemove a persistent effect by its key.
clear(): voidRemove 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(): numberGUI-scaled screen width in pixels.
getScaledHeight(): numberGUI-scaled screen height in pixels.
getWindowWidth(): numberRaw framebuffer width in pixels.
getWindowHeight(): numberRaw framebuffer height in pixels.
getGuiScale(): numberThe active Minecraft GUI scale factor.
Shapes
drawRect(x: number, y: number, w: number, h: number, color: number): voidFilled rectangle. color is an ARGB int.
drawRectRounded(x: number, y: number, w: number, h: number, color: number, radius: number): voidFilled rectangle with rounded corners.
drawHollowRect(x: number, y: number, w: number, h: number, thickness: number, color: number): voidRectangle outline of the given thickness.
drawHollowRectRounded(x: number, y: number, w: number, h: number, thickness: number, color: number, radius: number): voidRounded-corner rectangle outline.
drawCircle(x: number, y: number, radius: number, color: number): voidFilled circle centered at (x, y).
drawCircleOutline(x: number, y: number, radius: number, thickness: number, color: number): voidCircle outline of the given thickness.
drawLine(x1: number, y1: number, x2: number, y2: number, thickness: number, color: number): voidLine between two points.
drawArrow(x: number, y: number, width: number, height: number, rotation: number, color: number, thickness: number): voidArrow glyph, rotated by rotation degrees.
Text
drawText(text: string, x: number, y: number, size: number, color: number): voidDraw text at the given pixel size.
getTextWidth(text: string, size: number): numberRendered pixel width of text at that size.
Transform & clipping
push(): voidSave the current transform onto the stack.
pop(): voidRestore the last saved transform.
translate(x: number, y: number): voidTranslate the current transform.
scale(x: number, y: number): voidScale the current transform.
rotate(degrees: number): voidRotate the current transform, in degrees.
pushScissor(x: number, y: number, w: number, h: number): voidClip subsequent drawing to a rectangle.
popScissor(): voidRemove the last scissor clip.
setGlobalAlpha(alpha: number): voidMultiply the alpha of subsequent draws.
Images
renderImage(path: string, x: number, y: number, w: number, h: number): voidDraw an image file into the rect.
renderImageRounded(path: string, x: number, y: number, w: number, h: number, radius: number): voidDraw an image with rounded corners.
renderImageWithAlpha(path: string, x: number, y: number, w: number, h: number, alpha: number): voidDraw an image at the given alpha.
renderImageWithColor(path: string, x: number, y: number, w: number, h: number, color: number): voidDraw an image tinted by color.
Color
createColor(r: number, g: number, b: number, a: number): numberPack 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): voidDraw 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): voidDraw 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): voidDraw 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): voidDraw 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): voidRotate toward a world point with the general engine.
generalRotateToBlock(x: number, y: number, z: number): voidRotate toward a block with the general engine.
generalRotateToYawPitch(yaw: number, pitch: number): voidRotate to an absolute yaw/pitch with the general engine.
generalIsDone(): booleanWhether the general rotation has finished.
generalStopRotation(): voidStop the general engine.
generalIsRotating(): booleanWhether the general engine is actively rotating.
generalWithCallback(callback: () => void): voidRun a callback when the general rotation ends.
Mining engine
miningRotateToVec(x: number, y: number, z: number): voidRotate toward a world point with the mining engine.
miningRotateToBlock(x: number, y: number, z: number): voidRotate toward a block with the mining engine.
miningIsDone(): booleanWhether the mining rotation has finished.
miningStopRotation(): voidStop the mining engine.
miningIsRotating(): booleanWhether the mining engine is actively rotating.
Etherwarp engine
etherwarpRotateToVec(x: number, y: number, z: number): voidRotate toward a world point with the etherwarp engine.
etherwarpRotateToYawPitch(yaw: number, pitch: number, sigma: boolean): voidRotate to an absolute yaw/pitch with the etherwarp engine.
etherwarpIsDone(): booleanWhether the etherwarp rotation has finished.
etherwarpStopRotation(): voidStop the etherwarp engine.
etherwarpIsRotating(): booleanWhether the etherwarp engine is actively rotating.
etherwarpGetChangeDistance(x: number, y: number, z: number): numberAngular distance you would rotate to face this etherwarp target.
etherwarpOnEndRotation(callback: () => void): voidRun a callback when the etherwarp rotation ends.
Pathfind engine
pathFindRotateToVec(x: number, y: number, z: number): voidRotate toward a world point with the pathfind engine.
pathFindRotateToYawPitch(yaw: number, pitch: number): voidRotate to an absolute yaw/pitch with the pathfind engine.
pathFindIsDone(): booleanWhether the pathfind rotation has finished.
pathFindStopRotation(): voidStop the pathfind engine.
pathFindIsRotating(): booleanWhether 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): numberScalar 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(): numberRandom value in 0..1.
randomInt(min: number, max: number): numberRandom integer, inclusive of both bounds.
randomDouble(min: number, max: number): numberRandom double between min and max.
randomBool(): booleanRandom true / false.
chance(probability: number): booleanTrue with the given probability (0..1).
gaussian(): numberNormal distribution sample (mean 0, sd 1).
randomDelay(minMs: number, maxMs: number): numberRandom delay in milliseconds between the bounds.
pick<T>(array: T[]): T | nullRandom element of the array, or null when empty.
Numeric helpers
clamp(value: number, min: number, max: number): numberConstrain value to the range [min, max].
lerp(a: number, b: number, t: number): numberLinear interpolation from a to b by t.
round(value: number, decimals: number): numberRound value to the given number of decimals.
distance(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number3D Euclidean distance between two points.
distance2d(x1: number, z1: number, x2: number, z2: number): numberHorizontal (XZ) distance between two points.
toRadians(degrees: number): numberConvert degrees to radians.
toDegrees(radians: number): numberConvert radians to degrees.
util
Miscellaneous helpers: a pausable stopwatch, number and duration formatting, color stripping, and OS-level notifications.
util
stopwatch(): StopwatchCreate a pausable real-millisecond timer (handle below).
formatNumber(n: number): stringCompact human formatting, e.g. 1500000 -> "1.50M".
formatDecimal(n: number, digits: number): numberRound n to the given number of decimal places.
extractSeconds(text: string): numberParse a duration like "1h 20m 5s" into total seconds.
stripColor(text: string): stringRemove Minecraft section-sign (§) color codes.
desktopNotify(title: string, message: string): voidShow an OS desktop notification (vs the in-game notification API).
Stopwatch handle (util.stopwatch())
elapsed(): numberMilliseconds elapsed since the last reset.
hasReached(ms: number): booleanTrue once elapsed has reached ms.
reset(): voidReset elapsed time back to zero.
setElapsed(ms: number): voidForce the elapsed time to a specific value.
pause(): voidPause counting.
resume(): voidResume counting.
paused(): booleanWhether 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(): booleanWhether a container GUI is currently open.
name(): stringContainer 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[] } | nullOne non-empty inventory item, or null for an empty or out-of-range slot.
itemData(slot: number): object | nullParsed `custom_data` for an inventory item in slots 0–35, or null for an empty/out-of-range slot.
openItemData(menuSlot: number): object | nullParsed `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"): voidClick 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(): voidClose 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): voidPrint a client message to chat.
fps(): numberCurrent client framerate.
tps(): numberSmoothed server TPS (20 = perfect).
leftMouseButtonDown(): booleanWhether the left mouse button is currently held.
rightMouseButtonDown(): booleanWhether the right mouse button is currently held.
isKeyDown(key: number): booleanWhether 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(): stringIP / hostname of the connected server.
isOnHypixel(): booleanWhether you are connected to Hypixel.
isInSkyblock(): booleanWhether you are currently in Skyblock.
getCurrentLocation(): stringCurrent Skyblock location name.
isInLocation(name: string): booleanWhether 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): ScriptKeybindCreate one of at most 32 bindings for this script. Names must be non-blank and unique within the script.
ScriptKeybind handle
key(): numberCurrent key code, or 0 when unbound.
setKey(keyCode: number): voidChange the key code; 0 unbinds it.
isDown(): booleanWhether this binding’s key is currently held.
name(): stringThe name passed to create().
remove(): voidDetach 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
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: numberSuccess style id.
ERROR: numberError style id.
WARNING: numberWarning style id.
INFO: numberInfo style id.
Methods
push(durationMs: number, title: string, description: string, style: number): voidShow a notification for durationMs; style is one of the constants above.
success(title: string, description: string): voidShow a success-styled notification.
error(title: string, description: string): voidShow an error-styled notification.
warning(title: string, description: string): voidShow a warning-styled notification.
info(title: string, description: string): voidShow 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: numberBegin sneaking.
STOP_SNEAKING: numberStop sneaking.
STOP_SLEEPING: numberLeave a bed.
START_SPRINTING: numberBegin sprinting.
STOP_SPRINTING: numberStop sprinting.
RIDING_JUMP: numberJump while riding a mount.
OPEN_INVENTORY: numberOpen the vehicle inventory.
Interaction types
INTERACT: numberInteract (right-click) with an entity.
ATTACK: numberAttack (left-click) an entity.
INTERACT_AT: numberInteract with an entity at a precise hit vector.
Block-break actions
START_DESTROY_BLOCK: numberBegin breaking a block.
ABORT_DESTROY_BLOCK: numberCancel breaking a block.
STOP_DESTROY_BLOCK: numberFinish breaking a block.
Item actions
DROP_ALL_ITEMS: numberDrop the entire held stack.
DROP_ITEM: numberDrop a single held item.
RELEASE_USE_ITEM: numberRelease a charged / in-use item.
Client commands
PERFORM_RESPAWN: numberRespawn after death.
REQUEST_STATS: numberRequest statistics from the server.
OPEN_INVENTORY_ACHIEVEMENT: numberOpen-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(): stringThis script’s name.
log(message: string): voidScript console + latest.log.
info(message: string): voidConsole / log + a local info message in game chat.
warn(message: string): voidConsole / log + a local warning in game chat.
error(message: string): voidConsole / log + a local error in game chat.
chat(message: string): voidLocal game chat only; never sent to the server.
Persistent store
get(key: string): string | nullRead a stored value, or null when absent.
getOr(key: string, fallback: string): stringRead a stored value, or fallback when absent.
set(key: string, value: string): voidStore a value; persisted to polinex/scripts/data/<name>.properties.
has(key: string): booleanWhether a key is stored.
remove(key: string): voidDelete a stored key.
keys(): string[]All stored keys.
Local JSON
readJson(path: string): unknown | nullFor 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): numberWall-clock; run fn once after N * 50ms. Returns a timer id.
every(ticks: number, fn: () => void): numberWall-clock; run fn every N * 50ms until cleared or unload. Returns a timer id.
clearTimer(id: number): voidCancel 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.nameStable, obfuscation-proof packet id, e.g. "CLIENTBOUND_SYSTEM_CHAT" (or "UNKNOWN" if unmapped). This is the value you compare against packet.* constants.
event.typeRaw 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
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
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
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
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
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.valueRead 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
booleanboolean
slider / plusMinusnumber (integer)
decimalSlidernumber
dropdownthe selected option string (setSetting accepts a string or an index)
colornumber (0xAARRGGBB)
stringstring
range / decimalRange{ min, max } (setSetting accepts { min, max } or [min, max])
multiBoolstring[] 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
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
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
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
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.