Skip to content

Helper Libraries

Pure-Lua convenience wrappers shipped under lua/lib/. These are not extra bindings — they are ordinary Lua modules over the same eq2.* surface your own scripts use. require them rather than re-deriving common patterns.

local inv = require("lib.inventory")
local nav = require("lib.nav")

Anything you drop in lua/ or lua/lib/ resolves the same way, so your own modules sit alongside these.

lua/lib/

Module What
lib.chat Named channel-id constants plus thin wrappers over eq2.on_chat / eq2.write_chat.
lib.nav The /nav command set, moveto / follow / flank, and route save-load. See Navigation.
lib.navpath Path solving over the Detour navmesh. See Navmesh pathfinding.
lib.followbus Position beacon over the peer mesh, behind /nav followclose. See Close follow.
lib.config Per-character config file load/save under config/.
lib.inventory Item lookup and action helpers. See lib.inventory.
lib.loot.loot Loot pipeline: corpse discovery, item extraction, dispatch.
lib.loot.loottags Loot rule table: tag items keep/sell/broker/ignore, persist to config. Registers /autoloot.
lib.loot.sell Vendor sell engine: drives the merchant Sell tab. Registers /sellall.
lib.loot.broker Broker/market window automation.
lib.loot.vendcontainer Broker consignment lot reader.
lib.loot.notrade NO-TRADE confirmation handling.
lib.loot.nbg Need-Before-Greed loot roller, rules-based, dry-run by default. Registers /nbg.
lib.coin Coin parsing and formatting.
lib.combo Heroic-opportunity / combo helpers.
lib.abilitytypes Ability classification tables.
lib.spellrank Spell rank/tier parsing.
lib.dialog Conversation-advance policy over eq2.on_dialog / eq2.dialog.*, including reply_by_text.
lib.quest Quest-grab state machine over eq2.on_quest_offer / eq2.quest.* / lib.dialog.
lib.questjournal Quest journal tree walker: list, find, delete, share quests.
lib.choice HUD.Choice popup interaction (rez confirms, fast-travel confirms, etc.).
lib.travel Travel-select and retry state machine.
lib.stats Self-only character stats (attributes, vitals, resists) plus actor-level DPS/Fervor. See lib.stats.
lib.resident Resident script framework: command queue + worker loop for long-running scripts.
lib.questui / lib.travelui ImGui panel bodies (.draw()) for lua/panels.lua.
lib.discord.webhook Outbound Discord webhook client.
lib.discord.format Discord message/embed formatting for loot announcements.

Discord integration

lua/discord/ is a single resident script (/lua run discord) with two independent directions, behind /discord and /discordinbox:

Module What
discord.init Entry point. Registers both commands, loads config, boots the other three modules, and pumps them in one tick loop.
discord.loot Outbound. Watches your own and groupmates' loot chat and queues an embed for each match, sent through lib.discord.webhook. No relay bot needed — just a webhook URL.
discord.inbox Inbound. Tails config/discord/inbox.jsonl, a file an external relay bot (outside the DLL) writes one authorized command per line to, and runs each through eq2.cmd. Disabled by default; every line must also match an allow_commands prefix.
discord.config Shared config for both directions, one file at config/discord/discord.lua, written with defaults on first run.

lua/gyonin/lib/

Used by the bundled combat automation. Note the gyonin.lib. prefix — these are not under lua/lib/:

local combat = require("gyonin.lib.combat")
Module What
gyonin.lib.targets Role-target resolution, PBAE and friendly actor filtering.
gyonin.lib.casting Cast-and-wait helpers over Me.CastingSpell() / ability.IsReady().
gyonin.lib.combat Combat-state helpers.
gyonin.lib.rotation Generic rotation-table execution — buff, cure, and assist passes. Drives the per-class profiles in lua/gyonin/classes/.
gyonin.lib.gyonin_state Mode and persisted-settings state.

