automation and paths
Two small globals. automation drives the libstrat combat runner; paths is a lookup table
of known game windows.
automation
automation:CombatStatus(opts) --> status|nil, err
automation:EnableCombat(opts) --> ok, status_or_err
automation:DisableCombat(opts) --> ok, status_or_err
automation:EnsureCombat(opts) --> ready, status, err
Called with : — these are methods on the global table. . works too: each one detects
whether it was handed the automation table as a first argument and skips it, so
automation.CombatStatus({...}) behaves identically. The : form is the documented one.
automation is supplied by the host and can be nilThe global only exists when the host wires a combat controller into the VM. Both shipped hosts do — the libwiz console and the desktop app's trainer — but a plugin meant to survive any host should guard:
if automation == nil then return true end
EnsureCombat
The one to use. Idempotent: reads current status, enables only if not already running.
local ready, status, err = automation:EnsureCombat({
client_id = c:GetID(),
strategy = 'storm_cp',
owner = plugin.name,
})
| Option | Type | Purpose |
|---|---|---|
client_id | string | Which client. Also accepts client. |
client_ids | string[] | Several clients at once |
strategy | string | Strategy name (file name: or filename) |
owner | string | Who is driving. Use plugin.name. |
replace | boolean | Take over automation owned by someone else |
Its three returns are ready (whether automation is now enabled), the full status table, and
an error. It is the only method here that returns three values; EnableCombat and
DisableCombat return two, with the status in the second slot.
The second return is the full status table, not an owner string:
if status and status.owner ~= plugin.name then
-- someone else is driving
end
Two behaviours to plan around:
- It will not take over. If automation is already enabled,
EnsureCombatreturns that status unchanged and never calls enable — soreadycan betruewhilestatus.ownerandstatus.strategyare someone else's.readymeans "a runner is fighting", not "your request was applied". Comparestatus.strategyyourself, as the example below does. - The pre-check is single-client. It reads status for
client_idonly. Passingclient_idsfor a multi-client enable works, but the "is it already on?" test that gates it looks at the single-client status rather than at each id in the list.
On failure you get false, nil, err. There is also a third shape: false with a full status
table and the error combat automation did not start, which means the enable call succeeded
but the runner came back disabled.
Cooperating rather than seizing
local function ensure_combat(c)
if automation == nil then return true end
local ready, status, err = automation:EnsureCombat({
client_id = c:GetID(),
strategy = COMBAT_STRATEGY,
owner = plugin.name,
})
if not ready then
log_throttled('combat_enable', 'combat automation failed: ' .. tostring(err))
return false
end
if status and status.strategy ~= COMBAT_STRATEGY then
utils.LogOnce('combat_strategy',
'fighting with ' .. tostring(status.strategy) ..
' held by ' .. tostring(status.owner) .. ', not ' .. COMBAT_STRATEGY)
end
return true
end
When another owner holds the runner with a different strategy this logs once and carries on
rather than seizing. Stomping on another owner mid-duel is worse than fighting with the wrong
strategy, and the operator can fix it with disable combat.
The automation == nil guard lets the plugin run under a host that provides no combat module.
CombatStatus
local status, err = automation:CombatStatus({ client_id = c:GetID() })
local status, err = automation:CombatStatus(c:GetID()) -- a bare id works too
local status, err = automation:CombatStatus() -- no id: the whole runner
Read-only, and the only method here that returns nil rather than false on failure. Fields:
| Field | Type |
|---|---|
enabled | boolean |
owner | string |
strategy | string |
status | string |
client_ids | string[] |
clients | array of per-client status |
updated_at | number |
generation | number |
Per-client entries carry client_id, status, active, in_combat, strategy, error,
last_engaged_at, updated_at. last_engaged_at is what you would build a "have we actually
fought recently" watchdog on.
EnableCombat and DisableCombat
automation:EnableCombat({ client_id = id, strategy = 'storm_cp', owner = plugin.name })
automation:EnableCombat('storm_cp', client_id) -- positional form
automation:DisableCombat({ owner = plugin.name })
automation:DisableCombat('my_plugin') -- a bare owner string works too
automation:DisableCombat() -- disables everything
Both return ok, status on success and false, err on failure, so the second value is a
table in the good case and a string in the bad one. Test ok before touching it.
EnableCombat accepts client as an alias for client_id, and client_ids for a list.
Always pass owner to DisableCombat. Without it you switch off automation the operator
configured by hand, which is rude and confusing. A host is free to refuse a disable from an
owner that does not hold the runner — the console does, and returns the unchanged status
rather than an error, so a "successful" disable that leaves status.enabled true means
somebody else still owns it.
Enabling without a strategy leaves the choice to the host, and the two shipped hosts choose
differently — the console falls back to aoe, the desktop app to priority. Name the
strategy explicitly if you care which one runs.
plugin.on_stop = function()
state.enabled = false
if automation ~= nil then
automation:DisableCombat({ owner = plugin.name })
end
end
paths
Named window paths mirroring libwiz's registry, so plugins never retype one.
paths.CombatPass --> WindowPath (a string array)
paths.Get('CombatPass') --> path|nil, err
paths.Names() --> string[], sorted
Get and Names work with either . or : — they detect and skip the paths table when it
arrives as the first argument.
All 176 registry entries are stamped onto the global table at VM setup, so paths.CombatPass
is a plain lookup with no call overhead. paths.Get is for names computed at runtime and
returns nil, "unknown window path: X" for anything unregistered. Names are matched exactly,
apart from surrounding whitespace — there is no case-insensitive fallback.
Each value is a fresh Lua array of strings. Nothing stops you mutating one, but you are editing the global copy every later lookup returns, so build a new table if you need a variant.
Some registry paths contain an empty segment for an unnamed window:
paths.PetGameRewards
--> { 'WorldView', 'PetGameSplash', '', 'PetGameRewards' }
That '' is a real step in the window tree. Passing the array works. Joining it with / and
passing the string does not: the string form drops empty segments while splitting, and the
shortened path matches nothing. Pass paths.X straight to ClickWindow, IsWindowVisible,
GetWindowText, and WaitForWindow.
local ok, err = c:ClickWindow(paths.CombatPass)
local name = 'Combat' .. action
local path, err = paths.Get(name)
if not path then
log('no such window: ' .. name)
return
end
c:ClickWindow(path)
Discover what exists with utils.Log(table.concat(paths.Names(), ', ')) — anywhere in a
plugin, or as a one-liner via the console's scripts eval:
scripts eval utils.Log(table.concat(paths.Names(), ', '))
The registry covers the backpack, bazaar, character sheet, chat, combat HUD, deck, dialogs,
dungeon recall, and more. If the window you need is missing, hardcode the path as a local in
your plugin — and if it looks generally useful, it is worth reporting, since a path that
earns a spot in the built-in registry becomes a paths.* constant for everyone in a future
update.
Registry entries become fields on the same table that holds Get and Names. A registry
entry literally named Get or Names would shadow the function. Nothing in the registry
does today, but avoid those two names if you add entries.