Skip to main content

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 nil

The 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,
})
OptionTypePurpose
client_idstringWhich client. Also accepts client.
client_idsstring[]Several clients at once
strategystringStrategy name (file name: or filename)
ownerstringWho is driving. Use plugin.name.
replacebooleanTake 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.

Read the owner off the status table

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, EnsureCombat returns that status unchanged and never calls enable — so ready can be true while status.owner and status.strategy are someone else's. ready means "a runner is fighting", not "your request was applied". Compare status.strategy yourself, as the example below does.
  • The pre-check is single-client. It reads status for client_id only. Passing client_ids for 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:

FieldType
enabledboolean
ownerstring
strategystring
statusstring
client_idsstring[]
clientsarray of per-client status
updated_atnumber
generationnumber

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.

The default strategy is not the same on every host

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.

Never flatten a path into a string

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.

Names are stamped as table fields

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.