Aesthetic — Lua Scripting

Aesthetic lets you write your own modules in Lua — no client rebuild required. A script declares a module, its settings and event handlers; the module shows up in the GUI next to the built-in ones and its settings are saved to the config.

Script location

<Minecraft config folder>/aesthetic/scripts/*.lua

Every .lua file in this folder is a separate script. The folder is created on first launch together with a sample example.lua.

Hot reload

Scripts reload automatically:

  • save a file — the script reloads instantly, keeping setting values and the module's enabled state;
  • create a file — the script loads on the fly;
  • delete a file — its modules are unloaded.

Every reload is announced in chat with the [lua] prefix.

Errors

A load error prints to chat and the script is skipped. An error inside a callback prints to chat and disables the module so it doesn't spam every tick. Fix the code and save — the script reloads.

Minimal script

local module = ui.category("scripts"):module("code", "Hello")

module:event("tick", function()
    -- runs 20 times a second while the module is enabled
end)

Next: Getting Started.

Getting Started

1. Create a file and declare a module

Create my-module.lua in <config>/aesthetic/scripts/ — the game can be running:

local module = ui.category("scripts"):module("code", "My Module")
  • ui.category(name) — GUI category: "combat", "movement", "visuals", "player", "misc" or "scripts".
  • :module(icon, title[, description]) — icon name (e.g. "code", "bolt", "eye" — the editor autocompletes the full set) and the module title. The title must be unique across all modules.

2. Add settings

local delay = module:number("Delay", 20, 1, 100, 1, "t")
local text  = module:string("Text", "hello")
local on    = module:boolean("Enabled part", true)

Settings are read with :get() and show up in the module's GUI automatically. All types are listed in Modules & Settings.

3. Subscribe to events

local ticks = 0

module:callback("enable", function()
    ticks = 0
end)

module:event("tick", function()
    ticks = ticks + 1
    if on:get() and ticks % delay:get() == 0 then
        client.print_chat(text:get())
    end
end)

callback handles the lifecycle ("enable", "disable"), event handles game events (see Events). Event names ignore case and underscores: render_2d and render2d are the same.

4. Save and enable

Save the file — chat shows [lua] Loaded my-module.lua and the module appears in the GUI. Enable it and keep editing: every save reloads the script while keeping your setting values and the enabled state.

Editor support (autocomplete)

The mod generates language-server annotations for the whole API on every launch:

  • <config>/aesthetic/scripts/library/*.lua — LuaCATS stubs for all globals, events, settings and render canvases;
  • <config>/aesthetic/scripts/.luarc.json — configuration pointing lua-language-server at them.

Open the scripts folder in an editor with lua-language-server — the Lua extension in VS Code, the SumnekoLua plugin in IntelliJ, or lua_ls in Neovim — and you get completion, hovers, signature help and type checking out of the box, including event names with typed callback arguments (module:event("render_2d", function(render) ...), packet names and the full icon-name set.

The library/ folder is regenerated on every launch to match the installed mod version — don't edit it. .luarc.json is only written when missing, so your own tweaks survive.

Debugging

  • print("a", value) and client.print_chat(text) — output to chat ([lua] prefix);
  • aesthetic.log(msg) — output to the game log;
  • callback errors print to chat and disable the module — fix and re-enable.

Shared libraries

require resolves files relative to the scripts folder. Only top-level .lua files auto-load as scripts, so keep shared code in a subfolder:

local util = require "lib/util"   -- <scripts>/lib/util.lua

Modules & Settings

Creating a module

local module = ui.category("visuals"):module("draw-square", "My ESP")
MethodWhat it does
ui.category(name)picks a GUI category: combat, movement, visuals, player, misc, scripts
category:module(icon, title[, description])registers a new module and returns it; description is readable via modules
module:callback(name, fn)lifecycle hooks: "enable", "disable"
module:event(name, fn)subscribes to a game event (list)
module:toggle()flips the module on/off
module:enabled()returns true while the module is enabled
module:set_enabled(bool)enables or disables the module
module.namethe module's name

One callback per event: a repeated module:event("tick", ...) replaces the previous one.

Icon names autocomplete in the editor — the full set lives in the generated library/icons.lua stub (see autocomplete).

Settings

Settings are created with module methods, appear in the GUI automatically and are saved to the config. The first argument is the name (also the GUI label).

local b = module:boolean("Flag", true)
local n = module:number("Speed", 1.0, 0.1, 10.0, 0.1, "x")
local r = module:range("Bounds", 20, 80, 0, 100, 1, "%")
local s = module:string("Message", "hello")
local k = module:key("Boost", "R")
local c = module:color("Color", 0xFF4FF2A6)
local sel = module:selection("Mode", nil, { "A", "B", "C" })
local multi = module:multi_selection("Targets", { "Players" }, { "Players", "Mobs" })
SignatureNotes
boolean(name, default)on/off switch
number(name, default, min, max[, step[, unit]])a slider; step defaults to 1, unit is a label like "ms"
range(name, default_min, default_max, min, max[, step[, unit]])two-handle slider; the value is a {min, max} table
string(name, default)text field
key(name[, default])key bind (like Item Helper); default is a key name or a table of up to 3 names
color(name, argb[, alpha])color picker, 0xAARRGGBB; alpha = false hides the alpha slider
selection(name, default, options)pick one option; default = nil means the first one
multi_selection(name, defaults, options[, allow_empty])pick several options; defaults = nil means the first one; the value is an array of strings

Working with values

n:get()          -- current value
n:set(5)         -- set (numbers are clamped to [min, max])
n:reset()        -- back to default
n.value          -- sugar for get/set
sel:set("B")     -- selection: by option name
r:get().min      -- range returns {min=, max=}
r:set({ min = 10, max = 90 })

Colors come back as unsigned numbers, so comparing against 0xAARRGGBB literals works directly.

Key binds

local k = module:key("Boost", "R")          -- or { "LCtrl", "R" }
module:event("tick", function()
    if k:pressed() then
        -- held right now (always false while a screen is open)
    end
end)
k:get()                  -- bound key names, e.g. { "R" }
k:set({ "LCtrl", "R" })  -- rebind from code

Key names match the GUI labels: letters/digits ("R"), "Space", "LShift", "LCtrl", "LAlt", "Enter", "Tab", "F1""F25", mouse buttons "MMB", "M4", "M5", etc. The bind is editable in the module's GUI panel like any other setting.

Other modules — the modules global

The modules global reads and controls every registered module — built-in ones included, not just those created by scripts.

modules.list()      -- all module names
modules.active()    -- names of the currently enabled modules
modules.binds()     -- all binds: { { module = "Flight", keys = { "R" }, type = "toggle" }, ... }

local flight = modules.get("Flight")    -- case-insensitive; nil when there is no such module
flight:enabled()                        -- true/false
flight:set_enabled(true)
flight:toggle()
flight:binds()                          -- { { keys = { "R" }, type = "toggle" }, ... }

flight.name, .title, .description and .category describe the module. Bind type is "toggle" or "hold"; key names match the GUI labels (see key binds).

Reading and overriding settings

ref:settings() lists the setting names in GUI order; ref:setting(name) (also case-insensitive) returns a handle with the usual :get() / :set(v) / :reset() and the .value sugar. Its type field says which value shape applies — the same shapes as the setting builders above:

typevalue
booleanboolean
numbernumber, clamped to the setting's range
range{ min = , max = } table
stringstring
keyarray of key names; also has :pressed()
color0xAARRGGBB number
selectionoption name
multi_selectionarray of option names
orderpermutation of the current key array
local speed = modules.get("Speed")
for _, name in ipairs(speed:settings()) do
    aesthetic.log(name .. " = " .. tostring(speed:setting(name):get()))
end
speed:setting("Mode"):set("Strafe")     -- unknown options/keys raise an error

Overrides go through the same path as the GUI: the module reacts immediately and the new value persists to the config.

Persistence

Setting values and the enabled state survive game restarts and script hot-reloads. They are keyed by the module and setting names — renaming loses the stored values.

Events

Subscribe with module:event(name, fn). The callback receives one table with the event's fields (or nothing if the event has none). Names ignore case and underscores: render_2d == render2d == Render_2D.

Cancellable events have a :cancel() method — call it to stop the event:

module:event("chat", function(e)
    if e.text:find("spam") then e:cancel() end
end)

Writable fields are read back after your callback — assign a new value and the game uses it:

module:event("input", function(e)
    e.jump = true          -- player movement is controlled through input
end)

Callbacks only run while the module is enabled. The event table is only valid inside the callback — don't store it for later.

Ticks

EventFieldsNotes
tickbefore the player tick; cancellable
post_tickafter the player tick
pre_interactionbefore attacks and item use are processed

Input

EventFieldsNotes
inputforward, backward, left, right, jump, sneak, sprint — all writablecancellable; the way to control player movement
keykey, action, modscancellable; GLFW key codes
charchar, codepoint, modscancellable; a typed character
mouse_movex, ycancellable
mouse_scrollhorizontal, verticalcancellable
mouse_lookdelta_x, delta_ycancellable; camera rotation by mouse
cursor_lockstate = "lock" / "unlock"cancellable
slot_dragslot, button, shiftcancellable; dragging across container slots

Chat & text

EventFieldsNotes
chattextcancellable; a message arrived in chat
chat_sendtextcancellable; you are about to send a message
titletext, kind = "title"/"subtitle"/"actionbar"a title appeared on screen
boss_bartextthe boss bar text changed

Combat & interaction

EventFieldsNotes
attacktargetentity tablecancellable; before you hit
post_attacktargetafter the hit
item_usecooldown — writablecancellable
block_interacthand, x, y, z, sidecancellable; hand = "main"/"off"
block_placehand, x, y, z, side, itemitem is an item table
block_breakingstate = "start"/"stop", block, x, y, z

Movement

EventFieldsNotes
jumpyaw, cooldown — writablecancellable
landx, y, z, fall_distanceyou touched the ground
slowdownmultiplier — writablecancellable; slowdown while using an item

Network

Note: packet_send and packet_receive fire on network threads — don't use rendering or projection inside them.

EventFieldsNotes
packet_sendname, decoded fieldscancellable
packet_receivename, decoded fieldscancellable
packet_processreceived packets are applied to the world
movement_packetsafter movement packets are sent
player_packetsafter player packets are sent

name is the mod's own stable snake_case packet name — the same one packets.send accepts (Minecraft class names are obfuscated at runtime, so packets are never matched by class name). Common packets also carry decoded payload fields; the full list is on the Packets page.

module:event("packet_receive", function(e)
    if e.name == "entity_velocity" and e.entity_id == minecraft.player().id then
        e:cancel()  -- ignore knockback
    end
end)

World & modules

EventFields
chunk_loadx, z
chunk_unloadx, z
module_statemodule — module name, enabled

Rendering

render_2d, render_gui and render_3d receive a canvas / renderer object instead of a field table.

EventFieldsNotes
frameevery frame
render_2d2D canvasdraw over the game (HUD)
render_gui2D canvasdraw during GUI screens
render_3d3D rendererdraw in the world
camera_ratioshrink — writable
sky_colorcolor — writable, original
time_of_daytime — writable, originalvisual time of day
skyboxrendered — writable (true hides the vanilla sky)
fogred, green, blue, alpha, environmental_start, environmental_end, render_distance_start, render_distance_end — all writablecolor channels are 0..1
window_resizewidth, height

Game API

minecraft — game state

FunctionWhat it does
minecraft.player()returns your player as a table, or nil when not in a world
minecraft.player(name)finds a player by name (case doesn't matter), or nil
minecraft.players()returns every player in the world as an array; you are in the list too — check is_self
minecraft.entities([type_id])returns every entity; pass a type id like "minecraft:item" to keep only that type
minecraft.block(x, y, z)returns the block id at that position, like "minecraft:stone", or nil outside a world
minecraft.block_state(x, y, z)returns the full block state table at that position, or nil outside a world
minecraft.item(id)returns the item table for one default item, like minecraft.item("minecraft:netherite_sword"), or nil for an unknown id
minecraft.time()returns the world's time of day
minecraft.dimension()returns the dimension id, like "minecraft:overworld"
minecraft.in_game()returns true while a world is loaded
minecraft.effects()returns your active potion effects as an array of {id, amplifier, duration}
minecraft.armor()returns your worn armor as {head, chest, legs, feet} item tables
local p = minecraft.player()
if p and p.health < 6 then
    client.print_chat("low hp!")
end

local_player — your player

Reads and controls the local player in one place. Every function returns nil (or does nothing) while no player exists, so no guards are needed.

FunctionWhat it does
local_player.entity()the full entity table, same as minecraft.player()
local_player.position()returns x, y, z
local_player.velocity()returns x, y, z velocity, in blocks per tick
local_player.rotation()returns yaw, pitch
local_player.health()current health
local_player.food()food level (0–20)
local_player.saturation()saturation level
local_player.on_ground()true while standing on the ground
local_player.exists()true while a local player exists
local_player.set_velocity(x, y, z)sets the velocity directly, in blocks per tick
local_player.jump()jumps, even mid-air
local_player.set_position(x, y, z)teleports client-side to that position
module:event("tick", function()
    if local_player.on_ground() then
        local vx, _, vz = local_player.velocity()
        local_player.set_velocity(vx * 1.1, 0.42, vz * 1.1)
    end
end)

Entity table

Every function that returns an entity (player, players, entities, attack.target) gives you a plain table. It is a snapshot: it shows the moment it was created and does not update by itself.

Fields every entity has:

FieldMeaning
id, uuid, name, typeentity id, UUID, display name and type ("minecraft:player")
is_selftrue when this entity is your own player
alive, agewhether it is alive; how many ticks it has existed
posecurrent pose: "standing", "crouching", "swimming", "gliding", "sleeping", ...
x, y, z, eye_yposition; eye_y is the eye height
prev_x/y/z, delta_x/y/zposition on the previous tick and how far it moved this tick
vel_x/y/zcurrent velocity
yaw, pitchwhere the entity is looking
width, heighthitbox size
distancedistance from your player to this entity
on_ground, sneaking, sprinting, swimming, crawlingmovement state
spectator, invisible, glowing, silent, no_gravitystatus flags
on_fire, fire_ticks, fire_immuneburning state
in_water, wet, submerged, in_lavafluid state; wet also counts rain
frozen_ticks, frozenpowder snow freezing
air, max_airremaining air (drowning)
fall_distancehow far it has been falling
vehiclethe id of the entity it rides, or nil
passengersarray of rider entity ids, or nil when empty

Living entities (mobs, players) also have:

FieldMeaning
health, max_health, absorptionhearts, including the yellow absorption ones
armorarmor points (what the armor bar shows)
hurt_time, death_time, deadred-flash ticks after a hit; death animation ticks
baby, scalewhether it is a baby and its size multiplier
body_yaw, head_yawbody and head rotation (the entity's yaw is the head look)
movement_speedmovement speed attribute
blocking, climbing, gliding, sleeping, jumpingaction state; gliding means elytra flight
using_item, using_riptidewhether it is using an item / riptide-spinning
item_use_time, item_use_time_leftticks the current item has been / still needs to be used
held_item, offhand_itemitem tables of the hands
active_itemthe item being used right now (eating, blocking, ...), or nil
equipmentworn armor as {head, chest, legs, feet} item tables
effectsactive potion effects: array of {id, amplifier, duration, infinite, ambient, visible}

Players also have ping and gamemode. Your own player additionally has:

FieldMeaning
food, saturationhunger bar and hidden saturation
xp_level, xp_progress, total_xpexperience level, progress to the next level (0..1) and total points
attack_cooldownattack charge 0..1 — 1 means the next hit is full strength
flying, allow_flying, creativeability flags
walk_speed, fly_speedmovement ability speeds

Item table

Describes one item stack. Returned by held_item, equipment, inventory.item, minecraft.item(id) and others.

FieldMeaning
id, name, countitem id ("minecraft:diamond_sword"), display name and stack size
emptytrue for an empty slot
max_count, stackablehow many fit in one stack
rarity"common", "uncommon", "rare" or "epic"
use_actionwhat using it does: "none", "eat", "drink", "block", "bow", "spear", ...
damage, max_damagedurability used so far and the maximum (0 when not damageable)
damageable, damaged, unbreakabledurability flags
enchantedtrue when the item has any enchantment
enchantmentstable of id -> level, e.g. enchantments["minecraft:sharpness"] == 5
custom_namethe anvil-given name, or nil
food{nutrition, saturation, can_always_eat} for eatable items, or nil
tagsarray of item tag ids, like "minecraft:swords"
local held = minecraft.player().held_item
if held.enchantments["minecraft:sharpness"] then
    client.print_chat(held.name .. " has Sharpness")
end

Block state table

Returned by minecraft.block_state(x, y, z). Describes the exact block at a position, including its state properties.

FieldMeaning
id, nameblock id ("minecraft:chest") and display name
x, y, zthe queried block position (floored)
air, liquid, solid, replaceablebasic kind flags; replaceable means placing over it works (grass, water, ...)
burnablefire can consume it
tool_requireddrops only when mined with the right tool
opaque, full_cube, has_collisionshape flags
has_block_entityhas extra data like a chest or furnace
random_ticksreceives random ticks (crops grow, ice melts, ...)
emits_redstoneemits redstone power
piston_behavior"normal", "destroy", "block", "push_only", ...
hardness, blast_resistancemining and explosion resistance
slipperinessice-like sliding (0.6 is normal, 0.98 is ice)
velocity_multiplier, jump_velocity_multipliermovement slowdown on/in the block (soul sand, honey)
luminancelight it emits, 0–15
opacityhow much light it blocks
map_colorthe color it has on maps, as a number
propertiestable of state properties as strings, e.g. properties.facing == "north", properties.lit == "true"
fluid{id, level, still} when the block contains a fluid, or nil
tagsarray of block tag ids, like "minecraft:mineable/pickaxe"
local b = minecraft.block_state(x, y, z)
if b and b.properties.lit == "true" then
    client.print_chat(b.name .. " is lit")
end

client — client & chat

FunctionWhat it does
client.fps()returns the current FPS
client.ping()returns your ping in milliseconds
client.server()returns the server address, or nil in singleplayer
client.has_screen()returns true while any screen (inventory, chat, menu) is open
client.millis()returns the current time in milliseconds
client.tick_delta()returns the progress between two ticks (0..1) — useful for smooth movement
client.say_chat(text)sends text to the server chat; a leading / sends it as a command
client.command(cmd)sends a command to the server (the leading / is optional)
client.print_chat(text)shows a message in your chat only — nothing is sent to the server
client.key_down(code)returns true while a key is held down (GLFW key code, e.g. 32 is space)

print(...) — the standard Lua print, shown in your chat with the [lua] prefix.

aesthetic

FunctionWhat it does
aesthetic.log(msg)writes a line to the game log file
aesthetic.scriptthe file name of the current script

Standard libraries

math, string, table, bit32, coroutine, a limited os (os.time, os.clock, os.date) and require (see Getting Started) are available.

Rendering

Colors are 0xAARRGGBB (alpha in the high byte). 2D coordinates are logical pixels; draw calls and projection results are pixel-aligned automatically.

2D canvas

The argument of render_2d (HUD over the game) and render_gui (during screens):

module:event("render_2d", function(render)
    render.rect(8, 8, 120, 24, 0x90101010, 7)
    render.text("Hello", 16, 14, 9, 0xFFFFFFFF)
end)
FunctionWhat it does
width() / height()screen size in logical pixels
rect(x, y, w, h, color[, radius])draws a filled rectangle; radius rounds the corners
outline(x, y, w, h, color[, radius[, stroke]])draws a border (stroked inwards)
line(x1, y1, x2, y2, color[, stroke])draws a line between two points
circle(cx, cy, r, color)draws a filled circle
text(str, x, y[, size[, color[, font]]])draws text and returns its width
text_width(str[, size[, font]])returns the width the text would take, without drawing it
font_height([size[, font]])returns the line height of a font
px(v)aligns a logical coordinate to a physical pixel

Default text size is 9, default font is "regular". Built-in fonts: "regular", "medium", "consolas", "inter", "small", "icon" (icon glyphs by name — render.text("bolt", x, y, 9, color, "icon")).

The canvas is only valid inside the callback — don't store it.

Custom fonts — fonts

FunctionWhat it does
fonts.register(name, path[, antialias])loads a TTF/OTF file; the path is relative to the scripts folder (absolute works too)
fonts.exists(name)returns true if a font with this name is registered
fonts.register("pixel", "fonts/pixel.ttf")
-- ...
render.text("Hi", 8, 8, 12, 0xFFFFFFFF, "pixel")

Registering the same name again replaces the font (hot-reload friendly); built-in fonts can't be replaced.

3D renderer

The argument of render_3d. Coordinates are world coordinates — the camera offset is handled for you.

FunctionWhat it does
camera()returns the camera position as x, y, z
line(x1, y1, z1, x2, y2, z2, color[, width[, through_walls]])draws a line between two world points
box(min_x, min_y, min_z, max_x, max_y, max_z, color[, width[, through_walls]])draws a box outline
tracer(x, y, z, color[, width[, through_walls]])draws a line from the camera to a point

through_walls = true makes the shape visible through blocks.

module:event("render_3d", function(render)
    for _, p in ipairs(minecraft.players()) do
        if not p.is_self then
            local half = p.width / 2
            render.box(p.x - half, p.y, p.z - half,
                       p.x + half, p.y + p.height, p.z + half,
                       0xFF4FF2A6, 2)
        end
    end
end)

3D → 2D projection — projection

Converts world coordinates to screen coordinates (the same space the 2D canvas draws in). Call from render callbacks (render_2d, render_gui, frame).

FunctionReturns
projection.to_screen(x, y, z[, ignore_frustum])screen position sx, sy, or nil when the point is behind the camera or out of view
projection.box(min_x, min_y, min_z, max_x, max_y, max_z)the box on screen as x, y, w, h, or nil
projection.entity_box(entity_id)the entity's hitbox on screen as x, y, w, h (smoothly interpolated), or nil

ignore_frustum = true also projects points outside the view (it only fails behind the camera) — useful for edge-of-screen indicators.

module:event("render_2d", function(render)
    for _, p in ipairs(minecraft.players()) do
        if not p.is_self then
            local x, y, w, h = projection.entity_box(p.id)
            if x then render.outline(x, y, w, h, 0xFF4FF2A6, 0, 1) end
        end
    end
end)

Services

Bridges to the client's built-in services: smooth rotations, freelook, inventory, the vanilla interaction manager and movement prediction.

rotation

Server-side rotations with smoothing, priorities and automatic freelook (your camera stays put while the "server-side" facing rotates).

FunctionWhat it does
rotation.set(yaw, pitch[, priority])asks for a rotation; returns true if it was accepted
rotation.look_at(x, y, z[, priority])rotates to face a point (measured from the player's eyes)
rotation.camera()rotates towards where the camera looks (priority 100)
rotation.active()returns true while a managed rotation is running
rotation.lock_item_use()blocks new rotations until the end of the tick

A request lasts one tick — to keep facing a direction, request it every tick (usually from pre_interaction or tick). A request with a lower priority than the active one is rejected.

module:event("pre_interaction", function()
    local target = minecraft.player("Notch")
    if target then
        rotation.look_at(target.x, target.y + target.height / 2, target.z, 10)
    end
end)

freelook

FunctionWhat it does
freelook.active()returns true while freelook is on
freelook.activate() / deactivate()turns freelook on / off
freelook.lock() / unlock()freezes the camera (the mouse stops rotating it)
freelook.locked()returns true while the camera is frozen
freelook.yaw() / pitch()current camera angles

inventory

Actions are scheduled to a safe point of the tick and play nicely with movement and packets.

FunctionWhat it does
inventory.swap(a, b)swaps the items in two slots
inventory.drop(slot[, all])drops from a slot (all defaults to true — the whole stack)
inventory.merge(from, to)moves a stack onto another one
inventory.use(slot[, yaw, pitch])uses the item in a slot, optionally facing a direction first
inventory.item(slot)returns the item table {id, count, name}, or nil
inventory.find(item_id)returns the first slot holding that item, or nil
inventory.count(item_id)returns how many of that item you carry in total
inventory.slot()returns the selected hotbar slot (0–8)
inventory.set_slot(i)selects a hotbar slot (0–8)

Slot numbering

RangeMeaning
0crafting result
1–4crafting grid
5–8armor (helmet → boots)
9–35main inventory
36–44hotbar
45offhand
module:event("tick", function()
    local totem = inventory.find("minecraft:totem_of_undying")
    if totem and inventory.item(45).id ~= "minecraft:totem_of_undying" then
        inventory.swap(totem, 45)
    end
end)

interaction

The vanilla interaction manager: attacks, block/entity/item interactions, block breaking and screen-handler clicks. Everything goes through the vanilla client paths — correct packets, swing timers and sequence numbers. For raw packet control see Packets.

Hands are "main" / "off" (default "main"); sides are "up" | "down" | "north" | "south" | "east" | "west" (default "up"). Interactions return "success" | "fail" | "pass".

FunctionWhat it does
interaction.gamemode()current game mode, e.g. "survival"
interaction.attack(entity_id)attacks an entity (with a main-hand swing)
interaction.interact(entity_id[, hand])right-clicks an entity
interaction.use_item([hand])uses the held item
interaction.use_block(x, y, z[, side[, hand]])right-clicks a block (aims at its center)
interaction.attack_block(x, y, z[, side])starts breaking a block (the first hit)
interaction.update_breaking(x, y, z[, side])continues breaking; call every tick while "holding the button"
interaction.cancel_breaking()aborts the current breaking
interaction.break_block(x, y, z)finishes breaking instantly (creative / fully cracked)
interaction.breaking()true while a block is being broken
interaction.breaking_progress()breaking stage 0–10, -1 when idle
interaction.stop_using()releases bow / stops eating / lowers shield
interaction.swing([hand])swings an arm (animation + packet)
interaction.sync_id()sync id of the open screen handler (0 = inventory)
interaction.click_slot(sync_id, slot, button[, action])clicks a slot like a mouse click
interaction.click_button(sync_id, button)clicks a screen-handler button (stonecutter, loom, ...)
interaction.pick_block(x, y, z[, include_data])pick-block
interaction.pick_entity(entity_id[, include_data])pick-block on an entity
interaction.close_screen()closes the open screen (and tells the server)

click_slot actions: "pickup" (default), "quick_move" (shift-click), "swap" (button = hotbar slot 0–8, 40 = offhand), "clone", "throw", "quick_craft", "pickup_all".

module:event("pre_interaction", function()
    for _, p in ipairs(minecraft.players() or {}) do
        if not p.is_self and p.distance < 3 then
            interaction.attack(p.id)
            break
        end
    end
end)

prediction

Vanilla-accurate movement physics simulation: "where will this player be in N ticks?". The local player is simulated from the real input; other players from input guessed from their observed motion. Simulations are cached per client tick and stepped lazily — asking for tick 5 and then tick 8 only simulates three more ticks. Max 1200 ticks.

FunctionWhat it does
prediction.self(ticks)your snapshot ticks ticks ahead (0 = now)
prediction.self_path(from, to)array of your snapshots for [from, to]
prediction.self_with_input(input, ticks)one-off simulation with a custom held input
prediction.player(entity_id, ticks)another player's snapshot
prediction.player_path(entity_id, from, to)array of another player's snapshots
prediction.velocity(entity_id)de-smeared per-tick velocity → vx, vy, vz

A snapshot is a table: {x, y, z, vel_x, vel_y, vel_z, min_x, min_y, min_z, max_x, max_y, max_z, on_ground, fall_distance, horizontal_collision} (min_*/max_* are the bounding box). input for self_with_input is a table of booleans: {forward, backward, left, right, jump, sneak, sprint} — omitted keys are false.

