Skip to content

Events and Binds

Beyond reading state with mq.TLO.*, scripts can register slash commands, subscribe to game events, and drive the client.

Commands

eq2.register_command(name, fn)

Registers /name and routes it to fn(args). Returns false if the name is already taken.

eq2.register_command("greet", function(args)
  eq2.print("hello, " .. (args ~= "" and args or "world"))
end)

while true do eq2.delay(1000) end     -- the command dies with the script

args is the entire rest of the line as one string, unsplit. Parse it yourself:

local sub, rest = args:match("^(%S*)%s*(.-)$")

The command is unregistered automatically when the script stops.

Callbacks stored in C++ must bind the main thread

This is handled for you by eq2.register_command and every eq2.on_* below, but it's worth knowing why: a Lua callback held across the C++ boundary is bound as a main protected function. Binding the coroutine's own state instead means the handler fires against a coroutine suspended inside eq2.delay(), and fails.

eq2.cmd(line) / eq2.cmdf(fmt, ...)

Runs a slash command through the game's own input path — a real send for chat, casts, and everything else.

eq2.cmd("/say hello")
eq2.cmdf("/target_id %d", id)

eq2.cmd_available()

Is the command system ready to accept sends? false when the underlying hook didn't resolve.

eq2.target_by_id(spawn_id)

Target a specific actor by spawn id. Resolves the actor's ghost handle and calls the native target function. Returns true on success, false on failure.

if eq2.target_by_id(mob_id) then
  eq2.print("targeted " .. mq.TLO.Target.Name())
end

Prefer this over /target <name> when you have the spawn id, since name-based targeting can hit the wrong mob when duplicates exist.

Output

Call What
eq2.print(...) To clockwork's log and the overlay console. Multiple args are tostring'd and space-joined, like Lua's print.
eq2.write_chat(text) Local-only print into your own chat window. Sends nothing.
eq2.write_chatf(fmt, ...) Same, with string.format.

eq2.write_chat does not send. Use eq2.cmd("/say ...") to actually talk.

Chat timestamps

Call What
eq2.chat_timestamps(enabled, fmt?) Enable or disable timestamp injection into the game's own chat window. Optional fmt is a strftime format string.
eq2.chat_timestamps_enabled() Is timestamp injection currently on?
eq2.chat_timestamp_channels(t?) Get or set the per-channel filter table.

The /chattime script wraps the toggle.

Timing

Call What
eq2.delay(ms) Yield; resume after ms. Required periodically.
eq2.gametime() Millisecond timestamp, GetTickCount64()-style.

Events

Every subscription follows the same shape:

local token = eq2.on_something(function(payload) ... end)
eq2.off_something(token)     -- rarely needed; auto-swept on script stop

Handlers are dispatched on the game thread, from the pulse, off a snapshot — so a handler that unsubscribes itself mid-dispatch is safe.

Chat

eq2.on_chat(function(channel, text, local_echo)
  if text:find("invite") then eq2.cmd("/accept") end
end)
Arg Type Notes
channel number Channel id. lib.chat has named constants.
text string The line.
local_echo bool True if this is your own message echoing back.

Check local_echo before reacting — a handler that replies to text it also prints will loop.

Cast failures

eq2.on_cast_fail(function(code)
  if code == 0x26 then          -- out of line of sight
    require("lib.nav").los_start()
  end
end)

The payload is a single numeric reason code.

Command queue

eq2.on_cmdqueue(function(ev)
  eq2.print(ev.cmd)
end)

Observes commands the client's own UI pushes onto its internal queue — the verbs its buttons emit, which are not the same set as typed slash commands.

Field Type
cmd string
caller number — RVA of the caller, for diagnostics
self number
seq number
truncated bool
Call What
eq2.on_cmdqueue(fn) Subscribe. Returns a token.
eq2.off_cmdqueue(token) Unsubscribe.
eq2.cmdqueue_available() Is the cmdQueuePush observation hook resolved? Also gates whether on_cmdqueue will ever fire.

