Getting Started with Lua¶
Hello, world¶
Create lua/hello.lua in your clockwork folder:
eq2.print("hello, world")
Run it:
/lua run hello
Output goes to clockwork's log and to the overlay console (End, or
/console).
That script prints once and exits — its main chunk returned, so the script is done. Nothing to stop.
Looping¶
Most scripts want to keep running:
while true do
eq2.print("tick")
eq2.delay(1000)
end
/lua run hello
/lua stop hello
eq2.delay(ms) yields your coroutine and reschedules it. Every loop needs
one. A resume that runs more than 500 ms without yielding is killed by the
watchdog.
Reading game state¶
local me = mq.TLO.Me
eq2.print(me.Name()) -- string
eq2.print(me.Level()) -- number
eq2.print(me.PctHP()) -- 0-100
eq2.print(mq.TLO.Zone.Name())
Note the trailing (). me.PctHP without it is the function object — printing
it gives you function: 0x..., which is the single most common beginner
mistake.
Reads never error. Out of world they return "" / 0 / false, so guard on
the value:
if mq.TLO.Me.ID() ~= 0 then
-- actually in the world
end
Doing things¶
eq2.cmd() runs a slash command through the game's own input path — the exact
same route as typing it:
eq2.cmd("/say hello")
eq2.cmd('/useability "Fireball"')
eq2.cmdf("/target_id %d", 12345) -- string.format built in
eq2.write_chat() prints into your own chat window without sending anything:
eq2.write_chat("this is local only")
eq2.write_chatf("hp: %d%%", mq.TLO.Me.PctHP())
The distinction matters. eq2.cmd("/say hi") is a network send everyone sees.
eq2.write_chat("hi") is a local echo nobody else sees.
Arguments¶
Everything after the script name on /lua run arrives as your script's argument
string. LuaJIT gives it to you as ...:
-- lua/greet.lua
local args = ...
eq2.print("args: " .. tostring(args))
/lua run greet Antonica
Registering a command¶
-- lua/greet.lua
eq2.register_command("greet", function(args)
if args == "" then
eq2.print("usage: /greet <name>")
else
eq2.print("hello, " .. args)
end
end)
while true do eq2.delay(1000) end -- keep the command alive
/lua run greet
/greet world
/lua stop greet
The handler receives the entire rest of the line as one string — clockwork does not split it into arguments. Parse it yourself:
local sub, rest = args:match("^(%S*)%s*(.-)$")
eq2.register_command returns false if the name is already taken.
A real script¶
Put it together — warn when your health drops, and cure yourself:
-- lua/watchdog.lua
local last_warn = 0
while true do
local me = mq.TLO.Me
if me.ID() ~= 0 then
-- Cure gate: HasCurableDetriment(), NOT DetrimentCount() > 0.
-- The counters are signed and -1 means "present but incurable" --
-- a DetrimentCount() gate spins forever on res sickness.
if me.HasCurableDetriment() then
eq2.cmd('/useability "Cure"')
end
local hp = me.PctHP()
if hp < 40 and eq2.gametime() - last_warn > 5000 then
eq2.write_chatf("LOW HEALTH: %d%%", hp)
last_warn = eq2.gametime()
end
end
eq2.delay(500)
end
Modules¶
require works against lua/ and lua/lib/:
local inv = require("lib.inventory")
local scroll = inv.find("pure awe")
if scroll then inv.scribe(scroll) end
See Helper Libraries for what ships in the box.
Iterating¶
/lua restart with no name reloads every running script, reusing the args
each was started with. That is the fast edit-test loop, especially when you're
editing a shared module under lua/lib/ that several scripts have required.
/lua restart
Common mistakes¶
| Symptom | Cause |
|---|---|
Prints function: 0x... |
Missing () on a TLO leaf: Me.PctHP instead of Me.PctHP(). |
| Script dies after one pass | Watchdog — a loop with no eq2.delay(). |
Command vanishes right after /lua run |
The script returned. Add a while true loop. |
attempt to call method error |
Used : instead of . — Me:Level() should be Me.Level(). |
| Distance checks always think you're adjacent | Distance() returns 0.0 on failure. Require d > 0. |
| Heal never fires when you die | Me.PctHP() reads as a huge number while dead, not 0. |
| Cure loops forever | Gated on DetrimentCount() > 0. Use HasCurableDetriment(). |
| Abilities "unknown" right after login | The ability-name cache is still warming. Wait on AbilityCacheReady(). |
Next¶
- Events and Binds — react to chat, dialogs, quests, UI.
- Top-Level Objects — the full read surface.
- Slash Commands