Remote players' network positions are interpolated, so their raw per-tick delta is unreliable — prediction.velocity returns the recovered true motion and is what the simulation itself seeds from.

module:event("render_3d", function(render)
    for _, p in ipairs(minecraft.players() or {}) do
        if not p.is_self and p.distance < 20 then
            local s = prediction.player(p.id, 10)
            if s then
                render.box(s.min_x, s.min_y, s.min_z, s.max_x, s.max_y, s.max_z, 0x80FF4040, 2)
            end
        end
    end
end)

Packets

Raw network control through the packets global and the packet_send / packet_receive events.

Minecraft's class names are obfuscated at production runtime, so scripts can't match packets by Java class name. The mod maintains its own stable snake_case mapping: the same names appear as e.name in the packet events and are accepted by packets.send.

FunctionWhat it does
packets.list()sorted array of names packets.send can build
packets.send(name, fields)builds a C2S packet and sends it; returns true on success

packets.send returns false for an unknown name or when a required game object is missing (no world, unknown entity id). The send goes through the normal client pipeline, so it fires packet_send — a module that cancels its own scripted packets will swallow them too.

packets.send("move_position", { x = p.x, y = p.y + 0.05, z = p.z, on_ground = false })
packets.send("client_command", { mode = "start_sprinting" })
packets.send("player_action", { action = "swap_item_with_offhand", x = 0, y = 0, z = 0 })
packets.send("attack_entity", { entity_id = target.id })