The observe side above is read-only. To push onto the same queue yourself:

eq2.cmdqueue("some_internal_verb")
Call What
eq2.cmdqueue(line) Push a raw string straight onto the game's own internal command queue via cmdQueuePusha different channel from eq2.cmd/eq2.cmdf above, which go through RunInputLine's typed-command parser instead. This reaches internal-only verbs RunInputLine has no handler for. Sent verbatim, no formatting applied. Returns false if the hook hasn't resolved.

Quest offers

eq2.on_quest_offer(function(q)
  eq2.print("offered: " .. q.name)
  eq2.quest.accept()
end)
Field Notes
name Quest name.
accept_cmd This client's own server-supplied accept command. Never ship it to a peer — it is not portable.
crc Offer checksum.
seq Monotonic sequence number.

Poll-style equivalents, for policy that prefers not to use the event:

Call Returns
eq2.quest.hook_ok() Is the offer-capture hook live?
eq2.quest.pending() Is an offer captured and waiting?
eq2.quest.offer() {name, accept_cmd, crc} or nil.
eq2.quest.accept() Accept this client's captured offer.
eq2.quest.accept_click() Accept via UI button click.
eq2.quest.accept_all() Accept locally, then broadcast quest_accept so each peer accepts its own.
eq2.quest.offer_window() The offer window UiHandle, or nil.
eq2.quest.reward_window() The reward window UiHandle, or nil.
eq2.quest.reward_click() Click the reward-accept button.

Dialogs

eq2.on_dialog(function(d)
  if d.event == "open" then
    local st = eq2.dialog.status()
    for _, opt in ipairs(st.options) do eq2.print(opt.index .. ": " .. opt.text) end
  end
end)
Field Notes
event "open", "page", or "close".
ctx Opaque monotonic integer handle — not a pointer.
conv_id Conversation id.
option_count Number of replies offered.

"page" fires only on the observe path — your own eq2.dialog.reply() calls don't re-trigger it.

Call Returns
eq2.dialog.ready() Health gate: is the advance path resolved?
eq2.dialog.status() {open, conv_id, option_count, options} or nil. Each entry in options is {index, text}.
eq2.dialog.reply(index) Advance by 0-based reply index.

Matching an option by text rather than blind index is policy, not a binding — lib.dialog's reply_by_text does it.

Travel

eq2.on_travel_list(function(t)
  for _, d in ipairs(t.destinations) do eq2.print(d.text) end
end)
Field Notes
portal_id The portal that produced the list.
window Opaque monotonic generation counter — not a pointer.
destinations An array; each entry is {key, diff, data, text}.
Call What
eq2.travel.list() Snapshot of the observed destination list.
eq2.travel.map_list() Fast-travel map destinations.
eq2.travel.map_select(label) Commit a map destination by display label. Commits by record, so it reaches every zone instance.
eq2.travel.map_status() Map hook state.
eq2.travel.status() Portal-list hook state and relay counters.
eq2.travel.relay_last() Relay the last captured destination to peers.
eq2.travel.relay_enabled() / .set_relay_enabled(b) Master auto-relay switch, shared with zone doors.

eq2.travel.select() was removed

It fabricated a network message. Use eq2.travel.map_select(label).

Travel (continued):

Call What
eq2.travel.portal_select(...) Select a portal destination (replaces the removed select()).

Zone doors:

Call What
eq2.zone_door.click{spawn_id=, name=, prompt=, flag=} Table arg; needs at least spawn_id or name.
eq2.zone_door.interact(...) Interact with a door.
eq2.zone_door.status() Hook state and relay counters.
eq2.zone_door.relay_last(scope?) Relay the last captured click to peers. Optional scope filter.

Quest objects:

Call What
eq2.quest_object.status() Hook state for quest-clickable objects.
eq2.quest_object.relay_last(scope?) Relay the last quest-object interaction to peers.

Reply dialogs:

Call What
eq2.reply_dialog.status() Current reply-dialog state (for examinable items that open a ReplyDialog).
eq2.reply_dialog.select(index) Select a reply option by index.
eq2.reply_dialog.window() The dialog window UiHandle, or nil.

Network messages

eq2.on_net_msg(function(msg)
  eq2.print(("type=0x%04X len=%d"):format(msg.type_id, msg.len))
end)

Subscribe to inbound network messages. Two forms:

  • eq2.on_net_msg(fn) — wildcard, fires for every message.
  • eq2.on_net_msg(type_id, fn) — filtered to one message type.
Call What
eq2.on_net_msg(fn) / eq2.on_net_msg(type_id, fn) Subscribe. Returns a token.
eq2.off_net_msg(token) Unsubscribe.
eq2.net_msg_available() Is the net-message hook resolved?
eq2.net_stats() Per-type message histogram.
eq2.net_stats_reset() Clear the histogram.

Network send (outbound)

eq2.on_net_send(function(p)
  if not p.self then
    eq2.print(("vtable=%s flag=%d seq=%d"):format(
      ("0x%X"):format(p.vtable), p.flag, p.seq))
  end
end)

Subscribe to outbound network messages. Two forms:

  • eq2.on_net_send(fn) — wildcard, fires for every outbound send.
  • eq2.on_net_send(vtable, fn) — filtered to messages with a specific vtable pointer.
Field Type Notes
vtable number First 8 bytes of the message object (message type discriminator).
caller number Return address as an EverQuest2.exe RVA; 0 when the caller is outside the game module.
seq number Monotonic send counter.
flag number The flag byte passed to netSend.
self bool True when clockwork itself sent this message.
raw string Bounded raw prefix of the message object (up to 256 bytes).
Call What
eq2.on_net_send(fn) / eq2.on_net_send(vtable, fn) Subscribe. Returns a token.
eq2.off_net_send(token) Unsubscribe.
eq2.net_send_available() Is the netSend hook resolved?

Crafting

eq2.on_craft_round(function(ev)
  eq2.print(("round: result=%d quality=%d progress=%.0f durability=%.0f")
    :format(ev.result, ev.quality, ev.progress, ev.durability))
end)

Subscribe to crafting round-results (opcode 0xE5) — one event per round of a tradeskill combine, whether the round was triggered by you or the client's own auto-event logic.

Field Type Notes
result number 1-4.
durability number
progress number
quality number 0-4.
main_icon number
mod_a, mod_b number
message string
Call What
eq2.on_craft_round(fn) Subscribe. Returns a token.
eq2.off_craft_round(token) Unsubscribe.

lua/craft.lua and lua/autocraft.lua are the bundled scripts built on this — see /craft and /autocraft.

Loot

Call What
eq2.loot.status() Loot window state.
eq2.loot.items() Array of items in the current loot window.
eq2.loot.window() The loot window UiHandle, or nil.
eq2.loot.loot_item(a, b) Loot an item by index.

Broker

Call What
eq2.broker.consignments() Your broker consignment listings.
eq2.broker.container() The broker container (lot reader).

Native UI

Most native-UI scripts start by finding a window, then act on the UiHandle they get back:

local w = eq2.ui.window("Choice")
if w and w.visible then
  w.h:click()
end
Call What
eq2.ui.windows() Array of {name, visible} for every top-level window clockwork's window walk can see. Best-effort — see the caveat below.
eq2.ui.window(name) A top-level window by exact, case-insensitive name. A UiHandle, or nil.
eq2.ui.find(name_or_path) Recursive search through the game's own child lookup, starting from the UI root. Unlike eq2.ui.window(), this can reach a control nested inside an already-open window without waiting for a fresh show event — the trade-off is it walks the game's own (occasionally mistyped) child-lookup path rather than clockwork's window enumeration.

