-
Notifications
You must be signed in to change notification settings - Fork 0
Why Lua Macros
TrowlLord ships two macro systems. The first is a faithful re-implementation of
Clan Lord's classic macro language — the same "go" { ... }, set, goto, pause
syntax you've used for years, byte-for-byte compatible so your existing macros keep
working. The second is a full Lua 5.4 engine.
This page explains why the Lua engine exists: the classic language is wonderful for "send this command when I press this key," but it hits a wall the moment you need to manipulate text, keep a list of things, store a yes/no flag, or reuse logic. Those walls are exactly what Lua removes.
Both systems run side by side. You don't have to choose — use classic for your reflexes and reach for Lua when a macro starts fighting you.
| Feature | Classic | Lua |
|---|---|---|
| String functions (length, substring, replace, split, case) | ❌ only .word[n] / .letter[n]
|
✅ full string.* library |
| Real arrays / lists / maps | ❌ flat string keys like p1_name
|
✅ tables ({}), ipairs, pairs
|
| Booleans you can store | ❌ truth only exists inside if
|
✅ true / false are values |
Compound conditions (and / or / not) |
❌ one comparison per if
|
✅ if a and not b then
|
while / for loops |
❌ only label / goto
|
✅ while, for, repeat
|
| Functions with arguments & return values | ❌ call shares one flat scope |
✅ function f(x) return … end
|
Floating-point math, bitwise ops, math.*
|
❌ 32-bit ints only | ✅ full math.*
|
| Local scope / closures | ❌ dynamic, everything global-ish | ✅ local, closures |
Below are real macros that were painful (or impossible) in the classic language and are now a few clean lines of Lua.
The classic language gives you @text.word[n] and @text.letter[n] and nothing else.
There is no length, no substring, no find, no replace, no case conversion. "Does
this text log string contain 'sunstone', ignoring case?" simply can't be expressed — the closest
is the comparison operators, whose substring behavior is asymmetric and case-sensitivity
flips depending on which operator you pick (> is case-insensitive, >= is
case-sensitive…). It's a well-known footgun.
// Classic: "does my forehead item contain sunstone?" — fragile and case-sensitive
"check"
{
set item @my.forehead
if item >= "sunstone" // case-SENSITIVE substring test; misses "Sunstone"
"/think yes\r"
end if
}cl.bind_text("check", function()
local item = cl.my_item("right") or ""
if string.find(item:lower(), "sunstone", 1, true) then -- case-insensitive, reliable
cl.send("/think yes")
end
end)A harder one — parse a "give" command like /give Stinky 50 into pieces. In
classic you'd chain .word[0], .word[1], .word[2]… and hope the spacing is exactly
right; there's no way to validate that the amount is a number or to rejoin the tail.
cl.bind_text("give", function(rest)
-- pull out "<name> <amount>" with a pattern; reject anything malformed
local who, amount = rest:match("^(%S+)%s+(%d+)$")
if not who then
cl.message("usage: give <name> <amount>")
return
end
cl.send(string.format("/give %s %s coins", who, amount))
end)string.match, string.format, :lower(), :gsub() — none of these have any classic
equivalent.
myarr[2][3] isn't a 2-D array; it's literally the string key "myarr[2][3]". You can't
iterate it, you can't ask how long it is, and the index must be a literal or a single
variable — never an expression like myarr[i+1]. To "loop over a list" you hand-roll a
label/goto counter and pray.
// Classic: greet three friends — by hand, no iteration
"greet"
{
set f[0] "Stinky"
set f[1] "Drablak"
set f[2] "Karina"
set i 0
label loop
if i < 3
"/yell hi " f[i] "\r"
set i + 1
goto loop
end if
}local friends = { "Stinky", "Drablak", "Karina" }
cl.bind_text("greet", function()
for _, name in ipairs(friends) do
cl.send("/say Hi " .. name)
end
end)And because it's a real list you can do things classic can't express at all — build it up at runtime, keep a rolling history, dedupe, sort:
-- remember the last 5 people who fell, newest first, no duplicates
local fallen = {}
cl.register("watch_falls", function()
for _, line in ipairs(cl.textlog()) do
local who = line.text:match("(%S+) falls to a")
if who then
-- drop an existing entry, then push to front
for i = #fallen, 1, -1 do
if fallen[i] == who then table.remove(fallen, i) end
end
table.insert(fallen, 1, who)
fallen[6] = nil -- cap at 5
end
end
end)There is no boolean type. The only "true" the classic language understands is the literal
string "true" for a handful of @env flags. You can't write set ready (hp > 50), and
you can't write if low_health and not in_town — every if is a single comparison, so
compound conditions become nested ladders.
// Classic: "heal only if hurt AND not poisoned" — nested, awkward
"heal"
{
if hurt == "true"
if poisoned == "false"
"/useitem caduceus\r"
end if
end if
}local hurt = false
local poisoned = false
cl.bind_text("heal", function()
if hurt and not poisoned then
cl.send("/useitem caduceus")
end
end)A clean toggle — trivial in Lua, genuinely clumsy in classic (you'd juggle a string
"on"/"off" and compare it, or use an integer value of 0 and 1):
local sneaking = false
cl.bind_key("f3", function()
sneaking = not sneaking
cl.message(sneaking and "Sneaking ON" or "Sneaking OFF")
end)A classic "function" macro takes no arguments, returns nothing, and writes into the same
flat variable pool as its caller — so a helper that uses a temporary i silently clobbers
the caller's i. There are no closures and no recursion safety.
// Classic: a "double it" helper has to communicate through a shared variable
double
{
set result n * 2 // reads caller's n, writes caller's result — naming discipline only
}
"calc"
{
set n 21
call double
"/think " result "\r"
}local function double(n) return n * 2 end
cl.bind_text("calc", function(rest)
local n = tonumber(rest) or 0
cl.send("/think " .. double(n))
end)Because all Lua files load into one shared world, you can keep a library of helpers in one file and call them from any other — no copy-paste, no global-variable collisions.
This is the kind of macro that's a small project in classic (a hand-rolled goto queue of
p1_name, p2_name, … with manual index math and string-compare bugs) and a dozen
readable lines in Lua — strings, a list, a boolean, and a function all at once:
local party = { "Stinky", "Drablak", "Karina" }
local idx = 1
local function next_member()
local who = party[idx]
idx = idx % #party + 1 -- wrap around the list
return who
end
-- F1 heals the next party member in rotation, skipping yourself
cl.bind_key("f1", function()
local me = cl.my_simple_name()
local who = next_member()
if who and who:lower() ~= (me or ""):lower() then
cl.send("/useitem caduceus " .. who)
end
end)Lua doesn't replace classic — for a huge number of macros, classic is shorter:
- Pure key bindings:
f1 { "/think ready\r" } - Text abbreviations / expansions:
'ty' { "thank you!" } - One-liner command triggers and quick
pausesequences.
Reach for Lua when a macro needs to remember a list, manipulate text, hold a flag, share logic, or branch on more than one condition. That's the dividing line.
-
Lua API Cheatsheet — every
cl.*function on one page. - Lua Macro Guide — the full walkthrough with examples.
- The same content is built into the app: Macros window → Lua Guide.
- Lua language reference: https://www.lua.org/manual/5.4/