For attacking, block breaking and container clicks prefer the interaction service — it goes through the vanilla client paths with correct swing timers and sequence numbers. Raw packets are for when you need to lie to the server on purpose.

Sendable packets (C2S)

Optional fields show their defaults. hand is "main" / "off" (default "main"), side is "up" | "down" | "north" | "south" | "east" | "west" (default "up"), sequence defaults to 0.

NameFields
move_fullx, y, z, yaw, pitch, on_ground = true, horizontal_collision = false
move_positionx, y, z, on_ground = true, horizontal_collision = false
move_lookyaw, pitch, on_ground = true, horizontal_collision = false
move_on_groundon_ground = true, horizontal_collision = false
client_commandmode"stop_sleeping" | "start_sprinting" | "stop_sprinting" | "start_riding_jump" | "stop_riding_jump" | "open_inventory" | "start_fall_flying", jump_height = 0
client_statusmode"perform_respawn" | "request_stats"
hand_swinghand
player_actionaction"start_destroy_block" | "abort_destroy_block" | "stop_destroy_block" | "drop_all_items" | "drop_item" | "release_use_item" | "swap_item_with_offhand", x, y, z, side, sequence
attack_entityentity_id, sneaking = false
interact_entityentity_id, hand, sneaking = false
interact_entity_atentity_id, x, y, z (hit point), hand, sneaking = false
interact_blockx, y, z, side, hit_x/hit_y/hit_z (default block center), inside = false, hand, sequence
interact_itemhand, yaw, pitch (default current rotation), sequence
select_slotslot — hotbar 0–8
close_screensync_id
teleport_confirmid
keep_aliveid
commandcommand — without the leading /
player_inputforward, backward, left, right, jump, sneak, sprint — all default false
pick_item_from_blockx, y, z, include_data = false
pick_item_from_entityentity_id, include_data = false