windows()/window() may not see everything

The underlying walk is a best-effort read of the client's own window tree, not a guaranteed-complete enumeration. A window that exists but doesn't show up in windows() is a known gap, not necessarily a bug in your script — try eq2.ui.find() instead.

Once you have a UiHandle, see UiHandle for the full set of things you can do with it — read text, click, walk children, peek/poke raw offsets.

local token = eq2.on_ui_action(function(ev)
  if ev.name == "Accept" and ev.event == "OnPress" then
    ev.h:click()
  end
end)
Field Notes
control Opaque numeric handle id — never a pointer.
h The same handle as a UiHandle, so you can act on the control you just observed.
name Control name.
event Event name.
listeners Listener count.
vtable, thunk, arg Diagnostic hex, for log correlation only.

Arm the listener only while you need it

Every observed event mints a UI handle, and the client's handle table has 4096 slots. An always-on on_ui_action subscription across a long session can saturate it, after which every handle request fails for the rest of the session — which looks like a completely unrelated bug.

Subscribe when your window opens, unsubscribe when it closes.

Window show/hide

eq2.on_ui_showhide("Choice", function(e)
  if e.visible then
    -- suppress the popup by clearing the visible bit
    local flags = e.h:peek_u32(0xC8)
    if flags then e.h:poke_u32(0xC8, flags - (flags % 2)) end
  end
end)

Subscribe to window show/hide events. Fires for every widget the client shows or hides — the same hook that drives upsell suppression and quest-dialog capture, now exposed to Lua. Two forms:

  • eq2.on_ui_showhide(fn) — wildcard, fires for every show/hide.
  • eq2.on_ui_showhide(name, fn) — case-insensitive substring filter on the window name.
Field Type Notes
name string The window/control name (read via the handle system).
visible bool true = show, false = hide.
h UiHandle Adopted handle — call :poke_u32(), :peek_u32(), :name(), :find(), :text(), etc.
Call What
eq2.on_ui_showhide(fn) / eq2.on_ui_showhide(name, fn) Subscribe. Returns a token.
eq2.off_ui_showhide(token) Unsubscribe.

Unlike on_ui_action, this bus does not mint a handle per observed event in the push path — the handle is adopted only in the drain, so it has no handle-table saturation risk from high-frequency widget traffic. The g_showhide_wanted atomic gate means zero cost when no Lua listener exists.

Reacting to a show, not preventing it

The callback fires after the game has already shown the window (the event is queued in the hook and drained on the next pump). To suppress a show, clear the visible bit in the callback as shown above. The window may flash for one frame.

Multibox

eq2.peer.* broadcasts to selected clients; each peer executes locally on its own game thread.

Call What
eq2.peer.run(script, args) /lua run <script> <args> on every selected client, including this one.
eq2.peer.run_others(script, args) Same, excluding this one.
eq2.peer.cmd(line) A raw command line on every selected client.
eq2.peer.cmd_others(line) Same, excluding this one.
eq2.peer.send(to, channel, data) Send a message to one peer.
eq2.peer.broadcast(channel, data) Send a message on a named channel.
eq2.peer.group_send(group, channel, data) Send to a group channel.
eq2.peer.on(channel, fn) / eq2.peer.off(token) Subscribe. fn(from, data).
eq2.peer.join(channel) / eq2.peer.leave(channel) Channel membership.
eq2.peer.group_channel() The current group channel name.
eq2.peer.name() This client's peer identity.
eq2.peer.set_identity(name) Set this client's peer identity.
eq2.peer.list() / eq2.peer.count() Who's out there.

Don't wrap peer calls in /dga or /dge

eq2.peer.* already broadcasts. Sending a /dga-prefixed line as the payload makes every receiver's own /dga handler re-broadcast it, so the work runs twice on every peer.

