Spammer with HUD text
Settings, tick logic and 2D drawing in one module.
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
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
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.
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.