Client
Obtained from clients:First(), clients:Get(id), or clients[n]. Everything you can do to
a game client hangs off this object, either as a direct method or as one of its sub-objects.
Sub-objects
| Field | Type | Availability |
|---|---|---|
c.combat | CombatController | Always |
c.quest | QuestController | Always |
c.dialog | DialogController | Always |
c.entity | EntityFinder | Always |
c.inventory | InventoryController | After require('inventory') |
c.pet | PetController | After require('pet') |
c.fishing | FishingController | After require('fishing') |
c.spell | SpellController | After require('spell') |
c.equipment | EquipmentController | After require('equipment') |
c.garden | GardenController | After require('garden') |
c.drops | DropsController | After require('drops') |
See opt-in modules.
require is an error, not a nilc.pet without a prior require('pet') raises a Lua error — pet module not required - call require("pet") first — which aborts the current hook. It does not evaluate to nil, so
if c.pet then is not a usable capability check. Call require once at file scope.
Identity
c:GetName() --> string the game window's title, '' if unknown
c:GetID() --> string stable client id
c:GetPID() --> number OS process id, 0 if unknown
GetName() returns the window title, not the character name. On a logged-in client that
title usually contains the character name, but it is whatever the game put in the title bar,
and it is '' when the title could not be read.
GetID() is what you pass to automation and compare against data.client_id in event
payloads. It is the only identifier that is stable and yours to key state on.
State predicates
c:IsConnected() --> boolean
c:IsInCombat() --> boolean
c:IsLoading() --> boolean
c:IsInDialog() --> boolean
All four return false when the underlying read fails — there is no error return, so a
transient memory hiccup looks like "not in combat" rather than "do not know". For anything
that must not act on a false negative, confirm on the next tick before changing state.
IsInCombat() requires both a live duel pointer and a duel phase that has not ended. See
the note on combat state.
Stats
c:GetStats() --> PlayerStats|nil
c:GetHealth() --> number
c:GetMaxHealth() --> number
c:GetMana() --> number
c:GetMaxMana() --> number
c:GetGold() --> number
c:GetLevel() --> number
GetStats() returns everything in one read, which is cheaper than six calls when you need
several values:
local s = c:GetStats()
if s then
utils.Log(string.format('%d/%d hp, %d/%d mana, level %d in %s',
s.current_health, s.max_health, s.current_mana, s.max_mana, s.level, s.zone_name))
end
PlayerStats fields: current_health, max_health, current_mana, max_mana, gold,
level, zone_name, is_in_combat, is_loading, is_in_dialog, position.
GetStats() returns nil on a failed read. The six single-value getters return 0 on a
failed read instead, which is why the threshold helpers below exist — GetHealth() == 0 is
not proof you are dead.
Threshold helpers
c:HealthBelow(pct) --> boolean
c:HealthAbove(pct) --> boolean
c:ManaBelow(pct) --> boolean
c:ManaAbove(pct) --> boolean
Percentages are 0–100. These avoid the divide-by-zero you get from computing the ratio
yourself when a read fails and max comes back as 0.
Location
c:GetZone() --> string e.g. 'Grizzleheim/GH_Hero'
c:GetPosition() --> Position { x, y, z }
c:InRange(target, dist) --> boolean
GetZone() returns '' when the zone could not be read. Treat empty as "unknown", never as
"somewhere else" — a zone gate written as if c:GetZone() ~= FARM_ZONE then leave() end
walks out of the zone on a single failed read.
GetPosition() always returns a table, never nil. On a failed read that table is
{ x = 0, y = 0, z = 0 }, so an origin-looking position is the failure signal.
InRange is a 2D check — X and Y only, Z ignored. That is usually what you want, since a
target on a bridge above you is still "here" for interaction purposes. It accepts either a
Position or an Entity.
Movement
c:Teleport(x, y, z) --> ok, err
c:Teleport(position) --> ok, err
c:TeleportWithRecovery(x, y, z) --> ok, err
c:QuestTeleport() --> result, err
c:Goto(x, y, opts) --> ok, err
c:GotoStep(x, y, opts) --> done, err
c:SetYaw(angle) --> ok, err
Both teleports take either three numbers or a single Position-shaped table. In the numeric
form z is optional and defaults to 0; in the table form a missing x, y, or z also
reads as 0, so passing a table that is not a position silently teleports you to the origin.
Teleport vs TeleportWithRecovery
Teleport writes the position and returns. TeleportWithRecovery writes it, then verifies
the character actually ended up there, and steps back toward the origin if the game
rubber-banded it into geometry.
Use TeleportWithRecovery by default. Plain Teleport is for when you have already
verified the destination is walkable and want the lower latency.
Writing coordinates from zone A that belong to zone B appears to succeed — the write lands,
GetPosition() reports the new coordinates — but the character is now standing at those
coordinates in the wrong world.
Always verify GetZone() matches before trusting a positional teleport for cross-zone work.
Zone changes need a door, a sigil, or QuestTeleport.
QuestTeleport
Teleports toward the active quest objective and reports what happened:
local result, err = c:QuestTeleport()
if result then
utils.Log('outcome: ' .. result.outcome .. ' (' .. result.reason .. ')')
end
result.outcome is one of:
| Outcome | Meaning |
|---|---|
success | Arrived at the objective |
zone_change | The teleport moved you to a different zone |
combat_entered | A fight started en route — this counts as progress, not failure |
rubber_banded | The game rejected the position and moved you back |
zone_required | The objective is in another zone; a door is needed |
indeterminate | Cannot tell whether it worked |
unknown | — |
zone_required is not a failure, it is information: the goal is elsewhere and you need a
transition. Treating it as an error produces bots that retry forever.
Also on the result: reason, from_zone, to_zone, position.
QuestTeleport is the one movement call that returns nil, err rather than false, err on
failure — check result, not ok. A returned result always carries an outcome; the error
slot is nil whenever a result came back.
Goto and GotoStep
Goto walks to a point, blocking until it arrives or times out. GotoStep performs one step
of the same walk and returns whether it is done — use it inside a tick loop:
local done, err = c:GotoStep(x, y, { duration_ms = 250, tolerance = 60 })
if done then state.phase = 'arrived' end
Goto options: timeout_ms, tolerance, max_step_ms. Each is applied only when positive;
anything else falls back to the navigator's own default.
GotoStep options: tolerance (default 25 units) and duration_ms (default 100),
which caps how long the single step holds the walk key. Its two returns read differently from
every other call on this page:
done | err | Meaning |
|---|---|---|
true | nil | Already within tolerance — arrived |
false | nil | Walked one step, not there yet — call it again next tick |
false | string | The step failed |
So false alone is not a failure. Branch on err first, then on done.
SetYaw
c:SetYaw(angle) --> ok, err
Turns the character to face a heading, in radians, without moving it. Goto and GotoStep
already turn before they walk, so this is for aiming at something you are not walking to —
lining up a fishing cast, or facing an NPC before clicking. It returns
false, 'navigator unavailable' on a client whose navigator has not come up yet.
Zone survey
ZoneChunks
c:ZoneChunks(opts) --> Position[]|nil, string|nil
Centre points of the current zone, spaced so that standing on each loads every entity assigned to it. Sweep them to find entities outside the draw distance.
local chunks, err = c:ZoneChunks()
local chunks, err = c:ZoneChunks({ entity_distance = 2000, from = c:GetPosition() })
entity_distance defaults to 3,147 units — the client's entity draw distance — and chunks
are spaced at 90% of it for overlap. from sets the sort origin; without it, chunks come
back nearest-first from the player.
Returns an error for zones with no navigation data, so always check.
PotionCount
c:PotionCount() --> current, max
Potion charges held and the maximum. A potion restores both health and mana.
On a read failure this returns 0, 0 and logs — there is no error return. A transient memory
read hiccup therefore looks exactly like having no potions.
The signature is (number, number), not (number, number, err). Do not build behaviour that
must distinguish the two cases on this signal alone; corroborate with a second read or accept
the false negative.
UsePotion
c:UsePotion() --> used, err
c:UsePotion({ health_percent = 40, mana_percent = 30 }) --> used, err
Clicks the potion HUD button. With no thresholds it drinks whenever a charge is held; with thresholds it drinks only when health or mana is below them.
Returns false with no error when no potion was needed or none were held. So used == false, err == nil is a normal outcome, not a problem:
local used, err = c:UsePotion()
if err then log_throttled('potion', tostring(err)) end
if used then state.potions_drunk = state.potions_drunk + 1 end
Pause a tick or two after drinking — the stat reads lag the HUD animation.
Input
c:SendKey(key, duration_ms) --> ok, err
c:SetCursor(x, y) --> ok, err
duration_ms is the hold time and defaults to 100. key is either a raw virtual-key
number or one of these names (case-insensitive):
| Group | Names |
|---|---|
| Letters, digits | a–z, 0–9 |
| Editing | enter/return, escape/esc, space, tab |
| Arrows | up, down, left, right |
| Modifiers | shift, ctrl/control, alt |
| Navigation | pageup/page_up/prior, pagedown/page_down/next, home, end |
A name outside that table maps to key code 0 and is sent anyway — the binding never
validates it. SendKey('f5') presses nothing at all (there are no function keys here), and
SendKey('shft') is a silent no-op. Check the spelling against the list; the return value
will not tell you.
Pass a raw virtual-key number for anything the table does not name.
pagedown marks a location and pageup recalls to it. Getting them the wrong way round makes
a farm loop re-mark wherever it happens to be standing instead of returning to its spot.
SetCursor(x, y) moves the cursor to a client-relative integer pixel position without
clicking.
ExecuteCommand does not work
c:ExecuteCommand(cmd) --> ok, err
Every host routes it into libwiz, which refuses:
libwiz: ExecuteCommand failed: remote code execution is disabled.
Executing game code is a confirmed ban vector, so the binding is kept only so the seam exists.
Nothing you pass will run. Drive the game through SendKey, ClickWindow, and the data
writes the rest of this API exposes.
Windows
c:ClickWindow(path) --> ok, err
c:IsWindowVisible(path) --> visible, err
c:IsControlGrayed(path) --> grayed, err
c:GetWindowText(path) --> text, err
c:WaitForWindow(path, timeout_ms) --> ok, err
path is an array of window names, or a name from the paths
global. See Windows and UI.
A path may also be given as a single string, which is split on / and >.
Several genuine game paths contain an unnamed window — paths.PetGameRewards is
{ 'WorldView', 'PetGameSplash', '', 'PetGameRewards' }. That '' matches a child whose name
really is empty; it is a step in the tree, not padding.
The string form drops empty segments while splitting, so
'WorldView/PetGameSplash//PetGameRewards' resolves to a three-step path that matches nothing.
Never join a paths.* value into a string, and write any path containing a blank as an array.
IsControlGrayed returns true when the window is missing (alongside an error), so a
readiness poll cannot mistake an absent window for a ready one. It reads the ControlGrayed
field the game uses to mark a control busy, not the window's disabled flag.
GetWindowText only returns sensible data for ControlText-derived widgets; other window
types return unusable bytes. It also ignores visibility — a popup that has been closed
still reports the text it last held, so a plugin that polls text alone will act on a stale
message. Gate it:
local visible = c:IsWindowVisible(paths.PopupMessage)
local text = visible and c:GetWindowText(paths.PopupMessage) or ''
WaitForWindow defaults to a 10-second timeout and blocks — keep it out of on_tick. A
non-positive timeout returns false, 'timeout must be positive' instead of waiting forever.
Blocking waits
c:WaitForCombat(timeout_ms) --> boolean
c:WaitForCombatEnd(timeout_ms) --> boolean
c:WaitForZone(zone, timeout_ms) --> boolean
c:WaitForZoneChange(timeout_ms) --> boolean
c:WaitForDialog(timeout_ms) --> boolean
c:WaitForLoading(timeout_ms) --> boolean
Every one of them takes timeout_ms as its last argument and defaults it to 60000 —
a full minute, which is a very long time to hold up a tick loop. Pass an explicit timeout.
They return a bare boolean: true if the condition was met, false if it timed out, if the
client could not be resolved, or if the underlying reads were failing the whole time. There is
no error channel, so a timeout and a broken client look identical.
WaitForZone() with no zone name behaves exactly like WaitForZoneChange() — it waits for
the zone to become something other than what it is now. Pass a zone name to wait for a
specific destination.
All of these block the calling coroutine. They belong in execute or a lifecycle hook, never
in on_tick. The tick-loop equivalent is a phase with a counter — see
Ticks and timing.
Quest shortcut
c:ActiveQuest() --> Quest|nil
Same as c.quest:GetActiveQuest(), and nil on any failure. See
Quest and Dialog.