Skip to content

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