Decoded event fields

packet_send and packet_receive tables always carry name; for the packets below the payload is decoded too. Everything else still fires — just without extra fields.

Outgoing (packet_send) — all the sendable packets above are decoded back with the same fields (move_* carry x/y/z only when the packet changes position, yaw/pitch only when it changes look).

Incoming (packet_receive):

NameFields
keep_aliveid
disconnectreason
player_position_lookteleport_id, x, y, z, vel_x, vel_y, vel_z, yaw, pitch
entity_velocityentity_id, vel_x, vel_y, vel_z
entity_positionentity_id, x, y, z, yaw, pitch, on_ground
entity_position_syncentity_id, x, y, z, yaw, pitch, on_ground
entity_damageentity_id, source_cause_id, source_direct_id
damage_tiltentity_id, yaw
health_updatehealth, food, saturation
explosionx, y, z, radius, knockback_x/y/z? (present when you were pushed)
block_updatex, y, z, block — block id
select_slotslot

Note: packet events fire on network threads — collect data and act in tick, don't call rendering or projection from them.

Performance & @jit

-- @jit — bytecode compilation

A pragma line anywhere in the file:

-- @jit

compiles the script (and everything it requires) to JVM bytecode instead of interpreting it. Hot code gets JIT-compiled by the JVM and runs several times faster — worth it for heavy logic in tick, frame and render callbacks.