lib.nav wraps the raw movement bindings in the /nav command set plus route persistence to config/routes/*.lua.

The bindings underneath, if you want to drive movement yourself:

eq2.waypoints.record(route, label)      -- record current position into `route`
eq2.waypoints.clear(route)
eq2.waypoints.list(route)               -- { {label, x, y, z, heading}, ... }
eq2.waypoints.start(route, loop)        -- -> bool
eq2.waypoints.set_route(route, points)  -- points = { {x=, y=, z=, heading=, label=}, ... }
eq2.waypoints.set_goal(x, y, z)         -- retarget the ACTIVE route in place
eq2.waypoints.stop()
eq2.waypoints.status()
eq2.waypoints.tuning() / .set_tuning(t) -- steering parameters

status() returns:

{
  hookReady, active, loop, index, count, distance, vertical,
  headingError, forward, turnLeft, turnRight, route, state, reason,
}

Raw axis drives — rarely needed directly, since /nav fwd and /nav turn wrap them with auto-release. Call every frame to hold; they are not latching:

eq2.move_drive(forward, strafe)   -- each in [-1, 1]
eq2.move_release()
eq2.turn_drive(delta)             -- heading delta per frame
eq2.turn_release()

Vertical and jump controls (event-driven, call once not per-frame):

eq2.jump(true)       -- start jumping
eq2.jump(false)      -- stop
eq2.move_up(true)    -- swim/fly upward
eq2.move_down(true)  -- swim/fly downward

Close follow

follow(name, dist) reads the leader out of your client's actor table — a relayed, smoothed copy of someone else's character — and only resumes once the distance has drifted past its resume band.

lib.followbus puts the leader's own client's position on the peer mesh (channel nav.pos) so followclose(name, dist) can steer at a first-hand position and, more to the point, resume on the moving flag in the report instead of on drift. The follower is told the leader stepped off rather than inferring it.

local followbus = require("lib.followbus")

followbus.pump()                  -- leader side, every tick (nav.tick does this)
followbus.subscribe()             -- follower side; idempotent
followbus.sample(name, ttl_ms)    -- { x, y, z, heading, zone, hp, moving, at } or nil
followbus.reporters(ttl_ms)       -- who is broadcasting, freshest first
followbus.quiet()                 -- final "not moving" beacon on shutdown

Roughly 10 messages a second while moving and one a second while idle, throttled by the module. nav.tick() pumps and subscribes, so any client running nav or gyonin is already a beacon; navbeacon.lua covers a leader running neither.

Everything degrades rather than fails: with no mesh, no peers, or a leader running nothing, sample() returns nil and the follow falls back to the actor read. lib.nav announces the source in both directions and shows it in /nav status as close(peer) or close(actor).

A name in a payload is a claim

The mesh is unauthenticated and reachable across your LAN, and the character name a follower joins on travels inside the payload. Treat a report as a claim about who sent it, not proof — which is why lib.nav cross-checks a beacon against the leader's visible character when it can see one, and refuses one that is implausibly far away.

Leash every movement path

A chase with no radius gate will drag you across the zone. lib.nav's camp radius exists for this — an ungated line-of-sight chase once ran a character into melee well outside its assist radius, and the symptom is only visible with follow turned off.

Also remember Distance() returns 0.0 on failure, so every distance gate needs d > 0.

lib.navpath wraps the eq2.navmesh.* surface into a follow-the-waypoints loop. The navmesh data comes from offline-baked zone meshes under data/navmesh/.

local navpath = require("lib.navpath")
navpath.go(target_x, target_y, target_z)
Function What
path_to(gx,gy,gz [,fx,fy,fz]) Plan a path to (gx,gy,gz). Start defaults to player position. Returns corners, nil or nil, reason.
path_length(gx,gy,gz [,fx,fy,fz]) Total 3D path length to (gx,gy,gz). Start defaults to player position. Returns length or nil, reason. Runs the same Detour pipeline as path_to but returns a single number with no Lua table allocation — cheap enough to call per-mob for pull target sorting.
load(key) Load a zone's navmesh by key.
load_current() Load the navmesh for the current zone.
loaded() Currently loaded zone key, or "".
zones() List of zone keys with baked meshes.

The /navpath command (from navpathtarget.lua or navpathfollow.lua) wraps this into a script-level command.

Pathed routes

lib.nav uses pathed_route() internally for moveto, follow, flank, and line-of-sight chase. When a navmesh is loaded for the current zone, it queries lib.navpath.path_to() for a multi-corner A* path; when no mesh is loaded, it falls back to a straight-line single-waypoint route.

Navmesh path corners carry a flags field from Detour. When the OFFMESH link flag (0x04) is set on a corner, the waypoint is tagged with jump = true. lib.nav can use this to echo a jump input at offmesh transitions (e.g. ledge drops, short gaps). The route re-plans when the target moves more than 5 metres from the last plan.

-- What a pathed route looks like:
-- { {x=1, y=2, z=3, heading=0},
--   {x=4, y=5, z=6, heading=0, jump=true},   -- offmesh link
--   {x=7, y=8, z=9, heading=0} }

See also /navmap for the top-down mesh visualisation window.

lib.inventory

Wraps eq2.inventory() / eq2.equipped() with name lookup, and the native command strings with named functions. Matching is case-insensitive and accepts partial names.

Function What
list() / list_equipped() Return eq2.inventory() / eq2.equipped().
find(name) / find_equipped(name) First matching item, or nil.
actions(item) The action names the item's flag bits indicate.
equip(item) / equip_by_name(name) inventory equip <index> -1 0 0
unequip(item) / unequip_by_name(name) inventory unequip <index>
move(item, dest_bag) inventory move <index> <dest_bag> -3 0. dest_bag defaults to 0.
examine(item) / examine_by_name(name) info inventory <index>
scribe(item) / scribe_by_name(name) scribe_scroll_item <id>
use(item) / use_by_name(name) use_item <index>
read(item) / read_by_name(name) book read <index>
local inv = require("lib.inventory")
local scroll = inv.find("pure awe")
if scroll then
  eq2.print(table.concat(inv.actions(scroll), ", "))
  inv.scribe(scroll)
end

lua/inv_helper.lua is a runnable front-end over the same library — see /inv.

lib.stats

Character stats from the GameScene state blob (gs) and actor struct. The gs block is self-only — these functions read data that only exists for the local player. DPS and Fervor are actor-struct fields and accept any actor.

local stats = require("lib.stats")

eq2.print(("STR %d  STA %d  AGI %d  WIS %d  INT %d")
  :format(stats.Strength(), stats.Stamina(), stats.Agility(),
          stats.Wisdom(), stats.Intelligence()))

eq2.print(("HP %d/%d  Power %d/%d")
  :format(stats.CurrHP(), stats.MaxHP(), stats.CurrPower(), stats.MaxPower()))

eq2.print(("DPS %.1f  Fervor %.1f%%")
  :format(stats.DPS(mq.TLO.Me), stats.Fervor(mq.TLO.Me)))

Self-only (GameScene)

Function Returns
CurrHP() / MaxHP() Raw HP (integer, not percent)
CurrPower() / MaxPower() Raw power
HPRegen() / PowerRegen() Regen rate
Strength() Strength
Stamina() Stamina
Agility() Agility
Wisdom() Wisdom
Intelligence() Intelligence
ElementalResist() Elemental resist
NoxiousResist() Noxious resist
ArcaneResist() Arcane resist

Any actor

Function Returns
DPS(actor) DPS (float, e.g. 38.8)
Fervor(actor) Fervor % (float, e.g. 37.1)

dump(actor?)

Prints a formatted summary of all stats to the console. Defaults to mq.TLO.Me.

Editing a shared module

/lua restart with no name reloads every running script, which is what you want after editing anything under lua/lib/ — several scripts will have required it, and only a full restart picks the change up everywhere.

/lua restart

See also