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.)