If compilation fails, the script automatically loads interpreted and chat shows @jit compilation failed, running interpreted — remove the pragma or simplify the construct the compiler choked on.

Tips

  • Read settings once. Every setting:get() crosses into Java. In a hot callback, read values into locals at the top.

    module:event("render_2d", function(render)
        local argb = color:get()      -- once
        for i = 1, #list do ... end   -- not color:get() inside the loop
    end)
    
  • Collect in tick, draw in render. minecraft.players() / entities() build snapshot tables — too expensive per frame. Collect once per tick (20/s) and only project and draw in render_2d.

    local tracked = {}
    module:event("tick", function()
        local list = {}
        for _, e in ipairs(minecraft.entities()) do
            if not e.is_self and e.distance < 64 then list[#list + 1] = e end
        end
        tracked = list
    end)
    module:event("render_2d", function(render)
        for i = 1, #tracked do
            local x, y, w, h = projection.entity_box(tracked[i].id)
            if x then render.outline(x, y, w, h, 0xFF4FF2A6, 0, 1) end
        end
    end)
    
  • projection.entity_box(id) instead of rebuilding tables — it projects the live entity by id with interpolation, allocating nothing on the script side.

  • Localize hot functions: local entity_box = projection.entity_box before a loop avoids repeated table lookups.

  • Don't subscribe for nothing. A frame / packet_send callback fires very often; if once per tick is enough — use tick.

  • Coroutines are expensive — don't create them in a loop.

Examples

Spammer with HUD text

Settings, tick logic and 2D rendering in one module.

local module = ui.category("scripts"):module("code", "Lua Example")

local interval = module:number("Interval", 40, 5, 200, 1, "t")
local message = module:string("Message", "Hello from Lua!")
local mode = module:selection("Mode", nil, { "Client", "Chat" })
local hud = module:boolean("HUD text", true)
local color = module:color("Color", 0xFF4FF2A6)

local ticks = 0

module:callback("enable", function()
    ticks = 0
end)

module:event("tick", function()
    ticks = ticks + 1
    if ticks % interval:get() == 0 then
        if mode:get() == "Chat" then
            client.say_chat(message:get())
        else
            client.print_chat(message:get())
        end
    end
end)

module:event("render_2d", function(render)
    if not hud:get() or not minecraft.player() then return end
    local label = message:get() .. "  |  " .. client.fps() .. " fps"
    local w = render.text_width(label, 9) + 16
    render.rect(8, render.height() - 40, w, 22, 0x90101010, 7)
    render.text(label, 16, render.height() - 34, 9, color:get())
end)

2D ESP with JIT compilation

A full configurable ESP: target filtering, Box/Corner styles, health bar, names and distance. Data is collected once per tick; rendering only projects and draws.

-- @jit
local module = ui.category("visuals"):module("draw-square", "Lua ESP 2D")

local targets = module:multi_selection(
    "Targets",
    { "Players" },
    { "Players", "Mobs", "Items", "Other" }
)
local style = module:selection("Style", nil, { "Box", "Corner" })
local color = module:color("Color", 0xFF4FF2A6)
local thickness = module:number("Thickness", 1, 1, 5, 1)
local fill = module:boolean("Fill", false)
local names = module:boolean("Names", true)
local healthBar = module:boolean("Health bar", true)
local distanceTag = module:boolean("Distance", false)
local maxDistance = module:number("Max distance", 128, 16, 512, 8, "m")

local tracked = {}

local function classify(e)
    if e.type == "minecraft:player" then
        return "Players"
    elseif e.type == "minecraft:item" then
        return "Items"
    elseif e.health then
        return "Mobs"
    end
    return "Other"
end

module:callback("enable", function()
    tracked = {}
end)

module:callback("disable", function()
    tracked = {}
end)

module:event("tick", function()
    local selected = {}
    for _, t in ipairs(targets:get()) do
        selected[t] = true
    end
    local limit = maxDistance:get()
    local list = {}
    for _, e in ipairs(minecraft.entities()) do
        if not e.is_self and e.distance <= limit and selected[classify(e)] then
            list[#list + 1] = e
        end
    end
    tracked = list
end)

local function healthColor(frac)
    local r = math.floor(255 * (1 - frac))
    local g = math.floor(255 * frac)
    return 0xFF000000 + r * 0x10000 + g * 0x100
end

local function drawCorners(render, x, y, w, h, argb, stroke)
    local len = math.min(w, h) * 0.3
    render.line(x, y, x + len, y, argb, stroke)
    render.line(x, y, x, y + len, argb, stroke)
    render.line(x + w - len, y, x + w, y, argb, stroke)
    render.line(x + w, y, x + w, y + len, argb, stroke)
    render.line(x, y + h - len, x, y + h, argb, stroke)
    render.line(x, y + h, x + len, y + h, argb, stroke)
    render.line(x + w, y + h - len, x + w, y + h, argb, stroke)
    render.line(x + w - len, y + h, x + w, y + h, argb, stroke)
end

module:event("render_2d", function(render)
    local count = #tracked
    if count == 0 or not minecraft.in_game() then return end

    local argb = color:get()
    local stroke = thickness:get()
    local corner = style:get() == "Corner"
    local drawFill = fill:get()
    local drawNames = names:get()
    local drawHealth = healthBar:get()
    local drawDistance = distanceTag:get()
    local fillColor = (argb % 0x1000000) + 0x28000000

    for i = 1, count do
        local e = tracked[i]
        local x, y, w, h = projection.entity_box(e.id)
        if x then
            if drawFill then
                render.rect(x, y, w, h, fillColor)
            end
            if corner then
                drawCorners(render, x, y, w, h, argb, stroke)
            else
                render.outline(x - 1, y - 1, w + 2, h + 2, 0x80000000, 0, stroke + 2)
                render.outline(x, y, w, h, argb, 0, stroke)
            end
            if drawHealth and e.health then
                local frac = e.health / e.max_health
                if frac > 1 then frac = 1 end
                render.rect(x - 5, y, 2, h, 0xA0101010)
                render.rect(x - 5, y + h * (1 - frac), 2, h * frac, healthColor(frac))
            end
            if drawNames then
                local tw = render.text_width(e.name, 9)
                render.text(e.name, x + (w - tw) / 2, y - 13, 9, argb)
            end
            if drawDistance then
                local tag = string.format("%dm", math.floor(e.distance + 0.5))
                local tw = render.text_width(tag, 8)
                render.text(tag, x + (w - tw) / 2, y + h + 3, 8, 0xFFFFFFFF)
            end
        end
    end
end)

Auto totem via services

local module = ui.category("combat"):module("shield-heart", "Lua Auto Totem")

module:event("tick", function()
    if inventory.item(45).id ~= "minecraft:totem_of_undying" then
        local totem = inventory.find("minecraft:totem_of_undying")
        if totem then inventory.swap(totem, 45) end
    end
end)