For the same reason, /dga refuses to broadcast a line whose first token is itself a broadcast command.

Same-box peers are not on loopback

Clients on one PC are discovered at the host's LAN address, never 127.0.0.1. A loopback-only check will refuse every peer on the same machine.

Writing a peer-relay receiver

eq2.peer.cmd(line) / eq2.peer.cmd_others(line) deliver line to each target peer's own game thread, where it is dispatched exactly like a typed command: clockwork's own command registry first, then the game's native RunInputLine if nothing claims it.

That second half is the trap. If no script on the receiving box has registered the command name with eq2.register_command, the relayed line falls straight through to the game client as literal input. EverQuest II has no such command, so it's silently ignored — no error on the sender, no error on the receiver, nothing in the log. eq2.peer.cmd_others reports success the same way whether or not anything on the other end was listening.

In practice this means: for a relay to do anything, the matching receiver script must already be running on every peer that should react. Several clockwork scripts are built as relay/receiver pairs this way:

Relay sends Receiver script registers
/travelto <text> travel
/dialogreply <text> dialogreply
/maptravel <destination> maptravel
/resettimer <zone_id>, /resetalltimers resettimer
/qj delete <name> qj
/settarget <id>|<name> targetsync

None of these start themselves on a peer — the leader-side script only sends a command line, it never launches anything remotely (eq2.peer.run* exists for that, but relaying a launch at an already-running resident script is a no-op by design, since /lua run on a running script is refused). The receiver has to already be resident before the first relay arrives.

Autoload the receiver on every peer

Add the receiver script's name (bare, one per line — no /lua run, no args) to config/autoload.txt. clockwork launches every listed script once per character login. A per-character list also exists at config/<CharacterName>/autoload.txt for boxes that need a different set; see the shipped autoload.txt.example template.

A relay script's own comments are usually explicit about which receiver it depends on — check there before assuming a relay "isn't working" when it may just be talking to an empty room.

lib.resident (Helper Libraries) is the shared command-queue-plus-worker-loop shape most receiver scripts are built on.

ImGui

eq2.imgui.init("mypanel", function()
  if ImGui.Begin("My Panel") then
    ImGui.Text("hp: " .. mq.TLO.Me.PctHP() .. "%")
  end
  ImGui.End()
end)

while true do eq2.delay(100) end
Call What
eq2.imgui.init(name, fn) Register a per-frame draw callback. name must be globally unique.
eq2.imgui.destroy(name) Remove it.
eq2.imgui.exists(name) Is a window with this name registered?

The global ImGui table is a guard proxy: calling ImGui.* outside a draw callback raises a Lua error rather than corrupting the frame. That guard exists because out-of-frame calls were a live crash path.

ImGui.Image and texture ids

Passing a plain Lua number where a texture reference is expected is a guaranteed client crash — the DirectX 11 backend casts it straight to a device pointer. A binding existing and compiling is not evidence it is safe to call with an arbitrary number.

Movement

Vertical-axis and jump controls, added alongside the existing eq2.move_drive / eq2.turn_drive axes:

Call What
eq2.jump(active) Start (true) or stop (false) the jump input. Event-driven — call once, not per-frame.
eq2.move_up(active) Start/stop upward movement (e.g. swimming, flying).
eq2.move_down(active) Start/stop downward movement.

These complement eq2.move_drive() and eq2.turn_drive(), which handle forward/strafe and heading. See Navigation for the full movement surface.

Detour navmesh queries for offline-baked zone meshes:

Call What
eq2.navmesh.available() Is the navmesh subsystem initialized?
eq2.navmesh.zones() List of zone keys with baked meshes.
eq2.navmesh.load(zone_key) Load a zone's navmesh.
eq2.navmesh.load_current() Load the navmesh for the current zone.
eq2.navmesh.loaded() Currently loaded zone key, or nil.
eq2.navmesh.find_path(fx,fy,fz, gx,gy,gz) A* path from start to goal. Returns corners, nil or nil, reason.
eq2.navmesh.path_length(fx,fy,fz, gx,gy,gz) Total 3D path length through the navmesh (same pipeline as find_path, no table allocation). Returns length, nil or -1, reason.
eq2.navmesh.nearest_point(x,y,z) Snap to nearest point on the mesh.
eq2.navmesh.raycast(fx,fy,fz, gx,gy,gz) Raycast along the mesh.
eq2.navmesh.local_geometry(cx,cz,radius) Local mesh geometry around a point.

lib.navpath wraps find_path and path_length for common use.

Textures

Call What
eq2.texture.load(path) Load a texture from disk. Returns a TextureHandle with .width, .height, .valid.
eq2.texture.release(handle) Release a loaded texture.

Inventory

Call What
eq2.inventory() Snapshot of all bag items as an array of item.
eq2.equipped() Snapshot of equipped items.
eq2.inventory_space() Free bag slot count (same as Me.FreeInventory()).
eq2.item_def(item_def_id) Examine-level detail for one item template — see item_def. nil until primed.
eq2.item_examine(item_def_id) Requests examine data for an item template from the server. Fire-and-forget — poll item_def() for the result.

Process

Call What
eq2.process_memory() Current process memory stats.
eq2.trim_working_set() Release unused physical memory.

Files and misc

Call What
eq2.mkdir(path) Recursive mkdir -p.
eq2.listdir(path) List a directory.
eq2.luaDir Absolute path to the script directory.
eq2.name This script's registered name.
require("lsqlite3") SQLite, for script-side persistence. See below.

require("lsqlite3")

local sqlite3 = require("lsqlite3")
local db = sqlite3.open(eq2.luaDir .. "/mydata.sqlite3")
db:exec("CREATE TABLE IF NOT EXISTS seen (id INTEGER PRIMARY KEY)")

local stmt = db:prepare("SELECT * FROM seen")
for row in stmt:nrows() do eq2.print(row.id) end
stmt:finalize()
db:close()
SqliteDb method What
db:exec(sql) Run a statement with no result rows.
db:prepare(sql) Compile a statement, returns a SqliteStmt.
db:busy_timeout(ms) Set the lock-wait timeout.
db:errmsg() Last error message.
db:last_insert_rowid() Rowid of the last insert.
db:update_hook(fn) Callback on insert/update/delete.
db:close() Close the connection.
SqliteStmt method What
stmt:bind(...) Bind parameters.
stmt:step() Advance one row.
stmt:nrows() Iterator form — one table per row, for a for row in ... loop.
stmt:reset() Rewind to re-execute.
stmt:finalize() Release the statement.

The module table also carries sqlite3.open(path) and the usual result/open constants (OK, ROW, DONE, BUSY, INSERT, UPDATE, DELETE, OPEN_READWRITE, OPEN_CREATE, OPEN_READONLY, OPEN_NOMUTEX, OPEN_FULLMUTEX).

Advanced / internal surfaces

These exist and are reachable from any script, but they back clockwork's own tooling more than end-user automation — treat them as advanced/unstable rather than reaching for them by default.

Surface What
eq2.mem.* (read_u8u64/i8i64/f32/f64/ptr/cstr/bytes, chain, hexdump, base, scan, scan_all, rip_target) Raw process-memory reads, keyed off the signature registry. What lua/inspector.lua's struct inspector is built on.
eq2.sig.* (get, list, reresolve) Query the signature registry that resolves every hook/offset clockwork uses internally.
eq2.quests(), .achievements(), .quests_at(), .quest_list(), .quest_tree(), .jw_read(), .jw_str(), .scene_peek(), .journal_peek(), .selected_quest_id() Raw quest-journal tree primitives, returning QuestEntry/QuestListEntry handles. Prefer lib.questjournal, which wraps the common list/find/delete/share operations over this surface.

See also