Modules for querying game data that work the same across flavors. Each
module selects the best available client API at compile time in split
flavor mode, and with a runtime check in shared mode. Availability comes
from the flavor table. When a client lacks an API and the module has no
fallback, using the affected function for that flavor is error E8300. When a
fallback with reduced functionality is used, warning W8301 lists the missing
fields.
All modules respect secret values (Types §2.11):
values that the client may return as secrets are typed T | Secret<T> and are
passed through unchanged. The modules never compare, index or do arithmetic on
them internally. Where a function needs such an operation (e.g. matching a
spell ID), it skips values it cannot access and says so in its contract.
secret
wowlua:secret holds the small helpers secret values need (decision S26).
Each tests for secrecy first, so it never compares, tests or does arithmetic
on a secret. On clients without secret values nothing is secret.
import * as secret from "wowlua:secret" local hp = UnitHealth("target") -- number | Secret<number>bar:SetValue(secret.amount(hp)) -- secrets pass, nil becomes 0local shown = secret.known(hp, 0) -- number: 0 when secretif secret.flag(UnitIsUnit("target", "focus")) then … end -- a secret boolean reads as falseif not secret.isSecret(hp) then print(hp + 1) end -- narrows like issecretvalue| API | Signature | Notes |
|---|---|---|
isSecret |
(v: unknown) -> v is Secret<unknown> |
issecretvalue, or false where the client has none |
known |
<T>(v: T | Secret<T> | nil, fallback: T) -> T |
fallback when v is secret or nil |
flag |
(b: boolean | Secret<boolean> | nil) -> boolean |
For conditions: secret and nil read as false |
amount |
(v: number | Secret<number> | nil) -> number | Secret<number> |
For widgets that take secrets: nil becomes 0 |
The generated wow-api declarations type the arguments Blizzard documents
with SecretArguments = "AllowedWhenTainted" (widget setters such as
StatusBar:SetValue, FontString:SetText) as T | Secret<T>, except those
marked NeverSecret.
unit
wowlua:unit provides unit information helpers.
import * as unit from "wowlua:unit"import * as color from "wowlua:color" local info = unit.info("target") -- UnitInfo?if info then label:SetText(color.wrap(info.name, unit.classColor("target")))end local hp, maxHp = unit.health("target") -- number | Secret<number> eachbar:SetMinMaxValues(0, maxHp)bar:SetValue(hp) -- widget setters accept secrets for token in unit.group({ includePlayer = true }) do refreshRow(token)end| API | Signature | Notes |
|---|---|---|
info |
(unit: UnitToken) -> UnitInfo? |
nil if the unit does not exist. Fields: name (without the realm; on WoW: Forever it includes the surname), realm? (other realms only), fullName? (nil when the name is secret), guid?, class?, classFile?, level, isPlayer, isConnected, faction?, race?, role?. Fields the client may return as secrets, the yes/no ones included, are typed T | Secret<T>. |
exists |
(unit: UnitToken) -> boolean |
|
fullName |
(unit: UnitToken) -> string? |
"Name-Realm", from GetUnitName(unit, true); the own realm is filled in (GetNormalizedRealmName). nil if the name is secret. |
isSame |
(a: UnitToken, b: UnitToken) -> boolean | Secret<boolean> |
UnitIsUnit; may be secret in combat (read it with secret.flag) |
health |
(unit: UnitToken) -> (number | Secret<number>, number | Secret<number>) |
Current and maximum |
healthPercent |
(unit: UnitToken) -> (number | Secret<number>)? |
0 to 100. The client's UnitHealthPercent (with CurveConstants.ScaleTo100) where it has one, which may return a secret. Otherwise computed from health only when both values are accessible, else nil. |
power |
(unit: UnitToken, powerType?: number) -> (number | Secret<number>, number | Secret<number>) |
|
classColor |
(unit: UnitToken) -> ColorMixin? |
From wowlua:color (honors CUSTOM_CLASS_COLORS); nil when the class is secret |
group |
(opts?: { includePlayer: boolean?, includePets: boolean? }) -> (() -> UnitToken?) |
Iterates raid1..raidN in a raid (the player's own raid token only with includePlayer), party1..party4 (plus player when requested) in a party, and only player when solo. Pets follow their owner with includePets. A secret answer to "in a raid/group" or "is it me" counts as no. |
groupSize |
() -> number |
GetNumGroupMembers(), with 1 when solo |
The client's newer APIs (UnitHealthPercent, GetUnitName) are detected at
run time in every flavor mode; the result is the same as a compile-time
choice.
Roster
unit.roster keeps the group's members (the player included) by GUID and
reports who joins and leaves.
const m = unit.roster.byGuid(sourceGuid) -- RosterMember?if m then print(m.fullName, m.unit) endunit.roster.onChange(function(joined: RosterMember[], left: RosterMember[]) for _, m in ipairs(joined) do print(`{m.name} joined`) endend)| API | Signature | Notes |
|---|---|---|
roster.byGuid |
(guid: string) -> RosterMember? |
|
roster.byUnit |
(unit: UnitToken) -> RosterMember? |
The member behind a group token |
roster.members |
() -> RosterMember[] |
The player first, then by group index |
roster.onChange |
(fn: (joined: RosterMember[], left: RosterMember[]) -> ()) -> Subscription |
After each change |
RosterMember |
{ guid: string, unit: UnitToken, name: string | Secret<string>, fullName?: string, classFile?: string | Secret<string> } |
fullName is nil while the name is secret |
Semantics (decision S34):
- Lazy. The roster starts on first use: it registers
GROUP_ROSTER_UPDATE,PLAYER_ENTERING_WORLDandUNIT_NAME_UPDATE, and the group at that moment counts as already there (no joins are reported for it). - Coalesced. A burst of roster events rebuilds once, 0.2 seconds after the first. A query while a rebuild is pending rebuilds first, so queries are always current.
- Moves are not changes. A member whose unit token changes (party to raid, a raid reshuffle) keeps their entry with the new token.
- Secrets. A member whose GUID reads as secret keeps the last known entry for that unit token; one never seen is left out until it can be read.
aura
wowlua:aura reads auras and wraps the engine's aura containers. On
clients with secret auras (retail 12.x, WoW: Forever) every addon read of
auras errors in combat ("cannot be accessed when secret while tainted"), in
the open world too. The reads here are guarded and find nothing then; the
engine's aura containers are the way to show auras in combat: the
engine fills them, the addon only styles their buttons (decision S26).
import * as aura from "wowlua:aura" if aura.readable("player") then for a in aura.each("player", "HELPFUL") do print(a.name, a.applications) endend local debuff = aura.find("target", 589, "HARMFUL|PLAYER") -- AuraData? local sub = aura.onChange("player", function(u: UnitToken, changes: UnitAuraUpdateInfo?) if changes == nil or changes.isFullUpdate then rescan() else applyDelta(changes) endend) -- Shown in combat too: the engine matches filters and spell lists itself.local box = aura.container(frame, { unit = "target", groups = { { key = "mine", filter = "HARMFUL|PLAYER", max = 8 }, { key = "curses", filter = "HARMFUL", dispelTypes = { "Curse" } }, { key = "tracked", filter = "HELPFUL", spells = { 1459, 21562 } }, }, size = 26, growth = { "LEFT", "DOWN" },})| API | Signature | Notes |
|---|---|---|
readable |
(unit: UnitToken) -> boolean |
Whether the unit's auras can be read right now (false in combat on clients with secret auras) |
each |
(unit: UnitToken, filter: AuraFilter) -> (() -> AuraData?) |
Every aura; nothing when they cannot be read. Auras the client hands back as a secret table end the iteration. |
forEach |
(unit: UnitToken, filter: AuraFilter, fn: (a: AuraData) -> boolean?) |
Allocation-free variant. Returning true stops iteration. |
find |
(unit: UnitToken, spellId: number, filter: AuraFilter) -> AuraData? |
Auras whose spellId is secret are skipped |
findByName |
(unit: UnitToken, name: string, filter: AuraFilter) -> AuraData? |
Same skipping rule |
byInstanceId |
(unit: UnitToken, id: number) -> AuraData? |
nil when the client refuses |
onChange |
(unit: UnitToken, fn: (unit: UnitToken, changes: UnitAuraUpdateInfo?) -> ()) -> Subscription |
UNIT_AURA filtered to unit (a unit-filtered event frame, Core: events). changes is the client's update info; nil or isFullUpdate means rescan. |
container |
(parent: Frame, opts: ContainerOptions) -> AuraContainer |
The engine's container (CreateFrame("AuraContainer", …, "CustomAuraContainerTemplate"), loading Blizzard_AuraContainer). E8300 on flavors without aura containers. |
ContainerOptions |
{ unit, groups: AuraGroup[], size?, spacing?, anchor?, growth?, lineWidth? } |
size 30 and spacing 2 by default; anchor the corner icons start from ("TOPLEFT"), growth { "RIGHT", "DOWN" } by default; lineWidth before icons wrap |
AuraGroup |
{ key, filter, max?, spells?, dispelTypes?, size?, initialize? } |
max 16 by default; spells/dispelTypes become the engine's candidate filters (matched in combat too; spell IDs only where the client permits matching); initialize(button: AuraButton) builds each button once |
AuraContainer |
frame, setUnit(unit), refresh(), setGroupMax(key, n), setGroupSpells(key, ids?), setFlow(anchor, growth?), setLineWidth(width?), release() |
Containers rescan when their token points at someone new (PLAYER_TARGET_CHANGED, PLAYER_FOCUS_CHANGED, boss and group changes). Engine containers cannot be freed: release parks one (no unit, hidden) until the next setUnit. |
AuraButton |
Button & AuraButtonApi |
The engine's button: SetIcon, SetApplicationCount, SetDurationText, SetDurationBar, SetDurationCooldown, AddDispelTypeTexture, SetSpellName, SetCasterName, SetCancelAuraButtons, SetHideTooltipInCombat, SetTooltipAnchorPoint. Register regions only in initialize; the engine then drives them. |
AuraDatais the type the flavor'swow-apideclarations giveC_UnitAurasaura data. Every flavor hasC_UnitAuras, so no legacyUnitAurafallback is needed. On clients with secret auras its fields may be secret; the module never compares them.AuraFilteris a string of|-separated words. Each flavor's words come from its client'sAuraUtil.AuraFilters, extracted into theAuraFilterWordtype of itswow-apideclarations (decision S27;!negation where the flavor has it). Literal filters are checked for every targeted flavor (E8310); container groups' filters too.- The default button (no
initialize) looks like the client's buffs (decisions S17, S28): the icon, the stack count at the bottom right, the engine's duration text below the icon, a debuff-type border on harmful auras, and right click cancels the player's own buffs. - Engine calls are guarded: an error inside one would abort the engine's
whole button batch. Layout methods renamed between client builds
(
SetFlowLayout…/SetAuraLayout…) are tried in turn. - Iteration stops at the first missing index, matching the client APIs.
spell
wowlua:spell provides spell information across C_Spell and the legacy
global functions.
import * as spell from "wowlua:spell" local s = spell.info(133) -- SpellInfo?if s then icon:SetTexture(s.iconID) endlocal cd = spell.cooldown(133) -- SpellCooldownInfo?| API | Signature | Notes |
|---|---|---|
info |
(spell: number | string) -> SpellInfo? |
name, iconID, originalIconID, castTime, minRange, maxRange, spellID, from C_Spell.GetSpellInfo (every flavor's client has it) |
name, icon |
(spell) -> string?, (spell) -> number? |
|
link |
(spell) -> string? |
|
description |
(spell: number) -> string? |
May be empty until the spell data is loaded. Use wowlua:item's loadSpell first. |
cooldown |
(spell) -> CooldownInfo? |
startTime, duration, modRate (each number | Secret<number>: secret in combat on restricted clients), isEnabled, isActive (readable there), isOnGCD?. Falls back to the global GetSpellCooldown where C_Spell.GetSpellCooldown is missing. |
onCooldown |
(spell) -> boolean |
Whether it is cooling down: isActive, readable in combat too |
cooldownDuration |
(spell, ignoreGCD?: boolean) -> LuaDurationObject? |
For secret-safe display with Cooldown:SetCooldownFromDurationObject; nil where the client has no duration objects |
charges |
(spell) -> SpellChargeInfo? |
|
isKnown |
(spellId: number, isPet?: boolean) -> boolean |
C_SpellBook.IsSpellKnown, in the player's or the pet's spellbook |
item
wowlua:item loads and caches item and spell data asynchronously.
WoW loads item and spell data from the server lazily. The first query for an
uncached item returns nil, and the data arrives later.
import * as item from "wowlua:item" item.load(19019, function(data: ItemData?) if data then print(data.link, data.itemLevel) endend) local cached = item.get(19019) -- ItemData? (nil and starts loading if uncached) item.loadMany({ 19019, 17182 }, function(all: { [number]: ItemData? }) … end) item.loadSpell(133, function(ok: boolean) print(spell.description(133)) end) -- inside a wowlua:jobs joblocal info = item.await(ctx, 19019) -- yields until loaded| API | Signature | Notes |
|---|---|---|
get |
(item: number | string) -> ItemData? |
Returns immediately. On a cache miss, it starts loading and returns nil. A string is an item link, or a name the client has cached. |
load |
(item: number | string, fn: (data: ItemData?) -> (), opts?: LoadOptions) -> LoadHandle |
fn runs once the data is available. It receives nil if the item does not exist or loading timed out. If the item is already cached, fn runs synchronously before load returns. |
loadMany |
(items: (number | string)[], fn: (all: { [number]: ItemData? }) -> (), opts?) -> LoadHandle |
One callback after all items have resolved |
configure |
(opts: { cacheSize: number? }) |
The cache's size (default 500) |
loadSpell |
(spellId: number, fn: (ok: boolean) -> (), opts?) -> LoadHandle |
Ensures spell data (name, description) is loaded |
await |
(ctx: JobContext, item: number | string, opts?: LoadOptions) -> ItemData? |
Yields the job until loaded (Infrastructure: jobs) |
LoadOptions |
{ timeout: number? } |
Default timeout: 10 seconds |
LoadHandle |
Cancel()-style handle: cancel(), isDone(): boolean |
A cancelled load never calls its callback |
ItemData |
record | id, name, link, quality, itemLevel, minLevel, type, subType, stackCount, equipLoc, icon, sellPrice, classID, subclassID, bindType, expansionID. (The API's own ItemInfo is the item reference, number | string; the record has its own name so the two do not shadow each other.) |
Implementation: C_Item.RequestLoadItemDataByID and C_Spell.RequestLoadSpellData,
answered by ITEM_DATA_LOAD_RESULT (or GET_ITEM_INFO_RECEIVED) and
SPELL_DATA_LOAD_RESULT; every flavor's client has them, so no item or
spell mixins are needed. Concurrent loads of the same item (and gets
while it loads) share one request. Loaded ItemData records are cached per
addon. The cache holds at most item.configure({ cacheSize = 500 })
entries, with least-recently-used eviction. Callbacks run through the stdlib
error policy (Infrastructure: errors).
tooltip
wowlua:tooltip provides typed post-hooks for tooltips. On clients with the
tooltip data system it uses TooltipDataProcessor.AddTooltipPostCall, and
elsewhere it hooks the OnTooltipSet* scripts of the standard tooltips.
import onTooltip from "wowlua:tooltip"import * as tooltip from "wowlua:tooltip" @onTooltip("Item")local function addItemLevel(tip: GameTooltip, data: TooltipData) local info = item.get(data.id :: number) if info then tip:AddLine(`iLvl {info.itemLevel}`, 1, 1, 1) endend local h = tooltip.hook("Unit", function(tip: GameTooltip, data: TooltipData) … end)h:disable() -- hooks cannot be removed; this stops calling the handlertooltip.attach(MyAddonTooltip) -- also apply hooks to a custom tooltip (fallback clients)| API | Signature | Notes |
|---|---|---|
@onTooltip(kind) |
function decorator | Registers at load time. Registering decorator: captures the final decorated function (L26). |
hook |
(kind: TooltipKind, fn: (tip: GameTooltip, data: TooltipData) -> ()) -> TooltipHook |
|
TooltipHook |
enable(), disable(), isEnabled() |
|
attach |
(tip: GameTooltip) |
Fallback clients only: applies all hooks to an additional tooltip frame. A no-op on clients with the tooltip data system, which covers all tooltips. |
TooltipKind |
string: an Enum.TooltipDataType name ("Item", "Spell", "Unit", "Mount", …) |
Checked per targeted flavor from its API declarations (decision S22): a name in no flavor's Enum.TooltipDataType is E8330; a kind a targeted flavor lacks is E8300 for it. A flavor has the tooltip data system when its declarations include C_TooltipInfo; fallback flavors have only "Item", "Spell" and "Unit". A hook under @if is checked only for the flavors that keep it. At run time, hook errors for a kind the client lacks. |
TooltipData |
record | Normalized: kind, id?, guid? (string | Secret<string>), unit?, link?, raw? (the client's tooltip data where available). |
- Fallback clients hook
GameTooltip,ItemRefTooltipand the shopping tooltips withHookScript, reading the item, spell or unit with the tooltip's getter methods. - Handlers run through the stdlib error policy. A failing handler never breaks the tooltip or other handlers.
- With the data system, forbidden tooltips (
tip:IsForbidden()) are skipped.TooltipHookis a class; hooks run in registration order. - These are post-hooks only. They cannot suppress or replace tooltip content.
Testing
wowlua:mock support used by this chapter: mock.setUnit, mock.setGroup,
mock.secret(v) / mock.setRestricted(bool) (secret simulation for
issecretvalue / canaccessvalue), mock.setAuras(unit, auras),
mock.fireUnitAura(unit, changes?), mock.setSpell(id, info),
mock.setSpellCooldown(id, info), mock.setItem(id, info, { cached?, delay? })
(data arrives after mock.advanceTime), and mock.showTooltip(kind, data),
which runs post-hooks against a stub GameTooltip whose added lines can be read
back with tip:lines().