Skip to content

Examples

A few complete, runnable scripts from lua/ in the clockwork repo, chosen to show a specific pattern end to end. Getting Started and Events and Binds build up the pieces individually; these put them together the way a real script does.

Every script here also ships with clockwork — run any of them with /lua run <name>.

A command with subcommands

lua/chattime.lua registers /chattime, parses a handful of subcommands out of the raw argument string, and otherwise just sits parked:

local chat = require("lib.chat")

local SOCIAL = { chat.GUILD, chat.GROUP, chat.TELL, chat.SAY,
                  chat.WORLD_CHANNEL, chat.NPC_SAY }

local fmt     = "[%H:%M:%S] "
local scope   = "all"          -- "all" | "guild"
local enabled = true

local function apply()
  chat.timestamps(enabled, fmt, scope == "guild" and SOCIAL or {})
  eq2.print(string.format("chattime: %s, format %q, %s channels",
    enabled and "ON" or "off", fmt, scope))
end

eq2.register_command("chattime", function(rest)
  rest = (rest or ""):gsub("^%s+", ""):gsub("%s+$", "")

  if rest == "" then
    enabled = not enabled
  elseif rest == "status" then
    eq2.print(...)
    return
  elseif rest == "all" or rest == "guild" then
    scope = rest
    enabled = true
  else
    -- anything else is treated as a strftime format
    fmt = rest:sub(-1) == " " and rest or (rest .. " ")
    enabled = true
  end

  apply()
end)

apply()
while true do eq2.delay(60000) end     -- park until stopped

The while true do eq2.delay(60000) end at the bottom is doing real work even though the loop body is empty: it's what keeps the script — and therefore the /chattime registration — alive. See Cleanup is automatic for why nothing runs after /lua stop chattime: the coroutine simply never resumes past that eq2.delay() again, so there's no "turn it back off" code to write here at all — chattime.lua relies on clockwork's own script-stop teardown to disable timestamps on the C++ side instead of doing it in Lua.

See /chattime.

Reading a roster and acting on it

lua/group_heal.lua polls mq.TLO.Group, builds a target list including yourself, and casts a configured ability on whoever drops below a threshold — trimmed here for clarity (see the shipped script for the full version with status logging):

local ability          = "Nature's Salve IV"
local hp_threshold      = 60
local scan_delay_ms     = 500
local cast_cooldown_ms  = 2500

local last_cast = {}

local function heal_targets()
  local targets, seen = {}, {}

  local me = mq.TLO.Me
  if me.ID() ~= 0 then
    targets[#targets + 1] = me
    seen[me.ID()] = true
  end

  local group = mq.TLO.Group
  if group.Available() then
    for _, member in ipairs(group.Members()) do
      -- id ~= 0 excludes cross-zone members: /useabilityonplayer only
      -- reaches someone in this zone, so a cross-zone member can't be a
      -- target here even though Group.Members() lists them.
      local id = member.ID()
      if id ~= 0 and not seen[id] then
        targets[#targets + 1] = member
        seen[id] = true
      end
    end
  end

  return targets
end

while true do
  for _, member in ipairs(heal_targets()) do
    local name, hp = member.Name(), member.PctHP()
    local now = eq2.gametime()

    if name ~= "" and hp > 0 and hp <= hp_threshold
       and now - (last_cast[name] or 0) >= cast_cooldown_ms then
      -- Do NOT quote the ability name here -- the native parser treats the
      -- first token after the command as the player and the rest as ability.
      eq2.cmd(string.format("/useabilityonplayer %s %s", name, ability))
      last_cast[name] = now
    end
  end

  eq2.delay(scan_delay_ms)
end

Two things worth noticing: the hp > 0 guard is required because PctHP() reads as a huge number while a target is dead, not 0, and Group.Members() includes members who are out of your zone — their ID() reads 0, which is why the loop filters on id ~= 0 rather than trying to heal an id that can't be targeted anyway.

A peer-mesh position beacon

lua/navbeacon.lua is the entire script — nine lines that do something, the rest is comments explaining why it exists as its own file instead of folding into lib.nav:

local followbus = require("lib.followbus")

local TICK_MS = 50

if not (eq2.peer and eq2.peer.broadcast) then
  eq2.print("navbeacon: peer mesh unavailable on this client -- nothing to do.")
  return
end

followbus.subscribe()   -- also listen, so this client's own /nav status is useful

while true do
  followbus.pump()
  eq2.delay(TICK_MS)
end

Any client already running lib.nav or gyonin is already broadcasting its position for followclose to work — navbeacon.lua exists for the one client that runs neither: the leader a human is actually driving. Autoload it on that character (see Autoload the receiver on every peer) and every follower's /nav followclose <name> has a first-hand position to steer on, instead of falling back to a plain actor read.

See Close follow.

A front end over a helper library

lua/inv_helper.lua registers /inv, a thin command-line wrapper over lib.inventory — the shape to copy any time you want a shipped library reachable by hand from chat instead of only from other scripts:

local inv = require("lib.inventory")

local function desc(it)
  if not it then return "<not found>" end
  return string.format("[%d] '%s'  bag=%d stack=%d flags=0x%x  actions: %s",
    it.index, it.name, it.bag, it.stack, it.flags,
    table.concat(inv.actions(it), ", "))
end

eq2.register_command("inv", function(argstr)
  local w = {}
  for word in tostring(argstr or ""):gmatch("%S+") do w[#w + 1] = word end
  local verb = w[1]

  if verb == "find" then
    eq2.print(desc(inv.find(table.concat(w, " ", 2))))
  elseif verb == "scribe" then
    local it = inv.find(table.concat(w, " ", 2))
    if it then inv.scribe(it) end
  -- ...list / equipped / actions / examine / use / read / equip / unequip / move
  end
end)

while true do eq2.delay(1000) end

(The real script's dispatch table is longer — list, equipped, actions, examine, use, read, equip, unequip, move, help — and every name argument is matched partial and case-insensitive. See the shipped file for the complete command set.)

See also