Skip to content

Examples

Four complete modules to copy: HUD text, a 2D ESP, auto totem, and a command driving a renderer.

Spammer with HUD text

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

spammer.lualua
local module = ui.create("code", "Lua Example")

local interval = module:slider("Interval", 5, 200, 40, 1, "t")
local message = module:input("Message", "Hello from Lua!")
local mode = module:combo("Mode", "Client", "Chat")
local show_hud = module:switch("HUD text", true)
local color = module:color_picker("Color", 0xFF4FF2A6)

local ticks = 0

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

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

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

2D ESP

Target filtering, Box and Corner styles, health bar, names and distance. The entity list is built once per tick and the render callback only projects and draws it, which is the split Performance argues for.

Full source — esp2d.lua, ~130 lines
esp2d.lualua
local module = ui.create("draw-square", "Lua ESP 2D")

local targets = module:selectable(
    "Targets",
    { "Players", "Mobs", "Items", "Other" },
    { "Players" }
)
local style = module:combo("Style", "Box", "Corner")
local thickness = module:slider("Thickness", 1, 5, 1, 1)
local maxDistance = module:slider("Max distance", 16, 512, 128, 8, "m")
local color = module:color_picker("Color", 0xFF4FF2A6)
local fill = color:create():switch("Fill", false)
local overlays = module:label("Overlays"):create()
local names = overlays:switch("Names", true)
local healthBar = overlays:switch("Health bar", true)
local distanceTag = overlays:switch("Distance", false)

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:event("enable", function()
    tracked = {}
end)

module:event("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(world: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, accent, stroke)
    local len = math.min(w, h) * 0.3
    render.line(x, y, x + len, y, accent, stroke)
    render.line(x, y, x, y + len, accent, stroke)
    render.line(x + w - len, y, x + w, y, accent, stroke)
    render.line(x + w, y, x + w, y + len, accent, stroke)
    render.line(x, y + h - len, x, y + h, accent, stroke)
    render.line(x, y + h, x + len, y + h, accent, stroke)
    render.line(x + w, y + h - len, x + w, y + h, accent, stroke)
    render.line(x + w - len, y + h, x + w, y + h, accent, stroke)
end

module:event("render_2d", function(render)
    local count = #tracked
    if count == 0 or not world 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 accent = render.paint(argb)
    local fillPaint = render.paint((argb % 0x1000000) + 0x28000000)
    local boxPaint = render.paint(argb):stroke(stroke, "inside")
    local backPaint = render.paint(0x80000000):stroke(stroke + 2, "inside")
    local barBack = render.paint(0xA0101010)

    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, fillPaint)
            end
            if corner then
                drawCorners(render, x, y, w, h, accent, stroke)
            else
                render.rect(x - 1, y - 1, w + 2, h + 2, backPaint)
                render.rect(x, y, w, h, boxPaint)
            end
            local health = e:health()
            if drawHealth and health then
                local frac = health / e:max_health()
                if frac > 1 then frac = 1 end
                render.rect(x - 5, y, 2, h, barBack)
                render.rect(x - 5, y + h * (1 - frac), 2, h * frac,
                            render.paint(healthColor(frac)))
            end
            if drawNames then
                local name = e:name()
                local tw = render.text_metrics(name, 9).width
                render.text(name, x + (w - tw) / 2, y - 13, 9, accent)
            end
            if drawDistance then
                local tag = string.format("%dm", math.floor(e:distance() + 0.5))
                local tw = render.text_metrics(tag, 8).width
                render.text(tag, x + (w - tw) / 2, y + h + 3, 8)
            end
        end
    end
end)

Auto totem

auto-totem.lualua
local module = ui.create("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)

Waypoints

A client command owns the data (subcommands, completion over the saved names, chat replies) and the module draws it in the world. The two halves have different lifetimes: .wp answers as soon as the script loads, the markers appear once the module is enabled.

waypoints.lualua
local module = ui.create("map-pin", "Lua Waypoints")
local color = module:color_picker("Color", 0xFF4FF2A6)
local limit = module:slider("Draw distance", 32, 1024, 512, 32, "m")

local points = {}

local function saved_names()
    local list = {}
    for name in pairs(points) do
        list[#list + 1] = name
    end
    table.sort(list)
    return list
end

commands.register({
    name = "wp",
    aliases = { "waypoint" },
    description = "Marks spots you want to find again",
    usage = {
        ".wp add <name>  — remember where you stand",
        ".wp del <name>  — forget one",
        ".wp list        — everything saved, with distances",
    },
    params = {
        { name = "subcommand", type = "add|del|list" },
        { name = "name", optional = true },
    },
    execute = function(ctx, args)
        local here = player and player:position():floor()
        local sub, name = args[1], args[2]

        if not here then
            ctx:error("join a world first")
        elseif sub == "add" and name then
            points[name] = here
            ctx:success(("%s saved at %d %d %d"):format(name, here.x, here.y, here.z))
        elseif sub == "del" and name then
            if not points[name] then
                ctx:error("no waypoint called " .. name)
                return
            end
            points[name] = nil
            ctx:success(name .. " removed")
        elseif sub == "list" then
            local names = saved_names()
            if #names == 0 then
                ctx:info("nothing saved yet")
                return
            end
            for _, saved in ipairs(names) do
                local away = math.floor(here:distance(points[saved]) + 0.5)
                ctx:info(("%s — %dm"):format(saved, away))
            end
        else
            ctx:error("usage: " .. ctx.prefix .. "wp <add|del|list> [name]")
        end
    end,
    complete = function(_, args)
        if #args == 1 then return { "add", "del", "list" } end
        if #args == 2 and args[1] == "del" then return saved_names() end
        return {}
    end,
})

module:event("render_3d", function(render)
    if not player then return end
    local eye = player:eye_position()
    local far = limit:get()
    for name, pos in pairs(points) do
        if eye:distance(pos) <= far then
            render.box(pos, pos + vec3(1, 1, 1), color:get(), 1.5, true)
            render.text(name, pos + vec3(0.5, 1.5, 0.5), color:get(), 1, true)
        end
    end
end)

The waypoints live in the script’s own table, so a reload clears them. Persist them with io, or declare them as a module setting.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close