Files
ClassicAPI/docs/API.md
T

1.1 MiB
Raw Permalink Blame History

ClassicAPI — Lua Reference

Per-function reference for the calls ClassicAPI adds to the WoW 1.12.1 Lua environment. See the project README for installation and build instructions.

Contents

Account

SaveAccount(name, password) / DeleteAccount(name) / GetSavedAccounts() / LoginWithSavedAccount(name) — GlueXML only

Persists account credentials in Windows Credential Manager (per-user, DPAPI-encrypted) and dispatches login from C so the plaintext password never crosses the C↔Lua boundary on the way out. Designed for GlueXML rewrites of AccountLogin.lua that want a selection list of remembered accounts without persisting plaintext passwords to SavedVariables.

Only callable from the login screen (the "glue" Lua state). Calling them from an in-world addon errors with "attempt to call nil" — they aren't registered there.

  • SaveAccount(name, password) — encrypt and save under the current realmlist. Returns true on success, false if name or password is empty, no realmlist is set, or the OS rejects the write.

  • DeleteAccount(name) — remove the saved entry. Returns true if a matching entry existed and was deleted.

  • GetSavedAccounts() — returns a numeric-keyed table of entries for the current realmlist. Other realmlists' entries are invisible. Empty table if no realmlist is set. Each entry is a table:

    Field Type Notes
    name string Account name as saved.
    lastUsed number Unix epoch seconds of the last write to the vault entry. Refreshed automatically by LoginWithSavedAccount, so this functions as a "last used" timestamp. 0 if Windows didn't supply a timestamp (extremely rare, e.g. credentials manually injected).
    for i, entry in ipairs(GetSavedAccounts()) do
        print(entry.name, date("%Y-%m-%d %H:%M", entry.lastUsed))
    end
    
  • LoginWithSavedAccount(name) — decrypt internally and feed the credentials to the engine's login function. Returns true if credentials were found and dispatched, false if no such entry exists. Plaintext is never returned to Lua. Equivalent to what DefaultServerLogin(name, password) does, but with the password fetched from the vault rather than passed in. Also re-saves the credential (same value) to refresh the lastUsed timestamp.

Scoping per realmlist

Every operation is scoped by the current realmList CVar (the address from realmlist.wtf, e.g. "logon.turtle-wow.org"), not the friendly display name. The same account name on two different private servers therefore gets two distinct vault entries:

ClassicAPI/WoW/logon.turtle-wow.org/MYACCT
ClassicAPI/WoW/logon.other-server.com/MYACCT

Switching realmlists changes the visible account list automatically; no separate "select realm" step. If you need a refresh while still on the login screen, call Autologin_Load() (or GetSavedAccounts() directly) after the realmlist changes.

Storage

Entries live in the OS Credential Manager under the ClassicAPI/WoW/<realmlist>/ namespace. Users can inspect and wipe them via Windows' control /name Microsoft.CredentialManager → Generic Credentials, the same way they'd manage any other Windows- stored credential.

Security caveats

Realistic about what this defends against:

  • Defeats: casual file inspection of WTF, sharing the WTF folder, cloud-syncing it, another Windows user on the same machine, malware running as a different Windows user.
  • Does not defeat: any process running as the current Windows user can decrypt these via DPAPI — it's per-user, not per-process. No "decrypt and return plaintext" path is exposed to Lua, so a hostile addon can trigger a login with a saved account but can't extract the password. Plaintext is also unavoidably in process memory while the engine sends SRP.

Addon example

-- GlueXML, e.g. AccountLogin.lua: save credentials on first manual
-- login, one-click login on subsequent shows.

local function OnLoginClicked()
    local name = AccountLoginAccountEdit:GetText()
    local password = AccountLoginPasswordEdit:GetText()
    if not name or name == "" then return end

    if password and password ~= "" then
        -- Fresh login or password change: persist + log in.
        SaveAccount(name, password)
        LoginWithSavedAccount(name)
    else
        -- Reuse stored credentials.
        LoginWithSavedAccount(name)
    end
end

-- Populate a selection list from the vault, sorted by recency.
local accounts = GetSavedAccounts()
table.sort(accounts, function(a, b) return a.lastUsed > b.lastUsed end)
for i = 1, table.getn(accounts) do
    local e = accounts[i]
    print(i, e.name, "last used:", date("%m/%d/%Y", e.lastUsed))
end

-- Forget an account.
DeleteAccount("OLDACCT")

Action

GetActionInfo(slot)

Returns the action descriptor for a 1-based action-bar slot, in the shape actionType, id, subType. Returns nothing when the slot is empty.

local actionType, id, subType = GetActionInfo(1)
Slot contents Returns
empty nothing
spell "spell", spellID, "spell"
macro "macro", macroSlot
item "item", itemID
equipment set "equipmentset", setName

A button whose macro no longer exists also returns nothing.

The third return is always "spell" for a spell. Pet-bar actions are not in this table. Use PetHasActionBar and the pet-action functions for pet slots.

IsItemAction(slot)

Returns 1 if the 1-based action-bar slot holds an item. Otherwise it returns nil. A slot that is not in the range 1 to 120 also returns nil.

if IsItemAction(1) then
    print("Slot 1 holds an item.")
end

HasBonusActionBar()

Returns 1 while a bonus action bar is active. Otherwise it returns nil. A stance, a form or stealth can give a bonus bar. GetBonusBarOffset() gives the number of the bar.

if HasBonusActionBar() then
    print("Bonus bar", GetBonusBarOffset())
end

C_ActionBar.ShouldShowKeyring()

Returns true. The client always has a keyring. When this value is true, bag code includes the keyring (KEYRING_CONTAINER).

if C_ActionBar.ShouldShowKeyring() then
    ItemUtil.IterateBagSlots(KEYRING_CONTAINER, callback)
end

C_ActionBar.EnableActionRangeCheck(slot, enable)

Starts or stops the range check of a 1-based action-bar slot for ACTION_RANGE_CHECK_UPDATE. A slot that is not in the range 1 to 120 is ignored.

The client counts the calls for each slot. Each call with enable set to true adds one. Each call with enable set to false removes one. The client checks the slot while its count is more than zero. Thus, if two buttons show the same slot, each button can enable and disable the slot for itself.

After each call with enable set to true, the next check of the slot fires the event, also if the state did not change. This gives a new button the current state of its slot. You do not have to call IsActionInRange when you show a button.

A /reload sets the count of all slots to zero.

local f = CreateFrame("Frame")
f:RegisterEvent("ACTION_RANGE_CHECK_UPDATE")
f:SetScript("OnEvent", function()
    print("Slot", arg1, arg3 and (arg2 and "in range" or "out of range") or "no range check")
end)
C_ActionBar.EnableActionRangeCheck(1, true)

AddOns

C_AddOns.* getters that splat the GetAddOnInfo(arg) 7-tuple (name, title, notes, enabled, loadable, reason, security) into single-field accessors, plus a getter for the addon's optional dependencies. Most bypass GetAddOnInfo entirely and call the engine's per-field helpers directly.

All accept either a 1-based index (1..GetNumAddOns()) or an addon directory name string.

C_AddOns.GetAddOnName(indexOrName)

Returns the addon's directory name as the engine sees it (the folder name on disk). For numeric input, returns the engine's canonical casing; for string input, echoes the input verbatim once existence is confirmed. Returns nil for missing addons.

C_AddOns.GetAddOnName(1)            -- "Atlas-TW"
C_AddOns.GetAddOnName("myaddon")    -- "myaddon"
C_AddOns.GetAddOnName("garbage")    -- nil

C_AddOns.GetAddOnTitle(indexOrName)

Returns the ## Title: from the addon's .toc file, with WoW color-code escapes applied. nil for missing addons or addons without a title field.

C_AddOns.GetAddOnTitle("MyAddon") -- "My Addon"

C_AddOns.GetAddOnNotes(indexOrName)

Returns the ## Notes: from the .toc. nil for missing addons or addons without notes.

C_AddOns.GetAddOnNotes("MyAddon") -- "Shows a thing on the screen."

C_AddOns.IsAddOnLoadable(indexOrName)

Returns loadable, reason — a real boolean and a status string (or nil).

reason comes from a small status table the engine consults when populating GetAddOnInfo's 6th return: "DISABLED", "BANNED", "CORRUPT", "INSECURE", "NOT_DEMAND_LOADED", "INTERFACE_VERSION", "MISSING". nil when the addon is loadable. The full signature accepts optional character and demandLoaded arguments — those are ignored here, since there is no per-character addon enable state.

C_AddOns.IsAddOnLoadable("MyAddon")           -- true, nil
C_AddOns.IsAddOnLoadable("HardcoreTooltips")  -- false, "DISABLED"
C_AddOns.IsAddOnLoadable("garbage")           -- false, nil

C_AddOns.IsAddOnLoaded(indexOrName)

Returns loadedOrLoading, loaded — two booleans.

C_AddOns.IsAddOnLoaded("MyAddon")       -- true, true
C_AddOns.IsAddOnLoaded("garbage")       -- false, false
C_AddOns.IsAddOnLoaded(1)               -- true, true   (first addon by index)

C_AddOns.LoadAddOn(indexOrName)

Loads a LoadOnDemand addon. Returns loaded, value:

  • loaded — 1 when the addon loaded, or was already loaded. nil otherwise.
  • value — a locale-independent reason the load failed, such as "DISABLED", or "UNKNOWN_ERROR" when no specific reason is available. nil on success.
local loaded, reason = C_AddOns.LoadAddOn("Blizzard_AuctionUI")
if not loaded then
    print("could not load:", reason)
end

Test loaded for truth rather than comparing it to true — it is the number 1.

The two returns distinguish "load-in-progress" from "fully loaded", and they really do differ while an addon is loading. An addon's own files run part-way through its load, so code at file scope sees itself as loading but not yet loaded:

-- at file scope in MyAddon.lua
C_AddOns.IsAddOnLoaded("MyAddon")       -- true, false
-- and once the load has finished
C_AddOns.IsAddOnLoaded("MyAddon")       -- true, true

The same holds for anything else running inside that window, such as a dependency loaded on the way in: it sees the addon that pulled it in as true, false.

Unknown addons (numeric index past GetNumAddOns(), or string name not in the registry) return false, false.

An addon asking about itself from its own ADDON_LOADED handler gets true, false, because that event fires just before its load call returns. Asking about any other addon there is accurate.

C_AddOns.GetAddOnSecurity(indexOrName)

Returns an Enum.AddOnSecurityStatus integer (not a string):

Value Enum.AddOnSecurityStatus.* When
0 Secure Blizzard-signed addons (Blizzard_*).
1 Insecure Every user addon loaded from Interface/AddOns/. The default for any registered addon not in the secure or banned override sets.
2 Banned Entries the engine has explicitly disqualified — set by server addon-banlist responses, rare in practice.
3 NotAvailable No addon by that name / index exists.
C_AddOns.GetAddOnSecurity("Blizzard_TalentUI") -- 0 (Secure)
C_AddOns.GetAddOnSecurity("MyAddon")           -- 1 (Insecure)
C_AddOns.GetAddOnSecurity("Nonexistent")       -- 3 (NotAvailable)

if status == Enum.AddOnSecurityStatus.Secure then ...

C_AddOns.DoesAddOnExist(indexOrName)

Returns true iff the engine's addon registry has a matching entry, false otherwise. Cheap existence probe used by addons doing soft-dependency checks.

The implementation goes through the registry directly rather than dispatching to GetAddOnInfo — the engine echoes its input name back as ret1 unconditionally (before the lookup), so a GetAddOnInfo("garbage") ~= nil heuristic returns true for any string. This wrapper avoids that.

C_AddOns.DoesAddOnExist("MyAddon")     -- true
C_AddOns.DoesAddOnExist("garbage")     -- false

C_AddOns.GetAddOnOptionalDependencies(indexOrName)

Returns the addon's ## OptionalDeps: names as multiple return values, in declared order — the counterpart to the stock GetAddOnDependencies (required deps). An addon with no ## OptionalDeps: field returns nothing.

Arg and error handling match stock GetAddOnDependencies: a numeric index out of range (or a non-string/non-number argument) raises the usage error; an unknown name string returns nothing without erroring.

-- ## OptionalDeps: Atlas, pfQuest
C_AddOns.GetAddOnOptionalDependencies("MyAddon")  -- "Atlas", "pfQuest"
C_AddOns.GetAddOnOptionalDependencies("Atlas")    -- (nothing — no OptionalDeps)

C_AddOns.GetAddOnInterfaceVersion(indexOrName)

Returns the ## Interface: value of the addon TOC as a number.

If the TOC lists more than one value, separated by commas, the function returns the largest value that is not more than the client interface version. Errors for bad arguments are the same as for GetAddOnOptionalDependencies. An unknown name returns nil.

-- ## Interface: 11200
C_AddOns.GetAddOnInterfaceVersion("MyAddon")  -- 11200
-- ## Interface: 120100, 50504, 11200
C_AddOns.GetAddOnInterfaceVersion("Multi")    -- 11200
-- ## Interface: 20504, 11100, 11000
C_AddOns.GetAddOnInterfaceVersion("Old")      -- 11100

C_AddOns.GetAddOnLocalTable(name)

Returns the private table of the addon named name. This is the same table the addon's own files receive as the second value of ...:

-- inside MyAddon's files
local addonName, addonTable = ...

Another addon reads that table only when both conditions are true:

  1. MyAddon is loaded, and
  2. MyAddon's .toc file declares ## AllowAddOnTableAccess: 1.

If either condition is false, the function returns nil. An addon that does not opt in keeps its table private. The check protects each addon's table from access it did not permit.

-- MyAddon.toc contains: ## AllowAddOnTableAccess: 1
local t = C_AddOns.GetAddOnLocalTable("MyAddon")   -- MyAddon's table
t.sharedValue = 5                                  -- MyAddon sees this too

C_AddOns.GetAddOnLocalTable("OtherAddon")   -- nil (no opt-in directive)
C_AddOns.GetAddOnLocalTable("DoesNotExist") -- nil (not loaded)

Pass the addon name as a string. A numeric index returns nil — one addon refers to another by name, not by load order.

Conditional and multi-flavor TOC loading

Some addons support several game versions from one folder. ClassicAPI reads the TOC conventions those addons use, so they load here.

Flavor TOC files. An addon that supports several clients can name its TOC after the client it targets. The plain <Name>.toc is then optional. ClassicAPI reads two of these names:

  • <Name>_Turtle.toc — on a Turtle client only.
  • <Name>_ClassicAPI.toc — on any client.

ClassicAPI looks for _Turtle first, so it wins when both exist. Either one also wins over a plain <Name>.toc. An addon can therefore keep its plain TOC for other clients and put its ClassicAPI build in a suffixed file.

ClassicAPI does not read _Vanilla or _Classic. Those names belong to the Classic Era client, which is a separate program with its own engine and its own API. An addon's _Vanilla files are written for that client. Such a file can start the addon and then fail inside it, on a function or a template this client does not provide. So an addon that ships only _Vanilla and _Mainline TOCs does not load at all. Add a _ClassicAPI.toc to support this client.

Note the word vanilla. The two TOC conventions reuse it for different things. As a file suffix, _Vanilla names the Classic Era client, so ClassicAPI ignores it. As a directive value, vanilla names this client's game type, so [AllowLoadGameType vanilla] loads the line.

Flavor Bindings files. The same selection applies to an addon's keybinding file:

  • Bindings_Turtle.xml — on a Turtle client only.
  • Bindings_ClassicAPI.xml — on any client.

The order matches the TOC files: _Turtle first, and either one wins over a plain Bindings.xml. An addon that ships no plain Bindings.xml still loads its bindings from a flavor file. ClassicAPI does not read _Vanilla or _Classic here either.

Multi-flavor ## Interface: version. A TOC can list several interface versions on one line:

## Interface: 120100, 50504, 38002, 20506, 11200

The addon loads when the list contains this client's interface version, 11200. The position of that entry in the list does not matter. A list without it stays out of date, and the addon does not load.

Per-line directives. Inside a TOC, gate individual file lines with a condition, or expand a path variable:

# Loads on this client (game type is vanilla):
Vanilla.lua              [AllowLoadGameType vanilla]

# Dropped on this client:
Mainline.lua             [AllowLoadGameType mainline]

# Loads only under a matching client locale:
Localization\deDE.lua    [AllowLoadTextLocale deDE]

# Path variables expand before the file loads:
[Family]\Init.lua                # -> Classic\Init.lua
[Game]\Init.lua                  # -> Vanilla\Init.lua
Localization\[TextLocale].lua    # -> Localization\enUS.lua  (your locale)

How each token resolves on this client:

Directive Result
[AllowLoadGameType ...] Loads the line when the list contains vanilla.
[AllowLoadTextLocale ...] Loads the line when the list contains your client locale (the GetLocale() code).
[AllowLoad ...] Loads on game, drops on glue.
[Family] Classic
[Game] Vanilla
[TextLocale] Your client locale code, for example enUS.

A line loads only when every condition on it passes. An unknown condition drops the line. A file gated by a rule this client does not know stays unloaded.

Limits.

  • Conditions on ## metadata lines are not supported. Only file-reference lines are gated.
  • [AllowLoad glue] never loads. Addon files load in-game only.

SavedVariables loaded first

Normally an addon's files run first, then its SavedVariables load, then ADDON_LOADED fires. So file-scope code sees its SavedVariables as nil. An addon avoids this with one TOC line:

## SavedVariables: MyAddonDB
## LoadSavedVariablesFirst: 1

ClassicAPI honors ## LoadSavedVariablesFirst. For a flagged addon, its SavedVariables load before its files run, so file-scope code sees them:

-- With the directive, in MyAddon's file:
local db = MyAddonDB       -- the restored table, not nil

This covers both ## SavedVariables and ## SavedVariablesPerCharacter. File-scope reads and writes both work. A SavedVariables file exists only after the first save, so the first-ever login still sees nil — there is nothing on disk to load yet.

/reload picks up new addons and new files

A stock client fixes its view of the game folder at launch. A file you add while the game runs does not load until you restart the client. Only edits to files that already existed at launch take effect on /reload.

ClassicAPI removes the restart requirement. On every /reload:

  • A new folder under Interface\AddOns\ loads as a normal addon. It appears in GetNumAddOns() / GetAddOnInfo(), fires ADDON_LOADED, and its dependencies, SavedVariables, keybindings, and flavor TOC all work as usual.
  • New files added to an existing addon load (add the file and its TOC line, then /reload).
  • A first-time SavedVariables file survives /reload. On a stock client, the first save of a newly installed addon is written to disk but cannot be read back until a restart, so the settings appear lost. That quirk is fixed.
  • ## metadata edits take effect. Change any ## line — for example the ## SavedVariables: or ## Dependencies: list, the ## Title:, or the ## Interface: version — and /reload applies it. GetAddOnMetadata returns the new values.
  • Deleting an addon folder removes it from the addon list on the next /reload.

<Script file> paths in XML files

A <Script file> path that has a folder in it, such as <Script file="widgets\Button.lua"/>, loads relative to the folder of its XML file, the same as <Include file>. Libraries such as AceGUI-3.0 load their files this way.

A path that exists as given, such as <Script file="Interface\AddOns\MyAddon\Button.lua"/>, still loads from there. The XML file's folder is tried only when the path does not exist as given, and only when the file is in that folder. If the file is in neither place, FrameXML.log shows "Error loading" with the path.

APIDocumentation

ClassicAPI describes its own surface. Each function it adds carries a typed signature: the name and type of every argument and return value, which of them can be nil, and one sentence that says what the function does. Events, enumerations, and the table shapes functions return are described the same way.

You read this in the game with /classicapi, or from code through C_APIDocumentation.

/classicapi

Browse the API from the chat box. /capi is the short form.

/classicapi                        usage
/classicapi stats                  how much is documented
/classicapi system list            every system
/classicapi spell                  one system
/classicapi spell list             every function, event and table in a system
/classicapi search cooldown        search everything
/classicapi spell search cooldown  search one system

search also answers to s. Every search takes a Lua pattern.

Results are clickable. Click a name to print its full signature, with each argument and return value on its own line. Right-click copies a ready-to-paste call to the clipboard. Shift-click puts a /dump of the function in the chat box, with the cursor between the parentheses.

Functions are grouped into systems. A system is a namespace such as C_Spell, a group of plain globals such as SpellGlobals, or a widget method set such as SimpleTextureAPI. Find a system by its name or its namespace: /classicapi spell and /classicapi c_spell both work, and case does not matter.

The browser holds what ClassicAPI adds. The functions the client already ships are not in it.

C_APIDocumentation.GetSystems()

Returns an array of system names, sorted.

local systems = C_APIDocumentation.GetSystems()
-- { "Item", "ItemGlobals", "Spell", "SpellBook", "SpellGlobals", ... }

C_APIDocumentation.GetSystem(system)

Returns one system's documentation table, or nil if no system has that name. Match on the name or the namespace; case does not matter. Each call builds a new table, so the caller may keep it and change it.

local spell = C_APIDocumentation.GetSystem("C_Spell")
spell.Name        -- "Spell"
spell.Namespace   -- "C_Spell"
spell.Functions[1].Name
spell.Functions[1].Returns[1].Type
Field Meaning
Name The system's name.
Type "System" for a namespace or a group of globals, "ScriptObject" for a widget method set.
Namespace The table the functions live in, such as C_Spell. Absent for globals and widget methods.
Environment "All", "Game", or "Glue" — where the functions exist.
Functions Array of functions. Each has Name, Type, and where documented Documentation, Arguments, and Returns.
Events Array of events. Each has Name, LiteralName, and where documented Documentation and Payload.
Tables Array of table shapes. A Structure has Fields; an Enumeration also has NumValues, MinValue, and MaxValue.

An argument, return value, payload value, or field is described by Name, Type, and Nilable, plus Default and Documentation when they apply. Type is a Lua type such as number or string, or the name of a table shape in some system's Tables.

The layout matches the one Blizzard's own documentation uses, so a tool written against that data reads this without change.

_classicapi_UndocumentedAPI()

Returns an array of the registered names that carry no description yet, and its length. Names are being described one namespace at a time, so this list shrinks with each release.

local missing, count = _classicapi_UndocumentedAPI()

Animation

Animation groups on frames, textures and font strings, made in Lua or XML. Groups and animations are real UI objects: GetObjectType, IsObjectType, GetName, GetParent and named globals work as they do for frames.

A group updates once per frame, after its frame's OnUpdate. A group on a hidden frame does not advance until the frame shows again.

region:CreateAnimationGroup([name [, templateName]])

Creates an AnimationGroup on a frame, texture or font string and returns it. name can start with $parent. templateName names a virtual <AnimationGroup> to inherit. region:GetAnimationGroups() returns the region's groups. region:StopAnimating() stops every playing group of the region and, for a frame, of every frame, texture and font string inside it.

local group = frame:CreateAnimationGroup()
local fade = group:CreateAnimation("Alpha")
fade:SetChange(-1)            -- down to alpha 0
fade:SetDuration(0.5)
fade:SetSmoothing("OUT")
group:SetScript("OnFinished", function(self) frame:Hide() end)
group:Play()

AnimationGroup

Play([reverse]), Pause, Stop, Finish, IsPlaying, IsPaused, IsDone, IsPendingFinish, GetProgress, GetDuration, SetLooping("NONE" | "REPEAT" | "BOUNCE"), GetLooping, GetLoopState, GetMaxOrder, SetInitialOffset(x, y) / GetInitialOffset, SetIgnoreFramerateThrottle / GetIgnoreFramerateThrottle, SetToFinalAlpha(setToFinalAlpha) / IsSetToFinalAlpha, CreateAnimation([type [, name [, templateName]]]), GetAnimations, and SetScript / GetScript / HookScript / HasScript for OnLoad, OnPlay, OnPause, OnStop(self, requested), OnFinished(self, requested), OnLoop(self, loopState) and OnUpdate(self, elapsed).

When a group ends, its progress goes back to 0. After the end, IsDone returns false. To find out when a group ends, use OnFinished.

After SetToFinalAlpha(true), each Alpha animation of the group keeps the alpha that it ends on when the group finishes. This includes a group that ends after Finish. Stop puts the stored alpha back.

Animations with the same order play together, and the orders play one after another. An order lasts as long as its longest startDelay + duration + endDelay. BOUNCE plays every other loop backwards.

Animation

group:CreateAnimation(type) takes "Animation" (timing and scripts only), "Alpha", "Translation", "Scale", "Rotation" or "Path". Any other type makes an "Animation". An animation made in Lua joins the highest order the group already has.

Every animation has Play, Pause, Stop, IsPlaying, IsPaused, IsStopped, IsDelaying, IsDone, GetElapsed, GetProgress, GetProgressWithDelay, GetSmoothProgress / SetSmoothProgress, SetDuration / GetDuration, SetStartDelay / GetStartDelay, SetEndDelay / GetEndDelay, SetOrder / GetOrder (1 to 100), SetSmoothing("NONE" | "IN" | "OUT" | "IN_OUT") / GetSmoothing, SetParent(group [, order]), GetRegionParent, and the group's scripts except OnLoop.

  • Smoothing follows a cosine curve: IN starts slow, OUT ends slow and IN_OUT does both.
  • Progress steps at most 30 times a second, unless the group ignores the framerate throttle.

An animation changes the region of its group, or the target that you set. The target must be that region, or a frame, texture or font string inside it:

Method Target
SetTarget(region) The region.
SetTargetName(name) The region with that global name. name can start with $parent.
SetChildKey(childKey) The region at region[childKey], where region is the region of the group.
SetTargetKey(key) The region at the key path. $parent steps from the animation to its group, then to the region of the group, then to the parent of that region. The other parts are keys.
SetTargetParent() The region of the group.
  • Each method returns true if it finds a region now.
  • A name or a key is looked up again each time that the animation plays.
  • GetTarget() returns the target, or the region of the group if there is no target.
local glow = group:CreateAnimation("Alpha")
glow:SetChildKey("Glow")                    -- same as SetTargetKey("$parent.$parent.Glow")

Animation types: Alpha, Translation, Scale, Rotation

Type Methods
Alpha SetChange(change) / GetChange(), change from -1 to 1; SetFromAlpha(alpha) / GetFromAlpha(), SetToAlpha(alpha) / GetToAlpha(), from 0 to 1
Translation SetOffset(x, y) / GetOffset()
Scale SetScale(x, y) / GetScale(), SetScaleFrom(x, y) / GetScaleFrom(), SetScaleTo(x, y) / GetScaleTo(), SetOrigin(point, x, y) / GetOrigin()
Rotation SetDegrees / GetDegrees, SetRadians / GetRadians, SetOrigin(point, x, y) / GetOrigin()

Alpha changes the region's alpha while its group plays:

  • When a group with an Alpha animation starts, the region's alpha is stored. On a frame, the alpha of every frame, texture and font string inside it is stored too.
  • On each update the alpha goes back to the stored value, and then each Alpha animation adds change × progress. While the group plays, GetAlpha returns the animated value.
  • When the last group with an Alpha animation on the region, or on a frame around it, stops or finishes, the stored alpha comes back.
  • A value you set with SetAlpha while the group plays is replaced on the next update.

SetFromAlpha and SetToAlpha give an alpha that does not depend on the stored value:

  • On each update, the alpha becomes from + (to - from) × progress.
  • When you set one of the two values, the animation uses them. The value that you do not set is 0. SetChange makes the animation add a change again.
  • When more than one such animation applies to one region, the last one wins. An earlier order keeps its end value until the current order replaces it.
local fadeIn = group:CreateAnimation("Alpha")
fadeIn:SetFromAlpha(frame:GetAlpha())       -- start from the current alpha
fadeIn:SetToAlpha(1)

Translation moves what the region draws:

  • The offset is in UI pixels at a scale of 1. The scale of the region does not change it.
  • On a texture or font string, the texture or the text moves. On a frame, every texture and font string inside it moves, also those of the frames inside it. The frame itself does not move.
  • The rect of the region does not move. GetLeft, GetRect, anchors and mouse clicks stay where the layout puts them.
  • If the layout moves the region while the group plays, the drawing moves with it and keeps the offset.
  • When the last group with a Translation, Scale or Rotation animation stops or finishes, the drawing goes back to its place.

SetScaleFrom sets the scale at the start of a Scale animation, and SetScaleTo sets the scale at the end. Both are 1 by default. SetScale sets the end, the same as SetScaleTo, but it keeps x and y at 0.001 or more.

Scale and Rotation change the drawing in the same way:

  • On a texture, they scale or turn it around the origin from SetOrigin. The default origin is the center. The origin offset follows the direction of the texture's bottom edge.
  • A positive angle turns counter-clockwise.
  • More than one animation adds up in the order they apply. For example, a Rotation after a Translation turns around the moved texture.
  • On a font string, the text scales and turns around the point where its first line starts, and SetOrigin does not change that point. A font string uses only the x value of SetScale. When more than one Scale or Rotation animation applies to one font string, the last one wins.
  • Scaled text is stretched. It is not drawn again at the larger font size.

Path and ControlPoint

A Path moves the region through its control points. Each point is an offset from where the region is, in the same units as a Translation.

local path = group:CreateAnimation("Path")
path:SetDuration(2)
path:SetCurve("SMOOTH")
path:CreateControlPoint():SetOffset(50, 20)
path:CreateControlPoint():SetOffset(100, 0)
  • The path starts at offset 0 and ends at its last point.
  • The points are spread evenly over the animation: with 4 points, the region is at point 1 at a quarter of the progress, at point 2 at half, and so on.
  • SetCurve("NONE") (the default) moves in straight lines between the points. SetCurve("SMOOTH") moves along a smooth curve through them.
  • The drawing moves as with a Translation: the rect of the region does not move, and the drawing goes back to its place when the group ends.
Object Methods
Path SetCurve("NONE" | "SMOOTH") / GetCurve(), CreateControlPoint([name [, templateName [, order]]]), GetControlPoints(), GetMaxOrder()
ControlPoint SetOffset(x, y) / GetOffset(), SetOrder(order) / GetOrder(), SetParent(path [, order]), GetParent(), GetName(), GetObjectType(), IsObjectType(type)
  • The points go in the order of their order, from 1 to 100. A point without an order goes after the last one.
  • If you give a point an order that another point already has, the new point takes it, and the other points move up by one.
  • SetParent moves a point to another path. The point keeps its order.

<Animations> in XML

<Frame name="MyFrame">
    <Animations>
        <AnimationGroup parentKey="Pulse" looping="BOUNCE">
            <Alpha change="-0.5" duration="0.4" smoothing="IN_OUT"/>
            <Scripts>
                <OnFinished>MyFrame_OnPulseDone(self)</OnFinished>
            </Scripts>
        </AnimationGroup>
    </Animations>
</Frame>

An <Animations> block on a frame, texture or font string builds its groups before the frame's OnLoad, and each group's OnLoad runs when it is built. Groups read name, inherits, parentKey, parentArray, looping, initialOffsetX / initialOffsetY and setToFinalAlpha. parentArray adds the group to a list on its region, as it does for a frame or a texture. Animations read name, inherits, parentKey, target, targetKey, childKey, duration, startDelay, endDelay, order, smoothing, and their type's own attributes:

Type Attributes
Alpha change, or fromAlpha / toAlpha
Translation offsetX / offsetY
Scale scaleX / scaleY, fromScaleX / fromScaleY / toScaleX / toScaleY, <Origin>
Rotation degrees / radians, <Origin>
<Frame name="MyButtonGlow">
    <Layers>
        <Layer level="OVERLAY">
            <Texture parentKey="Glow" file="Interface\Buttons\UI-ActionButton-Border" alphaMode="ADD" alpha="0"/>
        </Layer>
    </Layers>
    <Animations>
        <AnimationGroup parentKey="FlashIn" setToFinalAlpha="true">
            <Alpha childKey="Glow" fromAlpha="0" toAlpha="1" duration="0.3"/>
        </AnimationGroup>
    </Animations>
</Frame>

A <Path> reads curve and a <ControlPoints> block:

<Path curve="SMOOTH" duration="2">
    <ControlPoints>
        <ControlPoint offsetX="50" offsetY="20"/>
        <ControlPoint parentKey="End" offsetX="100" offsetY="0"/>
    </ControlPoints>
</Path>

A <ControlPoint> reads name, inherits, parentKey (set on the path) and offsetX / offsetY.

  • A virtual <AnimationGroup>, animation element or <ControlPoint> at the top level of a file is a template for inherits and for the templateName arguments.
  • Animations without an order play one after another.
  • <Scripts> bodies see self and their script's arguments (elapsed, requested, loopState). A script element can name a global function with function=.

SetMaxAnimFramerate(framesPerSec) / GetMaxAnimFramerate()

Animations change what they show at most this many times per second. The default is 30. A group with SetIgnoreFramerateThrottle(true) changes on every frame.

SetMaxAnimFramerate(60)
print(GetMaxAnimFramerate())   -- 60
  • The value is for all animations.
  • 0 stops the animations from changing until they finish.

AuctionHouse

C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)

Posts numStacks auctions of quantity each, from a single source stack, in one call — a ClassicAPI extension modelled on the multi-sell flow. The engine's StartAuction only ever posts one whole item object, so this handles the splitting and the sequence of posts for you.

  • itemLocation — a { bagID = B, slotIndex = S } table identifying the source stack (0 = backpack, 1–4 = equipped bags; slotIndex 1-based).
  • duration — 1/2/3 for 2h/8h/24h (the three durations); the raw minute values 120/480/1440 are also accepted.
  • quantity — items per auction (1–255).
  • numStacks — number of auctions to post.
  • bid — minimum bid per stack, in copper (≥ 1).
  • buyout — buyout per stack, in copper (0 / omitted = no buyout).

Returns true when the job is accepted and posting has started, or false, reason on immediate rejection (AH not open, source empty, not enough items for numStacks × quantity, no free general bag slot to split into, a job already in progress, or bad arguments).

It's asynchronous. CMSG_AUCTION_SELL_ITEM has no count field — the server posts the entire item object a GUID points at. So to post a partial quantity the DLL first splits that amount into a bag slot (a server round-trip), then posts the resulting stack (another round-trip), repeating for each stack. Progress is reported through events rather than the return value:

Event Payload When
AUCTION_MULTISELL_START — job accepted, posting begins
AUCTION_MULTISELL_UPDATE postedCount, totalCount after each stack lands (final one has postedCount == totalCount)
AUCTION_MULTISELL_FAILURE — aborted (source changed under it, no free slot, AH closed, or a step timed out ~10s)

Splitting reuses one general-purpose bag slot (each post empties it for the next split); the final stack posts the source object directly with no split. Posting a whole existing stack (quantity == the source count, numStacks 1) needs no free slot at all. Every individual post goes through the engine's own StartAuction, so its validation (damaged-item reject, buyout normalization, duration check) applies to each stack.

-- Watch progress
local f = CreateFrame("Frame")
for _, e in ipairs({"AUCTION_MULTISELL_START", "AUCTION_MULTISELL_UPDATE",
                    "AUCTION_MULTISELL_FAILURE"}) do f:RegisterEvent(e) end
f:SetScript("OnEvent", function() print(event, arg1, arg2) end)

-- At an open auction house: post 3 stacks of 5, 2h, 100 bid / 200 buyout each
C_AuctionHouse.PostItem({ bagID = 0, slotIndex = 1 }, 1, 5, 3, 100, 200)

Limitations (v1): the source is the single stack at itemLocation (stacks aren't aggregated across bags — quantity × numStacks must fit in that one stack); temp splits use general-purpose bags only, so family-restricted items (arrows/shards that live in specialty bags) aren't supported; one job runs at a time.

CanCancelAuction(index)

Returns 1 when row index of the "owner" auction list is an auction that CancelAuction(index) can cancel. Returns nil for an index with no row. When the auction house is closed, it returns no value.

CancelAuction also needs money for the cancel fee when the auction has a bid. CanCancelAuction does not examine the money.

for i = 1, GetNumAuctionItems("owner") do
    if CanCancelAuction(i) then
        print(GetAuctionItemLink("owner", i))
    end
end

AuraUtil

Helper library over C_UnitAuras, backported from FrameXML. Its purpose on 1.12 is the allocation-free aura scan: the non-packed path builds no table per aura, which matters under Lua's garbage collector when you scan many units each frame.

AuraUtil.ForEachAura

AuraUtil.ForEachAura(unit, filter, batchSize, func [, usePackedAura]) calls func for each aura on unit matching filter, in order, until func returns a truthy value or the auras run out.

  • Non-packed (default): func receives the 15 positional values of C_UnitAuras.UnitAuraBySlot. No table is built per aura.
  • usePackedAura true: func receives the AuraData table from GetAuraDataBySlot.

The scan fetches slot ids in batches through C_UnitAuras.GetAuraSlots, then reads each aura by slot id. So the cost grows with the aura count, not with its square. batchSize sets how many slot ids one batch fetches. With nil, one batch fetches all of them. The value never caps how many auras func visits. A batchSize of 0 or less does nothing.

AuraUtil.ForEachAura("target", "HARMFUL", nil, function(name, icon, count)
    print(name, count)
end)

AuraUtil.FindAura

AuraUtil.FindAura(predicate, unit, filter [, arg1, arg2, arg3]) returns the positional values of the first aura for which predicate returns truthy, or nil. predicate receives the (up to three) caller arguments, then the aura's positional values:

local function castByMe(_, _, _, name, icon, count, dispelType, duration, exp, source)
    return source == "player"
end
local name = AuraUtil.FindAura(castByMe, "target", "HARMFUL")

AuraUtil.FindAuraByName

AuraUtil.FindAuraByName(auraName, unit, filter) returns the positional values of the first aura whose name matches auraName, or nil. Names are localized and not unique, so this returns the first match.

AuraUtil.UnpackAuraData

AuraUtil.UnpackAuraData(auraData) returns an AuraData table's fields as 15 positional values in the classic UnitAura order, or nil when auraData is nil. (Position 13 is the table's isFromPlayerOrPlayerPet.)

AutoComplete

Name completion for edit boxes. These functions find the names of the players you know that complete the text in an edit box. This system shows nothing. An addon shows the names, for example in a list under the chat edit box.

A player is a known name when one or more of these is true:

  • The player is in your party or raid. The entry has the InGroup bit.
  • The player is in your guild roster. The entry has the InGuild bit.
  • The player is on your friends list. The entry has the Friend bit.
  • You whispered the player, or the player whispered you, after you logged in. The entry has the InteractedWith bit.

A player on two or more of these lists is one entry, with the bit of each list. You are never an entry. The Online bit comes from your group, your guild roster and your friends list. A player that you know only from a whisper also has the Online bit.

Guild members are known names only after the client has the guild roster. GuildRoster() requests it.

C_AutoComplete.GetAutoCompleteResults(name, numResults, cursorPosition, allowFullMatch, includeFlags, excludeFlags)

Returns an array of the known names that complete name. Each entry is a table:

Field Type Value
name string The name of the player.
priority Enum.AutoCompletePriority The closest tie to the player (see below).
bnetID number Always 0.

The arguments:

  • name: the text in the edit box.
  • numResults: the maximum number of entries. At 0 or less, you get all of them.
  • cursorPosition: the position of the cursor in name, in characters, as editBox:GetUTF8CursorPosition() gives it. A value less than 1 or more than the length of name is the end of the text.
  • allowFullMatch: if this is false, a name equal to name is not an entry.
  • includeFlags: Enum.AutoCompleteEntryFlag bits. Each entry has one or more of them.
  • excludeFlags: Enum.AutoCompleteEntryFlag bits. No entry has one of them.

A name matches when it starts with name. Upper case and lower case are the same. If the cursor is inside the text, the text before the cursor must start the name. The text after the cursor must come later in the name. For example, if you type Jhn, put the cursor after the J and type o, then Jo|hn matches Johnson.

The order of the entries:

  1. The players that you whispered, the most recent first.
  2. All other names, in alphabetical order.

The priority of an entry is the first tie on this list that the player has: Friend, Guild, InGroup, Interacted. A player with none of them is Other.

local function ShowNames(editBox)
    local results = C_AutoComplete.GetAutoCompleteResults(editBox:GetText(), 5,
        editBox:GetUTF8CursorPosition(), false, Enum.AutoCompleteEntryFlag.Online, 0)
    for i = 1, table.getn(results) do
        print(results[i].name)
    end
end

Three settings change the results. See Name completion settings.

C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags)

Returns true if a known name is equal to name, and false if not. Upper case and lower case are the same. includeFlags and excludeFlags have the same function as in GetAutoCompleteResults.

if C_AutoComplete.IsRecognizedName("Johnson", Enum.AutoCompleteEntryFlag.InGuild, 0) then
    print("Johnson is in your guild.")
end

C_AutoComplete.GetAutoCompletePresenceID(name) / C_AutoComplete.GetAutoCompleteRealms()

GetAutoCompletePresenceID always returns nil, and GetAutoCompleteRealms always returns an empty table. No known name has a Battle.net presence ID or a realm name.

Enum.AutoCompleteEntryFlag

The bits of a known name, for includeFlags and excludeFlags.

Field Value Field Value
InGroup 1 Online 32
InGuild 2 InAOI 64
Friend 4 AccountCharacter 128
Bnet 8 RecentPlayer 256
InteractedWith 16

No entry has the Bnet, InAOI, AccountCharacter or RecentPlayer bit.

Enum.AutoCompletePriority

The values of an entry's priority.

Field Value Field Value
Other 0 Friend 4
Interacted 1 AccountCharacter 5
InGroup 2 AccountCharacterSameRealm 6
Guild 3

No entry has the priority AccountCharacter or AccountCharacterSameRealm.

Name completion settings

Three cvars change the results of GetAutoCompleteResults and IsRecognizedName. Each one is 1 by default. You can also set them on the Social page of the ClassicAPI options.

CVar At 1 At 0
autoCompleteResortNamesOnRecency The players that you whispered come first, the most recent first. All names are in alphabetical order.
autoCompleteWhenEditingFromCenter With the cursor inside the text, the text after the cursor must come later in the name. The name must start with all of the text.
autoCompleteUseContext includeFlags and excludeFlags select the entries. Every known name is an entry.
/console autoCompleteUseContext 0

CharacterList

GetSavedCharacterOrder(realm) / SetSavedCharacterOrder(realm, order) — GlueXML only

Persists a user-chosen ordering for the character-select list, per realm, per account. Designed for GlueXML mods that add drag-to-reorder to the character-select screen — but since the storage is a plain text file you can also pre-populate it by hand.

Only callable from the login / character-select screens (the "glue" Lua state). Calling them from an in-world addon errors with "attempt to call nil" — they aren't registered there.

  • GetSavedCharacterOrder(realm) — returns the saved order for realm as a string, or "" if none has been saved. Never nil.
  • SetSavedCharacterOrder(realm, order) — writes the order string. Pass "" to clear the saved entry for that realm.

The order value is a pipe-delimited list of character names, e.g. "Thrall|Jaina|Sylvanas". Names are matched case-sensitively against what GetCharacterInfo returns.

Storage file

Saved to WTF\Account\<ACCOUNT>\ClassicAPI.txt, one line per realm:

CharacterOrder.Octo=Thrall|Jaina|Sylvanas
CharacterOrder.AnotherRealm=Foo|Bar

<ACCOUNT> is the account name the launcher logged in with (uppercase, the same folder name WoW uses for SavedVariables). The realm key on each line is whatever string was passed as realm — typically the result of GetServerName(). This file is shared with other ClassicAPI account-scoped settings, so don't be surprised to see unrelated Key=Value lines in it; leave them alone.

You can pre-populate the file by hand if you don't have a GlueXML mod that calls SetSavedCharacterOrder. Just create or edit ClassicAPI.txt in the correct account folder before launching the game — any GlueXML code that reads GetSavedCharacterOrder(realm) will see your saved order.

Addon example

-- GlueXML, e.g. CharacterSelect.lua: persist the user's drag-reordered list
local realm = GetServerName() or ""
local names = {}
for i = 1, GetNumCharacters() do
    table.insert(names, (GetCharacterInfo(translationTable[i])))
end
SetSavedCharacterOrder(realm, table.concat(names, "|"))

-- On the next CHARACTER_LIST_UPDATE: read the saved order back
local saved = GetSavedCharacterOrder(realm)
if saved ~= "" then
    for name in string.gfind(saved, "([^|]+)") do
        -- reconcile against the current GetCharacterInfo names
    end
end

Chat

GetCurrentChatEvent()

Returns the name and the arguments of the CHAT_MSG_* event that is being handled now. It returns nothing when you call it outside a chat event. This is a ClassicAPI extension.

event, arg1, arg2, ..., arg12 = GetCurrentChatEvent()

The values are the values that the event gave to its handlers, in the same order: the event name, arg1 to arg10, then the line ID (arg11) and the sender GUID (arg12) (see CHAT_MSG_* arguments).

Use it when the event and arg1 to arg12 globals are not reliable. Other handlers and hooks can change these globals before your code reads them. This function always gives the values of the event that is in progress.

local orig = ChatFrame_OnEvent
function ChatFrame_OnEvent(event)
    local name, text, sender = GetCurrentChatEvent()
    if name then
        -- name, text and sender are the real values, even if a hook changed arg1/arg2
    end
    return orig(event)
end

If a chat event handler causes another chat event, the function gives the inner event while that handler runs. When the inner handler returns, it gives the outer event again.

GetCurrentChatGUID()

Returns the sender's GUID for the CHAT_MSG_* event that is being handled now, or nil. It gives the same sender as the event's arg12 (see CHAT_MSG_* arguments), with nil where arg12 is "". It is also nil when you call it outside a chat event.

The format is the same as UnitGUID: "0xHHHHHHHHLLLLLLLL".

local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_CHANNEL")
f:SetScript("OnEvent", function()
    local guid = GetCurrentChatGUID()
    if guid then
        local _, class, _, race = GetPlayerInfoByGUID(guid)
        print(string.format("%s: %s [%s %s]", arg2, arg1, class or "?", race or "?"))
    end
end)

If a chat event handler causes another chat event, the function gives the inner event's sender while that handler runs. When the inner handler returns, it gives the outer event's sender again.

SendSystemMessage(message)

Shows message in chat as a system message. Only your client shows the line. Nothing goes to the server.

The line has the system message color and fires CHAT_MSG_SYSTEM, with message as arg1. Chat frames and addons that watch system messages see it as they see a message from the game.

SendSystemMessage("Raid check complete.")
Argument Type Description
message string The text of the line. A number is also accepted.

If message is not a string or a number, the function raises the error Usage: SendSystemMessage("message").

The line cannot show icons. A |T or |A escape shows as text.

IsChatAFK()

Returns true when the player is marked AFK, false otherwise. Takes no arguments.

The value changes as soon as you type /afk, and when AFK clears because you chat or move with the autoClearAFK setting on. It also follows the server: when the server clears AFK (for example, in combat), the value becomes false. Use it to decide whether the next /afk sets or clears the status.

if IsChatAFK() then
    SendChatMessage("", "AFK") -- clears AFK
end

To get the AFK status of another player, use UnitIsAFK(unit).

IsChatDND()

Returns true when the player is marked DND, false otherwise. Takes no arguments.

The value changes when the server confirms the /dnd command, a short time after you type it.

if not IsChatDND() then
    SendChatMessage("Busy", "DND")
end

To get the DND status of another player, use UnitIsDND(unit).

Class

FillLocalizedClassList(table [, isFemale])

Fills the passed-in table with [classToken] = localizedClassName pairs for every class in ChrClasses.dbc, and returns the same table for chaining.

The table is mutated in place. Existing keys are overwritten; unrelated keys are preserved.

local classes = FillLocalizedClassList({})
-- classes.WARRIOR = "Warrior"
-- classes.MAGE    = "Mage"
-- classes.PRIEST  = "Priest"
-- ...

An optional isFemale boolean can fetch female-form names. ChrClasses.dbc has no separate female-name array — Name[9] (one localized string per locale) sits exactly between offsets +0x14 and +0x38, with the class token immediately after, leaving no room. The arg is accepted for signature parity but ignored; the same names are returned either way. Most locales (English included) wouldn't differentiate the two anyway, so callers won't typically notice.

Sparse class IDs (classID 6 and a few others are skipped) have NULL records and are silently skipped.

ClassColor

C_ClassColor.GetClassColor(className)

Returns the color of a class as a ColorMixin. Returns nil if className is not a class.

className is the class file name, not the localized name. It is the uppercase token that UnitClass returns as its second value.

local _, classFile = UnitClass("target")     -- "WARRIOR"
local color = C_ClassColor.GetClassColor(classFile)
if color then
    print(color:WrapTextInColorCode(UnitName("target")))
end

The color is the one the whole interface uses for that class, and every caller gets the same object. Do not write to its fields — the change applies everywhere. Use CreateColor(color:GetRGBA()) for a copy you can edit.

An addon that recolors a class changes what this function returns.

ClassicAPI namespace

Every function on this page is bound twice: under the name it documents, and under the same name inside the ClassicAPI table. Namespaced calls keep their namespace as a subtable.

ClassicAPI.GetServerTime()                       -- == GetServerTime
ClassicAPI.C_Item.IsBound(itemLocation)          -- == C_Item.IsBound
ClassicAPI.coroutine.create(fn)                  -- == coroutine.create

Both names hold the same function, so ClassicAPI.X == X is true. That equality is the point of the table. A Lua global belongs to whoever writes it last, and FrameXML and every addon load after ClassicAPI registers, so any of these names can be taken later with no error and no warning. The mirrored name cannot be taken, which gives you two things:

-- Reach a function whose global was replaced
local now = ClassicAPI.GetServerTime()

-- Detect that it was replaced
if GetServerTime ~= ClassicAPI.GetServerTime then
    -- something else owns this global now
end

Prefer the documented name. Code written against GetServerTime() runs on other clients; code written against ClassicAPI.GetServerTime() runs only here. Reach for the table when a name is known to be taken, or to test whether it is — not as a default calling style.

Two kinds of addition are absent from the table. Frame methods (tooltip:SetSpellByID(...)) are not globals, so nothing mirrors them. Enum tables and value globals such as CLASSIC_API_VERSION are values rather than functions, and a mirrored value would be a stale copy.

ColorUtil

The C_ColorUtil color-space and text-color-code helpers. All are pure functions (no engine state); conventions verified in-game.

Ranges: hue is in degrees, [0, 360); saturation / value / lightness are [0, 1]. RGB channels are [0, 1] (not 0–255). Achromatic (gray) inputs return a hue of -1 — a sentinel, not 0 — which the reverse conversions accept as "no hue" (equivalent to saturation == 0).

The color argument is a table with r, g, b fields in [0, 1] (ColorMixin-shaped). An optional a is honored (default 1), though the color code always encodes 8 hex digits with alpha.

C_ColorUtil.ConvertRGBToHSV(r, g, b)

Returns (h, s, v). Achromatic input → h == -1.

C_ColorUtil.ConvertRGBToHSV(0, 1, 0)         -- 120, 1, 1   (pure green)
C_ColorUtil.ConvertRGBToHSV(0.5, 0.5, 0.5)   -- -1, 0, 0.5  (gray)

C_ColorUtil.ConvertHSVToRGB(h, s, v)

Returns (r, g, b). A negative hue (or s == 0) is treated as achromatic and yields r == g == b == v.

C_ColorUtil.ConvertHSVToRGB(120, 1, 1)   -- 0, 1, 0

C_ColorUtil.ConvertHSVToHSL(h, s, v)

Returns (h, s, l). Hue passes through unchanged.

C_ColorUtil.ConvertHSLToHSV(h, s, l)

Returns (h, s, v). Hue passes through unchanged.

C_ColorUtil.ConvertHSLToRGB(h, s, l)

Returns (r, g, b). A negative hue (or s == 0) is achromatic → r == g == b == l.

C_ColorUtil.GenerateTextColorCode(color)

Returns the bare 8-hex color string "AARRGGBB" (alpha first), not including the |c escape.

C_ColorUtil.GenerateTextColorCode({r = 1, g = 0, b = 0})   -- "ffff0000"

C_ColorUtil.WrapTextInColor(text, color)

Wraps text in the color's escape sequence: "|c" .. AARRGGBB .. text .. "|r".

C_ColorUtil.WrapTextInColor("Hi", {r = 1, g = 0, b = 0})   -- "|cffff0000Hi|r"

C_ColorUtil.WrapTextInColorCode(text, colorCode)

Wraps text in the given bare color code: "|c" .. colorCode .. text .. "|r". Pair with GenerateTextColorCode (which produces colorCode).

local code = C_ColorUtil.GenerateTextColorCode({r = 1, g = 0, b = 0})
C_ColorUtil.WrapTextInColorCode("Hi", code)   -- "|cffff0000Hi|r"

Combat

InCombatLockdown()

Always returns false. Combat lockdown gates secure-frame UI manipulation, and there is no secure-frame system here — there's nothing to lock down. This function is a no-op stub, so addons that call it don't error on a missing global.

For "is the player actually in combat?", use UnitAffectingCombat("player"), which the engine ships.

if not InCombatLockdown() then
    -- always true in 1.12 — falls through unconditionally
end

if UnitAffectingCombat("player") then
    -- this is the real check
end

StartAttack([target])

Starts your melee auto-attack. The AttackTarget() global turns the attack on or off with each call. StartAttack only starts it — a call while you attack your current target does not toggle the attack off. So a /startattack macro can use it safely.

With no argument, it attacks your current target. With a unit token ("target", "focus", "party1"), it attacks that unit. A name or an unknown token uses the current target instead.

When you have no target, it attacks "softenemy" and your target stays empty. AttackTarget() does the same. If there is no soft enemy, the nearest enemy becomes your target and you attack it. See Soft enemy and soft friend.

StartAttack()            -- attack the current target; never cancels an attack
StartAttack("focus")     -- attack your focus unit

StopAttack()

Stops your melee auto-attack. If you are not attacking, the call does nothing.

Both functions also exist as slash commands: /startattack [target] and /stopattack. The commands accept the same [conditions] that /focus accepts, and each client language has its own command names next to the English ones (for example /angriffstart and /angriffstop on a German client).

/startattack
/startattack focus
/startattack [harm] target
/stopattack

stopAutoAttackOnTargetChange

A setting for your attacks when you change target. At 0 (the default), melee auto-attack continues on the new target if you can attack it. At 1, a target change stops melee auto-attack, Auto Shot and wand shots.

/console stopAutoAttackOnTargetChange 1

CombatLog

CombatLogGetCurrentEventInfo()

Returns the same values as the COMBAT_LOG_EVENT_UNFILTERED arguments, for the event that is being sent. It returns values only while a COMBAT_LOG_EVENT_UNFILTERED or COMBAT_LOG_EVENT handler runs. At other times it returns nothing.

f:SetScript("OnEvent", function()
    local timestamp, subevent, _, sourceGUID = CombatLogGetCurrentEventInfo()
end)

CombatLogAddFilter([events [, source [, dest [, spell]]]]) / CombatLogResetFilter()

CombatLogAddFilter adds a filter. CombatLogResetFilter removes all filters. An event matches when it matches one of the filters, or when there is no filter. The filter controls COMBAT_LOG_EVENT and the kept-entry functions. COMBAT_LOG_EVENT_UNFILTERED does not use it.

An event matches a filter only when each of its four parts matches:

Argument Value Matches
events string The subevent names in the string, divided by spaces or commas. The case of the letters is not important.
events nil Each subevent.
source number A source whose flags match this COMBATLOG_OBJECT_* mask (see below).
source string The source with this GUID.
source nil Each source.
dest number, string, nil The target, in the same way as source.
spell number The spell with this spell ID.
spell string The spell with this name. The case of A to Z is not important.
spell nil Each spell, and events with no spell.

A unit's flags match a mask when they have a COMBATLOG_OBJECT_TARGET, FOCUS, MAINTANK or MAINASSIST bit of the mask. Otherwise, the flags must have one affiliation bit, one reaction bit, one control bit and one type bit of the mask. A mask that does not name all four groups and has none of these special bits causes the error CombatLogAddFilter(): incomplete filter for srcMask (or dstMask).

The COMBATLOG_FILTER_* constants are complete masks:

Constant Matches
COMBATLOG_FILTER_ME You.
COMBATLOG_FILTER_MINE You, and the objects that you control.
COMBATLOG_FILTER_MY_PET Your pets and guardians.
COMBATLOG_FILTER_FRIENDLY_UNITS Friendly units that are not yours.
COMBATLOG_FILTER_HOSTILE_PLAYERS Hostile units that a player controls.
COMBATLOG_FILTER_HOSTILE_UNITS Hostile units that an NPC controls.
COMBATLOG_FILTER_NEUTRAL_UNITS Neutral units that are not yours.
COMBATLOG_FILTER_UNKNOWN_UNITS No unit: the flags of an event with no source or no target.
COMBATLOG_FILTER_EVERYTHING All units.
-- Your own damage.
CombatLogResetFilter()
CombatLogAddFilter("SWING_DAMAGE,SPELL_DAMAGE,SPELL_PERIODIC_DAMAGE", COMBATLOG_FILTER_MINE)

The filters stay after /reload. They are removed when you log out.

CombatLog_Object_IsA(flags, mask)

Returns 1 when flags match mask by the rule of CombatLogAddFilter. Otherwise, returns nil.

if CombatLog_Object_IsA(sourceFlags, COMBATLOG_FILTER_ME) then
    -- The source is you.
end

Kept entries

The combat log keeps each event as an entry, oldest first. An entry older than the retention time is removed when a new entry comes. The entries stay after /reload. They are removed when you log out.

Function Description
CombatLogGetRetentionTime() Returns the retention time, in seconds. The default is 300.
CombatLogSetRetentionTime(seconds) Sets the retention time. The combatLogRetentionTime CVar keeps the value.
CombatLogGetNumEntries([ignoreFilter]) Returns the number of entries that match the filter. With ignoreFilter, returns the number of all entries.
CombatLogSetCurrentEntry(index [, ignoreFilter]) Selects an entry. Index 1 is the oldest entry that matches the filter. Index 0 is the newest, and -1 is the entry before it. Returns 1 when the entry exists, else nil.
CombatLogAdvanceEntry(count [, ignoreFilter]) Moves the selection by count entries that match the filter. A negative count moves to older entries. Returns 1 when an entry is selected, else nil.
CombatLogGetCurrentEntry() Returns the values of the selected entry, in the same layout as CombatLogGetCurrentEventInfo. Returns nothing when no entry is selected.
CombatLogClearEntries() Removes all entries.

When the selected entry is removed, the selection moves to the next entry.

CombatLogAdvanceEntry counts the selected entry when it matches the filter. Thus, after CombatLogSetCurrentEntry(1), a count of 1 moves to the second entry that matches.

-- Print the last 5 entries.
if CombatLogSetCurrentEntry(-4) then
    repeat
        local timestamp, subevent, _, _, sourceName = CombatLogGetCurrentEntry()
        print(subevent, sourceName)
    until not CombatLogAdvanceEntry(1)
end

Console

The developer console is the overlay you open with ~ when the client is started with -console. These functions run commands in it, write to it, and read its state.

ConsoleExec(command)

Runs a console command line, the same way typing it into the console does.

ConsoleExec("ExportDBCFiles")
ConsoleExec("gxRestart")

Every command ConsoleGetAllCommands lists can be run this way, so anything the console can do is now reachable from Lua. The command is added to the console's history, and an unknown command writes Unknown command to the console rather than raising.

The console's own set command is not the same as SetCVar. It writes any cvar, including one SetCVar refuses as read-only and one no Lua function can read. ConsoleExec("set …") is therefore a way around the limits every other cvar function keeps, and it changes settings that persist. Prefer SetCVar unless you specifically want the console's behaviour.

Works whether or not the console is open, and whether or not -console was used.

ConsoleAddMessage(message)

Writes a line of text to the console. An empty string writes nothing. An argument that is not a string raises an error.

ConsoleAddMessage("Hello from Lua")

ConsoleEcho(message [, colorType])

Writes one line to the console. colorType is 0 to 8 and picks the colour, as ConsoleGetColorFromType describes; it defaults to 0, white.

ConsoleEcho("something happened")
ConsoleEcho("something went wrong", 3)   -- red

The line is kept whether or not the console is open, so it is there when you next open it.

ConsolePrintAllMatchingCommands(prefix)

Writes every command whose name begins with prefix to the console, the way its own completion lists candidates. The match ignores case. An empty prefix prints nothing; use ConsoleGetAllCommands to get everything.

ConsoleIsActive()

Returns whether the console is open right now. Always false when the client was started without -console, because the key that opens it does nothing then.

ConsoleGetColorFromType(colorType)

Returns r, g, b, a for one of the console's nine line colours, each 0 to 1, or nil outside that range.

Type Colour Type Colour
0 white 5 white
1 white 6 white
2 grey 7 white, half alpha
3 red 8 black
4 yellow

ConsoleGetFontHeight()

Returns the console's text height as a fraction of the screen height. The default is 0.02.

There is no matching setter. The font is built once while the client starts, and rebuilding it afterwards leaves every line already in the console pointing at a font that no longer exists.

SetConsoleKey(keyCode)

Sets the key that opens and closes the console.

keyCode is the client's own numeric key code, not a key name — the console compares raw key codes and nothing in it reads names. The key still only works when the client was started with -console.

CalculateStringEditDistance(a, b)

Returns how many single-character insertions, deletions or substitutions turn one string into the other. The console uses it to suggest a command when one is mistyped.

CalculateStringEditDistance("kitten", "sitting")   -- 3
CalculateStringEditDistance("help", "hepl")        -- 2

Returns nil if the shorter string is 256 characters or longer.

ConsoleGetAllCommands()

Returns a list of every console command and console variable the client has, each as a table:

Field Type Meaning
command string the name you type in the console
help string the description, or "" when it has none
category number an Enum.ConsoleCategory value
commandType number an Enum.ConsoleCommandType value
scriptContents string always ""
scriptParameters string always ""
for _, info in ipairs(ConsoleGetAllCommands()) do
    if info.commandType == Enum.ConsoleCommandType.Cvar then
        print(info.command, GetCVar(info.command))
    end
end

Console variables are included, and most of them carry no description, so help is often "" for them. Console macros and scripts do not exist on this client, so commandType is only ever Cvar or Command, and the two script fields are always empty.

Enum.ConsoleCategory holds Debug 0, Graphics 1, Console 2, Combat 3, Game 4, Default 5, Net 6, Sound 7, Gm 8, Reveal 9 and None 10. Every command reports one of Debug through Gm. Enum.ConsoleCommandType holds Cvar 0, Command 1, Macro 2 and Script 3.

The list is live: a command added while the game runs, such as one of the export commands below, appears in the next call.

ExportInterfaceFiles art|code (console command)

A developer-console command (not a Lua function) — type it into the ~ console, which is available when the client is launched with -console. It extracts Blizzard's stock UI files out of the game's MPQ archives onto disk. Useful for reading the FrameXML / GlueXML source you're backporting addons against, or pulling out the default art.

  • ExportInterfaceFiles code — writes .lua / .xml / .toc / .xsd files to BlizzardInterfaceCode\…
  • ExportInterfaceFiles art — writes .blp / .tga files to BlizzardInterfaceArt\…

Files are written relative to the client's working directory (next to WoW.exe), preserving their Interface\… subtree under the destination folder — e.g. Interface\FrameXML\FrameXML.toc becomes BlizzardInterfaceCode\FrameXML\FrameXML.toc. On completion the command prints a wrote N file(s) line back to the console.

> ExportInterfaceFiles code
ExportInterfaceFiles: wrote 1234 code file(s) to BlizzardInterfaceCode\

Notes:

  • It enumerates the mounted MPQs' (listfile), so it surfaces whatever the archives actually ship — including Blizzard's own UI addons under Interface\AddOns\Blizzard_* (AuctionUI, TalentUI, TradeSkillUI, etc.), which are part of the stock UI source. Files present in multiple archives (base + patches) are written once.
  • Your own loose, on-disk addons are not exported — they aren't in any archive's listfile, so they're never enumerated.
  • It runs synchronously and briefly freezes the client while it walks Interface\ — expected for a one-shot extraction.

ExportDBCFiles (console command)

A ClassicAPI-original companion to ExportInterfaceFiles that dumps every .dbc table the client loads to DBFilesClient\ under the working directory (next to WoW.exe). No subcommand — there's only one thing to export.

> ExportDBCFiles
ExportDBCFiles: wrote 155 .dbc file(s) to DBFilesClient\

Unlike the MPQ (listfile) alone (which omits ~18 of the tables the client actually loads — mostly cosmetic animation/sound/lookup DBCs that Blizzard left out of the index), this unions two sources for a complete set:

  1. the MPQ (listfile) under DBFilesClient\ — catches files that ship in the archives without a loader (e.g. wowerror_strings.dbc);
  2. a .text scan for the DBC path-getter pattern (mov eax, &"DBFilesClient\X.dbc"; ret) — the authoritative list of what this build loads. The master DBC init (FUN_0053f8b0) is straight-line with no iterable name table, so the per-DBC getters are the registry; scanning for them is the same technique that built docs/DBCs.md.

Results are deduped case-insensitively. Handy for a local DBC dump to inspect schemas/values directly — e.g. verifying a record's column layout against a known row, the way C_Item.GetEnchantInfo's effect columns were confirmed — without a standalone MPQ extraction tool.

ExportSoundFiles [subpath] (console command)

A ClassicAPI-original companion that dumps the client's sound files out of the MPQ archives to BlizzardSound\ under the working directory (next to WoW.exe). Every sound the client can play lives under one Sound\ root, and the export preserves that subtree — e.g. Sound\Creature\Ragnaros\RagnarosAggro01.wav becomes BlizzardSound\Creature\Ragnaros\RagnarosAggro01.wav.

> ExportSoundFiles Creature\Ragnaros
ExportSoundFiles: wrote 27 file(s) under Sound\Creature\Ragnaros to BlizzardSound\

The optional subpath narrows the export to a path prefix under Sound\. It is plain prefix matching, so Creature\Rag also catches Creature\Ragnaros. Use it: the whole tree is roughly a gigabyte, and the command runs synchronously, freezing the client until it finishes. Bare ExportSoundFiles exports everything.

Notes:

  • Every file under Sound\ is exported, whatever its extension. The tree is entirely audio (.wav, .mp3, .ogg), and a few files are named inconsistently, so filtering by extension would only hide them.
  • The export covers every sound the client can play, not only the ones its archives index: several hundred files it ships are named by no index at all, and the client's own sound table supplies them instead. That table also names files this build does not ship (sounds belonging to later content); those are skipped, so the written count lands under the number of names.
  • A narrowed export writes into the same layout a full one would, so running it repeatedly with different subpaths builds up one tree.
  • The destination is BlizzardSound\, deliberately not Sound\. The client prefers a loose file over the archived copy of the same path, so exporting into Sound\ would shadow the archives with a gigabyte of duplicates that get re-indexed at every launch.

pitchLimit degrees (console command)

Sets how far the camera can tilt up and down. The limit is in degrees and applies in both directions. The default limit is 89 degrees.

> pitchLimit 60

The command does not validate the value. A bare pitchLimit sets the limit to 0, which holds the camera level. The limit stays until you change it or restart the client.

AppendLogToFile [filename] [numLines] (console command)

Writes the console output to a text file. This is a ClassicAPI extension.

  • filename is relative to the client's working directory (next to WoW.exe). The default is ConsoleLogs\Log<Timestamp>.txt. The command makes missing folders. If the file exists, the command adds the lines to the end of it.
  • numLines limits the output to the most recent lines. Without it, the command writes all lines.

The file has the oldest line first. The line that you type at the time is not part of the output.

> AppendLogToFile
AppendLogToFile: wrote 212 line(s) to ConsoleLogs\Log20261019_143022.txt

Tab completion in the console

The console has three Tab functions:

  • Tab on a command name completes the name. Press Tab again for the next match. Press Shift+Tab for the previous match.
  • Tab after set, cvar_reset or cvar_default completes the CVar name. Matches cycle in alphabetical order, the same as for command names. This is a ClassicAPI extension.
  • Ctrl+Tab lists every command whose name contains the typed text, in alphabetical order. Case is not important. After set, cvar_reset or cvar_default, it lists the CVar names that start with the typed text.

Container

C_Container.GetContainerItemID(bagIndex, slotIndex)

Returns the itemID at the given bag/slot, or nil if the slot is empty or the indices are out of range. Positional-arg form of the same lookup C_Item.GetItemID({bagID=B, slotIndex=S}) performs.

  • bagIndex = 0 — the player's main backpack.
  • bagIndex = 1..4 — the player's equipped bag slots.
  • slotIndex — 1-based, capped at the bag's actual slot count (the engine's PackBagSlot rejects out-of-range slots and returns nil cleanly).
for slot = 1, 16 do
    local id = C_Container.GetContainerItemID(0, slot)
    if id then
        local _, type, subtype = C_Item.GetItemInfoInstant(id)
        -- ...
    end
end

C_Container.GetContainerItemInfo(containerIndex, slotIndex [, buf])

Returns a ContainerItemInfo table for the item in the given bag slot, or nil if the slot is empty / the indices are out of range. The structured-table form of the flat global GetContainerItemInfo, which only returned texture, itemCount, locked, quality, readable.

  • containerIndex = 0 — main backpack; 1..4 — equipped bag slots.
  • slotIndex — 1-based.
  • buf — optional table (ClassicAPI extension). When you give a table, the function writes the fields into it and returns it, instead of a new table. It clears the fields that the item does not have. For an empty slot, it returns nil and does not change the table.

A bag addon that reads every slot on each BAG_UPDATE can give the same buf to each call. Then the calls do not make a new table each time.

Table fields:

Field Type Notes
iconFileID string Icon path (no fileID system here, same as GetItemIcon). Absent if the item's static data isn't cached yet.
stackCount number Current stack size.
isLocked boolean Item is in a pending transaction (pickup/trade/mail in flight).
quality number|nil Enum.ItemQuality (0=Poor … 5=Legendary); nil until the item is cached.
isReadable boolean Has readable page text (books/letters).
hasLoot boolean Static LOOTABLE flag (best-effort — post-loot emptiness can't be tracked client-side).
hyperlink string Fully-decorated per-instance item link (enchant + random suffix), same as GetContainerItemLink.
isFiltered boolean true when a bag search is active and the item does not match it. See C_Container.SetItemSearch.
hasNoValue boolean true when the item's vendor sell price is 0.
itemID number Item ID.
isBound boolean Soulbound.
itemName string Per-instance decorated display name (falls back to the base name).

Live per-instance fields (stackCount, isLocked, isBound, itemID, hyperlink) are always present; cache-derived fields (iconFileID, quality, hasLoot, hasNoValue, itemName) are omitted/nil until the item's static data arrives (listen for GET_ITEM_INFO_RECEIVED). Passive — does not warm the item cache.

local info = C_Container.GetContainerItemInfo(0, 1)
if info then
    print(info.itemName, info.stackCount, info.quality, info.hyperlink)
end

-- One table for the whole backpack:
local buf = {}
for slot = 1, GetContainerNumSlots(0) do
    if C_Container.GetContainerItemInfo(0, slot, buf) then
        -- buf holds this slot's item until the next call
    end
end

C_Container.SetItemSearch(searchString)

Sets the text of the bag search. The bags, the bank, and the keyring all use the same search. An empty string clears the search.

When the search changes, the INVENTORY_SEARCH_UPDATE event fires. If the new text gives the same search, the event does not fire. For example, "Potion" after "potion" does not fire it.

An item matches the search when the search text is part of one of these:

  • the name of an equipment set that holds the item
  • the full name of the item, with its random suffix ("of the Owl")
  • the subclass name of the item, for example "Bow" or "Cloth"
  • the name of its equip slot, for example "Head" or "Two-Hand"

An item that does not match has isFiltered = true in C_Container.GetContainerItemInfo.

Rules of the match:

  • The search ignores case. On French, German, Spanish, and Russian clients, it also ignores accents: "epee" matches "Épée".
  • Cloaks do not match their subclass name ("Cloth").
  • Axes, maces, polearms, swords, staves, fist weapons, daggers, and spears do not match their subclass name. A search for "sword" finds a sword only when its name contains "sword". The equip slot name ("One-Hand", "Two-Hand") still matches.
  • The equipment set match uses the item that was saved in the set. Another copy of the same item does not match.
  • If the data of an item is not in the cache yet, the item matches.
  • When the data of an item arrives later, no event fires. Read isFiltered again on BAG_UPDATE and on INVENTORY_SEARCH_UPDATE.
  • The search stays after /reload. A logout clears it.
-- In the OnTextChanged script of a search box:
C_Container.SetItemSearch(this:GetText())

C_Container.IsContainerFiltered(containerIndex)

Returns true when a bag search is active and no item in the container matches it. Returns false when no search is active. A bag addon can use this to dim a bag button.

  • containerIndex — 0 backpack, 1..4 bags, -1 bank, 5..10 bank bags, -2 keyring.

When a search is active, these also return true:

  • an empty bag
  • an index that is not a container
  • the bank and the bank bags while the bank is closed
for bag = 0, 4 do
    print(bag, C_Container.IsContainerFiltered(bag))
end

C_Container.HasContainerItem(bagIndex, slotIndex)

Returns true if the given bag slot currently holds an item, false otherwise (empty slot or out-of-range indices). The occupancy sibling of GetContainerItemID — same slot resolution, but a plain presence check.

  • bagIndex = 0 — the player's main backpack; 1..4 — equipped bag slots.
  • slotIndex — 1-based.

Presence only: it does not require the item's static data to be cached, so it returns true for a just-looted item even before GetContainerItemID / GetItemInfo resolve for it.

if C_Container.HasContainerItem(0, 1) then
    -- backpack slot 1 is occupied
end

GetItemCooldown(itemInfo) / C_Container.GetItemCooldown(itemID)

Returns (startTime, duration, enable) for the cooldown of the spell triggered by the item's ON_USE effect. Direct-by-ID variant of the stock GetContainerItemCooldown(bag, slot) — no slot reference required.

-- Hearthstone (6948), right after using it:
/dump GetItemCooldown(6948)
-- 732120.014, 3600, 1  (startTime in GetTime() seconds, 60 min duration, ready)

-- Item with no ON_USE effect (or invalid itemID) returns all zeros.
/dump GetItemCooldown(2589)   -- Linen Cloth → 0, 0, 0
Field Notes
startTime GetTime()-compatible seconds when the cooldown started. 0 if no cooldown.
duration Cooldown length in seconds. 0 if no cooldown.
enable 1 for "ready or counting down" (normal state); 0 for "used but cooldown hasn't started yet" (the potion-in-combat case).

Input shapes:

  • GetItemCooldown(itemInfo) — accepts itemID, item:N / chat-link hyperlink, numeric string, or item name (resolved via the shared Item::Arg helper, same chain C_Item.GetItemCount etc. use).
  • C_Container.GetItemCooldown(itemID) — the namespaced signature accepts number / hyperlink but not spell name (per the spec "will not accept an itemlink or name", but link parsing falls out of the shared Item::Arg::Resolve for free, so we accept it).

Routes through FUN_ITEM_QUERY_COOLDOWN (0x006E2ED0) which finds the item's ON_USE spell slot in its ItemStats_C record and queries that spell's cooldown via the same manager player-spell cooldowns use.

C_Container.GetContainerItemDurability(containerIndex, slotIndex)

Bag/bank variant of GetInventoryItemDurability. Same (current, maximum) return shape and the same "nothing for items without durability" rule.

current, maximum = C_Container.GetContainerItemDurability(containerIndex, slotIndex)
  • containerIndex = 0 — the player's main backpack.
  • containerIndex = 1..4 — the player's equipped bag slots.
  • containerIndex = -1, 5..11 — bank-frame slots, only addressable while the bank window is open.
  • slotIndex — 1-based, capped at the bag's actual slot count.
for slot = 1, GetContainerNumSlots(0) do
    local cur, max = C_Container.GetContainerItemDurability(0, slot)
    if cur then
        -- item at backpack slot has durability
    end
end

Goes through the same bag-resolve chain C_Container.GetContainerItemID uses (engine's PackBagSlot → GetItemBySlot), then reads the same durability fields off the descriptor.

C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)

Bag/bank variant of GetInventoryItemRepairCost. Same copperCost single-int return and the same "0 for nothing to repair" semantics.

copperCost = C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)

containerIndex accepts the same values as C_Container.GetContainerItemDurability (0 = backpack, 1..4 = equipped bags, -1/5..11 = bank, etc.).

for slot = 1, GetContainerNumSlots(0) do
    local cost = C_Container.GetContainerItemRepairCost(0, slot)
    if cost > 0 then
        -- broken/damaged item sitting in the backpack
    end
end

Useful for "repair only items above N copper" smart-repair logic without scanning tooltips. A ClassicAPI extension.

C_Container.GetContainerItemCharges(containerIndex, slotIndex)

Per-slot counterpart of C_Item.GetItemCount's includeCharges=true mode — returns the total uses available in this single slot, where GetItemCount totals the same value across every matching slot.

uses = C_Container.GetContainerItemCharges(containerIndex, slotIndex)

containerIndex accepts the same values as C_Container.GetContainerItemDurability (0 = backpack, 1..4 = equipped bags, -1/5..11 = bank, etc.). slotIndex is 1-based.

Math is stack * usesPerItem, where usesPerItem is abs(SPELL_CHARGES[0]) for items with a negative charges field (consume-on-use: wands, healthstones, mana gems, sapper charges) and 1 for everything else. Worked examples:

Slot contents Stack Raw charges Returns
Wand of Decay at 50 charges 1 -50 50
Wand at 1 charge 1 -1 1
Healthstone 1 -1 1
Stack of 20 water 20 -1 20
Stack of 5 mana potions 5 -1 5
Hearthstone (cooldown-only) 1 0 1
Any other item in the slot 1 0 1

Returns nil only when the slot is empty.

-- "how many charges does my wand have left?"
local charges = C_Container.GetContainerItemCharges(0, 1)
print("Wand charges remaining: " .. charges)

-- "how many drinks across the bags?"
local total = 0
for bag = 0, 4 do
    for slot = 1, GetContainerNumSlots(bag) do
        if C_Container.GetContainerItemID(bag, slot) == waterID then
            total = total + C_Container.GetContainerItemCharges(bag, slot)
        end
    end
end

A ClassicAPI extension (addons otherwise read charges off tooltip text). Useful when you need a per-slot number rather than GetItemCount's rollup — e.g. to display "X charges" on the slot UI for a wand without re-walking every other matching item.

C_Container.GetContainerNumFreeSlots(bagID)

Returns the number of empty slots in the given bag, plus the bag's BagFamily bitfield (the type of items the bag is restricted to).

numberOfFreeSlots, bagType = C_Container.GetContainerNumFreeSlots(bagID)
  • bagID = 0 — the player's main backpack. Always reports (freeCount, 0) — the backpack is unfamilied (general-purpose).
  • bagID = 1..4 — the player's equipped bag slots. Slot count and bagType come from the bag item's cached ItemStats_C record — m_containerSlots and m_bagFamily respectively. If no bag is equipped (or the item somehow isn't cached): (0, 0).
  • Other bagID values (bank, keyring, out-of-range): (0, 0). Always returns two values, never nil.
local free, bagType = C_Container.GetContainerNumFreeSlots(0)
-- free = number of empty slots in backpack, bagType = 0

for bag = 0, 4 do
    local free = C_Container.GetContainerNumFreeSlots(bag)
    -- ...
end

bagType is derived for specialty bags. Bags leave the raw BagFamily field empty in Turtle's data (Soul Bag, Herb Bag, quiver, ammo pouch, … all read 0). We fall back to deriving the family from the bag's class + subclass, so a Soul Bag reports 0x4 here, a quiver 0x1, an ammo pouch 0x2 — the same values C_Item.GetItemFamily reports for both the bag and the items it holds. This also means CalculateTotalNumberOfFreeBagSlots now correctly excludes specialty bags from the general-purpose free-slot total (it previously counted them, since the raw field was 0). Note the Turtle custom families (Meat/Fish/Leather/Mining, 0x200–0x1000) collide with the standard bit meanings — see the GetItemFamily family table for the caveat.

The bitmask encoding. bagType is the bit position (1 << (familyID - 1)), not the raw stored familyID, so callers can bitwise-AND with itemFamily values from C_Item.GetItemFamily directly. We convert internally — see that function's notes for the encoding-shift story.

Implementation walks the bag using Item::Location::ResolveBag internally and counts slots that resolve to a null CGItem * (i.e., empty). Cross-checked in-game against a manual C_Container.GetContainerItemID walk; counts match.

C_Container.GetContainerFreeSlots(bagID)

Returns which slots in a bag are empty, as a table of slot numbers in ascending order.

freeSlots = C_Container.GetContainerFreeSlots(bagID)
  • bagID = 0 — the player's main backpack.
  • bagID = 1..4 — the player's equipped bag slots. If no bag is equipped there, the call returns nothing.
  • Other bagID values (bank, keyring, out of range) return nothing.

A full bag returns an empty table. A bag that is not there returns nothing at all, so an empty table and a missing bag stay easy to tell apart:

local freeSlots = C_Container.GetContainerFreeSlots(bagID)
if freeSlots then
    for _, slot in ipairs(freeSlots) do
        -- slot is empty
    end
end

Use C_Container.GetContainerNumFreeSlots when you only need how many there are.

C_Container.GetContainerItemQuestInfo(containerIndex, slotIndex)

Returns the quest state of one bag slot, as a single table.

questInfo = C_Container.GetContainerItemQuestInfo(containerIndex, slotIndex)

The table holds three fields:

  • isQuestItem — true when the item is a quest item.
  • questID — the quest the item begins, or nil when it begins no quest.
  • isActive — true when the quest in questID is already in the player's quest log.

questID is the quest the item starts, not a quest the item counts towards. Together the three fields say which marker a bag button should show: a questID the player has not accepted yet takes the exclamation mark, and either an accepted questID or a plain quest item takes the question-mark border.

local questInfo = C_Container.GetContainerItemQuestInfo(bag, slot)
if questInfo.questID and not questInfo.isActive then
    -- this item starts a quest the player has not picked up
elseif questInfo.questID or questInfo.isQuestItem then
    -- quest-related item
end

The table always comes back, so the fields are safe to read directly. An empty slot reports isQuestItem = false, no questID, and isActive = false.

The first read of an item can be blank. An item whose data has not arrived from the server yet reports the empty answer above, and asks for that data. Read it again after GET_ITEM_INFO_RECEIVED or the next BAG_UPDATE and it will be right.

C_Container.GetContainerItemEquipmentSetInfo(containerIndex, slotIndex)

Returns whether the item in a bag slot belongs to an equipment set, and the names of the sets it belongs to.

inSet, setList = C_Container.GetContainerItemEquipmentSetInfo(containerIndex, slotIndex)
  • inSet — true when the item is part of at least one equipment set.
  • setList — the set names, in the order the sets are stored, joined with PLAYER_LIST_DELIMITER (", " in English). nil when the item is in no set.
local inSet, setList = C_Container.GetContainerItemEquipmentSetInfo(bag, slot)
if inSet then
    GameTooltip:AddLine("Equipment Sets: " .. setList)
end

The match is on the exact item, not on the kind of item. An equipment set remembers the individual item that was saved into it, so a second copy of the same item in another slot reports false while the saved one reports true. Reorganising your bags does not change the answer: the item keeps its identity wherever it is stored.

An empty slot, and a slot in a bag you do not have, both report false.

C_Container.CalculateTotalNumberOfFreeBagSlots()

Returns the total number of free slots across the player's general-purpose bags — backpack plus any equipped bags 0..4 whose BagFamily is 0. Specialty bags (quivers, soul bags, herb bags, …) are excluded, matching FrameXML's implementation:

local total = 0
for bag = 0, 4 do
    local free, family = C_Container.GetContainerNumFreeSlots(bag)
    if family == 0 then total = total + free end
end
-- total == C_Container.CalculateTotalNumberOfFreeBagSlots()

Takes no arguments.

Same sparse-BagFamily caveat as GetContainerNumFreeSlots: because the server data leaves most bags' family field at 0, specialty bags that would normally be excluded are counted here. The result stays consistent with the per-bag GetContainerNumFreeSlots values it sums.

C_Container.IsContainerItemOpenable(containerIndex, slotIndex)

Positional-arg wrapper for C_Item.IsItemOpenable against a bag/slot pair. Same (isOpenable, canOpen) tuple — see the linked section for full semantics. Both returns are nil for empty slots or items whose data hasn't been cached yet.

-- Sweep main backpack for openables
for slot = 1, GetContainerNumSlots(0) do
    if C_Container.IsContainerItemOpenable(0, slot) then
        -- ...
    end
end

Same cache-warming caveat as the underlying C_Item.IsItemOpenable — a freshly-seen item may report false for one call while the SMSG_ITEM_QUERY_SINGLE round-trips. Bag-resident items are almost always already cached.

C_Container.PlayerHasHearthstone()

Returns the itemID of the hearthstone if one is in the player's bags, or nil otherwise.

itemID = C_Container.PlayerHasHearthstone()
if C_Container.PlayerHasHearthstone() then
    -- player can hearth
end

Match logic. An item counts as a hearthstone if either:

  1. Its itemID is 6948 (the standard Hearthstone — fast path, no cache lookup needed), or
  2. Its on-use spell is spell 8690 (the "Hearthstone" cast itself).

Rule 2 lets custom servers (Turtle WoW, etc.) ship reskinned hearthstone items with different itemIDs and still have them recognized — as long as their on-use is the Hearthstone spell. The return is the actual matched itemID, not a hardcoded constant, so code that wants to do something with the found id (SetItemRef for chat-linking, GetItemInfo for the item's name, etc.) gets the right value for any variant.

Cache dependency for rule 2. The on-use-spell check requires the item's ItemStats_C record to be in the local cache. Items currently in the player's bags are always cached (the engine pre-fills the cache during bag sync), so the check is reliable for this code path. The fallback to rule 1 (itemID equality) covers the moment-of-login window before the cache is fully populated.

Walks bags 0..4 via the same chain C_Container.GetContainerItemID uses internally. Stops on first match.

C_Container.UseHearthstone()

Locates the hearthstone in bags and uses it. Returns true if the hearthstone was found and the use call dispatched, false if no hearthstone is in bags.

used = C_Container.UseHearthstone()
if not C_Container.UseHearthstone() then
    print("No hearthstone in bags!")
end

true doesn't guarantee the cast started. The return reflects "we had a hearthstone to try with and called the engine's UseContainerItem on it." Whether the cast actually starts depends on cooldown, combat, movement, etc. — same downstream rules as calling UseContainerItem(bag, slot) manually. If you need to know whether the hearth completed, listen for the appropriate cast events (SPELLCAST_START / SPELLCAST_STOP).

Internally locates the hearthstone with the same walk PlayerHasHearthstone uses and hands the resulting CGItem * pointer to the engine's by-pointer use primitive at 0x005D8D00 — the same fallback path Script_UseContainerItem ends up at after every special cursor-mode branch (repair vendor, spell-cast targeting, drop-on-bag) is skipped. Bypassing the dispatcher avoids the Lua-stack roundtrip of pushing (bag, slot) only for Script_UseContainerItem to re-parse and re-resolve the same item.

C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)

Atomically swaps two bag slots on the server, no cursor involved. Returns true on send, false for bad args (missing bag, out-of-range slot, empty source).

-- Move slot 3 of bag 1 to slot 7 of the backpack
C_Container.SwapItems(1, 3, 0, 7)

-- Swap two backpack slots
C_Container.SwapItems(0, 1, 0, 2)

Bag IDs match the rest of C_Container.*:

  • 0 — backpack
  • 1..4 — equipped bags
  • -1 — main bank (24 slots; requires the bank window to be open)
  • 5..10 — equipped bank bags (requires the bank window to be open)

Slots are 1-based. The engine accepts both directions:

  • Same container (e.g. backpack ↔ backpack, bag1 ↔ bag1, bank ↔ bank) sends opcode 0x10D CMSG_SWAP_INV_ITEM with the two linear slot bytes.
  • Cross-container (e.g. backpack ↔ bag1, bag2 ↔ bank, bank ↔ bank bag) sends opcode 0x10C CMSG_SWAP_ITEM with (srcBag, srcSlot, dstBag, dstSlot).

Destination empty becomes a move; destination occupied becomes an atomic swap.

Bank slots require the bank window to be open. The engine doesn't sync bank-side CGContainers or bank inventory entries before the first bank interaction, so any bank EncodeBagSlot path returns false until then. Same constraint as C_Container.GetContainerItemInfo on bank IDs.

ClassicAPI-only. There is no other direct swap-two-slots call — addons otherwise drive the cursor with two PickupContainerItem calls in sequence. This single-call form bypasses the cursor entirely (same path C_EquipmentSet.UseEquipmentSet uses for its swap chain).

Send is fire-and-forget. Server confirms via the normal BAG_UPDATE / SMSG_INVENTORY_CHANGE_FAILURE flow; this call does not wait for that round-trip.

C_Container.MoveItem(srcBag, srcSlot, dstBag, dstSlot, count)

Atomic split-and-place: move exactly count items from src to dst, no cursor involvement. Returns true on send, false on bad args (missing bag, out-of-range slot, empty source, count < 1, or count > 255).

-- Split 5 items off backpack slot 1 into backpack slot 2
C_Container.MoveItem(0, 1, 0, 2, 5)

-- Top up a partial stack by moving 3 items from another stack
C_Container.MoveItem(1, 4, 0, 7, 3)

Server semantics are all-or-nothing — there is no partial-move form:

  • dst empty → places count there (clean split).
  • dst holds the same item & dstCount + count ≤ maxStack → merges count into dst.
  • dst holds a different item, or the merge would overflow → server rejects entirely (source untouched, SMSG_INVENTORY_CHANGE_FAILURE fires).

Use this instead of C_Container.SwapItems when you want to consolidate partial stacks — SwapItems swaps the whole slots, which isn't what you want when both slots hold the same item but one has a partial stack.

Bank IDs (-1, 5..10) work here too with the same bank-window constraint as SwapItems.

vs. the cursor approach. There is no other direct one-call move — addons otherwise string SplitContainerItem(bag, slot, count) + PickupContainerItem(dstBag, dstSlot) together to drive the cursor. This bundles the same two-step into one packet (CMSG_SPLIT_ITEM, opcode 0x10E), so the cursor is never touched.

Send is fire-and-forget (same as SwapItems).

C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])

Moves an item and lets the server pick the destination slot. Returns true on send, false for bad args (missing bag, out-of-range slot, empty source, or a source and destination the call cannot serve).

The server does not just look for an empty slot. It first merges the item into existing stacks of the same item, and splits it across several of them when that is what fits. It places a remainder in a free slot only if some is left over. So one call consolidates a partial stack:

-- Two partial stacks of item 12662: slot 1 holds 5, slot 2 holds 3.
C_Container.AutoStoreItem(0, 2)
-- Slot 1 now holds 8, and slot 2 is empty.

-- Confine the item to bag 2
C_Container.AutoStoreItem(0, 5, 2)

dstBag says where you want the item, and defaults to 0:

  • 0 — anywhere in the main inventory that it fits. This means the whole inventory, not the backpack. The backpack cannot be named separately as a destination.
  • 1..4 — that equipped bag only.
  • -1 — anywhere in the bank that it fits.

Deposits and withdrawals both work, and both merge on arrival:

-- Deposit, merging into stacks already in the bank
C_Container.AutoStoreItem(0, 3, -1)

-- Withdraw bank slot 5 back into the bags
C_Container.AutoStoreItem(-1, 5, 0)

A bank item always travels back to your bags, so you cannot ask for a new slot inside the bank. These three combinations are accepted, and every other pair returns false:

Source dstBag Result
inventory 0..4 stays in the inventory
inventory -1 goes to the bank
bank 0 comes back to the inventory

To consolidate stacks inside the bank, use C_Container.MoveItem for each pair. Individual bank bags (5..10) are not valid destinations.

Bank slots need the bank window open, the same as SwapItems and MoveItem.

Prefer this over merging stacks yourself. This call needs no stack-size lookup, because the server decides what fits. A merge you compute in Lua has to size each stack first with C_Item.GetItemMaxStackSizeByID, which returns nil until that item's data has arrived. A merge written that way silently skips any stack it cannot size.

There is no way to name the destination slot, which is the point of the call. Use C_Container.SwapItems or C_Container.MoveItem when you need to choose it.

Send is fire-and-forget (same as SwapItems and MoveItem).

C_Container.SortBags()

Arranges the items in your bags. Takes no arguments and returns nothing.

The work spans several frames, because every item move is a request to the server. A call made while a sort is still running is ignored, so a held keybind cannot stack them up.

Partial stacks are combined first. Then items are placed in this order, starting from the first slot:

  1. Hearthstone
  2. Weapons and armor
  3. Consumables
  4. Reagents
  5. Trade goods
  6. Quest items
  7. Everything else, best quality first
  8. Junk (gray items)

Junk fills from the last slot backward, so it collects away from everything else. Within each group, items sort by type, then subtype, then name, and fuller stacks come first.

Weapons and armor also sort by the slot that they go into. Weapons come first: one-hand and two-hand, then ranged. Armor follows. Shields and off-hand items come first, then head to feet, then neck, rings and trinkets. For example, all helms sit together, and then all shoulders. In one slot, items sort by subtype (cloth, leather, mail, plate), then best quality first, then name.

A specialty bag keeps only what it accepts, so a quiver holds ammunition and nothing else. Items that do not fit a specialty bag go to your general bags.

C_Container.SortBags()

An item that is still loading stays put. Until an item's data arrives from the server, it has no type, quality or name to group it by, and no max stack size. The sort does not move such an item or combine its stacks, and keeps other items out of its slot. Call C_Container.SortBags() again once the data arrives. This is most likely just after you log in.

A locked item stays put. An item on the cursor, in the trade window, or in a move that the server has not answered yet is locked. The sort does not move it, and keeps other items out of its slot. While the sort runs, the items that it moves are locked, the same as an item that you drag by hand.

This order is ClassicAPI's own. To sort a different way, build it from C_Container.SwapItems, C_Container.MoveItem and C_Container.AutoStoreItem.

C_Container.SortBankBags()

Arranges the items in your bank. Uses the same order as C_Container.SortBags(). Takes no arguments and returns nothing.

Open the bank first. The server refuses bank moves while the bank is closed, so a call made with the bank closed does nothing at all.

Only one sort runs at a time. A call made while a bag sort or another bank sort is still running is ignored.

C_Container.SortBankBags()

Items that are still loading or locked stay put, as in C_Container.SortBags().

C_Container.GetSortBagsRightToLeft() / C_Container.SetSortBagsRightToLeft(enable)

Sets which end of your bags a sort fills from. Get returns isEnabled. Set returns nothing, and applies to the next sort rather than re-sorting straight away.

When it is on, the sorted items start at the last slot of the last bag, and junk collects at the first slot of the first bag. Both ends move together, so junk stays at the opposite end from everything else.

C_Container.SetSortBagsRightToLeft(true)
C_Container.SortBags()

The setting is saved, so it survives logging out. It is stored as the sortBagsRightToLeft console variable, which GetCVar also reads.

C_Container.GetBackpackAutosortDisabled() / C_Container.SetBackpackAutosortDisabled(disable)

Keeps the backpack out of C_Container.SortBags(). Get returns whether it is disabled. Set returns nothing, and applies to the next sort rather than re-sorting straight away.

While it is on, a sort leaves every item in the backpack where it is and arranges only the equipped bags.

C_Container.SetBackpackAutosortDisabled(true)
C_Container.SortBags() -- bags 1..4 only

The setting is saved, so it survives logging out. It is stored as the backpackAutosortDisabled console variable, which GetCVar also reads.

C_Container.GetBankAutosortDisabled() / C_Container.SetBankAutosortDisabled(disable)

The same setting for the bank: it keeps the main bank container out of C_Container.SortBankBags(), leaving only the bank bags to be arranged.

C_Container.SetBankAutosortDisabled(true)
C_Container.SortBankBags() -- bank bags only

Stored as the bankAutosortDisabled console variable.

SetBagPortraitTexture(texture, bagIndex) / C_Container.SetBagPortraitTexture(texture, bagIndex)

Sets a texture to the icon of an equipped bag, cut to a round portrait. The two names are the same function.

SetBagPortraitTexture(ContainerFrame1Portrait, 1)   -- the first bag
C_Container.SetBagPortraitTexture(myTexture, 5)     -- the first bank bag
bagIndex Result
1 to 4 The icon of the equipped bag.
5 to 10 The icon of the bank bag. The bank must be open.
0 or less (backpack, bank, keyring) The texture does not change.
11 or more An error.

If the bag slot is empty, the texture is cleared. The error messages spell the name as SetBagPortaitTexture.

C_Container.GetContainerNumSlots(containerIndex) / C_Container.PickupContainerItem(containerIndex, slotIndex)

These are the stock GetContainerNumSlots and PickupContainerItem with their C_Container names. Each pair is the same function.

local numSlots = C_Container.GetContainerNumSlots(Enum.BagIndex.Backpack)  -- 16
C_Container.PickupContainerItem(Enum.BagIndex.Backpack, 1)

ContainerFrame_GetContainerNumSlots(bagID) / NUM_TOTAL_BAG_FRAMES

ContainerFrame_GetContainerNumSlots(bagID) returns two values: maxNumSlots, currentNumSlots. For the keyring, both values are GetKeyRingSize(). For other bags, both values are C_Container.GetContainerNumSlots(bagID).

NUM_TOTAL_BAG_FRAMES is the number of equipped bags (4). The bag indices from Enum.BagIndex.Backpack to NUM_TOTAL_BAG_FRAMES are the backpack and the equipped bags.

Creature

C_CreatureInfo.GetCreatureID(guid)

Returns the creature template / NPC id for a unit GUID, or nil. The id is the same one the game uses to look up the creature's name, and the same one Wowhead and Turtle's database show in their NPC pages.

C_CreatureInfo.GetCreatureID(UnitGUID("target"))   -- 1842 for Hogger
C_CreatureInfo.GetCreatureID(UnitGUID("pet"))      -- pet's creature template ID
C_CreatureInfo.GetCreatureID(UnitGUID("player"))   -- nil (no entry on players)

While the unit is in view, the id is read live from the unit, so it always matches the name you see. Some spawn points pick one of several templates, and a respawn can pick a different one. When such a unit is out of view, the id comes from the GUID instead, and that id is the spawn point's first template, which can differ from the unit's current name.

Accepts creature GUIDs (0xF130xxxx…) and pet GUIDs (0xF140xxxx…). Returns nil for:

  • non-string input or malformed GUIDs
  • player GUIDs — a player carries no template
  • a pet that is out of view — a pet GUID does not carry its template
  • game-object / dynamic-object / corpse / item GUIDs — C_CreatureInfo doesn't surface entry IDs for these even though the bits are in the same range; addons that need them can shift the raw GUID themselves ((guid >> 24) & 0xFFFFFF)
  • entry IDs of 0 — the engine never assigns 0; treated as "no info"

The standard 16-digit ("0xHHHHHHHHLLLLLLLL") and 8-digit ("0xLLLLLLLL") GUID-string forms are both accepted, the same forms the C_PlayerInfo.GUIDIs* functions take.

The creatureID this returns is exactly what C_CreatureInfo.GetCreatureInfoByID takes — GetCreatureInfoByID(GetCreatureID(UnitGUID("target"))) gives the target's cached name/type/family/rank.

C_CreatureInfo.GetCreatureInfoByID(creatureID)

Reads the client-side creature cache (creaturecache.wdb, fed by SMSG_CREATURE_QUERY_RESPONSE) for an NPC/creature by ID and returns a table — or nil if it isn't cached:

local info = C_CreatureInfo.GetCreatureInfoByID(61332)
-- {
--   creatureID = 61332,
--   name       = "Misthoof Stag",
--   subName    = "",     -- title/subtitle ("" if none)
--   type       = 1,      -- CreatureType: 1=Beast, 7=Humanoid, …
--   family     = 0,      -- CreatureFamily
--   rank       = 0,      -- 0=normal, 1=elite, 2=rareelite, 3=worldboss, 4=rare
--   displayID  = 10957,  -- model display ID
-- }

This is a synchronous peek — it returns data only for creatures already cached (seen this session, or loaded from creaturecache.wdb at login), and nil otherwise. Unlike UnitCreatureType/ UnitClassification, which only work on a live unit token, this answers for any creatureID you have cached — even one not currently in the world.

Reads through the engine's generic cache _GetRecord (with no callback, so it's a pure lookup — no network query) and pulls the fields straight off the cached data block. Field offsets verified against the binary (rank read by the engine's classification helper at [block+0x20]) and real creaturecache.wdb rows (Misthoof Stag→type 1, Nordrassil Nymph→type 7 rank 1, Greathorn Hunter→family 26).

For a creature that isn't cached yet, call RequestLoadCreatureByID first and read it once CREATURE_DATA_LOAD_RESULT fires.

C_CreatureInfo.RequestLoadCreatureByID(creatureID)

Asynchronously fetches a creature that isn't in the cache yet — issues SMSG_CREATURE_QUERY and fires CREATURE_DATA_LOAD_RESULT(creatureID, success) when the response lands, at which point GetCreatureInfoByID returns its data. The same shape as C_Item.RequestLoadItemData.

local f = CreateFrame("Frame")
f:RegisterEvent("CREATURE_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()
    -- 1.12 passes event args as globals: arg1 = creatureID, arg2 = success
    if arg2 then
        local info = C_CreatureInfo.GetCreatureInfoByID(arg1)
        -- info.name, info.type, info.rank, …
    end
end)

C_CreatureInfo.RequestLoadCreatureByID(10184)   -- Onyxia → fires the event,
                                                --  then GetCreatureInfoByID works

Returns true if the request was accepted (false only on bad input or a full pending set). If the creature is already cached, the event fires synchronously with success = true. On a query that never resolves it fires once with success = false after a timeout.

Implementation: a shared Cache::QueryLoad dispatcher hooks the engine's generic cache response parser (FUN_00556E20) once and routes by cache instance — the same hook serves the gameobject cache too. The creature/gameobject caches use this generic parser; the item and quest caches have their own, so there's no hook collision. Verified in-game: RequestLoadCreatureByID(10184) on an uncached Onyxia fired the event and populated the cache (type 2 Dragonkin, rank 3 world boss).

C_CreatureInfo.GetRaceInfo(raceID)

Localized race name + non-localized token for a race id, read from ChrRaces.dbc (always loaded, so this works for any id at any time — no unit token required, unlike UnitRace). Returns a RaceInfo table, or nil for a non-numeric / non-positive id or one with no row.

local info = C_CreatureInfo.GetRaceInfo(4)
-- { raceName = "Night Elf", clientFileString = "NightElf", raceID = 4 }
Field Type Notes
raceName string Localized display name (client's active locale).
clientFileString string Non-localized token ("Human", "NightElf", "Scourge") — the same string UnitRace returns as its 2nd value, used for texture paths.
raceID number Echo of the input id.

C_CreatureInfo.GetClassInfo(classID)

Localized class name + locale-independent token for a class id, read from ChrClasses.dbc. Returns a ClassInfo table, or nil for a non-numeric / non-positive id or one with no row.

local info = C_CreatureInfo.GetClassInfo(1)
-- { className = "Warrior", classFile = "WARRIOR", classID = 1 }
Field Type Notes
className string Localized display name (client's active locale).
classFile string Locale-independent token ("WARRIOR", "MAGE") — the same string UnitClass returns as its 2nd value, and the key used for class colors / icon coords.
classID number Echo of the input id.

C_CreatureInfo.GetCreatureFamilyInfo(creatureFamilyID)

Info for a pet/beast family (CreatureFamily.dbc row), read from the always-loaded client DBC. Returns a CreatureFamilyInfo table, or nil for a non-numeric / non-positive / unused id.

local info = C_CreatureInfo.GetCreatureFamilyInfo(27)
-- { id = 27, name = "Wind Serpent",
--   iconFile = "Interface\\Icons\\Ability_Hunter_Pet_WindSerpent" }
Field Type Notes
id number Echo of the input id.
name string Localized family name (client's active locale).
iconFile string Icon texture path. The DBC stores the path, so this is a string usable directly with texture:SetTexture(...). "" for families with no icon (warlock pets: Imp, Voidwalker, Succubus, Felhunter, …).

C_CreatureInfo.GetCreatureFamilyIDs()

Array of every populated CreatureFamily.dbc id, in ascending order. The id space is sparse (unused rows like 10, 13, 14, 22 are skipped), so this is the load-bearing piece for iterating families — each element round-trips with GetCreatureFamilyInfo.

local ids = C_CreatureInfo.GetCreatureFamilyIDs()
-- { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 15, 16, ... }
for _, id in ipairs(ids) do
    local fam = C_CreatureInfo.GetCreatureFamilyInfo(id)
    -- fam.name, fam.iconFile
end

C_CreatureInfo.GetFactionInfo(raceID)

Faction-group info for a race (Alliance or Horde), read from the client DBCs. Returns a FactionInfo table, or nil for a non-numeric / non-positive / unknown race, or a race with no named group.

local info = C_CreatureInfo.GetFactionInfo(1)   -- Human
-- { name = "Alliance", groupTag = "Alliance" }
C_CreatureInfo.GetFactionInfo(2)   -- Orc  → { name = "Horde", groupTag = "Horde" }
Field Type Notes
name string Localized faction-group name ("Alliance", "Horde", or the locale's translation).
groupTag string Locale-independent tag: "Alliance" or "Horde".

This is the by-raceID form of the stock UnitFactionGroup(unit) (which returns groupTag, name for a live unit) — same resolution, no unit required. It mirrors that engine function's chain exactly: race → FactionTemplate group mask → FactionGroup.dbc, selecting the row whose bit the mask sets and whose localized name is non-empty (that non-empty test is how the engine skips the always-present "Player" bit and the nameless "Monster" row to land on Alliance / Horde).

C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)

Info for a creature type (CreatureType.dbc row), read from the always-loaded client DBC. Returns a CreatureTypeInfo table, or nil for a non-numeric / non-positive / unused id.

local info = C_CreatureInfo.GetCreatureTypeInfo(7)
-- { id = 7, name = "Humanoid" }
Field Type Notes
id number Echo of the input id.
name string Localized type name (client's active locale).

The type ids: 1 Beast, 2 Dragonkin, 3 Demon, 4 Elemental, 5 Giant, 6 Undead, 7 Humanoid, 8 Critter, 9 Mechanical, 10 Not specified, 11 Totem. Pairs with UnitCreatureTypeID (id → name) and GetCreatureTypeIDs.

C_CreatureInfo.GetCreatureTypeIDs()

Array of every CreatureType.dbc id (contiguous 1..11), each round-tripping with GetCreatureTypeInfo.

local ids = C_CreatureInfo.GetCreatureTypeIDs()
-- { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }

Currency

GetCoinText(amount [, separator]) / C_CurrencyInfo.GetCoinText(amount [, separator])

Returns the amount of copper as money text, with a word for each coin. Both names call the same function.

GetCoinText(123456)       -- "12 Gold, 34 Silver, 56 Copper"
GetCoinText(123456, " ")  -- "12 Gold 34 Silver 56 Copper"
GetCoinText(5)            -- "5 Copper"
GetCoinText(0)            -- ""
Arg Type Description
amount number The amount of money, in copper.
separator string, optional The text between the coin parts. The default is ", ".

Only the coin parts that are not zero appear, in the order gold, silver, copper. An amount of 0 returns an empty string. If amount is not a number, the function raises the error Usage: GetCoinText(amount [,separator]).

The word for each coin comes from the global strings GOLD_AMOUNT, SILVER_AMOUNT and COPPER_AMOUNT. The embedded !!!ClassicAPI addon gives these strings for each language.

GetCoinTextureString(amount [, fontHeight]) / C_CurrencyInfo.GetCoinTextureString(amount [, fontHeight])

Returns the amount of copper as a money string with real coin icons — for example "12<gold> 34<silver> 56<copper>". The icons are inline |T…|t markup over the UI-MoneyIcons sprite sheet, rendered by ClassicAPI's inline-texture backport. Both names call the same C function.

Only the non-zero denominations appear, in gold → silver → copper order. An amount of 0 returns "0<copper>". A negative or fractional amount is clamped to 0 / rounded.

A space separates the denominations.

fontHeight sets the icon size. The default is 14. Like every size in |T…|t markup, the value is relative to a 14-point font: the coin grows and shrinks with the text of the fontstring that shows it. A fontHeight of 0 makes each coin as tall as that text. Below a UI scale of 1, icons are drawn smaller than the text by the UI scale.

GetCoinTextureString(123456)   -- "12g 34s 56c" with coin icons
GetCoinTextureString(5)        -- "5c" with a copper icon
GetCoinTextureString(5, 28)    -- same, with a coin twice the default size

The per-denomination formats are the GlobalStrings GOLD_AMOUNT_TEXTURE / SILVER_AMOUNT_TEXTURE / COPPER_AMOUNT_TEXTURE, backported in the embedded !!!ClassicAPI addon's locale layer — a locale can override them.

Constants.MoneyFormattingConstants

A table of constants for money text.

Field Value Description
GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER 10 With checkGoldThreshold, GetMoneyString shows no copper for an amount of more than this number of gold.
GetMoneyString(1234567, nil, true)  -- "123g 45s" with coin icons, no copper
GetMoneyString(51234, nil, true)    -- "5g 12s 34c" with coin icons

CVar

GetCVar(name)

Returns the value of the cvar as a string. If no cvar has that name, it returns no values, so the result reads as nil. Use this to find out if a cvar exists:

if not GetCVar("cameraYawE") then return end

If name is not a string or a number, GetCVar raises an error. GetCVar works the same way on the glue screens.

GetCVarDefault(name)

Returns the default value of the cvar as a string. If no cvar has that name, it returns no values. If name is not a string or a number, GetCVarDefault raises an error. GetCVarDefault works the same way on the glue screens.

C_CVar.SetCVar(name [, value])

Sets a cvar and saves it to Config.wtf, as SetCVar does. Returns true when the cvar is set.

C_CVar.SetCVar("SoftTargetFriend", 2)     -- true, GetCVar gives "2"
C_CVar.SetCVar("SoftTargetFriend")        -- true, GetCVar gives "0"
C_CVar.SetCVar("realmList", "x")          -- false: read-only
C_CVar.SetCVar("doesNotExist", 1)         -- nothing
  • A number value is set as its text, so 1.5 becomes "1.5".
  • If value is nil or missing, the cvar gets "0".
  • For a read-only cvar, it returns false and the value does not change.
  • If no cvar has that name, it returns no values, so the result reads as nil.
  • If name is not a string or a number, or value is a boolean or a table, it raises an error.

It also works on the glue screens.

C_CVar.GetCVarInfo(name)

Returns a cvar's value, its default, and what may be done to it. Seven values:

value, defaultValue, isStoredServerAccount, isStoredServerCharacter,
isLockedFromUser, isSecure, isReadOnly = C_CVar.GetCVarInfo(name)
Return Type Meaning
value string the current value
defaultValue string the value it was registered with
isStoredServerAccount boolean always false
isStoredServerCharacter boolean always false
isLockedFromUser boolean always false
isSecure boolean always false
isReadOnly boolean true when SetCVar will refuse to change it
local value, default = C_CVar.GetCVarInfo("gxResolution")
-- "2560x1080", "640x480"

local _, _, _, _, _, _, readOnly = C_CVar.GetCVarInfo("realmList")
-- readOnly is true

isReadOnly is the useful one: it tells you in advance whether SetCVar will work, and realmList, realmName and scriptMemory are read-only. Cvars are stored on your own machine, so the two isStoredServer returns are always false, and no cvar is locked or secure.

Returns nil for a name that is not a cvar, so you can tell "no such cvar" from "set to an empty string". Console commands are not cvars, so a command name returns nil too — C_CVar.GetCVarInfo("help") is nil, while ConsoleGetAllCommands lists both.

Any line left in Config.wtf becomes a cvar the next time the client starts, whether or not a setting by that name exists. The client keeps it, saves it again on exit, and lets you change it from the console, but Lua cannot read it. GetCVar, SetCVar and this function all return nil for one of these, so the Lua cvar functions agree with each other. ConsoleGetAllCommands does list them, because the console can reach them.

Available on the login screen as well as in-game.

C_CVar.DoesCVarExist(name)

Returns whether a cvar by that name can be read and written from Lua. A ClassicAPI extension.

C_CVar.DoesCVarExist("gxResolution")   -- true
C_CVar.DoesCVarExist("doesNotExist")   -- false
C_CVar.DoesCVarExist("help")           -- false: a console command, not a cvar

It answers the question worth asking before you call GetCVar or SetCVar, so it is true exactly when those work:

C_CVar.DoesCVarExist(name) == (C_CVar.GetCVarInfo(name) ~= nil)

That includes the Config.wtf case described above: a name the client keeps but does not implement is reported as false, because no Lua function can read it. Returns false for anything that is not a string.

C_CVar.AreCVarsLoaded()

Returns whether the client has finished loading its settings.

This is always true by the time any addon runs. Settings are read while the client starts, before there is any Lua to ask, and none of them are fetched over the network, so there is nothing to wait for. It is here so that ported code which checks it keeps working.

C_CVar.GetCVarBitfield(name, index) / C_CVar.SetCVarBitfield(name, index, value)

Read or write one bit of a cvar, so a single cvar can hold many separate true/false settings. The index counts from 1.

RegisterCVar("myAddonFlags", "0")

C_CVar.SetCVarBitfield("myAddonFlags", 1, true)
C_CVar.SetCVarBitfield("myAddonFlags", 5, true)
GetCVar("myAddonFlags")                          -- "17", bits 1 and 5
C_CVar.GetCVarBitfield("myAddonFlags", 5)        -- true
C_CVar.GetCVarBitfield("myAddonFlags", 2)        -- false

The index may be 1 to 64. GetCVarBitfield returns nil outside that range, or for a name that is not a cvar, so "no such bit" stays apart from "the bit is false".

SetCVarBitfield returns whether it worked. It returns false for an index outside the range, for a name that is not a cvar, and for a read-only cvar — the same cvars GetCVarInfo reports isReadOnly for. A successful write saves the cvar the same way SetCVar does.

Any cvar works, and a cvar you register yourself is the usual way to use these. Every bit is independent, so setting one leaves the rest alone.

C_CVar.SetTempCVar(name, value) / C_CVar.RemoveTempCVar(name)

SetTempCVar changes a cvar for this session only. The game uses the new value at once, but it does not save the value to Config.wtf. RemoveTempCVar puts back the value that the cvar had before the first SetTempCVar. Neither function returns a value.

-- Saved value of cameraDistanceMax is "15".
C_CVar.SetTempCVar("cameraDistanceMax", "40")
GetCVar("cameraDistanceMax")      -- "40"
-- After /reload or logout, Config.wtf still holds "15".
C_CVar.RemoveTempCVar("cameraDistanceMax")
GetCVar("cameraDistanceMax")      -- "15"

Use this pair for a mode that changes many cvars and later turns off, for example a gamepad mode. The saved settings of the user do not change.

  • If you call SetTempCVar two times on one cvar, RemoveTempCVar puts back the value from before the first call.
  • If SetCVar changes a cvar that has a temp value, the SetCVar value replaces the temp value. The game saves it, and RemoveTempCVar then does not change the cvar.
  • If value is nil (or another value that is not a string or a number), the cvar gets "0", as with SetCVar.
  • Some graphics and sound cvars (for example gxVSync) change only when the game restarts that system. A temp value on such a cvar starts to work at the same time as a SetCVar value would. It is not saved.
  • RemoveTempCVar does nothing for a cvar that has no temp value.

Both functions give the same errors as SetCVar: for a name that is not a cvar, and for a read-only cvar. Both are also available on the login and character-select screens.

C_CVar.GetCVarBool(cvar)

Returns the cvar's value coerced to a boolean, or nil if no cvar by that name is registered. The stock GetCVar returns a string.

C_CVar.GetCVarBool("gxMaximize")    -- true if the window is set fullscreen
C_CVar.GetCVarBool("CombatDamage")  -- false if floating damage text is off
C_CVar.GetCVarBool("doesNotExist")  -- nil — distinguishable from false

Coercion rules (only applied when the cvar exists):

Cvar string value Result
"1" or any non-zero numeric ("42", "-3", …) true
"true" (case-insensitive) true
"0" or empty false
Non-numeric, non-"true" ("on", "yes") false
Cvar name not registered nil

Implementation reads the cvar registry directly (engine's FUN_FIND_CVAR at 0x0063DEC0), bypassing GetCVar's Lua-error path for unknown cvars. The nil return lets callers distinguish "the cvar exists and is falsy" from "no such cvar."

Glue-state availability

C_CVar.GetCVarBool is also registered on the glue Lua state, and the engine's stock GetCVar / SetCVar / RegisterCVar / GetCVarDefault — which are otherwise exposed only in-game — are mirrored onto glue too. GlueXML patches at the login / realm / char-select screens can now read and write CVars directly. CVar storage is process-global, so writes on either state are immediately visible on the other (a SetCVar("foo", "1") from a GlueXML script will read back as "1" on the in-world side, and vice versa).

The interface memory limit

The scriptMemory cvar caps how much memory the interface may use, counted in kilobytes. Its own value is 49152, which is 48 MB.

Stage What happens
Interface memory goes over the limit The client frees what it can.
It is still over A popup asks you to disable add-ons and restart.
15 seconds later The client closes.

A value of 0 turns the check off. ClassicAPI sets 0 for you, so a large add-on set does not meet the popup, and it does that only while the cvar still holds its own value. If you picked a value yourself, your value stands.

SetCVar cannot change this cvar while you are in the world, because the client makes it read-only there. To pick your own limit, close the game and add the line yourself in WTF\Config.wtf:

SET scriptMemory "65536"

That asks for a 64 MB limit, and any value above 0 turns the check back on.

Cursor

GetCursorInfo()

Returns a tuple describing whatever the player has picked up on the cursor (item, money, spell, macro, merchant slot, pet), or nothing when the cursor is empty. Reads the same engine globals CursorHasItem / CursorHasSpell / GetCursorMoney consult, so the answer always matches what the rest of the engine sees.

Return shape per cursor state:

Cursor holds Returns
nothing nil
a bag item (PickupContainerItem, mail/trade detach with GUID) "item", itemID, itemLink
a drag-source item (action bar, auction slot) "item", itemID, itemLink
money (gold-split UI, PickupPlayerMoney) "money", copperAmount
a spell from the spellbook "spell", spellbookSlot, bookType, spellID
a macro "macro", macroIndex
a merchant slot "merchant", merchantIndex
a pet action "petaction"
a pet dragged in the stable "pet", stableIndex
an equipment set "equipmentset", setName

bookType is "spell" for player spells, "pet" for pet spells. spellbookSlot is the 1-based slot the spell lives at in the spellbook arrays, found by walking the player/pet books. 0 if the spellID isn't in either book (rare — passive talent-granted spells).

local kind, a, b, c = GetCursorInfo()
if kind == "item" then
    print(("cursor item: %d"):format(a))
elseif kind == "money" then
    print(("cursor money: %d copper"):format(a))
elseif kind == "spell" then
    print(("cursor spell: id=%d slot=%d (%s book)"):format(c, a, b))
elseif kind == "macro" then
    print(("cursor macro index: %d"):format(a))
end

The link always matches the item on the cursor.

  • A bag item keeps its identity on the cursor, so the link is the full form for that exact item, with its enchant and random suffix:
|cff1eff00|Hitem:16791:255:0:0|h[Silkstream Cuffs]|h|r
|cff1eff00|Hitem:9877:0:767:0|h[Sorcerer Cloak of the Owl]|h|r
  • A drag-source item (action bar, auction slot) keeps only its item ID on the cursor. The link is the plain link for that ID, without an enchant or a suffix. The client does not know which copy you mean, and the call does not guess.

If the item is not in the item cache, no name is available. In that case a drag-source item returns only "item" and itemID.

stableIndex

The 1-based slot of the pet in the stable, as ClickStablePet takes it.

Types we do not return

Some engine cursor types return nothing, for example a trade slot or mail attachment. These carry no object that GetCursorInfo can describe. Consumers can use CursorHasItem, CursorHasSpell and CursorHasMoney.

Changes to what the cursor holds are reported by the CURSOR_CHANGED event.

CursorHasMacro()

Returns true if the cursor holds a macro, and false if it does not.

DeathInfo

C_DeathInfo.GetSelfResurrectOptions()

options = C_DeathInfo.GetSelfResurrectOptions()

Returns an array of the ways that the dead player can resurrect themselves. Each entry is a SelfResurrectOption table:

Field Type Description
name string The name of the spell or the item.
optionType number Enum.SelfResurrectOptionType.Spell (0) or Enum.SelfResurrectOptionType.Item (1).
id number The spell ID or the item ID.
canUse boolean Always true.
isLimited boolean Always false.
priority number Always 0.

The array has one entry at most. A self-resurrection spell, such as Soulstone Resurrection or Reincarnation, comes before an item. The array is empty when the player is alive or a ghost, or has no option. The function returns nothing before the player enters the world.

local options = C_DeathInfo.GetSelfResurrectOptions()
if options and options[1] then
    print("You can resurrect with", options[1].name)
end

SELF_RES_SPELL_CHANGED fires when the self-resurrection spell changes.

C_DeathInfo.UseSelfResurrectOption(optionType, id)

Resurrects the dead player with an option from C_DeathInfo.GetSelfResurrectOptions. optionType and id must match an entry that it returns now. If they do not match, the call does nothing. An item option uses the item, and this stops a spell cast in progress.

local options = C_DeathInfo.GetSelfResurrectOptions()
local option = options and options[1]
if option then
    C_DeathInfo.UseSelfResurrectOption(option.optionType, option.id)
end

DurationUtil

C_DurationUtil.CreateDuration()

Returns a new duration object. A duration object holds a time span: a start time and a total duration. All times are GetTime() seconds. A new duration object holds a zero time span that starts at 0.

local duration = C_DurationUtil.CreateDuration()
duration:SetTimeFromStart(GetTime(), 2.5)
print(duration:GetRemainingDuration())  -- 2.5

These methods set the time span:

Method Time span
SetTimeFromStart(startTime, duration [, modRate]) Starts at startTime and lasts duration seconds.
SetTimeFromEnd(endTime, duration [, modRate]) Ends at endTime and lasts duration seconds.
SetTimeSpan(startTime, endTime) Starts at startTime and ends at endTime. If endTime is before startTime, the time span is zero and starts at startTime.
Reset() / SetToDefaults() Zero, and starts at 0.
Assign(other) The same as the time span of the duration object other.

These methods read the time span. Each one takes an optional modifier argument (see below):

Method Returns
GetStartTime() The start time.
GetEndTime() The start time plus the total duration.
GetTotalDuration() The total duration, in seconds.
GetElapsedDuration() The seconds from the start time to now, from 0 to the total duration. Before the start time, it is 0.
GetRemainingDuration() The total duration minus the elapsed duration. Before the start time, it is the total duration.
GetElapsedPercent() / GetRemainingPercent() The elapsed or remaining duration as a part of the total duration, from 0 to 1. A zero time span gives 1 elapsed and 0 remaining.
HasStarted() true when now is at or after the start time.
HasExpired() true when now is at or after the end time.
IsActive() true from the start time until the end time.

These methods take no arguments:

Method Returns
IsZero() true when the total duration is 0.
GetModRate() The mod rate of the time span. The default is 1.
Copy() A new duration object with the same time span. A change to one of the two objects does not change the other.
GetClockTime() The current time, as GetTime() gives it.
GetClock() Always nil.
HasSecretValues() Always false.
  • A new or reset duration object has started and has expired.
  • modRate must be more than 0. It changes only the results for Enum.DurationTimeModifier.BaseTime.
  • modifier is a value of Enum.DurationTimeModifier. RealTime (0) is the default. With BaseTime (1), the total duration is the total duration divided by the mod rate, from the same start time. For example, 1 second into a 4-second time span with a mod rate of 2, the remaining duration is 1 second of a 2-second total.
  • A function that returns a duration object returns a new Lua object on each call. Two of these Lua objects can hold the same time span. For example, statusBar:GetTimerDuration returns a new Lua object for the time span that you gave to statusBar:SetTimerDuration. A change made through one of them shows in the other.
  • A missing or wrong argument is an error.
  • FormatElapsedDuration(formatter [, modifier]), FormatRemainingDuration(formatter [, modifier]) and FormatTotalDuration(formatter [, modifier]) return the text that a NumericRuleFormatter makes from the duration.
  • The Evaluate… methods and SetClock are not available.

C_DurationUtil.CreateDurationTextBinding()

Returns a new text binding. A text binding writes text into a font string on each frame, from the time span of a duration object. Use it for a countdown text. Your code does not call SetText on each frame.

local formatter = C_StringUtil.CreateNumericRuleFormatter()
formatter:AddBreakpoint({ threshold = 0, format = "%.1f" })

local binding = C_DurationUtil.CreateDurationTextBinding()
binding:SetFontString(castBar.timeText)
binding:SetDuration(UnitCastingDuration("player"))
binding:SetFormatter(formatter)
binding:SetEnabled(true)

A new text binding is disabled. It writes text only when it is enabled and it has a font string and a format. These methods configure it:

Method Result
SetFontString(fontString) / GetFontString() The font string that gets the text.
SetDuration(duration) / GetDuration() The duration object. The binding keeps the object, not a copy.
SetFormatter(formatter) The text is the text of formatter for the remaining seconds.
SetTextFormat(formatString, components) The text is formatString, with each {} replaced by the text of one component, in order.
SetExpiredText([text]) / GetExpiredText() The text after the end time. nil removes it.
SetZeroDurationText([text]) / GetZeroDurationText() The text for a zero time span, or when the binding has no duration object. nil removes it.
SetUpdateInterval(seconds) / GetUpdateInterval() The minimum time between two updates. 0, the default, updates on each frame.
SetTimeModifier(modifier) / GetTimeModifier() The Enum.DurationTimeModifier that the binding reads the time span with. The default is RealTime.
SetEnabled(enabled), Enable(), Disable() / IsEnabled() Starts or stops the updates.

Each component of SetTextFormat is a table { property = property, formatter = formatter }. property is a value of Enum.DurationTextBindingProperty: RemainingDuration (0), RemainingPercent (1), ElapsedDuration (2), ElapsedPercent (3), TotalDuration (4), StartTime (5) or EndTime (6). formatter is a NumericRuleFormatter.

binding:SetTextFormat("{} / {}", {
    { property = Enum.DurationTextBindingProperty.RemainingDuration, formatter = formatter },
    { property = Enum.DurationTextBindingProperty.TotalDuration, formatter = formatter },
})

Other methods:

Method Result
GetFormattedText() The text that the binding writes now, or "" when it has no format.
UpdateFontString() Writes the text now.
CanFormatText() true when the binding has a format.
CanUpdateFontString() true when the binding has a format and a font string.
Copy() A new binding with the same configuration. The copy is disabled.
Assign(other) Copies the configuration of the binding other. The enabled state does not change.
SetToDefaults() Removes the configuration and disables the binding.
HasSecretValues() Always false.
  • An enabled binding continues to update when your code has no reference to it. To stop it, disable it.
  • When you enable a binding, it writes the text immediately.
  • The binding writes the text only when the text changes.
  • After the end time, the binding shows the expired text, if it has one. The binding stays enabled.
  • A zero time span shows the zero duration text before the expired text.
  • When the font string goes away with its frame, the binding has no font string. It then writes nothing.
  • A {} with no component writes nothing.
  • The color curve methods are not available.

EquipmentSet

C_EquipmentSet.* — named sets of gear you can save and equip again with one call.

Sets belong to one character. Each character keeps its own sets in a file on disk, so a set does not follow you to another character, and a copy of your WTF folder carries its sets with it.

Overview & file format

Sets are stored in WTF\Account\<account>\<realm>\<character>\ClassicAPI_EquipmentSets.txt in a line-oriented, human-readable format:

# ClassicAPI Equipment Sets v1
set 1
  name=Tanking
  icon=INV_Shield_06
  slot 1 guid=0x0000000040000123
  slot 2 ignored
  slot 5 guid=0x0000000040000789
set 2
  name=Healing
  icon=Spell_Holy_HolyBolt
  ...

Identity is by item GUID, saved at CreateEquipmentSet / SaveEquipmentSet time. A set remembers the exact items you wore, not the kind of item. UseEquipmentSet finds those items in your bags and moves each one that is not already in its slot.

Items in the bank count only while the bank window is open. Then UseEquipmentSet can equip them directly from the bank. When the bank is closed, a set item in the bank counts as missing.

We accept icon path strings (e.g. "INV_Shield_06"), since there is no fileDataID system here. Missing or unknown icons fall back to a default.

No cap on the number of sets per character. The full list re- serializes on every mutation; a corrupted file is harmless (parse errors leave the in-memory list empty and the next save rewrites the file from scratch).

Note: this is a fresh client-side namespace. It mirrors the C_EquipmentSet.* shape where it can, but anything that requires server-side state (cross-character sharing, the "Equipment Manager" specialization tab) isn't supported.

C_EquipmentSet.CanUseEquipmentSets()

Returns false while a swap from UseEquipmentSet is in progress, and true at all other times.

C_EquipmentSet.GetNumEquipmentSets()

Returns the count of sets stored for the current character. Loads the file on first call after login.

C_EquipmentSet.GetEquipmentSetIDs()

Returns a numeric-keyed table of every setID in storage order (insertion-order; not alphabetical). Empty table when nothing's saved.

C_EquipmentSet.GetEquipmentSetID(name)

Returns the numeric setID for a set with the given name, or nil if no set by that name exists. Names are exact (case-sensitive, no trimming).

C_EquipmentSet.GetEquipmentSetInfo(setID)

Returns nine values:

name, icon, setID, isEquipped,
numItems, numEquipped, numInInventory, numMissing, numIgnored

Each slot of the set that holds an item counts one time:

  • numEquipped — you wear the item, in any equipment slot.
  • numInInventory — the item is in your bags, or in the bank while the bank window is open.
  • numMissing — the item is in none of these places.
  • numIgnored — the slot is ignored.

numItems is numEquipped + numInInventory + numMissing. Slots that the set saved as empty are not counted.

isEquipped is true only when every slot that is not ignored holds exactly what the set saved. A slot that the set saved as empty must be empty. A missing item makes isEquipped false.

Returns nothing for an unknown setID.

C_EquipmentSet.GetIgnoredSlots(setID)

Returns a numeric-keyed table of slot indices (1..19) that the set has flagged ignored. Empty table when no slots are ignored. Ignored slots are recorded per-set at save time, by reading the global IgnoreSlotForSave state — not retroactively editable on a saved set.

C_EquipmentSet.GetItemIDs(setID)

Returns a table { [slot] = value } with an entry for each of the 19 slots:

  • 0 — the set saved the slot as empty.
  • 1 — the slot is ignored.
  • the itemID — for an item slot. The set saves the itemID with the item, so you get it also when the item is gone.
  • -1 — the itemID is not known. This occurs only for sets that were saved before ClassicAPI saved itemIDs.

C_EquipmentSet.GetItemLocations(setID)

Returns a table { [slot] = locationCode } with an entry for each of the 19 slots. Location codes use the same bit-packed encoding Blizzard's FrameXML EquipmentManager_UnpackLocation decodes (constants in Blizzard_FrameXMLBase/Classic/Constants.lua):

Bit/field Meaning
0x00100000 (PLAYER) Item is in a player slot (equipped or in a player bag)
0x00200000 (BAGS) Item is inside a bag (player or bank)
0x00400000 (BANK) Item is in the bank (main or bank bag)
bits 0..7 Slot. Inside a bag, the 1-based slot in that bag. Otherwise, the inventory slot ID
bits 8..15 Bag index — present only when BAGS bit is set

PLAYER and BANK are mutually exclusive in the encoding (the unpack uses if player elseif bank). Bank bags subtract 4 from the bagID before storing so the field fits cleanly. Unpack reverses this to give back bag IDs 5..10. Composition:

Location Bits Example
Equipped (paperdoll slot 1..19) `PLAYER slot`
Backpack / player bag 1..4 `PLAYER BAGS
Main bank slot 1..24 `BANK invSlot`
Bank bag 5..10 `BANK BAGS

A main-bank code holds the inventory slot ID, not the bank slot number. This is the value that BankButtonIDToInvSlotID(bankSlot) returns: bank slot N is inventory slot 39 + N. A location without the BAGS bit therefore goes straight to GetInventoryItemLink("player", slot), the same way the Blizzard helper EquipmentManager_GetItemInfoByLocation reads it.

Special values returned in the table entry:

  • 0 — the set saved the slot as empty
  • 1 — slot is ignored (GetIgnoredSlots lists these)
  • -1 — item is missing (was in the set, can't find now)

Bank locations are returned only while the bank window is open. When the bank is closed, an item in the bank is -1.

The packed codes pass cleanly through Blizzard's EquipmentManager_UnpackLocation if you want to use the shared-FrameXML helpers in your addon UI.

C_EquipmentSet.CreateEquipmentSet(name [, icon])

Snapshots the player's currently-equipped items into a new set and returns its setID. Honors IgnoreSlotForSave — slots flagged ignored at call time get the ignored marker instead of the equipped item's GUID.

Returns the new setID on success. Returns nothing if the name is empty or already in use.

The name must be valid UTF-8, have no line break, and be shorter than 32 characters. If not, the call raises the error Invalid string for setName. The | character is removed from the name.

icon defaults to "INV_Misc_QuestionMark" if omitted.

C_EquipmentSet.SaveEquipmentSet(setID [, icon])

Overwrites an existing set's contents with the player's currently- equipped items. Same ignored-slot handling as CreateEquipmentSet. If icon is provided it replaces the set's previous icon; otherwise the icon is left unchanged.

C_EquipmentSet.ModifyEquipmentSet(setID, newName)

Renames an existing set. Fails silently if the new name is empty, already in use by a different set, or the setID doesn't exist. The new name follows the same rules as in CreateEquipmentSet.

C_EquipmentSet.DeleteEquipmentSet(setID)

Removes the set. SetIDs are not reused — the next Create call allocates one higher than any seen.

C_EquipmentSet.IgnoreSlotForSave(slot) / UnignoreSlotForSave / IsSlotIgnoredForSave / ClearIgnoredSlotsForSave

Global "skip this slot the next time CreateEquipmentSet or SaveEquipmentSet runs" state, indexed by 1-based slot (1..19). Persists for the rest of the session; not written to the WTF file. Use it when building a set that should leave (say) the tabard slot free — set the ignore flag, then call CreateEquipmentSet, then optionally ClearIgnoredSlotsForSave() afterward.

C_EquipmentSet.EquipmentSetContainsLockedItems(setID)

Returns true if any item in the set is currently flagged "locked" by the engine — a pending pickup or use is in flight that UseEquipmentSet would race with. Reads bit 2 (0x04) of ITEM_FIELD_FLAGS for each resolvable item in the set.

C_EquipmentSet.UseEquipmentSet(setID)

Equips the set. Each item that is not in its slot moves with one server-side swap. Slots that the set saved as empty are emptied into free bag slots. The cursor is not used.

Returns true when the swap starts. Returns false when the setID is unknown, or when the swap cannot start. The result of the swap comes later, in EQUIPMENT_SWAP_FINISHED.

The swap does not start when another swap from UseEquipmentSet is in progress, or when the cursor holds an item. Then the error frame shows ERR_CLIENT_LOCKED_OUT ("You can't do that right now.").

Rules for each slot:

  • In combat, only the main hand, off hand, and ranged slots change. If a different slot had to change, the error frame shows ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S one time.
  • A missing item is skipped. The error frame shows ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S one time.
  • Items in the bank are equipped only while the bank window is open.
  • No free bag slot for an item that the set takes off: that slot keeps its item. When the swap ends, EQUIPMENT_SWAP_FINISHED reports a failure, and the error frame shows ERR_EQUIPMENT_MANAGER_BAGS_FULL.

CURSOR_UPDATE fires one time for each item that moves. The game's swap does a cursor cleanup after each move, also when the cursor did not change. Addons that react to CURSOR_UPDATE should check CursorHasItem() before they do work.

C_EquipmentSet.PickupEquipmentSet(setID)

Puts the set on the cursor. Drop it on an action button to make an equipment-set button. An unknown setID does nothing.

An equipment-set button shows the icon and the name of the set. When the player uses the button, WEAR_EQUIPMENT_SET fires, and the built-in handler equips the set. The button is grayed out while a swap is in progress, while an item of the set is locked, and while an item that the set would replace is locked. Its tooltip is the GameTooltip:SetEquipmentSet tooltip.

The button works like other action buttons. Drag it off to put the set on the cursor again. Drop another action on it, and the set goes to the cursor.

The game saves the button for each character. It comes back after the next login. If another client changes that action slot, the change of the other client is kept.

Changes to the set go to its buttons:

  • ModifyEquipmentSet and SaveEquipmentSet update the name and the icon.
  • DeleteEquipmentSet removes all buttons of the set.

For an equipment-set button, GetActionInfo(slot) returns "equipmentset", setName, and GetActionText(slot) returns the set name. While the set is on the cursor, GetCursorInfo() returns "equipmentset", setName, and CURSOR_CHANGED reports Enum.UICursorType.EquipmentSet.

Events

C_EventUtils.IsEventValid(eventName)

Returns true if the named event exists in the engine's event-name table and can be registered for, false otherwise. Useful for addons that gate code behind feature detection (e.g. registering for events that exist in some client builds but not others).

if C_EventUtils.IsEventValid("PLAYER_LOGIN") then ... end       -- true
if C_EventUtils.IsEventValid("COMBAT_LOG_EVENT") then ... end   -- false (not a registered event)

The check walks the engine's static event-name table at runtime, so it sees events added by any loaded DLL that injects names by overwriting .rdata strings in place — for example, on a Turtle WoW client running SuperWoWhook, IsEventValid("UNIT_CASTEVENT") returns true because SuperWoWhook patches the binary's event names with its own. This matches what frame:RegisterEvent will actually accept, which is what addon code needs to know.

GetFramesRegisteredForEvent(event)

Returns the frames currently registered for event, as multiple return values (not a table). Returns nothing when no frame is registered or the event name is unknown.

local f1, f2 = GetFramesRegisteredForEvent("PLAYER_LOGIN")
for i = 1, select("#", GetFramesRegisteredForEvent("BAG_UPDATE")) do
    local frame = select(i, GetFramesRegisteredForEvent("BAG_UPDATE"))
    print(frame:GetName())
end

Reads the engine's own subscriber chain — the exact list frame:RegisterEvent appends to — so it stays in sync with real registrations, including our AutoReserve-backed custom events. Frames created purely in C++ (nameplates, etc.) come back as their canonical wrapper, so :GetName() and other methods work; an anonymous frame's :GetName() is nil.

PLAYER_ENTERING_WORLD payload (isInitialLogin, isReloadingUi)

Adds a payload to the otherwise arg-less PLAYER_ENTERING_WORLD:

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:SetScript("OnEvent", function()
    local isInitialLogin, isReloadingUi = arg1, arg2
    if isInitialLogin then       -- fresh character login (from char-select)
    elseif isReloadingUi then    -- /reload
    else                          -- zone / instance transition
    end
end)
  • isInitialLogin — true on a fresh login (including logging out to char-select and back in), nil otherwise.
  • isReloadingUi — true when the fire is caused by a /reload, nil otherwise.
  • Both nil on an instance/zone transition.

Delivered as real event args (arg1/arg2), so positional handlers work unchanged. Values are true→1 / false→nil (the engine dispatcher has no boolean), so if isInitialLogin then … reads naturally. Existing handlers that ignore the args are unaffected.

PEW is a no-arg event, so the engine broadcasts it through the FUN_FIRE_EVENT_NO_ARGS dispatcher; we intercept that (via the shared Event::SignalHook) and re-broadcast through the variadic dispatcher with the payload. The flags are derived from whether the in-game Lua state was just (re)built (login/reload) and whether we just came through the glue/login screen — no PLAYER_ENTERING_WORLD-specific engine hook.

CHAT_MSG_* arguments lineID (arg11) and guid (arg12)

Every CHAT_MSG_* event has two more arguments after the usual ten:

  • arg11 — lineID. A number that identifies the message. Each message gets the next number, and all frames that get the message see the same number. Text emotes (CHAT_MSG_TEXT_EMOTE) always have 0.
  • arg12 — guid. The sender's GUID, in the same format as UnitGUID ("0xHHHHHHHHLLLLLLLL"). It is an empty string "" when the sender is not a player: monster chat, system messages and channel notices. It is also "" for messages from a GM.
local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_SAY")
f:RegisterEvent("CHAT_MSG_WHISPER")
f:SetScript("OnEvent", function()
    local lineID, guid = arg11, arg12
    if guid ~= "" then
        local _, class = GetPlayerInfoByGUID(guid)
        print(arg2 .. " is a " .. (class or "?"))
    end
end)

Use lineID when several handlers see the same message and you want to act on it only one time.

PLAYER_TOTEM_UPDATE event

Fires with arg1 = totemSlot (1 Fire, 2 Earth, 3 Water, 4 Air) when the state of a totem slot changes. This is the companion event to GetTotemInfo.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_TOTEM_UPDATE")
f:SetScript("OnEvent", function()
    local slot = arg1
    local have, name, start, duration, icon = GetTotemInfo(slot)
    -- read the changed slot again
end)

The event fires in these cases:

  • You drop a totem into the slot.
  • The client finds the totem creature, or the name of the totem changes. Until then, GetTotemInfo returns nil for the name.
  • The totem goes away: it dies, it expires, you recall it, or it moves out of view.

A drop fires on the next frame. Other changes can come up to 250 ms later.

BAG_UPDATE_DELAYED event

Fires (with no payload) once per frame in which any BAG_UPDATE fired. Register for BAG_UPDATE_DELAYED instead of BAG_UPDATE and rescan once per frame regardless of how many updates fired.

local f = CreateFrame("Frame")
f:RegisterEvent("BAG_UPDATE_DELAYED")
f:SetScript("OnEvent", function() RescanMyInventory() end)

A trade with 6 stacks (12 BAG_UPDATEs, all processed in one frame) produces exactly 1 BAG_UPDATE_DELAYED at the end of the frame.

Implemented by three hooks, none in regions other DLLs touch:

  • FUN_004F91A0 / FUN_004F9370 (bag subsystem at 0x004F9xxx) — each just sets a g_pending flag, no fire
  • FUN_0066FD50 (engine's per-frame world-subsystem update at 0x0066xxxx, deep in the rendering pipeline, only one caller in the entire binary) — drains the flag at the tail of the world tick

The world-tick hook is a tail hook (run original first, then fire DELAYED), so the fire happens after every other per-frame world work completes. Addon callbacks see DELAYED after their normal event handlers.

Coverage limitation. The keyring BAG_UPDATE(-2) path goes through a separate function (FUN_004F8DB0) that uses __thiscall with an awkward register-arg shape. Keyring updates currently won't trigger BAG_UPDATE_DELAYED. Player bag (0..4) and bank (5..10) updates work normally — the 95% case.

PLAYER_EQUIPMENT_CHANGED event

Fires once per paperdoll slot change — equip, unequip, or swap:

PLAYER_EQUIPMENT_CHANGED: equipmentSlot, hasCurrent
  • equipmentSlot (number) — the 1-based inventory slot that changed (1 = head … 19 = tabard; same numbering as GetInventoryItemLink("player", slot)).
  • hasCurrent — 1 when the slot now holds an item, nil when it's now empty (write if hasCurrent then — the event dispatcher can't push real booleans).

The stock UNIT_INVENTORY_CHANGED("player") doesn't say which slot changed; this event lets gear trackers, stat sheets, and tooltip decorators refresh exactly one slot instead of rescanning all 19.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
f:SetScript("OnEvent", function()
    print("slot", arg1, arg2 and "equipped" or "now empty")
end)

Fully event-driven — no polling. The DLL registers change observers for the 19 equipment GUID fields of the player descriptor through the engine's own field-observer system (the same mechanism the engine uses to watch bag slots), so the event fires exactly when the server's update packet writes a new item GUID into a slot, once per changed slot. Swapping two items (e.g. weapon ⇄ bag) fires once per affected equipment slot.

UPDATE_INVENTORY_DURABILITY event

Fires with no payload when the durability of an item changes. Examples are combat damage, the resurrection penalty, and repairs. To update your display, read each slot again with GetInventoryItemDurability.

local f = CreateFrame("Frame")
f:RegisterEvent("UPDATE_INVENTORY_DURABILITY")
f:SetScript("OnEvent", RefreshMyDurabilityHUD)

The event also fires at login, when your equipment or the equipment of a player near you changes, when an item breaks or is enchanted, and when you call UpdateInventoryAlertStatus(). It does not tell you what changed, and one action can make it fire more than one time. For example, a death fires it once for each damaged item. Keep the handler fast.

PLAYER_AVG_ITEM_LEVEL_READY event

Fires (with no payload) when the item data that GetAverageItemLevel requested has loaded. The next call to GetAverageItemLevel returns the new result.

If an item does not load, the event also fires, and the next call requests the item again.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_AVG_ITEM_LEVEL_READY")
f:SetScript("OnEvent", function()
    local overall, equipped = GetAverageItemLevel()
    print(overall, equipped)
end)

HEARTHSTONE_BOUND event

Fires (with no payload) every time the player binds their hearthstone at an innkeeper — including a rebind at the same inn. Addons listen to HEARTHSTONE_BOUND and re-read GetBindLocation() to refresh whatever bind-location UI they show.

It never fires on login or on a map change.

local f = CreateFrame("Frame")
f:RegisterEvent("HEARTHSTONE_BOUND")
f:SetScript("OnEvent", function()
    local newLocation = GetBindLocation()
    -- update UI...
end)

Fires every time the player confirms a bind, including when the new bind is at the same inn as before. The event is driven by the bind ACTION (server's SMSG_BINDPOINTUPDATE), not by the area name changing.

Implemented by hooking the BINDPOINTUPDATE packet handler at FUN_005ED3C0. The handler runs in two distinct cases:

  1. Initial post-login sync — the engine has just zeroed the "bind valid" flag during character-entry init (FUN_005E2510), and this packet repopulates it. We detect this by reading the flag before the original handler runs; if it's 0, this is the sync and we suppress the event.
  2. Innkeeper rebind — the flag is already 1. Fire.

Character switch is handled automatically: the character-entry init re-zeroes the flag, so each new character's first update is also treated as sync and suppressed.

DYNAMIC_GOSSIP_POI_UPDATED event

Fires (with no payload) when a gossip point of interest is added to the map or removed from it. When it fires, read the marker again with C_GossipInfo.GetPoiForUiMapID.

Player input-state events

Three pairs of zero-payload events fired from a single per-frame Tick::WorldTick callback that reads input state once and edge- detects each pair.

Event Fires on Source
PLAYER_STARTED_MOVING / PLAYER_STOPPED_MOVING WASD or autorun toggle UI-input controller flags at *0x00BE1148 + 0x04 (0x10 W | 0x20 S | 0x40 A | 0x80 D | 0x1000 autorun)
PLAYER_STARTED_TURNING / PLAYER_STOPPED_TURNING Mouselook bit held AND character body yaw is changing Mouselook bit 0x01 + per-frame delta on CGPlayer + 0x9C4 (body yaw, radians)
PLAYER_STARTED_LOOKING / PLAYER_STOPPED_LOOKING Free-look bit held AND camera-relative yaw is changing Free-look bit 0x02 + per-frame delta on [*0x00B4B2BC + 0x65B8] + 0xF0 (camera yaw, radians)
local f = CreateFrame("Frame")
for _, ev in ipairs({
    "PLAYER_STARTED_MOVING", "PLAYER_STOPPED_MOVING",
    "PLAYER_STARTED_LOOKING", "PLAYER_STOPPED_LOOKING",
    "PLAYER_STARTED_TURNING", "PLAYER_STOPPED_TURNING",
}) do
    f:RegisterEvent(ev)
end
f:SetScript("OnEvent", function(_, event) print(event) end)

Key-state semantics for MOVING: STOPPED_MOVING fires the instant the movement keys release, even if the character is still airborne from a jump (verified empirically). For an "is the character actually displacing this frame" signal, use GetUnitSpeed("player") and watch the first return.

Latched semantics for TURNING and LOOKING: each pair fires exactly once per RMB / LMB hold. STARTED waits until both the mouse-button bit is held AND the relevant yaw has actually changed ("camera has moved" semantics — clicking without dragging doesn't fire). STOPPED fires when the button bit clears (RMB / LMB release). The latch stays on through any drag- stop-drag motion within a single hold; no spurious flapping.

The camera-relative yaw at [camera + 0xF0] stays at 0 during RMB-mouselook (camera rotates with the character, so its offset from the character doesn't change), so TURNING and LOOKING are cleanly separable: RMB only fires TURNING, LMB only fires LOOKING.

Implementation: single subscriber on the shared Tick::WorldTick registry (same MinHook on FUN_0066FD50 shared with BAG_UPDATE_DELAYED). When any of the input controller / player / camera pointers is null (pre-login, loading screen, character select), all states reset so the next valid tick doesn't fire spurious STOPPED transitions.

GLOBAL_MOUSE_DOWN / GLOBAL_MOUSE_UP events

GLOBAL_MOUSE_DOWN fires when you press a mouse button, and GLOBAL_MOUSE_UP fires when you release one. The events fire when a frame takes the click, and when the click is not on a frame. The payload is the name of the button.

local f = CreateFrame("Frame")
f:RegisterEvent("GLOBAL_MOUSE_DOWN")
f:RegisterEvent("GLOBAL_MOUSE_UP")
f:SetScript("OnEvent", function()
    print(event, arg1)  -- "GLOBAL_MOUSE_DOWN", "LeftButton"
end)
Payload Button
"LeftButton" Left button
"RightButton" Right button
"MiddleButton" Middle button
"Button4" First side button
"Button5" Second side button

Behavior notes:

  • The events fire before the frame under the cursor gets its OnMouseDown or OnMouseUp.
  • If the game window loses the mouse while you hold a button, GLOBAL_MOUSE_UP fires for each held button.
  • Clicks during a loading screen or a screen fade do not fire the events. Clicks that the open console takes also do not fire them.

ACTION_RANGE_CHECK_UPDATE event

Fires when an action-bar slot that has EnableActionRangeCheck on goes into or out of range of its target.

ACTION_RANGE_CHECK_UPDATE: slot, isInRange, checksRange
  • slot (number) — the 1-based action-bar slot.
  • isInRange (boolean) — true when the target is in range. Do not use this value when checksRange is false.
  • checksRange (boolean) — false when the client did not do a range check. For example, the slot is empty, its action has no range, or there is no target.

The values are the values that IsActionInRange(slot) gives: 1 is isInRange = true, 0 is isInRange = false, and nil is checksRange = false. A macro that is aimed at a unit (for example [@focus]) uses that unit as its target.

The client checks the enabled slots on each frame, but only while a frame is registered for the event. The event fires only when the state of a slot changes, and after each enable call. When the action in a slot changes, the client checks the new action. The event fires if its state is different.

ACTION_USABLE_CHANGED event

Fires when the usable state of one or more action-bar slots changes. The payload gives only the slots that changed. Thus, a handler does not have to read IsUsableAction for each button.

ACTION_USABLE_CHANGED: changes
  • changes (table) — a list with one table for each slot that changed:
Field Type Meaning
slot number The 1-based action-bar slot.
usable boolean The new first return value of IsUsableAction(slot), as a boolean.
noMana boolean The new second return value of IsUsableAction(slot), as a boolean.

The event fires immediately after ACTIONBAR_UPDATE_USABLE, which does not change. When the action in a slot changes, the client fires ACTIONBAR_SLOT_CHANGED for that slot, not this event. Thus, read IsUsableAction for a slot on ACTIONBAR_SLOT_CHANGED.

local f = CreateFrame("Frame")
f:RegisterEvent("ACTION_USABLE_CHANGED")
f:SetScript("OnEvent", function()
    for _, change in ipairs(arg1) do
        print("Slot", change.slot, change.usable, change.noMana)
    end
end)

COMBAT_LOG_EVENT_UNFILTERED / COMBAT_LOG_EVENT events

COMBAT_LOG_EVENT_UNFILTERED fires once for each combat log event: a hit, a miss, a heal, a power gain, damage from the environment, a cast, a summon, an aura that starts or ends, a dispel, a death. COMBAT_LOG_EVENT fires only for the events that match the combat log filter (see CombatLogAddFilter). With no filter, it fires for each event. The text lines in the chat frames (CHAT_MSG_COMBAT_*, CHAT_MSG_SPELL_*) do not change.

The values identify units by GUID, not by name, and do not depend on the client language. Events come only for what the server sends, which is the area near you.

The first 11 values are the same for each event:

# Value Description
1 timestamp The Unix time of the event, in seconds, with milliseconds.
2 subevent What happened, for example "SPELL_DAMAGE".
3 hideCaster true when the event has no source, else false.
4 sourceGUID The GUID of the unit that did it, in the UnitGUID format.
5 sourceName The name of that unit. nil when there is no source.
6 sourceFlags COMBATLOG_OBJECT_* bits that describe the source.
7 sourceRaidFlags The COMBATLOG_OBJECT_RAIDTARGET* bit of the raid mark on the source.
8 destGUID The GUID of the unit it happened to.
9 destName The name of that unit.
10 destFlags COMBATLOG_OBJECT_* bits that describe the target.
11 destRaidFlags The raid mark bit of the target.

A name that is not known yet is the UNKNOWNOBJECT text ("Unknown").

The values after these depend on the subevent. First comes the prefix:

Subevent starts with Prefix values
SWING_, UNIT_, PARTY_KILL (none)
ENVIRONMENTAL_ environmentalType: "FATIGUE", "DROWNING", "FALLING", "LAVA", "SLIME" or "FIRE"
SPELL_, SPELL_PERIODIC_, RANGE_, DAMAGE_SHIELD, DAMAGE_SPLIT spellId, spellName, spellSchool

Then comes the suffix:

Subevent ends with Suffix values
_DAMAGE, DAMAGE_SHIELD, DAMAGE_SPLIT amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand
_MISSED missType, isOffHand, then amountMissed for an "ABSORB", "BLOCK" or "RESIST"
_HEAL amount, overhealing, absorbed, critical
_ENERGIZE amount, powerType, alternatePowerType
_DRAIN, _LEECH amount, powerType, extraAmount, alternatePowerType
_CAST_FAILED failedType: the error text that the client showed
_INTERRUPT, _DISPEL_FAILED extraSpellId, extraSpellName, extraSchool
_DISPEL extraSpellId, extraSpellName, extraSchool, auraType
_EXTRA_ATTACKS amount
_DURABILITY_DAMAGE itemId, itemName
_AURA_APPLIED, _AURA_REMOVED, _AURA_REFRESH auraType: "BUFF" or "DEBUFF"
_AURA_APPLIED_DOSE, _AURA_REMOVED_DOSE auraType, amount
_CAST_START, _CAST_SUCCESS, _INSTAKILL, _RESURRECT, _SUMMON, _CREATE, _DURABILITY_DAMAGE_ALL, UNIT_DIED, UNIT_DESTROYED, PARTY_KILL (none)

The subevents that fire:

  • SWING_DAMAGE, SWING_MISSED
  • RANGE_DAMAGE, RANGE_MISSED (Auto Shot, Shoot, Throw)
  • SPELL_DAMAGE, SPELL_MISSED, SPELL_HEAL, SPELL_ENERGIZE
  • SPELL_PERIODIC_DAMAGE, SPELL_PERIODIC_MISSED, SPELL_PERIODIC_HEAL, SPELL_PERIODIC_ENERGIZE, SPELL_PERIODIC_DRAIN, SPELL_PERIODIC_LEECH
  • DAMAGE_SHIELD, DAMAGE_SHIELD_MISSED, DAMAGE_SPLIT
  • ENVIRONMENTAL_DAMAGE
  • SPELL_CAST_START, SPELL_CAST_SUCCESS, SPELL_CAST_FAILED
  • SPELL_DRAIN, SPELL_LEECH, SPELL_INTERRUPT, SPELL_EXTRA_ATTACKS, SPELL_INSTAKILL, SPELL_RESURRECT, SPELL_DURABILITY_DAMAGE, SPELL_DURABILITY_DAMAGE_ALL
  • SPELL_SUMMON (a pet, a totem or another summoned unit), SPELL_CREATE (a game object, for example a portal or a fishing bobber)
  • SPELL_AURA_APPLIED, SPELL_AURA_REMOVED, SPELL_AURA_REFRESH, SPELL_AURA_APPLIED_DOSE, SPELL_AURA_REMOVED_DOSE
  • SPELL_DISPEL, SPELL_DISPEL_FAILED
  • UNIT_DIED, UNIT_DESTROYED (a totem or another summoned unit), PARTY_KILL

Some values have fixed contents:

  • critical, glancing, crushing and isOffHand are 1 or nil.
  • resisted, blocked and absorbed in a damage suffix are nil when they are 0.
  • missType is one of "MISS", "RESIST", "DODGE", "PARRY", "BLOCK", "EVADE", "IMMUNE", "DEFLECT", "ABSORB" or "REFLECT".
  • Schools are bit masks: 1 physical, 2 holy, 4 fire, 8 nature, 16 frost, 32 shadow, 64 arcane.
  • overkill is the damage above the health of the target. It is 0 when the target lives. overhealing is the part of a heal above the health that the target was missing. Both are 0 when the target is not in view, or when the server sends the health of the target as a percentage. Most servers do this for units outside your group. The absorbed of a heal is always 0.
  • alternatePowerType is always nil.
  • DAMAGE_SHIELD names its spell when the owner of the shield has one damage-shield aura of that school, for example Thorns. Otherwise spellId is 0, spellName is nil, and spellSchool is the school of the damage.
  • DAMAGE_SHIELD_MISSED with the missType "IMMUNE" fires for a target that is immune to the damage of a damage shield or of a periodic effect.
  • The COMBATLOG_OBJECT_MAINTANK and COMBATLOG_OBJECT_MAINASSIST bits are never set.
  • SPELL_CAST_START fires for a spell with a cast time, and has no target. SPELL_CAST_SUCCESS fires for an instant spell. A spell with a cast time gets no SPELL_CAST_SUCCESS.
  • SPELL_CAST_FAILED fires only for your own casts.
  • A spell that the game hides from the combat log fires no event, except SPELL_CAST_FAILED. The mage portals are such spells.
  • The amount of an _AURA_APPLIED_DOSE or _AURA_REMOVED_DOSE is the number of stacks after the change.
  • SPELL_AURA_REFRESH fires when the duration of an aura starts again while the aura stays. For your own auras, each refresh fires. For another unit, a refresh fires when the client saw the cast that caused it.
  • SPELL_DISPEL and SPELL_DISPEL_FAILED name the dispelling spell when its cast was seen, for example Dispel Magic or Purge. A dispel from an aura, for example Abolish Poison, has spellId 0 and spellName nil. The extra spell is the aura that the dispel removed, or did not remove.
  • SPELL_INSTAKILL has a source when the cast was seen. Otherwise it has none.
  • These events have no source: ENVIRONMENTAL_DAMAGE, UNIT_DIED, UNIT_DESTROYED, and an aura event when the caster of the aura is not known.
  • ENCHANT_APPLIED and ENCHANT_REMOVED do not fire.
  • On some servers, SPELL_INTERRUPT does not fire for a spell that also does direct damage, for example Pummel, Kick or Earth Shock. These servers do not send the interrupt for such a spell.

The flag constants are globals:

Constant Value
COMBATLOG_OBJECT_AFFILIATION_MINE / _PARTY / _RAID / _OUTSIDER 0x1 / 0x2 / 0x4 / 0x8
COMBATLOG_OBJECT_REACTION_FRIENDLY / _NEUTRAL / _HOSTILE 0x10 / 0x20 / 0x40
COMBATLOG_OBJECT_CONTROL_PLAYER / _NPC 0x100 / 0x200
COMBATLOG_OBJECT_TYPE_PLAYER / _NPC / _PET / _GUARDIAN / _OBJECT 0x400 / 0x800 / 0x1000 / 0x2000 / 0x4000
COMBATLOG_OBJECT_TARGET / _FOCUS 0x10000 / 0x20000
COMBATLOG_OBJECT_MAINTANK / _MAINASSIST 0x40000 / 0x80000
COMBATLOG_OBJECT_NONE 0x80000000 (no unit)
COMBATLOG_OBJECT_RAIDTARGET1 … _RAIDTARGET8 0x1 … 0x80 (star … skull)

Each group also has a _MASK constant, for example COMBATLOG_OBJECT_AFFILIATION_MASK.

local f = CreateFrame("Frame")
f:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
f:SetScript("OnEvent", function(self, event, timestamp, subevent, hideCaster,
        sourceGUID, sourceName, sourceFlags, sourceRaidFlags,
        destGUID, destName, destFlags, destRaidFlags, ...)
    if subevent == "SPELL_DAMAGE" and sourceGUID == UnitGUID("player") then
        local spellName, amount = arg[2], arg[4]
        print(spellName, "hit", destName, "for", amount)
    end
end)

A handler that reads the vanilla arg1 … argN globals gets the same values: arg1 is the timestamp, arg2 the subevent, and so on.

CURSOR_CHANGED event

Fires when what the cursor holds changes, and says what it held before:

CURSOR_CHANGED: isDefault, newCursorType, oldCursorType, oldCursorVirtualID
  • isDefault — 1 when the cursor is now empty, nil when it is holding something (write if isDefault then — the event dispatcher cannot push real booleans).
  • newCursorType (number) — what the cursor holds now, as an Enum.UICursorType value.
  • oldCursorType (number) — what it held before, same enum.
  • oldCursorVirtualID (number) — the previous content's identifying number: itemID, spellID, macro slot, merchant slot, stable slot, or a money amount in copper. 0 when the cursor was empty, and for a pet action.
local f = CreateFrame("Frame")
f:RegisterEvent("CURSOR_CHANGED")
f:SetScript("OnEvent", function()
    if arg2 == Enum.UICursorType.Item then
        -- an item was just picked up; GetCursorInfo() has the details
    end
end)

CURSOR_UPDATE is untouched and still fires as it always has, so nothing that listens to it needs changing. Prefer CURSOR_CHANGED for new code, because it reports every kind of cursor. CURSOR_UPDATE announces a pickup only for items: picking up money, a spell, a macro, a pet action or a stabled pet gives you one CURSOR_UPDATE that describes an empty cursor, and nothing at all once the new content is in place. CURSOR_UPDATE also fires in cases where the cursor did not change, such as one per item while a stack is moved.

CURSOR_CHANGED compares the cursor against the last state it reported, so it fires once per real change and stays quiet otherwise. It is checked once a frame, which means it arrives on the frame after the change rather than during it.

Two holdings that look alike still count as a change: picking up one of two identical stacks, dropping it, and picking up the other reports each pickup separately.

EQUIPMENT_SETS_CHANGED event

Fires (with no payload) after any mutation: Create, Save, Modify, Delete, and the four *IgnoredSlot* calls. Addon UI should re-read its set list / button state when this fires.

EQUIPMENT_SWAP_PENDING event

Fires with one payload argument, setID, when UseEquipmentSet starts a swap. Until EQUIPMENT_SWAP_FINISHED fires, CanUseEquipmentSets() returns false and equipment-set buttons are grayed out.

EQUIPMENT_SWAP_FINISHED event

Fires with two payload arguments, success and setID, when a swap from UseEquipmentSet ends. It always comes after EQUIPMENT_SWAP_PENDING, and never during the UseEquipmentSet call.

success is 1 when every slot that the swap changed holds its set item. success is nil when:

  • the server refused a move of a set item, or
  • an item could not be taken off because the bags were full, or
  • the equipped items stopped changing before the swap was complete. This occurs, for example, when you cancel the bind-on-equip confirmation for a set item.

Slots that the swap skipped (missing items, combat, a closed bank) do not make the swap fail.

WEAR_EQUIPMENT_SET event

Fires with one payload argument, setID, when the player uses an equipment-set action button. The event does not equip the set.

The built-in handler equips the set with UseEquipmentSet. If an item of the set is locked, or the player is casting, the handler does not equip the set. Instead, it shows ERR_CLIENT_LOCKED_OUT ("You can't do that right now.") in the error frame.

FACTION_STANDING_CHANGED event

Fires once per reputation change with (factionID, newStanding, repGained):

| arg1 (factionID) | Faction.dbc row ID of the faction whose standing changed | | arg2 (newStanding) | New total standing value (post-change barValue) | | arg3 (repGained) | Signed delta — positive on gain, negative on loss |

The engine exposes only CHAT_MSG_COMBAT_FACTION_CHANGE, whose arg1 is the localized chat string ("Your Stormwind reputation has increased by 100.") — addons have to parse the text and reverse-resolve the faction name back to an ID. This event lets addons skip that work.

Does not fire for the initial faction sync at login or /reload. Only fires when a real reputation gain or loss arrives from the server (SMSG_SET_FACTION_STANDING).

local f = CreateFrame("Frame")
f:RegisterEvent("FACTION_STANDING_CHANGED")
f:SetScript("OnEvent", function()
    -- in vanilla 1.12 OnEvent receives no args; engine sets `event`
    -- and `arg1..argN` as globals before invoking the handler.
    if event == "FACTION_STANDING_CHANGED" then
        local name = GetFactionInfoByID(arg1)
        print(string.format("%s: %+d  (now %d)", name, arg3, arg2))
    end
end)

repGained is signed: positive for gains, negative for the rare loss case (e.g. killing a Goblin Commodity Exchange NPC drops your rep with Booty Bay).

Implementation notes

Hooks the engine's per-rep-change notify dispatcher at 0x0062C5F0 — the chokepoint called once per (factionID, signedDelta) from the per-slot setter at 0x004D6330, gated by "value-actually-changed AND notify-flag-set". The setter has already written the new delta into the per-slot storage by the time the hook fires, so we read the total back via FUN_REPUTATION_GET_STANDING(factionID) (0x004D6370) to produce the newStanding payload.

The hook calls the original before firing our event so the engine's CHAT_MSG_COMBAT_FACTION_CHANGE still fires first — no behavior change for addons that depended on the chat text. A per-call snapshot of (factionID, newStanding, repGained) is captured before forwarding, which C_Reputation.GetLastStandingChange exposes — so addons can read the structured payload from inside the chat event without re-parsing the localized string.

INCOMING_RESURRECT_CHANGED event

Fires when a resurrection cast at a unit starts or stops. arg1 is the unit token. The event gives no value. UnitHasIncomingResurrection gives the new value.

The event fires one time for each token that names the unit, for example "party1" and "target". If the cast is at the corpse of a released player, arg1 names that player.

local f = CreateFrame("Frame")
f:RegisterEvent("INCOMING_RESURRECT_CHANGED")
f:SetScript("OnEvent", function()
    print(arg1, UnitHasIncomingResurrection(arg1))
end)

INVENTORY_SEARCH_UPDATE event

Fires with no payload when C_Container.SetItemSearch changes the bag search. When it fires, read isFiltered from C_Container.GetContainerItemInfo and the result of C_Container.IsContainerFiltered again. A logout clears the search, but it does not fire this event.

LOOT_HISTORY_ROLL_CHANGED / LOOT_HISTORY_ROLL_COMPLETE / LOOT_HISTORY_FULL_UPDATE events

Fire as group-loot rolls progress, so a loot-history UI can update without polling. Register like any engine event (frame:RegisterEvent("LOOT_HISTORY_ROLL_COMPLETE")), then read the item via C_LootHistory.GetItem / C_LootHistory.GetPlayerInfo.

Event Args When
LOOT_HISTORY_ROLL_CHANGED itemIndex, playerIndex (numbers) A player selected need, greed, or pass, or rolled the dice.
LOOT_HISTORY_ROLL_COMPLETE itemIndex (number) The item was decided — won or all-passed.
LOOT_HISTORY_FULL_UPDATE — The item set changed structurally — a new item appeared (roll opened) or the 128-item ring evicted the oldest (every index shifted). Re-read the whole list. Also fired by C_LootHistory.Clear().

See the LootHistory section for the read API.

LEARNED_SPELL_IN_SKILL_LINE event

Fires when the player learns a spell that enters the spellbook.

-- arg1 = spellID, arg2 = skillLineIndex, arg3 = isGuildPerkSpell
local f = CreateFrame("Frame")
f:RegisterEvent("LEARNED_SPELL_IN_SKILL_LINE")
f:SetScript("OnEvent", function()
    local tab = C_SpellBook.GetSpellBookSkillLineInfo(arg2)
    print("learned", C_Spell.GetSpellName(arg1), "in", tab.name)
end)
Arg Type Notes
spellID number The spell that was learned.
skillLineIndex number The 1-based spellbook tab that now holds it. Feed it to C_SpellBook.GetSpellBookSkillLineInfo.
isGuildPerkSpell nil Always nil (false).

The event fires next to the stock LEARNED_SPELL_IN_TAB, under the same rule: the game announced the new spell (a trainer, a quest reward, a talent) and the spell got a spellbook slot. The spells restored at login do not fire it. Profession recipes do not enter the spellbook, so they do not fire it.

LOOT_SCAN_COMPLETED event

Fires (no payload) once a C_Loot.ScanNearbyLoot() call has finished walking every reachable corpse. Handler should call C_Loot.GetLastScanResults() to read the collected { guid, coin, items } array.

local f = CreateFrame("Frame")
f:RegisterEvent("LOOT_SCAN_COMPLETED")
f:SetScript("OnEvent", function()
    local results = C_Loot.GetLastScanResults()
    -- ... process results
end)
C_Loot.ScanNearbyLoot()

Fires exactly once per ScanNearbyLoot() call regardless of outcome — even if every corpse timed out and the results table is empty. Doesn't fire spuriously for scans triggered by other addons (only one scan runs at a time; concurrent attempts return false from ScanNearbyLoot). See the Loot section for the full scan flow.

LOSS_OF_CONTROL_ADDED / LOSS_OF_CONTROL_UPDATE events

Fire as loss-of-control effects change, so a UI can react without polling. Register like any engine event (frame:RegisterEvent("LOSS_OF_CONTROL_ADDED")).

Event Args When
LOSS_OF_CONTROL_ADDED eventIndex (number) A new effect was applied — eventIndex is its 1-based index for C_LossOfControl.GetActiveLossOfControlData.
LOSS_OF_CONTROL_UPDATE unitToken (string) The active set changed — an effect was added, fell off, or expired. Re-scan with GetActiveLossOfControlData. Always "player" (only the local player is tracked), but passed so the event carries a unit token and leaves room for per-unit tracking.

Detection is a per-frame diff of the active set (effect expiry has no engine packet to hook), so events land within a frame of the change and coalesce multiple simultaneous changes into a single LOSS_OF_CONTROL_UPDATE. A counterspelled cast, for instance, fires two ADDED (the school lockout and, with Improved Counterspell, the silence) then an UPDATE as each expires. See the LossOfControl section for the effect data.

MAP_EXPLORATION_UPDATED event

Fires (with no payload) when the player explores a new area. When it fires, C_MapExplorationInfo already returns the new state. Use it to redraw a map-reveal layer only when the explored areas change.

local f = CreateFrame("Frame")
f:RegisterEvent("MAP_EXPLORATION_UPDATED")
f:SetScript("OnEvent", function()
    MyMapReveal_Refresh()
end)

MODIFIER_STATE_CHANGED event

Fires on every modifier-key press and release with (key, down):

| arg1 (key) | LSHIFT, RSHIFT, LCTRL, RCTRL, LALT, RALT | | arg2 (down) | 1 on press, 0 on release |

Only transitions fire — key autorepeat does not.

Releases that happen while WoW is not the focused window are also handled: keyboard messages only reach WoW while it has focus, so a modifier let go in the background is never seen live. On regaining focus the cached state is reconciled against the true physical key state and any missed transition fires then (see the focus-regain note below).

local f = CreateFrame("Frame")
f:RegisterEvent("MODIFIER_STATE_CHANGED")
f:SetScript("OnEvent", function()
    -- in vanilla 1.12 OnEvent receives no args; engine sets `event` and
    -- `arg1..argN` as globals before invoking the handler.
    if event == "MODIFIER_STATE_CHANGED" and arg1 == "LSHIFT" then
        print("left shift", arg2 == 1 and "down" or "up")
    end
end)

Implementation notes

L/R modifier distinction doesn't exist anywhere in the engine's own state — its IsShiftKeyDown chain bottoms out at GetKeyState(VK_SHIFT) (the merged virtual key, 0x10), never the L/R-aware VK_LSHIFT / VK_RSHIFT. The OS-level keystate does have the distinction; we capture it by installing a WH_GETMESSAGE Win32 thread hook on the engine's message-pump thread, decoding each WM_KEY{,SYS}{DOWN,UP} message (using MapVirtualKeyA(scancode, MAPVK_VSC_TO_VK_EX) to resolve VK_SHIFT to L/R and the KF_EXTENDED bit in lParam for VK_CONTROL / VK_MENU), and maintaining a 6-bit cached bitmap that the seven query functions read.

The thread-message hook is per-thread, not per-HWND — it survives renderer-state changes that recreate WoW's main window (e.g. toggling vertical sync), where an SetWindowLongPtr-style WNDPROC subclass would be left dangling.

Focus-regain reconciliation. WM_KEY* messages are only delivered while WoW is the foreground window, so a modifier released while WoW is in the background (alt-tabbed away) produces no message — the cached bit would stay stuck down and the release event would never fire. A second thread hook (WH_CALLWNDPROC, needed because WM_ACTIVATEAPP is sent rather than posted and so is invisible to WH_GETMESSAGE) watches for app activation; on regaining focus it polls GetAsyncKeyState for each L/R modifier — which reads global physical key state regardless of focus — and fires MODIFIER_STATE_CHANGED for any bit that changed while WoW was away. So the release surfaces the moment you tab back, and IsLeft/RightShiftKeyDown etc. read correct state immediately.

Note: the engine's own IsShiftKeyDown / IsControlKeyDown / IsAltKeyDown bottom out at the synchronous GetKeyState, which has the same background-release blind spot and is not reconciled by this hook — only ClassicAPI's L/R query functions and the event reflect the corrected state. Prefer IsModifierKeyDown / the L/R functions if you need the un-stuck value.

NAME_PLATE_CREATED / NAME_PLATE_UNIT_ADDED / NAME_PLATE_UNIT_REMOVED events

Fire when nameplate state actually changes. Payloads:

Event arg1 Notes
NAME_PLATE_CREATED nameplate Frame Fires once per unique CGNamePlateFrame pointer — same frame re-used via pool recycle does NOT refire.
NAME_PLATE_UNIT_ADDED "nameplateN" unit token Pass straight to UnitName / UnitGUID / UnitClass / etc., or to GetNamePlateForUnit for the frame. The token is positional — see Unit tokens for ordering semantics.
NAME_PLATE_UNIT_REMOVED "nameplateN" unit token Same as above. Computed from the plate's slot before it shifts out of the ordered list, so the token still resolves to the leaving unit during the event handler.
local f = CreateFrame("Frame")
f:RegisterEvent("NAME_PLATE_CREATED")
f:RegisterEvent("NAME_PLATE_UNIT_ADDED")
f:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
f:SetScript("OnEvent", function()
    if event == "NAME_PLATE_CREATED" then
        -- arg1 = the nameplate Frame itself
        arg1:SetAlpha(0.8)
    elseif event == "NAME_PLATE_UNIT_ADDED" then
        -- arg1 = "nameplate1" / "nameplate2" / ...
        local name = UnitName(arg1)
        local plate = C_NamePlate.GetNamePlateForUnit(arg1)
        -- ... style based on the unit ...
    end
end)

CREATED timing with nameplate addons (pfUI / TidyPlates / etc). The event fires when the engine allocates the underlying CGNamePlateFrame. Nameplate-mod addons typically decorate the frame on their own per-frame update — so arg1 at CREATED time is a bare frame with no addon-side decorations yet. For unit-specific work after the addon has decorated, use NAME_PLATE_UNIT_ADDED (fires next tick at the latest) or fetch the current frame on-demand via GetNamePlateForUnit(arg1).

Token stability gotcha. A plate keeps its token for its whole life, and the other plates never renumber when one plate is removed. But a removed plate frees its slot, and a later plate can reuse that slot. So "nameplate3" can name a different unit at a later time. For a key that stays with one unit, call UnitGUID(arg1) and store the GUID. See Unit tokens for the slot rules.

Implementation notes

Detected by per-frame polling, not engine hooks. Each world tick we walk the object hash for nameplated units and diff against the previous tick's snapshot. The engine has no event for "plate state changed", so the events are synthesized via diffing. The cost is ~20-50µs/frame even in busy raids — well below noise.

The diff approach absorbs the engine's transient hide/reshow cycle: there are ~7 code paths that briefly zero unit + 0xE60 (z-order rebuilds, anchor changes, flag-change re-eval) and the next frame's show path re-allocates from the pool. Those transient zeroes never become events because the unit appears in both the previous and current tick's snapshot.

PLAYER_FOCUS_CHANGED event

Fires whenever the player's focus changes — assignment via FocusUnit, clear via ClearFocus, or a FocusUnit(token) call where the token resolves to no GUID (implicit clear). No payload args; the new focus GUID is read off UnitGUID("focus") in the handler.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_FOCUS_CHANGED")
f:SetScript("OnEvent", function()
    local guid = UnitGUID("focus")
    if guid then
        print("focusing", UnitName("focus"))
    else
        print("focus cleared")
    end
end)

Identity-checked: assigning the same GUID twice is a no-op (no event refires). Fired whenever the player's focus target is changed, including when the focus target is lost or cleared.

PLAYER_MOUNT_DISPLAY_CHANGED event

Fires when the player mounts, dismounts, or changes to a different mount. The event has no payload.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_MOUNT_DISPLAY_CHANGED")
f:SetScript("OnEvent", function()
    print("Mount display changed")
end)

PLAYER_SOFT_ENEMY_CHANGED / PLAYER_SOFT_FRIEND_CHANGED events

Fires when the "softenemy" or the "softfriend" unit changes to a different unit, or to nothing. The events have no arguments. Read the new unit through its token. See Soft enemy and soft friend.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_SOFT_ENEMY_CHANGED")
f:SetScript("OnEvent", function()
    print("soft enemy:", UnitName("softenemy"))
end)

PLAYER_SOFT_INTERACT_CHANGED event

Fires when the "softinteract" object changes to a different object, or to nothing. See Soft interact.

  • arg1 is the GUID of the previous object, or nil.
  • arg2 is the GUID of the new object, or nil.
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_SOFT_INTERACT_CHANGED")
f:SetScript("OnEvent", function()
    if arg2 then
        print("can interact with", arg2)
    end
end)

PLAYER_SWING event

Fires each time one of your attack timers resets.

PLAYER_SWING: swingDuration, swingType
  • swingDuration (number) — the number of seconds from now until the next swing of this type.
  • swingType — an Enum.PlayerSwingType value: which weapon reset.
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_SWING")
f:SetScript("OnEvent", function()
    if arg2 == Enum.PlayerSwingType.MainHand then
        MainHandBar_Start(arg1)
    end
end)

A landed main-hand or off-hand hit resets that weapon's timer. An on-next-swing ability (Heroic Strike, Maul) resets the main-hand timer in its place. A ranged shot resets the ranged timer. Several melee hits can land at the same moment, such as an extra attack from Windfury. Even then, this event fires once per weapon, not once per hit. A parried hit can shorten swingDuration below the weapon's normal speed. Which spells interrupt the melee timers can differ by realm.

PLAYER_SWING does not fire for an attack that you declare while out of melee range. It fires once a swing is close enough to be real.

A spell that interrupts the melee timers fires PLAYER_SWING only while you auto-attack. If you cast it before you attack, the event fires when the attack starts, with the time that is left.

PLAYER_SWING_RANGE_UPDATE event

Fires when your current target moves into or out of range for a swing type that has range checking on. See C_SwingTimer.EnableRangeCheck.

PLAYER_SWING_RANGE_UPDATE: swingType, isInRange, checksRange
  • swingType — the Enum.PlayerSwingType value this update is for.
  • isInRange — 1 when the target is in range, nil when it is not. Ignore this value when checksRange is nil.
  • checksRange — 1 when a range check was possible, nil when it was not: for example, there is no current target, the target cannot be attacked, or no weapon is equipped for this swing type.

QUEST_ACCEPTED event

Fires once per quest the player just accepted, with two payload args: the 1-based quest log index and the questID. The signature is QUEST_ACCEPTED(questLogIndex, questID).

local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_ACCEPTED")
f:SetScript("OnEvent", function()
    -- 1.12: event payload is in `arg1`, `arg2`, ... globals
    if event == "QUEST_ACCEPTED" then
        local questLogIndex, questID = arg1, arg2
        -- ...
    end
end)

The event fires for every path that adds a quest to the local quest log. These paths include an accept from an NPC, a quest shared by a party member, and an auto-grant from a quest item. The quest is in the quest log before the event fires, so GetQuestLogTitle(questLogIndex) is valid inside the handler.

The client prints the "Quest accepted" system message first, then fires this event. A handler for CHAT_MSG_SYSTEM therefore also sees a current quest log.

The event fires again when a quest returns from complete to incomplete, because the client announces that quest a second time.

Does not fire on login or character entry. The bulk sync at that point is not an accept, and the client does not announce those quests either.

When the static data for a quest is not in the client cache yet, the event waits for the server to answer. The delay is one round trip. The system message waits with it, so the order does not change.

QUEST_QUERY_COMPLETE event

Fires on a Turtle WoW client when a copy of the list of completed quests arrives from the server. The event has no payload. Read the list with GetQuestsCompleted or C_QuestLog.GetAllCompletedQuestIDs.

The event fires a short time after each login, and after each request for the list. A request can come from QueryQuestsCompleted, from a quest turn-in, or from another addon. See Completed quests.

QUEST_REMOVED event

Fires once per quest leaving the local quest log, with the questID as the single payload arg. Covers both turn-ins and abandons — the two are distinguishable by whether a QUEST_TURNED_IN accompanies the removal. The signature is the bare questID.

local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_REMOVED")
f:SetScript("OnEvent", function()
    if event == "QUEST_REMOVED" then
        local questID = arg1
        -- ...
    end
end)

A QUEST_TURNED_IN event for the same quest identifies a turn-in. An abandon has no such event. For a turn-in, QUEST_TURNED_IN fires first and QUEST_REMOVED follows, because the turn-in reply does not change the quest log by itself.

When a quest log slot takes a different quest directly, QUEST_REMOVED fires for the old quest and QUEST_ACCEPTED fires for the new one.

Does not fire on login or character entry. The bulk sync at that point is not a removal.

QUEST_TURNED_IN event

Fires when the server confirms a quest turn-in (SMSG_QUESTGIVER_QUEST_COMPLETE, opcode 0x191). Payload is (questID, xpReward, moneyReward) — xpReward is the experience awarded (0 at max level or for non-XP-bearing quests), moneyReward is in copper.

local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_TURNED_IN")
f:SetScript("OnEvent", function()
    if event == "QUEST_TURNED_IN" then
        local questID, xp, money = arg1, arg2, arg3
        -- ...
    end
end)

Polyfilled by hooking the per-opcode SMSG handler at FUN_005DC400 (found via the CMSG side: Script_GetQuestReward → FUN_005015B0 → FUN_005EADC0 sends opcode 0x18E CMSG_QUESTGIVER_CHOOSE_REWARD; SMSG response is opcode 0x191). The hook saves the packet stream's read cursor (stream+0x14), peeks the first 4 uint32s of the body (questID, unknown, xp, money), restores the cursor, then calls the original — which re-reads the same data without seeing our peek. Side-effect-free.

Does NOT fire on quest abandon, on QUEST_FINISHED window transitions (which fire on Detail → Progress → Reward → Close — not a clean turn-in signal), or on server-reject paths (bag-full, quest invalidated, etc.). Only fires on a real successful turn-in.

Why not key off QUEST_FINISHED? The stock QUEST_FINISHED fires on every quest-window state transition, including reads, aborts, and player-side close. The SMSG_QUESTGIVER_QUEST_COMPLETE packet, by contrast, is server-authoritative — the server only sends it after committing the turn-in (XP / money / item awards done, quest removed from log). Hooking the packet handler gives us a clean turn-in signal.

SELF_RES_SPELL_CHANGED event

Fires with no payload when the self-resurrection spell of the player changes. For example, it fires when the player dies with a Soulstone, and again when the player comes back to life. When it fires, read C_DeathInfo.GetSelfResurrectOptions again. A change to an item option does not fire this event.

SOUNDKIT_FINISHED event

Fires when a sound finishes playing.

local f = CreateFrame("Frame")
f:RegisterEvent("SOUNDKIT_FINISHED")
f:SetScript("OnEvent", function()
    print("finished:", arg1)   -- the handle of the sound that ended
end)

C_Sound.PlaySound(8959, nil, nil, true)
Argument Meaning
arg1 soundHandle — the handle C_Sound.PlaySound returned for the sound that just ended.

A sound stopped with StopSound also sends the event.

It fires only for sounds played with runFinishCallback set to true. Sounds played any other way report nothing — every footstep and button click would otherwise raise an event.

The event arrives on the frame after the sound ends, which is when the client releases it.

SPELL_ACTIVATION_OVERLAY_SHOW / SPELL_ACTIVATION_OVERLAY_HIDE events

Fire when a spell alert starts or ends. See the SpellActivationOverlay section for the spells that have alerts.

Event Args When
SPELL_ACTIVATION_OVERLAY_SHOW spellID, texture, locationType, scale, r, g, b A proc aura landed on the player. spellID is the aura.
SPELL_ACTIVATION_OVERLAY_HIDE spellID (or nil) The aura left the player. If spellID is nil, every alert ends.
  • texture is a texture path that names the art of the alert. An addon that draws the alert supplies a texture file for each path.
  • locationType is an Enum.ScreenLocationType value.
  • scale is a size multiplier, and r, g and b are a tint from 0 to 255.

When the player enters the world (login, /reload or a zone change), SPELL_ACTIVATION_OVERLAY_HIDE fires with nil. Then SPELL_ACTIVATION_OVERLAY_SHOW fires again for each alert that continues.

local f = CreateFrame("Frame")
f:RegisterEvent("SPELL_ACTIVATION_OVERLAY_SHOW")
f:RegisterEvent("SPELL_ACTIVATION_OVERLAY_HIDE")
f:SetScript("OnEvent", function()
    if event == "SPELL_ACTIVATION_OVERLAY_SHOW" then
        MyAlert_Show(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
    elseif arg1 then
        MyAlert_Hide(arg1)
    else
        MyAlert_HideAll()
    end
end)

SPELL_ACTIVATION_OVERLAY_GLOW_SHOW / SPELL_ACTIVATION_OVERLAY_GLOW_HIDE events

Fire when the glow of a spell starts or ends.

Event Args When
SPELL_ACTIVATION_OVERLAY_GLOW_SHOW spellID The glow of the spell started.
SPELL_ACTIVATION_OVERLAY_GLOW_HIDE spellID The glow of the spell ended.

Each event names one spell ID. A spell with ranks fires one event for each rank in the spellbook. When the event fires, IsSpellOverlayed already gives the new answer.

UI_SCALE_CHANGED event

Fires (with no payload) each time the game applies the interface scale to UIParent. This occurs when:

  • the uiScale setting changes while useUiScale is on,
  • the useUiScale setting changes, or
  • the size of the game window changes.

When the event fires, UIParent already has the new scale. After a change of the window size, the event fires before DISPLAY_SIZE_CHANGED.

The event also fires when the new scale is the same as the old scale. A call to UIParent:SetScale() from Lua does not fire it.

local f = CreateFrame("Frame")
f:RegisterEvent("UI_SCALE_CHANGED")
f:SetScript("OnEvent", function()
    print(UIParent:GetScale())
end)

UNIT_AREA_CHANGED event

Fires when a group member moves to a different zone.

UNIT_AREA_CHANGED: unitTarget
  • unitTarget (string) — the unit token of the member, for example party1 or raid12. A member that has more than one token (for example party1 and raid3) gets one event for each token.

The event also fires for a member that the client cannot see, for example a member in another zone. The client puts a member that it can see in the zone of the player. As a result, the event fires when a member comes into view from a different zone. It does not fire when a member that the client can see moves to a different zone. The event does not fire for player.

For a raid member, the zone value of GetRaidRosterInfo already gives the new zone when the event fires.

local f = CreateFrame("Frame")
f:RegisterEvent("UNIT_AREA_CHANGED")
f:SetScript("OnEvent", function()
    print(UnitName(arg1), "changed zone")
end)

UNIT_AURA_DELTA event

Fires when the auras on a unit change. The payload tells you which auras changed, so a handler does not have to read all the auras of the unit again. This is a ClassicAPI extension. Its payload has the shape of the modern UNIT_AURA payload, but it holds instance IDs only. UNIT_AURA itself does not change.

UNIT_AURA_DELTA: unitTarget, updateInfo
  • unitTarget (string) — the unit token. A unit that has more than one token (for example target and party1) gets one event for each token.
  • updateInfo (table):
Field Type Meaning
isFullUpdate boolean true when the handler must read all the auras of the unit again. The three lists are then nil.
addedAuraInstanceIDs table? The instance IDs of the auras that the unit got. nil when there are none.
updatedAuraInstanceIDs table? The instance IDs of the auras that stayed on the unit but changed: a new stack count, a refresh, or a new duration. nil when there are none.
removedAuraInstanceIDs table? The instance IDs of the auras that went off the unit. nil when there are none.

An instance ID is the auraInstanceID field of an AuraData table. To read an added or updated aura, give its ID to C_UnitAuras.GetAuraDataByAuraInstanceID.

local spells = {}  -- auraInstanceID -> spellId, for the player
local buf = {}

local function ReadAll()
    spells = {}
    for _, filter in ipairs({ "HELPFUL", "HARMFUL" }) do
        local i = 1
        local d = C_UnitAuras.GetAuraDataByIndex("player", i, filter)
        while d do
            if d.auraInstanceID then
                spells[d.auraInstanceID] = d.spellId
            end
            i = i + 1
            d = C_UnitAuras.GetAuraDataByIndex("player", i, filter)
        end
    end
end

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("UNIT_AURA_DELTA")
f:SetScript("OnEvent", function()
    if event == "PLAYER_ENTERING_WORLD" then
        ReadAll()
        return
    end
    if arg1 ~= "player" then
        return
    end
    if arg2.isFullUpdate then
        ReadAll()
        return
    end
    for _, id in ipairs(arg2.removedAuraInstanceIDs or {}) do
        spells[id] = nil
    end
    for _, id in ipairs(arg2.addedAuraInstanceIDs or {}) do
        local d = C_UnitAuras.GetAuraDataByAuraInstanceID("player", id, buf)
        if d then
            spells[id] = d.spellId
        end
    end
end)

Some rules:

  • The tables are valid only during the handler. Each fire uses the same updateInfo table and the same three lists again. To keep a list, copy it.
  • The event gives changes, not the start state. Read all the auras of a unit before you use its deltas: when you register the event, and when a token starts to name a different unit (for example on PLAYER_TARGET_CHANGED). The event does not fire when a unit only gets a new token.
  • One event for each unit and token in each frame. All the changes of one frame come together.
  • isFullUpdate is true when no list is available. For example, this occurs when a unit comes back into view.
  • A refresh that changes nothing that the client can see does not fire the event.
  • When no frame is registered for the event, the client builds no lists.

UNIT_CONNECTION event

Fires when a group member goes offline or comes back online.

UNIT_CONNECTION: unitTarget, isConnected
  • unitTarget (string) — the unit token of the member, for example party1 or raid12. A member that has more than one token (for example party1 and raid3) gets one event for each token.
  • isConnected (boolean) — true when the member is online, false when the member is offline.

The event also fires for a member that the client cannot see, for example a member in another zone. When the event fires, UnitIsConnected(unitTarget) already gives the new state.

local f = CreateFrame("Frame")
f:RegisterEvent("UNIT_CONNECTION")
f:SetScript("OnEvent", function()
    print(UnitName(arg1), arg2 and "is online" or "is offline")
end)

UNIT_DISTANCE_CHECK_UPDATE event

Fires when a group member comes into view or leaves it.

UNIT_DISTANCE_CHECK_UPDATE: unitTarget, isInDistance
  • unitTarget (string) — the unit token of the member, for example party1 or raid12. A member that has more than one token (for example party1 and raid3) gets one event for each token.
  • isInDistance (boolean) — true when the client can see the member, false when the client cannot see the member.

The client can see a member that is near enough for the server to send it. The server sets this distance. When the event fires, UnitIsVisible(unitTarget) already gives the new state.

The event fires for party and raid members. It does not fire for player or for pets.

local f = CreateFrame("Frame")
f:RegisterEvent("UNIT_DISTANCE_CHECK_UPDATE")
f:SetScript("OnEvent", function()
    print(UnitName(arg1), arg2 and "came into view" or "left view")
end)

UNIT_FACTION event (fire-coverage fix)

UNIT_FACTION already exists in the event table — addons can register for it — but the engine never fires it on several paths where it should. We add the missing fires so addons that observe UNIT_FACTION stop missing state changes.

Payload: arg1 is the unit token string, always "player" for these paths (you can only toggle / sync rep on the local player).

local f = CreateFrame("Frame")
f:RegisterEvent("UNIT_FACTION")
f:SetScript("OnEvent", function()
    if event == "UNIT_FACTION" and arg1 == "player" then
        RefreshFactionUI()
    end
end)

Paths that now fire UNIT_FACTION("player"):

Path Trigger Detection
FactionToggleAtWar Lua call to toggle AT_WAR Slot's flag byte changed (skips PEACE_FORCED no-ops, looting-blocked attempts, standing-blocked attempts)
SetFactionInactive / SetFactionActive Lua call to toggle INACTIVE Slot's flag byte changed
SMSG_SET_FACTION_STANDINGS Server rep-change push (kills, quest turn-ins, etc.) Any byte in the 64-entry flag array changed — catches AT_WAR side-effect flips when standing crosses the -3000 threshold
SMSG_SET_FACTION_ATWAR Server force-change of AT_WAR (faction reset, server-side state push) Unconditional after byte write
SMSG_INITIALIZE_FACTIONS Server initial faction sync at login Unconditional after table populated

The last one fixes the case where addons relying on UNIT_FACTION (rather than PLAYER_LOGIN / VARIABLES_LOADED) miss the initial load entirely.

Not yet covered:

  • Unit-faction-template changes for non-player units (mind control, charm, server-side faction scripts). UNIT_FACTION should fire on the affected unit token; the broad helper isn't hooked here yet. Mostly affects nameplate / threat addons in rare encounter mechanics.

Implementation notes

FactionToggleAtWar / SetFactionInactive / SetFactionActive's inner setters (FUN_004D5FD0, FUN_004D60F0) write to the rep slot's flag byte and send the corresponding CMSG, but never call the engine's "fire UNIT_FACTION on this unit" dispatcher.

Since FactionToggleAtWar only ever runs for the player and the only unit token resolving to the player is "player", we fire UNIT_FACTION("player") directly via the engine's printf-style dispatcher (FUN_FIRE_EVENT) — same observable result without re-deriving the broad helper. We resolve UNIT_FACTION's event-table slot lazily by name (Event::Custom::LookupByName) so we stay correct against any DLL combination that reshuffles the table.

For the two SMSG-bound flag-flip paths (SetFactionStandings, SetFactionInactive-but-from-server), we snapshot the 64-entry rep slot flag-byte array before the original handler runs and compare after — single fire if any byte changed. The bulk snapshot is 64 byte reads (cheap) and naturally handles the case where one packet carries multiple rep updates whose threshold crossings ripple AT_WAR state across unrelated slots.

UNIT_IN_RANGE_UPDATE event

Fires when a unit moves into or out of the range that UnitInRange checks.

UNIT_IN_RANGE_UPDATE: unitTarget, isInRange
  • unitTarget (string) — the unit token.
  • isInRange (boolean) — the new first return value of UnitInRange(unitTarget).

The frames that are registered for the event select the units that the client checks:

  • A frame that uses RegisterUnitEvent adds its units. You can use any unit token, for example target or focus.
  • A frame that uses RegisterEvent adds pet, each party and raid member, and the pets of these members.

The client checks these units on each frame, but only while a frame is registered for the event. To check fewer units, use RegisterUnitEvent with only the units that you show.

The event fires only when the value changes. The first check of a unit does not fire the event. Thus, read UnitInRange when you start to show a unit.

local f = CreateFrame("Frame")
f:RegisterUnitEvent("UNIT_IN_RANGE_UPDATE", "party1", "party2")
f:SetScript("OnEvent", function()
    print(arg1, arg2 and "is in range" or "is out of range")
end)

UNIT_POWER_UPDATE / UNIT_POWER_FREQUENT / UNIT_MAXPOWER events

UNIT_POWER_UPDATE and UNIT_POWER_FREQUENT fire when the power of a unit changes. UNIT_MAXPOWER fires when the maximum power of a unit changes.

UNIT_POWER_UPDATE: unitTarget, powerType
UNIT_POWER_FREQUENT: unitTarget, powerType
UNIT_MAXPOWER: unitTarget, powerType
  • unitTarget (string) — the unit token. A unit that has more than one token (for example target and focus) gets one event for each token.
  • powerType (string) — the power that changed: "MANA", "RAGE", "FOCUS", "ENERGY", or "HAPPINESS".

The client gets each power change from the server, so UNIT_POWER_FREQUENT fires at the same times as UNIT_POWER_UPDATE. The events fire for each unit token that gets unit events. For a unit that the client can see, they fire for every power of the unit, not only for its primary power. For a group member that the client cannot see, for example a member in another zone, they fire for the primary power of the member only. The new values are UnitPower(unitTarget, type) and UnitPowerMax(unitTarget, type).

local f = CreateFrame("Frame")
f:RegisterUnitEvent("UNIT_POWER_UPDATE", "player")
f:SetScript("OnEvent", function()
    if arg2 == "ENERGY" then
        print("Energy:", UnitPower("player", Enum.PowerType.Energy))
    end
end)

UNIT_SPELL_HASTE event

Fires when the spell haste of a unit changes.

UNIT_SPELL_HASTE: unitTarget
  • unitTarget (string) — the unit token. A unit that has more than one token (for example target and focus) gets one event for each token.

The new value is UnitSpellHaste(unitTarget). The event fires for each unit token that gets unit events, and only for units that the client can see.

UNIT_TARGET event

Fires when the target of a unit changes.

UNIT_TARGET: unitTarget
  • unitTarget (string) — the unit token. A unit that has more than one token (for example target and focus) gets one event for each token.

The event fires for each unit token that gets unit events, focus included. It fires only for units that the client can see. The token of the new target is unitTarget .. "target", for example focustarget.

local f = CreateFrame("Frame")
f:RegisterUnitEvent("UNIT_TARGET", "focus")
f:SetScript("OnEvent", function()
    print("Focus target:", UnitName("focustarget") or "none")
end)

UNIT_TARGETABLE_CHANGED event

Fires when a unit becomes selectable or stops being selectable, for example a boss that leaves the fight for a phase.

UNIT_TARGETABLE_CHANGED: unitTarget
  • unitTarget (string) — the unit token. A unit that has more than one token (for example target and focus) gets one event for each token.

The event fires for each unit token that gets unit events, and only for units that the client can see.

UPDATE_MOUSEOVER_UNIT event (loss-fire fix)

UPDATE_MOUSEOVER_UNIT exists in the event table and fires when a mouseover unit is gained, but the engine never fires it on loss — moving the cursor off a unit clears the mouseover and signals nothing. We add the loss fire, so addons observing UnitExists("mouseover") can react when it becomes false.

Payload: the event carries no args; handlers read UnitExists("mouseover"), which is nil/false at fire time on the loss path.

local f = CreateFrame("Frame")
f:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
f:SetScript("OnEvent", function()
    if event == "UPDATE_MOUSEOVER_UNIT" then
        if UnitExists("mouseover") then
            -- gained (or changed to) a mouseover unit
        else
            -- mouseover unit lost (the vanilla gap this fills)
        end
    end
end)

Transitions and who fires them:

From To Fires? Source
unit nothing yes ClassicAPI (stock is silent here)
unit gameobject yes ClassicAPI (mouseover unit genuinely lost)
gameobject nothing no — (never fired a gain; not a unit)
anything unit yes engine's own inline fire (gain / unit→unit)

Implementation notes

The mouseover GUID lives in two globals written only by the engine's mouseover set/clear chokepoint (0x00492890), so every gain / loss / change flows through it — on a unit gain it fires UPDATE_MOUSEOVER_UNIT itself, on loss it clears the GUID silently. We co-hook that function: resolve whether the mouseover is a unit before and after the original, and fire on the unit→non-unit transition. Gating on unit type (not just "GUID present → absent") is load-bearing — the same GUID slot also holds gameobjects and items, which never fire a gain, so the type check is what keeps moving off a herb node or chest from spuriously firing. The !hasUnit half prevents a double-fire on any →unit transition the engine already covered.

The target is change-gated by its callers (if it ran per-frame the gain event would spam while hovering), so it's a cool hook — no per-frame overhead.

UPDATE_SHAPESHIFT_FORM event

Fires whenever the local player's shapeshift form changes — entering or leaving cat / bear / travel / stance / shadowform / stealth / ghost wolf / etc. No payload; call GetShapeshiftFormID() from the handler to read the new form.

The engine has only the plural UPDATE_SHAPESHIFT_FORMS (fires when the list of available forms changes — learning a new form, not changing into one). This singular event fires when the current form changes.

local f = CreateFrame("Frame")
f:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
f:SetScript("OnEvent", function()
    if event == "UPDATE_SHAPESHIFT_FORM" then
        print("now in form", GetShapeshiftFormID())
    end
end)

Implementation notes

Hooks the engine's bonus-action-bar refresh helper at 0x004E4FC0, which the engine calls every time the local player's UNIT_BYTES_1 descriptor field is broadcast and from FUN_004908C0 at post-login init. After the original runs, the post-hook reads byte 2 of UNIT_BYTES_1 (descriptor +0x212 — the form ID) and fires UPDATE_SHAPESHIFT_FORM only when the value differs from the last seen — filtering out the spurious recomputes for other UNIT_BYTES_1 bytes (standstate, etc.) the engine also routes through this helper.

The cached "last form" sentinel uses -1 for "player descriptor not yet resolvable" so transient resolution failures during early login don't get misread as leaving a form.

UNIT_SPELLCAST_* events

Cast/channel events for the local player and other units. Cast-bar / rotation addons register these instead of the arg-less SPELLCAST_* events and read unit, castGUID, spellID directly. Fifteen events are provided; six also fire for non-player units, and two fire only for your target and focus:

Event Fires when Units Args
UNIT_SPELLCAST_SENT CMSG_CAST_SPELL leaves the client (earliest point) player unit, target, castGUID, spellID, spellName, rank
UNIT_SPELLCAST_START a cast-time spell begins all unit, castGUID, spellID, spellName, rank
UNIT_SPELLCAST_STOP a cast-time spell ends (any reason) all same
UNIT_SPELLCAST_DELAYED pushback extends the cast player same
UNIT_SPELLCAST_SUCCEEDED the spell goes off (SMSG_SPELL_GO) — incl. instants all same
UNIT_SPELLCAST_INTERRUPTED a started cast is interrupted (a kick, or a cancel by moving or jumping) — never for channels all same
UNIT_SPELLCAST_FAILED a cast fails with an error shown: it is refused before it starts (range, mana, cooldown, moving), or a started cast fails (line of sight, target died) player same
UNIT_SPELLCAST_FAILED_QUIET a cast fails with no error shown (spammy retry, reticle cancel, …) player same
UNIT_SPELLCAST_CHANNEL_START a channel begins all same
UNIT_SPELLCAST_CHANNEL_UPDATE pushback shortens a channel player same
UNIT_SPELLCAST_CHANNEL_STOP a channel ends all same
UNIT_SPELLCAST_RETICLE_TARGET a ground-target reticle appears (AoE placement — Blizzard, Flare, …) player unit, "", spellID, spellName, rank
UNIT_SPELLCAST_RETICLE_CLEAR the reticle is placed or cancelled player unit, "", spellID, spellName, rank
UNIT_SPELLCAST_INTERRUPTIBLE the unit's current cast can be interrupted again target, focus unit
UNIT_SPELLCAST_NOT_INTERRUPTIBLE the unit's current cast can no longer be interrupted target, focus unit

unit (arg1) is the token of the casting unit — "player" for your own casts, or a unit token ("target", "focus", "party3", "nameplate2", "pet", "mouseover", …) for another unit. spellName / rank are ClassicAPI tail extensions; addons reading only the first three positional args are unaffected.

Per-token fan-out. A caster GUID can map to several tokens at once (your target is also party2 and nameplate1). The event fires once per token currently pointing at the caster, so a target-frame cast bar, a party2 frame, and a nameplate cast bar each get their own event with the same castGUID. Tokens are resolved fresh at fire time (they shift frame-to-frame). If you run SuperWoW, its raw-GUID token ("0x…") is deliberately filtered out — only standard tokens are fanned out.

Non-player limits. Only the six events above fire for other units, and they're best-effort — driven purely by the packets an observer receives:

  • SENT never fires (only your own outgoing casts are visible).
  • DELAYED / CHANNEL_UPDATE never fire (pushback is sent only to the caster, so another unit's bar can't stretch/shrink from damage).
  • FAILED never fires (a pre-cast requirement failure is client-local).
  • A remote cast/channel only has timing from the moment its SMSG_SPELL_START was observed; casters who were already casting when they came into range have no start time.

Channels never fire INTERRUPTED. Only CHANNEL_STOP fires when a channel ends, whether it completed or was cut short, for both the player and other units. INTERRUPTED is a cast-only event.

Channels without a bar. Some channels are flagged to show no channel bar (Hakkar's Blood Siphon, for example). They fire no channel events, and C_Spell.UnitChannelInfo returns nothing for them.

Interruptible events. NOT_INTERRUPTIBLE and INTERRUPTIBLE fire when the notInterruptible value of your target's or your focus's current cast changes during that cast. For example, the caster gains or loses an aura that blocks your interrupts. The first value of a cast fires no event: read it from C_Spell.UnitCastingInfo or C_Spell.UnitChannelInfo at START. The only argument is the unit token.

Reticle events fire for ground-targeted (AoE) spells only, always for the player: RETICLE_TARGET when the placement reticle comes up, RETICLE_CLEAR when it's placed or cancelled. There's no cast yet, so the castGUID slot (arg2) is empty — the engine's event dispatcher can't emit a nil mid-argument-list, so arg2 is "" here. unit (arg1) and spellID (arg3) are exact; arg2 is inconsequential for a reticle.

castGUID. A synthesized string of the shape Cast-<type>-<serverID>-<instanceID>-<zoneUID>-<spellID>-<castUID>. Server / instance / zone aren't known here, so those three fields are 0; the load-bearing parts are the spellID (field 6, which addons strsplit("-") out) and a unique-per-cast castUID (field 7). Every event of one cast carries the same castGUID, so SENT → START/CHANNEL_START → SUCCEEDED → STOP/CHANNEL_STOP all pair up — including across the caster and observers (they converge on the same value), and a chained same-spell recast gets its own castUID. The type and castUID follow the spell-cast-GUID spec:

  • Type 3 (real casts — the common case): castUID is time-based — the low 23 bits are the cast's UNIX-epoch second, the higher bits a per-second counter.
  • Type 2 (UNIT_SPELLCAST_FAILED — a local-only cast that never reached the server): castUID is a plain locally-incrementing integer.

Ordering:

  • Cast-time spell: SENT → START → SUCCEEDED → STOP.
  • Channel: SENT → CHANNEL_START → SUCCEEDED → CHANNEL_STOP (CHANNEL_START before SUCCEEDED).
  • Instant: SENT → SUCCEEDED.

INTERRUPTED vs FAILED vs FAILED_QUIET: the reason that the game gives for the failure decides the event:

  • INTERRUPTED: the reason is an interrupt. This includes an enemy kick, and a cast that you cancel by moving, jumping or pressing Escape.
  • FAILED_QUIET: the reason is one of three that show no error: SPELL_IN_PROGRESS (casting while already casting, or a spell-queue rejection), DONT_REPORT (fake fails, a cancelled ground reticle) and CHARMED.
  • FAILED: any other reason. This includes a spell that never started (out of range, not enough mana, on cooldown, moving) and a started cast that fails at the end (line of sight, target died).

A started cast that fails then fires STOP. Holding the cast key while running fires FAILED once for each new attempt.

Channel pushback (player). Taking damage while channeling shortens the channel; CHANNEL_UPDATE fires on each hit and C_Spell.UnitChannelInfo's endTimeMs re-anchors to the server's new remaining time, so cast bars shrink correctly. (The event carries no time — it's a "re-read now" trigger; timing is read back from UnitChannelInfo.)

Every fire is gated on whether any frame is registered for that event, so the whole system costs one pointer-compare per state transition when no addon uses it (no arg synthesis, no DBC lookups, no per-token fan-out).

local f = CreateFrame("Frame")
for _, e in ipairs({
    "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_STOP",
    "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_CHANNEL_START",
}) do f:RegisterEvent(e) end
f:SetScript("OnEvent", function()
    -- vanilla passes event/arg1/... as globals, not function params
    if arg1 == "target" then print(event, arg3) end  -- arg3 = spellID
end)

Additive to the stock SPELLCAST_* events. The engine's own arg-less SPELLCAST_START / SPELLCAST_CHANNEL_UPDATE / … still fire as before; these UNIT_-prefixed events sit on top. The empowered-cast events (UNIT_SPELLCAST_EMPOWER_*) are not implemented — there are no empowered casts here.

USER_WAYPOINT_UPDATED event

Fires (with no payload) whenever the user waypoint changes — when C_Map.SetUserWaypoint places one, and when C_Map.ClearUserWaypoint removes one. Re-read the pin with C_Map.GetUserWaypoint when it fires.

Clearing while no pin is set changes nothing, so it fires nothing.

local f = CreateFrame("Frame")
f:RegisterEvent("USER_WAYPOINT_UPDATED")
f:SetScript("OnEvent", function()
    local point = C_Map.GetUserWaypoint()
    -- redraw the pin, or hide it when point is nil
end)

WEAPON_SLOT_CHANGED event

Fires (with no payload) when the item in a weapon slot changes: main hand (16), off hand (17), or ranged (18). Equip, unequip, and swap all fire it.

Slot 18 holds relics (Libram, Idol, Totem) for Paladins, Shamans, and Druids. A relic is not a weapon, so the event does not fire for one. UnitHasRelicSlot("player") reports which kind of slot 18 the player has.

Use this instead of PLAYER_EQUIPMENT_CHANGED when only the weapons matter — a swing-timer bar, or any code that re-reads UnitAttackSpeed and UnitRangedDamage.

The event collapses to one fire per frame. A single action that changes two weapon slots fires the event one time, not two.

local f = CreateFrame("Frame")
f:RegisterEvent("WEAPON_SLOT_CHANGED")
f:SetScript("OnEvent", function()
    SwingBars_ReadWeapons()
end)

Expansion

Helpers for addons that want to gate code on which expansion the client targets. We always answer as LE_EXPANSION_CLASSIC (0) — there's nothing to detect. The matching number constants (LE_EXPANSION_*) are in the Globals section.

GetClassicExpansionLevel()

Returns the live expansion level as a number. Always 0 (LE_EXPANSION_CLASSIC) here.

if GetClassicExpansionLevel() >= LE_EXPANSION_BURNING_CRUSADE then
    -- never taken on 1.12
end

ClassicExpansionAtLeast(expansionLevel)

Returns true iff GetClassicExpansionLevel() >= expansionLevel. That reduces to expansionLevel <= 0, so only ClassicExpansionAtLeast(LE_EXPANSION_CLASSIC) (and any negative argument) are true; every later expansion answers false.

Errors if expansionLevel is missing or non-numeric.

if ClassicExpansionAtLeast(LE_EXPANSION_WRATH_OF_THE_LICH_KING) then
    -- WotLK+ code path; never taken on 1.12
end

ClassicExpansionAtMost(expansionLevel)

Returns true iff GetClassicExpansionLevel() <= expansionLevel. That reduces to expansionLevel >= 0, so the only false answer is for negative input.

Errors if expansionLevel is missing or non-numeric.

if ClassicExpansionAtMost(LE_EXPANSION_CLASSIC) then
    -- vanilla / Classic Era only code path
end

GetExpansionLevel() / GetAccountExpansionLevel()

GetExpansionLevel() returns the expansion level of the game. GetAccountExpansionLevel() returns the expansion level of the account. Both always return 0 (LE_EXPANSION_CLASSIC).

local expansionLevel = GetExpansionLevel()          -- 0
local accountLevel = GetAccountExpansionLevel()     -- 0

GetMaxPlayerLevel()

Returns the highest level that a character can reach. Always 60.

if UnitLevel("player") == GetMaxPlayerLevel() then
    -- hide the experience bar
end

Faction

GetFactionIDByIndex(factionIndex)

Returns the factionID (Faction.dbc row ID) for the entry at the given 1-based displayed-faction index. The engine uses this internally to look up Faction.dbc, but does not expose it from Lua.

  • Returns the factionID for real factions.
  • Returns 0 for header / category rows ("Other", "Inactive", etc.).
  • Returns nil if the index is out of range.

The engine actually returns 0 for some header types ("Other") and -1 for others ("Inactive"-style pseudo-rows); we collapse both to 0 so the user-facing convention is consistent.

for i = 1, GetNumFactions() do
    local name, _, _, _, _, _, _, _, isHeader = GetFactionInfo(i)
    if not isHeader then
        local factionID = GetFactionIDByIndex(i)
        -- ...
    end
end

GetFactionInfoByID(factionID)

Returns the same eleven values as GetFactionInfo(factionIndex), keyed by factionID instead of displayed index:

name, description, standingID, barMin, barMax, barValue,
atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep

Works for any factionID present in Faction.dbc, not just factions the player has rep with:

  • In the player's reputation list — full data, identical to GetFactionInfo(displayedIndex).
  • Not in the reputation list — name and description from Faction.dbc, Neutral defaults for the rep fields: standingID = 4, barMin = 0, barMax = 3000, barValue = 0, all flags nil.
  • Invalid factionID (out of range or empty DBC slot) — nil.
local name, _, standing = GetFactionInfoByID(69)  -- Darnassus
-- name = "Darnassus", standing = 5 (Friendly), etc. (encountered)

local name = GetFactionInfoByID(574)  -- Caer Darrow (faction that can't be encountered in standard Vanilla)
-- name = "Caer Darrow" — works even if you never had rep with it

GetFactionParentID(factionID)

Returns the parent factionID for a faction in a hierarchy (e.g. Stormwind's parent is Alliance Forces; The Defilers's parent is Horde Forces). Returns 0 for top-level factions with no parent, or nil for invalid factionIDs.

GetFactionParentID(72)     -- 469 (Stormwind → Alliance)
GetFactionParentID(469)    -- 0   (Alliance is top-level)
GetFactionParentID(99999)  -- nil

We expose this as its own getter, since GetFactionInfo doesn't have the slot.

Reads Faction.dbc ParentFactionID at record +0x48 directly — no displayed-list dependency, works for any faction in the DBC regardless of whether the player has rep with it.

C_Reputation.GetFactionStandings()

Returns a flat { [factionID] = currentStanding } table covering every faction in the player's reputation list. currentStanding is the same value GetFactionInfo puts in its barValue slot — `base

  • delta` from the rep slot, signed.

Always returns a table (possibly empty); never nil.

local standings = C_Reputation.GetFactionStandings()
for factionID, standing in pairs(standings) do
    print(GetFactionInfoByID(factionID), standing)
end

Unlike a GetNumFactions + GetFactionInfo walk, this skips header rows entirely and doesn't depend on the player having opened the reputation pane recently — it reads straight out of the per-faction rep-slot array, which the engine keeps populated for every faction the player has rep with.

If you need names instead of IDs, layer GetFactionInfoByID on top:

local byName = {}
for factionID, standing in pairs(C_Reputation.GetFactionStandings()) do
    byName[GetFactionInfoByID(factionID)] = standing
end

This is a ClassicAPI-only call. It returns a flat { [factionID] = standing } map rather than an array of struct tables.

C_Reputation.GetWatchedFactionData()

Returns a table describing the faction shown above the XP bar, or nil if no faction is being watched. The stock GetWatchedFactionInfo() returns the same data as a 5-tuple without the factionID; this struct accessor includes it.

Returns the same FactionData table shape as GetFactionDataByIndex — see that section for the full field list. isWatched is forced true (this faction IS the watched one by definition).

local data = C_Reputation.GetWatchedFactionData()
if data then
    print(string.format("%s (%d): %d / %d",
        data.name, data.factionID,
        data.currentStanding - data.currentReactionThreshold,
        data.nextReactionThreshold - data.currentReactionThreshold))
end

Implementation reads the watched RepListID from the player's +0xE68 sub-struct, indexes the per-faction rep slot array at 0x00B73290 to recover the factionID, then runs the shared ReadFactionData chain (Faction.dbc lookup, reaction band, rep slot flags, header/collapsed checks).

C_Reputation.GetFactionDataByID(factionID)

Returns a FactionData table for a faction by ID, or nil when the ID has no Faction.dbc record.

The table has the same shape as C_Reputation.GetFactionDataByIndex — see that section for the full field list.

The faction does not have to be in the player's reputation list. One the character has never encountered fills cleanly, with currentStanding 0 and atWarWith false, so you can read any faction's name, description, and standing thresholds without walking the displayed list.

local d = C_Reputation.GetFactionDataByID(69)   -- Darnassus
if d then
    print(d.name, d.currentStanding, d.nextReactionThreshold)
end

C_Reputation.GetFactionDataByIndex(factionSortIndex)

Returns a FactionData table for the faction at the given 1-based displayed-list index, or nil for an out-of-range index or a pseudo-row ("Other" / "Inactive" placeholders that don't have a Faction.dbc record). The index range matches what GetNumFactions covers — real factions plus category headers.

Fields:

Field Type Notes
factionID number Faction.dbc record ID.
name string Locale-applied.
description string Locale-applied (may be "").
reaction number 1=Hated .. 8=Exalted.
currentReactionThreshold number Band min standing value.
nextReactionThreshold number Band max standing value.
currentStanding number Current standing (base + delta).
atWarWith boolean Rep slot flags bit 0x02.
canToggleAtWar boolean currentStanding ≥ -3000 AND not peace-forced (flags bit 0x10).
isHeader boolean Faction is a category header in the displayed list.
isHeaderWithRep boolean Always false — parent factions don't aggregate rep.
isCollapsed boolean UI state: user has collapsed this header.
isWatched boolean This faction is shown above the XP bar.
canSetInactive boolean True when !isHeader && repListIndex ≥ 0 — i.e. the engine's SetFactionInactive/SetFactionActive will accept this faction.
isChild boolean Always false — no parent-child rep here.
hasBonusRepGain boolean Always false.
isAccountWide boolean Always false.
for i = 1, GetNumFactions() do
    local d = C_Reputation.GetFactionDataByIndex(i)
    if d and not d.isHeader and d.currentStanding < d.nextReactionThreshold then
        -- still grindable rep
    end
end

Implementation: resolves the 0-based index to a factionID via the engine's FUN_RESOLVE_FACTION_INDEX at 0x004D5FA0, then runs the shared ReadFactionData chain — no Lua-side round-trip through Script_GetFactionInfo. isHeader / isCollapsed come from the displayed-list header array at 0x00B736C0 (count at 0x00B736B0) and the per-character bitmask at 0x0084A0A4.

C_Reputation.SetSelectedFactionByID(factionID)

ClassicAPI extension. Selects a faction in the reputation pane by ID — the same convenience C_Reputation.SetWatchedFactionByID gives over the stock index-based call.

The selection is held as a faction ID, so this sets it directly and the faction does not have to be in the displayed list at the time. GetSelectedFaction() maps the selection back to a 1-based displayed-list index, and reports 0 while the faction is not listed.

Passing 0 clears the selection. Negative IDs are ignored.

C_Reputation.SetSelectedFactionByID(69)      -- Darnassus
print(GetFactionInfo(GetSelectedFaction()))  -- "Darnassus"

C_Reputation.SetWatchedFactionByID(factionID)

Sets the faction shown above the XP bar by ID. The stock SetWatchedFactionIndex(displayedIndex) accepts only a 1-based displayed-list index, which forces addons to walk the index list themselves. This wrapper takes a factionID directly.

  • factionID > 0 — sets the watched faction. Works even for factions the player hasn't yet encountered (the rep bar will show nothing until the player gains rep, then displays normally).
  • factionID == 0 — clears the watched faction.
  • factionID < 0 — silent no-op.

Returns nothing. The engine's UI / event machinery refreshes automatically — GetWatchedFactionInfo() reflects the new state on the next call.

C_Reputation.SetWatchedFactionByID(72)  -- Stormwind
print(GetWatchedFactionInfo())          -- "Stormwind", ...

C_Reputation.SetWatchedFactionByID(0)   -- clear
print(GetWatchedFactionInfo())          -- (empty)

Implementation calls the engine's inner watched-faction setter directly, bypassing Script_SetWatchedFactionIndex's displayed-index round-trip.

C_Reputation.ToggleFactionAtWarByID(factionID)

ClassicAPI extension. Flips a faction's at-war state by ID rather than by displayed-list position.

Every rule the stock index-based call applies still applies here. The toggle is refused when the faction is peace-forced (your own capitals, some quest factions), and peace can't be declared while standing is below -3000. Read C_Reputation.GetFactionDataByID first to see whether a faction will accept the change:

local d = C_Reputation.GetFactionDataByID(87)   -- Bloodsail Buccaneers
if d.canToggleAtWar then
    C_Reputation.ToggleFactionAtWarByID(87)
end

Non-positive IDs are ignored. The change is sent to the server, so it sticks the same way the reputation pane's checkbox does, and UNIT_FACTION fires for "player" when the state actually changes — a refused toggle stays silent.

C_Reputation.IsFactionActive(factionSortIndex) / C_Reputation.IsFactionActiveByID(factionID)

Returns whether a faction is not filed under the "Inactive" heading in the reputation pane. IsFactionActive takes a 1-based displayed-list position; the ByID form (a ClassicAPI extension) takes a faction ID.

Both report false for anything that isn't a real faction with a reputation slot — an out-of-range position, a category header, or an ID the character has no slot for.

C_Reputation.IsFactionActiveByID(87)        -- true until you park it
C_Reputation.SetFactionInactiveByID(87)
C_Reputation.IsFactionActiveByID(87)        -- now false

C_Reputation.SetFactionInactiveByID(factionID) / C_Reputation.SetFactionActiveByID(factionID)

ClassicAPI extension. Moves a faction into or out of the "Inactive" category by ID rather than by displayed-list position.

Marking a faction inactive files it under the collapsible "Inactive" heading in the reputation pane, which is how players park factions they have finished with. The displayed list is rebuilt, so positions of the other factions shift — read them again afterwards rather than reusing an index from before the call.

C_Reputation.SetFactionInactiveByID(87)   -- park Bloodsail Buccaneers
C_Reputation.SetFactionActiveByID(87)     -- and bring it back

Non-positive IDs are ignored. A faction with no reputation slot is left alone. The change is sent to the server, and UNIT_FACTION fires for "player" when the state actually changes.

canSetInactive on C_Reputation.GetFactionDataByID tells you whether a faction can take the change.

C_Reputation.GetLastStandingChange()

Returns factionID, newStanding, repGained for the rep change currently being fired, or nil if called outside that window.

Companion to FACTION_STANDING_CHANGED. The triple is the same payload as the event's arg1, arg2, arg3, but the getter is also live during the engine's CHAT_MSG_COMBAT_FACTION_CHANGE dispatch on the same hook — useful for addons that want to enrich the chat line with the factionID (which the chat-event payload doesn't carry) without double-registering for FACTION_STANDING_CHANGED:

local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_COMBAT_FACTION_CHANGE")
f:SetScript("OnEvent", function()
    local factionID, newStanding, repGained = C_Reputation.GetLastStandingChange()
    if factionID then
        -- factionID is the real ID, not a parsed-out name string
        print(factionID, newStanding, repGained)
    end
end)

Outside the in-flight FireNotify hook stack (i.e. anywhere except inside a CHAT_MSG_COMBAT_FACTION_CHANGE or FACTION_STANDING_CHANGED handler), returns nothing. There is no "last change" memory — the state is cleared as soon as the dispatch returns.

This is a ClassicAPI-only call. Addons can otherwise get the factionID from FACTION_STANDING_CHANGED directly.

ExpandAllFactionHeaders() / CollapseAllFactionHeaders()

Expands or collapses every header of the reputation list. Takes no arguments and returns nothing.

The list is sorted again and UPDATE_FACTION fires, as with ExpandFactionHeader(index) and CollapseFactionHeader(index). Nothing goes to the server, and the state is not saved.

ExpandAllFactionHeaders()
for i = 1, GetNumFactions() do
    print(GetFactionInfo(i))
end

Focus

The focus is one unit that you keep as a second target. Use the "focus" unit token to address it in every UnitX function.

The focus stays through /reload. It clears when you log out.

When the focus clears. The focus clears, and PLAYER_FOCUS_CHANGED fires, when one of these occurs:

  • The unit goes out of range or despawns. The focus stays when the unit is you, your pet, a party or raid member, or the pet of one. When that unit comes back into range, it is the focus again.
  • The unit dies and it is not friendly to you.
  • The unit comes back into range dead, and it is not friendly to you.
  • The unit changes so that you cannot select it.

You can focus a unit that is already dead or that you cannot select. The focus clears only when the unit changes to that state.

%f and %t in chat. In a chat message that you send, %f changes to the name of your focus, and %t (also %n) changes to the name of your target. When the unit has a raid marker, the name starts with that marker ({rt8}Name). With no focus, %f changes to FOCUS_TOKEN_NOT_FOUND ("<no focus>"). With no target, %t changes to TARGET_TOKEN_NOT_FOUND.

SendChatMessage("Sheep on %f", "PARTY")   -- "Sheep on {rt5}Defias Thug"
SendChatMessage("Kill %t first", "RAID")  -- "Kill {rt8}Defias Pillager first"

FocusUnit(unit)

Sets focus to the given unit. Argument is a unit token — "target", "mouseover", "party1", "nameplate1", anything the resolver accepts. Calling with no argument is shorthand for FocusUnit("target") (the /focus slash command default).

FocusUnit("target")        -- pin the current target
FocusUnit("nameplate1")    -- pin the unit behind nameplate1
FocusUnit()                -- same as FocusUnit("target")

Fires PLAYER_FOCUS_CHANGED if the resolved GUID differs from the current focus. No-op (no event) if the same unit is already focused (identity-checked first).

Passing a token that doesn't resolve to a unit (e.g. "target" with nothing targeted, or "party5" solo) clears focus — same as calling ClearFocus().

ClearFocus()

Drops the focus. Fires PLAYER_FOCUS_CHANGED if there was one; no-op otherwise.

ClearFocus()

Unit token (focus / focustarget)

"focus" resolves to whatever GUID FocusUnit last stashed. Accepted by every UnitX function:

local name = UnitName("focus")             -- nil if no focus
local hp   = UnitHealth("focus")
local _, class = UnitClass("focus")

Chains compose through the engine's standard suffix walker: "focustarget", "focustargettarget", etc. — same behavior as "targettarget", mirrored instruction-for-instruction in our hook so addons get the engine semantics they expect.

Returns nil cleanly when no focus is set; doesn't raise the "Unknown unit name" error.

Unit events fire for "focus". Every unit event fires with arg1 == "focus" for the focused unit, as it does for "target". This includes UNIT_HEALTH, UNIT_AURA, UNIT_COMBAT, UNIT_NAME_UPDATE, UNIT_PORTRAIT_UPDATE, and UNIT_MODEL_CHANGED. The events fire also when the focus is not your target, and for a focused groupmate out of range. A unit that is your target and your focus fires each event two times: one time with "target" and one time with "focus". (OnEvent handlers read the arg1 global, not a function parameter.)

Unit events for "focustarget". The unit that your focus targets also fires its unit events, with arg1 == "focustarget". This is a ClassicAPI extension. When the focus changes its target, the events follow the new target within one frame.

UnitTokenFromGUID scans "focus" right after "target", so a focused unit's GUID reverse-resolves to "focus" only if it isn't already addressable as "player" / "party*" / "raid*" / "nameplate*" / "target".

Bindings

Key bindings that cast a spell, use an item, run a macro or click a button; temporary override bindings; and modified clicks.

Direct-action bindings

Function Command it binds
ok = SetBindingSpell(key, spell) "SPELL " .. spell
ok = SetBindingItem(key, item) "ITEM " .. item
ok = SetBindingMacro(key, macro) "MACRO " .. macro
ok = SetBindingClick(key, buttonName [, mouseButton]) "CLICK " .. buttonName .. ":" .. (mouseButton or "LeftButton")

Each function binds key in the current binding set, as SetBinding does. It returns 1 when the binding changed, else nil, and fires UPDATE_BINDINGS. Call SaveBindings to keep the change.

A key bound to one of these commands does this:

Command When What
SPELL name Key release Casts the spell from your spellbook.
ITEM name Key release Uses the item with that name that you carry.
MACRO macro Key release Runs the macro. A number is a macro index; other text is a macro name.
CLICK name:button Key press and key release Sends the frame a mouse down on press and a mouse up on release.

SPELL and ITEM cast at you when the SELFCAST modified click is held, and at your focus when FOCUSCAST is held. Otherwise they use your target.

For CLICK, a button acts as if it was clicked with the mouse. It clicks on press when it is registered for any ...Down click, and on release when it is registered for any ...Up click. Its pushed look, OnMouseDown, OnMouseUp, PreClick, OnClick, PostClick and OnDoubleClick all run. The cursor does not have to be over the button.

RunBinding does not run these four commands.

Modifier keys during a binding

While a SPELL, ITEM, MACRO or CLICK binding runs, the modifier keys that are part of its key do not count as held. For a binding on ALT-1:

  • IsAltKeyDown(), IsLeftAltKeyDown() and the other modifier functions return nil for ALT.
  • IsModifiedClick("SELFCAST") is false when SELFCAST is ALT, so the binding does not cast at you.
  • [mod:alt] does not pass.

A binding that runs Lua sees the modifier keys that are held. A handler for SHIFT-E gets 1 from IsShiftKeyDown().

Override bindings

SetOverrideBinding(owner, isPriority, key [, command])
SetOverrideBindingSpell(owner, isPriority, key, spell)
SetOverrideBindingItem(owner, isPriority, key, item)
SetOverrideBindingMacro(owner, isPriority, key, macro)
SetOverrideBindingClick(owner, isPriority, key, buttonName [, mouseButton])
ClearOverrideBindings(owner)

An override binding replaces the saved binding of key until it is removed. It is never saved.

  • owner must be a frame.
  • isPriority is any value. A true value makes a priority override.
  • key is matched without regard to case. Write the modifiers in the order the game uses: ALT-CTRL-SHIFT-X.
  • A command of nil or "" removes the owner's override for key.
  • ClearOverrideBindings(owner) removes all of the owner's overrides.

For each key, the newest priority override wins. With no priority override, the newest normal override wins. With no override, the saved binding is used. When you remove an override, the next one is used again.

Overrides also apply to keys with fewer modifiers. See Keys with modifiers.

All overrides are removed on /reload and logout.

GetBindingAction(key [, checkOverride])

Returns the command bound to key. With checkOverride true, an override binding is returned first when one is active.

Keys with modifiers

If a key with modifiers has no binding, the game tries the same key with fewer modifiers. The game uses the first key in the list that has a binding.

Key pressed Keys that the game tries, in this order
SHIFT-1 SHIFT-1, 1
ALT-SHIFT-1 ALT-SHIFT-1, SHIFT-1, ALT-1, 1
ALT-CTRL-SHIFT-1 ALT-CTRL-SHIFT-1, CTRL-SHIFT-1, ALT-SHIFT-1, ALT-CTRL-1, SHIFT-1, CTRL-1, ALT-1, 1

For each key, the game uses an override before the saved binding. The game ignores a binding to a command that does not exist, and tries the next key.

A modifier that is not part of the matched key counts as held. For example, ALT-SHIFT-1 runs the binding on ALT-1. In that binding, IsShiftKeyDown() returns 1 and [mod:shift] passes.

GetBindingByKey(key)

Returns the command that a press of key runs, or nil. It uses the order in Keys with modifiers, and includes overrides.

Function keys F13 to F24

The keys F13 to F24 work like F1 to F12. You can bind them with SetBinding, in the key binding window, or as override bindings. A binding can use modifiers, for example CTRL-F13. OnKeyDown and OnKeyUp scripts get the key name, for example "F15".

Most keyboards do not have these keys. Macro tools and some gaming keyboards can send them.

Modified clicks

A modified click is an action that is active while a set of modifier keys, and optionally a mouse button, is held.

IsModifiedClick([action])            -- 1 or nil
GetModifiedClick(action)             -- "ALT", "SHIFT-BUTTON1", "NONE", ...
SetModifiedClick(action, binding)
GetNumModifiedClickActions()
GetModifiedClickAction(index)
  • IsModifiedClick(action) returns 1 when the action's keys are held. It also takes modifier names: IsModifiedClick("SHIFT"), IsModifiedClick("LCTRL"). With no argument, it returns 1 when any modifier key is held.
  • SetModifiedClick changes the current binding set. Call SaveBindings to keep the change. binding is modifier names joined with -, optionally followed by BUTTON1 to BUTTON5, or "NONE". An unknown action or binding raises an error.
  • A <ModifiedClick action="NAME" default="BINDING"/> line in an addon's Bindings.xml adds an action.

The actions and their defaults:

Action Default
SELFCAST ALT
FOCUSCAST NONE
AUTOLOOTTOGGLE SHIFT
MAILAUTOLOOTTOGGLE SHIFT
STICKYCAMERA CTRL
CHATLINK SHIFT-BUTTON1
DRESSUP CTRL-BUTTON1
SOCKETITEM SHIFT-BUTTON2
SPLITSTACK SHIFT
PICKUPACTION SHIFT
PICKUPITEM SHIFT
COMPAREITEMS SHIFT
OPENALLBAGS SHIFT
QUESTWATCHTOGGLE SHIFT
TOKENWATCHTOGGLE SHIFT
SHOWITEMFLYOUT ALT
SHOWMULTICASTFLYOUT ALT
TRADESEARCHADD SHIFT

The [mod:...] macro condition takes the same values as IsModifiedClick: [mod:shift], [mod:lctrl], [mod:SELFCAST].

ActionButtonUseKeyDown

A setting for the action-bar key bindings (main bar, side bars, pet bar). 1 uses the action when the key is pressed. 0 (the default) uses it when the key is released.

/console ActionButtonUseKeyDown 1

Predefined focus bindings (FOCUSTARGET / TARGETFOCUS)

Two key bindings appear in the keybind UI (Esc → Key Bindings) under the Targeting Functions group, between PETATTACK and the Interface section:

Binding Lua action Use
FOCUSTARGET FocusUnit("target") Pin current target as focus
TARGETFOCUS TargetUnit("focus") Switch target to the focus

No default key — assign one in the keybind UI like any other binding. The two together give you a quick "set focus → swap to focus" loop common in PvP / heal-tank-while-killing-add scenarios.

Implementation note: addon-side Bindings.xml files always render at the bottom of the keybind list (orphaned from any header="..." they declare) because the engine's binding registry is linearly indexed by insertion order and addons load after FrameXML. To land inside the TARGETING block, the DLL splices the two <Binding> entries into the engine's Interface\FrameXML\Bindings.xml at file-read time via a hook on FUN_FILE_READ — see src/bindings/Inject.cpp.

Frame

Region/Frame method backports. Addon ports routinely call these methods unconditionally — they're the C-level frame surface addons take for granted. This section is that surface. Methods are registered on the engine's own per-frame-type method registries (SetSize/GetSize on the Region base, so they resolve on frames, buttons, textures and fontstrings alike), and each delegates to the engine's own implementations — UI-scale conversion, object resolution and type checking are all the engine's code.

region:SetPoint("point") (one-argument form)

The stock SetPoint parser accepts every call shape — (point, region), (point, region, relativePoint [, x, y]), (point, x, y), even an explicit nil region — except the bare one-argument form, which raises the usage error (a fully-omitted third argument fails its type validation). A co-hook on the engine's Script_SetPoint normalizes region:SetPoint("RIGHT") to (point, 0, 0) — the parent-relative form (anchor to the parent's same point, zero offsets).

region:SetSize(width, height) / region:GetSize([ignoreRect])

The combined setter/getter for SetWidth+SetHeight / GetWidth+GetHeight. Works on any region type — frames, buttons, textures (including engine-created ones like button:GetNormalTexture()), fontstrings. ignoreRect has the same effect as on GetWidth and GetHeight.

region:GetWidth([ignoreRect]) / region:GetHeight([ignoreRect])

Returns the width or the height of the region. A region with no set size returns the size that its anchors give it, multiplied by the effective scale of the region. Thus the value changes with the UI scale. If ignoreRect is true, the method returns only the set size, which is 0 for a region with no set size.

GetUnscaledWidth and GetUnscaledHeight return the size in the same units as GetTop and GetBottom.

-- f has an effective scale of 0.75
local f = CreateFrame("Frame", nil, UIParent)
f:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0)
f:SetPoint("BOTTOMLEFT", UIParent, "TOPLEFT", 0, -160)
f:GetTop() - f:GetBottom()  -- 160
f:GetHeight()               -- 120
f:GetHeight(true)           -- 0

region:GetUnscaledWidth([ignoreRect]) / region:GetUnscaledHeight([ignoreRect]) / region:GetUnscaledSize([ignoreRect])

These methods return the width, the height, or both, of the region. A region with no set size returns the size that its anchors give it, in the same units as GetTop and GetBottom. This value is the same at every UI scale. A region with a set size returns that size, as GetWidth and GetHeight do. ignoreRect has the same effect as on GetWidth and GetHeight. Works on any region type. This is a ClassicAPI extension.

If you set the size or position of a region from the size of another region, use these methods.

-- the same f as in the GetHeight example
f:GetUnscaledHeight()       -- 160
f:GetUnscaledSize()         -- <width>, 160
f:GetUnscaledHeight(true)   -- 0

region:GetScale() / region:GetEffectiveScale()

Frames have these methods by default. Textures and font strings have them too. A texture or a font string has no scale of its own, so GetScale returns 1. GetEffectiveScale returns the effective scale of the frame that owns the region.

region:IsMouseOver([topOffset, bottomOffset, leftOffset, rightOffset])

true when the mouse cursor is within the region's rectangle, false otherwise. The four optional offsets shift the corresponding edge before the test (added to that edge: positive topOffset / rightOffset and negative bottomOffset / leftOffset enlarge the hit area). Registered on the Region base, so it works on any region type — frames, buttons, textures, fontstrings.

The test uses the scale of the region. A cursor exactly on an edge is outside the rectangle. A region with no position or no size returns false.

ItemRefTooltip:IsMouseOver()                 -- true while hovering the tooltip
Minimap:IsMouseOver(20, -20, -20, 20)        -- true within a 20px halo around it

region:GetRect()

Returns the region's left, bottom, width, height in one call (the combined form of GetLeft+GetBottom+GetUnscaledWidth+GetUnscaledHeight). Thus all four values use the same units. On the Region base — works on any region type. Returns nothing for a region with no resolved rect (unpositioned).

UIParent:GetRect()   -- 0, 0, <screenWidth>, <screenHeight>
Minimap:GetRect()    -- <left>, <bottom>, 140, 140

region:GetScaledRect()

Returns the same four values as GetRect, multiplied by the effective scale of the region. The values are in screen units, so you can compare the rects of regions that have different scales. Returns nothing for a region with no position.

region:IsRectValid()

Returns true when the anchors and the size of the region give it a position. Returns false otherwise, for example for a region with no anchors. If this method returns true, GetLeft and GetRect return numbers.

local f = CreateFrame("Frame")
f:SetSize(10, 10)
f:IsRectValid()          -- false
f:SetPoint("CENTER")
f:IsRectValid()          -- true

region:GetPoint([index])

Returns the anchor at index (1 when omitted) as point, relativeTo, relativePoint, x, y. The anchors are in point order, from TOPLEFT to BOTTOMRIGHT. The method returns nothing when there is no such anchor.

local point, relativeTo, relativePoint, x, y = MyFrame:GetPoint(1)

relativeTo is nil when the anchor is relative to the screen. A frame with no parent gets this anchor from SetPoint without a relativeTo, and from XML anchors without one.

region:GetPointByName(point)

Returns the anchor of the region at point (for example "TOPLEFT"), with the same values as GetPoint. Returns nothing when the region has no anchor at that point.

local point, relativeTo, relativePoint, x, y = MyFrame:GetPointByName("TOPLEFT")

region:AdjustPointsOffset(x, y) / region:ClearPointsOffset()

AdjustPointsOffset adds x and y to the offset of each anchor of the region. ClearPointsOffset sets the offset of each anchor to 0. Each anchor keeps its point, its relative region and its relative point.

t:SetPoint("CENTER", 5, 7)
t:AdjustPointsOffset(1, -1)   -- the anchor is now ("CENTER", 6, 6)
t:ClearPointsOffset()         -- the anchor is now ("CENTER", 0, 0)

ClearPointsOffset does not undo AdjustPointsOffset. It removes all of the offset, also the offset that SetPoint gave.

region:ClearPoint(point)

Removes the anchor of the region at point. The other anchors stay. The region does not move until its anchors or its size change again, the same as after ClearAllPoints.

MyFrame:ClearPoint("BOTTOMRIGHT")
MyFrame:SetPoint("BOTTOMRIGHT", OtherFrame, "BOTTOMRIGHT")

region:IsDragging()

true while the region is the frame currently being moved or resized by the mouse — i.e. after StartMoving/StartSizing and before StopMovingOrSizing — false otherwise. Reads the engine's single global drag target (the same slot StopMovingOrSizing checks to decide whether it owns the active drag). On the Region base; non-frame regions (textures, fontstrings) always report false, since only frames can be dragged.

-- in a frame's OnUpdate while the user drags it:
if self:IsDragging() then ... end

region:GetDebugName([preferParentKey])

Returns the name that the debug tools show for a region.

local f = CreateFrame("Frame", "SomeFrame", UIParent)
f.Icon = f:CreateTexture()
print(f:GetDebugName())                 -- "SomeFrame"
print(f.Icon:GetDebugName())            -- "SomeFrame.Icon"
print(f:CreateTexture():GetDebugName()) -- "SomeFrame.1a2b3c4d"
print(CreateFrame("Frame"):GetDebugName() == "") -- true
  • A region with a global name returns that name.
  • A region with no name returns the debug name of its parent, a dot, and the key that holds the region in the parent's table.
  • If no key in the parent holds the region, the address of the region (in hex) replaces the key.
  • A region with no name and no parent returns an empty string.
  • If preferParentKey is true, the parent-key form comes before the global name, at each level of the chain.

region:SetToDefaults()

Puts a frame, a texture or a font string back in the state it has when it is first made with no template. Use it to reuse a frame from a pool.

The state that the template, CreateFontString or the frame type gave the object is also removed:

  • A frame loses its anchors, size, scripts, attributes and mouse input. Its alpha and scale become 1, its frame strata becomes MEDIUM and its frame level becomes one more than the level of its parent. The frame is shown.
  • A button also loses its text, its font objects and its highlight lock. It is enabled and in the NORMAL state. It has no mouse input, but a new button has mouse input.
  • A texture loses its file, texture coordinates and vertex color. Its blend mode becomes BLEND and its draw layer becomes BACKGROUND.
  • A font string loses its text and its font. GetFont() returns nil, so set a font or a font object before you set text again. The text color becomes white, the shadow becomes black with no offset, and the justify becomes CENTER and MIDDLE.

These do not change: the parent, keyboard and mouse wheel input, the registered events, the child regions of a frame (for example, the textures and the label of a button), and the fields on the Lua table of the object.

region:GetSourceLocation()

Returns where the region was created, as a string.

local f = CreateFrame("Frame")
print(f:GetSourceLocation())            -- "Interface\AddOns\MyAddon\Core.lua:12"
print(PlayerFrame:GetSourceLocation())  -- "Interface\FrameXML\PlayerFrame.xml"
  • A frame that CreateFrame made returns the script file and the line of that call, as file:line. The file name is the chunk name, as in error messages. A /run or loadstring chunk shows as [string "..."].
  • A frame that an XML file defines returns the path of that XML file. There is no line number.
  • A texture or font string returns the location of its parent frame.
  • The method returns nil when no script and no XML file made the frame.

If CreateFrame runs inside pcall, the location is the script that called pcall. Child frames that a template adds to a CreateFrame frame return the location of that CreateFrame call.

AddSourceLocationExclude(fileName)

Makes GetSourceLocation skip a script file. A frame that code in that file creates returns the location of the code that called into the file. Use it in a file that creates frames for other code, such as a pool or a factory:

AddSourceLocationExclude("MyFramePool.lua")

fileName is the name of the file without its folder. Case is ignored.

GetMouseFoci()

Returns a table of the frames that get mouse motion at the cursor, from top to bottom. Index 1 is the frame that GetMouseFocus() returns, so GetMouseFocus() == GetMouseFoci()[1] is always true.

Usually the table holds one frame: the top mouse-enabled frame under the cursor. It holds more frames when frames pass mouse motion on with frame:SetPropagateMouseMotion(true). The table is empty when no frame gets mouse motion.

local foci = GetMouseFoci()
-- foci[1] == GetMouseFocus()
for i = 1, table.getn(foci) do
    print(foci[i]:GetName() or "<anonymous>")
end

CreateFrame(frameType [, name, parent, template, id])

template can be a list of templates. Commas or spaces divide the names. The templates apply from left to right, so a later template overrides an earlier one. All of the templates apply before the frame's OnLoad runs.

local b = CreateFrame("Button", nil, UIParent,
                      "UIPanelButtonTemplate, MyButtonTemplate", 3)
b:GetID()   -- 3

Each name in the list must be a template. If one name is not a template, CreateFrame gives the error CreateFrame(): Couldn't find inherited node "<name>".

id is a number or a string. It sets the frame ID before OnLoad runs. As a result, self:GetID() in OnLoad returns this value. The frame ignores a negative id.

CreateFrame("Cooldown" [, name, parent, template])

Creates a cooldown frame. The frame shows a dark sweep that moves clockwise over its area, then a short flash, then hides itself.

local cd = CreateFrame("Cooldown", nil, button, "CooldownFrameTemplate")
cd:GetObjectType()              -- "Cooldown"
cd:IsObjectType("Cooldown")     -- true
cd:SetCooldown(GetTime(), 10)   -- start, duration in seconds
cd:SetReverse(false)
cd:GetReverse()                 -- false
cd:SetDrawEdge(true)
cd:GetDrawEdge()                -- true
Method Notes
cooldown:SetCooldown(start, duration [, modRate]) Starts the sweep and shows the frame. start and duration are numbers in seconds. start is a time value, for example the value that GetTime() returns. When duration is over, the frame shows the flash for one second and then hides. modRate is 1 by default. It does not change when the sweep ends. The flash lasts modRate seconds, and GetCooldownDisplayDuration divides by modRate.
cooldown:SetCooldownDuration(duration [, modRate]) Starts a cooldown of duration seconds at the current time.
cooldown:SetCooldownFromExpirationTime(expirationTime, duration [, modRate]) Starts a cooldown of duration seconds that ends at expirationTime.
cooldown:Clear() Stops the cooldown and hides the frame. There is no flash, and OnCooldownDone does not run.
cooldown:GetCooldownTimes() Returns start, duration in milliseconds. While the sweep runs, these are the values of the cooldown. When the sweep ends, they change to the values of the flash: start is the end time, and duration is 1000 × modRate. After Clear(), both values are 0.
cooldown:GetCooldownDuration() Returns the duration that GetCooldownTimes() returns.
cooldown:GetCooldownDisplayDuration() Returns the duration that GetCooldownTimes() returns, divided by modRate.
cooldown:Pause() / cooldown:Resume() Stops the sweep at its current position, then starts it again from that position. The flash also stops when you pause it.
cooldown:SetPaused(paused) / cooldown:IsPaused() Sets and returns the paused state. A paused frame stays paused when you call SetCooldown. The sweep then stays at the position of that start time.
cooldown:SetReverse(reverse) / cooldown:GetReverse() Sets and returns the reverse flag. With the flag set, the sweep starts empty and becomes full. At the end, the sweep stays full while the flash plays, so an icon under it stays covered. Then the frame hides. The flag is false when the frame is created.
cooldown:SetDrawEdge(drawEdge) / cooldown:GetDrawEdge() Sets and returns the draw-edge flag. With the flag set, a bright line moves along the edge of the sweep. The flag is false when the frame is created.
cooldown:SetDrawSwipe(drawSwipe) / cooldown:GetDrawSwipe() Sets and returns the draw-swipe flag. With the flag cleared, the dark sweep is not drawn. The flag is true when the frame is created.
cooldown:SetDrawBling(drawBling) / cooldown:GetDrawBling() Sets and returns the draw-bling flag. With the flag cleared, the frame hides at the end without the flash. The flag is true when the frame is created.
cooldown:SetSwipeColor(r, g, b [, a]) Sets the color of the sweep. The default color is black with alpha 0.63. If you do not give a, the alpha is 1.
cooldown:SetEdgeColor(r, g, b [, a]) Sets the color of the edge line. The default color is white. If you do not give a, the alpha is 1.
cooldown:SetSwipeTexture(texture [, r, g, b, a]) Sets the texture and the color of the sweep. The texture covers the full area of the frame, and the sweep shows the part that it covers. The default texture is Interface\Cooldown\star4.
cooldown:SetEdgeTexture(texture [, r, g, b, a]) Sets the texture and the color of the edge line. The texture turns about its center with the sweep. Draw the line from the center to the top edge, as in Interface\Cooldown\edge, the default texture.
cooldown:SetBlingTexture(texture [, r, g, b, a]) Sets the texture and the color of the flash at the end. The default flash is Interface\Cooldown\star4 in light blue.
cooldown:SetRotation(radians) / cooldown:GetRotation() Sets and returns the angle where the sweep starts, in radians, clockwise from the top. For example, math.pi / 2 starts the sweep at the right side. The angle is 0 when the frame is created.
cooldown:SetHideCountdownNumbers(hide) / cooldown:GetHideCountdownNumbers() Sets and returns whether the countdown numbers are hidden. The flag is false when the frame is created.
cooldown:GetCountdownFontString() Returns the font string that shows the countdown numbers. It uses Fonts\FRIZQT__.TTF, size 16, with an outline, at the center of the frame. You can change its font, color, and position.
cooldown:SetCountdownAbbrevThreshold(seconds) / cooldown:GetCountdownAbbrevThreshold() Sets and returns the time left in seconds below which the numbers show as m:ss, for example 1:56. Above it, they show whole minutes, for example 3m. The threshold applies from one minute to one hour. The default is 120.
cooldown:SetCountdownMillisecondsThreshold(seconds) / cooldown:GetCountdownMillisecondsThreshold() Sets and returns the time left in seconds below which the numbers show tenths of a second, for example 3.3. The default is 0, so the numbers show whole seconds.
cooldown:SetMinimumCountdownDuration(milliseconds) / cooldown:GetMinimumCountdownDuration() Sets and returns the duration that a cooldown must be longer than to show numbers. The value applies to the full duration, not to the time left. The default is 2000.
cooldown:SetCountdownFormatter([formatter]) / cooldown:GetCountdownFormatter() Sets and returns the formatter that makes the countdown text. Give a formatter from C_StringUtil.CreateNumericRuleFormatter, or nil to use the default numbers again. GetCountdownFormatter returns nil if there is no formatter.

For the three texture methods, texture is a file path. An empty string gives a solid color. If you do not give r, g, and b, the color does not change. If you do not give a, the alpha is 1. A texture that does not load is not drawn. Interface\Cooldown\edge is always available. If you do not set a texture, the first SetCooldown call loads its default.

If a required number is missing, the method gives a usage error, for example Usage: <name>:SetCooldown(start, duration[, modRate]).

The draw-edge flag applies from the next SetCooldown call.

Only cooldown frames have these methods.

The countdown numbers show only when the countdownForCooldowns CVar is 1. The default is 0, so a cooldown shows no numbers until you set it:

SetCVar("countdownForCooldowns", "1")

The numbers show the time left, rounded up: tenths below the milliseconds threshold (3.3), seconds below one minute (57), then m:ss or whole minutes, then whole hours (2h) and days (2d). They do not show during the flash.

A countdown formatter replaces these rules and the two thresholds. The formatter gets the time left in seconds, not rounded, for example 28.118. The other conditions for the numbers stay the same. The frame keeps the formatter itself, not a copy. Thus a later change to its breakpoints changes the text. GetCountdownFormatter returns a new object for the same formatter each time. If the format gives an error, the frame shows no numbers.

To color the numbers by unit, give each breakpoint its own color code:

local Up = Enum.NumericRuleFormatRounding.Up
local formatter = C_StringUtil.CreateNumericRuleFormatter()
formatter:SetBreakpoints({
    { threshold = 0, step = 0.1, rounding = Up, format = "|cffff3333%.1f|r" },
    { threshold = 5, step = 1, rounding = Up, format = "|cffffffff%d|r" },
    { threshold = 60, format = "|cff33ffff%dm|r",
      components = { { div = 60, step = 1, rounding = Up } } },
    { threshold = 3600, format = "|cff3380ff%dh|r",
      components = { { div = 3600, step = 1, rounding = Up } } },
    { threshold = 86400, format = "|cff3333ff%dd|r",
      components = { { div = 86400, step = 1, rounding = Up } } },
})
cd:SetCountdownFormatter(formatter)

The OnCooldownDone script runs one time when the sweep of a cooldown ends. It runs before the flash. You can set it with SetScript, HookScript, or a <OnCooldownDone> element in <Scripts>:

cd:SetScript("OnCooldownDone", function()
    print(this:GetName() .. " is ready")
end)

The script does not run for Clear() or for a cooldown with a duration of 0.

You can also use these functions:

  • CooldownFrame_Set(cd, start, duration, enable [, forceShowDrawEdge, modRate]) starts the cooldown and sets the draw-edge flag. If enable is nil or 0, or start or duration is not more than 0, it calls CooldownFrame_Clear.
  • CooldownFrame_Clear(cd) calls Clear().
  • CooldownFrame_SetDisplayAsPercentage(cd, percentage) pauses the frame and shows the sweep at percentage (0 to 1).
  • CooldownFrame_SetTimer(cd, start, duration, enable) calls SetCooldown. If start, duration, or enable is not more than 0, it hides the frame.

CooldownFrame_Set, CooldownFrame_Clear, and CooldownFrame_SetTimer also accept the indicator models of the stock buttons. For these models, they use the stock timer.

A new cooldown frame has a sweep that is already over. If the frame is shown before the first SetCooldown call, it hides itself after one second.

XML files can also create a cooldown frame with a <Cooldown> element. The reverse, drawEdge, drawSwipe, and drawBling attributes set the four flags. The rotation attribute sets the start angle in degrees, for example rotation="180". The hideCountdownNumbers and minimumCountdownDuration attributes set the matching countdown values.

The <SwipeTexture>, <EdgeTexture>, and <BlingTexture> child elements set the three textures. Each element takes a file attribute and a <Color> child. Both are optional. Without file, the texture is a solid color. Without <Color>, the color does not change.

<Frames>
    <Cooldown name="$parentCooldown" inherits="CooldownFrameTemplate" drawEdge="true">
        <SwipeTexture>
            <Color r="0" g="0" b="0" a="0.8"/>
        </SwipeTexture>
        <EdgeTexture file="Interface\Cooldown\edge"/>
    </Cooldown>
</Frames>

A cooldown frame needs no template. A cooldown frame starts without anchors. Give it a position with SetPoint, SetAllPoints, or <Anchors>. CooldownFrameTemplate sets the frame to the full size of its parent, and the frame starts hidden.

frame:SetShown(shown)

Show() if shown is truthy, Hide() otherwise. Registered for frames, textures and fontstrings (each branch has its own engine Show/Hide implementation).

A frame moved to a new parent takes its children along

frame:SetParent(parent) moves the frames under frame along with it. frame lands at the parent's level + 1 (level 0 with no parent), and each child frame of the same strata moves up or down by the same number of levels, recursively, so the subtree keeps its own layering and sits above its new parent.

local host = CreateFrame("Frame", nil, UIParent)
host:SetFrameLevel(20)
local panel = CreateFrame("Frame")               -- level 0
local child = CreateFrame("Frame", nil, panel)   -- level 1
panel:SetParent(host)
panel:GetFrameLevel()   -- 21
child:GetFrameLevel()   -- 22

frame:SetFrameLevel(level) moves only frame. The frames under it keep the levels they have.

A ScrollFrame keeps clipping its content when it moves

A ScrollFrame's scroll child, and all the frames in it, stay clipped to the ScrollFrame. This is also true when the ScrollFrame, or a frame above it, gets a new parent. As a result, a ScrollFrame that an addon builds in one place and then moves into a window still clips its content to its edges. AceGUI-3.0 widgets work this way.

local holder = CreateFrame("Frame", nil, UIParent)
local scroll = CreateFrame("ScrollFrame", nil, holder)
local content = CreateFrame("Frame", nil, scroll)
scroll:SetScrollChild(content)
holder:SetParent(SomeWindow)   -- content stays clipped to scroll

fontstring:GetStringHeight()

This is the companion to GetStringWidth. It returns the height of the rendered text, in UI pixels. The result includes word wrap: wrapped text measures lines × fontHeight + (lines − 1) × spacing (the SetSpacing value). Empty or unset text returns 0.

local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
f:SetWidth(100)
f:SetText("a long line that wraps a few times")
frame:SetHeight(f:GetStringHeight() + 16)  -- size the box to the text

Related: the stock fontstring:GetStringWidth() includes inline |T…|t icon widths, so the measured width matches the rendered width. This includes edit boxes: icons in edit-box text show as icons.

fontstring:GetUnboundedStringWidth()

Returns the width of the text on one line, in UI pixels, with no wrap or size limit applied. Note: GetStringWidth ALSO ignores word wrap — that is a classic API trap. Here the two methods always agree. The width of the text AS RENDERED (the widest wrapped line) is GetWrappedWidth, below. Inline |T…|t icon widths are included. Empty or unset text returns 0.

fontstring:SetTextToFit([text])

Sets the text of the font string, and then sets the width of the font string to the width of the text on one line (GetUnboundedStringWidth).

  • The width changes each time, also when the font string was wider than the text.
  • The height does not change.
  • With no text, or empty text, the font string has no width of its own again.

fontstring:GetWrappedWidth()

Returns the width of the text as it renders, in UI pixels: the width of the widest wrapped line. This is the method to use for the visible size of wrapping text — GetStringWidth and GetUnboundedStringWidth both measure the string on one line and ignore wrapping. Text that does not wrap returns the same value as GetStringWidth. Inline |T…|t icons are counted. Empty or unset text returns 0.

The wrapped width can carry a ≤1px difference from a direct measure of the same line text — the same rounding noise GetStringWidth itself carries.

local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
f:SetWidth(100)
f:SetText("a long line that wraps a few times")
-- f:GetStringWidth()   -> ~165 (one line, ignores wrap)
-- f:GetWrappedWidth()  -> ~94  (the widest rendered line)

fontstring:GetNumLines()

Returns the number of wrapped lines the text renders as. It works whether or not the text has rendered yet. Empty or unset text returns 0.

fontstring:GetLineHeight()

Returns the height of one text line in UI pixels — the font height, without the SetSpacing value. Use it with GetNumLines and GetSpacing to reconstruct GetStringHeight per line.

fontstring:GetFontHeight([calculated]) / fontstring:SetFontHeight(height)

GetFontHeight returns the height of the font, in the same units as the second return value of GetFont.

  • calculated is true when you omit it. A string that draws at the pixel size of its font then returns that pixel size.
  • When calculated is false, the result is always equal to the second return value of GetFont.

SetFontHeight changes only the height. The font file and the flags (OUTLINE, THICKOUTLINE, MONOCHROME) stay the same, as in a SetFont call with the current file and flags. A height of 0 or less has no effect. If the string has no font, SetFontHeight raises an error.

Font objects have the same pair. On a Font object, GetFontHeight takes no argument. The font strings that use the font change with it.

local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
f:SetFontHeight(18)               -- same file and flags, new height
print(f:GetFontHeight(false) == select(2, f:GetFont()))   -- true
GameFontNormal:SetFontHeight(14)  -- strings that use GameFontNormal change too

fontstring:IsTruncated()

Returns true when the engine cut the text off with an ellipsis (...). The engine truncates only when the text does not fit a bounded box. The fontstring needs a width and a height, and the text must overflow that box. Fontstrings always wrap at spaces. So a box that is only one line tall truncates, but a taller box wraps the text and fits it.

The result reflects what is on screen, so the fontstring must have drawn at least once. GetText still returns the full text, not the cut-off line.

local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
f:SetWidth(60)
f:SetHeight(14)             -- one line tall
f:SetText("a name too long to fit")
-- after it draws: f:IsTruncated() -> true  (draws "a name too...")

fontstring:SetMaxLines(maxLines) / fontstring:GetMaxLines()

Caps the fontstring to maxLines wrapped lines. Text past the cap is cut off with an ellipsis (...). Pass 0 (or nothing) for no cap.

SetMaxLines(1) gives a single-line label that ends in ... when the text is too wide, even inside a tall box. SetWordWrap(false) does the same and also drops the lines after a line break.

The cap needs a width to take effect: the text must have an edge to overflow. GetMaxLines returns the current cap (0 when unset). Use IsTruncated to check whether the cap actually cut the text.

local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
f:SetWidth(120)
f:SetMaxLines(1)                 -- one line, then "..."
f:SetText("a really long guild or player name that will not fit")
-- draws "a really long gu..."   ; f:IsTruncated() -> true

fontstring:SetIndentedWordWrap([indent]) / fontstring:GetIndentedWordWrap()

When indent is true, every line of the text after the first starts 15 screen pixels in. This applies to wrapped lines and to lines after a line break. The text also wraps 15 pixels earlier on those lines, and the string height counts the narrower lines.

  • indent is true when you omit it.
  • GetIndentedWordWrap returns 1 or nil.
  • A right-justified line moves 15 pixels to the left. A centered line does not move.
  • Font objects, edit boxes, message frames, scrolling message frames and SimpleHTML frames have the same pair. On those, the setting goes to the font, and every font string that uses the font follows it. A font string that has its own setting keeps it. SimpleHTML takes an optional element first: html:SetIndentedWordWrap("h1", true).
ChatFrame1:SetIndentedWordWrap(true)   -- wrapped chat lines start 15 px in

You can also set the value in XML with the indented attribute on a FontString:

<FontString parentKey="Text" inherits="GameFontHighlight" indented="true"/>

fontstring:SetWordWrap([wrap]) / fontstring:CanWordWrap()

When wrap is false, the text does not wrap. The string shows only its first line. When the line is wider than the string, it ends in .... Lines after a line break ( ) do not show.

  • wrap is true when you omit it.
  • CanWordWrap returns 1 when the text wraps, else nil.
  • The string needs a width for the ... cut. Without a width, the line shows in full.
local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
f:SetWidth(100)
f:SetWordWrap(false)
f:SetText("one two three four five six")   -- draws "one two three four fi..."

You can also set the value in XML with the wordwrap attribute on a FontString. The default is true.

<FontString parentKey="Label" inherits="GameFontHighlightSmall" wordwrap="false" justifyH="LEFT"/>

fontstring:GetFieldSize()

Returns the number of bytes of text that the string can hold. A string with no size limit returns infinity (1.#INF). A string from XML with bytes="n" holds n - 1 bytes, and SetText cuts longer text.

fontstring:SetFormattedText(format [, ...]) / button:SetFormattedText(format [, ...])

Sets the text to string.format(format, ...). The result is the same as SetText(string.format(format, ...)), and the text gets the same escape handling as SetText. A bad format string raises a Lua error. On a font string with no font, the method raises Font not set, as SetText does.

bar.text:SetFormattedText("%d/%d (%.1f%%)", cur, max, cur / max * 100)
button:SetFormattedText("Buy (%d)", count)

The |4 plural code

Text can pick a singular or a plural word with |4singular:plural;. The choice uses the last number before the code in the same text. The code works in the same places as the |1 and |2 grammar codes: SetText, SetFormattedText, button text, tooltip lines, chat lines, chat bubbles and name plates.

label:SetFormattedText("%d |4item:items; left", n) -- "1 item left", "3 items left"

The rule for the choice depends on the client language:

  • English, German and Spanish use the singular for 1 and the plural for all other numbers.
  • French, Korean and Chinese use the singular for 0 and 1 and the plural for all other numbers.

texture:SetRotation(angle [, cx, cy])

Rotates a texture by angle radians. A positive angle turns the texture counter-clockwise.

The optional cx, cy set the pivot point, as a normalized position inside the texture from 0 to 1. The default pivot is the center, (0.5, 0.5). An angle of 0 clears the rotation and returns the texture to upright.

The method turns the four corners of the drawn quad, so the whole texture stays visible and no corner is clipped. It works on any texture, including engine-created ones. The rotation holds across moves and resizes, and costs nothing per frame while it stays still. GetRotation() returns the current angle in radians.

local t = frame:CreateTexture(nil, "ARTWORK")
t:SetAllPoints(frame)
t:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
t:SetRotation(math.rad(45))      -- 45 degrees counter-clockwise
t:SetRotation(math.pi, 0, 1)     -- half turn around the top-left corner

A <Texture> element takes a rotation attribute, in degrees. It turns the texture around the center:

<Texture parentKey="Arrow" file="Interface\Buttons\UI-SpellbookIcon-NextPage-Up" rotation="90"/>

texture:SetVertexOffset(vertexIndex, offsetX, offsetY)

Moves one corner of a texture by offsetX, offsetY pixels. vertexIndex picks the corner: UPPER_LEFT_VERTEX, LOWER_LEFT_VERTEX, UPPER_RIGHT_VERTEX, or LOWER_RIGHT_VERTEX (values 1 to 4). +x is right and +y is up.

Moving the corners warps the whole quad, so the texture and its background move together. It suits skew, trapezoid, and fake-perspective effects, and waving animations. It composes with SetRotation: the texture rotates first, then the offsets apply. The offset holds across moves, resizes, and SetTexture, and costs nothing per frame while it stays still.

GetVertexOffset(vertexIndex) returns the current offsetX, offsetY for a corner (0, 0 if unset).

Note: an offset that turns the quad inside-out — mirroring it past its opposite edge — is not drawn. The engine skips a back-facing quad, so keep the warp within a non-mirrored shape.

local t = frame:CreateTexture(nil, "ARTWORK")
t:SetAllPoints(frame)
t:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
-- pull the top two corners inward: a trapezoid (fake perspective)
t:SetVertexOffset(UPPER_LEFT_VERTEX, 20, 0)
t:SetVertexOffset(UPPER_RIGHT_VERTEX, -20, 0)

texture:SetColorTexture(colorR, colorG, colorB [, a])

Fills the texture with a solid color. Each channel is 0 to 1. The alpha a is optional and defaults to 1 (opaque). This is a second name for a fill the engine already does — SetTexture(r, g, b [, a]) with numbers instead of a path. It is the same engine call under a second name, so the clamping and the opaque default match the engine.

local t = frame:CreateTexture(nil, "BACKGROUND")
t:SetAllPoints(frame)
t:SetColorTexture(0.1, 0.6, 1.0, 0.8)  -- semi-transparent blue fill

texture:SetMask(path)

Clips a texture to the shape of a mask — for round portraits, round minimap buttons, and other shaped art.

path is a mask texture. The mask's ALPHA channel is the shape: where the mask is opaque the texture shows, where the mask is transparent the texture is hidden. The mask stretches across the texture's full display area, so it always lines up with the texture. Pass nil or "" to remove the mask; the texture returns to its full rectangle. If the file does not load, the texture keeps the mask it had.

A mask file must be one the client can load and must hold its shape in the alpha channel:

  • A BLP, or an UNCOMPRESSED 32-bit TGA. The client cannot decode an RLE-compressed TGA.
  • The shape must be in the alpha channel, with white color. A mask that is fully opaque does nothing; a mask whose shape is only in its color does not clip.
  • Bottom-origin. A top-origin TGA loads upside down.

Two masks ship in the client and need no file of your own: Textures\MinimapMask (round) and Interface\CharacterFrame\TempPortraitAlphaMask (round portrait). For any other shape, put your own mask texture in your addon folder and pass its path, the same way you ship any custom texture.

local t = frame:CreateTexture(nil, "ARTWORK")
t:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
t:SetMask("Textures\\MinimapMask")               -- clip the icon to a circle
t:SetMask("Interface\\AddOns\\MyAddon\\round")   -- or your own mask texture
t:SetMask(nil)                                    -- remove the mask

The mask ignores SetTexCoord. If you crop the texture to one sprite of a sprite sheet, the full mask shape still clips that sprite. The mask follows texture:SetRotation. It also combines with masks from texture:AddMaskTexture: the texture shows only where every mask is opaque.

frame:CreateMaskTexture([name, layer, ...])

Creates a MaskTexture and returns it. A MaskTexture is a texture that never draws itself. Instead it clips other textures to its shape.

Give the mask a shape with SetTexture, place and size it like any texture (SetPoint, SetSize, SetAllPoints), then attach it with texture:AddMaskTexture. Because the mask has its own position and size, it can cover only part of the target, sit to one side, or move each frame.

The arguments match frame:CreateTexture. The mask shape lives in the texture's alpha channel and must load from a BLP or an uncompressed 32-bit TGA — the same rule as texture:SetMask.

local mask = frame:CreateMaskTexture()
mask:SetTexture("Interface\\AddOns\\MyAddon\\circle")
mask:SetAllPoints(icon)      -- cover the whole icon → a round icon
icon:AddMaskTexture(mask)

texture:AddMaskTexture(mask)

Clips this texture to mask's shape and position. Where the mask is opaque the texture shows; where the mask is transparent the texture is hidden.

Call it again with a different mask to add another. A texture with several masks shows only where ALL of them are opaque — the masks intersect. So two circles show their overlapping lens, and a square plus a circle show a square with rounded corners. Up to 7 masks apply at once. A mask set with texture:SetMask uses one of the 7 slots. Masks cannot subtract, so you cannot cut a hole with this.

A mask honors texture:SetRotation: rotate the mask and its clip shape turns with it. The mask can also move or resize each frame (drive it from an OnUpdate), so the clip animates.

Outside its own rectangle, a mask repeats the alpha of its nearest edge pixel. A mask whose outer edge is transparent, like a circle, hides the texture outside the mask. A mask with an opaque edge does not clip the texture past its rectangle. To clip only part of a texture, use a mask with a transparent border.

texture:RemoveMaskTexture([mask])

Removes mask from this texture. With no argument, removes every mask, so the texture returns to its full rectangle.

texture:GetNumMaskTextures()

Returns the number of masks attached to this texture.

texture:GetMaskTexture(index)

Returns the mask at index (1 is the first one added), or nil when there is no mask at that index.

texture:SetAtlas(atlas [, useAtlasSize])

Points the texture at a named piece of art. This sets the texture file and the coordinates of the rectangle inside it, so one call replaces a SetTexture and a SetTexCoord. See Texture for the atlas names and for C_Texture.RegisterAtlas.

Pass true for useAtlasSize to also resize the texture to the art's own pixel size. Any further arguments are accepted and ignored.

After SetAtlas, texture coordinates address the art and not the file it sits in, so 0 to 1 covers the whole of the art. SetTexCoord(0, 0.5, 0, 0.5) shows the top-left quarter of the art, and GetTexCoord reads back what you asked for. SetTexture points the texture at a file again and clears the atlas. SetAtlas turns tiling off and sets both wrap modes to "CLAMP".

When the name is not known, the texture keeps whatever it was showing. Call _classicapi_DumpAtlasMisses() to list the names that were asked for and not found.

local t = frame:CreateTexture(nil, "ARTWORK")
t:SetAtlas("MinimapArrow", true)   -- draws the art at its own 32x32 size

texture:GetAtlas()

Returns the atlas name last set on this texture, or nil when it is showing something else. Pointing the texture at a file with SetTexture clears the name.

texture:ResetTexCoord()

Drops a crop made with SetTexCoord or SetSpriteSheetCell and shows the whole of what the texture points at.

For a texture set with SetAtlas, the whole of what it points at is the atlas's own rectangle, so the art comes back and the atlas name stays set. For any other texture it is the whole file, the same as SetTexCoord(0, 1, 0, 1).

local t = frame:CreateTexture(nil, "ARTWORK")
t:SetAtlas("MinimapArrow")
t:SetTexCoord(0, 0.5, 0, 0.5)   -- shows the top-left quarter of the art
t:ResetTexCoord()               -- shows the art again

texture:SetSpriteSheetCell(cell, numRows, numColumns)

Crops the texture to one cell of an evenly divided grid. A sprite sheet holds several images in one file, and this works out the share of the file that one image occupies, so you do not write the coordinates yourself.

  • cell — which image to show. The first cell is 1. Cells count left to right, then top to bottom.
  • numRows, numColumns — the shape of the grid.

Two further arguments, cellWidth and cellHeight, are accepted and ignored.

The texture keeps what it was showing when cell falls outside the grid.

-- The eight raid markers share one 4x4 sheet. Cell 8 is the skull.
t:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
t:SetSpriteSheetCell(8, 4, 4)

texture:SetTexture(file [, wrapModeHorizontal, wrapModeVertical])

Points the texture at an image file. The two optional arguments set the wrap mode of the horizontal and the vertical axis. The wrap mode decides what the texture shows where its coordinates go past 0 or 1:

  • "REPEAT" or true repeats the image.
  • "CLAMP", false or nil stretches the pixels at the edge of the image. This is the default.

"CLAMPTOBLACKADDITIVE", "CLAMPTOBLACK", "CLAMPTOWHITE" and "MIRROR" work the same as "CLAMP".

Each call with a file sets both modes again, so SetTexture(file) sets them back to "CLAMP". A color fill, SetTexture(r, g, b), keeps the modes. An axis with tiling on always repeats (see texture:SetHorizTile).

bg:SetTexture("Interface\\AddOns\\MyAddon\\stripes", "REPEAT", "REPEAT")
bg:SetTexCoord(0, 4, 0, 2)   -- four copies across, two copies down

A <Texture> element takes the same modes in its hWrapMode and vWrapMode attributes:

<Texture parentKey="Fill" file="Interface\AddOns\MyAddon\stripes" hWrapMode="REPEAT"/>

texture:SetHorizTile(tiling) / texture:SetVertTile(tiling)

Turns tiling on or off for one axis. A tiled axis repeats the image at its own size: one pixel of the image covers one unit of the texture's width or height. For example, an image 32 pixels wide on a texture 100 units wide shows the image a little more than three times across. The count changes when the texture changes size. GetHorizTile() and GetVertTile() return true or false.

Tiling sets each coordinate on that axis that is not 0 to the repeat count, so GetTexCoord returns the count. The count needs the size of the image, so for a new image it is set when the texture is first drawn. Tiling stays on when SetTexture sets a new file. A texture that shows an atlas does not tile, and neither does a texture with SetTexCoordModifiesRect on.

A tiled status bar texture keeps the image at its own size as the bar fills. The bar shows more or fewer copies and does not squeeze the image.

local edge = frame:CreateTexture(nil, "BORDER")
edge:SetTexture("Interface\\AddOns\\MyAddon\\border-top")
edge:SetPoint("TOPLEFT")
edge:SetPoint("TOPRIGHT")
edge:SetHeight(16)
edge:SetHorizTile(true)

A <Texture> element takes horizTile and vertTile attributes. A <TexCoords> child must keep a tiled axis at 0 and 1, for example left="0" right="1" with horizTile. Other values are an error in the XML log, and the texture then shows the whole image. The other axis can take any values, so one strip of a file can tile:

<Texture parentKey="Rail" file="Interface\AddOns\MyAddon\rails" horizTile="true">
  <TexCoords left="0" right="1" top="0.25" bottom="0.5"/>
</Texture>

texture:SetDesaturation(amount)

Removes some or all of the color from a texture. amount is a number from 0 (full color) to 1 (grayscale). The method clamps values outside this range. At every amount, the texture keeps the tint that SetVertexColor gives it.

SetDesaturated(true) is the same as SetDesaturation(1), and SetDesaturated(false) is the same as SetDesaturation(0). All three methods change the same setting, so SetDesaturated(false) also clears a partial amount.

icon:SetDesaturation(0.5)          -- half of the color is gone
icon:SetVertexColor(1, 0.5, 0.5)   -- the gray keeps a red tint
icon:SetDesaturated(false)         -- full color again

A <Texture> element also takes a desaturated attribute. desaturated="true" is the same as SetDesaturated(true):

<Texture parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark" desaturated="true"/>

texture:GetDesaturation()

Returns the current desaturation amount as a number from 0 to 1. A texture that SetDesaturated(true) made gray returns 1.

The texture stores the amount in 255 steps, so the value that you read back can differ a little from the value that you set. For example, 0.5 reads back as 0.50196.

IsDesaturateSupported()

Returns 1 when the graphics card can draw desaturated textures. Returns nothing (nil) when it cannot.

if not IsDesaturateSupported() then
  icon:SetVertexColor(0.5, 0.5, 0.5)   -- gray it by hand
end

Texture size and shape

A texture can have any width and any height. The two sides do not need to be powers of two, and they do not need to be equal. A 397x385 photo, a 1919x1079 screenshot, and a 32x1024 strip all load and draw correctly.

There is no fixed upper limit on texture size. The only limit is the maximum texture size of your graphics card, which is 16384 on most modern cards. A texture larger than this limit does not load.

This applies to every path that loads a texture: SetTexture, SetTexCoord sprite sheets, inline texture markup, tooltips, and masks. The client allocates memory for a large texture only when an addon loads one. An addon that uses small textures costs nothing extra.

Two limits remain. Both are about the BLP format, not the image size:

  • A very large uncompressed BLP file does not load. The limit is the file size, about 2 MB, or 32 MB with VanillaHelpers. The texture stays blank. This affects only uncompressed BLP. A DXT-compressed BLP is about eight times smaller and stays under the limit. A TGA file has no size limit.
  • A non-power-of-two BLP with DXT compression or a mip chain is untested.

For a large or non-power-of-two image, use an uncompressed 32-bit TGA, or a DXT-compressed BLP for a smaller file.

local t = frame:CreateTexture(nil, "ARTWORK")
t:SetSize(397, 385)
t:SetTexture("Interface\\AddOns\\MyAddon\\photo")   -- a 397x385 uncompressed TGA

fontstring:SetRotation(angle [, cx, cy])

Rotates a FontString's text by angle radians. A positive angle turns the text counter-clockwise.

The optional cx, cy set the pivot point, as a normalized position inside the text from 0 to 1. The default pivot is the center, (0.5, 0.5). An angle of 0 clears the rotation and returns the text to upright.

The rotation is visual only. GetStringWidth, GetStringHeight, GetRect, and SetPoint all stay axis-aligned, so a rotated label measures and anchors as if it were upright. GetRotation() returns the current angle in radians.

The method turns the glyph vertices, so the whole text stays visible. It works on any FontString, and the rotation holds across text changes, moves, and resizes. It costs nothing per frame while the text stays still.

Inline |T…|t icons inside a rotated FontString do not turn with the text. They draw as separate anchored regions. Plain text rotates correctly.

local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
fs:SetPoint("CENTER")
fs:SetText("Rotate me")
fs:SetRotation(math.rad(45))     -- 45 degrees counter-clockwise
fs:SetRotation(0)                -- back to upright

A <FontString> element takes a degrees or a radians attribute. It turns the text around the center. If the element has both, radians is used:

<FontString parentKey="SideLabel" inherits="GameFontNormal" text="Side" degrees="90"/>

editBox:SetCursorPosition(position)

Moves the text cursor to position in the edit box, and clears any selected text. position is a 0-based offset in bytes. For plain ASCII text a byte offset is the same as a character index. The engine limits an out-of-range value to the text length, so editBox:SetCursorPosition(string.len(text)) always lands at the end.

editBox:GetCursorPosition()

Returns the cursor's current offset in the edit box, in bytes.

local box = ChatFrameEditBox
box:SetText("hello world")
box:SetCursorPosition(5)          -- cursor after "hello"
print(box:GetCursorPosition())    -- 5

editBox:GetUTF8CursorPosition()

Returns the cursor position as a character count, not a byte count. For text with only ASCII characters this equals GetCursorPosition. For text with multibyte characters it counts characters, so the two values differ.

editBox:ClearHighlightText()

Clears the selected text, leaving the cursor where it is.

editBox:HasFocus()

Returns true if this edit box has keyboard focus.

GetCurrentKeyBoardFocus()

Returns the edit box that has keyboard focus. If no edit box has focus, it returns nil.

local box = GetCurrentKeyBoardFocus()
if box then print(box:GetName()) end

editBox:HasText()

Returns true if the edit box holds any text.

editBox:SetHighlightColor(r, g, b [, a])

Sets the color of the text-selection highlight. Each value is 0 to 1. Alpha is optional and defaults to 1. The new color shows the next time the box paints a selection.

ChatFrameEditBox:SetHighlightColor(1, 0, 0)   -- red selection

editBox:GetHighlightColor()

Returns the selection-highlight color as four numbers: r, g, b, a, each 0 to 1.

editBox:SetCountInvisibleLetters(count) / editBox:IsCountInvisibleLetters()

Whether the edit box's SetMaxLetters limit counts the characters of color codes and a hyperlink's hidden part. Off by default: only the letters you see count. Turn it on for text whose real limit is in bytes, such as a macro body.

box:SetMaxLetters(12)
box:SetText("|cffff0000abcdefg|r")   -- kept whole: 7 letters
box:SetCountInvisibleLetters(true)
box:SetText("|cffff0000abcdefg|r")   -- "|cffff0000ab": the code counts as 10

XML sets it with the EditBox attribute countInvisibleLetters="true". Only the limit changes. GetNumLetters still counts only the letters you see, and the cursor moves over letters as before.

editBox:ClearHistory()

Removes every line from the up/down input history. The history line limit (SetHistoryLines) stays, so the box keeps recording new lines afterward. Does nothing when the box has no history.

editBox:Enable() / editBox:Disable() / editBox:IsEnabled()

editBox:Disable() removes the keyboard focus from the box and runs its OnDisable script. editBox:Enable() runs its OnEnable script. editBox:IsEnabled() returns true or false.

  • A disabled box still gets the focus when the user clicks it, or when you call editBox:SetFocus().
  • Each call runs the script, also when the box is already in that state.

slider:Enable() / slider:Disable() / slider:IsEnabled()

slider:Disable() stops the mouse from moving the slider and runs its OnDisable script. slider:Enable() lets the mouse move it again and runs its OnEnable script. slider:IsEnabled() returns true or false.

  • A disabled slider still runs its OnMouseDown and OnMouseUp scripts, and slider:SetValue() still changes its value.
  • Each call runs the script, also when the slider is already in that state.

OnEnable and OnDisable (scripts)

Every frame can have an OnEnable and an OnDisable script. Set them with SetScript, HookScript, or in XML. A button runs OnEnable when button:Enable() enables it, and OnDisable when button:Disable() disables it. A button does not run them when its state does not change. An edit box or a slider runs them from its Enable() and Disable().

button:SetScript("OnDisable", function(self) self:SetAlpha(0.5) end)
button:SetScript("OnEnable", function(self) self:SetAlpha(1) end)

OnMinMaxChanged(self, min, max) (script)

A slider or a status bar runs this script when SetMinMaxValues changes its range. min and max are the new range. Set it with SetScript, HookScript, or in XML.

  • A call that does not change the range does not run the script.
  • The script runs before the frame applies its value to the new range, so OnValueChanged runs after it.
  • A status bar whose min is more than its max gets max for both.
  • The range in the frame's XML also runs the script when the frame loads.
bar:SetScript("OnMinMaxChanged", function(self, min, max)
    self.text:SetText(min .. " - " .. max)
end)

statusBar:SetReverseFill(isReverseFill) / statusBar:GetReverseFill()

statusBar:SetReverseFill(true) makes the bar fill from the right edge. A vertical bar fills from the top edge. statusBar:GetReverseFill() returns true or false.

  • The bar texture shows the same part of its image as in a normal fill. Only the side that the fill grows from changes.
  • The fill style Enum.StatusBarFillStyle.Reverse has the same result.

statusBar:SetRotatesTexture(rotatesTexture) / statusBar:GetRotatesTexture()

statusBar:SetRotatesTexture(true) turns the image of the bar texture by a quarter turn. Use it to put a horizontal bar image on a vertical bar. statusBar:GetRotatesTexture() returns true or false.

statusBar:SetFillStyle(fillStyle) / statusBar:GetFillStyle()

Sets how the bar fills as its value increases. fillStyle is a value of Enum.StatusBarFillStyle, or its name as a string:

Value String Fill
Enum.StatusBarFillStyle.Standard (0) "STANDARD" From the left edge, or from the bottom edge of a vertical bar. This is the default.
Enum.StatusBarFillStyle.StandardNoRangeFill (1) "STANDARD_NO_RANGE_FILL" As Standard. A bar whose min and max are equal shows full, but a bar whose min and max are both 0 shows empty.
Enum.StatusBarFillStyle.Center (2) "CENTER" From the center to the two edges.
Enum.StatusBarFillStyle.Reverse (3) "REVERSE" From the right edge, or from the top edge of a vertical bar.

statusBar:GetFillStyle() returns the number.

  • A string is not case-sensitive. Any other value is an error.
  • The Center style ignores statusBar:SetReverseFill().
  • The bar texture covers only the filled part of the bar. A region that is anchored to statusBar:GetStatusBarTexture() moves with the edge of the fill. When the bar is empty, the texture has a size of 0 along the fill, at the start edge.
bar:SetFillStyle(Enum.StatusBarFillStyle.Center)

You can also set the three values in XML. A StatusBar takes the reverseFill, rotatesTexture and fillStyle attributes. fillStyle takes the string names in the table. A name that is not in the table changes nothing.

<StatusBar name="MyBar" reverseFill="true" fillStyle="CENTER">
    <BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
</StatusBar>

statusBar:SetTimerDuration(duration [, interpolation [, direction]]) / statusBar:GetTimerDuration()

statusBar:SetTimerDuration(duration) gives the bar a timer. The bar then fills by itself on each frame from the time span of duration, a duration object. Your code does not call SetValue on each frame. statusBar:GetTimerDuration() returns the duration object of the timer, or nil when the bar has no timer.

direction is a value of Enum.StatusBarTimerDirection:

Value Fill
Enum.StatusBarTimerDirection.ElapsedTime (0) The bar fills as the time goes. This is the default.
Enum.StatusBarTimerDirection.RemainingTime (1) The bar empties as the time goes.
local duration = UnitCastingDuration("target")
if duration then
    bar:SetTimerDuration(duration, Enum.StatusBarInterpolation.Immediate,
        Enum.StatusBarTimerDirection.ElapsedTime)
end
  • The bar keeps the duration object, not a copy. If you change the time span of the object later, the bar shows the change on the next frame.
  • After the end time, the bar stays full (ElapsedTime) or empty (RemainingTime). The timer stays on the bar.
  • While the bar has a timer, GetValue() returns 0 and GetMinMaxValues() returns 0, 0. The timer does not run the OnValueChanged script.
  • SetValue does not remove the timer. SetMinMaxValues removes it.
  • A hidden bar does not move. When it shows again, its fill is correct on its first frame.
  • The bar keeps its fill style, from SetFillStyle, SetReverseFill and SetRotatesTexture. The bar texture moves with the fill, as it does for SetValue.
  • interpolation is a value of Enum.StatusBarInterpolation. The default is Immediate. With ExponentialEaseOut, the fill eases from the fill that the bar showed before, and then follows the timer with the same ease (see statusBar:SetValue). A jump in the time span then eases too.
  • A duration that is not a duration object, or an enum argument that is not one of its values, is an error.

statusBar:SetValue(value [, interpolation]) / statusBar:SetMinMaxValues(min, max [, interpolation])

These are the usual SetValue and SetMinMaxValues, with an optional interpolation argument. interpolation is a value of Enum.StatusBarInterpolation:

Value Fill
Enum.StatusBarInterpolation.Immediate (0) The fill shows the new value at once. This is the default.
Enum.StatusBarInterpolation.ExponentialEaseOut (1) The fill eases from the fill that it showed before to the new value.
healthBar:SetValue(UnitHealth("target"), Enum.StatusBarInterpolation.ExponentialEaseOut)
  • On each frame, the remaining gap is multiplied by exp(-18 * elapsed), with elapsed in seconds. The speed does not change with the frame rate.
  • The ease works on the fill as a part of the bar. Thus, a full-bar jump takes the same time on each range: approximately 0.4 seconds.
  • When the gap is less than 0.1% of the bar, the next frame shows the new value and the ease stops.
  • GetValue() returns the new value at once. OnValueChanged runs one time, at the call, with the new value.
  • A new eased value continues from the fill that the bar shows. An Immediate value stops the ease.
  • With SetMinMaxValues, the fill keeps its part of the bar and eases to the part that the value has in the new range.
  • A hidden bar does not ease. It continues when it shows again.
  • Any interpolation value other than ExponentialEaseOut works as Immediate. It does not cause an error.

statusBar:GetInterpolatedValue() / statusBar:IsInterpolating() / statusBar:SetToTargetValue()

statusBar:GetInterpolatedValue() returns the value that the fill shows now. While the bar eases, it is between the old value and the new value. Else it is the same as GetValue(). statusBar:IsInterpolating() returns true while the bar eases. statusBar:SetToTargetValue() stops the ease and shows the value at once.

statusBar:SetColorFill(colorR, colorG, colorB [, a])

Fills the bar texture with one color. The values go from 0 to 1, and a is 1 when you do not give it. If the bar has no bar texture, the method makes one in the ARTWORK layer.

bar:SetColorFill(0, 0.8, 0)

statusBar:SetStatusBarDesaturation(desaturation) / statusBar:GetStatusBarDesaturation()

Sets how much color the bar texture loses, from 0 (full color) to 1 (gray). This is texture:SetDesaturation() on the bar texture. statusBar:GetStatusBarDesaturation() returns the value.

statusBar:SetStatusBarDesaturated(desaturated) sets the value to 1 when desaturated is true, and to 0 when it is false. statusBar:IsStatusBarDesaturated() returns true when the value is more than 0.

  • The value belongs to the bar texture. A new bar texture starts with full color.
  • A bar with no bar texture ignores the setters and returns 0 or false.

editBox:IsInIMECompositionMode()

Returns 1 while an input method (for example, one for Chinese, Japanese, or Korean) composes text in the box. Returns nil at other times.

OnCharComposition(self, text) (script)

An edit box runs this script each time an input method changes the text that it composes. text is the composed text at that time.

OnTextChanged(self, userInput) (script)

The OnTextChanged script gets one argument. userInput is true when the user changed the text: by typing, deleting, pasting, with an input method, or with editBox:Insert. It is false when SetText or SetNumber changed the text. Handlers that read globals find the same value in arg1.

searchBox:SetScript("OnTextChanged", function(self, userInput)
    if userInput then
        RunSearch(self:GetText())
    end
end)

Edit box caret fixes

  • A text that ends with a color code (|cffffffff), or that has a link with no end, does not freeze the client when the user presses Right or Delete. SetText can give a box such a text.
  • In a single-line box, the caret moves over |n as two characters, as the box shows them.
  • In a right-justified box, a click puts the caret on the correct character when the line has color codes, links, or characters that are not ASCII.

frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])

A rename of the SetMinResize / SetMaxResize pair. The max pair is applied only when both values are given.

frame:SetClampRectInsets(left, right, top, bottom) / frame:GetClampRectInsets()

For a frame clamped to the screen (SetClampedToScreen), the rectangle kept on screen is the frame's own rect with each edge moved by its inset, in the frame's units and screen directions. A negative left or bottom, or a positive right or top, grows that side: the frame stays that much further from the screen edge. The opposite sign lets that much of the frame go off screen. The clamp applies while the frame is moved, dragged or re-laid out. GetClampRectInsets returns the four values, 0, 0, 0, 0 when none are set.

frame:SetClampedToScreen(true)
frame:SetClampRectInsets(0, 0, 0, -60)   -- keep 60 above the bottom
frame:SetClampRectInsets(0, 0, -25, 0)   -- the top 25 may leave the screen

When the grown rect is larger than the screen, it ends up flush with the top right.

button:SetNormalFontObject(font) / button:GetNormalFontObject()

Sets or gets the font of the button's text in its normal state. These methods are the same as button:SetTextFontObject(font) and button:GetTextFontObject(). The highlight and disabled font methods keep their names.

button:SetNormalFontObject(GameFontHighlight)   -- same as SetTextFontObject
button:GetNormalFontObject()                    -- same as GetTextFontObject

button:SetMotionScriptsWhileDisabled(enabled) / button:GetMotionScriptsWhileDisabled()

A disabled button does not run its OnEnter and OnLeave scripts, so it cannot show a tooltip. When enabled is true, the disabled button runs these two scripts when the mouse enters and leaves it. The button does not show its highlight and does not change its text font. With no argument, SetMotionScriptsWhileDisabled sets the value to false. GetMotionScriptsWhileDisabled() returns true or false. The default is false.

You can also set the value in XML with the motionScriptsWhileDisabled attribute on a Button or CheckButton.

button:Disable()
button:SetMotionScriptsWhileDisabled(true)
button:SetScript("OnEnter", function()
    GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
    GameTooltip:SetText("Not available yet")
end)
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
<Button name="MyButton" motionScriptsWhileDisabled="true"/>

button:SetEnabled(enabled)

If enabled is true, the button is enabled, the same as button:Enable(). If enabled is false or nil, the button is disabled, the same as button:Disable(). You can call this method on buttons and check buttons.

button:SetEnabled(GetMoney() >= cost)

region:IsProtected() / region:CanChangeProtectedState()

No frame is protected. region:IsProtected() returns isProtected, isProtectedExplicitly, and both values are always nil. region:CanChangeProtectedState() always returns 1. Libraries such as AceHook-3.0 call these methods before they hook the scripts of a frame. You can call both methods on frames, textures, and font strings.

frame:CanChangeAttribute() / frame:AllowAttributeChanges()

Code can always change the attributes of a frame. frame:CanChangeAttribute() always returns 1. frame:AllowAttributeChanges() does nothing.

frame:SetPropagateKeyboardInput([propagate]) / frame:GetPropagateKeyboardInput()

Sets or gets whether the frame passes keys on to the frames below it.

A frame with an OnKeyDown or OnKeyUp script takes each key that it gets. The frames below it and the key bindings do not get that key. When propagate is true, the OnKeyDown and OnChar scripts of the frame run, and then the key goes on to the frames below and to the key bindings.

  • propagate is false when you omit it.
  • GetPropagateKeyboardInput returns true or false.
  • A frame that passes a key on does not get the OnKeyUp script for that key. The frame that takes the key gets it.
-- Print each key without blocking movement or key bindings.
local f = CreateFrame("Frame", nil, UIParent)
f:EnableKeyboard(true)
f:SetPropagateKeyboardInput(true)
f:SetScript("OnKeyDown", function() print(arg1) end)

You can also set the value in XML with the propagateKeyboardInput attribute:

<Frame name="MyKeyWatcher" enableKeyboard="true" propagateKeyboardInput="true"/>

frame:SetPropagateMouseMotion(propagate) / frame:CanPropagateMouseMotion()

Sets or gets whether the frame passes mouse motion on to the next frame under the cursor.

Usually only the top mouse-enabled frame under the cursor gets mouse motion. When that frame passes motion on, the next mouse-enabled frame under the cursor also gets it, and that frame can pass it on too. The next frame is the frame below in the draw order. It is not always the parent.

Each frame that gets motion gets OnEnter when the cursor comes to it, and OnLeave when the cursor goes away from it. When the cursor moves from a frame onto a child that passes motion on, the frame does not get OnLeave.

  • CanPropagateMouseMotion returns true or false. The default is false.
  • Clicks do not pass on with motion. For clicks, use frame:SetPropagateMouseClicks.
  • In XML, propagateMouseInput="Motion" or propagateMouseInput="Both" on a frame sets the value to true.
local row = CreateFrame("Button", nil, UIParent)
row:SetWidth(200)
row:SetHeight(20)
row:SetPoint("CENTER")
row:SetScript("OnEnter", function(self) self:LockHighlight() end)
row:SetScript("OnLeave", function(self) self:UnlockHighlight() end)

local close = CreateFrame("Button", nil, row)
close:SetWidth(16)
close:SetHeight(16)
close:SetPoint("RIGHT")
close:SetPropagateMouseMotion(true)
-- The row keeps its highlight while the cursor is on the close button.

frame:IsMouseMotionFocus()

Returns true when the frame gets mouse motion at the cursor, and false when it does not. It returns true for each frame in GetMouseFoci().

frame:SetMouseClickEnabled(enabled) / frame:SetMouseMotionEnabled(enabled)

Sets whether the frame takes mouse clicks and whether it takes mouse motion. frame:IsMouseClickEnabled() and frame:IsMouseMotionEnabled() return true or false.

Mouse motion is OnEnter and OnLeave. Mouse clicks are OnMouseDown, OnMouseUp and the clicks of a button. The two are separate:

  • A frame that takes only motion gets OnEnter and OnLeave. A click on it goes to the next frame under the cursor that takes clicks.
  • A frame that takes only clicks never gets OnEnter or OnLeave. The frame under it keeps the mouse motion. A click on the frame goes to the frame.

frame:EnableMouse(enable) sets both values. frame:IsMouseEnabled() returns 1 when the frame takes both clicks and motion, and nil when it does not.

In XML, enableMouseClicks="true" and enableMouseMotion="true" on a frame turn on one value each. enableMouse="true" turns on both.

local overlay = CreateFrame("Frame", nil, UIParent)
overlay:SetAllPoints(UIParent)
overlay:SetMouseMotionEnabled(true)
overlay:SetScript("OnEnter", function() print("over the overlay") end)
-- Clicks go through the overlay to the frames under it.

frame:SetPropagateMouseClicks(propagate) / frame:CanPropagateMouseClicks()

Sets or gets whether a click on the frame also goes to the next frame under the cursor that takes clicks.

Each frame gets OnMouseDown, and then OnMouseUp, from top to bottom. The next frame can pass the click on too.

  • CanPropagateMouseClicks returns true or false. The default is false.
  • In XML, propagateMouseInput="Clicks" or propagateMouseInput="Both" on a frame sets the value to true.

frame:SetDontSavePosition([dontSave]) / frame:GetDontSavePosition()

Sets or gets whether the client saves the position of the frame.

The client saves the position and size of each named frame that the user moved or sized to layout-cache.txt. At the next login, the client puts the frame back there. When dontSave is true, the client does not save the frame. Use this for a frame whose position your addon saves itself.

  • dontSave is true when you omit it.
  • GetDontSavePosition returns 1 or nil.

You can also set the value in XML with the dontSavePosition attribute:

<Frame name="MyAddonFrame" movable="true" dontSavePosition="true"/>

frame:SetUsingParentLevel(usingParentLevel) / frame:IsUsingParentLevel()

Sets or gets whether the frame uses the frame level of its parent.

A child frame usually has a frame level one more than its parent. When usingParentLevel is true, the frame has the same level as its parent. The frame stays at that level when you call SetFrameLevel, when you give it a new parent, and when its parent's level changes with its children.

  • IsUsingParentLevel returns true or false. The default is false.
  • When you set the value to false, the frame keeps its current level.

You can also set the value in XML with the useParentLevel attribute:

<Frame name="MyFrame">
    <Frames>
        <Frame parentKey="Background" useParentLevel="true" setAllPoints="true"/>
    </Frames>
</Frame>

frame:SetClipsChildren(clipsChildren) / frame:DoesClipChildren()

Sets or gets whether the frame clips its child frames.

When clipsChildren is true, each frame under the frame draws only inside the rectangle of the frame. The parts outside the rectangle are not shown, and the mouse does not touch them there. The textures and font strings of the frame itself are not clipped.

The clipped frames keep their frame strata and frame levels. A frame under two clipping frames draws only where both rectangles overlap.

  • DoesClipChildren returns true or false. The default is false.
  • The rectangle is the area of the frame less its hit rect insets.

You can also set the value in XML with the clipChildren attribute:

<Frame name="MyList" clipChildren="true">
    <Size x="200" y="100"/>
</Frame>

frame:SetAlphaGradient(index, gradient) / frame:ClearAlphaGradient() / frame:HasAlphaGradient()

Fades what the frame draws near its edges.

  • index is 0 or 1. Index 0 is the left and top edges. Index 1 is the right and bottom edges.
  • gradient is a vector from CreateVector2D(x, y). x is the length of the fade in from the left or right edge. y is the length of the fade in from the top or bottom edge. The lengths use the scale of the frame. A length of 0 gives no fade at that edge.

At the edge, the alpha is 0. The alpha increases in a straight line to full alpha at the end of the length. The fade applies to the textures and font strings of the frame and to all frames under it.

The fade shows only when the frame clips its children (frame:SetClipsChildren(true)). HasAlphaGradient returns true after you set a gradient, also when the frame does not clip. ClearAlphaGradient removes the gradient of both indexes.

list:SetClipsChildren(true)
list:SetAlphaGradient(0, CreateVector2D(0, 40)) -- fade 40 in from the top
list:SetAlphaGradient(1, CreateVector2D(0, 40)) -- fade 40 in from the bottom

scrollBox:SetEdgeFadeLength(length) uses this method.

Limits:

  • A texture that is desaturated (SetDesaturation above 0) or that has a mask does not fade.
  • With the OpenGL renderer (gxApi set to opengl), nothing fades.

frame:DesaturateHierarchy(desaturation [, excludeRoot])

Sets the desaturation of each texture of the frame and of all frames under it. desaturation is a number from 0 (full color) to 1 (grayscale), as for texture:SetDesaturation(amount). When excludeRoot is true, the textures of the frame itself do not change.

The method sets the value on each texture, so texture:GetDesaturation() returns the new value after the call. A texture that you add later keeps its own value. Font strings and models do not change.

button:DesaturateHierarchy(1) -- gray
button:DesaturateHierarchy(0) -- full color again

frame:GetBoundsRect()

Returns left, bottom, width, height of the smallest rectangle that holds the frame, its shown textures and font strings, and its shown child frames with their contents. The values use the scale of the frame, as region:GetRect() does. A scroll frame does not include its scroll child. If the rectangle is empty, the method returns nothing.

frame:HookScript(scriptType, handler)

Adds HookScript to every frame. The previously-set handler runs first, then handler. Both get the same arguments: (self, event, ...) for OnEvent and (self, ...) for every other script, with every argument the script has. Handlers that read the this / event / argN globals keep working too.

  • When the script has no handler yet, handler becomes the handler, and GetScript returns it.
  • An error in handler is shown like any other script error, through the error handler (geterrorhandler()). The previous handler still runs.
  • Errors: Usage: <name>:HookScript("type", function) for bad arguments (<unnamed> for a frame with no name), and the same doesn't have a "<type>" script error as SetScript for an unknown script type.
button:HookScript("OnEnter", function(self)
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    GameTooltip:SetText("hello")
    GameTooltip:Show()
end)

HookScript takes an optional third argument, bindingType. With LE_SCRIPT_BINDING_TYPE_INTRINSIC_PRECALL or LE_SCRIPT_BINDING_TYPE_INTRINSIC_POSTCALL, the hook goes on that intrinsic script instead of the handler.

Script binding types

Each script of a frame has three parts. They run in this order:

Constant Value Part
LE_SCRIPT_BINDING_TYPE_INTRINSIC_PRECALL 0 The intrinsic script that runs first
LE_SCRIPT_BINDING_TYPE_EXTRINSIC 1 The handler
LE_SCRIPT_BINDING_TYPE_INTRINSIC_POSTCALL 2 The intrinsic script that runs last

The intrinsic scripts come from an intrinsic frame type, or from HookScript with a bindingType.

  • SetScript(scriptType, handler) sets the handler only. SetScript(scriptType, nil) removes the handler, and the intrinsic scripts still run.
  • GetScript(scriptType) returns the handler. It returns nil when the script has only intrinsic scripts.
  • GetScript(scriptType, bindingType) returns one part, or nil.
  • HookScript(scriptType, hook) hooks the handler, so the hook runs before the postcall script.
  • An error in one part is shown through the error handler. The other parts still run.
local f = CreateFrame("EventFrame")
f:SetScript("OnShow", nil)
print(f:GetScript("OnShow", LE_SCRIPT_BINDING_TYPE_INTRINSIC_POSTCALL))

smf:SetHyperlinksEnabled(enabled) / smf:GetHyperlinksEnabled()

On ScrollingMessageFrame and SimpleHTML frames. With hyperlinks off, the frame's OnHyperlinkEnter and OnHyperlinkClick scripts don't run. The links still show in the text. OnHyperlinkLeave still runs, so a tooltip that was open when you turned hyperlinks off still closes. Hyperlinks are on by default. If you call SetHyperlinksEnabled() with no argument, it turns them on.

ChatFrame1:SetHyperlinksEnabled(false)   -- links in this chat window stop reacting
ChatFrame1:GetHyperlinksEnabled()        -- false

smf:GetMessageInfo(index)

On ScrollingMessageFrame. Returns one message of the frame: its text, its color, and the extra values that AddMessage got for the message. Index 1 is the oldest message, and smf:GetNumMessages() is the index of the newest message. If no message has the index, the function returns nothing. If index is not a number, the function gives an error.

  • text is the text that the frame keeps, with its color codes.
  • r, g and b are the color that the frame shows for the message. After UpdateColorByID changes the color, these values also change. The frame keeps each color value in steps of 1/255, so 0.5 returns as 0.50196….
  • smf:AddMessage(text, r, g, b, ...) keeps all the values after b. GetMessageInfo returns these values in the same order, nil values included.

An addon that replaces AddMessage and gives only text, r, g, b to the original function removes the extra values.

ChatFrame1:AddMessage("hello", 1, 0.5, 0, 7, "extra")
ChatFrame1:GetMessageInfo(ChatFrame1:GetNumMessages())
-- "hello", 1, 0.50196078431373, 0, 7, "extra"

for i = 1, ChatFrame1:GetNumMessages() do
    local text, r, g, b = ChatFrame1:GetMessageInfo(i)   -- oldest message first
end

smf:BackFillMessage(text, r, g, b, ...)

On ScrollingMessageFrame. Adds a message as the oldest message of the frame. AddMessage adds the newest message. BackFillMessage adds history behind the messages that the frame already has. An addon that rebuilds a frame from a history walks the history from the newest entry to the oldest and adds each entry with this function. The messages that the frame already has do not move.

  • text, r, g, b and the extra values work as in AddMessage. Without r, g and b, the message takes the color of the frame's font. GetMessageInfo returns the extra values after the color.
  • If the frame is at its line limit (SetMaxLines), the function drops the message, because the message is older than each message that the frame keeps.
  • If the frame is scrolled to the bottom, the frame shows the message when there is room above its other lines. If the frame is scrolled up, the frame keeps its view.
ChatFrame1:BackFillMessage("older", 1, 1, 1)
ChatFrame1:GetMessageInfo(1)   -- "older", 1, 1, 1

smf:RemoveMessagesByPredicate(predicate)

On ScrollingMessageFrame. Removes the messages that predicate selects. The function calls predicate(text, r, g, b, ...) for each message, from the oldest to the newest, with the values that GetMessageInfo returns for the message. If predicate returns true, the function removes the message. The other messages keep their order. If predicate is not a function, the function gives an error.

  • The function calls predicate for all messages before it removes any message. If predicate gives an error for a message, the frame keeps that message.
  • The frame keeps its scroll offset (GetScrollOffset). If fewer messages remain than the offset, the offset becomes the oldest message.
  • The frame shows the lines that remain at once. If no message remains, the result is the same as smf:Clear().
-- remove the messages that were added with the extra value 7
ChatFrame1:RemoveMessagesByPredicate(function(text, r, g, b, id)
    return id == 7
end)

smf:SetScrollOffset(offset) / smf:GetScrollOffset() / smf:GetMaxScrollRange() / smf:ScrollByAmount(amount)

On ScrollingMessageFrame. The scroll offset is the number of lines that the frame is scrolled away from the newest message. When the frame is not scrolled, the offset is 0. The largest offset is GetMaxScrollRange(), that is GetNumMessages() - 1, or 0 for an empty frame. At that offset, the oldest message is the bottom line, or the top line in TOP insert mode.

  • SetScrollOffset(offset) scrolls to offset. An offset less than 0 or more than GetMaxScrollRange() changes to the nearest limit.
  • ScrollByAmount(amount) adds amount to the offset. A positive amount scrolls to older messages. After the oldest message shows on the frame, the offset does not increase. ScrollUp() stops at the same position. As a result, the offset can stay less than GetMaxScrollRange(). The function also shows the faded lines again, also when the offset does not change.
  • If offset or amount is not a number, the function gives an error.
ChatFrame1:ScrollByAmount(3)     -- 3 lines up
ChatFrame1:GetScrollOffset()     -- 3
ChatFrame1:SetScrollOffset(ChatFrame1:GetMaxScrollRange())   -- the oldest message
ChatFrame1:SetScrollOffset(0)    -- the newest message again

smf:SetInsertMode(mode) / smf:GetInsertMode()

On ScrollingMessageFrame. The insert mode tells where the newest message shows.

  • SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM (2) shows the newest message at the bottom, with older messages above it. This is the default.
  • SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP (1) shows the newest message at the top, with older messages below it.

In both modes, the scroll offset counts lines from the newest message. A change of mode moves the lines that show and runs the display-refreshed callbacks. If mode is the current mode, SetInsertMode does nothing. If mode is not one of the two values, the function gives an error.

local f = CreateFrame("ScrollingMessageFrame", nil, UIParent)
f:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP)
f:AddMessage("first")
f:AddMessage("second")   -- shows above "first"
f:GetInsertMode()        -- 1

smf:AddOnDisplayRefreshedCallback(callback)

On ScrollingMessageFrame. Adds a function that the frame calls as callback(frame) after it refreshes the messages that it shows. The frame refreshes after a new message, a scroll, a resize, a font change, or Clear().

  • The callbacks run one time, in the next update of the frame, in the order that you added them.
  • A hidden frame runs its callbacks when it shows again.
  • An error in one callback goes to the error handler, and the other callbacks still run.
  • A change that a callback makes to the frame does not run the callbacks again.
  • If callback is not a function, the method gives an error. You cannot remove a callback.
ChatFrame1:AddOnDisplayRefreshedCallback(function(frame)
    MyScrollLabel:SetText(frame:GetScrollOffset() .. " / " .. frame:GetMaxScrollRange())
end)

frame:IsEventRegistered(event)

Returns true if the frame is registered for event, else false (also false for an unknown event name). If the registration came from frame:RegisterUnitEvent, the unit tokens follow as extra returns, in the order they were given. Works on any frame.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:IsEventRegistered("PLAYER_LOGIN")   -- true
f:IsEventRegistered("PLAYER_LOGOUT")  -- false

f:RegisterUnitEvent("UNIT_HEALTH", "player", "target")
f:IsEventRegistered("UNIT_HEALTH")    -- true, "player", "target"

frame:RegisterUnitEvent(event, ...units)

Registers the frame for event, but the frame's OnEvent runs only when the event's first argument (the unit token) is one of units. Use it in place of RegisterEvent for UNIT_* events, so a handler for one unit does not receive every other unit's events. Returns whether the frame is now registered.

local f = CreateFrame("Frame")
f:RegisterUnitEvent("UNIT_HEALTH", "player")
f:SetScript("OnEvent", function(self, event, unit)
    -- unit is always "player" here
end)

Unit tokens compare without regard to case. Every token that names a unit works, including focus, nameplateN, and markN. A unit that is several tokens at once (your target who is also party1) fires the event once for each token that matches.

Pass as many units as you need. Other clients take at most four, so keep to four for code you also run elsewhere — accepting more is a ClassicAPI extension.

f:RegisterUnitEvent("UNIT_HEALTH", "player", "party1", "party2", "party3")

Behavior notes:

  • A registration keeps its kind until you unregister it. RegisterEvent on a unit-filtered registration keeps the filter. RegisterUnitEvent on a plain registration keeps it plain. RegisterUnitEvent on a unit-filtered registration replaces the units. To switch kinds, call UnregisterEvent first.
  • If you give no unit, or no unit argument is a string, the call acts as RegisterEvent.
  • The filter applies only when the event's first argument is a string. For an event whose first argument is a number, or that has no arguments, the frame receives the event as with RegisterEvent.
  • UnregisterEvent and UnregisterAllEvents remove the filter with the registration.
  • frame:IsEventRegistered(event) returns the unit tokens after the boolean.

FrameUtil.RegisterFrameForUnitEvents(frame, events, ...units) calls RegisterUnitEvent for each event in the events table.

frame:GetEffectiveAlpha()

Returns the frame's effective alpha: the alpha that the frame is drawn with. This is its own GetAlpha() multiplied by the effective alpha of its parent, so a fade on UIParent fades all of its children. Alpha is stored in steps of 1/255: SetAlpha(0.5) stores 127/255 ≈ 0.498, and each multiplication rounds down to a step.

UIParent:GetEffectiveAlpha()   -- 1
-- with UIParent at 0.5 and Minimap's own alpha 1:
Minimap:GetEffectiveAlpha()    -- ~0.498 (0.5 truncates to 127/255)

OnSizeChanged sizes are in frame units

An OnSizeChanged handler receives the frame's new width and height in the units that GetUnscaledWidth() and GetUnscaledHeight() return. A frame of size 20 x 10 gets (20, 10). arg1 and arg2 hold the same values.

frame:SetScript("OnSizeChanged", function(self, w, h)
    label:SetWidth(w - 8)
end)

Frame alpha multiplies into child frames

A frame is drawn at its own alpha multiplied by the drawn alpha of its parent. Thus a child frame is never more visible than its parent.

  • Each frame keeps its own alpha. When the alpha of a parent changes, the alpha of its children does not change.
  • A frame that you create or move into a transparent frame is also transparent.
  • A <Frame alpha="0"> hides the frames in its <Frames> element.
  • GetAlpha() returns the frame's own alpha. GetEffectiveAlpha() returns the drawn alpha.
  • The 3D model of a Model frame uses the drawn alpha of the frame.
local parent = CreateFrame("Frame", nil, UIParent)
local child = CreateFrame("Frame", nil, parent)
child:SetAlpha(0.5)
parent:SetAlpha(0.2)
child:GetAlpha()            -- ~0.498
child:GetEffectiveAlpha()   -- ~0.098
parent:SetAlpha(1)
child:GetAlpha()            -- ~0.498

A child that is under a transparent parent stays hidden, even if you set the alpha of the child to 1. To show the child, set the alpha of the parent.

frame:SetAttribute(name, value) / frame:SetAttributeNoHandler(name, value) / frame:ClearAttribute(name) / frame:GetAttribute(...)

Adds the frame attribute system — a per-frame, case-insensitive key→value store — as native methods on every frame. value can be any Lua type and round-trips exactly.

cleared = frame:ClearAttribute(name) removes an attribute and returns whether it was set. Unlike SetAttribute, it does not fire OnAttributeChanged.

f:SetAttribute("unit", "party1")
f:GetAttribute("unit")            -- "party1"
f:ClearAttribute("unit")          -- true  (was set; now removed)
f:ClearAttribute("unit")          -- false (nothing to clear)
f:SetAttribute("count", 3)
f:GetAttribute("count")           -- 3
f:GetAttribute("missing")         -- nil

GetAttribute also has the modifier form GetAttribute(prefix, name, suffix), which tries, in order, and returns the first match (the precedence used for type1/*type1-style resolution):

  1. prefix..name..suffix
  2. "*"..name..suffix
  3. prefix..name.."*"
  4. "*"..name.."*"
  5. name

The modifier form is used only when exactly these three arguments are passed. A nil prefix or suffix counts as an empty string.

SetAttributeNoHandler sets the value without firing OnAttributeChanged; SetAttribute fires it (see below). SetAttribute(name, nil) clears an attribute; leaving the value out (SetAttribute(name)) is an error.

Attributes can also be set in XML, with an <Attributes> element inside a frame:

<Button name="MyUnitButton" inherits="SecureUnitButtonTemplate">
    <Attributes>
        <Attribute name="unit" value="player"/>
        <Attribute name="count" type="number" value="3"/>
        <Attribute name="enabled" type="boolean" value="true"/>
    </Attributes>
</Button>
  • type is string (the default), number, boolean or nil. Every type except nil needs a value.
  • The attributes are set before the frame's OnLoad runs, and they do not fire OnAttributeChanged.
  • Attributes in an inherited template are set first, so the frame's own values replace them.
  • A malformed entry is skipped with a warning in Logs\FrameXML.log.

Unit-frame mouseover — the headline use. While the cursor is over a frame that has attributes, the mouseover unit token resolves to the frame's unit, as SecureButton_GetModifiedUnit(frame) returns it: the unit attribute, or a modifier-qualified one such as shift-unit while Shift is held, with unitsuffix added. This is the piece of SecureUnitButton behavior unit-frame addons rely on — and there is no combat lockdown or taint here, so no secure machinery is needed to provide it.

local f = CreateFrame("Button", "MyUnitFrame", UIParent)
f:SetWidth(120); f:SetHeight(40)
f:SetPoint("CENTER")
f:SetAttribute("unit", "party1")
-- Hover it → the `mouseover` token resolves to party1:
--   UnitName("mouseover"), UnitHealth("mouseover"), GameTooltip:SetUnit("mouseover"),
--   mouseover-cast, and UPDATE_MOUSEOVER_UNIT all work.

The frame's own scripts are not touched, so an addon that sets its own OnEnter / OnLeave cannot break it. Hovering the frame is the same as hovering the unit's 3D model: the model highlights, the mouseover tooltip shows, and UPDATE_MOUSEOVER_UNIT fires. The unit is read again every frame, so a unit="target" frame follows your current target while hovered, and pressing a modifier switches to the matching unit attribute. Setting a string unit also EnableMouses the frame so a bare frame becomes hoverable at all (real unit frames already are). The unit value may be any token the resolver understands — "party1", "target", "focus", "nameplateN", or a raw GUID literal. Set unit to a non-string (e.g. nil) to stop the binding.

Click actions (type1 / type2 / …). A type attribute makes clicking the frame perform one action on its unit — the secure-button model: exactly one verb per click, resolved from the attributes.

f:SetAttribute("type1", "target")        -- left-click targets the unit
f:SetAttribute("type2", "focus")         -- right-click sets ClassicAPI focus to it
-- click-casting, expressed purely as attributes:
f:SetAttribute("shift-type1", "spell")
f:SetAttribute("shift-spell1", "Flash Heal")   -- shift-left-click heals the unit

Resolution. The verb is read from [prefix]type[suffix], where the prefix is the held modifiers (alt-, ctrl-, shift-, in that order) and the suffix is the button number (1=Left, 2=Right, 3=Middle, 4/5=side). Precedence is prefix..type..suffix → type..suffix → type, so type1 applies under any modifier unless a modifier-specific attribute (shift-type1) overrides it — and type (no suffix) is the catch-all.

Verbs. The resolved verb reads its own extra attributes (also modifier/button-qualified, same precedence as type):

Verb Extra attributes Action
target — Targets the unit (or clears the target if unit is "none"). Respects the engine's default-interaction precedence: with a spell on the cursor it casts on the unit, with an item on the cursor it drops it on the unit, instead of switching target.
assist — Targets the unit's target.
focus — Sets the ClassicAPI focus to the unit.
spell spell Casts the spell. With a unit, it casts on that unit via C_Spell.CastAtUnit — the unit's GUID goes straight to the cast dispatcher (no target juggling), and a ground-target spell lands at the unit's feet. With no unit, it casts on the current target through CastSpellByName, like a plain /cast.
item item, or bag+slot Uses an item on the unit. item may be a name / itemID / link (used via C_Item.UseItemByName, unit as the target) or a "bag slot" string like "0 1" (used via UseContainerItem). The deprecated bag+slot attributes are used when item is unset.
macro macrotext or macro Runs the macrotext text as RunMacroText does. If macrotext is not set, it runs the saved macro that macro names, by name or index. The clicked button is the macro's button. To give the macro a unit, the handler selects the clicked unit, runs the macro, then restores the target from before the click.
stopcasting — Stops the current cast.
menu / togglemenu — Pops the standard unit dropdown at the cursor (whisper / inspect / trade / invite / …, the same menu PlayerFrame / TargetFrame / PartyMemberFrame show).

Every verb that acts on a unit uses the frame's unit attribute (resolved with the same modifier/button precedence).

One verb per click. Setting a type* attribute installs a chained OnClick on that frame only (nothing global). When a verb resolves, our handler owns the click and does not run the frame's previous OnClick — so a configured type1 never fires alongside the addon's own click handler. Unconfigured clicks (no matching type, or an unrecognized verb) fall through to the frame's own OnClick, so an addon can keep custom behavior there. The frame must be a Button registered for the click (left is the Button default; right needs RegisterForClicks("RightButtonUp")).

Clobbering. The handler self-heals: addons that re-SetScript("OnClick", …) (pfUI on every raid relayout) replace our closure, so re-set a type* attribute afterward to reinstall — re-wiring detects and skips its own closure, so it never double-chains.

OnAttributeChanged is a real SetScript / GetScript / HookScript-able frame script, on every frame — SetAttribute fires it after setting the value, SetAttributeNoHandler doesn't. The handler gets (self, name, value), where name is the lowercased attribute name. The this / arg1 / arg2 globals are set to the same values.

f:SetScript("OnAttributeChanged", function(self, name, value)
    if name == "unit" then
        MyFrame_Update(self, value)   -- react to unit changes
    end
end)
f:SetAttribute("unit", "party1")     -- fires the handler (name="unit", value="party1")
f:SetAttributeNoHandler("unit", "party2")  -- sets it silently, no handler

A handler can set attributes itself, on its own frame or another one. Those changes fire their OnAttributeChanged handlers too.

SetModernScriptArgs(enable) / GetModernScriptArgs()

Global toggle (not a frame method) for positional script arguments. The engine always invokes a frame-script handler with zero Lua arguments — the handler reads this / arg1..argN / event as globals. With this enabled, every handler additionally receives its values as real positional arguments, so handlers written as function(self, delta) … end work unmodified:

  • most scripts: (self, arg1..argN) — e.g. OnMouseWheel(self, delta), OnClick(self, button, down), OnValueChanged(self, value), OnUpdate(self, elapsed).
  • OnEvent: (self, event, arg1..argN).
  • OnClick, PreClick and PostClick: down is true for a click on a mouse press and false for a click on a release. For button:Click(name, down), it is the value of down.
  • OnEnter and OnLeave: (self, motion). motion is true when a mouse move caused the enter or leave. It is false when the frame moved, showed or hid under a cursor that did not move.

The this / arg1 / event globals stay set, so a handler that reads them still works. A handler that declares no parameters is unaffected — it cannot see the positional arguments. A frame method or other C function that you give directly to SetScript also gets the positional arguments. For example, f:SetScript("OnDragStart", f.StartMoving) calls f:StartMoving(). SetModernScriptArgs(enable) sets the state and returns it. GetModernScriptArgs() returns the current state.

Caveat — a parameter that was always nil now gets a value. With zero arguments passed, any parameter a handler declared was always nil. When this feature is on, a declared parameter gets its real value. A function(self, delta) handler needs this behavior. But it also changes a handler that declared a parameter and expected it to be nil. One example is a function used both as a direct call (with a real argument) and as a script handler. If such a handler misbehaves, disable the feature with SetModernScriptArgs(false).

Default ON. Positional handler signatures are a core Lua 5.1 feature, so ports that use them work with no setup. It reimplements the tail of the engine's hottest Lua path (the runner that fires for every OnUpdate, every frame); if you ever need the exact stock dispatch, SetModernScriptArgs(false) turns it off and both runners become a straight passthrough.

-- On by default; SetModernScriptArgs(false) would turn it off.
local f = CreateFrame("Frame")
f:EnableMouseWheel(true)
f:SetScript("OnMouseWheel", function(self, delta)
    -- `self` and `delta` are bound; `this` / `arg1` still work too
end)

GetModernScriptArgs()       -- true (default)

XML script bodies see self and their arguments

An XML script body can use self and its script's arguments by name:

<Scripts>
    <OnLoad>self:RegisterEvent("PLAYER_LOGIN")</OnLoad>
    <OnUpdate>self.timer = (self.timer or 0) + elapsed</OnUpdate>
    <OnClick>MyButton_OnClick(self, button, down)</OnClick>
    <OnEvent>MyAddon_OnEvent(self, event, ...)</OnEvent>
</Scripts>
Script Names after self
OnUpdate elapsed
OnEvent event, then ... for the event's arguments
OnClick, PreClick, PostClick button, down
OnDoubleClick, OnMouseDown, OnMouseUp, OnDragStart button
OnMouseWheel delta
OnEnter, OnLeave motion
OnSizeChanged w, h
OnValueChanged value
OnMinMaxChanged min, max
OnTextChanged userInput
OnChar text
OnKeyDown, OnKeyUp key
OnHyperlinkClick link, text, button
OnHyperlinkEnter, OnHyperlinkLeave link, text
OnVerticalScroll, OnHorizontalScroll offset
OnScrollRangeChanged xrange, yrange
OnCursorChanged x, y, w, h
OnAttributeChanged name, value
OnColorSelect r, g, b
OnInputLanguageChanged language

Other scripts, such as OnLoad and OnShow, get self only.

A body gets these names when it does one of these things:

  • It reads self.
  • It uses ....
  • It reads one of its script's names, and none of this / arg1..argN.

Other bodies keep reading the this / arg1..argN and event globals, and nothing they read changes. event alone does not count.

The values come from the positional script arguments, so they need SetModernScriptArgs to be on (the default). _classicapi_SetTranspileOption("XmlHandlerArgs", false) turns this off.

SecureCmdOptionParse(options [, quiet])

Parses a macro conditional string. Returns the value of the first clause that matches. This is the parser behind /cast [combat] Spell-style options.

The options string is a list of clauses, separated by ;. A clause is zero or more [...] condition groups, then a value:

SecureCmdOptionParse("[combat] Attack; [nostealth] Prowl; Rest")

Rules:

  • A clause with no group always matches.
  • Groups in one clause are OR'd. The first group that passes wins.
  • Conditions in a group are separated by , and are AND'd.
  • A no prefix negates a condition. A :a/b/c suffix adds arguments, which are OR'd.
  • A @unit or target=unit piece sets the group's target. Conditions that need a unit use it, and default to "target".
  • The unit can be a character name as well as a token, so [target=Feral] Rejuvenation heals the player called Feral. The name matches on its start and picks the nearest match, and it has to be someone around you. See UnitTokenFromName.
  • A group whose only piece is a @unit matches only while that unit exists. So [@mouseover][] Spell uses the mouseover unit when there is one, and falls through to [] (your target) when there is not. A bare [] always matches. A group with conditions leaves the existence test to them, so [@focus,noexists] works as written.
  • @none and @cursor name no unit, so they always match. @none asks for no unit: /target clears your target, and the other commands act with no unit named. @cursor names the position under your mouse, which /cast and /use use to place a ground-target spell or item.

Returns the matched value, plus the passing group's target token as a second value (nil when the group set no target). Returns nil when no clause matches.

A condition this parser does not know cannot pass, so its group fails. The name of the first such condition comes back as a third value, and the parser also prints it once. Pass quiet to get the third value without the message.

Use quiet when you read options that somebody else wrote. Another macro addon has its own conditions, and a macro that works under that addon names conditions this parser never heard of. The third value tells you those options belong to that addon, so you can leave them to it. The #showtooltip reader does exactly this: a macro whose conditions it cannot evaluate stays as the engine left it.

SecureCmdOptionParse("hello")                -- "hello"
SecureCmdOptionParse("[combat] a; b")        -- "a" in combat, else "b"
SecureCmdOptionParse("[@focus,harm] a; b")   -- "a", "focus" when focus is hostile
SecureCmdOptionParse("[nocombat] rest")      -- "rest" out of combat, else nil

Supported conditions. Each maps to real 1.12 state:

Condition Meaning
combat the player is in combat
exists / dead the target exists / is dead or a ghost
help / harm the target is friendly / hostile
party / raid the target is in your party / raid
group / group:party / group:raid you are in a group / party or raid / raid
stance / stance:N you are shapeshifted / in bar form N (form is the same)
stealth / mounted / swimming / indoors / outdoors player state
mod / mod:shift / mod:ctrl / mod:alt a modifier key is held (modifier is the same)
button:N the click running now used button N — 1 left, 2 right, 3 middle, 4, 5 (btn is the same)
bar:N / actionbar:N the current action bar page is N
bonusbar / bonusbar:N a bonus bar is active / bonus bar N is active
pet / pet:name you have a pet / a pet with that name or family
channeling / channeling:spell you are channeling / channeling that spell
equipped:type an item of that type is equipped, as IsEquippedItemType tells (worn is the same)
cursor the cursor holds an item, a spell, or money
spec / spec:1 always true (there is one spec)
known:spellID / known:name you know that spell

Always false. flying, flyable, vehicleui, and unithasvehicleui describe state that does not exist here, so they never match.

Unknown condition. An unrecognized keyword never matches, and prints a warning once. Keywords are case-sensitive: [Combat] is unknown, [combat] is not.

The [stance:N] number is the shapeshift bar slot, not the form ID that GetShapeshiftFormID() returns. On a druid, bar slot 1 is Bear and slot 3 is Cat. The condition tests the value of GetShapeshiftForm(true). A paladin aura is on the stance bar, but it is not a form, so [stance] is false for a paladin.

[button:N] matches two buttons. The first is the button of the click the macro is running inside, the same value GetMouseButtonClicked() returns. The second is the button the macro was started with, the same value GetRunningMacroButton() returns. You can give a button name in place of the number: [button:rightbutton] and [button:2] are the same test, and names are not case-sensitive.

When neither button is set, [button:N] answers as if the left button were used. That is the case for a state driver poll and for the re-evaluation behind a macro's #showtooltip icon, neither of which comes from a click.

known is a ClassicAPI extension, not a Blizzard conditional. A numeric argument uses IsPlayerSpell, so it matches any known spell — a spellbook spell, a talent, a profession recipe, or a racial. A name argument matches only a spellbook spell, because there is no name-to-ID resolver. Use the spell ID for a talent or a recipe.

RegisterStateDriver / UnregisterStateDriver

RegisterStateDriver(frame, state, values) drives a frame's state from a macro conditional string. The driver re-runs SecureCmdOptionParse(values) on a 0.2 second poll and applies the result.

For the state "visibility", the value "show" or "hide" shows or hides the frame:

RegisterStateDriver(myFrame, "visibility", "[combat] hide; show")

For any other state, the driver sets the attribute "state-"..state to the value. An OnAttributeChanged handler then reacts to it.

UnregisterStateDriver(frame, state) removes one driver. UnregisterStateDriver(frame) with no state removes every driver on the frame.

The driver also rescans at once on combat, target, focus, form, pet, group, action-bar, and modifier-key changes, so state that a poll would lag catches up without delay.

RegisterAttributeDriver / UnregisterAttributeDriver

RegisterAttributeDriver(frame, attribute, values) is the same as the state-driver pair, but it sets attribute directly instead of adding the "state-" prefix. An attribute name that starts with _ is ignored.

The driver coerces a numeric value to a number, and the literal string "nil" to nil, before it sets the attribute.

UnregisterAttributeDriver(frame, attribute) removes one driver. UnregisterAttributeDriver(frame) with no attribute removes every driver on the frame.

RegisterUnitWatch / UnregisterUnitWatch / UnitWatchRegistered

RegisterUnitWatch(frame [, asState]) shows or hides a frame as its unit attribute comes into and out of existence. Set the unit attribute first:

myUnitFrame:SetAttribute("unit", "target")
RegisterUnitWatch(myUnitFrame)     -- shown only while you have a target

With asState true, the watch sets the boolean attribute "state-unitexists" instead of calling Show/Hide, so an OnAttributeChanged handler drives the visibility.

The Show/Hide form also sets a "statehidden" attribute (true when hidden), so unit-frame code can tell a driver-hidden frame from one the user hid.

UnregisterUnitWatch(frame) stops the watch. The frame stays in its last shown or hidden state. UnitWatchRegistered(frame) returns whether a watch is active.

SecureButton_GetAttribute / SecureButton_GetUnit

SecureButton_GetAttribute(frame, name) reads an attribute. When the frame opts in with a "useparent-"..name or "useparent*" attribute, it walks up to the parent frame to find the value.

SecureButton_GetUnit(frame) returns the frame's unit attribute, with an optional unitsuffix attribute appended. The unit-watch and click systems use it to find a frame's unit.

No taint. These are functional copies of the secure templates. There is no combat lockdown or taint here, so nothing is protected — the names exist for addon compatibility.

PreClick / PostClick button scripts

Two button scripts that run immediately before and after a click. They are real scripts: set them with SetScript, read them with GetScript, and hook them with HookScript.

One click runs the handlers in this order:

PreClick  ->  OnClick  ->  PostClick

PreClick and PostClick fire on every click. The button does not need an OnClick handler.

Each handler gets arg1 — the mouse button name ("LeftButton", "RightButton", …) — the same value OnClick gets. With positional arguments on (the default), the handler signature is function(self, button).

A CheckButton changes its checked state before PreClick runs, so GetChecked() inside PreClick already returns the new state.

A double-click runs OnDoubleClick only. It does not run PreClick or PostClick.

local btn = CreateFrame("Button", "MyButton", UIParent)
btn:SetScript("PreClick",  function() print("before the click:", arg1) end)
btn:SetScript("OnClick",   function() print("the click") end)
btn:SetScript("PostClick", function(self, button) print("after the click:", button) end)

Button:RegisterForClicks("AnyUp" | "AnyDown" | ...)

RegisterForClicks accepts two collective names:

  • "AnyUp" — the button responds to the release of every mouse button.
  • "AnyDown" — the button responds to the press of every mouse button.

"AnyUp" stands for the five *Up names and "AnyDown" for the five *Down names. You can mix them with the explicit names.

btn:RegisterForClicks("AnyUp")
btn:RegisterForClicks("AnyDown", "LeftButtonUp")

The explicit names are LeftButtonUp, LeftButtonDown, RightButtonUp, RightButtonDown, MiddleButtonUp, MiddleButtonDown, Button4Up, Button4Down, Button5Up, and Button5Down. Names are not case-sensitive.

A name that is not in this list counts as no click type. A call with only unknown names makes the button respond to no click at all. Button:Click() ignores the click types, so a button in that state still responds to Click().

You can also set the click types in XML with the registerForClicks attribute on a Button or CheckButton. Put spaces or commas between the names. The attribute replaces the click types of an inherited template, and a RegisterForClicks call in OnLoad replaces the attribute.

<Button name="MyButton" registerForClicks="AnyUp"/>
<Button name="MyRowButton" registerForClicks="LeftButtonUp, RightButtonUp"/>

button:RegisterForMouse(...)

Sets which mouse buttons run the OnMouseDown and OnMouseUp scripts of the button. Each argument is a mouse button and an action: LeftButtonDown, LeftButtonUp, RightButtonDown, RightButtonUp, MiddleButtonDown, MiddleButtonUp, Button4Down, Button4Up, Button5Down, or Button5Up. AnyDown and AnyUp stand for all five buttons. Names are not case-sensitive, and a name that is not in this list counts as no button.

  • A new button responds to AnyDown and AnyUp.
  • Each call replaces the previous one. A call with no argument removes all buttons.
  • OnMouseUp runs only when the down action of the same button is also registered.
  • This method changes only OnMouseDown and OnMouseUp. Clicks still follow RegisterForClicks, and drags still follow RegisterForDrag.
btn:RegisterForMouse("RightButtonDown", "RightButtonUp")
btn:SetScript("OnMouseDown", function() print("down", arg1) end)  -- right button only
btn:SetScript("OnMouseUp",   function() print("up", arg1) end)    -- right button only

GetClickFrame(name)

Returns the frame with the given global name. The /click command uses it to change a frame name into the frame.

The function returns the frame only when _G[name] holds a real frame, and the GetName() of that frame is equal to name. If the global holds a different value, a plain table, or a frame with a different name, the function returns nil. This name test makes sure that a changed global does not return the wrong frame.

GetClickFrame("MyButton")     -- the frame named "MyButton", or nil

FriendList

C_FriendList.SendWhoQueryByName(name)

Issues a /who name-filter query for a specific player. Results buffer into the engine's WhoList so a normal WHO_LIST_UPDATE + GetWhoInfo(i) flow can read them — no chat output, no "Found N players matching..." system message.

The engine exposes SendWho(query) and SetWhoToUI(flag) only as separate primitives, so addons that want to silently look up a single player's class/level/zone (e.g. to color an unknown name in chat) have to manage state, cooldown timing, and friends-panel suppression themselves. This collapses the invocation half of that dance to one call with a clean true/false return.

Returns true if the query was sent, false if any of:

  • the name was empty / nil
  • the call is within the 5-second cooldown of a previous send
  • the engine's WhoSystem isn't initialized yet (pre-login)

A false return is a no-op — the engine state isn't touched, no pending flag is set, and the cooldown isn't extended. Safe to call on every chat line that mentions an unknown player; the call will naturally rate-limit.

The cooldown matches the server's: CMSG_WHO is silent-dropped server-side at roughly 5-second granularity, so a faster client just wastes queries that won't get a response.

if C_FriendList.SendWhoQueryByName("Bob") then
    -- query sent; result will arrive via WHO_LIST_UPDATE within ~1s
end

This call alone does not suppress the friends-panel popup on response — for that, wrap FriendsFrame_OnEvent and gate on C_FriendList.IsWhoQueryPending():

local original = FriendsFrame_OnEvent
_G.FriendsFrame_OnEvent = function()
    if event == "WHO_LIST_UPDATE" and C_FriendList.IsWhoQueryPending() then
        return
    end
    return original()
end

(Auto-suppressing the popup from C++ would require hooking the engine's FrameScript_SignalEvent or the SMSG_WHO opcode handler — both high-traffic / high-risk sites. The wrap above is reliable and takes 5 lines, so it stays addon-side for now.)

Implementation reads the WhoSystem singleton from 0x00C28168 and calls the inner sender at 0x005AEBB0 (__thiscall(this = WhoSystem, queryStr)), the same chokepoint Script_SendWho tail-calls. The whoToUI flag at 0x00C2A12C is flipped to 1 first so the SMSG_WHO handler routes results into the list (WHO_LIST_UPDATE path) instead of printing to chat.

C_FriendList.IsWhoQueryPending()

Returns true within ~5 seconds of the most recent SendWhoQueryByName that returned true; otherwise false. Time-based — the engine doesn't expose a "response arrived" hook to C++ yet, so the window is a conservative upper bound on the response RTT, which in practice lands within a few hundred ms.

The intended use is gating an addon-side FriendsFrame_OnEvent wrap so the user-issued /who window still works while addon- issued silent queries don't pop the panel — see the example under SendWhoQueryByName.

Concurrent callers see a shared pending flag — if pfUI and another addon both call SendWhoQueryByName close in time, both queries contribute to the same pending window. This is acceptable for the "suppress popup for any in-flight DLL-issued query" use case; addons that need per-call tracking should manage their own ticket state on top.

C_FriendList.GetNumWhoResults()

Returns two numbers: how many /who results you can read, and a server-reported count.

local shown, serverCount = C_FriendList.GetNumWhoResults()

shown is the number of entries you can index with GetWhoInfo. The server caps the list (50, or 49 on some cores), so shown is at most that. Run a /who (or SendWhoQueryByName) first to populate the list.

serverCount is the second header value the server sends, and its meaning depends on the server core:

  • Mangos and cmangos derived cores send the number of characters that matched your query.
  • Turtle WoW (tortoise-wow) sends the whole realm's online population when more than 49 players are online, ignoring your filter. Below that count it sends the match count.

Do not rely on one meaning across servers. On Turtle it is a live population counter for any query. On other cores it is a match total.

C_FriendList.GetWhoInfo(index)

Returns a table for the /who result at index (1-based), or nil when the index is out of range.

local info = C_FriendList.GetWhoInfo(1)
-- {
--   fullName = "Nadamage", fullGuildName = "",
--   level = 26, raceStr = "Troll",
--   classStr = "Mage", filename = "MAGE",
--   area = "Hillsbrad Foothills",
-- }

Table fields:

  • fullName — the character name.
  • fullGuildName — the guild name, or "" when the player has no guild.
  • level — the character level.
  • raceStr — localized race name, or nil when unknown.
  • classStr — localized class name, or nil when unknown.
  • filename — locale-independent class token ("WARRIOR", "MAGE", …), or nil when unknown. This is the key for RAID_CLASS_COLORS. The stock GetWhoInfo does not provide it, which is the main reason to use this table form.
  • area — the zone name.

There is no gender field — the /who result stores no sex.

The global GetWhoInfo(index) is also kept, which returns positional values and no class token. This namespaced form returns a table and adds filename.

C_FriendList.IsFriend(guid)

Returns true if the player with the given GUID is on your friends list, false if not.

C_FriendList.IsFriend(UnitGUID("target"))    -- true if the target is a friend
C_FriendList.IsFriend("0x00000000000ABCDE")

guid is a GUID string — the "0x…" form that UnitGUID returns. You can also pass a plain character name, because the friends list is keyed by name (AddFriend(name)).

The check reads the engine's friends list directly. It matches the input GUID against each friend's stored GUID. The server sends that GUID for both online and offline friends. As a fallback it also compares character names, so the answer holds for any friend the client saw this session.

C_FriendList.IsIgnored(token)

Returns true if the player is on your ignore list, false if not.

C_FriendList.IsIgnored("Bob")
C_FriendList.IsIgnored("0x00000000000ABCDE")

token is a character name or a GUID string. The ignore list stores GUIDs, not names. A GUID always matches. A name matches only a player the client has seen this session — the same players GetIgnoreName can name — because the name comes from the client's name cache.

C_FriendList.IsIgnoredByGuid(guid)

Returns true if the player with the given GUID is on your ignore list, false if not.

C_FriendList.IsIgnoredByGuid(UnitGUID("target"))
C_FriendList.IsIgnoredByGuid("0x00000000000ABCDE")

guid is a GUID string — the "0x…" form that UnitGUID returns. This is the exact key the ignore list uses, so it works for every ignored player.

C_FriendList.GetNumFriends()

Returns the number of players on your friends list. The stock global GetNumFriends() returns the same count — this is the namespaced form.

C_FriendList.GetNumFriends()   -- e.g. 12

C_FriendList.GetNumOnlineFriends()

Returns how many friends on your list are online. This is a subset of GetNumFriends, which counts online and offline friends together.

C_FriendList.GetNumOnlineFriends()   -- e.g. 3

There is no stored online count, so the value is the number of friends whose connected flag is set — the same flag GetFriendInfo reports.

C_FriendList.GetFriendInfo(name)

Returns a FriendInfo table for the friend with the given name, or nil if that name is not on your list. The name match is case-insensitive.

local info = C_FriendList.GetFriendInfo("Sarahnity")
-- {
--   name = "Sarahnity", connected = true, level = 60,
--   className = "Druid", classFilename = "DRUID", area = "Stratholme",
--   guid = "0x000000003B9CB7BF",
--   afk = false, dnd = false,
--   mobile = false, referAFriend = false, rafLinkType = 0,
-- }

Table fields:

  • name — the friend's character name.
  • connected — true when the friend is online.
  • level — the friend's level, or 0 when unknown.
  • className — localized class name, or nil when unknown.
  • classFilename — locale-independent class token ("WARRIOR", "MAGE", …), or nil when unknown. Present whenever className is. This is the key for RAID_CLASS_COLORS and other class tables, so you can color a friend's name without matching the localized class name.
  • area — localized zone name, resolved to the parent zone, or nil when unknown.
  • guid — the friend's GUID string.
  • notes — your note for this friend, or nil if none. Set it with SetFriendNotes.
  • afk / dnd — the friend's status flags.
  • mobile / referAFriend / rafLinkType — always false / false / 0. There is no mobile app, Recruit-A-Friend, or RAF link data.

Notes are a ClassicAPI addition — there is no stock note field. ClassicAPI persists them per character in WTF\Account\<acct>\<realm>\<char>\ClassicAPI_FriendNotes.txt, local to this client (not stored on the server, not shared with other machines).

C_FriendList.GetFriendInfoByIndex(index)

The same FriendInfo table, addressed by a 1-based list index instead of a name. Returns nil for an index below 1 or above GetNumFriends.

for i = 1, C_FriendList.GetNumFriends() do
    local info = C_FriendList.GetFriendInfoByIndex(i)
    print(info.name, info.connected, info.className, info.area)
end

See C_FriendList.GetFriendInfo for the field list.

C_FriendList.SetFriendNotes(name, notes)

Sets your note for the friend with the given name. Pass an empty string or nil to clear it. There is no return value, and the name must already be on your friends list.

C_FriendList.SetFriendNotes("Sarahnity", "raid healer")
C_FriendList.SetFriendNotes("Sarahnity", "")   -- clears the note

The note is stored client-side and read back through the notes field of GetFriendInfo. Setting a note fires FRIENDLIST_UPDATE so the friends UI and note-aware addons refresh. See the persistence note under GetFriendInfo.

C_FriendList.SetFriendNotesByIndex(index, notes)

The same as SetFriendNotes, addressed by a 1-based list index instead of a name. An out-of-range index does nothing.

C_FriendList.SetFriendNotesByIndex(1, "tank")

AddOrRemoveFriend(name[, notes])

If a friend has the name name, removes that friend. If not, adds name as a friend. The name compare ignores case. When name is not a string, the function does nothing.

When the function adds a friend, it keeps notes as the note of that friend. The note is set when the server adds the friend. If the server does not add the friend, the note is discarded at logout.

AddOrRemoveFriend(UnitName("target"), "met in Stranglethorn")

GameObject

The gameobject analog of C_CreatureInfo — reads the client-side gameobject cache (gameobjectcache.wdb, fed by SMSG_GAMEOBJECT_QUERY_RESPONSE) by GO entry ID.

C_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)

Synchronous peek — returns a table for a cached gameobject, nil otherwise:

local info = C_GameObjectInfo.GetGameObjectInfoByID(47297)
-- { gameObjectID = 47297, name = "Mesa Elevator", type = 11, displayID = 360 }

type is the GameObjectType (0=door, 1=button, 3=chest, 5=generic, 6=trap, 10=goober, …). Field offsets verified against the binary (name [block+0x08]) and real gameobjectcache.wdb rows (Mesa Elevator 47297→type 11; Windrunner 176250→type 15 displayID 7087).

C_GameObjectInfo.RequestLoadGameObjectByID(gameObjectID)

Asynchronously fetches an uncached gameobject — issues SMSG_GAMEOBJECT_QUERY and fires GAMEOBJECT_DATA_LOAD_RESULT( gameObjectID, success) when it lands, after which GetGameObjectInfoByID returns its data. Returns true if accepted; fires synchronously on a cache hit. Same shape as C_CreatureInfo.RequestLoadCreatureByID, and rides the same shared Cache::QueryLoad dispatcher (the gameobject cache is a sibling class of the creature cache with its own _GetRecord/parser, hooked separately).

local f = CreateFrame("Frame")
f:RegisterEvent("GAMEOBJECT_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()  -- arg1 = gameObjectID, arg2 = success
    if arg2 then local info = C_GameObjectInfo.GetGameObjectInfoByID(arg1) end
end)
C_GameObjectInfo.RequestLoadGameObjectByID(7000032)

ClosestGameObjectPosition(gameObjectID)

Returns xPos, yPos, distance — the world position of the nearest game object with the given GO-template ID, and its distance from the player in yards. Returns nothing (nil) when no matching object is in range.

local x, y, dist = ClosestGameObjectPosition(1617)   -- nearest Herb Bush, etc.
if x then
    -- x, y = world coords; dist = yards from the player
end

The game-object counterpart of ClosestUnitPosition — same visible- object-manager scan, filtered to game objects (GUID prefix 0xF110). Same caveat: this finds the nearest currently-visible object of that entry rather than reading a static starting-zone database (there is no such database here).

GameTooltip

GameTooltip:SetItemByID(itemID)

Renders an item tooltip from just an itemID. The alternative is constructing an item hyperlink and calling SetHyperlink — tooltip:SetHyperlink("item:" .. id .. ":0:0:0:0:0:0:0") — which works but forces every caller to know the hyperlink format.

GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetItemByID(6948)  -- Hearthstone
GameTooltip:Show()

Implementation: snprintf the hyperlink string and dispatch to the existing Script_GameTooltip_SetHyperlink (registry slot 12).

Item cache caveat. 1.12 lazy-loads item data into the client-side cache at 0x00C0E2A0 — the cache is fed by SMSG_ITEM_QUERY_SINGLE_RESPONSE only when the player encounters an item. For an itemID the player has never seen, the tooltip renders only the name; full data appears once the cache is warm.

The fix is to ensure the item is cached before opening the tooltip:

if C_Item.IsItemDataCachedByID(itemID) then
    GameTooltip:SetItemByID(itemID)
else
    C_Item.RequestLoadItemDataByID(itemID)
    -- register for ITEM_DATA_LOAD_RESULT(loadedItemID, success)
    -- and call SetItemByID once the matching itemID arrives
end

This caching behavior matches what C_Item.GetItemInfoInstant documents — same underlying cache. The same caveat applies with C_Item.RequestLoadItemData(itemLocation) / Item:OnItemLoad-style continuation.

GameTooltip:SetItemByGUID(itemGUID)

Renders the tooltip for a specific item instance identified by GUID — same string C_Item.GetItemGUID returns ("0xHHHHHHHHLLLLLLLL"). Distinct from SetItemByID: this path goes through the live CGItem, so the tooltip includes enchant lines, random-suffix-decorated name + bonuses, and locked/broken state — the same depth SetBagItem / SetInventoryItem would produce for that specific copy.

local guid = C_Item.GetItemGUID({ equipmentSlotIndex = 10 }) -- legs
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetItemByGUID(guid)
GameTooltip:Show()

Resolves through the engine's own FUN_OBJECT_RESOLVE_BY_GUID (same path C_Item.GetItemLocation uses), builds the full item:N:enchant:gem:gem:gem:gem:suffix:unique-style hyperlink via the engine's link builder, then dispatches to Script_GameTooltip_SetHyperlink. The link round-trip is what gives GameTooltip:GetItem() the dressed link back when called after — same shape GetInventoryItemLink / GetContainerItemLink emit for the same slot.

Silent no-op when the GUID is malformed, the item isn't currently loaded in the client (e.g. items only the server knows about), or the GUID resolves to a non-item object (creature/gameobject GUIDs go to a different resolver and won't match).

GameTooltip:SetUnitAura(unit, index, [filter])

Unified-aura method. index is in the same HELPFUL / HARMFUL index space as C_UnitAuras.GetAuraDataByIndex, so an index you got from C_UnitAuras always opens the matching tooltip. That includes a debuff that the server parked in a buff slot. The tooltip itself comes from the engine's own SetUnitBuff or SetUnitDebuff. filter defaults to helpful when omitted. Only its HELPFUL or HARMFUL token is read, so pass an index from the same plain list.

GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetUnitAura("player", 1, "HELPFUL")  -- first buff
GameTooltip:SetUnitAura("player", 1, "HARMFUL")  -- first debuff
GameTooltip:SetUnitAura("player", 1)              -- defaults to HELPFUL
GameTooltip:Show()

The engine methods index their own slot ranges, which differ from the C_UnitAuras index space once a debuff sits in a buff slot. This method translates between the two, so you never need to without conditionally splitting on filter.

GameTooltip:AddTexture(texture)

Puts a texture at the start of the tooltip's last line and moves the text of that line to the right of the texture. Returns true when the texture was added, false when it was not.

GameTooltip:AddLine("Hearthstone")
GameTooltip:AddLine("Use: Returns you to your home inn.")
GameTooltip:AddTexture("Interface\\Icons\\INV_Misc_Rune_01")
GameTooltip:Show()

The call adds no texture and returns false in these cases:

  • The tooltip has fewer than two lines. The first line cannot have a texture.
  • The tooltip already has three textures.
  • The last line already has a texture.
  • The line above the last line has a texture. Put at least one line without a texture between two textured lines.

ClearLines and every Set* call remove the textures. Each texture is 12x12. Do not resize or move a texture after you add it: the text of the line does not move with it.

GameTooltip:SetSpellByID(spellID [, isPet [, showSubtext]])

Shows the tooltip of a spell. The spell can be any spell ID, also a spell that the player did not learn.

Argument Type Description
spellID number The ID of the spell.
isPet boolean If true, the cost, range, and cooldown are those of the pet. The default is false.
showSubtext boolean If true, the rank shows to the right of the spell name. The default is false.

Returns 1 when the tooltip shows a cooldown that counts down. Otherwise, returns nil. The tooltip shows the remaining cooldown only for a spell with a cooldown of 1 minute or more.

If spellID is not a number, or is less than 0, the function raises an error. If no spell has this ID, the tooltip clears and hides.

GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetSpellByID(133)              -- Fireball
GameTooltip:SetSpellByID(133, false, true) -- Fireball, with its rank
GameTooltip:Show()

GameTooltip:AddSpellByID(spellID [, isPet [, showSubtext]])

Adds the tooltip of a spell below the lines that the tooltip already shows. The tooltip does not clear first. The arguments and the return value are the same as for SetSpellByID.

Use this function to make one tooltip from more parts, for example a header line and a spell.

GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:AddLine("Rank 3 grants:")
GameTooltip:AddSpellByID(133)  -- Fireball, below the header
GameTooltip:Show()

This function does not change the value of GetSpell. GetSpell continues to give the spell of the tooltip before the added lines. If no spell has this ID, the function adds no lines.

GameTooltip:GetItem()

Returns (name, link, itemID) for whichever item the tooltip is currently displaying, or nothing if it isn't showing an item. The standard call returns only (name, link); we extend with itemID as a third return so callers don't have to gsub-extract it from the link.

The engine stashes two fields per Set* item call:

  • tooltip+0x398 ← itemID (always populated)
  • tooltip+0x380/+0x384 ← item GUID (only when there's a real CGItem — SetBagItem, SetInventoryItem, SetLootItem, SetMerchantItem, etc. Zero for SetItemByID / SetHyperlink which have no instance.)

Both fields are zeroed by the per-tooltip Clear on Hide / before the next Set*.

GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")

-- SetItemByID — no per-instance data, basic link returned
GameTooltip:SetItemByID(6948)
local name, link, id = GameTooltip:GetItem()
-- name = "Hearthstone"
-- link = "|cffffffff|Hitem:6948:0:0:0:0:0:0:0|h[Hearthstone]|h|r"
-- id   = 6948

-- SetInventoryItem — full dressed link with enchant + random suffix
GameTooltip:SetInventoryItem("player", INVSLOT_BACK)
local name, link, id = GameTooltip:GetItem()
-- name = "Superior Cloak of the Eagle"   -- decorated name, agrees with the link
-- link = "|cff1eff00|Hitem:9805:247:843:0|h[Superior Cloak of the Eagle]|h|r"
-- id   = 9805

Two link paths:

Set* path Link form Dressing
SetBagItem, SetInventoryItem, SetLootItem, SetMerchantItem, … Full dressed link via the engine's own builder at 0x0052AE00 Enchant ID, random-suffix factor, unique ID, suffix-decorated name
SetItemByID, SetHyperlink (item:N with no instance data) Basic colored link itemID + cached quality + cached name only

The dressed-link path works for items not in player inventory (merchant, loot, mailbox, trade window, etc.) — the engine's resolver finds any CGItem the client has loaded.

Returns nothing for: non-item tooltip, uncached itemID on the no-GUID path (fires a background cache warmup), or empty name.

GameTooltip:GetSpell()

Returns (name, rank, spellID) for whichever spell the tooltip is currently displaying, or nothing if it isn't showing a spell. The engine stashes the spellID on the tooltip frame at +0x39C whenever any SetX spell path runs (SetSpell, SetSpellByID, SetUnitBuff/SetUnitDebuff, SetTalent, etc.) and zeroes it on the next Clear / Hide.

GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetSpellByID(25306)
local name, rank, spellID = GameTooltip:GetSpell()
-- name = "Fireball"
-- rank = "Rank 12"
-- spellID = 25306

rank is the empty string (not nil) for spells whose Spell.dbc rank slot is blank — most racials, talent passives, and proc-triggered spells. The rank position is always populated.

GameTooltip:HasItem() / GameTooltip:HasSpell()

Boolean companions to GetItem / GetSpell. Return true if the tooltip is currently displaying an item / spell, false otherwise. Cheaper than GetItem / GetSpell when all you need is the predicate — single uint32 read against OFF_TOOLTIP_ITEM_ID / OFF_TOOLTIP_SPELL_ID, no DBC lookup or link-build.

if GameTooltip:HasItem() then
    local _, link = GameTooltip:GetItem()
    -- only do the GetItem work when we actually need it
end

GameTooltip:GetUnit() / GameTooltip:GetUnitGUID() / GameTooltip:HasUnit()

GetUnit() returns (name, unitToken) for the unit that the tooltip shows. It returns nothing if the tooltip does not show a unit.

  • name is the unit's display name. This is the same string that is in the tooltip header. For a remote unit that is not in the cache yet, it is "UNKNOWNOBJECT" or "Unknown Being".
  • unitToken is the string that was given to SetUnit, as it was given (for example "target", "party2", "focus"). For the tooltip that shows when the cursor is on a unit in the world, it is "mouseover". For the tooltip that shows when the cursor is on a unit frame with a unit attribute, it is the frame's unit (for example "party1"). This token also works for a group member who is offline or far away, but "mouseover" does not.

GetUnitGUID() returns (name, guidString). guidString has the "0xHHHHHHHHLLLLLLLL" format of UnitGUID(unit). Use it when you must identify the unit after its token changes. For example, "mouseover" does not point to the unit after the cursor moves away, but the GUID stays the same.

HasUnit() returns true if the tooltip shows a unit.

IsUnit(unit) returns 1 if unit names the unit that the tooltip shows, and nil if not. It compares units, not token strings. For example, if the tooltip shows your target and your target is you, then IsUnit("target") and IsUnit("player") both return 1. A string that is not a unit token gives nil.

GameTooltip:SetUnit("target")
local name, unit = GameTooltip:GetUnit()
-- name = "Hogger", unit = "target"

local _, guid = GameTooltip:GetUnitGUID()
-- guid = "0xF130001234..." (Creature) or "0x000000...ABC123" (Player)

if GameTooltip:HasUnit() then
    -- fast check, no name lookup
end

if GameTooltip:IsUnit("mouseover") then
    -- the tooltip shows the unit under the cursor
end

Note: If a tooltip continues to show a unit from SetUnit, and the cursor then moves onto the same unit in the world, GetUnit() continues to return the SetUnit token, not "mouseover".

GameTooltip:GetGameObject() / GameTooltip:HasGameObject()

GetGameObject() returns (name, id, guid) for whichever gameobject the tooltip is currently displaying, or nothing if it isn't showing one. name is the cached display name (or "" until the gameobject cache has loaded the record). id is the gameobject's template / entry — same key gameobjectcache.wdb uses, and what the server sent in the spawn packet. guid is the canonical "0xHHHHHHHHLLLLLLLL" format.

HasGameObject() is the boolean companion — returns true if the tooltip is currently displaying a gameobject (single uint32 read, no resolution work).

-- Mouse over a chest, vein, signpost, etc. then:
local name, id, guid = GameTooltip:GetGameObject()
-- name = "Iron Deposit"
-- id   = 1731
-- guid = "0xF110000006C30000..."

if GameTooltip:HasGameObject() then
    -- cheap predicate
end

There is no Lua-callable SetGameObject method — gameobjects only populate the tooltip via in-world mouseover. The engine's hover handler at FUN_00492890 dispatches to a tooltip populator (0x0052AA20) that writes the GUID into tooltip+0x370/+0x374, right between the unit slot (+0x368) and the item slot (+0x380). The shared tooltip-clear zeroes the same slot on every subsequent SetX, so these methods follow the same gating pattern as HasUnit / HasSpell / HasItem.

Returns nothing for: tooltip not showing a gameobject, or the gameobject has left the engine's visibility window (object manager evicted it). HasGameObject() stays true in the latter case — the tooltip-frame slot still has the GUID, even though the live object has been freed.

GameTooltip:GetOwner()

Returns the frame that called tooltip:SetOwner(frame, anchor), or nil if the tooltip hasn't been owned by anyone since its last Clear / Hide. The engine ships SetOwner and IsOwned but never the matching reader; this adds it.

GameTooltip:SetOwner(SomeFrame, "ANCHOR_TOPLEFT")
local owner = GameTooltip:GetOwner()
-- owner == SomeFrame

Returns only the owner frame, not (owner, anchorPoint). The anchor string is reachable via the stock GameTooltip:GetAnchorType() (slot 5 in the GameTooltip method registry).

GameTooltip:SetAnchorType(anchorType [, xOffset [, yOffset]])

Changes how the tooltip is anchored to its owner, and moves the tooltip there. It takes the same anchor names as SetOwner. The offsets are in pixels, and both default to 0.

GameTooltip:SetAnchorType("ANCHOR_RIGHT", 10, 0)

A tooltip that has no owner keeps its anchor type. An anchor name that is not known means ANCHOR_LEFT.

ANCHOR_TOP and ANCHOR_BOTTOM

SetOwner and SetAnchorType accept these two anchor names, and GetAnchorType returns them.

  • ANCHOR_TOP puts the bottom edge of the tooltip on the top edge of the owner.
  • ANCHOR_BOTTOM puts the top edge of the tooltip on the bottom edge of the owner.

GameTooltip:GetMinimumWidth() / GameTooltip:GetPadding()

GetMinimumWidth() returns the width that SetMinimumWidth set. It returns a second value, which is always nil.

GetPadding() returns the padding of the tooltip in layout units, not in the pixels that SetPadding takes.

GameTooltip:SetTalentByID(talentID)

Renders a tooltip for the talent identified by Talent.dbc primary key — the natural pair to GetTalentIDByIndex. Works for any class's talents, not just the player's.

Two-tier resolution:

Tier When it applies Tooltip rendered
Player class (rich) talentID belongs to one of the player's loaded tabs Full talent tooltip — name, "Rank N/M", description, prereqs, "click to learn" prompts
Cross-class (fallback) talentID is from another class Spell tooltip for the talent's rank-1 spellID — name, cast time, range, mana cost, description

The fallback exists because the engine only loads the local player's class talent data into its per-player TabInfo arrays. For other classes, we look up the talent in Talent.dbc directly and dispatch the rank-1 spell tooltip — functionally "what does this talent do?" without the rank counter. Talent name and "Rank 0/N" decorations on top of the spell description for cross-class are not replicated here yet.

Silent no-op (no tooltip change) when:

  • talentID doesn't match any record in Talent.dbc
  • talentID is nil, non-numeric, or non-positive
-- Player's own class — rich tooltip
local talentID = GetTalentIDByIndex(1, 9)        -- player's tab 1, talent 9
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetTalentByID(talentID)
GameTooltip:Show()

-- Other class — spell tooltip for the talent's primary ability
GameTooltip:SetTalentByID(2065)                   -- works regardless of player class
GameTooltip:Show()

GameTooltip:SetInventoryItemByID(itemID)

Renders the tooltip for the equipped instance of itemID — walks character-pane slots 1..19, finds the matching item, and shows it with its actual enchants, random-suffix stats, and broken/locked state. Distinct from SetItemByID, which shows the clean ItemSparse data with no instance-specific decorations.

For example, on a pair of boots with a run-speed enchant equipped:

Method Renders
SetItemByID(<bootsID>) Base boots tooltip — name, armor, durability, level req. No enchant.
SetInventoryItemByID(<bootsID>) Same plus Enchanted: Minor Speed and any random-suffix lines.

Silent no-op if the item isn't currently equipped — fall back to SetItemByID for unworn items, or check via C_Item.IsEquippedItem first.

When the player has duplicates of the same itemID equipped (matched MH/OH weapons, identical rings, identical trinkets), the lower-numbered slot wins — MAINHAND before OFFHAND, FINGER1 before FINGER2, TRINKET1 before TRINKET2 (verified empirically).

local _, _, _, _, _, _, _, _, _, _, _, _, _, link = GetItemInfo(itemID)
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
if C_Item.IsEquippedItem(itemID) then
    GameTooltip:SetInventoryItemByID(itemID)  -- shows enchants/suffix
else
    GameTooltip:SetItemByID(itemID)            -- shows base stats
end
GameTooltip:Show()

Shows the tooltip for a link. The link can be a full chat link or only its payload (the text between |H and |h). The method also shows spell: and quest: links.

GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetHyperlink("|cff9d9d9d|Hitem:7073:0:0:0|h[Broken Fang]|h|r")
GameTooltip:SetHyperlink(C_Spell.GetSpellLink(133))   -- "spell:133:0" inside
GameTooltip:SetHyperlink("quest:783")
Link Shows
|H<payload>|h[text]|h The tooltip for the payload, as in the rows below
item: The item tooltip
enchant:<spellID> The spell tooltip, for an Enchanting formula or a trade-skill recipe (such as a GetTradeSkillRecipeLink link)
spell:<spellID> The spell tooltip of SetSpellByID, with the rank and without the cooldown
quest:<questID>[:level] The quest title, then the objectives text. Tokens such as $N are expanded, as in the quest log.

Other link types (for example achievement: and talent:) raise the error "Unknown link type".

If the spell ID or quest ID is not valid, the tooltip is empty.

A quest link reads the quest cache. If the quest is not in the cache, the tooltip is empty (a scanning tooltip gets NumLines() == 0). The client then asks the server for the quest, with the same query the quest log sends. Call SetHyperlink again after the data arrives. To know when the data arrives, call C_QuestLog.RequestLoadQuestByID and wait for QUEST_DATA_LOAD_RESULT.

Fills the tooltip with an item that the player has equipped in the slot of the given item. The tooltip shows a grey Currently Equipped header, then the tooltip of the equipped item. If you give comparisonTooltip, a green and red list of stat changes comes after it. This is the method behind item-comparison ("shopping") tooltips. FrameXML's GameTooltip_ShowCompareItem uses it.

Call it on the tooltip that shows the comparison. This is usually ShoppingTooltip1 or ShoppingTooltip2, but any GameTooltipTemplate frame works.

Arg Meaning
itemLink Required. The item to compare (chat link, item: string, or itemID). Its slot selects the equipped item.
offset Optional, 1-based, default 1. Selects which equipped item to show when the slot holds more than one. See the list below.
shiftButton Accepted, with no effect.
comparisonTooltip Optional. A tooltip that shows the compared item, usually the tooltip that the player hovers. If it shows an item, the stat changes of that item against the equipped item come after the equipped item. If you do not give it, no stat changes show.

How offset selects the equipped item:

  • Rings and trinkets: offset counts only the filled slots. If only the second ring slot has a ring, offset 1 shows that ring.
  • One-hand weapons and two-hand weapons: if both hands hold an item, offset 1 shows the off hand and offset 2 shows the main hand. If only one hand holds an item, offset 1 shows it.
  • Main-hand-only weapons: offset 1 shows the main hand.
  • Shields, off-hand weapons, and held items: offset 1 shows the off hand. If the off hand is empty and the main hand holds a two-hand weapon, offset 1 shows that weapon.
  • All other items: offset 1 shows the item in the one slot.

Returns 1 when the method filled the tooltip. It returns nil when the link is not valid, the item is not cached, the item does not go in an equipment slot, or no equipped item matches offset. When it returns nil, the tooltip does not change.

The stat changes cover the same keys as C_Item.GetItemStats (base stats, resistances, weapon DPS, and on-equip-spell bonuses like crit, attack power, and spell power). The compared item includes its random suffix. The colors are the INCREASE_STAT_COLOR and DECREASE_STAT_COLOR of the client. The method only fills the tooltip. The caller anchors and shows it.

-- Compare the hovered item against what's equipped:
local _, link = GameTooltip:GetItem()
ShoppingTooltip1:SetOwner(GameTooltip, "ANCHOR_NONE")
if ShoppingTooltip1:SetHyperlinkCompareItem(link, 1, nil, GameTooltip) then
    ShoppingTooltip1:Show()
end
ShoppingTooltip2:SetOwner(GameTooltip, "ANCHOR_NONE")
if ShoppingTooltip2:SetHyperlinkCompareItem(link, 2, nil, GameTooltip) then
    ShoppingTooltip2:Show()  -- second ring, trinket, or hand
end

GameTooltip:IsEquippedItem()

Returns true when the item the tooltip is currently displaying is equipped in one of the player's character-pane slots (1..19).

Works regardless of how the tooltip was populated — link paths (SetItemByID, SetHyperlink) and CGItem paths (SetInventoryItem, SetBagItem, SetItemByGUID, …) both resolve to the displayed item. Returns false for a tooltip that isn't showing an item (e.g. a spell tooltip) with no false positives.

GameTooltip:SetInventoryItem("player", INVSLOT_HAND)
if GameTooltip:IsEquippedItem() then ... end  -- true

GameTooltip:SetBagItem(0, 1)
if GameTooltip:IsEquippedItem() then ... end  -- false unless that item is also worn

GameTooltip:SetFrameStack([showHidden, showRegions, highlightIndexChanged])

Fills the tooltip with the frames under the cursor and returns the highlighted entry. This is the method that /fstack uses. The method also shows the tooltip, so set an owner first.

GameTooltip:SetOwner(UIParent, "ANCHOR_NONE")
GameTooltip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -20, 20)
local highlight = GameTooltip:SetFrameStack()  -- shown frames only
GameTooltip:SetFrameStack(true)                -- also hidden frames
GameTooltip:SetFrameStack(false, true)         -- also textures and font strings
GameTooltip:SetFrameStack(false, false, 1)     -- move the highlight down one entry

The first line shows the text DEBUG_FRAMESTACK and the cursor position, in the same units as GetCursorPosition(). Below it, the frames are in groups by strata, from the top strata down. Each group starts with the strata name. In a group, frames go from the highest frame level down. Each frame line shows <level> name:

  • A frame with a name shows that name.
  • A frame with no name shows its Lua table, for example table: 1A2B3C4D.
  • When the global variable with the frame's name is not that frame, the line shows the table and the name, for example table: 1A2B3C4D (MyFrame).

With showRegions, the textures and font strings of a listed frame come after that frame, when the cursor is in their rectangle. A region line is indented three more spaces and shows the level of its frame.

The color of a line tells you the state of the entry:

Color Entry
Orange A shown frame, and the mouse is enabled
Blue A shown frame, and the mouse is not enabled
Cyan A shown region (only with showRegions)
Grey Hidden (only with showHidden)

The frame colors have two shades. The shade changes when the frame level changes.

A frame is in the list when the cursor is in its rectangle. Hit-rect insets and clipping by a parent frame have no effect on the list. The list also includes frames that do not get mouse input.

The highlight. One entry is the highlighted entry. Its line starts with --> in place of the indent, and the method returns its frame or region. After the list, the tooltip shows:

  • ANCHORS, then one line per anchor point: <index> point relativeTo relativePoint x y, the values that GetPoint(index) returns. If the anchor is relative to the screen, the line is <index> point x y.
  • SOURCE, then the location that GetSourceLocation() returns. These two lines are absent when the location is not known.

The highlight starts at the first entry. highlightIndexChanged moves it that many entries down the list, or up for a negative number, and wraps at both ends. The index stays between calls, so the highlight does not move while the cursor rests. Pass 0, or nothing, to keep it.

The method runs the OnTooltipSetFrameStack script before it shows the tooltip. The script gets the highlighted entry, or nil, as arg1.

OnTooltipSet* scripts

OnTooltipSetItem / OnTooltipSetSpell / OnTooltipSetUnit / OnTooltipSetGameObject — real frame scripts, settable with the standard SetScript / GetScript / HookScript — that fire whenever a tooltip's item, spell, unit, or gameobject is set. These tooltip scripts let addons annotate tooltips by hooking one script per object type instead of wrapping every Set* method. Available on all GameTooltip-type frames (GameTooltip, ItemRefTooltip, ShoppingTooltip1/2, AtlasLootTooltip, …).

Script Fires after any of
OnTooltipSetItem SetBagItem, SetInventoryItem, SetHyperlink (item:), SetMerchantItem, SetAuctionItem, SetItemByID, …
OnTooltipSetSpell SetSpell, SetSpellByID, SetTalent, SetShapeshift
OnTooltipSetUnit SetUnit and unit mouseover
OnTooltipSetGameObject gameobject mouseover (herb/ore nodes, chests, mailboxes, signs)
OnTooltipSetFrameStack SetFrameStack

The engine ships only OnTooltipAddMoney / OnTooltipCleared / OnTooltipSetDefaultAnchor; these five are added.

The handler receives the tooltip as the global this, the frame-script convention — not a self argument (like every built-in script: OnShow, OnEvent, OnTooltipCleared, …). A function(self) self:… end handler will see self == nil; use this.

GameTooltip:HookScript("OnTooltipSetItem", function()
    local name, link, id = this:GetItem()   -- the tooltip is the global `this`
    if id then this:AddLine("ID: " .. id, 0.6, 0.6, 0.6) end
end)

GameTooltip:HookScript("OnTooltipSetUnit", function()
    local name, unit = this:GetUnit()
    if unit then this:AddLine(UnitClassBase(unit), 0.6, 0.6, 0.6) end
end)

Auras don't fire OnTooltipSetSpell — SetUnitBuff / SetUnitDebuff / SetPlayerBuff / SetUnitAura use a separate aura-tooltip builder, so hovering a buff/debuff does not trigger the spell script. Unit auras are a distinct tooltip data type rather than a spell.

Caveat — the event fires from inside the tooltip build. Lightweight handler work is fine: reading this:GetItem() / this:GetUnit(), this:AddLine(...), printing, etc. But avoid re-entrant tooltip rebuilds from the handler — e.g. GameTooltip_ShowCompareItem(), or a Set* call on another tooltip. Because the handler runs mid-build, re-entering the tooltip / FrameScript machinery can collide with other DLLs that hook the same Lua paths (nampower, SuperWoW, weirdutils, …) and crash the client. If you need heavy work like that, defer it to the next frame:

GameTooltip:HookScript("OnTooltipSetItem", function()
    C_Timer.After(0, function() GameTooltip_ShowCompareItem() end)
end)

GameTooltip:SetEquipmentSet(name)

Fills the tooltip with a summary of the named equipment set: header, total item count, and per-bucket counts (equipped / in inventory / ignored / missing). Each missing item is listed on its own line by name.

GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetEquipmentSet("MyTank")
GameTooltip:Show()
MyTank                     (header, white)
14 items
12 equipped                (green)
1 in inventory
1 slots ignored            (gray)
Missing: Crown of the Endless Conqueror   (red)

Silent no-op if no set with that name exists. The owner anchor must be set before the call — SetEquipmentSet only fills the lines, it doesn't re-anchor the tooltip frame.

The counts are the same as in C_EquipmentSet.GetEquipmentSetInfo. An item in the bank counts as "in inventory" only while the bank window is open.

Localization. The count lines are formatted through Game::Lua::PushLocalizedFormatInt — Blizzard's ITEMS_VARIABLE_QUANTITY, ITEMS_EQUIPPED, ITEMS_IN_INVENTORY, ITEM_SLOTS_IGNORED, and ITEM_MISSING FrameXML globals are tried first, with English C-string fallbacks for servers stripped of standard GlobalStrings. The companion !!!ClassicAPI addon ships English defaults for these keys so the tooltip works on bare-bones builds where FrameXML hasn't populated them.

Missing-item names. The on-disk equipment-set format (WTF\Account\<acct>\<realm>\<char>\ClassicAPI_EquipmentSets.txt) persists each slot as both a 64-bit GUID and the itemID at save time:

set 1
  name=MyTank
  icon=INV_Helmet_03
  slot 1 guid=0xC00000000A1B2C3D item=51220
  slot 5 ignored

The GUID is the live handle for items currently in inventory; the itemID is the type identifier used when the live CGItem isn't findable (item sold, mailed, on another character, etc.) — without it we couldn't recover the name for missing items. Sets saved before the itemID field was added load fine but render their missing slots with the count summary %d missing instead of named lines; re-saving a set repopulates itemIDs.

Equipment sets on item tooltips

An item tooltip shows the line Equipment Sets: <names> when the item belongs to one or more sets. The line uses EQUIPMENT_SETS and PLAYER_LIST_DELIMITER, and is added before OnTooltipSetItem runs.

Set the cvar dontShowEquipmentSetsOnItems to 1 to hide the line:

SetCVar("dontShowEquipmentSetsOnItems", "1")

GameTooltip:SetTotem(slot)

Fills the tooltip with the active shaman totem in slot (1 Fire, 2 Earth, 3 Water, 4 Air).

GameTooltip:SetOwner(TotemButton, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetTotem(1)   -- Fire slot; shows itself, no :Show() needed
Searing Totem              (name, yellow)
45 Sec                     (time remaining, white)

The tooltip has two lines. The first line is the name of the totem in NORMAL_FONT_COLOR. The second line is the time remaining, in white. The method shows the tooltip itself, so a :Show() after it is not necessary. Set the owner and the anchor before the call.

If the slot has no active totem, or the slot is not 1 to 4, the method does nothing. Thus a totem button can call it from OnEnter without a test.

Time line. Under one minute, the line shows whole seconds, rounded down. From one minute, it shows minutes, rounded up. When the time is over, the line shows 0 Sec until the totem goes away. The text comes from the GlobalStrings SPELL_TIME_REMAINING_SEC and SPELL_TIME_REMAINING_MIN. If a client does not have these keys, the English %d Sec and %d Min are used. The data comes from the same source as GetTotemInfo.

Globals

CLASSIC_API_VERSION

Defined once FrameScript has booted. Addons can use this to detect that the DLL is loaded and which version is in use. The value is X*10000 + Y*100 + Z for a tag of vX.Y.Z passed to CMake at configure time via -DCLASSICAPI_TAG=vX.Y.Z.

Untagged builds (local dev, CI without a release tag) get a sentinel value of 99999999 — encoded as v9999.99.99, chosen to be higher than every plausible real release so addon-side feature gates like CLASSIC_API_VERSION >= 10200 don't reject the dev build.

if CLASSIC_API_VERSION and CLASSIC_API_VERSION >= 10200 then
    -- ClassicAPI v1.2.0 or newer is loaded (or untagged dev build)
end

If you specifically want to detect a dev build:

if CLASSIC_API_VERSION == 99999999 then
    -- running against a locally-built, untagged DLL
end

INTERFACE_VERSION

The client's interface (TOC) version number — 11200 on this client. This is the number the addon loader checks each addon's ## Interface: line against, so it's the right value to compare a required version to. Read from the engine and re-published on every /reload.

if INTERFACE_VERSION >= 11200 then ... end

LE_EXPANSION_*

The expansion-level enum, exposed as Lua globals so addons don't have to gate on if LE_EXPANSION_CLASSIC then (the constant being defined is itself the probe). Values match the Enum.ExpansionLevel table. The matching helper functions (GetClassicExpansionLevel / ClassicExpansionAtLeast / ClassicExpansionAtMost) live in the Expansion section.

Constant Value
LE_EXPANSION_LEVEL_CURRENT 0 (this is Classic)
LE_EXPANSION_CLASSIC 0
LE_EXPANSION_BURNING_CRUSADE 1
LE_EXPANSION_WRATH_OF_THE_LICH_KING 2
LE_EXPANSION_CATACLYSM 3
LE_EXPANSION_MISTS_OF_PANDARIA 4
LE_EXPANSION_WARLORDS_OF_DRAENOR 5
LE_EXPANSION_LEGION 6
LE_EXPANSION_BATTLE_FOR_AZEROTH 7
LE_EXPANSION_SHADOWLANDS 8
LE_EXPANSION_DRAGONFLIGHT 9
LE_EXPANSION_WAR_WITHIN 10
LE_EXPANSION_MIDNIGHT 11
-- Classic version check using modern idiom
if LE_EXPANSION_LEVEL_CURRENT < LE_EXPANSION_BURNING_CRUSADE then
    -- pure-1.x (vanilla / Classic Era) code path
end

-- Probe for ClassicAPI presence
if LE_EXPANSION_LEVEL_CURRENT then
    -- the constants are defined → ClassicAPI is loaded
end

LE_ITEM_QUALITY_*

The item-quality enum (Enum.ItemQuality), exposed as Lua globals so addons can do if quality >= LE_ITEM_QUALITY_RARE then ... against the integer quality returned by GetItemInfo / C_Item.GetItemInfoInstant.

Constant Value Color in tooltip
LE_ITEM_QUALITY_POOR 0 gray (junk)
LE_ITEM_QUALITY_COMMON 1 white
LE_ITEM_QUALITY_UNCOMMON 2 green
LE_ITEM_QUALITY_RARE 3 blue
LE_ITEM_QUALITY_EPIC 4 purple
LE_ITEM_QUALITY_LEGENDARY 5 orange
LE_ITEM_QUALITY_ARTIFACT 6 gold
LE_ITEM_QUALITY_HEIRLOOM 7 light blue
LE_ITEM_QUALITY_WOWTOKEN 8 orange

Values 0..5 (POOR..LEGENDARY) correspond to actual qualities present here. Higher values are exposed for source compatibility — items here will never carry those quality values, so a comparison like quality == LE_ITEM_QUALITY_HEIRLOOM trivially never matches and the rest of the code path is unreachable.

local _, _, quality = GetItemInfo(itemID)
if quality and quality >= LE_ITEM_QUALITY_RARE then
    -- highlight in UI
end

LE_UNIT_STAT_*

The primary-stat enum (Enum.UnitStat), exposed as Lua globals so addons can index UnitStat(unit, statIndex) symbolically:

Constant Value Stat
LE_UNIT_STAT_STRENGTH 1 Strength
LE_UNIT_STAT_AGILITY 2 Agility
LE_UNIT_STAT_STAMINA 3 Stamina
LE_UNIT_STAT_INTELLECT 4 Intellect
LE_UNIT_STAT_SPIRIT 5 Spirit

Values are stable across every WoW expansion — UnitStat("player", 1) has always returned strength.

local _, effective = UnitStat("player", LE_UNIT_STAT_AGILITY)

Enum.AddOnSecurityStatus

The integer enum C_AddOns.GetAddOnSecurity returns. Matches Blizzard's Enum.AddOnSecurityStatus:

Value Field Notes
0 Secure Blizzard-signed addons.
1 Insecure User addons; default for any registered addon.
2 Banned Server-disqualified entries.
3 NotAvailable No addon by that name / index.
if C_AddOns.GetAddOnSecurity(name) == Enum.AddOnSecurityStatus.Secure then
    -- it's a Blizzard_* addon
end

Enum.BagIndex

The container indices that the C_Container functions and the stock container functions take:

Value Field
-2 Keyring
-1 Bank
0 Backpack
1 to 4 Bag_1 to Bag_4
5 to 10 BankBag_1 to BankBag_6

Keyring and Bank have the same values as KEYRING_CONTAINER and BANK_CONTAINER.

for bag = Enum.BagIndex.Backpack, NUM_TOTAL_BAG_FRAMES do
    -- the backpack and the equipped bags
end

Enum.InventoryType

The equip-type enum — the numeric inventoryType reported by C_Item.GetItemInfoInstant / C_Item.GetItemInventoryType, and the argument the C_Item.GetItemInventorySlot* functions take.

Value Field Value Field
0 IndexNonEquipType 15 IndexRangedType
1 IndexHeadType 16 IndexCloakType
2 IndexNeckType 17 Index2HweaponType
3 IndexShoulderType 18 IndexBagType
4 IndexBodyType 19 IndexTabardType
5 IndexChestType 20 IndexRobeType
6 IndexWaistType 21 IndexWeaponmainhandType
7 IndexLegsType 22 IndexWeaponoffhandType
8 IndexFeetType 23 IndexHoldableType
9 IndexWristType 24 IndexAmmoType
10 IndexHandType 25 IndexThrownType
11 IndexFingerType 26 IndexRangedrightType
12 IndexTrinketType 27 IndexQuiverType
13 IndexWeaponType 28 IndexRelicType
14 IndexShieldType

Values 29..34 (IndexProfessionToolType, IndexProfessionGearType, IndexEquipablespell{Offensive,Utility,Defensive,Weapon}Type) are included for parity — items here never report them.

if C_Item.GetItemInventoryType(loc) == Enum.InventoryType.IndexHeadType then ...

Enum.ItemClass

The item-class enum — the numeric classID reported as the 12th return of GetItemInfo and taken by GetItemClassInfo / GetItemSubClassInfo. The obsolete slots keep their key names even though ItemClass.dbc labels them "…(OBSOLETE)".

Value Field Value Field
0 Consumable 8 ItemEnhancement
1 Container 9 Recipe
2 Weapon 10 CurrencyTokenObsolete
3 Gem 11 Quiver
4 Armor 12 Questitem
5 Reagent 13 Key
6 Projectile 14 PermanentObsolete
7 Tradegoods 15 Miscellaneous

Values 16..19 (Glyph, Battlepet, WoWToken, Profession) are included for parity — items here never report them.

if select(12, GetItemInfo(id)) == Enum.ItemClass.Weapon then ...

Enum.ItemQuality

The item-quality enum — the numeric quality reported by GetItemInfo / C_Item.GetItemQuality.

Value Field Value Field
0 Poor 4 Epic
1 Common 5 Legendary
2 Uncommon 6 Artifact
3 Rare

The Heirloom (7) and WoWToken (8) tiers are omitted — no such items exist here.

if select(3, GetItemInfo(id)) == Enum.ItemQuality.Epic then ...

Enum.PlayerSwingType

The weapon that a PLAYER_SWING or C_SwingTimer value applies to:

Value Field Meaning
0 MainHand The main-hand melee weapon.
1 OffHand The off-hand melee weapon, when one is equipped.
2 Ranged The ranged weapon: a bow, gun, crossbow, or wand.

Enum.PowerType

The integer enum UnitPowerType returns and UnitPower / UnitPowerMax accept. Only slots 0..4 are defined. Slot 4 is Happiness (pet happiness).

Value Field Notes
-2 HealthCost Sentinel for "use HEALTH instead of POWER".
-1 None Sentinel for "unit's primary power" (= omit the arg).
0 Mana
1 Rage
2 Focus
3 Energy
4 Happiness Pet happiness.
local rage = UnitPower("player", Enum.PowerType.Rage)

Enum.SpellBookSpellBank

Selects which spellbook a C_SpellBook.* slot query reads. Passed as the spellBank argument to C_SpellBook.GetSpellBookItemInfo.

Value Field
0 Player
1 Pet

Enum.SpellBookItemType

Categorizes a spellbook slot. The spellbook only ever holds real spells, so C_SpellBook.GetSpellBookItemInfo returns Spell for a player-book slot and PetAction for a pet-book slot. The other values exist for signature parity but never occur.

Value Field Notes
0 None Empty slot. Never returned — an empty slot yields nil instead.
1 Spell A player-book spell.
2 FutureSpell A not-yet-learned trainer spell. Never occurs.
3 PetAction A pet-book spell.
4 Flyout A flyout group. Never occurs.

Enum.UICursorType

What the cursor is holding. Reported as the newCursorType and oldCursorType arguments of CURSOR_CHANGED.

Value Field Notes
0 Default Cursor empty.
1 Item
2 Money
3 Spell
4 PetAction
5 Merchant
6 ActionBar Never occurs.
7 Macro
8 Ammo Never occurs.
9 Pet A pet dragged in the stable.
10-20 GuildBank … PerksProgramVendorItem Never occur.

The values marked "never occur" name content this client does not have. They are still defined, so comparing against any field is valid.

PetAction and Pet are reported with the same meaning as the "petaction" and "pet" strings of GetCursorInfo.

Glue

Backports of two C_Glue session helpers. C_Glue is registered on the glue (login / character-select) Lua state; IsOnGlueScreen is also registered in-game, so code that runs in either environment can branch on it. Neither needs an engine flag — both answers fall out of which glue boot / Lua state is asking.

C_Glue.IsFirstLoadThisSession()

Returns true only while the first glue screen since the process launched is showing, false on every later return to the glue screen. Login ↔ character-select transitions stay within the first glue session (they never enter the world), so it stays true across them — it flips false only once you've entered the world and logged back out. Glue state only.

if C_Glue.IsFirstLoadThisSession() then
    -- one-time-per-launch startup (intro cinematic, news, …)
end

Backed by a process-static count of glue boots (the glue script-registration pass fires once per boot: initial launch + each world→glue logout); boot 1 is the first load.

C_Glue.IsOnGlueScreen()

Returns true when a GlueXML screen is showing (login or character select — no character in the world), false in the world. Registered on both Lua states, so it's callable from glue and in-game code alike. The two states are mutually exclusive (the glue state exists only when no character is in the world), so the answer is fixed per state — true from the glue registration, false from the in-game one.

if C_Glue.IsOnGlueScreen() then
    -- at login / character select
end

Gossip

Struct-shaped wrappers around the flat GetGossipText / GetGossipOptions / GetGossipAvailableQuests / GetGossipActiveQuests / SelectGossipOption / SelectGossipAvailableQuest / SelectGossipActiveQuest / CloseGossip surface. The data is the same — these calls just read the engine's two gossip-state arrays (0x00BBBE90 for options, 0x00BB74C0 for quests, both filled by the SMSG_GOSSIP_MESSAGE handler at 0x004E26E0) and shape it into the struct-tables that addons expect.

Fields the server simply doesn't send — and therefore aren't in any of the returned tables — include rewards and spellID, per-option status (Available / Unavailable / Locked / AlreadyComplete — the server doesn't compute this), and UX hints like overrideIconID and selectOptionWhenOnlyOption.

The icon field is the raw icon-type byte from SMSG_GOSSIP_MESSAGE (server-extensible — pservers can add custom NPC types past the Blizzard 0..10 range). Default Blizzard types resolve to Interface\GossipFrame\<Type>GossipIcon:

Value Type Value Type
0 Gossip 6 Banker
1 Vendor 7 Petition
2 Taxi 8 Tabard
3 Trainer 9 BattleMaster
4 Healer 10 Auctioneer
5 Binder (innkeeper)

C_GossipInfo.GetText()

Returns the gossip greeting string the engine resolved from the NPC's referenced NPC_TEXT.dbc row. Same value GetGossipText() returns; provided so addons that prefer the C_GossipInfo namespace don't have to mix the two surfaces.

print(C_GossipInfo.GetText())

C_GossipInfo.GetOptions()

Returns an array of gossip-option tables in display order. Each entry contains:

Field Type Notes
gossipOptionID number The engine's optionIndex. The same value C_GossipInfo.SelectOption expects; arbitrary integer assigned by the server.
name string Option text (locale-applied by the server).
icon number Raw icon-type byte from SMSG_GOSSIP_MESSAGE — passed through unmapped so pserver-added types (anything past the default 0..10 range) survive. See the type table above for the default Blizzard categories and the Interface\GossipFrame\<Type>GossipIcon path each one resolves to.
flags number Bit 0 set = boxCoded (the option asks for confirmation text — Are you sure? boxes).
orderIndex number 1-based position in the emitted list. Matches the index SelectGossipOption (legacy) expects.
for _, opt in ipairs(C_GossipInfo.GetOptions()) do
    print(opt.gossipOptionID, opt.name, "icon", opt.icon)
end

C_GossipInfo.GetAvailableQuests()

Returns an array of deliverable-quest tables (quests the giver is offering to start), in display order. Filter mirrors the engine's GetGossipAvailableQuests — status field at +0x008 not in {3, 4}.

Field Type Notes
questID number Quest.dbc row ID.
title string Localized quest title.
questLevel number Quest level.

C_GossipInfo.GetActiveQuests()

Returns an array of active-quest tables (quests the giver wants you to turn in, in-progress or complete). Status field at +0x008 in {3, 4}.

Field Type Notes
questID number Quest.dbc row ID.
title string Localized quest title.
questLevel number Quest level.
isComplete boolean true when the engine's status byte is 4 (ready to turn in); false when it's 3 (still in progress).

C_GossipInfo.GetNumOptions() / GetNumAvailableQuests() / GetNumActiveQuests()

Convenience counters — return the length of each of the three lists above without building the table.

C_GossipInfo.SelectOption(gossipOptionID) / SelectOptionByIndex(orderIndex)

Picks a gossip option. SelectOption resolves the gossipOptionID to the engine's 1-based slot, then tail-calls the engine's native SelectGossipOption. SelectOptionByIndex is a thin passthrough for callers that already have the slot index (e.g. from opt.orderIndex on a GetOptions() entry, or from a UI button bound directly to the slot).

Both end in the same CMSG; the option-ID variant exists so addons can drive selections off the gossipOptionID key without keeping their own slot mapping. Returns nothing.

The stock SelectGossipOption doesn't accept a confirmation-text argument, so for boxCoded options the engine's own confirm dialog runs as usual — there is no way to send the password from the script.

C_GossipInfo.SelectAvailableQuest(questID)

Picks a deliverable quest by questID. Walks the active-vs-available filter the same way GetAvailableQuests does, locates the matching 1-based slot, and tail-calls the engine's SelectGossipAvailableQuest. Returns nothing.

C_GossipInfo.SelectActiveQuest(questID)

Picks an in-progress quest by questID. Same shape as SelectAvailableQuest but walks the active filter and tail-calls SelectGossipActiveQuest. Returns nothing.

C_GossipInfo.CloseGossip()

Closes the gossip window. Direct passthrough to the engine's CloseGossip. Returns nothing.

C_GossipInfo.GetPoiForUiMapID(uiMapID)

Returns the ID of the gossip point of interest on the map uiMapID, or nil. Some gossip options add this marker to the map, for example the directions that a city guard gives. There is only one marker at a time, and its ID is always 1.

The function returns nil when there is no marker, or when the marker is not inside the map. The marker goes away when the player comes within 10 yards of it, or 8 minutes after it was added.

C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)

Returns a table that describes the gossip point of interest on the map uiMapID, or nil. It returns nil in the same conditions as GetPoiForUiMapID, and when gossipPoiID is not the ID that GetPoiForUiMapID returns.

Field Type Description
name string The name of the location, for example "Stormwind Bank".
textureIndex number The icon index. It is the same index that GetMapLandmarkInfo returns for this marker.
position vector2 The position on the map, from 0 to 1 on each axis.
inBattleMap boolean Always false.
local mapID = C_Map.GetBestMapForUnit("player")
local poiID = C_GossipInfo.GetPoiForUiMapID(mapID)
if poiID then
    local info = C_GossipInfo.GetPoiInfo(mapID, poiID)
    print(info.name, info.position:GetXY())
end

GetNumGossipAvailableQuests() / GetNumGossipActiveQuests()

Returns the number of quests that GetGossipAvailableQuests() / GetGossipActiveQuests() return. Each quest is one (title, level) pair, so the count always agrees with the list. These functions count quests in gossip menus only. For a questgiver that has no gossip menu, use GetNumAvailableQuests() / GetNumActiveQuests().

local quests = { GetGossipAvailableQuests() }
for i = 1, GetNumGossipAvailableQuests() do
    local title, level = quests[i * 2 - 1], quests[i * 2]
end

GetNumGossipOptions()

Returns the number of options that GetGossipOptions() returns. Each option is one (text, type) pair, so the count always agrees with the list.

local options = { GetGossipOptions() }
for i = 1, GetNumGossipOptions() do
    local text, gossipType = options[i * 2 - 1], options[i * 2]
end

Hooks

hooksecurefunc(name, callback) / hooksecurefunc(table, name, callback)

Post-call hook: the original function runs first, then callback runs with the same args (return values discarded). The original's return values propagate to the caller.

hooksecurefunc("GetSpellInfo", function(spellID)
    print("GetSpellInfo called with " .. tostring(spellID))
end)

hooksecurefunc(GameTooltip, "SetInventoryItem", function(self, unit, slot)
    -- runs after the engine fills the tooltip
    print("inventory tooltip:", unit, slot)
end)

The "secure" label refers to taint-propagation behavior for protected-frame manipulation. There is no taint system here, so the function is equivalent to a plain "after-hook" — the name is kept for API parity.

  • All of the original's return values reach the caller, however many there are.
  • An error in the original reaches the caller, the same as without the hook.
  • An error in callback is shown like any other script error, through the error handler (geterrorhandler()). The original's return values still reach the caller.
  • name can be a number. It is looked up as its string form.
  • The hook is written directly into the table. A __newindex metamethod on the table does not run. The original is read normally, so a method the table inherits through __index can be hooked.
  • Works in the world and on the login screen.

Errors:

  • Usage: hooksecurefunc([table,] "function", hookfunc) when name is not a string or number, or callback is not a function.
  • hooksecurefunc(): <name> is not a function when table[name] is not a function (a typo, or an unknown frame method).
  • The two-argument form naming a function that cannot be hooked on _G (error hooksecurefunc: function is unhookable): getfenv, getmetatable, hooksecurefunc, ipairs, issecurevalue, issecurevariable, next, pairs, pcall, pcallwithenv, rawget, rawset, scrub, securecall, securecallfunction, secureexecuterange, select, setfenv, setmetatable, type, unpack, wipe, xpcall. This is the same list the current client enforces. The three-argument form is not restricted, even with _G as the table.

Input

The engine ships IsShiftKeyDown / IsControlKeyDown / IsAltKeyDown but they only report "any shift/ctrl/alt" — there's no built-in way to tell left from right. These seven functions add the missing distinction plus an IsModifierKeyDown() rollup.

securecall(func, ...) / securecallfunction(func, ...) / securecallmethod(object, method, ...) / secureexecuterange(tbl, func, ...)

Call a function so that an error in it does not stop the code that called it. The error goes to the error handler (geterrorhandler()).

  • securecall(func, ...) calls func(...) and returns its results. func can also be the name of a global function. After an error, securecall returns nothing.
  • securecallfunction(func, ...) is the same, but func must be a function.
  • securecallmethod(object, method, ...) calls object[method](object, ...) and returns its results. The lookup of method ignores the metatable of object. If object is not a table, or method is not a function, the error goes to the error handler.
  • secureexecuterange(tbl, func, ...) calls func(key, value, ...) for each entry of tbl. After an error in one call, the other entries still run. It returns nothing.
print(securecall(math.max, 1, 9))   -- 9
secureexecuterange({10, 20}, function(key, value, extra)
    print(key, value, extra)
end, "x")

IsLeftShiftKeyDown() / IsRightShiftKeyDown()

IsLeftControlKeyDown() / IsRightControlKeyDown()

IsLeftAltKeyDown() / IsRightAltKeyDown()

Each returns 1 when the corresponding key is physically down, nil otherwise — matching the convention the stock IsShiftKeyDown etc. use.

if IsLeftShiftKeyDown() and not IsRightShiftKeyDown() then
    -- left-shift-only binding
end

IsModifierKeyDown()

Returns 1 if any of the six modifier keys is down, nil otherwise. Equivalent to IsShiftKeyDown() or IsControlKeyDown() or IsAltKeyDown() but in one call.

IsMouseButtonDown([button])

Returns true if the given mouse button is currently held, false otherwise. With no argument (or nil), returns true if any mouse button is held.

button is either a 1-based ID or a name string:

ID Name
1 "LeftButton"
2 "RightButton"
3 "MiddleButton"
4 "Button4" (XBUTTON1 / first side button)
5 "Button5" (XBUTTON2 / second side button)

Unrecognized IDs / names return false (the named button just isn't held — bad input doesn't fall through to the any-button check).

The state is the same state that the game uses for mouse input. When the game window loses focus, no button stays held.

if IsMouseButtonDown("RightButton") then
    -- right-button-held bindings
end

GetMouseButtonName(button)

Returns the name of a mouse button from its ID. The IDs are the same as the IDs of IsMouseButtonDown: 1 gives "LeftButton", 2 gives "RightButton", and 5 gives "Button5".

The name is the same name that OnClick gets in arg1 for that button. An ID that is not a button gives "UNKNOWN".

If button is a string that is not a number, the function returns this string. If button is nil or another type, the function returns nil.

local name = GetMouseButtonName(3) -- "MiddleButton"

GetMouseButtonClicked()

Returns the name of the mouse button that started the handler that is now running. It works inside these handlers, on any frame: OnMouseDown, OnMouseUp, PreClick, OnClick, PostClick, and OnDoubleClick. At any other time it returns nil.

A helper function that the handler calls can also read the name. arg1 is not in scope there.

The name is one of "LeftButton", "MiddleButton", "RightButton", "Button4", or "Button5". A Button:Click() call with no argument gives "LeftButton". A Button:Click(name) call with any other name gives "UNKNOWN".

OnDragStart is not in the list above. In OnDragStart this function returns nil, and arg1 holds the button.

The name is valid only while the handler runs. A held mouse button does not keep it set. If a handler clicks a second button, the name of that inner click applies until the inner handler ends. Then the outer name comes back.

button:SetScript("OnClick", function()
    if GetMouseButtonClicked() == "RightButton" then
        -- right-click behavior
    end
end)

Instance

GetInstanceInfo()

Returns a 9-value tuple, with degenerate values for the fields this client doesn't actually track:

name, instanceType, difficultyID, difficultyName, maxPlayers,
dynamicDifficulty, isDynamic, instanceID, instanceGroupSize
  • name — localized instance/zone name from Map.dbc.
  • instanceType — "none" (open world), "party" (5-man dungeon), "raid", "pvp" (battleground), or "arena" (unused).
  • difficultyID — always 1. There is no heroic mode.
  • difficultyName — always "Normal".
  • maxPlayers — type-default cap: 5 for dungeons, 40 for raids, 40 for battlegrounds, 0 for open world. See caveat below.
  • dynamicDifficulty — always 0.
  • isDynamic — always false.
  • instanceID — current map ID. The field is named "instanceID" but it's really the Map.dbc row ID.
  • instanceGroupSize — mirrors maxPlayers (no per-group-config variants).
/dump GetInstanceInfo()
-- In Stormwind:    "Kalimdor",   "none", 1, "Normal",  0, 0, false,   1,  0
-- In Deadmines:    "Deadmines",  "party",1, "Normal",  5, 0, false,  36,  5
-- In Molten Core:  "Molten Core","raid", 1, "Normal", 40, 0, false, 409, 40

Caveat on maxPlayers. There is no per-instance cap data client-side. The server enforces caps via SMSG_TRANSFER_ABORTED when entry is denied, but that information never reaches the client otherwise. So we return the type's canonical max. Zul'Gurub and AQ20 return 40 instead of their true 20; non-AV battlegrounds (WSG 10, AB 15) return 40 instead of their true cap; any custom raid on a private server (e.g. Turtle WoW) returns 40 regardless of its real cap. Addons that need exact caps must supply their own per-mapID table.

IsActiveBattlefieldArena()

Returns isArena, isRegistered, two booleans.

  • isArena is true while the player is in an arena match. It becomes true when the arena map loads. The open-world free-for-all arenas (for example the Gurubashi Arena) are not arena matches, and return false.
  • isRegistered (a rated match) is always false. The client gets no data about rated matches.
local isArena = IsActiveBattlefieldArena()
if UnitIsDeadOrGhost("player") and not isArena then
    -- not in an arena: point at the corpse
end

InterfaceOptions

InterfaceOptionsFrame and addon option panels

Later clients list addon option panels in the Interface Options window, InterfaceOptionsFrame. An addon adds its panel with InterfaceOptions_AddCategory(panel) (Ace3's AddToBlizOptions does this for every Ace3 addon) and opens it with InterfaceOptionsFrame_OpenToCategory(panel). Stock 1.12 has neither, so Ace3 addons stop while they initialize.

local panel = CreateFrame("Frame")
panel.name = "MyAddon"                   -- required: the list entry
panel.okay = function(self) end          -- optional: Okay pressed
panel.cancel = function(self) end        -- optional: Cancel pressed
panel.default = function(self) end       -- optional: Defaults pressed
panel.refresh = function(self) end       -- optional: panel shown
InterfaceOptions_AddCategory(panel)

local sub = CreateFrame("Frame")
sub.name, sub.parent = "Display", "MyAddon"   -- listed under MyAddon
InterfaceOptions_AddCategory(sub)

InterfaceOptionsFrame_OpenToCategory(panel)   -- or the category's name

The window lists addon panels under AddOns; a category with children starts collapsed. To open the window, click AddOn Options in the game menu (above Key Bindings). When you close a window that you opened from the game menu, the game menu opens again. Okay and Cancel call okay / cancel on every panel. Escape does the same as Cancel. Defaults calls default on the panel shown. InterfaceOptionsFrame_Show() toggles the window. The names match later clients (InterfaceOptionsFrameAddOns with its buttons, buttonHeight and update fields, the InterfaceOptionsFrameAddOnsList scroll frame, InterfaceOptionsFramePanelContainer.displayedPanel, InterfaceOptionsListButtonTemplate, INTERFACEOPTIONS_ADDONCATEGORIES), because addons and libraries use them directly.

This is the addon side only. The client's own settings stay in their own windows, and the Categories list, which later clients fill with the game's settings, exists but stays empty.

Item

itemLocation argument shapes

Every C_Item.* function on this page that takes itemLocation accepts any of three forms — the ItemLocation mixin shapes plus a GUID-string convenience form:

{ equipmentSlotIndex = N }     -- 1-based, character pane
{ bagID = B, slotIndex = S }   -- both required
"0xHHHHHHHHLLLLLLLL"           -- a GUID string from C_Item.GetItemGUID

Table forms are O(1) — the engine knows the slot. The GUID-string form is O(~80) — the function walks equipment slots 1..19 and bags 0..4 comparing each CGItem's stored GUID against the requested one. Bank and keyring are not walked. Fine for sporadic addon use; avoid for per-frame polling.

C_Item.DoesItemExist(itemLocation) / C_Item.DoesItemExistByID(item)

Existence checks for an inventory location or an item ID.

  • C_Item.DoesItemExist(itemLocation) — true iff the equipment-slot or (bagID, slotIndex) location resolves to a populated inventory slot on the active player. Empty slots, missing bags, or malformed tables return false.
  • C_Item.DoesItemExistByID(item) — true for any positive item ID. The item does not have to be in the cache, and the call does not load it. The function does not check that the ID belongs to a real item. C_Item.IsItemDataCachedByID tells you if the data is cached. C_Item.RequestLoadItemDataByID loads it. Accepts a numeric itemID, a bare "item:NNN..." string, or a full chat link. Other input returns false.
if C_Item.DoesItemExist({equipmentSlotIndex = INVSLOT_HEAD}) then ... end
if C_Item.DoesItemExistByID(6948) then ... end

C_Item.EquipItemByName(item [, dstSlot])

Equips item. With dstSlot (a 1-based character-pane slot, 1..19), equips to that specific slot; without, the engine auto-picks based on the item's inventory type.

item takes either of two forms:

  • An item reference: an itemID number, bare "item:N" string, a full chat link, or a localized item name — the shapes C_Item.IsEquippedItem takes. The first matching item in your bags is equipped. Name matching is case-insensitive against each candidate's decorated name (random suffix included — a suffixed item matches its full name, not the base), the same shared predicate C_Item.IsEquippedItem uses.
  • An item location: {bagID = B, slotIndex = S}, {equipmentSlotIndex = N}, or an item GUID string — the shapes C_Item.GetItemID takes. A location names one exact item, so use it when you hold two of the same item with different enchants or suffixes and which one you equip matters.

An {equipmentSlotIndex = N} location names an item you are already wearing, so it needs a dstSlot to move to: that is how you swap rings (11 ↔ 12), trinkets (13 ↔ 14) or weapons (16 ↔ 17). Without dstSlot there is nothing for the engine to pick, and the call does nothing.

Returns nothing. Silently no-ops when:

  • the input is nil, an empty string, or otherwise unparseable
  • you do not have the item; a reference searches your bags only, so an item you are already wearing is only reachable by location
  • the engine refuses the equip — combat, locked item, type mismatch with dstSlot, locked equipment slot, etc.

Two paths based on dstSlot:

  • Explicit dstSlot (1..19): cursor-free direct swap. Calls the engine's own FUN_INVENTORY_SWAP (0x005E0C40) — the same primitive Script_EquipCursorItem dispatches to after resolving cursor state. We hand it the source item GUID, source container GUID, source linear slot, and target paperdoll slot; the engine builds and sends the CMSG_SWAP_INV_ITEM (or CMSG_AUTOEQUIP_ITEM for cross-container) packet through its normal pipeline. The cursor is never read or written. An item already on the cursor stays on the cursor.
  • No dstSlot (engine auto-picks slot from inventory type): falls back to the cursor-pickup + AutoEquipCursorItem path because the auto-pick logic reads off cursor state.

Both paths first return anything on the cursor to the slot it came from, so a held item is neither clobbered nor left visually locked. The item is resolved after that, which means a location naming the slot a held item came from still finds it — while held, that slot reads empty.

-- By itemID, auto-pick slot:
C_Item.EquipItemByName(2589)

-- By name, force into off-hand (slot 17):
C_Item.EquipItemByName("Linen Cloth", 17)

-- From a chat link:
C_Item.EquipItemByName(itemLink)

-- That exact bag slot, auto-pick where it goes:
C_Item.EquipItemByName({ bagID = 0, slotIndex = 1 })

-- Move the main-hand weapon to the off-hand:
C_Item.EquipItemByName({ equipmentSlotIndex = 16 }, 17)

C_Item.GetCurrentItemLevel(itemLocation) / C_Item.GetDetailedItemLevelInfo(item)

Returns the item's base ilvl from m_itemLevel (cache record +0x38). There is no per-instance scaling (no upgrades, no warforging), so "current" and "base" item level are always identical — both APIs return the same single value. GetDetailedItemLevelInfo is spec'd to return (current, isPreview, base); we push only the current level, callers that care about the extra returns will see nil for them.

local ilvl = C_Item.GetCurrentItemLevel({equipmentSlotIndex = INVSLOT_HEAD})

C_Item.GetItemCount(itemInfo, [includeBank], [includeUses])

Returns the number of itemInfo that the player carries.

count = C_Item.GetItemCount(itemInfo [, includeBank [, includeUses]])
  • itemInfo — a numeric itemID, a string that contains "item:NNN" (full chat links work), or an item name. The name must match the full name of an item that you carry, with the suffix ("Foo of the Owl"). Case is not important. The count is for the item ID of the first match.
  • includeBank (optional, default false) — also count the bank and the bank bags. The bank does not have to be open.
  • includeUses (optional, default false) — for an item that does not stack and that has charges, return the total charges instead of the item count. If the item's use spell needs reagents, return the number of casts that the carried reagents allow. For all other items, this flag has no effect.

The count includes:

  • equipped items
  • equipped bags (the bag items themselves)
  • the backpack, the contents of the equipped bags, and the keyring
  • the bank and the bank bags, if includeBank is true

Broken items (durability 0) are not counted. includeUses does not count bank items.

local n = C_Item.GetItemCount(2589)               -- Linen Cloth that you carry
local n = C_Item.GetItemCount(2589, true)         -- also the bank
local n = C_Item.GetItemCount("item:2589")        -- string form
local n = C_Item.GetItemCount("Linen Cloth")      -- by name

-- Equipped items count:
local trinketID = GetInventoryItemID("player", INVSLOT_TRINKET1)
C_Item.GetItemCount(trinketID)                    -- 1

-- includeUses returns the total charges of an item with charges:
C_Item.GetItemCount(chargedItemID, false, true)   -- the sum of the charges
C_Item.GetItemCount(linenID, false, true)         -- the stack count (no charges)

C_Item.GetItemData(itemLocation) / C_Item.GetItemDataByID(item)

ClassicAPI-only kitchen-sink reader: returns a single table with every field we can extract from the cached ItemStats_C record, so addons that need more than a couple of fields don't have to chain a dozen C_Item.* calls. Saves both stack churn and the cost of redoing the cache lookup for every accessor.

data = C_Item.GetItemDataByID(item)        -- numeric id / "item:NNN" / chat link
data = C_Item.GetItemData(itemLocation)    -- {bagID,slotIndex} or {equipmentSlotIndex}

Returns nil if the input doesn't resolve to a valid itemID or the item isn't cached yet. Does not warm the cache — same passive- reader contract as C_Item.GetItemInfoInstant. Callers that need to wait for cache fill should call C_Item.RequestLoadItemDataByID and listen for ITEM_DATA_LOAD_RESULT.

Returned table:

Key Type Source / notes
itemID number Echoed from input.
name string m_name[0] — localized display name.
description string | nil m_description. Omitted when empty.
icon string | nil "Interface\\Icons\\<name>". Omitted when no icon.
displayInfoID number m_displayInfoID → ItemDisplayInfo.dbc row.
quality number 0..5 — see LE_ITEM_QUALITY_* enum.
classID number m_class — ItemClass.dbc row.
subclassID number m_subClass — ItemSubClass.dbc row.
className string Localized class name (e.g. "Weapon").
subclassName string Localized subclass name (e.g. "One-Handed Swords").
inventoryType number m_inventoryType — raw integer.
equipLoc string "INVTYPE_*" constant or "" for non-equippable.
bindType number m_bonding — 0=none, 1=BoP, 2=BoE, 3=BoU, 4=Quest.
flags number Raw m_flags u32.
isConjured bool Flag bit 0x2.
isOpenable bool Flag bit 0x4.
isLootable bool Flag bit 0x10 — has loot generators (right-click loot).
isWrapper bool Flag bit 0x200 — gift-wrappable.
maxStackSize number m_stackable.
maxCount number m_maxCount — 0=unlimited, 1=unique, otherwise per-character cap.
containerSlots number m_containerSlots — bag slot count, 0 for non-bags.
bagFamily number m_bagFamily converted to a bitmask (1 << (id-1)).
buyPrice number Vendor buy price in copper.
sellPrice number Vendor sell price in copper.
itemLevel number Base ilvl from ItemSparse.
requiredLevel number Minimum character level.
requiredSkill number SkillLine.dbc row, 0=none.
requiredSkillRank number Skill rank required.
requiredSpell number Spell.dbc row required to learn / use, 0=none.
requiredHonorRank number PvP honor rank required, 0=none.
requiredCityRank number Reserved; always 0 here.
requiredFaction number Faction.dbc row, 0=none.
requiredFactionRank number Reputation tier (Friendly/Honored/…).
allowableClass number Class bitmask, -1 = all classes.
allowableRace number Race bitmask, -1 = all races.
armor number Armor value.
block number Shield block value.
maxDurability number 0 for items without durability.
stats table Sparse { [statType] = statValue, … }. Empty for items without stat allocations.
resistanceHoly number Holy resistance.
resistanceFire number Fire resistance.
resistanceNature number Nature resistance.
resistanceFrost number Frost resistance.
resistanceShadow number Shadow resistance.
resistanceArcane number Arcane resistance.
damageMin table 5-element float array — weapon min damage per damage slot.
damageMax table 5-element float array.
damageType table 5-element integer array — damage school per slot.
delay number Weapon swing time in ms.
ammoType number Ammo subclass for ranged weapons.
rangedModRange number Range modifier for bows/guns.
spells table Sparse array of {id, trigger, charges, cooldown, category, categoryCooldown} records — one per non-empty spell slot (up to 5).
useSpellID number | nil Convenience — the spellID of the first slot with trigger=ON_USE (0). Omitted when the item has none. Same value C_Item.GetItemSpell returns.
lockID number Lock.dbc row for the item's lock, 0=none.
itemSet number ItemSet.dbc row, 0=not part of a set.
pageText number PageText.dbc row for readable items.
pageMaterial number Book page material.
languageID number Language for in-game-readable books.
startQuest number Quest row started by right-clicking this item.
material number Material type (cloth/leather/metal etc. for hit sounds).
sheath number Weapon sheath style.
randomProperty number Random property template id.
area number AreaTable.dbc — bound area for area-locked items, 0=none.
map number Map.dbc — bound map for map-locked items, 0=none.
local data = C_Item.GetItemDataByID(6948)  -- Hearthstone
-- data.name == "Hearthstone"
-- data.quality == 1, data.classID == 15, data.subclassID == 0
-- data.bindType == 1   (BoP)
-- data.useSpellID == 8690   (the recall spell)
-- data.maxStackSize == 1, data.maxCount == 1

local sword = C_Item.GetItemDataByID(2092)  -- Worn Shortsword
-- sword.equipLoc == "INVTYPE_WEAPONMAINHAND"
-- sword.damageMin[1] == 1.0, sword.damageMax[1] == 2.0, sword.delay == 1900
-- sword.stats == {}   (no stat allocations on the starter sword)

Cache state. The function reads the same DBCache_ItemStats network cache every other C_Item.* reader consults. Items already in the player's inventory are typically populated by the engine's natural prefetch before PLAYER_ENTERING_WORLD returns; for other items (quest rewards, AH items, link hovers, etc.) the data lands once a SMSG_ITEM_QUERY_SINGLE_RESPONSE arrives. Use C_Item.RequestLoadItemDataByID if you specifically need to trigger the load.

Stats encoding. The stats table is keyed by the ItemModType enum:

Key Meaning
0 Mana
1 Health
3 Agility
4 Strength
5 Intellect
6 Spirit
7 Stamina

Higher-numbered slots (consumable per-second-regen, defense rating, hit/crit/dodge/parry chance, weapon/spell-power) only appear on items the server actually carries — items here mostly use 0..7. Empty slots (both type and value zero) are omitted, so iterating stats yields only the stats actually allocated.

A ClassicAPI-only reader — GetItemInfo returns its tuple piecemeal and C_Item.GetItemInfo adds a few more. GetItemData is a single-call superset useful for tooltip addons and item-data caches.

C_Item.GetItemFamily(item)

Returns the BagFamily bitmask for an item — i.e., what kind of specialty bag it belongs in. 0 means "general-purpose" (no specialty bag preference). Returns nil if the item isn't in the cache.

familyBitmask = C_Item.GetItemFamily(item)

item accepts a numeric itemID or a string containing "item:NNN" (the bare shorthand and full chat links both parse correctly), same as C_Item.GetItemInfoInstant.

C_Item.GetItemFamily(2512)   -- 1   (Wooden Arrow → quiver-family)
C_Item.GetItemFamily(2516)   -- 2   (Light Shot → ammo pouch-family)
C_Item.GetItemFamily(6265)   -- 4   (Soul Shard → soul bag-family)
C_Item.GetItemFamily(2447)   -- 32  (Peacebloom → herb bag-family)
C_Item.GetItemFamily(6948)   -- 0   (Hearthstone → general-purpose)

Bitmask values follow the ItemBagFamily.dbc IDs converted to the bitmask form (1 << (familyID - 1)). The IDs 1–9 are the standard families; 10–13 are Turtle WoW custom families (the stock client stops at Keys):

Bit Value Raw ID Family Notes
0 1 1 Quiver (arrows)
1 2 2 Ammo Pouch (bullets)
2 4 3 Soul Bag
5 32 6 Herb Bag
6 64 7 Enchanting Bag
7 128 8 Engineering Bag
8 256 9 Keyring
9 512 10 Meat Bag (Turtle) ⚠ collides with the standard Gem Bag bit (0x200)
10 1024 11 Fish Bag (Turtle) ⚠ collides with the standard Mining Bag bit (0x400)
11 2048 12 Leather Bag (Turtle) no standard equivalent bit
12 4096 13 Mining Bag (Turtle) ⚠ not the standard Mining Bag bit (0x400)

Turtle custom families & the bit collision. Turtle added four cooking/gathering bag families (Meat, Fish, Leather, Mining) as ItemBagFamily.dbc rows 10–13, rather than reusing the standard later IDs 4 (Leatherworking) / 5 (Inscription), which this client left unused. Because we convert the raw ID straight through 1 << (rawID - 1), these land on bits 9–12 — the same numeric values assigned to Gem Bag (0x200) and Mining Bag (0x400) elsewhere. So an addon that hard-codes family == 0x200 to mean "Gem Bag" will misread a Turtle Meat Bag. The values are self-consistent within Turtle (a Meat Bag and the meat items it holds both report 0x200, which is all the "does this item fit this bag" test needs) — just don't assume the standard bit meanings for anything above 0x100.

Encoding under the hood. The engine stores the raw 1-based BagFamily ID (arrow=1, bullet=2, soul shard=3, herb=6, …). We convert on the way out via bitmask = 1 << (rawID - 1), so callers see the bitmask encoding they expect — addons can band(family, FAMILY_BAG_HERB_BAG) directly.

Legacy Blizzard bags derive from (class, subClass). The stock bags shipped with an empty m_bagFamily field — the four profession bags (Soul Bag, Herb Bag, Enchanting Bag, Engineering Bag, all Container class) and quivers/ammo pouches (Quiver class). For a bag whose field is 0 we recover the family from its class + subclass — the same signal the "24 Slot Soul Bag" tooltip line is built from. A Felcloth Bag reports 0x4 (Soul Shards), a quiver 0x1 (arrows), an ammo pouch 0x2 (bullets) — each matching the items it holds. Keyed on class because subclass numbers repeat across classes (Container subclass 2 = Herb Bag, Quiver subclass 2 = Quiver). This is automatic; you no longer need to read subClass yourself. (Turtle's own custom bags — Meat/Fish/Leather/Mining — populate the field directly; a Turtle-specific resolver module supplies their mapping as a safety net, kept separate from the stock table.)

nil vs 0. We return nil (not 0) for items the cache lookup fails on, so callers can distinguish "item not cached, retry after the event lands" from "item exists but has no family preference." Both are safe to treat as 0 for routing-logic purposes; the distinction helps debugging.

Auto-warmup on cache miss. First call for an uncached item returns nil AND triggers the engine's cache fill in the background. Listen for GET_ITEM_INFO_RECEIVED(itemID, success) and retry once the matching itemID arrives — "nil first call, value second call." We fire GET_ITEM_INFO_RECEIVED when the cache lands, to stay consistent with our other implicit-warmup paths (GetItemInfo, SetItemByID). Addons that already listen for that event get the notification for free.

Equivalent to the global GetItemFamily and C_Item.GetItemFamily.

C_Item.GetItemGUID(itemLocation)

Returns the per-instance 64-bit GUID of the item at the location, formatted as "0xHHHHHHHHLLLLLLLL" (16 hex digits, hi dword first). Same format UnitGUID uses — GUIDs are plain qwords with no "Item-Server-..."-style prefix scheme. Returns nil for empty / invalid locations.

local guid = C_Item.GetItemGUID({equipmentSlotIndex = INVSLOT_HEAD})
-- "0x40000000000DEFCA"

Reads CGItem instance block at +0x08 → GUID qword at +0x00. The GUID is stable per-character-session and survives moves between bags / character pane, so it's the right identifier for "this exact item instance" — equipment-set tracking, soulbind matching, or any addon code that needs to follow a single item across slot moves.

C_Item.GetItemID(itemLocation)

Returns the itemID of the item at the given location, or nil if the slot is empty or the location is malformed. Useful as the input to GetItemInfoInstant/GetItemInfo when you only know which slot an item came from (rather than its link or ID).

Accepts the same itemLocation shapes as IsBound:

local id = C_Item.GetItemID({equipmentSlotIndex = INVSLOT_HEAD})
if id then
    local _, type, subtype = C_Item.GetItemInfoInstant(id)
    -- ...
end

C_Item.GetItemIDForItemInfo(itemInfo)

Returns the itemID for an itemID, an "item:N" string or an item link. It does not need the item's data in the cache. An item name, or input that names no item, returns nothing.

/dump C_Item.GetItemIDForItemInfo("item:6948")
-- 6948

C_Item.GetItemInfo(itemInfo)

The full GetItemInfo tuple, sourced from the client-side item cache plus the class/subclass/inventory-type DBC name lookups. The namespaced counterpart to the stock global GetItemInfo (which returns only a short tuple); this returns the wide set.

Accepts a numeric itemID, an "item:NNN" string, or a full chat link.

Returns 18 values:

# Return Notes
1 itemName
2 itemLink basic item:ID hyperlink (colored, bracketed name)
3 itemQuality 0–5
4 itemLevel
5 itemMinLevel required level
6 itemType localized class name (e.g. "Weapon")
7 itemSubType localized subclass name (e.g. "Sword")
8 itemStackCount max stack size
9 itemEquipLoc INVTYPE_* token, "" for non-equippable
10 itemTexture icon path (no fileID system here)
11 sellPrice vendor sell price in copper
12 classID
13 subclassID
14 bindType 0 none / 1 BoP / 2 BoE / 3 BoU / 4 quest
15 expansionID always 0 (classic)
16 setID item-set ID, nil if none
17 isCraftingReagent always false (no such flag in the item data)
18 itemDescription item flavor/description text ("" if none)

Asynchronous on a cache miss: if the item isn't cached yet it returns nothing (nil) and warms the cache; the value lands on a retry after GET_ITEM_INFO_RECEIVED fires — same contract as the stock GetItemInfo. Fields 4–18 are the ones the stock global never returned.

local name, link, quality, ilvl, minLevel, itype, isub, stack, equipLoc,
      tex, sell, classID, subID, bind, expac, setID, reagent, desc
    = C_Item.GetItemInfo(6948)   -- Hearthstone

C_Item.GetItemInfoInstant(item)

Accessor for the always-available subset of item info — the fields that depend only on classification, not on player-specific state. Synchronous, side-effect-free: peeks the client-side item cache and returns whatever it has without warming or queueing.

Accepts a numeric itemID or a string containing "item:NNN" (matches both the bare "item:1234" shorthand and full chat links like "|cff...|Hitem:1234:...|h[Name]|h|r"). Item names are not accepted — there is no name → ID resolver, and it's rarely the form addon code actually has on hand.

Returns seven values:

itemID, itemType, itemSubType, itemEquipLoc, icon, classID, subClassID

The itemID return is always populated for any input that resolves to a positive integer (so it's safe to use the function as an ID extractor on a link without first warming the cache). The remaining six fields come from the cache record when present — when the item isn't cached yet they're all nil, but the call still returns a 7-tuple so the positional shape is preserved.

  • itemType / itemSubType are the localized class / subclass names (e.g. "Weapon" / "One-Handed Swords"), read from ItemClass.dbc and ItemSubClass.dbc.
  • itemEquipLoc is the "INVTYPE_*" constant (e.g. "INVTYPE_HEAD"), or "" for non-equippable items.
  • icon is a path string ("Interface\\Icons\\..."), matching what the rest of the API returns. There is no fileID system here, so a path is the only meaningful value.
  • classID / subClassID are the raw enum integers (e.g. 2, 7 for one-handed swords).
local id, type, subtype, equipLoc, icon, classID, subClassID
    = C_Item.GetItemInfoInstant(6948)  -- Hearthstone, cached
-- type="Miscellaneous", subtype="Junk", equipLoc="",
-- icon="Interface\\Icons\\INV_Misc_Rune_01", classID=15, subClassID=0

local id = C_Item.GetItemInfoInstant("|cff...|Hitem:6948:0:0:0|h[Hearthstone]|h|r")
-- id == 6948 even when the cache is cold — the call parses the link
-- to extract the ID without consulting the cache.

The actual class/subclass values reflect this client's data. For example, there is no Cloth subclass under Trade Goods — Silk Cloth lives at (7, 0) in this client, not (7, 5).

No auto-warmup. Unlike GetItemInfo or C_Item.GetItemNameByID, GetItemInfoInstant does not trigger a network query on a cache miss. The "Instant" name is contractual — callers that need the cache populated should use C_Item.RequestLoadItemDataByID explicitly, or just call GetItemInfo (which warms via the Script_GetItemInfo hook).

C_Item.GetItemInventorySlotInfo(inventorySlot)

Returns the localized display name for an Enum.InventoryType value — e.g. C_Item.GetItemInventorySlotInfo(1) → "Head". Returns nil for the non-equip slot (0) or out-of-range values.

The engine's INVTYPE table stores the equipLoc key ("INVTYPE_HEAD"); the display name is the FrameXML global string of that key (INVTYPE_HEAD = "Head"), so the result is correctly localized on non-English clients.

C_Item.GetItemInventorySlotKey(inventorySlot)

Returns the equipLoc key string for an Enum.InventoryType value — e.g. C_Item.GetItemInventorySlotKey(1) → "INVTYPE_HEAD". This is the same INVTYPE_* token C_Item.GetItemInfoInstant returns as its itemEquipLoc. Returns nil for the non-equip slot (0) or out-of-range values. Pair it with GetItemInventorySlotInfo to turn the key into a localized label.

C_Item.GetItemInventoryType(itemLocation) / C_Item.GetItemInventoryTypeByID(item)

Returns the numeric Enum.InventoryType straight off the cache record's m_inventoryType field (+0x2C) — the integer sibling of GetItemInfoInstant's 4th return (which gives the INVTYPE_* string).

Value Constant Slot
0 INVTYPE_NON_EQUIP_IGNORE non-equippable
1 INVTYPE_HEAD head
2 INVTYPE_NECK neck
… … …
20 INVTYPE_ROBE chest (full-body robes)
26 INVTYPE_RANGEDRIGHT ranged
27 INVTYPE_QUIVER quiver/ammo pouch

Items here only produce values 0..28; the higher constants (INVTYPE_PROFESSION_*, INVTYPE_EQUIPABLESPELL_*, etc.) are never returned. Code that compares against the higher enum values still resolves correctly because items here just don't carry those types.

local t = C_Item.GetItemInventoryTypeByID(19019)  -- Thunderfury: 17 = INVTYPE_2HWEAPON
if t == 1 then -- head
    ...
end

C_Item.GetItemLink(itemLocation)

Returns the fully-decorated per-instance hyperlink for the item at the location — same string GetContainerItemLink(bag, slot) or GetInventoryItemLink("player", slot) would return for the same slot. Enchant ID, random-suffix, and any other per-instance data attached to the CGItem are preserved.

local link = C_Item.GetItemLink({bagID = 0, slotIndex = 1})
-- "|cffa335ee|Hitem:16539:911:::::::70::::::::::|h[General's Silk Boots]|h|r"
--                          ^^^ — enchant ID (Speed +15%) preserved

Implemented by reading the location table's fields and tail-calling the engine's link script function (Script_GetContainerItemLink at 0x004F9930 for bag locations, Script_GetInventoryItemLink at 0x004C8C10 for equipment slots). The link string is built by the engine off the live CGItem, so it always matches what other addons see via the older positional-arg APIs.

C_Item.GetItemLocation(itemGUID)

Reverse lookup from a GUID string (the format C_Item.GetItemGUID returns) to an itemLocation table identifying the slot currently holding that item. Returns nil if the GUID isn't resident in the walked scope.

local guid = C_Item.GetItemGUID({equipmentSlotIndex = INVSLOT_HEAD})
-- player moves item to bag, drops it, etc. — guid stays valid across
-- bag/equipment rearrangements until the item leaves the session
local loc = C_Item.GetItemLocation(guid)
-- loc might now be { bagID = 1, slotIndex = 4 }, or nil if sold

We return a plain table with the same field shape every other C_Item.* API in ClassicAPI accepts as input ({equipmentSlotIndex=N} or {bagID=B, slotIndex=S}), so the result pipes straight into C_Item.GetItemQuality(loc), C_Item.GetItemLink(loc), etc.

Walked scope. Character-pane equipment (slots 1..19) + backpack (bagID 0) + the four equipped bags (bagIDs 1..4). Bank and keyring are NOT walked — same scope as the rest of the C_Container.* family. Worst case is ~80 slot lookups; fine for sporadic use, not appropriate for per-frame polling.

Implementation. We don't go through the engine's typed object-by-GUID helper at 0x00468460 (the one used for the auction- sell slot) because that path asserts on bad input — stale GUIDs from addon-side caching would crash the client. Instead we walk equipment + bag slots, read each CGItem's GUID, and compare. Misses return nil; hits never need a fallback.

C_Item.GetItemMaxStackSize(itemLocation) / C_Item.GetItemMaxStackSizeByID(item)

Returns the item type's max stack size — what you'd find as the 7th return of GetItemInfo(item). 1 for non-stackable items; 5, 20, 200, etc. for stackables. Different from C_Item.GetStackCount / the engine's GetContainerItemInfo, which return the current count in a specific slot.

local cap = C_Item.GetItemMaxStackSizeByID(2589)  -- Linen Cloth → 20

Single uint32 read at cache record +0x60 (m_stackable). By-ID form fires a background cache fill on miss and returns nil; re-call after GET_ITEM_INFO_RECEIVED.

C_Item.GetItemName(itemLocation) / C_Item.GetItemNameByID(item)

Returns the item's display name as a string, or nil for empty / uncached / invalid inputs.

The location form points at a live item instance, so it returns the decorated name — random suffix included ("Iridium Chain of the Owl", not the base "Iridium Chain") — built off the CGItem via the engine's own name builder, matching the bracketed name in C_Item.GetItemLink. (It falls back to the base ItemStats_C.m_name[0] name if the instance build yields nothing.)

The ByID form has no instance — an itemID can't carry a random suffix — so it returns the base m_name[0] name only. Cache miss on the ByID form returns nil and fires the cache fill so the next call (after GET_ITEM_INFO_RECEIVED) succeeds.

-- location form → decorated name (random suffix included)
local name = C_Item.GetItemName({equipmentSlotIndex = 2})  -- "Iridium Chain of the Owl"
-- ByID form → base name only (no instance → no suffix)
local name = C_Item.GetItemNameByID(6948)                  -- "Hearthstone"

C_Item.GetItemQuality(itemLocation) / C_Item.GetItemQualityByID(item)

Returns the item's quality as an integer (0=Poor, 1=Common, 2=Uncommon, 3=Rare, 4=Epic, 5=Legendary), matching the LE_ITEM_QUALITY_* constants. Single uint32 read at cache record +0x1C.

if C_Item.GetItemQualityByID(itemID) >= LE_ITEM_QUALITY_RARE then
    -- highlight rare-or-better drop
end

C_Item.GetItemSellPrice(itemLocation) / C_Item.GetItemSellPriceByID(item)

Returns the vendor sell price in copper, per unit (multiply by stack count for the per-stack value). Matches the 11th return of GetItemInfo. Returns nil on cache miss / invalid input. Cache miss fires a background fill so a follow-up call after GET_ITEM_INFO_RECEIVED returns the value.

local unit = C_Item.GetItemSellPriceByID(2589)   -- Linen Cloth → 25 (copper)
local stack = unit * C_Item.GetItemMaxStackSizeByID(2589)

Single uint32 read at cache record +0x28 (m_sellPrice). Tooltips don't surface this — the field is populated on every sellable item but the engine's tooltip code never reads it — so this function exposes data that's been sitting in the cache unused.

C_Item.GetItemSetID(itemLocation) / C_Item.GetItemSetIDByID(item)

Returns the ItemSet.dbc ID for the item if it's part of an equipment set (e.g., 181 for any Magister's Regalia piece), or nil if the item isn't part of any set / the item isn't cached / the slot is empty.

local setID = C_Item.GetItemSetIDByID(16682)   -- Magister's Regalia: 181
local setID = C_Item.GetItemSetIDByID(6948)    -- Hearthstone: nil

-- by itemLocation (worn or in-bag)
local chestSet = C_Item.GetItemSetID({ equipmentSlotIndex = 5 })

This is the 16th return of GetItemInfo (the setID field). Single uint32 read at cache record +0x1C0 (m_itemSet). Same cache-warm pattern as the other *ByID getters — cache misses return nil and fire a background load; call C_Item.RequestLoadItemDataByID(itemID) if you need synchronous- after-event resolution.

C_Item.GetItemSetInfo(setID)

Returns a table describing the item set, or nil if setID doesn't resolve to an ItemSet.dbc row.

local info = C_Item.GetItemSetInfo(181)
-- {
--   setID = 181,
--   name = "Magister's Regalia",
--   requiredSkill = 0,
--   requiredSkillRank = 0,
--   items = { 16685, 16683, 16686, 16684, 16687, 16689, 16688, 16682 },
--   bonuses = {
--     { spellID = 29091, threshold = 2 },   -- 2-piece bonus
--     { spellID = 27867, threshold = 6 },   -- 6-piece bonus
--     { spellID = 18679, threshold = 8 },   -- 8-piece bonus
--     { spellID = 30777, threshold = 4 },   -- 4-piece bonus
--   },
-- }
Field Type Notes
setID number Echo of the input.
name string Localized set name.
requiredSkill number Skill line ID required to use the set; 0 if none.
requiredSkillRank number Required rank in that skill line; 0 if none.
items array itemIDs in the set. Empty slots filtered, so #info.items is the real item count. Order is the engine's own (matches the set's order in the DBC; not necessarily slot-sorted).
bonuses array { spellID, threshold } tables for each non-empty set-bonus slot. threshold is the number of equipped set pieces required to grant spellID. Order is the DBC's; bonuses with the same threshold are not normalized.

Reads ItemSet.dbc directly — no cache warm-up needed (DBCs load at boot). Returns nil for setID == 0, out-of-range IDs, or rows that don't exist.

C_Item.GetItemSpell(item)

Returns (spellName, spellID) for the on-use spell attached to an item (potions, trinkets, scrolls, hearthstone, food/drink, etc.), or nil for items without one (vendor trash, regular gear, weapons with passive procs).

item accepts the same input shapes as GetItemInfo — a numeric itemID, a chat-link "|cffffffff|Hitem:6948:0:0:0|h[...]|h|r" fragment, or a "item:NNN" short form.

C_Item.GetItemSpell(6948)
-- "Hearthstone", 8690

C_Item.GetItemSpell(13442)
-- "Mighty Rage Potion", 17528

C_Item.GetItemSpell(11288)  -- a soulstone (trigger=4, not ON_USE)
-- nil

C_Item.GetItemSpell(2589)   -- Linen Cloth, no spell
-- nil

Returns the ON_USE spell only. Items can carry up to 5 spell entries in their ItemStats_C record, each with its own trigger code:

Trigger Meaning Surfaced by GetItemSpell?
0 ON_USE yes
1 ON_EQUIP (passive aura on gear) no
2 CHANCE_ON_HIT (weapon procs) no
4 SOULSTONE (on-death) no
5 ON_USE_NO_DELAY no (TODO: should we add this?)
6 LEARN_SPELL (recipes) no

GetItemSpell reports on-use triggers only. Addons that need the other trigger types (proc auras, recipe targets) should reach into the cache directly — the spell slots are at ItemStats +0x11C (5 spell IDs) and +0x130 (5 trigger codes).

Auto-warmup on cache miss. Items not yet in the local cache return nil and silently kick off an SMSG_ITEM_QUERY_SINGLE request. A second call after GET_ITEM_INFO_RECEIVED lands the data. Same warmup pattern as C_Item.GetItemFamily and the rest of our cache-backed accessors.

C_Item.GetItemStatDelta(itemLink1, itemLink2 [, statTable])

Returns a table of the per-stat difference between two items, computed as item1 - item2. A positive value means the first item has more of that stat. Only stats whose delta is not zero appear.

-- Compare a ring in your bags against the equipped one:
local delta = C_Item.GetItemStatDelta(
    C_Item.GetItemLink({bagID = 0, slotIndex = 3}),
    C_Item.GetItemLink({equipmentSlotIndex = 11}))
-- delta = { ITEM_MOD_INTELLECT_SHORT = 6, ITEM_MOD_SPIRIT_SHORT = 2 }
-- (the bag ring has +6 Int / +2 Spirit more than the equipped one)

The key set, the input forms, random-suffix handling, statTable, and the cache behavior are the same as for C_Item.GetItemStats. Returns nothing if either item is not cached.

Returns a table of an item's stats, keyed by the FrameXML global-string names WoW's own GetItemStats uses (so _G[key] yields the display label — _G["ITEM_MOD_STRENGTH_SHORT"] → "Strength"). Only stats the item actually carries are present.

statTable. If you give a table, the function writes the stats into that table and returns it, so it does not make a new table. The function does not clear the table first. Clear it yourself before you use it again, or keys from the last item stay in it.

C_Item.GetItemStats("item:2244")            -- Krol Blade
-- { ITEM_MOD_STRENGTH_SHORT = 7, ITEM_MOD_STAMINA_SHORT = 5,
--   ITEM_MOD_DAMAGE_PER_SECOND_SHORT = 40.89,
--   ITEM_MOD_CRIT_MELEE_RATING = 1, ITEM_MOD_CRIT_RANGED_RATING = 1 }

C_Item.GetItemStats("item:12022:0:769")     -- "of the Owl" random suffix
-- { ITEM_MOD_INTELLECT_SHORT = 6, ITEM_MOD_SPIRIT_SHORT = 6 }

Sources. The table is assembled from three places, so it reflects what the item actually grants — not just its stored stat slots:

  1. Base record — the stored attributes, armor, resistances, and (for weapons) DPS.
  2. On-equip spells (SpellTrigger == ON_EQUIP) — the engine stores the "special" bonuses (crit, attack power, spell power, hit, mp5, defense, …) as an equip spell whose aura effect carries the value, not as a stat slot. Their auras are decoded into the keys below.
  3. Random suffix — the link's third item:id:enchant:SUFFIX:unique field (ItemRandomProperties.dbc → SpellItemEnchantment.dbc): stat suffixes ("of the Owl") are themselves equip spells, armor suffixes ("of Toughness") are direct resistance enchants. A bare itemID or a suffix-less link contributes nothing here.

Keys.

Key Stat Source
ITEM_MOD_STRENGTH_SHORT / _AGILITY_ / _STAMINA_ / _INTELLECT_ / _SPIRIT_SHORT Base attributes record / MOD_STAT
ITEM_MOD_MANA_SHORT / ITEM_MOD_HEALTH_SHORT Mana / health on equip record / MOD_INCREASE_ENERGY (35) / MOD_INCREASE_HEALTH (34)
RESISTANCE0_NAME Armor record / MOD_RESISTANCE bit 0
RESISTANCE1_NAME … RESISTANCE6_NAME Holy / Fire / Nature / Frost / Shadow / Arcane resistance record / MOD_RESISTANCE / type-4 enchant
ITEM_MOD_DAMAGE_PER_SECOND_SHORT Weapon DPS (float) avg damage ÷ swing time
ITEM_MOD_ATTACK_POWER_SHORT Attack power MOD_ATTACK_POWER (99)
ITEM_MOD_RANGED_ATTACK_POWER_SHORT Ranged attack power (ranged-only items) MOD_RANGED_ATTACK_POWER (124)
ITEM_MOD_CRIT_MELEE_RATING + ITEM_MOD_CRIT_RANGED_RATING Crit % weapon-crit aura (52)
ITEM_MOD_HIT_MELEE_RATING + ITEM_MOD_HIT_RANGED_RATING Hit % hit aura (54)
ITEM_MOD_HIT_SPELL_RATING Spell hit % spell-hit aura (55)
ITEM_MOD_CRIT_SPELL_RATING Spell crit % spell-crit aura (57 / 71)
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT Spell damage, all schools MOD_DAMAGE_DONE (13), all magic schools
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT / _FIRE_ / _NATURE_ / _FROST_ / _SHADOW_ / _ARCANE_SPELL_DAMAGE_DONE_SHORT Spell damage for one school only (ClassicAPI extension) MOD_DAMAGE_DONE (13), school mask
ITEM_MOD_SPELL_HEALING_DONE_SHORT Healing MOD_HEALING_DONE (135)
ITEM_MOD_SPELL_PENETRATION Target spell resistance ignored MOD_TARGET_RESISTANCE (123), magic schools
ITEM_MOD_ARMOR_PENETRATION_RATING Target armor ignored (flat) MOD_TARGET_RESISTANCE (123), armor
ITEM_MOD_MANA_REGENERATION mp5 (mana) MOD_POWER_REGEN (85)
ITEM_MOD_HEALTH_REGEN hp5 MOD_HEALTH_REGEN_IN_COMBAT (161)
ITEM_MOD_DEFENSE_SKILL_RATING Defense skill MOD_SKILL (30, misc 95)
ITEM_MOD_DODGE_RATING Dodge % MOD_DODGE_PERCENT (49)
ITEM_MOD_PARRY_RATING Parry % MOD_PARRY_PERCENT (47)
ITEM_MOD_BLOCK_RATING Block chance % MOD_BLOCK_PERCENT (51)
ITEM_MOD_BLOCK_VALUE Shield block value record (m_block) / MOD_SHIELD_BLOCKVALUE (158)

Spell damage and healing are separate. An item with "+N damage and healing" reports ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = N and ITEM_MOD_SPELL_HEALING_DONE_SHORT = N. An item with "+N Fire damage" reports only ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = N, because that bonus applies to Fire spells only. To get an item's total bonus for one school, add the all-schools key and that school's key.

Values are native percentages, not ratings. There is no rating system here, so the percent-based stats (crit / hit / defense) report the raw percentage under the *_RATING key — e.g. Krol Blade's "+1% crit" is ITEM_MOD_CRIT_MELEE_RATING = 1. A native percent is level-independent. Flat stats (attack power, spell damage, healing, mp5) are the item's actual magnitudes.

Rating stats that don't exist here (haste, mastery, versatility, expertise, resilience) never appear. A generic "+N attack power" item carries both a melee and a ranged AP aura; the generic ITEM_MOD_ATTACK_POWER_SHORT key subsumes it, so ITEM_MOD_RANGED_ATTACK_POWER_SHORT shows only for ranged-only items (scopes, ranged weapons). Rarer auras with no clean single key — percent attack power, per-weapon-line skill, damage-taken, on-hit / on-use procs — are skipped.

Input. Accepts a full chat hyperlink, an item:N… link string, or a bare itemID. Returns nothing if the item is not cached yet. Call GetItemInfo to load it, then try again.

GetItemClassInfo(classID)

Returns className — the localized ItemClass.dbc name for an item class ID (the classID from Enum.ItemClass / the 12th GetItemInfo return). Also available as C_Item.GetItemClassInfo(classID).

GetItemClassInfo(2)   -- "Weapon"
GetItemClassInfo(4)   -- "Armor"
GetItemClassInfo(1)   -- "Container"

Returns nil for an unknown class. Obsolete slots return the client's literal string (GetItemClassInfo(3) → "Jewelry(OBSOLETE)") — use Enum.ItemClass for the key names.

GetItemSubClassInfo(classID, subClassID) / C_Item.GetItemSubClassInfo(classID, subClassID)

Returns subClassName, subClassUsesInvType for an item class/subclass pair (the global form documents the second value as isArmorType — same value).

GetItemSubClassInfo(2, 7)   -- "One-Handed Swords", false
GetItemSubClassInfo(4, 1)   -- "Cloth", true
  • subClassName (string) — the localized ItemSubClass.dbc name (verbose form, e.g. "One-Handed Swords"; falls back to the short form for subclasses that only populate it, e.g. "Consumable").
  • subClassUsesInvType (boolean) — true for subclasses whose items are labeled by inventory slot rather than by subclass name. Here that's exactly the armor material types (Miscellaneous, Cloth, Leather, Mail, Plate); false for weapons, shields, librams, idols, totems, and non-equipment. Read from the ItemSubClass.dbc flags bit 0x200.

Returns nil if the (classID, subClassID) pair has no row.

C_Item.GetItemUniqueness(itemLocation) / C_Item.GetItemUniquenessByID(item)

The two functions have different signatures:

C_Item.GetItemUniqueness(itemLocation) returns (limitCategory, limitMax):

Field Source
limitCategory Always 0 — there is no LimitCategory.dbc.
limitMax ItemStats_C.m_maxCount — 0 = unlimited, 1 = "Unique", higher = inventory cap.

C_Item.GetItemUniquenessByID(itemID) returns (isUnique, limitCategoryName, limitCategoryCount, limitCategoryID):

Field Source
isUnique m_maxCount > 0 — true for any unique-tagged item.
limitCategoryName Always nil — no categories here.
limitCategoryCount m_maxCount when isUnique, else nil.
limitCategoryID Always nil.

Both functions return nothing (zero Lua return values) if the item record isn't cached. The by-ID variant fires a background cache fill on miss; re-call after GET_ITEM_INFO_RECEIVED.

local _, max = C_Item.GetItemUniqueness({equipmentSlotIndex = 13})
-- max = 1 for typical trinkets (Unique)
local isUnique, _, count = C_Item.GetItemUniquenessByID(81013)
-- isUnique = true, count = 10  (Southern Sand Crawler Leg, "Unique (10)")

The category half is for items with a shared unique-equipped limit ("Unique-Equipped: Eye of Azshara"). There are no such items here, so the category fields stay nil/0.

C_Item.GetStackCount(itemLocation)

Returns the current stack count in a specific slot — distinct from C_Item.GetItemCount(item), which sums every stack of that itemID across the player's inventory. Useful when an addon needs to know "this specific stack has 13/20", not "I have 47 total".

local n = C_Item.GetStackCount({bagID = 0, slotIndex = 1})
-- 13 (the bag's first slot has 13 of whatever item)

Reads ITEM_FIELD_STACK_COUNT directly off the item's m_objectFields — same field GetContainerItemInfo returns as itemCount. Returns 0 for empty / unresolvable locations.

C_Item.GetWeaponEnchantInfo()

Returns a 12-tuple, including the temp-enchant IDs that the stock 8-return global omits.

hasMain, mainExpire, mainCharges, mainEnchantID,
hasOff,  offExpire,  offCharges,  offEnchantID,
hasRanged, rangedExpire, rangedCharges, rangedEnchantID
   = C_Item.GetWeaponEnchantInfo()
-- Apply Brilliant Mana Oil to mainhand, then:
local has, expireMs, charges, enchantID = C_Item.GetWeaponEnchantInfo()
-- has = true, expireMs = 1799850, charges = 0, enchantID = 2629

-- Apply Instant Poison instead:
-- has = true, expireMs = 1799850, charges = 40, enchantID = 323

The expiration value is the time left in milliseconds. It counts down continuously, the same value the stock GetWeaponEnchantInfo gives. An enchant with no time limit reports 0.

An oil runs on time alone and reports charges = 0. A poison reports a charge count as well as a time.

These values come from the temporary enchant: oils, sharpening stones, poisons, and shaman weapon imbues.

The permanent enchant (Crusader, Mongoose, etc., in slot 0 at +0x40) is not reported here — that's a separate field and GetWeaponEnchantInfo doesn't expose it either. Get the permanent enchant ID by parsing GetInventoryItemLink("player", slot) (the link includes it as the 2nd numeric field).

The stock global GetWeaponEnchantInfo is unchanged — old addons reading positions 4..8 by index still work.

Equivalent to the extended form of GetWeaponEnchantInfo.

C_Item.GetItemTempEnchantInfo(itemLocation)

Returns the temporary enchant on any item you own — a poison, a weapon oil, a sharpening stone, or a shaman imbue:

hasEnchant, expirationMs, charges, enchantID
   = C_Item.GetItemTempEnchantInfo(itemLocation)
-- The poison on a swap weapon that sits in a bag
local has, expireMs, charges, enchantID =
    C_Item.GetItemTempEnchantInfo({ bagID = 0, slotIndex = 3 })
if has then
    local info = C_Item.GetEnchantInfo(enchantID)
    print(info.name, charges .. " charges")
end

The location takes the same forms as the other C_Item location calls: a table ({equipmentSlotIndex=N} or {bagID=B, slotIndex=S}), or the GUID string from C_Item.GetItemGUID.

The four returns match one weapon slot of C_Item.GetWeaponEnchantInfo, so the two read the same way. enchantID goes straight into C_Item.GetEnchantInfo for the name and the effects.

A weapon keeps its poison while it rests in a bag. The equipped-slot calls cannot see that weapon. This call can.

This call never returns the permanent enchant (Crusader, Mongoose). That enchant is a separate field.

An empty slot returns false, 0, 0, 0. An item you do not own returns the same, and so does a location that resolves to nothing.

For an equipped item, C_PaperDollInfo.GetTemporaryEnchantmentInfo gives the same enchant as a table.

This call is a ClassicAPI extension.

C_Item.GetEnchantInfo(enchantID)

Resolves an item-enchantment ID — the enchantID returned by C_Item.GetWeaponEnchantInfo for a weapon's temporary enchant, and the same IDs item permanent enchants use — into a table:

local info = C_Item.GetEnchantInfo(enchantID)
-- info.enchantID = <id>
-- info.name      = "Crusader"        -- localized display name
-- info.effects   = { {type=1, amount=0, arg=20007} }
-- info.spellID   = 20007             -- spell-type enchants only
-- A proc/equip enchant → chain its spellID into C_Spell:
local info = C_Item.GetEnchantInfo(1900)          -- "Crusader"
if info.spellID then
    print(C_Spell.GetSpellDescription(info.spellID))  -- the proc's text
end

effects is an array of the record's non-empty effect slots, each { type, amount, arg } where type is the standard ITEM_ENCHANTMENT_TYPE:

type meaning carries
1 combat-proc spell arg = spellID
2 weapon damage amount = +damage
3 equip spell / aura arg = spellID
4 resistance / armor amount = +value
5 stat arg = stat index, amount = value
6 totem —
7 use spell arg = spellID

For spell types (1/3/7) arg is a spellID feedable into C_Spell.GetSpellInfo / GetSpellDescription; the first such id is also surfaced at top level as spellID for convenience (absent for non-spell enchants like sharpening stones).

Returns nil for a non-numeric / non-positive id, an out-of-range id, or a record with no name.

Reads SpellItemEnchantment.dbc (records 0x00C0D7D8, count 0x00C0D7DC) — the 24-column table every enchant ID indexes: Type[3]@+0x04, Amount[3]@+0x10, EffectArg[3]@+0x28, Name[8]@+0x34 (locale-indexed). The layout was verified by parsing the on-disk DBC against known records (Crusader 1900 → type 1, arg 20007; Sharpened +3 → type 2, amount 3). Fully resident from boot, so it answers for any enchant ID with no caching or round-trip.

Lives in C_Item (not C_Spell) because the id originates from C_Item.GetWeaponEnchantInfo and the concept is an item enchantment — SpellItemEnchantment is just the DBC's internal name (enchants are implemented via spell effects).

Not derivable: the source item. The enchant record holds no back-reference to the item that applied it, and there is no client-side reverse index (enchantID → item). Finding it would need an external scraped DB (pfQuest/Questie-style).

C_Item.IsBound(itemLocation)

Returns true if the item at the given location is soulbound, false otherwise (including when the slot is empty or the location is malformed). The client tracks the soulbound bit on each item instance directly; previously the only way to read it from Lua was a scan-tooltip hack (SetBagItem + string-compare against the localized ITEM_SOULBOUND constant) — slow, locale-fragile, and one of the hottest paths during bag updates.

if C_Item.IsBound({equipmentSlotIndex = INVSLOT_HEAD}) then ... end
if C_Item.IsBound({bagID = 0, slotIndex = 1}) then ... end
if C_Item.IsBound(itemGUID) then ... end

IsConsumableItem(item) / C_Item.IsConsumableItem(item)

Tells if the item is used up when you use it. An item is consumable in these conditions:

  • The item is ammo or a thrown weapon.
  • The item has a use effect with charges that run out.
  • The item has a use effect that costs reagents.

The item class has no effect on the result. For example, Rough Dynamite is a trade good and it is consumable. An on-use trinket is not consumable.

item is an item ID, an item link, or the name of an item that you carry. An item that is not in the item cache is not consumable. The function does not start a load for that item.

The two forms return different types. IsConsumableItem returns 1 or nil. C_Item.IsConsumableItem returns true or false.

IsConsumableItem(118)            -- Minor Healing Potion → 1
C_Item.IsConsumableItem(2512)    -- Rough Arrow → true (ammo)
C_Item.IsConsumableItem(4358)    -- Rough Dynamite → true
C_Item.IsConsumableItem(6948)    -- Hearthstone → false
C_Item.IsConsumableItem(18820)   -- Talisman of Ephemeral Power → false

IsEquippableItem(item) / C_Item.IsEquippableItem(item)

Tells if the item goes in an equipment slot. Bags, ammo, thrown weapons, and relics also count as equippable. The result does not tell if your character can use the item.

item is an item ID, an item link, or the name of an item that you carry. An item that is not in the item cache is not equippable. The function does not start a load for that item.

The two forms return different types. IsEquippableItem returns 1 or nil. C_Item.IsEquippableItem returns true or false.

IsEquippableItem(12640)          -- Lionheart Helm → 1
C_Item.IsEquippableItem(2512)    -- Rough Arrow → true (ammo)
C_Item.IsEquippableItem(6948)    -- Hearthstone → false

IsUsableItem(item) / C_Item.IsUsableItem(item)

Returns (usable, noMana): can the player use the item now? This is the item version of IsUsableSpell. It answers "can I click this item now", not "can I equip it". For equip requirements, use C_PlayerInfo.CanUseItem.

The global IsUsableItem(item) returns 1 or nil for each value, like IsUsableAction. C_Item.IsUsableItem(item) returns booleans. item is an itemID number or an "item:N..." link.

IsUsableItem(6948)                     -- Hearthstone in your bags → 1, nil
local usable, noMana = C_Item.IsUsableItem(13446)  -- Major Healing Potion
-- usable=true,  noMana=false → you can use it now
-- usable=false, noMana=true  → only the power cost is missing
-- usable=false, noMana=false → not carried, effect still active,
--                              dead, in a form that blocks it, and more

The function makes these checks, in this order:

  1. You carry the item: equipped or in bags 0 to 4. An item that you do not have is not usable.
  2. The on-use effect is not held. Some effects stay unusable while they are active, and their cooldown starts only when the effect ends. A cooldown that is running does not make the item unusable.
  3. If the item has no on-use effect, it is usable while you control your character.
  4. Otherwise, the result is the same test that IsUsableSpell uses for the on-use spell. noMana is true only when the power cost is the one requirement that fails.

The item's level, class and race requirements are not part of this test. If you use an item that you cannot use, the game shows its usual error.

Like C_Item.IsEquippableItem, this function only reads the item cache. An item that is not cached returns (nil/false, nil/false), and the function does not load it. To wait for the data, call C_Item.RequestLoadItemDataByID first, then check again on ITEM_DATA_LOAD_RESULT.

IsEquippedItemType(type)

Tells if you have an item of the given type equipped. Returns 1 or nil. A broken item does not count.

type is one of these, without regard to case:

  • An equip location key, for example "INVTYPE_SHIELD".
  • The name of an equip location, for example "Head" or "Off Hand".
  • An item class name, for example "Weapon" or "Armor".
  • An item subclass name, for example "Shields", "Plate", or "Sword". Both the long and the short subclass names match.

An empty or missing type causes an error. The [equipped:type] macro condition uses this function.

IsEquippedItemType("INVTYPE_HEAD")   -- a helm is equipped → 1
IsEquippedItemType("Shields")        -- no shield equipped → nil
IsEquippedItemType("Weapon")         -- → 1

IsCurrentItem(item)

Tells if you are using the item now. Returns 1 while the cast of the item's use effect runs, else nil. The action button of the item shows its checked state for the same time.

item is an item ID, an item link, or the name of an item that you carry.

-- During the Hearthstone cast:
IsCurrentItem(6948)            -- → 1
IsCurrentItem("Hearthstone")   -- → 1

IsDressableItem(item)

Tells if you can try on the item in the dressing room. Returns 1 or nil. These items are dressable:

  • An item that goes in an equipment slot.
  • A recipe that teaches you to make an item that goes in an equipment slot.

item is an item ID, an item link, or the name of an item that you carry. If the item, or the item that the recipe makes, is not in the item cache, the result is nil. The function does not start a load.

IsDressableItem(12640)                    -- Lionheart Helm → 1
IsDressableItem("Plans: Lionheart Helm")  -- → 1
IsDressableItem(6948)                     -- Hearthstone → nil

IsHelpfulItem(item) / IsHarmfulItem(item)

Tell if the use effect of the item aims at a friend (IsHelpfulItem) or at an enemy (IsHarmfulItem). Each returns 1 or nil. They give the same result as IsHelpfulSpell / IsHarmfulSpell for the use spell of the item. An item with no use effect returns nil from both.

item is an item ID, an item link, or the name of an item that you carry. If the item is not in the item cache, both return nil. The functions do not start a load.

IsHelpfulItem(118)    -- Minor Healing Potion → 1
IsHarmfulItem(118)    -- → nil
IsHarmfulItem(4358)   -- Rough Dynamite → 1

C_Item.IsEquippedItem(item)

Returns true if item is currently equipped in any of the 19 character-pane slots, false otherwise. Walks slots 1..19 in order and short-circuits on the first match.

item accepts the same shapes as GetItemInfo:

Form Example Match strategy
itemID 2589 exact itemID equality
bare link "item:2589:0:0:0" parses the first numeric field
chat link \124cffffffff\124Hitem:2589:0:0:0\124h[Linen Cloth]\124h\124r extracts itemID after \124Hitem:
name "Linen Cloth" case-insensitive match against each equipped item's decorated name (random suffix included)

Returns false (no Lua error) for invalid input — nil, empty string, or a string that doesn't parse as any of the above.

Name matching is against the item's decorated name: a random-suffix item matches only its full name ("Krol Blade of the Bear"), not the base ("Krol Blade"). Unsuffixed items match their plain name. The candidate must be in the client item cache; equipped items always are, so that's a non-issue in practice. This match logic is shared with the by-name action APIs (C_Item.UseItemByName / EquipItemByName) — one predicate, one set of semantics.

if C_Item.IsEquippedItem(2589) then
    -- Linen Cloth is equipped (silly example — pick a wearable item)
end

-- From a chat link click:
if C_Item.IsEquippedItem(itemLink) then ...

-- By localized name:
if C_Item.IsEquippedItem("Thunderfury, Blessed Blade of the Windseeker") then ...

C_Item.IsItemDataCachedByID(item) / C_Item.IsItemDataCached(itemLocation)

Returns true if the item's static data is currently in the client-side item cache, false otherwise. The "ByID" variant takes an itemID or "item:NNN"-style string; the location variant takes the {equipmentSlotIndex=} / {bagID=, slotIndex=} table.

These read the cache without firing a server query — pair with RequestLoadItemData(ByID) if you need to ensure the data is loaded before checking.

if not C_Item.IsItemDataCachedByID(itemID) then
    C_Item.RequestLoadItemDataByID(itemID)
    -- (poll IsItemDataCachedByID on a timer until true)
end

C_Item.IsItemGUIDInInventory(itemGUID)

Returns whether the item with the given "0x…" GUID is currently carried by the player — the 19 equipment slots plus the bags (backpack + the four equipped bags). Equipped items count; the bank does not.

local g = C_Item.GetItemGUID({bagID = 0, slotIndex = 1})
C_Item.IsItemGUIDInInventory(g)   -- true
C_Item.IsItemGUIDInInventory(
    C_Item.GetItemGUID({equipmentSlotIndex = 1}))   -- true (equipped counts)

Accepts the GUID string C_Item.GetItemGUID / UnitGUID return; returns false for a malformed or zero GUID. To also cover the bank, use C_Item.GetItemCount(itemID, true) > 0 instead.

C_Item.ItemHasRange(item) / ItemHasRange(item)

Whether the item has a range restriction — i.e. its on-use spell reports a nonzero min or max range through the engine's own resolver. Items with no on-use spell, or whose spell is rangeless (potions, food, self-buff trinkets, hearthstone), have none.

item is an itemID, "item:NNN" string, or item link, same as C_Item.IsItemInRange. Uncached items read as having no range and fire no load.

The two forms return different types. ItemHasRange returns 1 or nil, like 3.3.5. C_Item.ItemHasRange returns true or false.

ItemHasRange(1251)               -- Linen Bandage → 1 (has a range)
C_Item.ItemHasRange(118)         -- Minor Healing Potion → false
C_Item.ItemHasRange(6948)        -- Hearthstone → false

C_Item.IsItemInRange(item, targetUnit)

Returns true if the item is in range of targetUnit, false if out of range, and nil when the range check doesn't apply — the item has no on-use spell, its on-use spell is rangeless, or the item / unit can't be resolved. Returns nil for items with no range restriction.

item is an itemID, "item:NNN" string, or item link (item names aren't resolvable — there is no name→ID map — and return nil). targetUnit is a unit token.

-- a targeted on-use item (net / bomb / thrown / targeted quest item):
C_Item.IsItemInRange(itemID, "target")   -- true / false
-- rangeless / self-use item:
C_Item.IsItemInRange(118, "target")       -- Minor Healing Potion → nil

An item's range comes from the spell it fires on use, so this resolves the item to its on-use spell (C_Item.GetItemSpell's spell) and runs the exact same range test as C_Spell.IsSpellInRange — the two agree for the same underlying spell. Like the spell version, it returns nil for a unit that the on-use spell cannot be cast on, and it does not check line of sight. Passive reader — an item not yet in the client item cache returns nil with no background fetch (items you'd range-check are normally in bags / on the action bar and already cached). Absent tokens (e.g. "target" with no target) return nil; a genuinely unrecognized token string raises a Lua error, same contract as C_Spell.IsSpellInRange.

C_Item.IsItemOpenable(itemLocation)

Returns two values: (isOpenable, canOpen).

  • isOpenable — true if the item is right-click-openable (sack, clam, simple chest, quest box, lockbox, wrapped gift — anything whose tooltip could show <Right Click to Open>). Unused wrapping paper is not openable.
  • canOpen — true if the player can right-click this specific instance and trigger the open action right now. For unlocked-by- default items (clams, sacks) this matches isOpenable. For items with a lock (lockboxes), it's only true after the lock has been removed (rogue Pick Lock, key item, etc.) — so a priest looking at a fresh lockbox sees (true, false), while a rogue who just picked it sees (true, true). A wrapped gift is always (true, true).

Both values are nil when the item data isn't cached yet (background SMSG_ITEM_QUERY_SINGLE fired) or when the location is empty / invalid. Lets callers distinguish "data unknown" from "definitely not openable".

There's no ByID variant — the canOpen check depends on a specific instance's ITEM_FIELD_FLAGS, which you can only have via an itemLocation. If you only have an itemID, use C_Item.GetItemInfoInstant to inspect the item type instead.

-- Loop a bag and gather every openable the player can act on now
for slot = 1, GetContainerNumSlots(0) do
    local _, canOpen = C_Container.IsContainerItemOpenable(0, slot)
    if canOpen then
        -- pickup + use; this slot is good to open
    end
end

-- Show a different hint depending on the lock state
local isOpenable, canOpen = C_Item.IsItemOpenable({bagID = 0, slotIndex = 1})
if isOpenable and not canOpen then
    print("This item is locked.")
end

C_Item.IsLocked(itemLocation)

Returns true if the item is in the client-side "in-transaction" lock state — picked up onto the cursor, mail-attached, trade-attached, mid-swap, etc. Reads the per-CGItem instance flag at +0x314 bit 0 (not the ITEM_FIELD_FLAGS descriptor field — the actual lock is on the instance, not in m_objectFields).

if C_Item.IsLocked({bagID = 0, slotIndex = 1}) then
    -- skip the action; slot is mid-transaction
end

The lock is client-managed, server-confirmed:

  • Engine sets the bit optimistically in pickup/equip/attach paths, before the transaction packet goes to the server. UI greys immediately, no round-trip wait.
  • Engine clears the bit when the matching SMSG_UPDATE_OBJECT confirms the transaction.

Listen for the engine's ITEM_LOCK_CHANGED event (no payload — the engine fires it on every set/clear and addons re-poll the items they care about) to react without timed polling.

C_Item.LockItem(itemLocation)

Set the client-side lock on a single item by location. Calls the same engine primitive (FUN_ITEM_LOCK_BY_GUID = 0x004953E0) the engine itself uses optimistically before sending pickup/equip transactions. Fires ITEM_LOCK_CHANGED the same way.

C_Item.LockItem({bagID = 0, slotIndex = 1})

Setting the lock doesn't make anything real happen on the server. It's a pure client-side flag — the player's UI greys the item out until something clears it. No transaction packet is sent, no cancel-on-failure mechanism kicks in. Treat this as a UI hint for custom workflows (batch-equip preview, drag-target highlighting, etc.), not real transaction state.

The lock stays until you call UnlockItem / UnlockAllItems, OR the engine receives an SMSG_UPDATE_OBJECT for the item from the server, which clears it as a side effect.

C_Item.LockItemByGUID(itemGUID)

Same as LockItem but takes a GUID string in the canonical "0xHHHHHHHHLLLLLLLL" format C_Item.GetItemGUID returns. The engine resolves through the object manager, so this can mark items the player doesn't currently have in bags/equipment — trade window contents, mail attachments, freshly looted but not yet bagged — as long as the CGItem is loaded.

local guid = C_Item.GetItemGUID({equipmentSlotIndex = INVSLOT_HEAD})
C_Item.LockItemByGUID(guid)

Same caveats as LockItem — purely client-side, doesn't affect any real engine transaction state.

C_Item.PickupItem(itemInfo)

Finds the item matching itemInfo and picks it up onto the cursor — the C-side equivalent of clicking it. From there the held item can be dropped into a bag/equipment slot, deleted (DeleteCursorItem), sold at a merchant, etc.

itemInfo accepts the same shapes as C_Item.EquipItemByName — itemID number, bare "item:N" string, full chat link, or a localized item name (matched case-insensitively against each candidate's decorated name, random suffix included). Unlike the by-name equip/use functions, the search covers equipment (slots 1..19, checked first) and then bags (0..4), so PickupItem picks up a worn item as readily as a bagged one.

Returns nothing. Silently no-ops when:

  • the input is nil, an empty string, or otherwise unparseable
  • no matching item is found in equipment or bags
  • the cursor is already holding something (CursorHasItem() is true)
  • the matched item is locked (mid-transaction)

Drives the engine's cursor primitives directly — the paperdoll-slot pickup (FUN_004C7300) for an equipped match, the container pickup (FUN_00494B60 + client-side item lock) for a bag match — not the Lua PickupInventoryItem / PickupContainerItem globals. Same shared Item::Cursor wrappers EquipItemByName's auto-slot path uses.

-- Pick up a bagged item by name, then drop it into the off-hand slot:
C_Item.PickupItem("Linen Cloth")
PickupInventoryItem(17)  -- or EquipCursorItem(), etc.

-- Pick up a worn item by ID (unequips onto the cursor):
C_Item.PickupItem(2589)

-- From a chat link:
C_Item.PickupItem(itemLink)

C_Item.RequestLoadItemDataByID(item) / C_Item.RequestLoadItemData(itemLocation)

Asks the engine to fetch the item's data from the server if not already cached. Returns true if the request was initiated (or the input was parseable to an itemID), false for malformed input. Fire-and-forget — the engine handles the round-trip; the data lands in the cache when the server responds.

Fires ITEM_DATA_LOAD_RESULT(itemID, success) when the data lands in the cache. Synchronously fired when the item was already cached (so polling code paths still work), asynchronously fired when the engine's SMSG response handler completes a network fetch.

success is 1 or nil. The engine's printf-style event dispatcher has no %b token, so we encode the boolean as 1 for success / nil for failure (leaning on lua_pushstring(NULL) → lua_pushnil). if success then … distinguishes them correctly — nil is falsy, 1 is truthy. Same encoding as GET_ITEM_INFO_RECEIVED and QUEST_DATA_LOAD_RESULT.

local f = CreateFrame("Frame")
f:RegisterEvent("ITEM_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()
    -- vanilla 1.12: event payload is in `arg1`, `arg2`, ... globals
    if event == "ITEM_DATA_LOAD_RESULT" and arg2 then
        local _, type = C_Item.GetItemInfoInstant(arg1)
        -- ...
    end
end)
C_Item.RequestLoadItemDataByID(2589)

Engine-aligned cache strategy. RequestLoadItemData does NOT eagerly create a cache entry. Empirically (e.g. pfQuest calling it at addon-load / PLAYER_ENTERING_WORLD against a cleared WDB), pre- creating the entry races the engine's natural inventory prefetch and can leave the item permanently pending — the engine sees the entry exists and skips its own SMSG_ITEM_QUERY_SINGLE, while our early query gets dropped. Instead we track the itemID and hook the engine's SMSG_ITEM_QUERY_SINGLE_RESPONSE handler at 0x0055BDB0: when the engine fills any entry, we sweep tracked items and fire ITEM_DATA_LOAD_RESULT for matches. For items the engine doesn't prefetch (quest rewards, AH browse targets, etc.) we escalate after ~60 ticks by calling CacheFetch ourselves — by then engine state has settled and our query goes through normally. Hard timeout at ~1200 ticks (~20–40s) fires ITEM_DATA_LOAD_RESULT(itemID, nil).

C_Item.UnlockAllItems()

Sweep clear of every CGItem the engine knows about — same primitive the engine itself runs on PLAYER_LEAVING_WORLD. One ITEM_LOCK_CHANGED fires at the end.

C_Item.UnlockAllItems()

Cheapest recovery if you don't know which item is stuck. Same caveat as UnlockItem: this is purely client-side state — it won't cancel any pending server-side transactions.

C_Item.UnlockItem(itemLocation)

Force-clear the client-side lock on one item. Recovery primitive — fires ITEM_LOCK_CHANGED so the UI refreshes.

C_Item.UnlockItem({bagID = 0, slotIndex = 1})

Useful when a transaction packet was sent but the server's confirmation never arrived: the item stays visually greyed indefinitely (the only built-in trigger for the unlock-all sweep is logout). This call gives you an in-session escape hatch.

Cursor / server state isn't touched. Unlocking doesn't tell the server "cancel my transaction" — it only clears the local visual lock. If the item is genuinely mid-flight, the server's next update will set the lock right back. For cursor-cancel semantics, pair with the engine's ClearCursor().

C_Item.UseAtCursor(item)

Uses item at the world position under your cursor. This is the ClassicAPI form of [@cursor] for on-use items with a ground target, for example Iron Grenade or a demolition charge. You do not click the reticle.

item takes either of two forms:

  • An item reference: an itemID, bare "item:N", a full chat link, or a localized name — the shapes C_Item.UseItemByName takes. The first matching item in your bags is used.
  • An item location: {bagID = B, slotIndex = S}, {equipmentSlotIndex = N}, or an item GUID string — the shapes C_Item.GetItemID takes. A location names one exact item, so use it when you hold two stacks of the same thing and the one you mean matters.
C_Item.UseAtCursor(4068)            -- Iron Grenade at cursor
C_Item.UseAtCursor("Iron Grenade")
C_Item.UseAtCursor({ bagID = 0, slotIndex = 1 })

Returns true when the effect of the item lands at the cursor. Returns false when:

  • the cursor is not on the ground, for example over a frame or off the screen
  • the item has no ground target
  • the item needs another target in addition to the ground position
  • you do not carry the item, or the argument is not an item
  • the game refuses the use, for example while you are shapeshifted

If the cursor is not on the ground, the reticle is cancelled and the item is not used, so an off-screen call does not waste a grenade. An item with no ground target is used normally, on your current target.

A reticle that is already up when you call this function is cancelled first. The function only places the reticle that its own call opens.

C_Item.UseAtUnit(item, unit [, placeGroundSpell])

Uses item at unit. This is the item form of C_Spell.CastAtUnit, and it covers both kinds of on-use item:

  • An item with a ground target, for example Iron Grenade, lands at the feet of the unit. You do not click the reticle.
  • An item with a unit target, for example a bandage, is used on the unit. Your current target does not change.

item accepts the same two forms as C_Item.UseAtCursor. unit is any unit token ("player", "target", "mouseover", "party1", and more).

C_Item.UseAtUnit(4068, "target")            -- Iron Grenade at the target's feet
C_Item.UseAtUnit("Iron Grenade", "player")
C_Item.UseAtUnit({ bagID = 0, slotIndex = 1 }, "player")
C_Item.UseAtUnit("Linen Bandage", "party1") -- bandage party1

placeGroundSpell is true by default. If it is false, an item with a ground target opens its reticle for you to click, and the function does not place it.

Returns true when the item went out at the unit. With placeGroundSpell set to false, a reticle left up for you also returns true. Returns false when:

  • you do not carry the item, or the argument is not an item
  • the game refuses the use
  • the unit is absent
  • the unit is not a target that the item accepts

The function reads the unit before it uses the item. As a result, an absent unit fails and the item is not used.

When placeGroundSpell is true, a reticle that is already up is cancelled first. The function only places the reticle that its own call opens.

A unit token that names nothing at the moment, such as "party3" while you are solo, returns false. A string that is not a unit token raises the standard "Unknown unit" error, as UnitHealth("garbage") and C_Spell.CastAtUnit do. That differs from C_Item.UseItemByName, which does nothing for an argument it cannot use.

C_Item.UseItemByName(item [, unit])

Uses item. Returns nothing; silently no-ops when:

  • the input is nil, an empty string, or otherwise unparseable
  • the player does not have the item
  • the engine refuses the use — cooldown, locked item, level requirement, etc.

item takes either of two forms:

  • An item reference: an itemID number, bare "item:N" string, a full chat link, or a localized item name — the shapes C_Item.EquipItemByName takes. The first matching item in your bags is used. Name matching is case-insensitive against each candidate's decorated name (random suffix included), the same shared predicate C_Item.IsEquippedItem uses.
  • An item location: {bagID = B, slotIndex = S}, {equipmentSlotIndex = N}, or an item GUID string — the shapes C_Item.GetItemID takes. A location names one exact item, so use it when you hold two stacks of the same thing and the one you mean matters.

The optional unit argument is a unit token ("player", "target", "focus", "partyN", "raidN", "nameplateN", …) used as the cast target for items that fire a spell (scrolls, traps, on-use targeted effects). For self-use items (hearthstone, potions, food) the engine overwrites the target with the item's own GUID before dispatch, so passing a unit to those is harmless and has no effect. Unrecognized strings are treated as "no target" rather than raising, matching the silently-no-op contract of item.

C_Item.UseItemByName("Hearthstone")                       -- hearth home
C_Item.UseItemByName(6948)                                -- same thing, by ID
C_Item.UseItemByName("Major Healing Potion")
C_Item.UseItemByName("Scroll of Stamina IV", "target")    -- buff your tank
C_Item.UseItemByName({ bagID = 0, slotIndex = 1 })        -- that exact slot

Locates the item directly, then hands the CGItem * to the engine's by-pointer use primitive at 0x005D8D00. That primitive dispatches internally based on item type (food, potion, on-use spell, scroll, quiver, ...) so a single call covers every item category. We skip Script_UseContainerItem entirely — its branches for repair vendor, spell-cast targeting, and drop-on-bag cursor modes don't apply to an addon-issued call from a clean cursor.

The engine ships ~14 Get*ItemLink functions covering every frame that lets you mouse over an item (loot, merchant, quests, auction, trade, mail, tradeskill, craft). To get the itemID, the standard pattern is to call the Link function and scrape the number out of the returned string with gsub / match. Direct-ID accessors exist for only a handful of these (GetLootSlotItemID, GetInboxItemID, GetQuestItemID, GetMerchantItemID, …).

These add the missing ID companions, so the whole Get*ItemLink family has a 1-to-1 ID companion. Each reads the same itemID the engine reads when building the link, with no string parsing required.

All return nil for an empty slot, an out-of-range index, or when the relevant UI frame isn't open.

Function Companion to Notes
GetLootRollItemID(rollID) GetLootRollItemLink Group-loot roll ID (from START_LOOT_ROLL). Walks the engine's in-progress roll list.
GetLootSlotItemID(slot) GetLootSlotLink 1-based slot. Returns nil for coin slots.
GetMerchantItemID(index) GetMerchantItemLink 1-based; the active merchant's inventory.
GetQuestItemID(type, index) GetQuestItemLink type is "reward" or "choice"; active quest-offer / completion UI.
GetQuestLogItemID(type, index) GetQuestLogItemLink Same args; reads the currently selected quest-log entry.
GetTradePlayerItemID(slot) GetTradePlayerItemLink 1..7; your side of the trade window.
GetTradeTargetItemID(slot) GetTradeTargetItemLink 1..7; the other player's side.
GetAuctionItemID(type, index) GetAuctionItemLink type is "list", "owner", or "bidder".
GetAuctionSellItemID() GetAuctionSellItemInfo The item currently in the sell slot. No args.
GetInboxItemID(mailID) GetInboxItem 1-based mail index; nil for gold-only mail.
GetTradeSkillItemID(index) GetTradeSkillItemLink The recipe's created item itemID.
GetTradeSkillReagentItemID(index, reagentIndex) GetTradeSkillReagentItemLink reagentIndex is 1-based; reagents are densely packed (no skipping empty slots).
GetCraftReagentItemID(craftIndex, reagentIndex) GetCraftReagentItemLink Craft frame (enchanting / beast training) — same shape as tradeskill reagents.
GetCraftSpellID(craftIndex) GetCraftItemLink The craft frame's link is Hspell:, not Hitem: — so the companion is a spellID, not an itemID. Addons scraping for itemID get nil today; this returns the actual identifier.
-- Drop-in for the typical scrape:
--   local id = tonumber((GetLootRollItemLink(rollID)):match("item:(%d+)"))
local id = GetLootRollItemID(rollID)

-- Plays nice with the existing data APIs:
if id then
    local _, _, _, _, _, classID = C_Item.GetItemInfoInstant(id)
    -- ...
end

Quest UI distinction. GetQuestItemID reads the active quest offer (the QuestFrame you see when accepting/turning in a quest); GetQuestLogItemID reads the currently-selected entry in the quest log. The two cover different states intentionally — the same split as GetQuestItemLink / GetQuestLogItemLink.

GetTradeSkillItemID vs GetCraftSpellID. Recipes are split across two UI frames: TradeSkill (smithing / alchemy / tailoring / etc., which always produce a finished item) and Craft (enchanting formulas / beast-training tomes, where the spell IS the deliverable). GetTradeSkillItemID returns the produced itemID; GetCraftSpellID returns the recipe spellID — because there's no produced item to point at.

GetAverageItemLevel()

Returns avgItemLevel, avgItemLevelEquipped.

  • avgItemLevel is the average item level of the best gear that the player owns and can use. The function looks at equipped items, the bags, the bank and the bank bags. The bank window does not have to be open.
  • avgItemLevelEquipped is the average item level of the items that the player wears.
local overall, equipped = GetAverageItemLevel()

The function counts an item only if the player can use it. An item that is red in its tooltip does not count. Shirts, tabards, bags, quivers and ammo do not count.

The function keeps the best item for each kind of slot. For rings, trinkets and one-hand weapons it also keeps the second best, because the player can wear two of them. An item counts in one slot only.

The average uses the better of two weapon choices:

  • One-hand: the main hand and the off hand, divided over 17 slots.
  • Two-hand: the best two-hand weapon, divided over 16 slots.

The main hand and the off hand come from the weapons that the player can hold together. If the player can dual wield, the off hand can be a second one-hand weapon. If not, the off hand is a shield or an off-hand item.

An empty slot counts as item level 0. When you remove an item, the average goes down.

Item data that is not loaded. If the data for an item is not loaded, the function requests it. Until all requested data loads, the function returns the last full result, or 0, 0 before the first one. When the data loads, PLAYER_AVG_ITEM_LEVEL_READY fires. Call the function again then.

GetInventoryItemDurability(invSlot)

Returns (current, maximum) durability for the player's equipped item at invSlot (1-based, character-pane slots 1..19), or nothing if the slot is empty or the item has no durability concept (rings, trinkets, necks, backs, shirts, tabards, etc.).

current, maximum = GetInventoryItemDurability(invSlot)
local cur, max = GetInventoryItemDurability(INVSLOT_CHEST)
if cur then
    -- cur, max are positive integers (e.g. 65, 65 for full chest)
end

-- Items without durability return nothing — both locals are nil:
local cur, max = GetInventoryItemDurability(INVSLOT_FINGER1)
-- cur == nil, max == nil

Player-only. GetInventoryItemDurability takes only the slot, no unit token. Inspect targets / party members' equipment durability isn't broadcast, so we couldn't expose it for other units even if we wanted to.

(0, max) vs nothing. Items that have a durability concept but are currently broken (current == 0, max > 0) still return (0, max). The "nothing" return is reserved for items that have no durability fields populated at all — max is the discriminator, matching the engine's own GetInventoryItemBroken logic.

Reads ITEM_FIELD_DURABILITY (+0xA0) and ITEM_FIELD_MAXDURABILITY (+0xA4) directly off the CGItem's m_objectFields descriptor at +0x114 — same descriptor C_Item.IsBound reads FLAGS from. No DBC indirection.

GetInventoryItemID(unit, slot)

Returns the itemID of the item equipped at slot (1-based) on unit, or nil if the slot is empty / the unit isn't valid / the unit doesn't expose its equipment to the local client. Same arg shape as 1.12's GetInventoryItemLink(unit, slot) — drop-in for code that just needs the ID and would otherwise have to parse the link string.

  • For "player" (and any token resolving to the local player, e.g. "target" when self-targeted): walks the private inventory manager. Supports the full slot range (equipment 1..19, bag slots 20..23, bank slots, etc.) — same range GetItemBySlot accepts internally.
  • For other player-controlled units ("target", "party1"..party4", inspect targets): reads the unit's broadcast visible-items array. Equipment slots 1..19 only.
  • For NPCs / creatures: returns nil. The visible-items array isn't populated for non-player-controlled units in 1.12, so we gate this on UnitPlayerControlled to avoid the engine crash that GetInventoryItemLink itself would trigger on the same input.
local id = GetInventoryItemID("player", INVSLOT_HEAD)
if id then
    local _, type, subtype = C_Item.GetItemInfoInstant(id)
    -- ...
end

-- Inspect a party member without parsing a hyperlink:
local headID = GetInventoryItemID("party1", INVSLOT_HEAD)

GetInventoryItemRepairCost(invSlot)

Returns the cost in copper to repair the player's equipped item at invSlot. Same value GameTooltip:SetInventoryItem returns as its third out-parameter.

copperCost = GetInventoryItemRepairCost(invSlot)
local cost = GetInventoryItemRepairCost(INVSLOT_CHEST)
if cost > 0 then
    -- e.g. cost == 12345
end

Returns 0 for slots that are empty, items without a durability concept, fully-repaired items, or items whose stats aren't cached yet (rare — happens briefly after login before SMSG_ITEM_QUERY_RESPONSE arrives for newly-seen items). The "no return" path is reserved for invalid input (non-numeric slot).

Player-only, same constraint as GetInventoryItemDurability — other units' durability isn't broadcast in 1.12.

Discount is vendor-context-dependent. The faction-rep + PvP rank discount is only applied when the player has a merchant window open (i.e. has received SMSG_LIST_INVENTORY). Called from anywhere else, you get the raw, undiscounted base cost.

The engine tracks the current merchant via globals DAT_00BDDFA0/A4 (the merchant's GUID), set when the merchant frame opens and zeroed when it closes. The helper short-circuits the discount path when those globals are zero.

For consistent "what will I pay" semantics inside addons, only trust this value when MerchantFrame:IsShown() is true — or call it once per merchant-visit and cache.

Calls the engine's own per-item repair-cost helper at 0x004FAF30, which is the same function Script_GameTooltip_SetInventoryItem calls for its repairCost return. The raw cost comes from DurabilityCosts.dbc (indexed by item subclass and slot type) × DurabilityQuality.dbc (indexed by item quality); the discount — when applicable — stacks faction reputation with the merchant (Friendly+ unlocks a base 5%) and PvP rank (Sergeant Major+ adds another 5%, Knight-Lieutenant+ stacks one more).

This is a ClassicAPI addition — there is no standalone Lua function for per-item repair cost otherwise; the only other way to read it is the tooltip's third return value. We expose it directly because the underlying calculation is already in the engine and a Lua function is the natural surface.

GetInventoryItemsForSlot(slot, returnTable [, transmogrify])

Populates returnTable with every item in the player's equipment and bags that's eligible to be equipped in slot (1..19). Returned table is keyed by the packed ItemLocation bitfield — same encoding C_EquipmentSet.GetItemLocations uses — with the itemLink as the value. Returns the same returnTable reference for chaining.

local t = {}
GetInventoryItemsForSlot(16, t)   -- mainhand
for loc, link in pairs(t) do
    -- loc is a number like 0x00300103; link is "|c…|Hitem:…|h[…]|h|r"
end
slot Eligible InventoryTypes
1 HEAD, 2 NECK, …, 10 HANDS The slot's matching InventoryType
11, 12 FINGER INVTYPE_FINGER (11) — items appear under both ring slots
13, 14 TRINKET INVTYPE_TRINKET (12)
15 BACK INVTYPE_CLOAK (16)
16 MAINHAND INVTYPE_WEAPON (13), INVTYPE_2HWEAPON (17), INVTYPE_WEAPONMAINHAND (21)
17 OFFHAND INVTYPE_WEAPON (13), INVTYPE_SHIELD (14), INVTYPE_WEAPONOFFHAND (22), INVTYPE_HOLDABLE (23)
18 RANGED INVTYPE_RANGED (15), INVTYPE_THROWN (25), INVTYPE_RANGEDRIGHT (26)
5 CHEST INVTYPE_CHEST (5), INVTYPE_ROBE (20)
19 TABARD INVTYPE_TABARD (19)

Compatibility check is a single bitwise AND against a static invType → slotMask table, with two adjustments:

  • 2H weapons (INVTYPE_2HWEAPON) confined to the mainhand slot only. There is no titanic-grip mechanic, and the server would reject an offhand equip anyway.
  • INVTYPE_WEAPONMAINHAND / WEAPONOFFHAND confined to their literal slot rather than "either hand".

The transmogrify arg is accepted and ignored. There is no transmog system to query against, and the flag's effect (broader cross-eligibility for visual swaps) reduces to the regular equip check here. Private servers like Turtle WoW layer their own transmog systems on top; this function doesn't currently plumb the flag through to any of them.

Bank items aren't included. The engine's GetItemBySlot gates bank slots on a banker GUID that's only set while the bank window is open (bank shows up only when the bank UI is open). We don't walk bank slots — addons that want bank inclusion need their own bank scan with the bank UI open.

SetInventoryPortraitTexture(texture, unit, slot)

Sets a texture to the icon of the item in an inventory slot of a player, cut to a round portrait.

SetInventoryPortraitTexture(myTexture, "player", 16)   -- main hand
  • unit must be a player. For other units, the texture is cleared.
  • slot is an inventory slot number, the same number that GetInventoryItemTexture takes. 0 is the ammo slot.
  • If the slot is empty, the texture is cleared.
  • A slot that is not an inventory slot causes an error. The error messages spell the name as SetInventoryPortaitTexture.

GetItemIcon(itemID) / C_Item.GetItemIcon(itemLocation) / C_Item.GetItemIconByID(item)

Three accessors that all return the icon path for an item, differing only in how you address the item:

Function Input
GetItemIcon(itemID) numeric itemID (global)
C_Item.GetItemIcon(itemLocation) {equipmentSlotIndex=N} or {bagID=B, slotIndex=S}
C_Item.GetItemIconByID(item) numeric itemID OR "item:NNN" string (full chat links work)

All three return the icon path string (e.g. "Interface\\Icons\\INV_Misc_Rune_01"), or nil if the item isn't in the client-side cache, the slot is empty, or the display info record is missing. The path is suitable for direct use with texture:SetTexture(...).

local path = GetItemIcon(6948)                                -- "Interface\\Icons\\INV_Misc_Rune_01"
local path = C_Item.GetItemIcon({equipmentSlotIndex = INVSLOT_HEAD})
local path = C_Item.GetItemIconByID("|cff...|Hitem:6948:0:0:0|h[Hearthstone]|h|r")

iconID-vs-path deviation. This value can be a fileID:number elsewhere. There is no fileID system here — same situation as C_Spell.GetSpellTexture and C_Spell.GetSpellInfo's iconID field. We surface the path string everywhere for consistency.

Equivalent to the global GetItemIcon and the C_Item.GetItemIcon / GetItemIconByID family.

IsDualWielding()

Returns true if the player can dual wield and has a weapon in the off-hand slot. Otherwise it returns false.

if IsDualWielding() then ... end

A shield or a held item (tome, orb, libram) in the off hand does not count. The result is also false until the client has the off-hand item's data.

Item objects

These functions make an item object (ItemMixin):

  • Item:CreateFromItemID(itemID)
  • Item:CreateFromItemLink(itemLink)
  • Item:CreateFromItemGUID(itemGUID)
  • Item:CreateFromBagAndSlot(bagID, slotIndex)
  • Item:CreateFromEquipmentSlot(equipmentSlotIndex)
  • Item:CreateFromItemLocation(itemLocation)

Its methods read the item through C_Item. Some of them are GetItemID, GetItemName, GetItemLink, GetItemIcon, GetItemQuality and GetStackCount. The methods that read the item's data return nil until that data is in the cache.

When the load of the item's data ends, item:ContinueOnItemLoad(callback) calls callback(itemID, success). If the data is already in the cache, the call occurs immediately. If the data loaded, success is 1. If the load fails (for example, the server has no item with this ID), the callback also occurs, with success set to nil. If the item is empty (for example, an empty bag slot), ContinueOnItemLoad raises an error.

item:ContinueWithCancelOnItemLoad(callback) does the same, and returns a function. A call to that function cancels the callback before it occurs.

local item = Item:CreateFromItemID(19019)
item:ContinueOnItemLoad(function(itemID, success)
    if success then
        print(item:GetItemLink())
    end
end)

ItemEventListener and QuestEventListener are the listeners behind these callbacks. When the data of a quest loads, QuestEventListener:AddCallback(questID, callback) calls callback(questID, success). If the load fails, the callback does not occur.

ItemUtil

Helper functions for items, bags and equipment slots, backported from FrameXML. Each itemLocation is an ItemLocation object (see itemLocation argument shapes).

ItemUtil.GetItemDetails returns itemName, itemTexture, quantity, itemRarity, itemLink from C_Item.GetItemInfo. It returns quantity as you gave it. ItemUtil.GetItemHyperlink returns only the item link. Both return nil for an item that is not in the cache.

ItemUtil.PickupBagItem(itemLocation)

Picks up the item at a bag location, with C_Container.PickupContainerItem. An equipment location does nothing.

ItemUtil.IterateBagSlots(bag, callback) / ItemUtil.IterateInventorySlots(firstSlot, lastSlot, callback)

These functions call callback(itemLocation) for each slot that holds an item, in slot order. When callback returns a true value, the walk stops.

  • ItemUtil.IterateBagSlots walks one bag. If callback stopped the walk, it returns true. Otherwise it returns false.
  • ItemUtil.IterateInventorySlots walks the equipment slots from firstSlot to lastSlot. It returns nothing.

ItemUtil.IteratePlayerInventory(callback) / ItemUtil.IteratePlayerInventoryAndEquipment(callback)

ItemUtil.IteratePlayerInventory walks the backpack, the equipped bags and then the keyring, with the same callback rule as ItemUtil.IterateBagSlots. If callback stopped the walk, it returns true.

ItemUtil.IteratePlayerInventoryAndEquipment walks the same bags. If callback does not stop the walk, it then walks the equipped slots (INVSLOT_FIRST_EQUIPPED to INVSLOT_LAST_EQUIPPED). It returns nothing.

ItemUtil.IteratePlayerInventory(function(itemLocation)
    print(C_Item.GetItemLink(itemLocation))
end)

ItemUtil.FilterOwnedItems(itemIDs)

Returns a new array with the IDs from itemIDs that are in the player's bags or keyring. Equipped items do not count.

/dump ItemUtil.FilterOwnedItems({6948, 19019})
-- {6948} with a Hearthstone in the bags

These functions return a new array of Item objects, one for each entry of the input array:

  • ItemUtil.TransformItemIDsToItems(itemIDs)
  • ItemUtil.TransformItemGUIDsToItems(itemGUIDs)
  • ItemUtil.TransformItemLocationsToItems(itemLocations)
  • ItemUtil.TransformItemLocationItemsToGUIDItems(itemLocations), which gives the same result as ItemUtil.TransformItemLocationsToItems

ItemUtil.GetValidatedItemLocation(equipSlot) / ItemUtil.GetEquipSlotTexture(equipSlot)

If the equipment slot holds an item, ItemUtil.GetValidatedItemLocation returns an ItemLocation for that slot. Otherwise it returns nil.

ItemUtil.GetEquipSlotTexture returns the icon of the item in the equipment slot. If the slot is empty, it returns the texture of the empty slot from C_PaperDollInfo.GetInventorySlotInfoForInvSlot.

ItemUtil.GetEmptyEquipSlotTooltip(equipSlot) / ItemUtil.GetEmptyEquipSlotTooltipForSlotName(slotName)

Both functions return the display name of an equipment slot, for example "Head". ItemUtil.GetEmptyEquipSlotTooltipForSlotName takes a slot name, for example "HeadSlot", and returns the global string with that name in upper case (HEADSLOT). ItemUtil.GetEmptyEquipSlotTooltip takes a slot number.

/dump ItemUtil.GetEmptyEquipSlotTooltip(1)
-- "Head"

ItemUtil.DisplayEquipSlotTooltip(frame, tooltip, equipSlot, suppressComparison, checkRelic)

Shows the item in an equipment slot of the player on tooltip. If the slot is empty, the tooltip shows the name of the slot at the right of frame. If checkRelic is true and the player has a relic slot, the name is RELICSLOT ("Relic").

If suppressComparison is true, the function calls GameTooltip_SuppressAutomaticCompareItem(tooltip). That function sets tooltip.suppressAutomaticCompareItem. No ClassicAPI code compares items automatically, so the flag has no effect.

Launch

Options you add to the command line that starts the game, alongside -console. They are not Lua functions — put them on the launcher shortcut, or on whichever executable you start the client with:

VanillaFixes.exe -console -config Config2.wtf

An option name is matched without regard to case, and -name value, -name=value and /name value all work. Put double quotes around a value that contains spaces.

-config <name> (launch switch)

Reads and writes WTF\<name> in place of WTF\Config.wtf, so one install can hold several settings profiles.

VanillaFixes.exe -console -config Config-raid.wtf

The name must be a plain filename with no folder in it. A name that contains \, / or : is refused, and the client uses Config.wtf instead. That way a setting is never written somewhere you cannot find it.

The client both loads and saves through this one name. Everything the client keeps in that file follows the profile: video and sound settings, console variables, and the account name the login screen remembers. The addon enable list does not, because it lives in WTF\Account\ instead.

On first use the file does not exist yet. The client starts from its defaults and writes the file when you quit.

-gluescript / -gluescriptFile / -gamescript / -gamescriptFile (launch switches)

Run Lua as the client starts. Four options, in two pairs: one pair for the login and character-select screens, one pair for in-game. In each pair, the plain option takes the code itself and the File option takes the path to a file that contains it.

Option Runs
-gluescript <code> at the login and character-select screens
-gluescriptFile <path> the file's contents, at those same screens
-gamescript <code> one time, in-game
-gamescriptFile <path> the file's contents, one time in-game
VanillaFixes.exe -console -gamescript "print('hello from the command line')"
VanillaFixes.exe -console -gamescriptFile C:\scripts\startup.lua

When each one runs. Every script runs after the interface for its screen has loaded. So it can call interface functions, read and change frames, and use anything an addon has set up.

A glue script runs each time the login and character-select screens appear. That includes the first time and every return from the world when you log out. A script that logs in for you therefore keeps working after a logout.

A game script runs one time only, on the first frame after you enter the world. It does not run again for /reload, and it does not run again if you log out and back in on another character.

Files. A path may be absolute or relative to the folder the client runs in. The file is read at the moment the script runs, so you can edit it between launches without touching the shortcut. A file the client cannot read is reported in Logs\classicapi_debug.log and the rest of the launch continues.

Both at once. If you pass both options of a pair, both run: the code first, then the file.

Errors. A script that fails to compile, or that raises while it runs, is reported the same way any other script error is. It does not stop the client, and it does not stop the other scripts.

Localization

Functions that write numbers as text, with the separators and abbreviations of the client language.

BreakUpLargeNumbers(largeNumber [, natural])

Returns the number as text, with a separator between each group of three digits.

BreakUpLargeNumbers(1234567)          -- "1,234,567"
BreakUpLargeNumbers(1234567.891)      -- "1,234,567"
BreakUpLargeNumbers(123.456)          -- "123.5"
BreakUpLargeNumbers(5)                -- "5"
BreakUpLargeNumbers(-1234567)         -- "-1,234,567"
BreakUpLargeNumbers(12345678, true)   -- "12.3 million"
BreakUpLargeNumbers(123456, true)     -- "123,456"
Arg Type Description
largeNumber number The number to write.
natural boolean, optional true writes a number of one million or more with a word, for example "1.2 million". The default is false.

A whole number, or a number of 1000 or more, loses its decimals. The function removes them and does not round. Any other number keeps one decimal, rounded to the nearest tenth. A half rounds away from zero: 0.25 becomes "0.3". A number with a decimal has no separator, so 999.96 becomes "1000.0".

The separator is the global string LARGE_NUMBER_SEPERATOR, and the decimal point is DECIMAL_SEPERATOR. When the breakUpLargeNumbers CVar is 0, the function writes the digits with no separator.

With natural, a positive number of one million or more gets the largest word that fits: million, billion or trillion. The number before the word keeps one decimal when it is less than 100. The function removes the other decimals and does not round, and it does not show a decimal of .0: 2000000 becomes "2 million". Smaller numbers and negative numbers are whole numbers with separators.

The words are the global strings SECOND_NUMBER, THIRD_NUMBER and FOURTH_NUMBER. On Chinese and Korean clients, these words count in steps of ten thousand, so the words start at 10,000. On German, Spanish and French clients, the words contain a |7 plural code, which shows as written.

AbbreviateNumbers(number [, options])

Returns a short form of the number, for example "1.2K" for 1234.

AbbreviateNumbers(999.9)     -- "999.9"
AbbreviateNumbers(1234)      -- "1.2K"
AbbreviateNumbers(12345)     -- "12K"
AbbreviateNumbers(1999999)   -- "1.9M"
AbbreviateNumbers(-12345)    -- "-12345"
AbbreviateNumbers(1234, { breakpointData = {
    { breakpoint = 100, abbreviation = "h", significandDivisor = 10,
      fractionDivisor = 10, abbreviationIsGlobal = false },
} })                         -- "12.3h"
Arg Type Description
number number The number to shorten.
options table, optional A NumberAbbrevOptions table. See below.

The function uses a list of breakpoints, from the largest to the smallest. It uses the first breakpoint that the number reaches. The result is floor(number / significandDivisor) / fractionDivisor, followed by the abbreviation. Because the function rounds down, 1999999 becomes "1.9M". When the number is less than all breakpoints, the function returns the number as Lua writes it. This is always true for a negative number.

The default breakpoints give one decimal below ten of a unit, and no decimal from ten up. The units are thousands (K), millions (M), billions and trillions. The abbreviations for billions and trillions name global strings that do not exist, so these numbers have no letter: AbbreviateNumbers(1.5e10) returns "15". On Chinese and Korean clients, the default units are ten thousand and one hundred million.

NumberAbbrevOptions fields:

Field Type Description
breakpointData table, optional A list of NumberAbbreviationBreakpoint tables, from the largest breakpoint to the smallest. The list replaces the default breakpoints.
locale string, optional The language of the default breakpoints. "koKR", "zhCN" and "zhTW" give the Chinese and Korean breakpoints. The default is the language of the client.

A config field has no effect.

NumberAbbreviationBreakpoint fields:

Field Type Description
breakpoint number The smallest number for this entry.
abbreviation string The name of the global string that comes after the number.
significandDivisor number The function divides the number by this value and rounds down.
fractionDivisor number The function then divides by this value, which gives the decimals.
abbreviationIsGlobal boolean, optional false puts the abbreviation text itself after the number. The default is true.

When an abbreviation names a global string that does not exist, nothing comes after the number. When an entry of breakpointData is not a table, or a field has the wrong type, the function raises an error.

AbbreviateLargeNumbers(number [, options])

Returns a short form of a large number, in whole thousands or millions.

AbbreviateLargeNumbers(12345)       -- "12,345"
AbbreviateLargeNumbers(123456)      -- "123 K"
AbbreviateLargeNumbers(12345678)    -- "12345 K"
AbbreviateLargeNumbers(123456789)   -- "123 M"
AbbreviateLargeNumbers(-12345)      -- "-12,345"

The arguments are the same as for AbbreviateNumbers, and the function uses breakpoints in the same way. The default breakpoints start at 100,000 for thousands (FIRST_NUMBER_CAP, " K") and at 100,000,000 for millions (SECOND_NUMBER_CAP, " M"). When the number is less than all breakpoints, the result is BreakUpLargeNumbers(number).

The function rounds the result to the nearest whole number. With the breakpoint from the AbbreviateNumbers example, 123456 becomes "1235h". On Chinese and Korean clients, or with one of their locale values, the result keeps its decimals. There, the default breakpoints use ten thousand and one hundred million: 123456789 becomes 1.23 and the SECOND_NUMBER_CAP string.

C_StringUtil.GetDefaultAbbreviationBreakpoints([locale])

Returns a new list of the default breakpoints of AbbreviateNumbers, as NumberAbbreviationBreakpoint tables. You can change the list and give it as breakpointData.

Arg Type Description
locale string, optional The language, for example "zhCN". The default is the language of the client.
local breakpoints = C_StringUtil.GetDefaultAbbreviationBreakpoints()
-- breakpoints[1] = { breakpoint = 10000000000000,
--                    abbreviation = "FOURTH_NUMBER_CAP_NO_SPACE",
--                    significandDivisor = 1000000000000,
--                    fractionDivisor = 1, abbreviationIsGlobal = true }

C_StringUtil.CreateNumericRuleFormatter()

Returns a new formatter that changes numbers into text. The formatter has a list of breakpoints. Each breakpoint is a rule for the numbers from its threshold up to the next breakpoint.

local Up = Enum.NumericRuleFormatRounding.Up
local formatter = C_StringUtil.CreateNumericRuleFormatter()
formatter:SetBreakpoints({
    { threshold = 0, step = 0.1, rounding = Up, format = "%.1f" },
    { threshold = 5, step = 1, rounding = Up, format = "%d" },
    { threshold = 60, format = "%dm", components = { { div = 60, step = 1, rounding = Up } } },
})
formatter:FormatNumber(3.24)   -- "3.3"
formatter:FormatNumber(42.5)   -- "43"
formatter:FormatNumber(125)    -- "3m"
formatter:FormatNumber(-1)     -- "-1"
Method Notes
formatter:FormatNumber(number) Returns the text for number. The formatter uses the breakpoint with the largest threshold that is equal to or less than number. If no breakpoint applies, the text is the number as tostring writes it, for example "9.99". number must be a finite number.
formatter:SetBreakpoints(breakpoints) Replaces all breakpoints with the NumericRuleFormatBreakpoint tables in the list. If one table is not correct, the breakpoints do not change.
formatter:AddBreakpoint(breakpoint) Adds one NumericRuleFormatBreakpoint.
formatter:GetBreakpoints() Returns a new list of the breakpoints, from the largest threshold to the smallest. Each field has a value: step and rounding are 0 if you did not set them, and components is an empty table. min and max are in the table only if you set them.
formatter:ClearBreakpoints() Removes all breakpoints.
formatter:Copy() Returns a new formatter with the same breakpoints. A change to one formatter does not change the other.

If two breakpoints have the same threshold, the first one that you added applies. A formatter has only these methods. You cannot store other values in it.

NumericRuleFormatBreakpoint:

Field Type Description
threshold number The smallest number that the breakpoint applies to. It must be a finite number.
step number, optional Rounds the number to a multiple of step. A step of 0 or less does not round.
rounding Enum.NumericRuleFormatRounding, optional How step rounds. The default is Nearest.
min number, optional After the rounding, a smaller number changes to min.
max number, optional After the rounding, a larger number changes to max.
format string A format string, as for string.format. Without components, the format gets the number as its one value. The format can contain color codes, for example "|cffff3333%d|r".
components table, optional A list of NumericRuleFormatComponent tables. Each component gives one value to format, in order.

NumericRuleFormatComponent:

Field Type Description
div number, optional Divides the number by div. The result keeps its fraction, so "%d" writes 125 with div = 60 as 2.
mod number, optional After div, keeps the remainder of a division by mod. The remainder has the sign of the number: -65 with mod = 60 gives -5.
step number, optional After div and mod, rounds the result to a multiple of step.
rounding Enum.NumericRuleFormatRounding, optional How step rounds. The default is Nearest.

A div or mod of 0 has no effect. Each component starts from the number after the step, min, and max of its breakpoint.

Enum.NumericRuleFormatRounding:

Name Value Description
Nearest 0 To the nearest multiple. A number halfway between two multiples goes up: 2.5 gives 3, and -2.5 gives -2.
Up 1 To the next larger multiple.
Down 2 To the next smaller multiple.

The formatter selects the breakpoint before it rounds. For example, with breakpoints at 0 (step = 1, Up) and at 60, the number 59.5 uses the first breakpoint and gives "60".

Each component rounds separately. With format = "%d:%02d" and the components { div = 60, step = 1, rounding = Down } and { mod = 60, step = 1, rounding = Up }, 116 gives "1:56", but 119.5 gives "1:60". If you also set step = 1 and rounding = Up on the breakpoint, 119.5 changes to 120 first and gives "2:00".

If format asks for more values than the breakpoint gives, FormatNumber gives an error that starts with NumericFormatter:FormatNumber():.

breakUpLargeNumbers (CVar)

A setting for BreakUpLargeNumbers and the functions that use it. 1 (the default) puts a separator between each group of three digits. 0 writes the digits with no separator.

/console breakUpLargeNumbers 0

Loot

Programmatic loot operations that the stock Lua surface doesn't expose: enumerate nearby lootable corpses, open a loot session against one by GUID, take a specific item out of one without going through the visible LootFrame, and pre-scan every reachable corpse to build a picker table before any cursor work. Foundation for AoE-loot, auto-loot-by-filter, and similar addons that the stock LootSlot / LootSlotInfo flow can't drive.

All guid args are hex-string GUIDs in the same form UnitGUID returns ("0xF130..."). All itemID args are integers.

C_Loot.GetNearbyLootableUnits()

Returns a 1-indexed array of { guid = "0x..." } subtables, one per visible unit that the server has flagged lootable AND that's within the engine's right-click-loot interact range. Empty table pre-login, when no lootable units are in range, or when the local player has no loot rights on visible corpses (the server only sets the lootable flag for clients with rights).

for _, unit in ipairs(C_Loot.GetNearbyLootableUnits()) do
    C_Loot.LootUnit(unit.guid)
end

The interact-range check matches what the engine uses for the hover-to-loot cursor — slightly tighter than melee range, looser than "on top of." Server still enforces real range on the resulting CMSG_LOOT; in-client OOR requests just silently get no response.

C_Loot.LootUnit(guid)

Opens a loot session against the given corpse. The loot window appears asynchronously via the normal LOOT_OPENED event once SMSG_LOOT_RESPONSE arrives — same flow as right-clicking the corpse, just dispatched programmatically.

No return value. Silent no-op when:

  • The object manager is NULL (glue / character-select).
  • The GUID doesn't resolve to a visible unit (out of render range, despawned, wrong typemask).
  • The local player pointer can't be resolved.
  • You are mounted. A right-click on a corpse also does nothing while you are mounted.

Errors only on bad Lua usage (missing arg, unparseable GUID).

-- Loot everything in range, one corpse at a time. With autoloot on,
-- the engine drains each window automatically; without it, you
-- still get the visible loot frame per corpse.
for _, unit in ipairs(C_Loot.GetNearbyLootableUnits()) do
    C_Loot.LootUnit(unit.guid)
end

useDistanceCheck is hardcoded off — the engine won't try to auto-walk the player toward a corpse just out of range. That'd be a surprise side effect for an AoE-loot loop.

C_Loot.LootUnitItem(guid, itemID)

Takes a specific item from a specific corpse without using the visible loot frame's slot list. Dispatches CMSG_LOOT_ITEM directly.

Two paths, picked automatically:

  1. Fast path — the corpse's loot window is already open for the requested target. Synchronously finds the slot, sends the take packet, returns true if the slot was found / false if not.
  2. Async path — opens the corpse's window first (same wire as C_Loot.LootUnit), waits for the response, then sends the take packet automatically. Returns true once the request is queued. No "did it succeed" event; observe BAG_UPDATE / ITEM_PUSH to confirm.

false immediately when:

  • Args missing or wrong types (raises an error, not returns false).
  • Another LootUnitItem is mid-flight (one at a time).
  • The world isn't loaded.
  • The target GUID doesn't resolve to a visible unit.
  • You are mounted, or you cannot loot now (for example, another loot window is open).
  • (sync path only) No slot in the open window holds the itemID.

The send bypasses the BoP-confirmation dialog. Server still enforces all real permissions (loot rights, distance, master-loot rules); the BoP prompt is purely a client-side courtesy that callers of a programmatic API have already opted out of.

-- Scan, then take just one specific item out of one specific corpse.
C_Loot.ScanNearbyLoot()  -- LOOT_SCAN_COMPLETED → results below
local results = C_Loot.GetLastScanResults()
local guid = results[1].guid
local itemID = results[1].items[1].itemID
C_Loot.LootUnitItem(guid, itemID)

C_Loot.ScanNearbyLoot()

Initiates a pre-scan: opens each lootable corpse in range one at a time, scrapes its slot contents (itemID, count, item link), closes the window, advances. The LOOT_OPENED / LOOT_CLOSED events are suppressed for the duration of the scan so LootFrame and other listeners don't react — the scan is invisible to addons that aren't opted in.

Returns true if the scan was queued, false if another scan is already in progress, the world isn't loaded, or you are mounted. Completion is signaled via the LOOT_SCAN_COMPLETED event; results are read via C_Loot.GetLastScanResults().

local f = CreateFrame("Frame")
f:RegisterEvent("LOOT_SCAN_COMPLETED")
f:SetScript("OnEvent", function()
    for _, corpse in ipairs(C_Loot.GetLastScanResults()) do
        for _, item in ipairs(corpse.items) do
            ChatFrame1:AddMessage(item.link or ("item:"..item.itemID))
        end
    end
end)
C_Loot.ScanNearbyLoot()

Per-corpse step timeout is ~3-6 seconds (180 WorldTick frames) so a single dropped server response doesn't hang the whole scan. Failed corpses simply don't appear in the results. A corpse that gets no response does not block looting after the scan.

C_Loot.LootAllCorpses([max])

Loots every nearby lootable corpse in sequence — the same corpse walk as ScanNearbyLoot, but it actually takes the loot: coin (CMSG_LOOT_MONEY) plus every item slot (CMSG_LOOT_ITEM) from each window before releasing and advancing to the next corpse. The optional max caps how many corpses are visited (default: all in range).

Returns true if the loot walk was queued, false if a walk (this or ScanNearbyLoot) is already in progress, the world isn't loaded, or you are mounted — the two share one state machine, so IsScanInProgress reports either, and only one runs at a time.

Like the scan, the walk is silent: LOOT_OPENED / LOOT_CLOSED are suppressed so LootFrame never flickers open per corpse. The normal item-received and money events still fire (CHAT_MSG_LOOT, CHAT_MSG_MONEY, inventory updates), so the player sees what they got.

-- Loot everything within interact range
C_Loot.LootAllCorpses()

-- Loot at most 5 corpses
C_Loot.LootAllCorpses(5)

Completion is signaled by LOOT_SCAN_COMPLETED; afterward GetLastScanResults() reports what each corpse held — i.e. what was looted.

Range and permissions. Only corpses inside the engine's own right-click-loot range are queued (same distance test as GetNearbyLootableUnits), and only those the server flagged lootable for you. The item sends bypass the client-side BoP/unique confirm dialog (appropriate for a programmatic loot-all), but the server still enforces every real rule — loot rights, distance, master-loot. Unlike a right-click, this does not change your target or auto-walk you toward out-of-range corpses.

C_Loot.IsScanInProgress()

Returns true while a ScanNearbyLoot call is mid-flight, false otherwise. Use to gate re-scan attempts so addons don't spam-call during the engine's per-corpse round-trip.

C_Loot.GetLastScanResults()

Returns the most-recent completed-scan results as a 1-indexed array:

{
    [1] = {
        guid = "0x...",    -- corpse GUID
        coin = 1234,       -- copper amount; 0 if no coin
        items = {
            [1] = {
                itemID = 2589,
                count  = 4,
                link   = "|cff...|Hitem:2589:0:0:0|h[Linen Cloth]|h|r",
                                   -- omitted if cache entry wasn't loaded
            },
            ...
        },
    },
    ...
}

Empty table before the first scan completes. Replaced wholesale by each new scan — re-read after LOOT_SCAN_COMPLETED rather than caching the table.

The link field is the engine's full hyperlink (color + payload + display name + reset). Random-suffix items like "Stringy Wolf Meat of the Bear" survive round-trip — the link encodes the per-instance enchant / suffix / unique fields, not just the base itemID. For tooltip display, pass it to GameTooltip:SetHyperlink as is — it takes the full link as well as the item:... payload.

LootHistory

The C_LootHistory namespace gives the history of group-loot rolls. Each item in the history has the vote and roll of each player, and the winner.

The name and class of each player are kept from the time of the roll. They stay correct after that player leaves the group.

The history keeps the last 128 rolled items. It stays through /reload and clears at logout. C_LootHistory.Clear() clears it at any time. The master-loot functions (SetExpiration, GiveMasterLoot, CanMasterLoot) are not available.

The LOOT_HISTORY_ROLL_CHANGED, LOOT_HISTORY_ROLL_COMPLETE, and LOOT_HISTORY_FULL_UPDATE events tell you when the history changes. Refer to the Events section.

C_LootHistory.GetNumItems()

Returns the number of rolled items in the history.

C_LootHistory.GetItem(itemIndex)

itemIndex starts at 1. The oldest item is 1. Returns rollID, itemLink, numPlayers, isDone, winnerIdx, isMasterLoot, isCurrency:

  • rollID — a stable ID for the roll. The itemIndex of an item changes when the history is full and drops its oldest item. The rollID does not change. Use it as the key for data about a row, for example "this row is expanded".
  • itemLink — item:<id>:0:<suffix>:<unique>. You can give it to GetItemInfo and GameTooltip:SetHyperlink. It has the random suffix of the item, so an "of the X" item keeps its suffix.
  • numPlayers — the number of players in the list for this item.
  • isDone — true when the item has a winner, or when all players passed.
  • winnerIdx — the player index of the winner, for C_LootHistory.GetPlayerInfo(itemIndex, winnerIdx). nil when the item has no winner yet, or when all players passed. The winner is always in the player list.
  • isMasterLoot — always false.
  • isCurrency — always false.

Returns nothing when itemIndex is out of range.

C_LootHistory.GetPlayerInfo(itemIndex, playerIndex)

Both indexes start at 1. Returns name, class, rollType, roll, isWinner, isMe:

  • name — the name of the player.
  • class — the class token ("WARRIOR", "MAGE", and more) for RAID_CLASS_COLORS. nil when the class is not known.
  • rollType — 0 = pass, 1 = need, 2 = greed.
  • roll — the dice roll, 1 to 100. nil when the player has no roll. For example, a player who passed has no roll. A greed voter has no roll when a need voter wins.
  • isWinner — true when this player won the item.
  • isMe — true when this player is you.

Returns nothing when itemIndex or playerIndex is out of range.

C_LootHistory.Clear()

Clears the history. Returns nothing. A ClassicAPI extension, for a "clear history" button. Fires LOOT_HISTORY_FULL_UPDATE, so an open display shows the empty history.

LossOfControl

C_LossOfControl — the active crowd-control / interrupt effects on the local player. There is no such aggregation layer, so it's synthesized: CC types from the player's debuffs, and the school-interrupt lockout from the SMSG_SPELL_COOLDOWN the server sends on a Counterspell / Kick.

C_LossOfControl.GetActiveLossOfControlDataCount()

Returns the number of active loss-of-control effects on the player (0 when none).

C_LossOfControl.GetActiveLossOfControlData(index)

Returns a LossOfControlData table for the index-th active effect (1-based, up to the count above), or nil if the index is out of range.

for i = 1, C_LossOfControl.GetActiveLossOfControlDataCount() do
    local d = C_LossOfControl.GetActiveLossOfControlData(i)
    print(d.locType, d.displayText, d.timeRemaining)
end
Field Type Notes
locType string "STUN", "FEAR", "ROOT", "CONFUSE", "CHARM", "POSSESS", "SILENCE", "PACIFY", "PACIFYSILENCE", "DISARM", or "SCHOOL_INTERRUPT".
spellID number The spell causing the effect. 0 for SCHOOL_INTERRUPT (the interrupting spell isn't in the lockout packet).
displayText string Effect name — the spell name for CC, "Interrupted" for SCHOOL_INTERRUPT.
iconTexture string The spell's icon path (there are no FileDataIDs here; the path is the functional equivalent). "" for SCHOOL_INTERRUPT.
startTime number? GetTime()-epoch seconds. nil when the effect has no timer.
timeRemaining number? Seconds remaining.
duration number? Effect duration, in seconds.
lockoutSchool number The locked spell-school mask for SCHOOL_INTERRUPT (feed to C_Spell.GetSchoolString for the name); 0 for CC effects.
priority number Always 0 — no equivalent here.
displayType number Always 2 (show for the effect's full duration).

Fidelity notes:

  • SCHOOL_INTERRUPT is derived from the server's own lockout packet (Player::ProhibitSpellSchool sends a single SMSG_SPELL_COOLDOWN listing every spell of the interrupted school at one uniform duration), so the school and duration are authoritative. The interrupting spell itself isn't transmitted, hence spellID = 0 and displayText = "Interrupted".
  • CC timing comes from the server's timer for your debuff, the same timer GetPlayerBuffTimeLeft uses. It includes diminishing returns and the caster's talents. startTime / timeRemaining / duration are nil when the debuff has no timer.
  • auraInstanceID is the instance ID of the CC debuff (see AuraData). It is nil for SCHOOL_INTERRUPT, which is not an aura.

C_LossOfControl.GetSchoolLockout([filterMask])

Returns lockedMask, secondsRemaining for the school-interrupt lockout — the SCHOOL_INTERRUPT slice of the list above, without building the list.

local mask, remaining = C_LossOfControl.GetSchoolLockout()
if mask ~= 0 then
    print(C_Spell.GetSchoolString(mask), "locked for", remaining)
end

-- One school's own remaining time. Masks are 1 << schoolIndex:
-- physical 1, holy 2, fire 4, nature 8, frost 16, shadow 32, arcane 64.
local locked, fireRemaining = C_LossOfControl.GetSchoolLockout(4)
if locked ~= 0 and fireRemaining < 0.5 then
    -- fire clears within half a second
end
Return Type Notes
lockedMask number Every currently locked school OR'd together, as 1 << schoolIndex — the same shape as lockoutSchool. 0 when nothing is locked.
secondsRemaining number? Until every school in lockedMask is clear, i.e. the lockout ending last. nil when lockedMask is 0.

filterMask narrows the scan to those schools, so one school's own remaining time is GetSchoolLockout(1 << schoolIndex). Omitted or 0 means all schools.

Two schools can be locked at the same time — each SMSG_SPELL_COOLDOWN batch locks one, so being kicked on fire and then on frost leaves both active. That is why this returns a mask rather than a single school, and why walking the list above and stopping at the first SCHOOL_INTERRUPT entry is wrong.

Prefer this over the list walk whenever only the school lockout matters: it reads the lockout state directly, allocating no table and skipping the debuff scan that every GetActiveLossOfControlData call performs. Cheap enough to call per frame from a macro conditional.

The LOSS_OF_CONTROL_ADDED / LOSS_OF_CONTROL_UPDATE events fire as these effects change — see Events. Note that they are driven by a diff of which effects are active, so re-locking an already-locked school extends its lockout without firing either event. Read the remaining time when you need it rather than caching it on the events.

For the effect that blocks one specific spell, shaped as a cooldown, see C_Spell.GetSpellLossOfControlCooldown and its spellbook-slot variants.

Lua

Standard-library functions that this Lua 5.0 is missing — restored by ClassicAPI so addons find them. Most are single-function additions (several are just the 5.0→5.1 renames — string.gmatch←gfind, math.fmod←math.mod); coroutine.* restores the whole stripped coroutine library.

Lua 5.1 syntax

1.12 runs Lua 5.0. It cannot compile five pieces of Lua 5.1 syntax that many addons use. These are the length operator #, the modulo operator %, ... used as an expression, 0x hexadecimal number literals, and leveled long brackets ([=[ ... ]=]). ClassicAPI rewrites addon source to the 5.0 equivalent before it compiles, so all five work:

local n = #myTable          -- length operator
local r = a % b             -- modulo operator
local args = { ... }        -- ... as an expression, not only in a parameter list
local mask = 0xFF00         -- hex number literal
local doc = [=[ has ]] in it ]=]   -- leveled long bracket

The rewrite is transparent. You do not call anything. It runs on every chunk the client compiles — addon files, loadstring, and XML <OnLoad> handlers.

What each form does:

  • #x gives the length of a string, or a border of a table (the value Lua 5.1 # returns). It ignores table.setn.
  • a % b uses the Lua 5.1 result a - floor(a/b)*b, which takes the sign of b. This differs from math.mod (C fmod, truncated toward zero): -1 % 3 is 2, while math.mod(-1, 3) is -1.
  • ... as an expression yields all the varargs. The ... in a function parameter list stays as the vararg declaration.
  • 0xFF00 becomes its decimal value (65280) — the same number Lua 5.1 produces. The 5.0 lexer rejects 0x literals, so without this an addon needs tonumber("0xFF00", 16).
  • [=[ ... ]=] (a leveled long bracket, with any number of =) holds text a plain [[ ... ]] cannot, such as text that contains ]]. ClassicAPI rewrites a leveled long string to a plain long string, or to a quoted string when its body needs one. It removes a leveled long comment (--[=[ ... ]=]).

Addon file arguments. An addon can read its name and its private table from the file arguments:

local addonName, addonTable = ...

Stock 1.12 never passed these to an addon file. ClassicAPI supplies them to any file under Interface\AddOns\ that reads .... addonName is the folder name. addonTable is one table shared by every file of that addon. To read another addon's table, use C_AddOns.GetAddOnLocalTable, which needs that addon's opt-in.

Limits.

  • The rewrite reads strings and comments correctly. A % in "%d" or a # in --[[ # ]] is left alone.
  • A plain long string that contains [[ nests and closes by depth, the same as the 5.0 engine. Lua 5.1 does not nest. For text that contains [[ or ]], use a leveled bracket ([=[ ... ]=]).
  • ClassicAPI rewrites a leveled long string to a plain [[ ... ]] when it can, which keeps the exact value. A body that contains [[ or ]], or ends with ], becomes a quoted string instead. A quoted string keeps a leading newline in the value. Lua 5.1 drops that newline.
  • Only integer hex is converted. Hex floats (0x1.8p3) and literals wider than 64 bits are left as-is. Both are almost nonexistent in addon code.
  • Error line numbers stay correct. The rewrite adds no new lines.
  • The globals __len and __mod are the rewrite's helper functions. Treat them as internal. Do not call them directly.
  • To turn a rewrite off for diagnosis, call _classicapi_SetTranspileOption(name, false), where name is "Length", "Modulo", "VarargExpansion", "HexLiterals", or "LongBrackets". This reverts affected chunks to the state that fails to compile, so use it only to answer "is the rewrite breaking this addon?".
  • _classicapi_TranspileStats() returns six numbers: the chunks loaded, their bytes, the chunks that needed a full token pass, those bytes, the milliseconds the rewrite has used since the client started, and the number of XML script bodies compiled with their parameters.

Upvalue limit

A function can use up to 60 upvalues — the outer locals it refers to.

Lua 5.0 stops at 32 and rejects the whole file with too many upvalues (limit=32). A large handler that reads many file-level locals can be valid Lua 5.1, which allows 60, and still fail to load here for that reason alone. ClassicAPI raises the limit to the same 60.

Nothing changes for a function within the old limit. Past 60 the error is the same message with the new number: too many upvalues (limit=60).

for loop variables per pass

Each pass of a for loop has its own loop variables, so a function made inside the loop keeps the values of the pass that made it.

local show = {}
for i, name in ipairs({ "a", "b" }) do
    show[i] = function() return name end
end
show[1]()   -- "a"
show[2]()   -- "b"

This applies to both for ... in loops and numeric loops, and to every function made in the loop body, at any depth.

A function sees the value the variable has at the end of its pass. If the body changes a loop variable after it makes the function, the function sees the changed value.

If the body assigns to a loop variable, the next pass starts from the new value. For example, i = i + 1 in a numeric loop skips a pass. Lua 5.1 does not do this. Do not assign to loop variables in code that must work the same on every client.

String methods (s:upper(), s:format(...))

In Lua 5.1, every string value accepts method calls: s:method(...) resolves through the string table. Lua 5.0 has no string methods — on stock 1.12, any s:upper() fails with attempt to index a string value. ClassicAPI restores the 5.1 behavior:

("asd"):upper()              -- "ASD"
("%d gold"):format(price)    -- method call on a literal
msg:match("^!(%w+)")         -- method call on a variable
link:sub(1, 5)
  • Both call forms work and give the same result: s:upper() and string.upper(s).

  • Every function in the string table is reachable as a method. That includes the ClassicAPI additions (match / gmatch, reverse) and any function an addon adds to string.

  • Reading an unknown key gives nil, the Lua 5.1 result: ("x").nope is nil, and ("x"):nope() fails with attempt to call, not attempt to index.

  • Writing to a string (("x").y = 1) still fails, the same as Lua 5.1.

  • getmetatable("") returns the shared string metatable, the same table for every string. Its __index is the string table. You can change the metatable, but setmetatable cannot replace it:

    getmetatable("").__index == string                -- true
    getmetatable("").__index.shout = function(s) return s:upper() .. "!" end
    ("hi"):shout()                                    -- "HI!"
    
  • Works everywhere strings do: in the world, on the login screen, and inside coroutines.

  • To turn method resolution off for diagnosis, call _classicapi_SetStringMethods(false). Strings then have no metatable and error on index, the stock 1.12 behavior. Use it only to answer "is this backport breaking this addon?".

getfenv / setfenv environment protection

A sandbox can hide the environment table that it gives to restricted code. Put a metatable on that table with an __environment field:

local view = setmetatable({}, { __environment = "protected" })
setfenv(restrictedFn, view)

After this, getfenv() in the sandbox returns the __environment value. It does not return the real table, so the restricted code cannot get the true environment.

__environment has no effect on setfenv. setfenv returns the function that it changed, so you can write it on one line:

local f = setfenv(loadstring(source), env)

setfenv(0, env) changes the environment of the running thread and returns nothing.

select(index, ...)

The Lua 5.1 vararg helper, backported to 1.12's Lua 5.0. Two forms:

  • select('#', ...) → the number of extra arguments passed after the index.
  • select(n, ...) → all arguments from position n onward. n may be negative to count back from the end (-1 is the last argument).

Errors on a non-numeric, non-'#' first argument or an index of 0 / below the negative range (matching Lua 5.1's luaL_checkint + index out of range behavior). Registered on both Lua states, so it's available to in-game addons and glue-screen (login/char-select) code alike.

select('#', 'a', 'b', 'c')   -- 3
select(2, 'a', 'b', 'c')     -- 'b', 'c'
select(-1, 'a', 'b', 'c')    -- 'c'

-- Idiomatic vararg count inside a function:
local function log(...)
    for i = 1, select('#', ...) do
        print(i, (select(i, ...)))
    end
end

Ported from Lua 5.1's luaB_select (lbaselib.c). The numeric form pushes nothing — the varargs are already on the stack, so it just returns the count of the slice from n+1 onward.

unpack(list [, i [, j]])

The Lua 5.1 range form. The stock 5.0 unpack(list) takes no range arguments and ignored extras silently — unpack(t, 2) returned the whole table: wrong values, no error. ClassicAPI replaces unpack with the 5.1 version (a port of luaB_unpack):

unpack({10, 20, 30})        -- 10, 20, 30
unpack({10, 20, 30}, 2)     -- 20, 30
unpack({10, 20, 30}, 2, 2)  -- 20

i defaults to 1 and j to the table length — the table.getn length, so a vararg arg table's n field is honored and embedded nils keep their slots. Errors on a non-table first argument, exactly like 5.1.

xpcall(f, msgh [, arg1, ...])

Calls f under the message handler msgh, passing the trailing arguments to f. Returns true plus everything f returned, or false plus the handler's return.

Stock xpcall takes only (f, msgh) and calls f with no arguments. It does not error on extra arguments — it discards them, so a function that reads its parameters receives nil. ClassicAPI forwards them instead:

local ok, sum = xpcall(function(a, b) return a + b end, geterrorhandler(), 2, 3)
-- ok = true, sum = 5

A two-argument call behaves exactly as before, so the capturing-closure form stays correct and needs no change:

xpcall(function() return f(a, b) end, handler)

Available to in-game addons and glue-screen code alike.

error("...") reaches the handler, the same as a runtime error. An addon can replace the global error with a function that does not raise. For example, pfUI's error prints the message to the chat frame and returns. Then f continues, and xpcall returns true.

GetCallstackHeight() / GetErrorCallstackHeight() / SetErrorCallstackHeight([height])

An error handler uses these functions to find the code that caused an error. The handler can then show the stack from that code, and not from the handler.

  • GetCallstackHeight() returns the number of call levels on the stack. The function that calls GetCallstackHeight is level 1. C functions count as levels, the same as in debugstack.
  • GetErrorCallstackHeight() returns the stack height of the code that caused the error, while the game reports an error to the error handler. If code calls the error handler directly, GetErrorCallstackHeight returns the value that SetErrorCallstackHeight set, or nil.
  • SetErrorCallstackHeight(height) sets the height for an error handler that code calls directly, with no error. SetErrorCallstackHeight(nil) removes the value. CallErrorHandler and assertsafe use this function to report from the code that called them.

This function returns the stack from the code that caused the error:

local function ErrorStack()
    local errorHeight = GetErrorCallstackHeight()
    if not errorHeight then
        return debugstack(2)
    end
    return debugstack(GetCallstackHeight() - errorHeight + 1)
end

Works in the world and on the login screen.

collectgarbage(opt [, arg])

Accepts Lua 5.1's string options. The stock 5.0 collector takes only an optional numeric threshold and errored on every string option (bad argument #1 (number expected, got string)).

  • "count" — returns the KB of Lua memory in use (the gcinfo() value).
  • "collect" — runs a full garbage-collection cycle. Returns 0.
  • "step" — accepted no-op returning true. The 5.0 collector cannot step incrementally; true means "cycle finished", so repeat until collectgarbage("step") loops exit immediately.
  • "stop" / "restart" / "setpause" / "setstepmul" — accepted no-ops returning 0. The 5.0 collector has no toggle or tuning; callers lose the optimization, nothing breaks.
  • A number (or no argument) keeps the stock threshold behavior unchanged.

table.wipe(t) / wipe(t)

Removes every key from t, leaving it empty but preserving its internal hash and array capacity. Returns t for chaining. wipe is the same function as a global.

local cache = {}
-- ... fill it up ...
table.wipe(cache)              -- cache is now {}
local also_t = table.wipe(t2)  -- also_t === t2 after wipe

Uses Lua 5.0's lua_next entry points. lua_next walks the hash node array linearly, so the canonical lua_next + rawset(k, nil) "during-iteration removal" pattern works in practice even though it's technically undefined per the Lua reference manual.

Errors on non-table input.

table.count(tbl)

Returns (numTableNodes, numArrayNodes, maxArrayIndex) describing how a table is populated (a diagnostic). These are counts of live entries, not the table's allocated capacity:

  • numTableNodes — total number of key/value pairs.
  • numArrayNodes — how many of those have an integer key in the range [1..numTableNodes] (the "contiguous array part" heuristic).
  • maxArrayIndex — the largest positive integral key (>= 1), or 0 if there is none. Negative/zero integral keys don't count.
table.count({ 10, 20, 30 })           -- 3, 3, 3
table.count({ a = 1, b = 2 })         -- 2, 0, 0
table.count({ [1] = "x", foo = "y" }) -- 2, 1, 1
table.count({ [100] = "x" })          -- 1, 0, 100
table.count({ [-3] = "x" })           -- 1, 0, 0

Pure iteration over the table (lua_next), so no dependency on Lua's internal storage layout. Errors on non-table input.

table.maxn(t)

Returns the largest positive numeric key of t, or 0 when it has none (Lua 5.1, luaB_maxn). Unlike table.getn it scans every key, so it sees past nil holes in sparse arrays:

table.maxn({ 10, 20, 30 })          -- 3
table.maxn({ [1] = "x", [9] = "y" }) -- 9  (getn would say 1)
table.maxn({ a = 1 })               -- 0

Errors on non-table input.

Stale table lengths (5.1 healing)

Lua 5.0 stores a table's table.insert / table.getn length out of band. Clearing a table's keys (for k in pairs(t) do t[k] = nil end) does not reset it — 5.0 code must also call table.setn(t, 0). Lua 5.1 removed setn entirely; lengths are computed from the table itself.

ClassicAPI gives you the 5.1 behavior — code written for Lua 5.1 (or that detects it) can skip setn and still get correct lengths.

When the stored length points past the last value, and the table has no explicit n field, table.getn — and everything built on it: table.insert, table.remove, table.concat, table.sort, table.foreachi, unpack — returns the true border, the same answer Lua 5.1 gives. This includes a table that is stale by exactly one slot (a one-element table that was cleared, or a recycled table whose previous use was one slot longer). Four things deliberately keep their old behavior:

  • A table with an explicit numeric n field (the vararg arg contract) keeps its stored count, so trailing nils survive (unpack({10, nil, 30, n = 3}) still returns all three slots).
  • A trailing nil appended by table.insert(t, nil) is a valid empty slot, not a stale length. ClassicAPI remembers the append itself, so the stored length stays as it is and the next table.insert adds after the nil rather than writing over it. This is the Lua 5.0 behavior that Ace2-era argument builders depend on. Only the two-arg append form is remembered; table.insert(t, pos, nil) is not.
  • A table with weak values (__mode = "v" or "kv") keeps its stored length. The garbage collector clears those slots when nothing else holds the value, so a nil slot does not show that the length is stale. Table-recycling pools depend on this.
  • table.setn still works; the heal only changes the answer when the stored length points past the last value and the nil was not put there by table.insert.

Mixin(object, ...) / CreateFromMixins(...)

The FrameXML table-mixin primitives, provided as engine C functions.

  • Mixin(object, ...) — shallow-copies every key/value from each table argument onto object (later arguments win on key collisions) and returns object. Non-table arguments are skipped.
  • CreateFromMixins(...) — Mixin({}, ...): returns a new table seeded from the given mixins.
local Greeter = {}
function Greeter:Hello() return "hi, "..self.name end
local obj = CreateFromMixins(Greeter)
obj.name = "Bob"
obj:Hello()   -- "hi, Bob"

ClassicAPI provides these as C functions (rather than leaving them in the !!!ClassicAPI addon) so they exist whenever the DLL is injected — even if the addon is disabled. The composite CreateAndInitFromMixin(mixin, ...) (which calls a :Init method) remains a Lua helper in the addon. The taint-based SecureMixin family is intentionally omitted — there is no execution taint system, so there is nothing for it to guard.

string.match / string.gmatch

The Lua 5.1 pattern helpers, backported to 1.12's Lua 5.0 (which ships only find / gfind / gsub).

  • string.match(s, pattern [, init]) — returns the captures of the first match, the whole match when the pattern has no captures, or nil when it doesn't match. Same pattern engine as string.find — it's find returning the captures instead of the start, end indices, so it reuses the engine's own matcher rather than reimplementing it.
  • string.gmatch(s, pattern) — the match iterator, for for x in string.gmatch(s, pat) do ... end. This is exactly Lua 5.0's string.gfind (renamed in 5.1); it's registered as a direct alias.
local y, m, d = string.match("2024-01-15", "(%d+)-(%d+)-(%d+)")  -- "2024","01","15"
string.match("no digits", "%d+")                                  -- nil

for word in string.gmatch("a,bb,ccc", "[^,]+") do print(word) end -- a / bb / ccc

Both call forms work: string.match(s, p) and s:match(p). See String methods. string.match is also the global strmatch, as on 3.x clients, alongside 1.12's own strfind, strsub and the other globals for the 5.0 string functions.

string.gsub table replacement

Lua 5.1 allows a table as gsub's replacement: every match looks up the first capture (the whole match when the pattern declares no captures) as a key and substitutes the value. A nil or false value keeps the original match. The stock 5.0 gsub accepts only a string or function replacement and errors on a table (string or function expected).

("$name the $class"):gsub("%$(%w+)", { name = "Bob", class = "Druid" })
-- "Bob the Druid", 2

("$name and $unknown"):gsub("%$(%w+)", { name = "Bob" })
-- "Bob and $unknown", 2   (a missing key keeps the match, as in 5.1)

Both string.gsub and the bare gsub global have the upgrade; string and function replacements behave exactly as before. One residual: when the pattern itself contains a %1..%9 back-reference, a missing table key removes the match instead of keeping it.

strsplit(sep, str [, pieces])

The WoW global; splits str on any character in sep and returns the pieces as multiple values.

  • sep — a set of delimiter characters (not a pattern); each character is a delimiter. "," splits on commas; " -" splits on spaces and dashes.
  • pieces (optional) — caps the number of results. After pieces - 1 splits the rest of the string (delimiters and all) is returned as the final piece. 0 or omitted = unlimited. 1 returns the whole string unsplit.

Consecutive/trailing delimiters produce empty pieces.

strsplit(",", "a,b,c")        -- "a", "b", "c"
strsplit(",", "a,b,c,d", 2)   -- "a", "b,c,d"     (capped at 2)
strsplit(" -", "a b-c")       -- "a", "b", "c"     (space OR dash)
strsplit(",", "a,,b")         -- "a", "", "b"      (empty middle piece)
local zone, x, y = strsplit(":", "Durotar:52:38")

Errors strsplit(): Stack overflow if a string splits into more pieces than the Lua stack can grow to hold (lua_checkstack guards every push).

Also registered as string.split. The separator comes first, so as a string method it is called on the separator: (","):split("a,b").

strjoin(delimiter, ...)

The inverse of strsplit: concatenate the vararg pieces with delimiter between each.

strjoin(",", "a", "b", "c")   -- "a,b,c"
strjoin("", "a", "b", "c")    -- "abc"
strjoin(",")                  -- ""   (no pieces)

A nil / non-string-coercible piece is treated as an empty string (so the delimiters still line up) rather than erroring.

Also registered as string.join.

strtrim(str [, chars])

Remove any of the characters in chars from both ends of str. chars is a literal set of characters (not a Lua pattern); it defaults to whitespace (space, tab, \n, \v, \f, \r).

strtrim("  hello  ")        -- "hello"
strtrim("xxhelloxx", "x")   -- "hello"
strtrim("[a]", "[]")        -- "a"

Also registered as string.trim, so it works as a string method: (" hello "):trim() is "hello".

strreplace(str, find, replace)

Replace every occurrence of the literal substring find with replace, returning (result, count). This is a plain-text replace — no pattern magic-character escaping, unlike string.gsub. An empty find returns str unchanged with count 0.

strreplace("a.b.c", ".", "-")        -- "a-b-c", 2   (no pattern escaping)
strreplace("hello world", "o", "0")  -- "hell0 w0rld", 2

Not a stock WoW global — a ClassicAPI convenience. For pattern-based replacement use string.gsub.

string.reverse(s) / strrev(s)

Returns s with its bytes reversed. Lua 5.1 added string.reverse; 5.0's string library lacks it. Byte-wise (not UTF-8 aware), matching stock Lua. Registered both on the string table and as the strrev global.

string.reverse("abc")   -- "cba"
strrev("hello")         -- "olleh"

strlenutf8(s)

The number of characters in a UTF-8 string. strlen counts bytes, so the two differ for text with accented or non-Latin letters.

strlenutf8("abc")       -- 3
strlen("Ärger")         -- 6   (Ä is two bytes)
strlenutf8("Ärger")     -- 5

strcmputf8i(string1, string2)

Compares two UTF-8 strings and ignores case. The result is a number: negative when string1 sorts first, 0 when the two match, positive when string2 sorts first. Only the sign has meaning.

The case fold covers ASCII letters, the Latin-1 letters à..þ, œ, and the Cyrillic alphabet. Other characters compare by code point.

strcmputf8i("Thrall", "thrall")    -- 0
strcmputf8i("Ärger", "ärger")      -- 0
strcmputf8i("abc", "abd") < 0      -- true
table.sort(names, function(a, b) return strcmputf8i(a, b) < 0 end)

strsplittable(sep, str [, pieces])

The same split as strsplit, returned as one array table instead of multiple values. The arguments have the same meaning. The table form has no stack limit, so a string can split into any number of pieces.

strsplittable(":", "hello:world:banana")   -- { "hello", "world", "banana" }
strsplittable(",", "a,b,c,d", 2)           -- { "a", "b,c,d" }

strconcat(...) / string.concat(...)

Joins every argument into one string with nothing between them, like the .. operator. Numbers convert to text, and any other value is an error. With no arguments it returns "".

strconcat("a", "b", 1)   -- "ab1"
strconcat()              -- ""

math.fmod(x, y)

The floating-point remainder of x / y (the quotient truncated toward zero), same as C's fmod. A pure 5.0→5.1 rename: Lua 5.0 ships the identical C function as math.mod, and 5.1 renamed the Lua-facing name to math.fmod. Registered as a direct alias — math.mod remains available too, so code written for either name works.

math.fmod(7, 3)     -- 1
math.fmod(-7, 3)    -- -1   (result takes the sign of the dividend)
math.fmod(5.5, 2)   -- 1.5

math.modf(x)

Returns (integral, fractional) — the integral part truncated toward zero and the fractional part carrying x's sign. Lua 5.1 function, genuinely missing from 1.12's math library (not just renamed like fmod).

math.modf(3.7)    -- 3, 0.7
math.modf(-3.7)   -- -3, -0.7
math.modf(5)      -- 5, 0

math.huge

Positive infinity, as the numeric constant math.huge (Lua 5.1). Handy as a "larger than anything" sentinel in min/max scans.

local best = math.huge
for _, v in ipairs(costs) do best = math.min(best, v) end

coroutine.*

The Lua 5.1 coroutine library: create, resume, running, status, wrap and yield. It works in the world and on the login screen. One WoW-specific quirk, with error(), is called out below.

coroutine.create(fn)

Creates a new coroutine with fn as its body and returns the resulting thread. fn must be a Lua function — passing a C function (any engine global like GetTime or UnitName) raises bad argument #1 to 'create' (Lua function expected).

The new coroutine starts suspended; nothing runs until coroutine.resume is called on it. The args passed to the first resume become the body's arguments.

coroutine.resume(co, ...)

Resumes a suspended coroutine, passing the trailing args to either the body (first resume) or back as the return values of the coroutine.yield that paused the coroutine.

Returns:

  • true, ...values on success, where values are whatever the coroutine yielded (mid-execution) or returned (on completion).
  • false, errMsg on error. Two error sources:
    • The coroutine isn't suspended. errMsg is "cannot resume <status> coroutine", where <status> is running, normal or dead.
    • The body raised an error: a Lua VM error, or a call to error. errMsg is the error message, usually with a file:line prefix.

Passing more arguments, or getting back more results, than the stack can hold raises too many arguments to resume / too many results to resume.

Caveat — an addon can change error(). If an addon replaces the global error with a function that does not raise, error("msg") returns normally. pfUI does this: its error prints the message to the chat frame. In a coroutine body, the body then continues, and resume does not return false.

coroutine.yield(...)

Suspends the current coroutine. Whatever's passed to yield becomes the return values of the matching coroutine.resume call. When the coroutine is next resumed, the args passed to resume become the return values of yield.

Raises an error if called outside a coroutine (attempt to yield across metamethod/C-call boundary) or from a C function (cannot yield a C function).

coroutine.status(co)

Returns the coroutine's state as a string:

Value Meaning
"running" co is the currently-running coroutine (i.e. the same thread that called status).
"suspended" co is paused — either initial (never resumed) or mid-yield.
"normal" co is active but not running: it resumed another coroutine, which is running now.
"dead" co's body has returned or errored.

coroutine.wrap(fn)

Equivalent to coroutine.create(fn) followed by returning a closure that calls coroutine.resume(co, ...) on each invocation — but raises errors instead of returning false, msg. A string error gets the caller's file:line added in front; any other error value is raised unchanged. Lets you use a coroutine as if it were an ordinary function:

local gen = coroutine.wrap(function()
    for i = 1, 5 do coroutine.yield(i) end
end)
print(gen())  -- 1
print(gen())  -- 2
print(gen())  -- 3

Calling the wrapped function after the body has completed raises cannot resume dead coroutine. The canonical "iterator that terminates cleanly through for ... in" idiom is to yield an explicit nil at the end so the for-loop's nil sentinel fires:

local function range(from, to)
    return coroutine.wrap(function()
        for i = from, to do coroutine.yield(i) end
        coroutine.yield(nil)
    end)
end

for n in range(1, 5) do print(n) end

coroutine.running()

Returns the coroutine that is currently running, or nil when called from the main thread (Lua 5.1's contract — the 5.2 second return, ismain, is not provided).

coroutine.running()   -- nil (main thread)
coroutine.wrap(function()
    print(coroutine.status(coroutine.running()))   -- "running"
end)()

Async pattern (RunAsync + C_Timer.After)

Coroutines pair naturally with C_Timer.After for chunked "do some work, wait a frame, do more" loops. The canonical scheduler:

local function RunAsync(fn)
    local co = coroutine.create(fn)
    local function step()
        local ok, delay = coroutine.resume(co)
        if not ok or coroutine.status(co) == "dead" then return end
        C_Timer.After(delay or 0, step)
    end
    step()
end

RunAsync(function()
    for i = 1, 100 do
        DoExpensiveWork(i)
        coroutine.yield(0)     -- next frame
    end
    coroutine.yield(0.5)        -- pause half a second
    DoFinalWork()
end)

coroutine.yield(n) returns n to the scheduler, which schedules the next step after that many seconds. yield(0) is the "resume on the next frame" form (since C_Timer.After(0, ...) fires on the next OnUpdate tick). RunNextFrame(cb) in src/embed/AddOns/!!!ClassicAPI/Util/FunctionUtil.lua wraps the same primitive for the non-coroutine case.

Macros

Macro features: the /cast and /use commands with [conditions], the #showtooltip and #show directives, and extensions to how the engine reads cast lines. Macro authors get them once ClassicAPI.dll is loaded.

/cast and /use

Both commands accept [conditions] and a @unit target. The first clause that matches gives the value. See SecureCmdOptionParse for the syntax and the list of conditions.

/cast Frostbolt
/cast [mod:shift] Frostbolt; Fireball
/cast [@player] Renew
/cast [@mouseover,help] Flash Heal; Flash Heal
/use Healthstone
/use 13
/use 0 1

The value is read in this order:

  • bag slot (for example 0 1) uses the item in that bag slot.
  • A number from 1 to 19 uses the item equipped in that inventory slot.
  • item:N, or a pasted item link, uses that item by ID.
  • The name of an item that you carry uses that item. An item name wins over a spell name.
  • Every other value is cast as a spell. A number is a spellID (/cast 5019), see Numeric spellIDs.

A ! in front of a spell name starts the spell but never turns it off. Use it for the abilities that toggle: auto-repeat shots (Shoot, Auto Shot) and the self-buffs (stance, aspect, seal, form, tracking). If the ability is already on, the line does nothing, so you can press the button again without stopping it.

/cast !Shoot
/cast !Auto Shot
/cast [@focus] !Auto Shot
/cast !Battle Stance

With a @unit target other than target, a spell is cast on that unit through C_Spell.CastAtUnit, and the item is used on that unit. [@player] uses the item on yourself. This works for an item named by bag slot or by inventory slot as well as by name.

/use always uses the item. Clicking a bag slot sells the item while a merchant window is open, and repairs it under the repair cursor, but the command does neither.

[@cursor] places a ground-target spell or item at the position under your mouse, through C_Spell.CastAtCursor and C_Item.UseAtCursor. A spell or item with no ground effect is cast or used normally.

[@player] drops a ground-target spell or item at your own feet. A spell or item with no ground effect is cast or used on you.

Your own feet and the cursor are the only two positions a ground-target spell or item can be aimed at this way. [@target] and the other units cast the spell on that unit when it takes a unit, and otherwise bring up the reticle for you to click, which is aim you already had.

/cast [@cursor] Blizzard
/use [@cursor] item:4390
/use [@player] item:4390

/use is /cast under a second name. Each client language has its own command names next to the English ones (for example /benutzen on a German client).

If SuperCleveRoidMacros is loaded, it handles /cast lines that contain conditions and all /use lines. Plain /cast Name lines still go through ClassicAPI.

/castsequence

Casts one action from a list, and moves to the next one each time a cast succeeds. The list is separated by commas and takes the same [conditions] and @unit syntax as /cast.

/castsequence Insect Swarm, Moonfire, Wrath
/castsequence [@focus] Insect Swarm, Moonfire, Wrath
/castsequence reset=combat Immolate, Corruption, Shadow Bolt

Each step is read the same way a /cast value is, so a step can name a spell, an item you carry, a bag slot, or an inventory slot. A step that names an item you can equip, and are not wearing, equips it instead of using it.

The sequence moves on only when a cast succeeds. A cast that fails or is interrupted leaves the sequence where it is, so the next press tries the same step again. A step that names nothing castable is stepped past.

reset= restarts the sequence. It takes one or more of these, joined by /:

Value Restarts when
target Your target changes.
combat You leave combat.
shift, ctrl, alt You press the button with that key held.
A number That many seconds pass without a press.
/castsequence reset=target/3 Rend, Thunder Clap

The sequence is keyed by its text. Two macros with the same list share one position and advance together, and the same list in one macro on two action bars is one sequence.

With #showtooltip, the action button follows the step the sequence is on.

/castrandom and /userandom

Casts one action picked at random from a comma-separated list. Both names are the same command, the way /use is /cast under a second name, so either one casts a spell or uses an item.

/castrandom Moonfire, Starfire, Wrath
/userandom [@player] Healing Potion, Rejuvenation Potion

The pick is held until a cast succeeds. A cast that fails or is interrupted keeps the same pick, so the next press tries that action again rather than picking another one.

The action button shows the first action in the list: its icon, cooldown and usable state, and with #showtooltip its tooltip. Name a value on the #showtooltip line to show a different one:

#showtooltip Starfire
/castrandom Moonfire, Starfire, Wrath

More commands with [conditions]

These commands take the same [conditions] and @unit syntax as /cast. Each client language has its own command names next to the English ones.

Targeting. @unit sets what is acted on. A value that is not a unit token is matched against character names, nearest first, by the start of the name.

Command Does
/target Targets a unit or a name.
/targetexact Targets only a full name match.
/cleartarget Drops the target.
/targetlasttarget Targets your previous target.
/targetlastenemy Targets your previous enemy.
/targetenemy Steps through nearby enemies.
/targetfriend Steps through nearby friendly units.
/targetenemyplayer Steps through nearby hostile players.
/targetfriendplayer Steps through nearby friendly players.
/targetparty Steps through your party.
/targetraid Steps through your raid.
/assist Targets what another unit has targeted.
/follow Follows a unit.

The stepping commands treat their value as a reverse flag, so /targetenemy [mod:shift] 1 steps backwards while shift is held.

/target [@mouseover,harm] [] Bob
/targetexact Bobby
/assist [@focus]
/cleartarget [dead]

Casting and player state.

Command Does
/stopcasting Stops the current cast.
/stopmacro Stops the rest of the macro.
/cancelaura Removes one of your buffs by name.
/cancelform Leaves your current shapeshift form.
/dismount Dismounts you.
/cancelaura Power Word: Shield
/cancelform [stance:1]
/stopcasting [mod:alt]

/stopmacro stops the macro that is running it. The lines after it do not run. A line that runs a second macro is not affected: a /stopmacro in that second macro stops the second one, and the first one goes on.

/cast Moonfire
/stopmacro [mod:shift]
/cast Wrath

Equipment. /equip takes an item name, and /equipslot takes an inventory slot number from 1 to 19 followed by an item name.

/equip Thunderfury
/equipslot 16 Thunderfury

Action bars. /changeactionbar takes a page from 1 to 6. /swapactionbar takes two pages and moves to whichever one you are not on.

/changeactionbar 2
/swapactionbar 1 2

Pet. /petattack, /petfollow, /petstay, /petpassive, /petdefensive and /petaggressive take conditions only. /petautocaston, /petautocastoff and /petautocasttoggle take a pet spell name.

/petattack [harm]
/petautocaston Growl
/petfollow [@player,noharm]

/petattack always sends the pet at your current target. Conditions still decide whether it runs, so /petattack [@mouseover] cannot aim at your mouseover.

#showtooltip and #show

Put #showtooltip in a macro, usually on the first line. The action button then shows the spell or item that the macro is about: its tooltip, cooldown, usable state, count, range, and auto-repeat highlight. If the macro icon is the question mark, the button, the macro window, and the cursor while you drag the macro also show the icon of that spell or item. The icon selector still shows the question mark, because that is the icon the macro keeps. #show does the same, but keeps the macro name as the tooltip.

#showtooltip
/cast Frostbolt

#showtooltip [mod:shift] Frostbolt; Fireball
/cast [mod:shift] Frostbolt; Fireball

#showtooltip Healthstone
/use Healthstone

#showtooltip spell:1539
/cast 1539

#showtooltip
/cast !Shoot

#show 13
/use 13
  • #showtooltip <value> shows that value. The value takes the same forms as /cast: a spell name, a spellID, an item name, item:N, an item link, an inventory slot (1 to 23), or bag slot. It also takes spell:N, which names a spell by ID and nothing else. It accepts the same [conditions]. An item that you carry wins over a spell of the same name, as in /cast. A spell name can carry the ! prefix, and the button shows that spell.
  • A spell you name by ID shows even when you have not learned it. The icon and the tooltip come from the spell itself, so #showtooltip spell:1539 and #showtooltip 1539 both show spell 1539. The button is still greyed out, because you cannot cast it. This applies to a value the directive names itself. The bare form below reads your /cast lines, and a spell there resolves only as far as the cast does.
  • An item named by ID shows even when you carry none of it, since the icon and tooltip come from the item itself. An item named by name has to be on you or equipped for the button to find it.
  • #showtooltip with no value reads the action lines of the macro, in order, up to the first line without conditions. It shows the first line whose clause matches. The line without conditions is the default. The action lines are /cast, /use, /castsequence (the step it is on), /castrandom and /userandom (the first entry), and /equip and /equipslot (items only).
  • #showtooltip none or #show none shows nothing. The button keeps the macro's own icon and name, and has no cooldown or usable state.
  • Conditions are evaluated again when a modifier key, your target, your mouseover unit, or your combat state changes, and about five times per second otherwise. So the button follows [mod:...], [combat], [@target,harm] and the other conditions.
  • If no clause matches, the button shows the macro's own icon and name. A group that only names a unit ([@mouseover]) matches only while that unit exists, so [@mouseover] Rejuvenation alone shows ? with nothing under the cursor, and [@mouseover][] Rejuvenation shows the spell and casts on your target instead. If the value names a spell by name that you do not know, nothing shows. Name it by ID to show it anyway.
  • The unit of the clause that matched ([@focus], [target=party1]) is the unit for the button's range check. IsActionInRange then tells whether that unit is in range, not whether your target is. With no unit in the clause, the range check uses your target.

A macro without #showtooltip or #show works the same way as #show with no value. If its icon is the question mark, it shows the icon of its first action, and its button follows that action's cooldown, count and usable state. The tooltip stays the macro name.

Lines that start with # are comments. They never run, and they never reach chat.

GetMacroSpell and GetMacroItem return what the directive resolved to.

If SuperCleveRoidMacros is loaded, it controls macro display, and ClassicAPI does not evaluate #showtooltip. A build that hands its own results over through C_Macro.SetMacroDisplay keeps #showtooltip working for the macros it does not claim.

A macro that names a condition SecureCmdOptionParse does not know belongs to another macro addon. Its conditions decide what the macro does, and ClassicAPI cannot evaluate them, so the button stays as the engine left it. The addon that owns those conditions can still drive the button through C_Macro.SetMacroDisplay. Nothing is printed about the condition: the macro works, and the player has nothing to correct. An edit to the macro tests the conditions again.

Numeric spellIDs in /cast and CastSpellByName

Pure-numeric input is now accepted anywhere a spell name would be:

/cast 5019                     -- in a macro line; casts Shoot if known
CastSpellByName("5019")        -- same effect from Lua / /run / chat

The spellID is resolved through Spell.dbc to the locale-resolved name, then handed to the engine's existing name resolver — so all the normal downstream behavior applies: the spellbook lookup gates on "player knows this spell," action-bar UI sees the cast as if it had been named, macros containing /cast 5019 get tagged with the right spellID in the engine's spell-state cache (so IsCurrentAction(slot) and IsAutoRepeatAction(slot) work for the macro slot).

Input Outcome
/cast 5019 Casts Shoot if you know it (any rank, highest known)
/cast 1234 Falls through as "unknown spell," same as /cast Foo
/cast Shoot Unchanged — names still work
/cast 5019(Rank 1) Falls through — rank suffix with a numeric stem isn't supported (use the name form if you need a specific rank)

Implemented as a single hook on the engine's name → spellbook-slot resolver at 0x004B3950 — covers /cast, /castsequence aliases, CastSpellByName, the macro parser's tagging path, and anywhere else the engine resolves a spell name. Numeric input with garbage trailing characters falls through unchanged.

CastSpellNoToggle as a macro cast line

Putting CastSpellNoToggle("Shoot") in a macro body now tags the macro with Shoot's spellID the same way /cast Shoot or CastSpellByName("Shoot") would. Without this, the macro casts correctly but the macro parser doesn't know to associate the slot with any spell — so action-bar UIs that call IsCurrentAction(slot) / IsAutoRepeatAction(slot) (pfUI's actionbar, ShaguTweaks, etc.) never light up the slot.

Recognized forms in a macro body:

CastSpellNoToggle("Shoot")        -- by name (string)
CastSpellNoToggle("Cat Form")     -- shapeshift / aspect / stance all tag the same way
CastSpellNoToggle(5019)           -- by spellID

The engine's first-match-wins rule still applies — if your macro is:

/cast Wand
CastSpellNoToggle("Shoot")

…the macro is tagged with Wand (the engine recognized /cast Wand on line 1; our additional pattern is only consulted when the engine didn't find any of its own patterns).

Macro tagging happens at macro edit/save time. Existing macros need to be opened in the Macro UI and re-saved once after dropping in the new DLL to pick up the new parser behavior.

StopMacro()

Stops the macro body that is running right now. The lines after the call do not run. This is what /stopmacro calls once its conditions pass.

StopMacro()

The call applies to the macro that is running it. If a macro line runs a second macro, a StopMacro in that second macro stops the second one only, and the first one goes on. A call made while no macro is running does nothing, and leaves nothing behind for the next macro you press.

RunMacroText(text [, button])

Runs text as a macro. Each line runs as a line of a saved macro runs: a slash command runs, and any other line goes to chat. # lines are skipped, and /stopmacro stops the lines after it.

RunMacroText("/target Hogger\n/cast Fireball")
RunMacroText("/cast [button:2] Frost Nova; Fireball", "RightButton")
Argument Type Description
text string The macro text. Separate the lines with \n. The first 1023 bytes are used.
button string (optional) The button that the macro runs with. GetRunningMacroButton() returns it, and [button:N] tests it.

If text is not a string or a number, the function raises the error Usage: RunMacroText("text" [, "button"]).

GetRunningMacro()

Returns the index of the saved macro that is running now. Returns nil when no macro is running, and while a RunMacroText text runs, because that text is not a saved macro.

macroIndex = GetRunningMacro()

If a macro line runs a second macro, the function returns the index of the second macro while it runs. Then it returns the index of the first macro again.

GetRunningMacroButton()

Returns the button that the running macro was started with, or nil.

button = GetRunningMacroButton()
Started by Button
RunMacroText(text, button) button
A secure button with the macro type The clicked button, for example "RightButton"
A key bound with SetBindingMacro "LeftButton"
An action bar button nil

If a macro line runs a second macro, the function returns the button of the second macro while it runs. Then it returns the button of the first macro again.

Macro names in GetMacroInfo / EditMacro / PickupMacro / DeleteMacro

The four take a macro name wherever they take an index. A name matches the macro that GetMacroIndexByName finds. An unknown name is treated like an index with no macro, so code can clear macros that may not exist:

  • GetMacroInfo returns nils.
  • PickupMacro and DeleteMacro do nothing.
  • EditMacro changes no macro. It returns the index of the first empty macro slot, or 0 when all slots are full.
DeleteMacro("MyScratchMacro")              -- no error if it doesn't exist
local name, texture, body = GetMacroInfo("MyMacro")
PickupMacro("MyMacro")

Numbers and numeric strings ("3") are still indexes.

GetMacroBody(macro)

Returns the text of a macro. macro is the macro's index or its name. If no macro is there, the function returns nothing.

/dump GetMacroBody(1)
/dump GetMacroBody("MyMacro")

GetMacroSpell(macroSlot)

Returns (name, rank, spellID) for the spell that a macro shows. When the macro has a #showtooltip or #show directive, this is the spell that the directive resolved to. Otherwise it is the spell that the first /cast / /castsequence / CastSpellByName(...) line resolves to. Returns nothing when the macro slot is empty, the macro has no cast, the shown value is an item, or the name does not resolve to a spell that the player knows.

-- macro slot 1's body: "/cast Fireball(Rank 5)"
local name, rank, id = GetMacroSpell(1)
-- name = "Fireball", rank = "Rank 5", id = 25306

-- macro slot 2's body: "/cast Fireball"  (no explicit rank)
local name, rank, id = GetMacroSpell(2)
-- name = "Fireball", rank = "Rank 5", id = 25306  (the highest known rank)

-- macro slot 3's body: only /script lines or no cast
GetMacroSpell(3)
-- (no returns)

CastSpellNoToggle("<name>") macros are also recognized — the parser hook from the CastSpellNoToggle section tags them with the same spellID a /cast line would, so GetMacroSpell sees them too.

Re-save existing macros once after dropping in the DLL. The spellID cache is populated at edit time. Macros that existed before ClassicAPI loaded (or that use CastSpellNoToggle and were last edited under stock 1.12) will have a stale 0 cache — opening them in the Macro UI and clicking Okay re-runs the parser and the new behavior takes effect.

GetMacroItem(macroSlot)

Returns (name, link) for the item that a macro shows: the item its #showtooltip or #show line resolved to, or else its first action line. Returns nothing when the macro shows a spell or nothing. If you carry the item, the name and the link are those of your item, with its suffix and enchant.

-- macro slot 4's body: "#showtooltip Healthstone" then "/use Healthstone"
local name, link = GetMacroItem(4)
-- name = "Major Healthstone", link = "|cffffffff|Hitem:9421:0:0:0|h[Major Healthstone]|h|r"

SetMacroSpell(macro, spell [, target]) / SetMacroItem(macro, item [, target])

Sets the spell or the item that a macro's action button shows. The button then follows it, as for a #showtooltip value: icon, cooldown, count, usable state and range.

SetMacroSpell("Heal", "Flash Heal", "focus")   -- show Flash Heal, range to focus
SetMacroItem(3, "Hearthstone")                 -- show the Hearthstone
Argument Type Description
macro number or string The macro's index, or its name.
spell string SetMacroSpell only. A spell in your spellbook, by name, with an optional (Rank N).
item string SetMacroItem only. An item you carry, by name, or item:N or an item link.
target string (optional) A unit token or a name. The button's range check uses this unit in place of your target.

Both functions return 1 when they find the spell or the item. If they do not find it, they return nil, and the button is greyed. If no macro has that index or name, they return nothing.

The setting stays until you edit the macro, or until the interface reloads. Then the macro shows what its own lines resolve to again. The unit of target is found when you call the function. If that unit goes away, the button has no range state.

If spell or item is not a string or a number, the function raises the error Usage: SetMacroSpell(macro, spell [,target]) or Usage: SetMacroItem(macro, item [,target]).

GetMacroIcons / GetMacroItemIcons / GetLooseMacroIcons / GetLooseMacroItemIcons

A 4-function append-to-table icon enumeration surface. Each takes a Lua table as its only argument and appends icon basenames; returns nothing (mutation is the contract).

local spellList, itemList = {}, {}

-- Modern's canonical call order (mirrors IconDataProvider.lua):
GetLooseMacroIcons(spellList)
GetLooseMacroItemIcons(itemList)
GetMacroIcons(spellList)
GetMacroItemIcons(itemList)

-- spellList now has `Ability_*` / `Spell_*` basenames;
-- itemList now has `INV_*` basenames.

The split is loose (icons you dropped into Interface\Icons\ on disk) vs mpq (icons that ship inside the game's MPQ archives), crossed with Spell (basenames that start with Ability_ or Spell_) vs Item (basenames that start with INV_).

Quirk worth noting: GetMacroIconInfo(i) lists only the Ability_* and Spell_* icons, plus the question-mark icon (INV_Misc_QuestionMark), which is always entry 1. A macro needs that icon for #showtooltip to show the spell or item icon. The archives hold thousands of INV_* icons that it never shows. These four functions do return them, so GetMacroItemIcons gives you the item icons that GetMacroIconInfo cannot reach.

An INV_* file that you put in Interface\Icons\ yourself is different: GetMacroIconInfo does list it, after a restart of the client.

Each appended entry is the uppercase basename stripped of the Interface\Icons\ prefix and any .blp/.tga extension (e.g. "INV_SWORD_25", "ABILITY_KICK"). Callers concat the prefix themselves before texture:SetTexture(...). Output is alphabetically sorted within each function (sorted lazily on first read).

Capture-time dedup ensures each unique basename only appears once per source/prefix combination even though most files flow through multiple scan callbacks. There is no cross-dedup between loose and mpq, or between spell and item (a file that exists both as a loose drop-in and inside an MPQ will appear in both the loose and mpq lists).

The legacy globals GetNumMacroIcons / GetMacroIconInfo remain available unchanged.

C_Macro.CreateMacro / C_Macro.EditMacro

These functions create or edit a macro. You give the icon as a texture string — a full Interface\Icons\<name> path or a bare <name> basename — instead of a GetMacroIconInfo index.

C_Macro.CreateMacro(name, iconTexture, body, isCharacterMacro) -> index
C_Macro.EditMacro(index, name, iconTexture, body) -> index

Why this exists: the stock CreateMacro / EditMacro globals take an icon index into the GetMacroIconInfo list. That list does not include INV_* item icons. As a result, the stock API cannot set an item icon, even though the engine stores and shows it correctly. These two functions write the icon basename directly. Any texture under Interface\Icons\, including an item icon, can be saved.

CreateMacro(name, iconTexture, body, isCharacterMacro):

  • name — the macro name. This argument is necessary.
  • iconTexture — a string, for example "INV_Sword_25" or "Interface\\Icons\\INV_Sword_25". The function removes the Interface\Icons\ prefix, and adds it again when a caller reads the icon. A nil value sets no icon.
  • body — the macro text. A nil value sets an empty body.
  • isCharacterMacro — a true value puts the macro in the per-character tab. A false or nil value uses the general tab.
  • The function returns the 1-based index of the new macro. It returns nil when the list is full (18 macros per tab).

EditMacro(index, name, iconTexture, body):

  • index — a 1-based slot number or a macro name.
  • name / iconTexture / body — a nil argument keeps that field unchanged.
  • The function returns the 1-based index of the macro. It returns nil when index does not match a macro that exists.
-- item icon that legacy CreateMacro cannot set:
local i = C_Macro.CreateMacro("Bag", "INV_Misc_Bag_08", "/say hi", false)
-- change only the icon; name and body stay:
C_Macro.EditMacro(i, nil, "INV_Potion_51", nil)
-- edit by name:
C_Macro.EditMacro("Bag", nil, nil, "/wave")

The iconTexture argument must be a string. CreateMacro reads a number as no icon. EditMacro reads a number as no change. For the numeric-index path, use the legacy globals.

The legacy CreateMacro / EditMacro globals do not change. They stay index-only. String-icon callers use the C_Macro namespace instead. Edits persist across sessions, the same as edits in the Macro UI.

C_Macro.GetMacroIcon(macroSlot)

Returns the icon the macro currently shows, as Interface\Icons\<name>. Returns nothing when the slot is empty.

C_Macro.GetMacroIcon(1)   -- "Interface\\Icons\\Spell_Nature_Rejuvenation"

This is the icon on the action button: the spell's or item's when the macro shows one and the macro's own icon is the question mark, and the macro's own icon otherwise.

GetMacroInfo returns the icon the macro stores, which is the icon its icon selector edits. Use this function when you want the icon a macro shows, and GetMacroInfo when you want the one the player picked.

C_Macro.SetMacroDisplay(macroSlot, value [, unit])

Tells the client what a macro is about, so the action button shows it. For an addon that parses macros itself and wants the button to follow, instead of replacing GetActionTexture, GetActionCooldown, IsUsableAction and the rest in Lua.

C_Macro.SetMacroDisplay(1, "Frostbolt")                -- show this
C_Macro.SetMacroDisplay(1, "Flash Heal", "mouseover")  -- show this, range to the mouseover
C_Macro.SetMacroDisplay(1, false)                      -- mine, nothing matched, show ?
C_Macro.SetMacroDisplay(1, nil)                        -- released, `#showtooltip` resumes

macroSlot is the macro index, the same one GetMacroInfo and GetMacroSpell take. value takes every form a #showtooltip value takes: a spell name or ID, spell:N, an item name, item:N, an item link, an inventory slot, or bag slot. An item you carry wins over a spell of the same name, as in /cast. A spell you name by ID shows even when the player has not learned it, the same as a directive that names its own value.

Returns true when the value named a spell or an item.

The range unit. unit is optional. It names the unit that the button's range check uses in place of your target. Usually it is the [@unit] of the clause that your parser matched. It takes any unit token or GUID string that UnitExists takes, for example "mouseover", "focus" or "party2target". If you give no unit, or nil, the range check uses your target.

The client finds the unit again each time it checks the button's range. So if "mouseover" moves to a different unit, the range follows it with no new call. The same is true for "focus", "target" and every other token. This is different from the target of SetMacroSpell, which is found once, when you call it.

If the token points at no unit at that time, for example "mouseover" with nothing under the cursor, the range check uses your target. If the unit is too far away for the client to see it, for example a party member in another zone, the button has no range state.

The function ignores unit when value is false, nil or an empty string, because the macro then aims at nothing. A call with the same value and a different unit changes the unit. If unit is not a unit token, the function raises the error Unknown unit name: <unit>, the same error that UnitExists gives. Then it publishes nothing.

What the button picks up. The spell is written where the client keeps each macro's spell, which is the field its own buttons read. So cooldown, range, usable, out-of-mana greying, the current-cast highlight and auto-repeat all follow, with no function replaced. The count, cooldown and consumable state for an item follow too. The tooltip follows only when the macro has a #showtooltip line. A macro with #show, or with no directive, keeps its own name as the tooltip. The icon appears on the action button, on the cursor while you drag the macro, and in the macro window. C_Macro.GetMacroIcon reads it back.

The icon replaces the macro's own only when that is the question mark, which is the same rule #showtooltip follows. A macro with a chosen icon keeps it.

Call it whenever your answer changes. Nothing is re-evaluated for you, except the unit that a unit token points at. The value stands until you publish another, and it survives the client re-reading the macro, so an edit elsewhere cannot quietly replace it.

Publishing takes the macro over: #showtooltip is not parsed for it while you hold it, and nil gives it back.

For macro addons that currently take over the action bar. ClassicAPI stands down from macro display entirely when SuperCleveRoidMacros is loaded, since it owns the bar with its own conditionals. A build that drives this instead sets CleveRoids.ClassicAPIMacroDisplay = true, which lifts that. Ownership is then per macro: published ones show what you published, and ones you do not claim fall back to #showtooltip. Forks that do not set the flag keep the old all-or-nothing behavior, so an older one cannot end up fighting for the same buttons.

Mail

Mail-attachment link getters. Both accept the optional attachmentIndex arg, even though only one attachment per message is supported here — pass-through compatibility. Any attachmentIndex other than 1 is treated as no-op.

Both functions return (link, itemID). itemID is cheap to surface at the same lookup site and saves callers a string.match on the link.

GetSendMailItemLink([attachmentIndex])

Returns the fully-decorated per-instance hyperlink for the item currently attached to the outgoing-mail slot, plus that item's itemID. Same enchant / random-suffix / unique-ID decoration as GetContainerItemLink would return for the same item back in the player's bag — the attached item still exists as a live CGItem until the mail is sent, so the link builder has full instance state to draw from.

Returns nothing when:

  • No item is attached.
  • attachmentIndex is anything other than nil or 1.
  • The attachment's CGItem can't be resolved (shouldn't normally happen — engine writes the GUID on attach and clears it on detach).
local link, itemID = GetSendMailItemLink()
if link then
    ChatFrame1:AddMessage("Attached: " .. link .. " (id " .. itemID .. ")")
end

GetInboxItemLink(messageIndex[, attachmentIndex])

Returns the basic itemID-only hyperlink (|cff…|Hitem:N:0:0:0|h[Name]|h|r) for the item attached to inbox message messageIndex (1-based), plus the itemID.

Why basic-link instead of per-instance: inbox entries do store per-instance modifiers inline (enchant at +0x12C, suffix factor at +0x134, random property at +0x138 — the same fields GameTooltip:SetInboxItem copies onto the tooltip to render a fully- decorated preview). We intentionally ignore them in the link: the itemID-only link builder (FUN_0061E290(itemID)) ignores the per-instance fields. Per-instance data only fully manifests on the client side once the player takes the item out of the mail and the engine spawns a real CGItem; the inbox-entry fields are display-only hints used by the tooltip builder.

Errors on missing or non-numeric messageIndex. Returns nothing when:

  • messageIndex is out of range (< 1 or > GetInboxNumItems()).
  • The message slot is empty / has no attached item.
  • attachmentIndex is anything other than nil or 1.
for i = 1, GetInboxNumItems() do
    local link, itemID = GetInboxItemLink(i)
    if link then
        ChatFrame1:AddMessage(i .. ": " .. link .. " (id " .. itemID .. ")")
    end
end

Map

The closed world map follows the player's zone

While the world map is closed, it stays on the zone that you are in. The map goes back to your zone at these times:

  • when you close the map
  • when you enter the world
  • when you go into a new zone

GetPlayerMapPosition("player") reads the zone that the map is set to. As a result, code can read your position without setting the map first:

-- world map closed, after you go into a new zone
local x, y = GetPlayerMapPosition("player")   -- your position in this zone

While the map is open, a zone change does not move it. The map stays on the zone that you look at.

C_Map.GetAreaInfo(areaID)

Returns the localized AreaTable.dbc name for an area id — zones and subzones alike — or nil for non-numeric / non-positive input or an id with no row. AreaTable.dbc is a client DBC (always loaded, synchronous, localized), so the name is available for any id with no async/cache step — unlike creature/item names.

C_Map.GetAreaInfo(12)   -- "Elwynn Forest"
C_Map.GetAreaInfo(9)    -- "Northshire Valley"

The name is in the client's active locale — the same source as GetRealZoneText and the paperdoll zone name — so it matches what the game displays everywhere.

C_Map.GetAreas()

ClassicAPI extension. Returns { [areaID] = name, … } — every AreaTable.dbc row with a non-empty localized name, as an id→name map. Same "surface the hidden DBC" pattern as C_Map.GetAreaTriggers.

The enumeration — not the per-id GetAreaInfo — is what name databases need: name→id resolution, reverse search, and iterating the full id set. Every key round-trips with GetAreaInfo; empty-name rows are omitted. The map includes every named area (technical/internal ones too), so consumers wanting only "real" zones should filter.

local areas = C_Map.GetAreas()
print(areas[12])                 -- "Elwynn Forest"

-- build a name -> id index
local byName = {}
for id, name in pairs(areas) do byName[name] = id end

Lets a name-DB addon drop its shipped zone-name tables (pfQuest's db/<locale>/zones.lua): repoint pfDB["zones"]["loc"] = C_Map.GetAreas() (merging so the fork's overrides still apply). Live client data in the active locale — no locale/rename drift vs a scraped table.

C_Map.GetBestMapForUnit(unitToken)

Returns the AreaTable.dbc area ID the given unit is currently in, or nil if the unit isn't trackable. There is no UiMap.db2 concept here, so the closest equivalent to a "best map" is the zone-level AreaTable ID — exactly what the engine itself tracks for the local player (GetRealZoneText reads it) and what gets broadcast for party/raid members via SMSG_PARTY_MEMBER_STATS_FULL.

Coverage:

  • "player" — always works.
  • "party1".."party4" — works even for members in other zones / on other continents (slot-indexed GUID table stays populated regardless of local CGUnit availability).
  • "raid1".."raid40" — same; covers the full raid roster.
  • "target", "mouseover", "focus" and any other token that resolves to the player or a group member — works.
  • NPCs, other players not in your group — nil.
/dump C_Map.GetBestMapForUnit("player")
-- 1519                                            (Stormwind City)
/dump C_Map.GetBestMapForUnit("party3")
-- 876                                             (party member on GM Island)
/dump C_Map.GetBestMapForUnit("target")
-- nil                                             (target is an NPC)

The returned ID is an AreaTable.dbc area ID, not a UI map ID. Stormwind City here is 1519. Addons that hardcode UI map IDs need a translation table (or, simpler: compare against IDs this same function produces).

C_Map.GetMapAreaIDs()

ClassicAPI extension. Returns { [mapName] = areaID, … } — each WorldMapArea.dbc map's name (the dir the engine uses for its map textures) mapped to its AreaTable areaID.

The stock client has no GetCurrentMapAreaID, and GetMapZones() enumerates only a continent's outdoor zones — so a browsed city or instance map (Orgrimmar, Undercity, Alterac Valley) can't be resolved to a zone id, which means addons can't match zone-keyed content (a flight master, a POI) to it. This maps those names to their zone ids:

-- resolve a browsed map to an areaID, cities included
local areaID = C_Map.GetMapAreaIDs()[GetMapInfo()]

The key is the WorldMapArea name, which matches GetMapInfo()'s returned dir for the vast majority of maps — but a few Blizzard map folders are spelled differently from the area name (the Orgrimmar map's folder is "Ogrimmar"), so GetMapAreaIDs()[GetMapInfo()] can miss for those. For those, resolve by zone name instead (GetMapZones → name → areaID).

Continent-spanning rows (areaID 0) and unnamed rows are omitted. Pairs with C_Map.GetAreas (areaID → name) and C_Map.GetMapWorldSize (areaID → yards).

C_Map.GetMapOverlays([areaID])

ClassicAPI extension. Returns every WorldMapOverlay.dbc entry for a zone — explored or not — with the tile grid fully resolved. The engine's own GetNumMapOverlays / GetMapOverlayInfo only expose overlays the character has discovered (the exploration gate is a server-fed list), which is why map-reveal addons traditionally ship hand-measured overlay tables; this replaces those with a live DBC read that works for every zone on any client.

With no argument, returns the overlays of the world map's currently viewed zone (the same continent/zone selection GetMapInfo resolves — so it drops straight into a WorldMapFrame_Update hook). With a numeric areaID (AreaTable.dbc id, the same identity C_Map.GetBestMapForUnit returns), returns that zone's overlays from anywhere.

Each overlay table:

field meaning
textureName bare DBC name ("GOLDSHIRE")
texturePath Interface\WorldMap\<dir>\GOLDSHIRE (engine format)
areaID primary AreaTable id the overlay reveals (first nonzero)
areaIDs array of all AreaTable ids it reveals (usually one; 52 of 707 overlays span 2–3 subzones)
textureWidth / textureHeight the DBC placement rect
offsetX / offsetY placement on the zone canvas
mapPointX / mapPointY DBC map-point fields
hitRectTop / hitRectLeft / hitRectBottom / hitRectRight hit rectangle in world-map canvas px (≈1002×668); the tight clickable bounds of the landmass
tileCols / tileRows / upscaled the resolved tile grid
tiles ready-to-draw tile array (below)
fileDataIDs ordered tile texture paths (redundant with tiles[].file)

The hitRect* fields are the source addons use to place a subzone center (the texture rect includes transparent padding, so its center is off; the hit rect is the tight landmass bounds). Normalize against the ≈1002×668 world-map canvas:

local cx = (ov.hitRectLeft + ov.hitRectRight) / 2 / 1002 * 100  -- center X %
local cy = (ov.hitRectTop  + ov.hitRectBottom) / 2 /  668 * 100  -- center Y %
local w  = (ov.hitRectRight  - ov.hitRectLeft) / 1002 * 100      -- width  %
local h  = (ov.hitRectBottom - ov.hitRectTop)  /  668 * 100      -- height %

Overlays with areaID > 0 cover the [subzone → parent-zone + center/size] mapping that map addons hand-scrape (pfQuest's pfDB["zones"]["data"]). Reading it live matches this client's maps — on a modified client (Turtle) those differ from the scraped numbers, which is the point.

A row may be texture-less — a clickable subzone region with no distinct art. It still appears, with textureName == "", zero textureWidth/ textureHeight, and an empty tiles table (so ipairs(ov.tiles) iterates zero times), carrying its areaID/areaIDs + hitRect*. Filter art consumers on ov.textureName ~= "". (Stock 1.12 and Octo happen to give every overlay a texture, but the schema permits texture-less rows.)

Each tiles entry: file (texture path including the tile number, SetTexture-ready), width / height (draw size in map pixels), texCoordX / texCoordY (right/bottom texcoords), offsetX / offsetY (absolute canvas position). Drawing an overlay is a dumb loop:

for _, ov in ipairs(C_Map.GetMapOverlays()) do
    for _, t in ipairs(ov.tiles) do
        local tex = pool:Acquire()
        tex:SetTexture(t.file)
        tex:SetWidth(t.width)  tex:SetHeight(t.height)
        tex:SetTexCoord(0, t.texCoordX, 0, t.texCoordY)
        tex:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", t.offsetX, -t.offsetY)
        tex:Show()
    end
end

Why tiles exists (don't derive the grid yourself). The canonical layout is ceil(w/256) × ceil(h/256) tiles numbered row-major, and on stock 1.12 data that's always right — but Octo's shipped data breaks it in three distinct ways: DBC rects that round away a narrow sliver column (Icepoint's Kaneq'nuun has a real 8px third column; trusting ceil shears the whole overlay into misplaced strips), unrelated foreign tiles appended to an overlay's number sequence (drawing by file count scatters duplicate landmass), and same-rect re-exports at higher resolution (Stonetalon's Windshear Crag: 4 real tiles for a 1-tile rect, all of which must be kept and scaled). ClassicAPI disambiguates per overlay from the actual BLP header dimensions via the engine's own VFS (a sweep of all 707 overlays found 108 with such art/DBC mismatches), caches the result for the session, and hands back tiles that are simply correct. textureWidth/textureHeight remain the raw DBC rect — reliable for placement; it's only the tile count they imply that lies.

C_Map.GetMapWorldSize([areaID])

Returns a zone's world size in yards as width, height — the physical extent the map represents, read straight from the WorldMapArea.dbc placement rect. Addons use it to turn map-relative percents into yard distances (range rings, "N yards away" readouts, proximity checks).

Takes an areaID (AreaTable.dbc id — the same identity C_Map.GetBestMapForUnit returns and C_Map.GetMapOverlays accepts), returns that zone's size; with no argument, the world map's currently viewed zone (same resolution GetMapOverlays() uses with no arg). Returns nil, nil when the zone can't be resolved.

width is the east-west span (world Y), height the north-south span (world X) — the same denominators behind the C_Map.GetAreaTriggerInfo map-percent transform, so mapX/100 * width and mapY/100 * height give a point's offset in yards within the zone.

local w, h = C_Map.GetMapWorldSize(85)   -- Tirisfal: ~4518.7, 3012.5

C_Map.GetMapInfo(uiMapID)

Returns a map's details table, or nil for an id that names no map:

field meaning
mapID the id you passed
name localized map name
mapType Enum.UIMapType: 1 world, 2 continent, 3 zone, 4 dungeon
parentMapID the map one level up, or 0 at the top
flags always 0

What a uiMapID is. Maps form a three-level tree:

World
 +-- Continent (Kalimdor, Eastern Kingdoms)
      +-- Zone (Durotar, Elwynn Forest, ...)

A zone is named by its positive AreaTable.dbc area id — the same identity C_Map.GetBestMapForUnit returns. The world and continent levels have no area id, so they use negative ids: Kalimdor is -13, Eastern Kingdoms is -14, and the world map is -694. Instance maps (dungeons, raids, battlegrounds) are addressed the same negative way. The two ranges never overlap, so every function in this section accepts either kind.

Read the ids from GetMapChildrenInfo, GetFallbackWorldMapID, or GetBestMapForUnit rather than writing them in by hand.

/dump C_Map.GetMapInfo(14)
-- mapID=14, name="Durotar", mapType=3, parentMapID=-13
/dump C_Map.GetMapInfo(-13)
-- mapID=-13, name="Kalimdor", mapType=2, parentMapID=-694

The world map names the whole world but carries no coordinate rect, so the coordinate functions return nil for it.

C_Map.GetMapChildrenInfo(uiMapID)

Returns an array of C_Map.GetMapInfo tables for the maps one level below uiMapID — the continents of the world map, or the zones of a continent. A zone has no children, so it gives an empty table. Always returns a table.

local continents = C_Map.GetMapChildrenInfo(C_Map.GetFallbackWorldMapID())
-- 2 entries: Kalimdor (-13), Eastern Kingdoms (-14)

local zones = C_Map.GetMapChildrenInfo(-13)
-- 35 entries: Durotar (14), Mulgore (215), ...

The zone list covers every map a continent has a world map for, city maps included.

C_Map.GetMapInfoAtPosition(uiMapID, x, y)

Returns the C_Map.GetMapInfo table for the zone at a position on uiMapID, or nil when the point is on no zone. x and y are map-relative, each 0..1.

Pass a continent id to answer "which zone sits under this point on the continent map" — the hit test behind clicking a continent.

/dump C_Map.GetMapInfoAtPosition(-13, 0.589, 0.52)
-- Durotar

C_Map.GetFallbackWorldMapID()

Returns the uiMapID of the whole-world map — the safe default to show when no specific map is known. Returns 0 if the client has no world map.

local worldID = C_Map.GetFallbackWorldMapID()   -- -694

C_Map.MapHasArt(uiMapID)

Returns whether the map has a drawable background — that is, whether the client ships the map's tile art.

This is independent of whether a map has coordinates. The whole-world map has art but no coordinate rect, while an instance map can have a rect and no art.

C_Map.MapHasArt(14)     -- true   (Durotar)
C_Map.MapHasArt(-13)    -- true   (Kalimdor)
C_Map.MapHasArt(-600)   -- false  (an instance map with no art)

C_Map.GetMapArtLayers(uiMapID)

Returns the map's art layers, as an array of layer descriptions. Always returns a table; a map with no art gives an empty one.

This client draws a map's background as one fixed layer, so there is at most a single entry, and it reads the same for every map that has art:

Field Value
layerWidth / layerHeight 1002, 668 — the map canvas in pixels
tileWidth / tileHeight 256, 256
minScale / maxScale 1, 1
additionalZoomSteps 0

Dividing the canvas by the tile size gives the grid the background is cut into — four tiles wide by three tall, the twelve GetMapArtLayerTextures returns:

local layer = C_Map.GetMapArtLayers(14)[1]
local cols = math.ceil(layer.layerWidth / layer.tileWidth)    -- 4
local rows = math.ceil(layer.layerHeight / layer.tileHeight)  -- 3

The scale range is 1 to 1 with no extra zoom steps because a map does not zoom here — changing zoom switches between the continent and zone maps instead of scaling one.

C_Map.GetMapArtLayerTextures(uiMapID, layerIndex)

Returns the background tile textures for one of a map's art layers, in draw order — left to right, then top to bottom. Always returns a table.

A map here has a single art layer, so layerIndex 1 gives the background and any other index gives an empty table. A map with no art gives an empty table too.

local tiles = C_Map.GetMapArtLayerTextures(14, 1)
-- 12 entries, "Interface\WorldMap\Durotar\Durotar1" through "...Durotar12"

for i, path in ipairs(tiles) do
    _G["MyMapTile" .. i]:SetTexture(path)
end

The tiles form a grid four wide and three tall of 256-pixel squares — the layout the world map itself draws.

Entries are texture paths, ready to pass to SetTexture, in place of the numeric file ids: a texture is named by its path here. The same substitution appears in C_Map.GetMapOverlays for its fileDataIDs.

C_Map.GetPlayerMapPosition(uiMapID, unitToken)

Returns a unit's position inside a zone as a Vector2DMixin. Call :GetXY() for the two map-relative coordinates (each 0..1), or read .x / .y. Returns nil when the unit's world position is outside that zone — a different zone, or a map with no world rect.

uiMapID is a zone or continent id (see C_Map.GetMapInfo) — pass a continent to get the unit's position on the continent map. The result comes from the unit's live world coordinates, so it is current the moment you call it and does not depend on which map the world map frame shows.

local uiMapID = C_Map.GetBestMapForUnit("player")
local pos = C_Map.GetPlayerMapPosition(uiMapID, "player")
local x, y = pos:GetXY()   -- e.g. 0.595, 0.600

Works for the player and any unit whose object is loaded (in range or targeted). Other units return nil. An unknown unit token raises an error, the same as UnitHealth and the other unit functions. In a city or subzone, GetBestMapForUnit can return a subzone id that has no world rect — pass a zone-level id for those.

C_Map.GetWorldPosFromMapPos(uiMapID, mapPosition)

Converts a map-relative position on a zone to absolute world coordinates. Returns continentID, worldPosition:

  • continentID — the Map.dbc map the zone sits on (0 Eastern Kingdoms, 1 Kalimdor, or an instance map).
  • worldPosition — a Vector2DMixin of the world coordinates (.x north, .y west).

uiMapID is a zone or continent id (see C_Map.GetMapInfo). mapPosition is any table with .x / .y in 0..1 (a Vector2DMixin, or the result of C_Map.GetPlayerMapPosition). Returns nil when the zone has no world rect. This is the inverse of C_Map.GetMapPosFromWorldPos.

local uiMapID = C_Map.GetBestMapForUnit("player")
local pos = C_Map.GetPlayerMapPosition(uiMapID, "player")
local continentID, world = C_Map.GetWorldPosFromMapPos(uiMapID, pos)

C_Map.GetMapPosFromWorldPos(continentID, worldPosition[, overrideUiMapID])

Converts absolute world coordinates to a map-relative position. Returns uiMapID, mapPosition:

  • uiMapID — the AreaTable.dbc zone the point falls in.
  • mapPosition — a Vector2DMixin with .x / .y in 0..1.

continentID is a Map.dbc map id. worldPosition is a table with .x / .y world coordinates. Without overrideUiMapID, the result is the zone the point falls in. Pass overrideUiMapID (a zone or continent id) to force the result into that specific map. The call then returns nil when the point lies outside it. This is the inverse of C_Map.GetWorldPosFromMapPos.

local uiMapID, pos = C_Map.GetMapPosFromWorldPos(continentID, world)

C_Map.GetMapRectOnMap(uiMapID, topUiMapID)

Returns where a zone sits on its continent map, as four numbers: minX, maxX, minY, maxY — the zone's bounding rectangle in 0..1 continent coordinates. Returns nothing (nil) when the zone has no world rect.

uiMapID is the zone. topUiMapID is the map to measure against, normally the zone's continent (see C_Map.GetMapInfo). When topUiMapID names no usable map, the zone's own continent is used.

local minX, maxX, minY, maxY = C_Map.GetMapRectOnMap(14, 1)  -- Durotar on Kalimdor

A zone placed off the edge of the standard continent map can return values outside 0..1.

C_Map.SetUserWaypoint(uiMapPoint)

Places the user waypoint — the single player-set map pin — and returns whether it was set. Fires USER_WAYPOINT_UPDATED.

uiMapPoint is a table with these fields:

field meaning
uiMapID the map the pin sits on
position a vector2 with .x / .y in 0..1
z optional height

Build one with UiMapPoint.CreateFromCoordinates(uiMapID, x, y[, z]) or UiMapPoint.CreateFromVector2D(uiMapID, position[, z]).

C_Map.SetUserWaypoint(UiMapPoint.CreateFromCoordinates(14, 0.5, 0.6))

Returns false for a malformed point, or for a map that cannot hold a pin (see C_Map.CanSetUserWaypointOnMap). The pin lasts for the session and survives a UI reload.

C_Map.GetUserWaypoint() / C_Map.HasUserWaypoint() / C_Map.ClearUserWaypoint()

GetUserWaypoint returns the pin as a UiMapPoint table, or nil when none is set. HasUserWaypoint returns whether one is set. ClearUserWaypoint removes it and fires USER_WAYPOINT_UPDATED.

local point = C_Map.GetUserWaypoint()
if point then
    local x, y = point.position:GetXY()
end

C_Map.GetUserWaypointPositionForMap(uiMapID)

Returns the pin's position on uiMapID as a vector2, or nil when the pin does not fall on that map.

The pin is stored against one map, so this converts it to whichever map you are drawing. A pin dropped on a zone resolves on that zone's continent, and the reverse.

-- pin at (0.5, 0.6) on Durotar (14)
local pos = C_Map.GetUserWaypointPositionForMap(-13)   -- on Kalimdor
-- 0.589, 0.534

GetUserWaypointHyperlink returns a link describing the pin, or nil when none is set. GetUserWaypointFromHyperlink reads such a link back into a UiMapPoint, or nil when the string is not one.

The payload is worldmap:uiMapID:x:y, with both coordinates multiplied by 10000.

/dump C_Map.GetUserWaypointHyperlink()
-- "|cffffff00|Hworldmap:14:5000:6000|h[Map Pin Location]|h|r"

GetUserWaypointFromHyperlink takes either a whole link or the bare worldmap: payload, so it can read a link out of chat text.

C_Map.CanSetUserWaypointOnMap(uiMapID)

Returns whether a pin can be placed on the given map. A map needs a coordinate rect to hold one, so zones, continents, and instance maps accept a pin while the whole-world map does not.

C_Map.CanSetUserWaypointOnMap(14)    -- true  (Durotar)
C_Map.CanSetUserWaypointOnMap(-694)  -- false (the world map)

C_Map.GetAreaTriggerInfo(triggerID) / C_Map.GetAreaTriggers([mapID])

ClassicAPI extension. Exposes AreaTrigger.dbc — the client's 517 static trigger volumes (subzone-entry, exploration, and teleport triggers). The engine loads the DBC but exposes none of its geometry to Lua, which is why map addons (pfQuest et al.) ship hand-scraped trigger tables; this is a live DBC read, same "surface the hidden data" pattern as C_Map.GetMapOverlays.

  • GetAreaTriggerInfo(triggerID) — one trigger's info table, or nil for a missing / out-of-range id.
  • GetAreaTriggers([mapID]) — an array of every trigger's info table, optionally filtered to a single Map.dbc id (0 Eastern Kingdoms, 1 Kalimdor, 30 Alterac Valley, 489 Warsong Gulch, …).

Each info table carries both the authoritative raw geometry and a derived, ready-to-draw form:

field meaning
id AreaTrigger.dbc row id
mapID Map.dbc id (continent, or an instance map)
x / y / z continent-space world coordinates of the center
radius sphere-trigger radius (0 for a box trigger)
isBox true when the trigger is an oriented box
boxLength / boxWidth / boxHeight / boxYaw box dims + yaw (all 0 for a sphere)
areaID AreaTable zone the point falls in — absent if unresolved
mapX / mapY position as a 0..100 zone-relative percent — absent alongside areaID

The raw x/y/z come straight from the DBC. areaID/mapX/mapY are derived from the WorldMapArea.dbc zone rect (WoW's world axes: +X north, +Y west). mapX is the horizontal map axis (off world Y), mapY the vertical (off world X) — the standard WoW / pfQuest / Astrolabe convention:

mapX% = (locLeft - y) / (locLeft - locRight)  * 100   -- horizontal
mapY% = (locTop  - x) / (locTop  - locBottom) * 100   -- vertical

The zone is chosen by which one's drawn landmass the point sits on: WorldMapArea rects are loose overlapping boxes, so a point near a border falls in several; the WorldMapOverlay hit rects trace the real landmass, and the zone the point is deepest inside an overlay of wins (the runtime stand-in for an ADT area lookup). When the point is on no zone's overlay it falls back to the most-interior containing rect; when no rect contains it at all (open sea, an instance with no WorldMapArea row, a degenerate rect), the three derived fields are simply omitted — the raw world coords are always present.

-- One trigger, its position as a world-map percent point:
local t = C_Map.GetAreaTriggerInfo(2)
if t.areaID then
    print(t.areaID, t.mapX, t.mapY)  -- 85  21.89  67.87  (Tirisfal)
end

-- Every trigger in Warsong Gulch:
for _, t in ipairs(C_Map.GetAreaTriggers(489)) do
    -- t.x/y/z world coords, t.radius or t.isBox geometry, t.mapX/mapY
end

Matches the pfQuest-turtle trigger table 1:1. Keyed by trigger id, the resolved areaID + mapX/mapY line up with pfQuest-turtle's scraped areatrigger-turtle.lua (id 2 → {21.89, 67.87, 85} Tirisfal; id 45 → {68, 17, 796}). One difference in kind: a scraped table may list a trigger under several maps at once (a subzone and its parent continent); this resolves to the single zone whose drawn landmass the point sits on (via the WorldMapOverlay hit rects — the runtime stand-in for an ADT area lookup). An addon can drop GetAreaTriggers() straight in place of a shipped table.

MerchantFrame

The C_MerchantFrame.* calls and GetBuybackItemLink give the items and the buyback list of a vendor.

The vendor window must be open for GetItemInfo, GetNumJunkItems and the sell functions. Without a vendor, GetNumJunkItems returns 0, the sell functions do nothing and GetItemInfo returns nil. The buyback getters do not need an open window. After you close the window, they still give the last buyback list.

C_MerchantFrame.GetItemInfo(slot)

Returns a table describing the merchant's item at the given 1-based slot, or nil if no merchant is open / the slot is out of range.

Table fields:

Field Type Notes
itemID number Item record key, suitable for C_Item.GetItemInfoInstant / cache lookups.
price number Copper cost per stack (multiply by stackCount for unit price).
stackCount number Units delivered per purchase (1 for most equipment, e.g. 5 for stacks of cloth).
numAvailable number Limited-supply count, or -1 for unlimited stock.
isPurchasable boolean Always true in this build — there is no "blocked from buying" flag.
isThrottled boolean Always false — there's no anti-spam concept here.
hasExtendedCost boolean Always false — currency/honor-cost merchants don't exist here.
local info = C_MerchantFrame.GetItemInfo(1)
if info then
    print(info.itemID, info.price, info.stackCount)
end

Reads directly from the 28-byte merchant entry at VAR_MERCHANT_ITEMS + (slot-1) * MERCHANT_STRIDE (the same flat array Script_GetMerchantItemInfo walks). Compared to the existing GetMerchantItemID, this returns the wider struct shape in one call.

C_MerchantFrame.GetBuybackItemID(slot)

Returns the item ID of the item in a buyback slot, 1 to 12. Returns nil if the slot is empty.

for slot = 1, GetNumBuybackItems() do
    local id = C_MerchantFrame.GetBuybackItemID(slot)
    if id then
        print(slot, id)
    end
end

GetBuybackItemLink(slot)

Returns the item link of the item in a buyback slot, 1 to 12. Returns nil if the slot is empty. The link has the enchant and the random suffix of the item that you sold.

for slot = 1, GetNumBuybackItems() do
    print(slot, GetBuybackItemLink(slot))
end

C_MerchantFrame.GetNumJunkItems()

Returns the count of grey-quality (LE_ITEM_QUALITY_POOR) items in the player's bags 0..4 that SellAllJunkItems would sell. Returns 0 when no merchant frame is open — the count is gated on merchant context, since it's meant as a "what would the sell-junk button do right now" signal rather than a passive inventory query.

-- Inside a MERCHANT_SHOW handler:
local junk = C_MerchantFrame.GetNumJunkItems()
if junk > 0 then
    print("Selling " .. junk .. " junk item(s)")
    C_MerchantFrame.SellAllJunkItems()
end

Quality is read from each item's ItemStats cache record at the m_quality field (OFF_ITEMSTATS_QUALITY = 0x1C). Items with unloaded cache records (rare for items in your own bags) are skipped — same conservative behavior the sell path uses.

C_MerchantFrame.SellAllJunkItems()

Sells every quality-0 item in the player's bags to the open merchant. No-op when no merchant frame is open.

Sells are dispatched one per frame via the shared WorldTick subscriber, not in a tight loop within the call — the network path drops packets when CMSG_SELL_ITEM is flooded (a 10-item burst consistently lost the 2nd-to-last sell in testing). Calling pace matches click-by-click selling. For 10 junk items, expect the queue to drain in ~10 frames (~150ms at 60fps).

C_MerchantFrame.SellAllJunkItems()

If the player closes the merchant frame or opens a different vendor mid-drain, the remaining queue is discarded rather than mis-routed to the new merchant.

Each sell is delivered via the engine's internal MerchantSellItem helper (FUN_MERCHANT_SELL_ITEM, opcode CMSG_SELL_ITEM/0x1A0) called with count = 0 ("sell whole stack"). Bypasses Script_UseContainerItem entirely — addons hooking UseContainerItem will not see these sells, and there's no risk of the use-not-sell dispatch branch firing if the merchant state changes mid-loop.

C_MerchantFrame.IsMerchantItemRefundable(slot)

Always returns false. There is no refund mechanic here; the function exists for API parity so addons that gate behavior on refundability don't break.

if not C_MerchantFrame.IsMerchantItemRefundable(slot) then
    -- always taken in this build
end

C_MerchantFrame.IsSellAllJunkEnabled()

Always returns true. There is no client setting to disable the sell-all-junk button, so the feature is always on. The function exists so addons that gate SellAllJunkItems on this don't no-op silently.

MapExplorationInfo

C_MapExplorationInfo.GetExploredMapTextures and GetUnexploredMapTextures (ClassicAPI extension) are the exploration-filtered views of a zone's WorldMapOverlay.dbc overlays. Together with C_Map.GetMapOverlays (which returns all overlays) they form the three-way split:

function returns
C_MapExplorationInfo.GetExploredMapTextures(areaID) only overlays the player has discovered
C_MapExplorationInfo.GetUnexploredMapTextures(areaID) only overlays not yet discovered
C_Map.GetMapOverlays(areaID) all overlays, explored or not

All three return the same per-overlay table shape (see GetMapOverlays). These two only filter it. An overlay counts as explored when the player has explored one of its areas. This is the same rule that GetNumMapOverlays and GetMapOverlayInfo use. The areaID argument works for any zone, not only the zone on the world map. With no argument, both functions use the current map view.

C_MapExplorationInfo.GetExploredMapTextures([areaID])

Returns an array of the overlay tables for overlays the local player has discovered in the zone — same shape as C_Map.GetMapOverlays, including fileDataIDs (here it holds SetTexture-ready texture paths, since there are no numeric fileDataIDs). Empty before the player is resident (character select).

for _, tex in ipairs(C_MapExplorationInfo.GetExploredMapTextures(12)) do
    -- tex.fileDataIDs[i] -> SetTexture path; tex.offsetX/offsetY placement
end

C_MapExplorationInfo.GetUnexploredMapTextures([areaID])

ClassicAPI extension — the complement: overlays the player has not discovered, the pieces a map-reveal addon draws over the fogged base map. The related C_Map.GetMapOverlays returns everything. Same table shape and areaID semantics as the explored getter.

C_MapExplorationInfo.GetExploredAreaIDsAtPosition(uiMapID, normalizedPosition)

Returns the explored areas at a point on a map, as an array of AreaTable IDs. Returns nil when there are none.

  • uiMapID — the map to look on.
  • normalizedPosition — a CreateVector2D(x, y) value (or a {x = …, y = …} table). Both values go from 0 to 1 across the map.

An area is at the point when its revealed map art covers the point. The match follows the exact shape of the art, not a rectangle around it. A point on bare base map gives nil, even inside the bounds of an area's art. When art overlaps, the most visible area comes first. Unexplored areas are never in the result.

Use the first ID that has a name to label the map under the cursor:

local ids = C_MapExplorationInfo.GetExploredAreaIDsAtPosition(mapID, CreateVector2D(x, y))
local name = ids and C_Map.GetAreaInfo(ids[1])

Model

These methods are on PlayerModel frames unless the heading says otherwise. DressUpModel and TabardModel frames are PlayerModel frames too, so they have the same methods. A PlayerModel sets its own light and camera, so a model shows correctly without SetLight or SetCamera.

model:SetDisplayInfo(displayID)

Shows a creature on the model by its display ID (a CreatureDisplayInfo ID). The model gets the creature's skin textures.

A character-based display (an NPC on the shared Character\<Race>\<Sex> model) is dressed the same way a spawned NPC is: baked body texture, hair, face, facial hair, and equipment, with the attached helm and shoulder models. The dressing starts when the model file finishes loading, a short time after the call. Weapons do not show: the server sends NPC weapons only for spawned units, and no client table holds them.

A display ID of 0, or an ID that is not in CreatureDisplayInfo, does not change the model. The model stays on the display until the next SetDisplayInfo, SetCreature, or SetUnit call. This is also true when you hide the frame and show it again.

local m = CreateFrame("PlayerModel", "PreviewModel", UIParent)
m:SetWidth(200)
m:SetHeight(260)
m:SetPoint("CENTER", 0, 0)
m:SetDisplayInfo(330)   -- a human NPC

model:SetCreature(creatureID)

Shows a creature on the model by its creature ID (the creature's database ID, not a display ID). The model is the same as SetDisplayInfo with the creature's display ID.

The creature must be in the client creature cache. The client adds a creature to the cache when you see it, target it, or put the mouse on it. The client also loads the cache from creaturecache.wdb at login. If the creature is not in the cache, the call does not change the model. To show such a creature, request its data. When the data arrives, call SetCreature:

local creatureID = 448
C_CreatureInfo.RequestLoadCreatureByID(creatureID)

local f = CreateFrame("Frame")
f:RegisterEvent("CREATURE_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()
    if arg1 == creatureID and arg2 then   -- (creatureID, success)
        PreviewModel:SetCreature(creatureID)
    end
end)

model:SetPortraitZoom(zoom)

Moves the camera between the full-body view and the model's portrait view. 0 is the full-body view (the default). 1 is the portrait view: the head and shoulders, as in a unit-frame portrait. A value between 0 and 1 gives a view between the two.

A model that has no portrait camera keeps the full-body view.

-- a 3D portrait for a unit frame
local p = CreateFrame("PlayerModel", nil, PlayerFrame)
p:SetWidth(64)
p:SetHeight(64)
p:SetPoint("TOPLEFT", PlayerFrame, "TOPLEFT", 0, 0)
p:SetUnit("player")
p:SetPortraitZoom(1)

model:SetCamDistanceScale(scale)

Changes the distance of the camera from the model. 1 is the default distance. 2 moves the camera two times farther away. 0.5 moves it half the distance. The method uses 0.1 for a value less than 0.1. A value of 0 or less is an error.

This works together with SetPortraitZoom: the scale applies to the view that SetPortraitZoom selects.

model:RefreshCamera()

Configures the camera again for the current model. The model configures its camera when it finishes loading. Usually, you do not need this method.

model:SetRotation(rotation [, animate])

Turns the model to a facing, in radians. By default the model turns with a short step animation. If animate is false, the model turns at once, with no animation.

PreviewModel:SetRotation(math.pi / 2, false)

DressUpModel:UndressSlot(slot)

Removes the item in one inventory slot from a DressUpModel. slot is an inventory slot number: 1 head, 3 shoulder, 4 shirt, 5 chest, 6 waist, 7 legs, 8 feet, 9 wrist, 10 hands, 15 back, 16 main hand, 17 off hand, 19 tabard. Other slot numbers do nothing.

DressUpModel:UndressSlot(1)   -- remove the helm

DressUpModel:TryOn(item)

Puts an item on a DressUpModel. item is one of these:

  • An item ID.
  • An item link, or an item: string.
  • A trade-skill recipe link (enchant:). The model tries on the item that the recipe makes.
  • The name of an item that you carry, equipped or in your bags.

If the item is a pattern, plan, or other recipe item, the model tries on the item that the recipe makes. Thus Control-click on a pattern opens the dressing room with the item that the pattern makes.

If item is none of these, the game shows the error "That item can't be equipped". The model chooses the hand for a weapon itself.

DressUpModel:TryOn("|cff1eff00|Hitem:2077:0:0:0|h[Magician Staff]|h|r")

Model:SetGlow(value)

This method exists on every Model frame, for code from later clients. It has no effect in the game.

ChatBubbles

C_ChatBubbles.GetAllChatBubbles([includeForbidden])

Returns a 1-based table of the currently-active chat-bubble Frame objects (the speech bubbles above characters who /say or /yell). This replaces the old "iterate over WorldFrame children and guess which are bubbles" idiom — we hand you the exact set instead.

for _, bubble in ipairs(C_ChatBubbles.GetAllChatBubbles()) do
    -- The spoken text lives in the bubble's FontString region.
    for _, region in ipairs({bubble:GetRegions()}) do
        if region.GetText and region:GetText() then
            print(region:GetText())
        end
    end
end

Chat bubbles are CGChatBubbleFrames — full frames the engine creates in C++ without ever calling CreateFrame (same as default nameplates). The returned frames are the engine's canonical wrappers: real, method-capable (:GetRegions(), :IsShown(), :GetPoint(), …), and identical to the object any other addon that touches the bubble sees, so decorations survive. The spoken-text FontString is parented to the bubble, so it shows up in :GetRegions().

includeForbidden is accepted for signature parity and ignored — there are no forbidden frames here, so every active bubble is returned regardless. Returns an empty table when no bubbles are showing. A bubble whose owner just despawned can linger for one frame before the engine prunes it; filter on bubble:IsShown() if that matters.

NamePlate

C_NamePlate.* returns nameplate Frame objects keyed off unit data. The engine doesn't ship the API, but the underlying data (per-unit nameplate pointer at CGUnit + 0xE60) exists. We enumerate visible units via the local-player-anchored object hash table, filter by TYPEMASK_UNIT, and return matches.

The "nameplateN" unit-token family is also supported — see Unit tokens below. NAME_PLATE_UNIT_ADDED / _REMOVED / _CREATED events fire via a per-tick visible-plate diff (see the events section).

nameplateNotSelectedAlpha

The alpha of a default nameplate whose unit is not the current target. At the default 0.5, a non-target plate draws half-faded. At 1, every plate draws at full alpha. The target plate always draws at full alpha. Addons that draw their own plates keep their own settings.

/console nameplateNotSelectedAlpha 1

nameplateSelectedAlpha

The alpha of a default nameplate whose unit is the current target. At the default 1, the target plate draws at full alpha. At 0.5, it draws half-faded like the rest. Addons that draw their own plates keep their own settings.

/console nameplateSelectedAlpha 0.5

C_NamePlate.GetNamePlates()

Returns a 1-based table of nameplate Frame objects — one per CGUnit that currently has an allocated Lua-registered nameplate. The frames are real Lua tables with methods (:GetName(), :GetWidth(), :SetAlpha(), etc.) and any addon-added decorations on them.

local plates = C_NamePlate.GetNamePlates()
for i, plate in ipairs(plates) do
    print(i, plate:GetName(), plate:GetWidth())
end

Two kinds of plates can show up. Both give you the same frame object that every other path gives you for that plate:

  • Addon-created plates (pfUI, TidyPlates, NamePlateMod, and others): built with CreateFrame. The addon puts its own fields and methods on that frame, and you see them here.

  • Default engine plates: built by the engine without CreateFrame. The first call registers the frame with Lua. Every later call gives you that same object.

Identity is stable across calls for both kinds. Two calls give you the same table for one plate, so you can compare frames with ==. Fields that you set on a frame stay on it.

A plate frame comes from a pool. When the unit goes out of range, the engine can give that frame to a different unit. Do not store a frame across that point. When you need plates, call GetNamePlates() again.

Reading region content from a default nameplate

Default plates have six child regions in stable positions. Walk them with :GetRegions():

local plates = C_NamePlate.GetNamePlates()
for _, plate in ipairs(plates) do
    local regions = {plate:GetRegions()}
    local name  = regions[3]:GetText()              -- e.g. "Joseph Dalton"
    local level = tonumber(regions[4]:GetText())    -- e.g. 60
    -- regions[1], [2], [5], [6] are textures (border, healthbar,
    -- glow, raid-icon — order depends on the engine's draw order)
end

Lua 5.0 has no select(), so collect into a table via {plate:GetRegions()} and index. Addon-created plates have different region layouts — those frames inherit whatever shape the addon built, not this one.

C_NamePlate.GetNamePlateForUnit(unitToken)

Returns the nameplate Frame for a single unit (resolved via the engine's token-to-GUID path, so out-of-range party/raid members work too), or nil if the unit has no allocated nameplate.

local plate = C_NamePlate.GetNamePlateForUnit("target")
if plate then
    local regions = {plate:GetRegions()}
    print("targeting:", regions[3]:GetText())   -- e.g. "Santora"
end

The frame object is the same one that GetNamePlates() gives you for that plate. Do not store the result across the unit going out of range. The engine can give that frame to a different unit.

C_NamePlate.GetNamePlateForGUID(guidString)

Same as GetNamePlateForUnit but takes the "0xHHHHHHHHHHHHHHHH" GUID-string form. Useful when you've stored a unit GUID across events (e.g., converted the positional "nameplateN" token to a GUID via UnitGUID(arg1) at NAME_PLATE_UNIT_ADDED time) and need the frame later.

local platesByGuid = {}

local f = CreateFrame("Frame")
f:RegisterEvent("NAME_PLATE_UNIT_ADDED")
f:SetScript("OnEvent", function()
    -- arg1 = "nameplateN" token; convert to stable GUID for storage
    local guid = UnitGUID(arg1)
    platesByGuid[guid] = C_NamePlate.GetNamePlateForGUID(guid)
end)

Returns nil if the GUID doesn't parse, doesn't resolve to a visible CGUnit, or the unit has no allocated nameplate.

C_NamePlate.GetNamePlateGUIDs()

Returns a 1-based table of GUID strings ("0xHHHHHHHHHHHHHHHH" format) — one per CGUnit with an allocated nameplate, regardless of whether the frame has been registered with Lua. Catches default engine nameplates that GetNamePlates can't surface as frames.

/dump C_NamePlate.GetNamePlateGUIDs()
-- { "0xF13000C36C26FD02", "0xF130000009276912", ... }

Walks the local-player-anchored object hash table for TYPEMASK_UNIT entries, filters by *(unit + 0xE60) != nullptr. The per-unit nameplate pointer is set by FUN_006086E0's "show nameplate" path regardless of which nameplate system rendered it. Order follows hash-bucket iteration and isn't stable across calls.

Unit tokens (nameplateN)

"nameplate1", "nameplate2", … work as unit tokens against every UnitX function: UnitName, UnitGUID, UnitClass, UnitHealth, UnitHealthMax, UnitLevel, UnitFaction, UnitReaction, UnitExists, UnitIsPlayer, UnitIsEnemy, UnitIsDead, etc. — ~30 functions for free.

-- A slot can be vacant, so skip gaps. Do not stop at the first one.
for i = 1, 40 do
    local token = "nameplate" .. i
    if UnitExists(token) then
        print(i, UnitName(token), UnitClass(token))
    end
end

Indices are assigned slots: a plate keeps its slot for its entire lifetime, so surviving plates are never renumbered when another plate is removed. A removed plate frees its slot, and the next new plate reuses the lowest free slot. So a middle plate vanishing leaves the others' nameplateN tokens unchanged (that slot simply becomes vacant until a new plate reuses it) — UnitExists("nameplateN") returns false for a currently-free slot. Stable for the lifetime of a single plate; no reordering, ever.

Token chains work too — "nameplate1target", "nameplate1targettarget", etc. — by mirroring the engine's own targettarget-style suffix walker (read UNIT_FIELD_TARGET off m_objectFields, loop). Other suffixes (pet, master) aren't supported by the engine's own walker either, so they don't compose.

Out-of-range indices return nil cleanly without raising "Unknown unit name" — UnitExists("nameplate99") just returns false.

Unit events fire for "nameplateN". Every unit event fires with arg1 == "nameplateN" for a unit that has a nameplate, as it does for a party token. This includes UNIT_HEALTH, UNIT_AURA, UNIT_COMBAT, and UNIT_NAME_UPDATE. The events start after NAME_PLATE_UNIT_ADDED and stop after NAME_PLATE_UNIT_REMOVED.

Implementation note. We hook FUN_TOKEN_TO_GUID (the central token→GUID resolver) so the entire Script_Unit* surface gains the new token form transparently. The hook is gated by an SStrCmpI prefix check against "nameplate"; non-nameplate tokens ("player", "target", "partyN", etc.) fall straight through to the unmodified resolver. The ordered list is maintained alongside the existing NAME_PLATE_UNIT_ADDED / _REMOVED diff in the per-tick nameplate walker.

Unit tokens (markN)

"mark1" … "mark8" work as unit tokens against every UnitX function, resolving to whichever unit currently wears that raid-target marker (mark1 = star, mark2 = circle, … mark8 = skull — the same 1–8 order as SetRaidTarget / GetRaidTargetIndex):

if UnitExists("mark8") then                       -- something is skull-marked
    print(UnitName("mark8"), UnitHealth("mark8"))
end

UnitExists("markN") returns false when that marker is unset, when the marked unit is out of range / not synced, or when the marker is on a non-unit (a marked corpse, gameobject, or loot). Token chains compose — "mark1target", "mark1targettarget" — via the same suffix walker as the other families.

Unit events fire for "markN". Every unit event fires with arg1 == "markN" for the marked unit, as it does for "target". This includes UNIT_HEALTH, UNIT_AURA, UNIT_COMBAT, and UNIT_NAME_UPDATE:

local f = CreateFrame("Frame")
f:RegisterEvent("UNIT_HEALTH")
f:SetScript("OnEvent", function()
    if arg1 == "mark8" then
        -- skull's health changed; UnitHealth("mark8") is fresh here
    end
end)

A unit that you mark while it is out of range fires its events when it comes into range. A marker on something that is not a unit fires no events.

Unit tokens (GUID literals)

A raw 64-bit GUID literal — "0x" followed by up to 16 hex digits (e.g. "0xF130000000000A5") — works as a unit token against the same UnitX surface, so a GUID can be passed anywhere a token is expected:

local guid = UnitGUID("target")        -- e.g. "0xF13000..."
print(UnitName(guid), UnitHealth(guid), UnitClass(guid))

Hex parsing is case-insensitive; the parsed GUID is handed to the engine's own object-manager lookup (the identical path "player" takes), so a GUID for a unit that isn't currently loaded (out of range, not synced) resolves to nil exactly like any absent unit — no error. Suffix chains compose: "0x…target" walks to the referenced unit's target.

This is the input direction only — to obtain a token's GUID use UnitGUID.

Compatible with SuperWoW. SuperWoW provides the same GUID-input support, so when it's loaded we detect it and defer to its resolver (no double handling); when it isn't, this fills the gap. Either way GUID-token input behaves identically, so addons needn't care whether SuperWoW is present.

IsUnitToken(value)

Returns true when value is a unit token, and false when it is anything else, such as a character name.

Several functions come in pairs, one that takes a token and one that takes a name, and each raises an error on the other kind. Use this to pick between them:

if IsUnitToken(x) then TargetUnit(x) else TargetByName(x) end
if IsUnitToken(x) then FollowUnit(x) else FollowByName(x) end

That is the common case for conditional slash commands, because SecureCmdOptionParse returns either kind from the same expression. /target [@focus] gives the token focus, and /target Bob gives the name Bob.

The answer covers every token family the client accepts, including nameplateN, markN, focus, GUID literals, and suffix chains such as focustarget.

The question is whether the text names a token, not whether a unit is there. So "target" with nothing targeted, and "party3" while solo, are both true. A missing or non-string argument is false.

A name that reads like a token is treated as the token. Call the by-name function directly to reach a player named Target.

ClassicAPI extension.

NameCache

GUID-keyed cache of player names and classes. The engine itself maintains an in-memory NameCache at 0x00C0E228, populated by SMSG_NAME_QUERY_RESPONSE — but it isn't exposed to Lua, and it doesn't survive /reload. This module surfaces it as GetPlayerInfoByGUID and adds an opt-in persistent layer that survives /reload and full client restarts.

C_PlayerCache.SetEnabled turns on the on-disk cache. It is off by default. Without it, the module only reads the engine's in-memory cache.

Three sources feed the cache when it is on:

  1. Name-query responses. Every SMSG_NAME_QUERY_RESPONSE the engine processes is copied to the cache. This covers chat, group and raid sync, guild updates, and every player you can see — anything the engine queries a name for.
  2. C_PlayerCache.RememberPlayer (no-op when the cache is off). Lets addons add sources the engine never sees, such as /who results.
  3. Import on enable. When you turn the cache on, it copies every player the engine already knows from this session.

When a player gets a new name, the server tells every client that is online. The cache then forgets the old name. If you were offline during the rename, the cache keeps the old name until the game gets the player's name again.

Storage:

  • WTF\Account\<account>\ClassicAPI.txt — account-level settings (PersistentNameCacheEnabled). The opt-in toggle stays per-account.
  • WTF\ClassicAPI\NameCache-<realm>@<realmList>.txt — one cache file for each realm, shared across every account on the same server. The file name has the realm name and the realmlist address, so two private servers with the same realm name do not share a file. Alts on different accounts use the same file. Each line is one player: the GUID, the name, the realm, the class, the race, and the sex, with tabs between them. The realm is empty for a player on your realm. You can edit the file by hand. If you used the cache before, the old file (WTF\ClassicAPI\<realmList>\<realm>\ClassicAPI_NameCache.txt) is copied into the new file the first time you log in to that realm. The old file and its empty folders are deleted after the new file is saved.

GetPlayerInfoByGUID(guid)

Returns localizedClass, englishClass, localizedRace, englishRace, sex, name, realm for a player GUID the engine has cached, or nil on a cache miss.

local _, class, _, race, sex, name = GetPlayerInfoByGUID(UnitGUID("target"))
-- e.g. "WARRIOR", "NightElf", 2, "Sylphir"

GetPlayerInfoByGUID("0x0000000000000777")  -- same shape for a literal GUID

guid is the "0xHHHHHHHHLLLLLLLL" string returned by UnitGUID. Bare 8-hex "0xLLLLLLLL" (hi-dword zero) is also accepted.

Returns:

| 1 localizedClass | "Warrior" / "Krieger" etc. — from ChrClasses.dbc indexed by locale. | | 2 englishClass | "WARRIOR" (uppercase tag, the same value UnitClass returns as its 2nd return) — ChrClasses.dbc filename field. | | 3 localizedRace | "Human" / "Mensch" / "Humain" etc. — from ChrRaces.dbc indexed by locale. | | 4 englishRace | "Human", "Orc", "Dwarf", "NightElf", "Scourge" (the filename for what addons call Undead), "Tauren", "Gnome", "Troll" — from ChrRaces.dbc filename field. | | 5 sex | 2 = male, 3 = female. Matches UnitSex convention (the cache stores 0/1; we add 2). | | 6 name | Character name. | | 7 realm | The realm that the server sends with the player's name, or "" when the server sends no realm. |

Cache coverage: the engine populates entries from SMSG_NAME_QUERY_RESPONSE. Anything the client has already seen populates the cache: chat (whispers, says, party chat), raid/group events, guild updates, visible objects (target, party, raid in zone). Names of offline friends never seen in chat are not cached — GetPlayerInfoByGUID returns nil for them until the client does something that triggers a name query. This module deliberately does not trigger queries from a passive getter; a future C_PlayerInfo.RequestLoadPlayerByID would do that explicitly and fire a load-result event.

Persistent fallback: when C_PlayerCache.SetEnabled(true) has been opted into, a per-realm on-disk cache extends coverage across sessions. If the engine cache does not have the player, GetPlayerInfoByGUID uses the persistent cache. It returns name, realm, class, race, and sex from storage. Returns nil only when both the engine and persistent caches miss.

Implementation: calls the engine's get-or-fetch primitive at 0x0055F080 with a NULL callback (pure cache read). The cache instance lives at 0x00C0E228; entry layout (name, realm, race, sex, class) was reverse-engineered from the SMSG_NAME_QUERY_RESPONSE write path at 0x0055F310.

UnitNameFromGUID(guid)

Returns name, realm for the player identified by guid, or nil if no player with that GUID has been encountered yet.

It tries four sources in order:

  1. The object manager — any currently-synced unit (a player you can see, your target, a party or raid member, an NPC in range).
  2. Your friends list — it keeps name and GUID for online and offline friends, so a friend resolves even when not synced and never seen in chat. Always on.
  3. The engine name cache — every player name that the client got in this session, in range or not.
  4. The persistent on-disk name cache, when you have enabled it.
local name, realm = UnitNameFromGUID(UnitGUID("target"))
if name then
    ChatFrame1:AddMessage("Target: " .. name)
end

-- Resolving a chat link
local name, realm = UnitNameFromGUID("0x0000000000000777")

realm is the realm that the server sends with the player's name. It is "" when the server sends no realm, and when the name comes from your friends list. It is never nil, so you can test realm == "".

Doesn't trigger a network query on miss. To populate an unknown GUID, either let the engine receive a SMSG_NAME_QUERY_RESPONSE through normal chat/target interaction, or call C_PlayerCache.RememberPlayer explicitly.

C_PlayerCache.GetPlayerInfoByName(name)

Returns localizedClass, englishClass, localizedRace, englishRace, sex, name, realm, guid for a player, or nil if the name is not known. Use it when you have a player's name but not the GUID. For example, a system message names a player in its text, and its arg12 is "".

local _, class, _, race, _, _, _, guid = C_PlayerCache.GetPlayerInfoByName("Gedwyr")
-- "MAGE", "Human", "0x000000003B9ADAA7"

C_PlayerCache.GetPlayerInfoByName("NeverSeen")  -- nil (not known)

The function looks for the name in this order:

  1. Your party and raid members. This includes members that are out of range or in a different zone.
  2. Players near you.
  3. Your friends list, including offline friends.
  4. Players the game knows by name in this session. For example, players who sent a chat message, were in your group, or were near you. The game forgets these players when you log out.
  5. The persistent NameCache, if you enabled it with C_PlayerCache.SetEnabled(true).

Steps 1–4 work when the persistent NameCache is off.

The name must be the full name. Case does not matter ("gedwyr" finds "Gedwyr"). A name without a realm finds a player on your realm. For a player from a different realm, pass "Name-Realm", the form that chat events give for the sender. Spaces in the realm name do not matter. For a "Name-Realm" name, the function uses only steps 4 and 5. The name return is the name as the server stores it, without the realm, not the text you passed in.

The function only finds players, never NPCs. It does not send a query to the server.

Returns (vs. GetPlayerInfoByGUID):

Slot Value Notes
1–7 same as GetPlayerInfoByGUID see above
8 guid "0xHHHHHHHHLLLLLLLL" — the player's full GUID, so you can pass it to GUID-keyed functions. GetPlayerInfoByGUID does not return it, because you already have it.

C_PlayerCache.RememberPlayer(guid, name, classToken)

Adds a (guid → name, classID, raceID, sex) entry to the persistent name cache. For the engine-driven coverage (chat / group / guild / visible objects), no addon-side feeding is needed — those flow into the cache automatically through the engine's SMSG_NAME_QUERY_RESPONSE write path. This call exists for the other sources libunitscan-style addons harvest from but the engine NameCache doesn't see directly: /who results, mouseover, target snapshots, etc.

C_PlayerCache.RememberPlayer(guid, name, classToken [, raceToken [, sex]])

Returns true on success, false if the persistent cache isn't enabled or the required args are malformed.

When the game already has the player's data from the server in this session, the call keeps the server's data. It still returns true.

  • guid — "0xHHHHHHHHLLLLLLLL" string (same format UnitGUID returns). 8-hex "0xLLLLLLLL" is also accepted (hi-dword zero).
  • name — the name of a player on your realm, or "Name-Realm" for a player from a different realm. Names in any language work, for example Chinese. The cache keeps at most 47 bytes of a name (15 Chinese characters) and never cuts a character in two. Tabs and line breaks are removed.
  • classToken — uppercase token like "WARRIOR", "MAGE". Looked up against ChrClasses.dbc filename field, case-insensitive. Passing an unknown token keeps the entry's prior class (so a name-only sighting doesn't erase good class data).
  • raceToken (optional) — uppercase token like "NIGHTELF", "SCOURGE" (the filename for Undead). Same resolution as classToken, against ChrRaces.dbc. Omitted/unknown → leaves prior race alone.
  • sex (optional) — 0 (male) or 1 (female), matching the wire-format convention the engine cache stores. UnitSex uses 2/3; pass UnitSex - 2 if you're forwarding that value. Omitted/0 → leaves prior sex alone (so this call can't be used to flip a stored value back to male). The server's data replaces the stored value the next time the game gets the player's name.
-- Harvest /who results into the persistent cache
local function OnWhoUpdate()
    for i = 1, GetNumWhoResults() do
        local name, _, _, _, class, _, _, _, _, _, _, sex = GetWhoInfo(i)
        -- class/race come back localized from GetWhoInfo; the
        -- locale-token tables in pfUI/AceLocale handle the
        -- inverse mapping. RememberPlayer expects engine tokens
        -- ("WARRIOR" etc.).
    end
end

Each non-zero field is updated on the existing entry; zeros are treated as "caller doesn't know" and preserve prior real data. A name-only update (classToken passed but unknown) refreshes the name without erasing class.

The "deleted character recreated with same name, different class" collision case that name-keyed caches suffer from doesn't apply here: GUIDs are permanent for the life of a character, so the new character has a different GUID and replaces the old character's entry.

C_PlayerCache.SetEnabled(enabled)

Opts into (or out of) the persistent name cache. enabled is a boolean (numeric 0/1 also accepted). Persists to WTF\Account\<account>\ClassicAPI.txt as PersistentNameCacheEnabled=1/0, so the choice survives client restarts.

When enabled:

  • Every SMSG_NAME_QUERY_RESPONSE the engine processes is also written to WTF\ClassicAPI\NameCache-<realm>@<realmList>.txt.
  • Lua-side C_PlayerCache.RememberPlayer calls become effective (they're no-ops when the cache is disabled).
  • GetPlayerInfoByGUID gains the cross-session fallback path documented above.
  • Every player the engine already knows from this session is copied to the cache.

When disabled, the on-disk file is left in place (re-enabling later restores the prior contents); future writes are simply suppressed.

C_PlayerCache.SetEnabled(true)

Returns nothing.

C_PlayerCache.IsEnabled()

Returns the current state of the persistent name cache as a boolean. false until SetEnabled has been called (or its prior call survived in the on-disk settings file).

if C_PlayerCache.IsEnabled() then
    -- persistent fallback is active for GetPlayerInfoByGUID
end

NewItems

Bag interfaces use these functions to show a glow on items that are new.

An item is new when you get it after login. Items that you loot, create, buy, get as a quest reward, take from mail, or get in a trade are new. These items do not become new:

  • Items that you had at login.
  • Items that you move between bags, the bank, and your equipment.
  • Loot that goes onto a stack that you already have.
  • The new stack that you make when you split a stack.

The flag stays with the item when it moves to a different slot. Only items in your bags (bags 0 to 4) can be new.

BAG_NEW_ITEMS_UPDATED fires when an item gets or loses the flag. The event has no arguments.

C_NewItems.IsNewItem(bagID, slotIndex)

Returns true if the item currently in bag bagID slot slotIndex is flagged new, false otherwise (empty slot, unflagged item, or non-numeric args). bagID 0 is the backpack; 1–4 are the equipped bags. Slot indices are 1-based, matching GetContainerItemInfo.

if C_NewItems.IsNewItem(0, 3) then
    -- show the "new" glow on backpack slot 3
end

C_NewItems.RemoveNewItem(bagID, slotIndex)

Clears the new flag for the item in that slot, for example after the player sees the item. No-op if the slot is empty or the item wasn't flagged. Fires BAG_NEW_ITEMS_UPDATED when it actually removes a flag. Because the flag is GUID-keyed, the item stays un-flagged even if it's later moved to another slot.

C_NewItems.ClearAll()

Clears every new flag at once. Fires BAG_NEW_ITEMS_UPDATED if anything was flagged.

PaperDollInfo

C_PaperDollInfo.GetInventorySlotInfoForInvSlot(invSlot)

Returns the paper doll slot for an inventory slot number. It is the reverse of GetInventorySlotInfo(slotName), which starts from the slot name. It returns four values:

Return Value
invSlot The slot number.
slotTexture The texture path of the empty slot.
checkRelic true for the ranged slot (18), which can also hold a relic. Otherwise false.
slotName The slot name, for example "HeadSlot".

If no paper doll slot has this number, it returns nothing. An argument that is not a number raises an error.

/dump C_PaperDollInfo.GetInventorySlotInfoForInvSlot(18)
-- 18, "interface\\paperdoll\\UI-PaperDoll-Slot-Ranged.blp", true, "RangedSlot"

C_PaperDollInfo.GetTemporaryEnchantmentInfo(slot)

Returns the temporary enchant on an equipped item as a table. slot is an INVSLOT_ constant, for example INVSLOT_MAINHAND. If the slot has no temporary enchant, the call returns nothing.

Field Value
enchantID The enchant. C_Item.GetEnchantInfo gives its name and effects.
remainingTimeMs The time left in milliseconds. If the enchant has no time limit, the value is 0.
chargesRemaining The charges left. An oil has 0.
hasExpirationTime true if the enchant has a time limit.
local info = C_PaperDollInfo.GetTemporaryEnchantmentInfo(INVSLOT_MAINHAND)
if info then
    print(info.enchantID, info.remainingTimeMs, info.chargesRemaining)
end

C_Item.GetItemTempEnchantInfo reads the same enchant on a weapon in a bag.

PartyInfo

These functions invite players to your group, remove players from your group, and change the group leader. Each function sends a request to the server, and the server decides if the request is valid. If the server refuses a request, the game shows its usual error message. The functions return nothing.

InviteUnit(targetName)

Invites a player to your group by name.

InviteUnit("Allianceman")
Arg Type Description
targetName string The name of the player. The function does not read unit tokens. To invite by unit token, use InviteToParty.

If targetName is your own name, the game shows the error "You cannot invite yourself." If the name is longer than 48 bytes, the function raises the error Name too long. If targetName is not a string, the function does nothing.

UninviteUnit(name)

Removes a player from your group.

UninviteUnit("party1")       -- Remove the first party member
UninviteUnit("Allianceman")  -- Remove a group member by name
Arg Type Description
name string A unit token, such as "party1", or the full name of a group member.

If name is not a string or is empty, the function does nothing.

PromoteToLeader(name [, exactNameMatch])

Makes a group member the group leader. You must be the group leader.

PromoteToLeader("party1")             -- Promote the first party member
PromoteToLeader("Alliance")           -- Promote the best match for "Alliance"
PromoteToLeader("Allianceman", true)  -- Promote only "Allianceman"
Arg Type Description
name string A unit token, such as "party1", or a player name.
exactNameMatch boolean, optional If true, the name must match the full name of the player. The default is false: the function selects the player whose name starts with the longest part of name.

If name is not a string, the function raises the error Usage: PromoteToLeader(name).

Pet

IsPetAttackAction(index)

Returns true if the pet bar slot holds the Attack command. Otherwise it returns false. The index is 1 to 10. If you have no pet, it returns false.

IsPetAttackActive(index) also needs the pet to attack now. IsPetAttackAction does not.

for i = 1, NUM_PET_ACTION_SLOTS do
    if IsPetAttackAction(i) then
        print("Attack is in slot", i)
    end
end

IsAtStableMaster()

Returns true while the stable window is open. Otherwise it returns false.

if IsAtStableMaster() then
    print("Stable slots:", GetNumStableSlots())
end

PlayerInfo

C_PlayerInfo.CanUseItem(itemID)

Returns true if the local player meets the item's use/equip requirements, false otherwise. This is the "would the item be red in the tooltip" gate, and rounds out the item-usability trio:

Function Question it answers
C_Item.IsEquippableItem(item) Does the item fit some slot? (static, player-independent)
IsUsableItem(item) Is the item's on-use ability castable right now?
C_PlayerInfo.CanUseItem(itemID) May this player equip/use it at all?

So this is what answers "a Mail chest on a Mage" (armor proficiency), "a level-40 item at level 20" (RequiredLevel), and class/race-restricted gear.

itemID is a number (item links are also accepted). Checks performed:

  1. Proficiency — for weapons/armor, the engine keeps a per-item-class subclass-proficiency bitmask (fed by SMSG_SET_PROFICIENCY); the item's subclass bit must be set. Item classes with no proficiency concept (consumables, trade goods, …) are unrestricted.
  2. RequiredLevel ≤ player level.
  3. AllowableClass / AllowableRace masks include the player.
  4. RequiredSkill — the player must have the item's skill line at an effective rank ≥ RequiredSkillRank (e.g. a mount that "Requires Riding (150)", a tool that "Requires Engineering (200)").
  5. RequiredSpell — the player must know the item's prerequisite spell (or a higher rank of it): a crafting specialization ("Requires Armorsmith") or a proficiency spell.
  6. RequiredHonorRank / RequiredCityRank — PvP-rank gates (the player's honor rank and earned PvP-medal bitmask).
  7. RequiredReputation — current standing with the item's faction must reach the required reaction band (e.g. "Requires Honored with …").
C_PlayerInfo.CanUseItem(12640)  -- Lionheart Helm (plate) → true on a Warrior, false on a Mage
C_PlayerInfo.CanUseItem(6948)   -- Hearthstone → true (no restrictions)
C_PlayerInfo.CanUseItem(19872)  -- a mount → false without Riding (150)
C_PlayerInfo.CanUseItem(12717)  -- Plans: Lionheart Helm → false without the Armorsmith spec

Synchronous, like C_Item.IsEquippableItem: an uncached item returns false with no async load fired. Warm the cache (C_Item.RequestLoadItemDataByID) and re-check on ITEM_DATA_LOAD_RESULT if needed.

C_PlayerInfo.GUIDIsPlayer(guid) / GUIDIsCreature / GUIDIsPet / GUIDIsGameObject

Type checks on the raw 1.12 GUID format. GUIDs encode the entity type in the high 16 bits of the qword — players have 0x0000 (low dword = player ID), creatures 0xF130xxxx, pets 0xF140xxxx, game objects (herbs / chests / etc.) 0xF110xxxx, items 0x4000xxxx. Each function returns true only for its specific prefix; the "0x0000000000000000" sentinel returns false from all four.

if C_PlayerInfo.GUIDIsPlayer(UnitGUID("target")) then
    -- target is a player
end

-- Combat log triage: did a player or a creature land that hit?
local function OnCombatLogEvent(self, event, timestamp, subevent, hideCaster, sourceGUID)
    if subevent == "SWING_DAMAGE" then
        if C_PlayerInfo.GUIDIsPlayer(sourceGUID) then ... end
    end
end

GUIDIsPlayer takes a single GUID argument; the other three are companions for the most common type-distinction needs. For other types (corpse, dynamic object, transport, item) the underlying classifier exists internally — let us know if you need one exposed.

Accepts either the 16-digit "0xHHHHHHHHLLLLLLLL" form or the 8-digit "0xLLLLLLLL" shortcut (high dword implicitly zero). Malformed input returns false rather than raising — tolerant of stale GUIDs from addon-side caches.

C_PlayerInfo.GetName / GetClass / GetRace / GetSex / IsConnected(playerLocation)

The PlayerLocation-taking player accessors. A PlayerLocation (see PlayerLocation / PlayerLocationMixin) wraps one way to identify a player; these resolve it to a unit and return the requested attribute.

Function Returns
C_PlayerInfo.GetName(playerLocation) name
C_PlayerInfo.GetClass(playerLocation) className, classFilename, classID
C_PlayerInfo.GetRace(playerLocation) raceID
C_PlayerInfo.GetSex(playerLocation) sex (1 neutral / 2 male / 3 female)
C_PlayerInfo.IsConnected([playerLocation]) isConnected
local loc = PlayerLocation:CreateFromUnit("target")
local className, classFile, classID = C_PlayerInfo.GetClass(loc)
local name = C_PlayerInfo.GetName(loc)
local raceID = C_PlayerInfo.GetRace(loc)          -- feed into C_CreatureInfo.GetRaceInfo
local sex = C_PlayerInfo.GetSex(loc)
  • Supported location kinds: unit and guid. Unit locations use their token directly; GUID locations resolve only while the GUID maps to a currently-visible unit (target, party/raid member, someone in range). The chat-line / who / battlefield-score / community / voice kinds have no client-side unit resolution in 1.12 and return nil.
  • Everything is read straight from the engine's own data — no Unit* Lua wrappers are called. GetName uses the polymorphic object name getter (the same path as UnitNameFromGUID); GetSex reads the UNIT_FIELD_BYTES_0 gender byte and reports it as UnitSex's 2/3; IsConnected treats a currently-synced object as online and otherwise consults the group-member roster's online bit, exactly as UnitIsConnected does. IsConnected with no argument defaults to the local player.
  • GetRace / GetClass return the numeric ids that UnitRace / UnitClass don't (those give only names). classID round-trips with C_CreatureInfo.GetClassInfo and raceID with C_CreatureInfo.GetRaceInfo; GetClass's className / classFilename are the same localized name + non-localized token ("WARRIOR") those return.
  • An unresolvable or unsupported location returns nil. A malformed unit token raises (the engine's token→GUID resolver rejects it), same as passing it to UnitName/UnitClass directly.

PvP

SetPVP(enable) / C_PvP.SetPVP(enable)

Asks the server to turn the PvP preference of the player on or off.

  • enable is true or a nonzero number to turn the preference on.
  • false, nil or 0 turns the preference off.

The result does not depend on the current state, as it does with TogglePVP(). The new value comes back with the next update from the server. Then GetPVPDesired() returns it.

GetPVPDesired()

Returns true when the player chose to stay flagged for PvP, else false. This value is the preference, not the flag. For the flag, use UnitIsPVP("player").

IsPVPTimerRunning() / GetPVPTimer()

When the player turns the PvP preference off, the PvP flag stays for five minutes. IsPVPTimerRunning() returns true during these five minutes.

GetPVPTimer() returns a number of milliseconds:

  • While the timer runs, the time left plus 1000.
  • When the countdown is at zero but the flag stays, 999.
  • When no timer runs, 301000.
if IsPVPTimerRunning() then
    print(format("PvP off in %d s", GetPVPTimer() / 1000))
end

The timer starts only when the player turns the preference off while flagged. Other flags show no timer, for example the flag that stays after the player leaves hostile territory. PvP combat restarts the five minutes on the server, but not the countdown. Thus the countdown can reach zero before the flag goes off. The timer stops when the flag goes off, when the player turns the preference on, or at logout.

PVP_TIMER_UPDATE event

Fires with "player" when the timer starts or stops. Then IsPVPTimerRunning() and GetPVPTimer() return the new values.

local f = CreateFrame("Frame")
f:RegisterEvent("PVP_TIMER_UPDATE")
f:SetScript("OnEvent", function()
    if IsPVPTimerRunning() then
        f.endTime = GetTime() + GetPVPTimer() / 1000
    else
        f.endTime = nil
    end
end)

Profiling

These functions measure how much Lua memory and CPU time each addon uses. Addon managers and performance tools read them.

UpdateAddOnMemoryUsage() / GetAddOnMemoryUsage(index or "name")

GetAddOnMemoryUsage returns the Lua memory that an addon uses, in kilobytes. The value comes from the last call to UpdateAddOnMemoryUsage. Call UpdateAddOnMemoryUsage first. Each call counts all memory again, so do not call it every frame.

UpdateAddOnMemoryUsage()
print(GetAddOnMemoryUsage("MyAddon"))   -- 854.04
print(GetAddOnMemoryUsage(3))           -- the third addon in the list
  • The memory of an addon is all Lua memory that the addon's code makes. This includes the memory that its file load, its SavedVariables, and its scripts make.
  • The objects that an XML template makes belong to the addon that defines the template. This is true when a different addon uses the template.
  • Blizzard addons, unknown names, and addons that have not loaded return 0.
  • An index that is not in the range 1..GetNumAddOns() raises an error.

scriptProfile

A setting that turns CPU profiling on (1) or off (0, the default). The CPU functions in this section return 0 while profiling is off. The game reads the setting when the UI loads, so a change starts after /reload.

/console scriptProfile 1
/reload

Profiling makes each Lua function call slower. Turn it on only when you measure.

GetFunctionCPUUsage(func [, includeSubroutines])

Returns two values: the time in milliseconds that the function ran, and the number of calls to it. If includeSubroutines is true (the default), the time includes the functions that it calls. If includeSubroutines is false, the time is only the function's own code.

ResetCPUUsage()
MyAddon_Update()
print(GetFunctionCPUUsage(MyAddon_Update))         -- 11.81  1
print(GetFunctionCPUUsage(MyAddon_Update, false))  -- 0.004  1
  • A recursive call counts in the call count. Its time counts only once.
  • A coroutine that waits in coroutine.yield does not add time.
  • An argument that is not a function raises an error.

UpdateAddOnCPUUsage() / GetAddOnCPUUsage(index or "name")

GetAddOnCPUUsage returns the time in milliseconds that an addon's functions ran. The value comes from the last call to UpdateAddOnCPUUsage. The time of each function counts once, for the addon that made the function.

UpdateAddOnCPUUsage()
print(GetAddOnCPUUsage("MyAddon"))   -- 23.96

GetFrameCPUUsage(frame [, includeChildren])

Returns two values: the time in milliseconds that the frame's scripts ran, and the number of calls to them. Each script counts, OnUpdate included. If includeChildren is true (the default), the values include the child frames of the frame.

print(GetFrameCPUUsage(MyFrame))          -- the frame and its children
print(GetFrameCPUUsage(MyFrame, false))   -- the frame only

A script that you remove with SetScript(name, nil) does not count.

GetEventCPUUsage([event])

Returns two values: the time in milliseconds that event handlers ran, and the number of events that fired. The values are for all events. The event argument has no effect.

print(GetEventCPUUsage())   -- 39.06  27

The event count increases also when profiling is off. The time stays 0 when profiling is off.

GetScriptCPUUsage()

Returns the time in milliseconds that all frame scripts ran. When a script runs inside a different script, the time counts for both scripts.

ResetCPUUsage()

Sets all CPU times and call counts to 0, and the event count too. Memory values do not change.

_classicapi_LuaHeapAudit() / _classicapi_GcTagOf(value)

Two aids for developers who test the memory values.

_classicapi_LuaHeapAudit() returns three values: the bytes of all Lua objects, the total bytes of Lua memory, and the number of objects that belong to an addon. The first value is a little less than the second.

_classicapi_GcTagOf(value) returns the name of the addon that a table, function, userdata, or thread belongs to. It returns nil for an object that the game's own code made. For an object that a macro or /run made, it returns a hexadecimal string such as "0x0083DE24".

print(_classicapi_GcTagOf(MyAddon_Update))   -- "MyAddon"
print(_classicapi_GcTagOf(UIParent))         -- nil

Quest

C_QuestLog.GetInfo(questLogIndex)

Returns a QuestInfo table for one row of the quest log, or nil if the index is not a row of the log. The index counts headers, the same as GetQuestLogTitle(index).

Field Value
title Quest name. For a header row, the header text.
questLogIndex The index you gave.
questID The quest ID. 0 for a header row.
level, difficultyLevel Quest level.
suggestedGroup Always 0. The server does not send it.
frequency Always Enum.QuestFrequency.Default.
isHeader true for a header row.
isCollapsed true for a collapsed header row.
questClassification Always Enum.QuestClassification.Normal.
readyForTranslation Always true.
all other boolean fields Always false.
campaignID, headerSortKey Always nil.

The table does not show if the quest is complete. Use C_QuestLog.GetQuestLogTitle for that.

for i = 1, GetNumQuestLogEntries() do
    local info = C_QuestLog.GetInfo(i)
    if info and not info.isHeader then
        print(info.questID, info.title, info.level)
    end
end

C_QuestLog.GetQuestLogTitle(questLogIndex)

title, level, suggestedGroup, isHeader, isCollapsed, isComplete, frequency,
questID, startEvent, displayQuestID, isOnMap, hasLocalPOI, isTask, isBounty,
isStory, isHidden, isScaling = C_QuestLog.GetQuestLogTitle(questLogIndex)

Returns the same row data as C_QuestLog.GetInfo, as a list of values. Returns nothing if the index is not a row of the log.

  • isHeader and isCollapsed are true or false.
  • isComplete is 1 when the objectives are done, -1 when the quest failed, and nil for all other cases.
  • suggestedGroup is always 0. frequency is always Enum.QuestFrequency.Default. questID is 0 for a header row.
  • startEvent and all values after questID are always false.

The third value is suggestedGroup, not the quest tag. The global GetQuestLogTitle(index) still returns the tag ("Elite", "Dungeon", …) in its third value.

C_QuestLog.GetQuestIDForLogIndex(index)

Returns the questID (Quest.dbc row ID) for the entry at the given 1-based quest log index. The stock GetQuestLogTitle doesn't return it, even though the engine has it internally.

  • Returns the questID for real quests.
  • Returns 0 for header rows (zone / category dividers).
  • Returns nil if the index is out of range.
for i = 1, GetNumQuestLogEntries() do
    local title, level, questTag, isHeader, isCollapsed, isComplete
        = GetQuestLogTitle(i)
    local questID = C_QuestLog.GetQuestIDForLogIndex(i)  -- 0 for headers
    -- ...
end

C_QuestLog.GetLogIndexForQuestID(questID)

The inverse of GetQuestIDForLogIndex: returns the 1-based quest log index of questID, or nil if the quest isn't in the log. The index spans the full entry list (headers included), so it can be passed straight to GetQuestLogTitle(index) / GetQuestLogLeaderBoardID(...) and the other index-based quest-log accessors.

local index = C_QuestLog.GetLogIndexForQuestID(questID)
if index then
    local title = GetQuestLogTitle(index)
    -- ...
end

C_QuestLog.GetHeaderIndexForQuest(questID)

Returns the 1-based log index of the collapsible header (zone / "Dungeon" / class-sort category, …) that questID sits under, or nil if the quest isn't in the log. The quest log is laid out header-then-its-quests, so this locates the quest and walks back to the nearest preceding header row. Pass the result to GetQuestLogTitle(headerIndex) for the category name.

local headerIndex = C_QuestLog.GetHeaderIndexForQuest(questID)
if headerIndex then
    local zone = GetQuestLogTitle(headerIndex)  -- e.g. "Westfall"
end

C_QuestLog.RequestLoadQuestByID(questID)

Asks the engine to fetch the static data for questID (title, description, objectives, reward text) from the server if not already cached. Returns no values — fire-and-forget.

Fires QUEST_DATA_LOAD_RESULT(questID, success) when the data lands in the cache. Synchronously fired when the data was already cached (so polling code paths still work), asynchronously fired after SMSG_QUEST_QUERY_RESPONSE lands when the engine had to round-trip to the server.

success is 1 or nil. The engine's printf-style event dispatcher has no %b token, so we encode the boolean as 1 for success / nil for failure. if success then ... works as expected (nil falsy, 1 truthy). Same encoding as ITEM_DATA_LOAD_RESULT and GET_ITEM_INFO_RECEIVED.

Server limitation: for invalid questIDs (ones the server doesn't have), the server silently drops the query — it doesn't send a "not found" response packet. The engine's pending callback never resolves, so QUEST_DATA_LOAD_RESULT doesn't fire with success=0 either. Addons that need to handle "request timed out" should use their own timer (the Lua polyfill at !!!ClassicAPI/Util/QuestUtil.lua uses a 180-second wait).

local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()
    -- vanilla 1.12: event payload is in `arg1`, `arg2`, ... globals
    if event == "QUEST_DATA_LOAD_RESULT" and arg2 == 1 then
        local questID = arg1
        -- title/description/objectives are now in the engine's quest cache
    end
end)
C_QuestLog.RequestLoadQuestByID(2)

C_QuestLog.IsOnQuest(questID)

Returns true if questID is currently in the player's quest log (either still in progress or ready to turn in — both states live in the log). Returns false for invalid input (non-positive, non-number, or simply not in the log).

if C_QuestLog.IsOnQuest(215) then
    -- player has "Jungle Secrets" accepted
end

Walks the same VAR_QUEST_LOG_ENTRIES array C_QuestLog.GetQuestIDForLogIndex reads and matches against each real entry's questID, skipping the zone/category header rows.

Cheaper than the engine's IsUnitOnQuest(logIndex, unit), which requires the addon to walk the log to find the matching index first and resolves a unit token even when the answer is just about the player. Same answer in a single call.

C_QuestLog.IsUnitOnQuest(unit, questID)

Returns true if the unit has questID in their quest list. Arg order is unit first, questID second — and questID-keyed rather than log-index-keyed like the stock IsUnitOnQuest(logIndex, unit).

if C_QuestLog.IsUnitOnQuest("party1", 215) then
    -- partymate is also on Jungle Secrets
end

For unit == "player" equivalent to C_QuestLog.IsOnQuest(questID). For other tokens (party/raid members, target), the unit must be in the engine's sync range — the data comes from SMSG_QUESTGIVER_QUEST_DETAILS broadcasts, which only reach you while the other player is within the client's sync window. Returns false for NPCs, units out of range, and units that haven't synced their quest list yet.

Returns false (no lua_error) for invalid input: non-string unit, non-number / non-positive questID, unresolvable token, or unit that's not player-controlled (i.e., a creature). The UNIT_FLAG_PLAYER_CONTROLLED gate is mandatory — without it the +0xE68 deref would AV on any NPC.

C_QuestLog.GetTitleForQuestID(questID)

Returns the title (string) for questID from the engine's quest static-info cache, or nil if the data isn't loaded. Doesn't require the quest to be in the player's quest log — works for any questID once its data has been fetched. Header rows are excluded (their titles live in QuestSort.dbc, not in this cache); for those you'd use the existing GetQuestLogTitle.

The cache is populated lazily — by the engine's own quest-log path when the player has the quest, or explicitly by C_QuestLog.RequestLoadQuestByID. If the title isn't there yet, queue a load and read on the event:

local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()
    if event == "QUEST_DATA_LOAD_RESULT" and arg2 == 1 then
        local title = C_QuestLog.GetTitleForQuestID(arg1)
        if title then
            -- title is now available
        end
    end
end)
C_QuestLog.RequestLoadQuestByID(215)

C_QuestLog.GetQuestDetails(questID)

Returns a table with every field we know how to read from the engine's quest static-info cache for questID, or nil if the data isn't loaded. One round-trip alternative to calling GetTitleForQuestID / GetQuestLogRewardMoney / etc. across each of its individual accessors. Quest-helper-style addons that want structured data per quest are the primary consumers.

Like GetTitleForQuestID, this is a pure cache probe — pair it with C_QuestLog.RequestLoadQuestByID and listen for QUEST_DATA_LOAD_RESULT if the quest isn't already cached.

Returned table

Field Type Notes
questID number Echo of the input.
title string Locale-applied.
level number Quest level from the static record.
questType string | absent Localized tag string — "Elite", "Group", "PvP", "Dungeon", "Raid" — sourced from QuestInfo.dbc. Field is omitted for plain quests with no tag.
rewardMoney number Reward copper. 0 if the quest requires money instead.
requiredMoney number Copper the player must hand in to complete (e.g. quartermaster contributions). 0 for quests with no money requirement.
rewardMoneyAtMaxLevel number The level-60 reward bonus. Added to rewardMoney when the player is level 60; populated even on low-level quests.
rewardSpellID number spellID of a spell taught on completion (e.g. profession recipes), or 0 for no learned spell reward.
srcItemID number itemID the questgiver hands the player on accept (e.g. the sigil in "Verdant Sigil" — given on accept, read by the player, then turned back in). 0 = no source item. Same as the quest's requirements[].id for "give-then-return" quests.
questFlags number Raw QUEST_FLAGS_* bitfield — only bit 0x08 (sharable) is positively confirmed-tested by the engine; other bits are presumed-stored but unverified.
isSharable boolean Convenience extraction of bit 0x08 from questFlags.
description string The questgiver's narrative text. Raw — printf-style $N (player name), $C (class), $R (race) tokens are not substituted; use string.gsub if you need runtime values.
objectives string The "what you must do" summary text, same raw / un-substituted format as description.
endText string The end text of the quest, in the raw format of description. Most quests have no end text, and the string is empty.
poi table | absent Point-of-interest marker ({mapID, x, y, opt}). Set on quests with a server-supplied "go here" location; omitted when mapID == 0.
rewardItems array of {id, count} Items the quest gives unconditionally on turn-in. Empty when there are no fixed rewards.
choiceItems array of {id, count} "Pick one" reward items. Empty when none.
requirements array of {kind, id, count, text} Objectives. kind is "monster" (creature), "object" (gameobject), or "item" (collect / interact). text is the questgiver's per-objective override (e.g. "Investigate the cave" instead of the auto-generated "Mor'shan Bear: 0/8"); empty string means use the auto-format. Order: NPC/GO objectives first, then item objectives, both 1-indexed. Only non-empty slots included.

Not included — race, class, skill, time limit, and suggested-player count. The SMSG_QUEST_QUERY_RESPONSE packet doesn't ship those fields; the server enforces them and filters quests before broadcasting, so the client only ever sees quests it could accept and never receives the static restriction values. Verified empirically: a race-restricted starter (3120 Verdant Sigil — Night Elf Druid only), a dungeon quest (914), and a timed delivery (3364 Scalding Mornbrew — 5-min authored timer) all have those cache slots zero-filled in memory. Addons that need that data must source it from an external scraped database like pfQuest's.

Field reliability: everything currently returned is either confirmed-correct via the engine's own Script_GetQuestLog* accessors or empirically verified against in-game quest semantics. The poi field is the only remaining hypothesis — its offsets are confirmed but no test quest has yet exercised it.

XP rewards aren't exposed — there is no GetQuestLogRewardXP, and per emulator-decoded packet structure, the server doesn't include an XP field in SMSG_QUEST_QUERY_RESPONSE. XP is computed server-side at turn-in from level-scaling tables.

Example

local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()
    if event == "QUEST_DATA_LOAD_RESULT" and arg2 == 1 then
        local d = C_QuestLog.GetQuestDetails(arg1)
        if d then
            print(d.title, "level", d.level, d.questType or "(no tag)")
            for _, r in ipairs(d.requirements) do
                print("  -", r.kind, r.id, "x", r.count)
            end
        end
    end
end)
C_QuestLog.RequestLoadQuestByID(1467)

Sample output for quest 1467 ("Reagents for Reclaimers Inc."):

Reagents for Reclaimers Inc.  level 40  (no tag)
  - item 6253 x 1

with d.choiceItems = {{id=6793, count=1}, {id=6794, count=1}}, d.rewardMoney = 3500, d.rewardMoneyAtMaxLevel = 1920, etc.

C_QuestLog.GetNumQuestObjectives(questID)

The quest's objective count — exactly the length GetQuestDetails(questID).requirements would have — read straight from the quest cache with no Lua tables or strings materialized. Returns nil when the quest isn't cached (same contract as GetQuestDetails).

local n = C_QuestLog.GetNumQuestObjectives(1467)   -- 1

Use this instead of GetQuestDetails when only the count matters — e.g. bounds-checking authored objective indices across every step of a guide at load time. GetQuestDetails copies the quest's description / objectives / completion text into Lua strings and builds ~20 table fields per call; harmless one at a time, but a login-path loop over hundreds of cached quests can put real pressure on the fixed-size Lua memory pool. This accessor costs a few dozen byte reads regardless of cache temperature.

C_QuestLog.IsQuestDataCachedByID(questID)

Returns true if the quest's static data is currently in the client-side quest cache, false otherwise. Pure cache probe — no server query, no side effects. Pair with C_QuestLog.RequestLoadQuestByID when you need to ensure the data is loaded before checking.

Returns false (not error) for invalid input — non-number, non-positive, or non-existent questIDs all yield false, matching the C_Item.IsItemDataCachedByID shape.

if not C_QuestLog.IsQuestDataCachedByID(questID) then
    C_QuestLog.RequestLoadQuestByID(questID)
    -- listen for QUEST_DATA_LOAD_RESULT, or re-check on a timer
end

GetQuestLogLeaderBoardID(objectiveIndex [, questIndex])

Companion to the stock GetQuestLogLeaderBoard — returns (id, kind) for the same objective the existing call formats text for. id is always positive (the raw RequiredNPCOrGo field is signed, but the sign is folded into kind instead); kind is one of "monster", "object", or "item". Returns nothing for: out-of-range objectiveIndex, header rows, quests whose static data hasn't been cached yet, or event-text-only objectives that have no entry ID.

Arg shape matches the engine's actual Script_GetQuestLogLeaderBoard (its usage string says (index) but the function also accepts the 2-arg form): with questIndex provided, looks up the entry at that 1-based quest-log slot; without it, falls back to whichever quest is currently selected via SelectQuestLogEntry.

-- Selected quest path (pairs with GetQuestLogLeaderBoard's selected mode)
SelectQuestLogEntry(13)
local text, kind, done = GetQuestLogLeaderBoard(1)
local id              = GetQuestLogLeaderBoardID(1)
-- text = "Young Stranglethorn Tiger slain: 4/10"
-- kind = "monster"
-- id   = 2630   (the creature template / entry)

-- Explicit-quest path
local text, kind = GetQuestLogLeaderBoard(1, 13)
local id, k2     = GetQuestLogLeaderBoardID(1, 13)
-- kind == k2 always

The ID is the same key creaturecache.wdb / gameobjectcache.wdb / itemcache.wdb use, so cross-referencing against the existing item or creature caches is direct — no localized-text parsing required.

Why not just expose this on GetQuestLogLeaderBoard itself? Modifying the engine's existing return tuple would silently break addons that destructure positionally (local text, type, done = GetQuestLogLeaderBoard(...)). A separate function keeps the existing call wire-compatible.

Source: the cached quest record (returned by Quest::Cache::Peek) carries the SMSG_QUEST_QUERY_RESPONSE objective arrays inline at fixed offsets:

  • +0x149C — int32 RequiredNPCOrGo[4] (positive = creature entry, negative = gameobject entry)
  • +0x14AC — uint32 RequiredNPCOrGoCount[4]
  • +0x14BC — uint32 RequiredItem[4]
  • +0x14CC — uint32 RequiredItemCount[4]

Iteration mirrors the engine at 0x004E0110: walk the NPC/GO array first, skip zero slots, then the item array. 1-based objectiveIndex counts only non-empty slots.

GetQuestLogCompletionText([questLogIndex])

Returns the text that the quest log shows when a quest is ready to turn in. The text is the quest objectives. Tokens such as $N (player name) and $C (class) are replaced, as in the quest log.

  • questLogIndex is the 1-based index in the quest log. Without it, the function uses the selected quest.
  • The function returns nothing for a header row or an index that is out of range.
  • The function returns nothing until the quest data is in the cache. It does not request the data. Use C_QuestLog.RequestLoadQuestByID for that.
SelectQuestLogEntry(2)
print(GetQuestLogCompletionText())
-- "Kill 12 Kobold Laborers, then return to Marshal McBride at Northshire Abbey."

Completed quests

The functions in this group give the quests that the character has completed. They exist only on a Turtle WoW client, or on a client that is built on it. On other clients, they are not defined.

The server keeps the list of completed quests. The client gets a copy of the list at these times:

  • At each login, a short time after PLAYER_ENTERING_WORLD. Until then, the list is empty.
  • After each QueryQuestsCompleted call.
  • After each quest turn-in.
  • When an addon sends the .queststatuses chat command.

Each copy replaces the list, and then QUEST_QUERY_COMPLETE fires.

The list does not include repeatable quests. When you turn in a quest, the quest goes into the list before QUEST_TURNED_IN fires. A repeatable quest stays in the list until the next copy arrives.

The list stays after /reload. The server sends the copy as CHAT_MSG_ADDON messages with the prefix TWQUEST, so other addons also see these messages.

QueryQuestsCompleted()

Turtle WoW only (see Completed quests). Sends a request for the list of completed quests to the server, and returns at once. When the list arrives, QUEST_QUERY_COMPLETE fires.

Each call sends a request. If the server does not answer, the event does not fire. For example, the server ignores a request that it gets during the loading screen of a teleport.

local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_QUERY_COMPLETE")
f:SetScript("OnEvent", function()
    local completed = GetQuestsCompleted()
    -- completed[questID] is true for each completed quest
end)
QueryQuestsCompleted()

GetQuestsCompleted([questTable])

Turtle WoW only (see Completed quests). Returns a table in which the key of each completed quest is its quest ID and the value is true.

  • If you give questTable, the function adds the quests to that table and returns it.
  • If you do not give a table, the function returns a new table.

The function returns the list that the client has now. It does not send a request.

if GetQuestsCompleted()[4813] then
    -- The Fragments Within is complete
end

C_QuestLog.IsQuestFlaggedCompleted(questID)

Turtle WoW only (see Completed quests). Returns true if the quest is in the list of completed quests, and false if not. Until the list arrives after login, the function returns false for all quests.

C_QuestLog.GetAllCompletedQuestIDs()

Turtle WoW only (see Completed quests). Returns an array of the completed quest IDs, from the lowest ID to the highest.

A turn-in always prints "quest completed."

Every quest turn-in puts ERR_QUEST_COMPLETE_S ("Bashal'Aran completed.") in chat as a CHAT_MSG_SYSTEM line, with its sound. Usually the line comes before the experience and money lines, and QUEST_TURNED_IN follows it. If the client does not yet have the quest's data, the line comes when the data arrives: after the experience and money lines, and after QUEST_TURNED_IN. Do not depend on the order.

Vanilla prints the line only for a quest with no follow-up. When the quest's data names a next quest in its chain, the client leaves the line out, so a turn-in in the middle of a chain prints only "Experience gained" and "Received". Addons that follow turn-ins through the line, such as Zygor Guides Viewer 2.0 and its 3.3.5a port, missed every quest with a follow-up.

local pattern = "^" .. string.gsub(ERR_QUEST_COMPLETE_S, "%%s", "(.+)")
local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_SYSTEM")
f:SetScript("OnEvent", function()
    local _, _, title = string.find(arg1, pattern)
    if title then
        -- title was just turned in, chain quests included
    end
end)

Sound

Every sound the client plays comes from one table of sound entries. Each entry has an id — a SoundKitID — and one or more audio files. The same ids the modern client uses are the ids here, so 8959 is the raid warning in both.

To see the file paths, extract them with the ExportSoundFiles console command.

PlaySound(soundKitID) / PlaySound(soundName)

Plays a sound.

  • Give a number and it plays that SoundKitID. It returns willPlay, soundHandle.
  • Give a string and it plays the sound entry with that name, exactly as before. It returns nothing.
PlaySound(8959)                  -- true, 394043692
PlaySound("igMainMenuOpen")      -- plays, returns nothing

The id is the only thing added here. For a channel, or to be told when the sound ends, use C_Sound.PlaySound below.

C_Sound.PlaySound(soundKitID [, channel, forceNoDuplicates, runFinishCallback])

Plays a SoundKitID, and takes only a number. Returns willPlay, soundHandle.

local willPlay, handle = C_Sound.PlaySound(8959)

willPlay is false when the sound cannot start. That happens when the file is muted, when the id names no entry, or when a duplicate is refused. It is also false while the interface loads, and while you enter or leave the world. PlaySound with a sound name is silent at those times too.

An entry can hold several files. The client picks one of them, with the same weighting it uses for its own sounds.

Argument Meaning
channel The volume slider that applies: "SFX", "Music", "Ambience" or "Master". Upper and lower case do not matter. Leave it out, or give another value, for "SFX". "Master" uses only the master volume. "Ambience" uses the sound volume for a SoundKitID.
forceNoDuplicates Pass true to play nothing while the same file is already playing. An entry with several files can still play a second copy if the client picks a different file.
runFinishCallback Pass true to get the SOUNDKIT_FINISHED event when this sound ends.

C_Sound.PlaySoundWithOptions(params)

The same as C_Sound.PlaySound, with the arguments in a table. Returns success, soundHandle.

local ok, handle = C_Sound.PlaySoundWithOptions({
    soundKitID = 8959,
    volumeOverride = 0.4,
    runFinishCallback = true,
})
Field Meaning
soundKitID The sound to play.
volumeOverride Scales the sound's volume. 1 leaves it alone, 0 is silent. Leave it out to play at the sound's own volume.
runFinishCallback Pass true to get the SOUNDKIT_FINISHED event when this sound ends.
forceNoDuplicates Pass true to play nothing while the same file is already playing.
uiSoundSubType Accepted and ignored.
overridePriority Accepted and ignored.

C_Sound.GetSoundScaledVolume(soundHandle)

Returns the volume a sound is playing at — its own volume after any scaling. Returns nil once the sound has ended.

local _, handle = C_Sound.PlaySoundWithOptions({ soundKitID = 8959, volumeOverride = 0.4 })
C_Sound.GetSoundScaledVolume(handle)

C_Sound.IsPlaying(soundHandle)

Returns true while the sound for that handle is still playing.

local _, handle = C_Sound.PlaySound(8959)
C_Sound.IsPlaying(handle)   -- true, until the sound ends

A handle is only meaningful while its sound plays. After the sound ends the client can give the same handle value to a later sound, so a handle you have held for a while can report true for a different sound. Read it soon after you get it.

StopSound(soundHandle [, fadeoutTime])

Stops a sound that one of the play functions on this page started.

local _, handle = C_Sound.PlaySound(8959)
StopSound(handle)          -- stops at once
StopSound(handle, 1500)    -- fades out over 1.5 seconds

fadeoutTime is in milliseconds. A fade shorter than 100 ms stops the sound at once. StopSound does nothing when the sound has already ended. It also does nothing for a sound that the client started itself, such as music or ambience.

A stopped sound with runFinishCallback set sends SOUNDKIT_FINISHED when it ends.

PlaySoundFile(file [, channel])

Plays an audio file by its path. Returns willPlay, soundHandle.

local willPlay, handle = PlaySoundFile("Sound\\Interface\\RaidWarning.wav", "Master")

channel takes the same values as in C_Sound.PlaySound. For a file, "Ambience" uses the ambience volume.

The client limits how many files can play at the same time. When the limit is reached, PlaySoundFile returns false. A file holds its place until it ends. A file that fades out still holds its place. Give the handle to StopSound to stop the file before it ends.

C_Sound.PlayItemSound(soundType, item)

Plays the noise an item makes when you handle it, without doing the action.

C_Sound.PlayItemSound(Enum.ItemSoundType.Pickup, { bagID = 0, slotIndex = 1 })
C_Sound.PlayItemSound(Enum.ItemSoundType.Drop, { equipmentSlotIndex = 1 })

soundType is an Enum.ItemSoundType value:

Value Field
0 Pickup
1 Drop
2 Use
3 Close

item is an item location — { bagID = B, slotIndex = S } or { equipmentSlotIndex = N }. It also takes the other forms this documentation's item functions accept: an item GUID string, an itemID, an item link, or the name of an item you carry.

All four types are passed through to the client, which looks the sound up in its own item-sound data. With the data a stock client ships, only Pickup and Drop produce a sound: items are grouped into sound groups, and every group a real item belongs to fills in those two while leaving Use and Close empty. Those two are accepted and play nothing.

A server that fills in the missing entries gets them for free — nothing here limits the type, so Use and Close start playing as soon as the data defines them.

The sound comes from the item's display, so items that look alike sound alike. A sword sounds like metal and a robe sounds like cloth, whatever else differs between them.

Nothing plays, and nothing is raised, when the item cannot be found or its data has not arrived yet.

C_Sound.PlayVocalErrorSound(vocalErrorSoundID)

Plays your character's spoken complaint for an error — "my bags are full", "I have no ammo" — in the voice of their race and sex.

C_Sound.PlayVocalErrorSound(Enum.Vocalerrorsounds.Inventoryfull)
C_Sound.PlayVocalErrorSound(Enum.Vocalerrorsounds.Outofammo)

Enum.Vocalerrorsounds holds all 68 values, from Inventoryfull (0) to ExhaustedObsolete (67). Every one of them has recordings for the playable races.

Some combinations have no recording for one sex, and a few have none at all. Those play nothing, and raise nothing.

MuteSoundFile(file) / UnmuteSoundFile(file)

Stops a sound file from playing, and lets it play again.

MuteSoundFile("Sound\\Interface\\RaidWarning.wav")
PlaySound(8959)      -- false: silent
UnmuteSoundFile("Sound\\Interface\\RaidWarning.wav")
PlaySound(8959)      -- true: plays again

file is the path of the audio file, such as "Sound\\Creature\\Ragnaros\\RagnarosAggro01.wav". Upper and lower case do not matter, and you can use / in place of \\.

A mute applies to the file, so it covers every sound that uses it, whoever starts it. MuteSoundFile returns true when the path is now muted. UnmuteSoundFile returns true when the path had been muted.

Mutes last until you log out or reload. To keep them, save your list and apply it again on login.

C_Sound.GetRecentSoundFiles()

Returns the files that played most recently, newest first. Use it to find the path of a sound you just heard.

local recent = C_Sound.GetRecentSoundFiles()
-- recent[1] = { file = "Sound\\interface\\RaidWarning.wav",
--               time = 96036833, muted = false }

Each entry has three fields:

Field Meaning
file The path of the audio file. Pass it to MuteSoundFile.
time When it played, in milliseconds on GetTime()'s scale. GetTime() * 1000 - time is how long ago.
muted true when the file is muted.

The list holds the last 64 different files. A file that plays again moves to the front instead of taking a second place, so a repeating footstep cannot push the rest out.

Muted files stay in the list, marked muted = true, so you can still find one to un-mute.

The list covers every sound the client plays, not only the ones you start. Opening a bag or taking a step puts a file in it.

Spell

All C_Spell.* functions in this section accept any spell identifier, not just a numeric spellID. The first argument can be:

  • number — spellID, used directly.
  • |Hspell:N|h hyperlink — extracted by parsing for the embedded spell:N payload, so C_Spell.X(GetSpellLink(id)) round-trips cleanly.
  • spell name — case-sensitive lookup through the engine's name resolver (FUN_RESOLVE_SPELL_NAME_TO_BOOK_ID, the same chain CastSpellByName uses). Tolerates (Rank N) suffixes via the engine's parser. Bounded to spells in the player's known spellbook — not arbitrary Spell.dbc rows.

Unrecognized identifiers (garbage strings, nil, tables, etc.) return nil rather than raising a Lua usage error — the permissive C_Spell.* convention. Function entries below write (spellID) in the signature for brevity but the broader shape is accepted everywhere.

The legacy globals in this section (GetSpellInfo, IsPassiveSpell, IsHarmfulSpell, IsUsableSpell, etc.) keep their existing (slot, bookType) overload — only the C_Spell.* namespace is broadened.

C_Spell.DoesSpellExist(spellID)

Returns true if spellID resolves to a populated Spell.dbc record, false otherwise. Cheap pre-flight check for any of the spellID-keyed C_Spell.* calls — if this returns false, the rest of the family will return nil/empty.

if C_Spell.DoesSpellExist(133) then
    local info = C_Spell.GetSpellInfo(133)  -- safe
end

C_Spell.IsSpellDataCached(spellIdentifier)

Returns true if the data of the spell is loaded. spellIdentifier is a spell ID, a spell link, or the name of a spell in your spellbook. The data of every spell that exists is always loaded. Thus the result is true for each spell that exists, and false for all other values.

C_Spell.IsSpellDataCached(133)     -- true
C_Spell.IsSpellDataCached(999999)  -- false

C_Spell.RequestLoadSpellData(spellIdentifier)

Asks for the data of a spell. spellIdentifier is a spell ID, a spell link, or the name of a spell in your spellbook. The function returns no values.

The function sends SPELL_DATA_LOAD_RESULT(spellID, success) before it returns. If the spell exists, success is 1. If no spell has this ID, success is nil. If spellIdentifier does not give a spell ID, the event does not occur.

local f = CreateFrame("Frame")
f:RegisterEvent("SPELL_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()
    if arg2 then
        print(C_Spell.GetSpellName(arg1))
    end
end)
C_Spell.RequestLoadSpellData(133)  -- prints "Fireball"

C_Spell.GetSchoolString(schoolMask)

Returns the localized name for a damage-school bitmask. Resolves single-bit masks via the engine's SPELL_SCHOOL<n>_CAP global strings (SPELL_SCHOOL0_CAP = "Physical", SPELL_SCHOOL2_CAP = "Fire", etc.).

C_Spell.GetSchoolString(4)    -- "Fire"
C_Spell.GetSchoolString(32)   -- "Shadow"

Spells here are single-school — there are no multi-school combos ("Frostfire", "Shadowflame", "Spellstorm", etc.). Any multi-bit mask returns "Unknown", the documented fallback for unnamed combinations.

Lua 5.0 doesn't support hex literals (0x04 is a syntax error); pass mask values as decimals, or via tonumber("0x04", 16) if you want to keep hex notation in source.

Decimal Hex School
1 0x01 Physical
2 0x02 Holy
4 0x04 Fire
8 0x08 Nature
16 0x10 Frost
32 0x20 Shadow
64 0x40 Arcane

GetSpellInfo(spellID) / GetSpellInfo(slot, bookType)

Returns nine values, plus a 10th value: spellID, for any spell ID — including spells the player has not learned. The stock client has no GetSpellInfo Lua function at all (only GetSpellName/GetSpellTexture, both of which take a spellbook slot rather than an ID), so addons that need spell metadata for arbitrary IDs (raid frames, debuff trackers, aura libraries) currently can't get it.

Returns name, rank, icon, cost, isFunnel, powerType, castTime, minRange, maxRange, spellID. Returns nil if the spell ID is out of range.

  • cost is the cost you pay, with your talents and gear. Rage is in whole points (Heroic Strike reports 15).
  • isFunnel is true when the spell costs power every second, such as Health Funnel or Drain Soul. Then cost is the cost per second.
  • castTime is your cast time in milliseconds, with your talents and cast speed. It is 0 for an instant or passive spell.
  • minRange and maxRange are your range in yards, with your talents. Both are 0 for a melee spell.

For a "pet" book slot, cost, castTime and the ranges are your pet's values.

Four input forms are accepted:

  • GetSpellInfo(spellID) — direct DBC lookup by ID.
  • GetSpellInfo(slot, bookType) — same shape as the stock GetSpellName(slot, bookType). slot is 1-based, bookType is "spell" (player) or "pet". The slot is resolved to a spellID via the engine's spellbook array, then the same DBC reads run. Returns nil for empty / out-of-range slots.
  • GetSpellInfo("name") — looks the name up in the player's, then the pet's, spellbook and returns the highest rank you know. The match is exact and case-sensitive. The name must be a spell you have. A name you do not know returns nil (it does not raise an error). It is not a database-wide search — a name shared by many ranks or by NPC spells has no single answer, so only your own spellbook is used.
  • GetSpellInfo("name(Rank N)") — a rank in parentheses pins that exact rank instead of the highest, the same SpellName(Rank N) form CastSpellByName accepts. A space before the parenthesis is allowed ("Mind Blast (Rank 8)"). A rank you do not know returns nil.
  • GetSpellInfo("|Hspell:ID|h[Name]|h") — a spell hyperlink. The spellID inside the link is used directly, so this works for any spell, learned or not.
local name, rank, icon, _, _, _, _, _, _, spellID = GetSpellInfo(133)
-- name="Fireball", rank="Rank 1", icon="Spell\\Fire\\...", spellID=133

-- spellbook overload
local _, _, _, _, _, _, _, _, _, id = GetSpellInfo(1, "spell")
-- id is the spellID at player spellbook slot 1

-- by name (highest rank you know), a specific rank, and a link
local name = GetSpellInfo("Fireball")
local _, rank = GetSpellInfo("Mind Blast (Rank 8)")  -- rank="Rank 8"
local _, _, _, _, _, _, _, _, _, id = GetSpellInfo(GetSpellLink(133))

The same name and link forms work for GetSpellLink, IsPassiveSpell, IsHarmfulSpell, and IsHelpfulSpell, which share this resolver.

Note on the 10th return. We kept the existing 9 returns (so addons that worked against the previous signature still work) and just appended spellID at position 10.

C_Spell.GetSpellInfo(spellID)

Table-style accessor for the same data. Returns a Lua table of the spell's metadata, or nil if the spell ID is out of range.

Table fields:

Field Type Notes
name string Localized name
iconID string Icon path (e.g. "Interface\\Icons\\Spell_Fire_FlameBolt"). See note below.
castTime number Your cast time in milliseconds, with talents and cast speed; 0 for an instant or passive spell
minRange number Your minimum range in yards, with talents; 0 for a melee spell
maxRange number Your maximum range in yards, with talents; 0 for a melee spell
spellID number Echo of the input
rank string Localized rank (e.g. "Rank 1") — an extra field
cost number The cost you pay, with talents and gear; rage in whole points; per second when isFunnel — an extra field
isFunnel boolean True when the spell costs power every second — an extra field
powerType number 0=mana, 1=rage, 2=focus, 3=energy, 4=happiness — an extra field
local info = C_Spell.GetSpellInfo(133)
-- info.name = "Fireball"
-- info.iconID = "Interface\\Icons\\Spell_Fire_FlameBolt"
-- info.castTime = 1500
-- info.spellID = 133
-- info.rank = "Rank 1"
-- ... etc.

iconID is a path string. This value can be a fileID:number elsewhere. There is no fileID system here — assets are referenced by path strings. We surface the icon path so it's directly usable with texture:SetTexture(info.iconID). If you're backporting code that expects a number, this is the field to adjust.

Extra fields. The four fields beyond the base spec (rank/cost/isFunnel/powerType) are present because they're in Spell.dbc. Including them costs nothing.

C_Spell.GetSpellName(spellID)

Returns the localized name of spellID, or nil if the spell ID is out of range or has no name in the current locale. Convenience accessor for the name field of C_Spell.GetSpellInfo when that's all you need — single field read, no DBC indirection beyond the locale lookup.

local name = C_Spell.GetSpellName(133)  -- "Fireball"

C_Spell.GetSpellTexture(spellID)

Returns the icon path string for spellID (read from SpellIcon.dbc via the spell's SpellIconID field), or nil if the spell ID is out of range or the icon record is empty.

Path string, not fileID. This value can be a fileID:number elsewhere. There is no fileID system here — see the same note on C_Spell.GetSpellInfo's iconID field. Pass directly to texture:SetTexture(...).

local path = C_Spell.GetSpellTexture(133)
-- path = "Interface\\Icons\\Spell_Fire_FlameBolt"

GetSpellLink(spellID) / GetSpellLink(slot, bookType)

Returns the chat-style spell hyperlink and the spellID:

link, spellID = GetSpellLink(spellID)
              = GetSpellLink(slot, bookType)

Format is |cff71d5ff|Hspell:ID:0|h[Name]|h|r — the standard 1.12 spell-link wrapper. The trailing :0 after the spellID matches the extended hyperlink shape (where the field is a sub-data slot for pet-spellbook flags etc.); the engine ignores it during link parsing, but addons grepping with |Hspell:(%d+): patterns will pick it up correctly.

Two input forms, mirroring GetSpellInfo:

  • GetSpellLink(spellID) — direct DBC lookup.
  • GetSpellLink(slot, bookType) — resolves the spellbook slot to a spellID first. Useful when iterating the player's known spells: caller gets back both the link AND the underlying ID without a separate lookup.

Returns nil if the spellID/slot doesn't resolve to a real spell.

local link = GetSpellLink(133)
DEFAULT_CHAT_FRAME:AddMessage("Cast " .. link .. "!")

-- Walking the spellbook to print every learned spell:
for slot = 1, 1000 do
    local link, id = GetSpellLink(slot, "spell")
    if not link then break end
    -- ...
end

C_Spell.GetSpellLink(spellID)

Table-namespace variant. Same link string as GetSpellLink(spellID), but returns only the link — no spellID echo since the caller already had it on hand to make the call.

local link = C_Spell.GetSpellLink(133)  -- "|cff71d5ff|Hspell:133:0|h[Fireball]|h|r"

C_Spell.GetSpellDescription(spellID)

Returns the formatted spell description for any spellID — including spells the player has not learned. $s1/$s2/$o1/$d-style placeholders are resolved to base-rank values; ranges and durations appear as actual numbers (e.g. "14 to 22 Fire damage", not "$s1 to $s2 Fire damage"). Returns nil if the spell ID is out of range or has no description in the current locale.

The 1.12 client doesn't expose this from Lua at all — the only existing path is the scan-tooltip hack (set a hidden tooltip via SetHyperlink("spell:"..ID) and read each TextLeftN:GetText() line). That's slow, GC-heavy, and shares the global scan tooltip with every other addon. This function calls the engine's own description formatter directly — same code path the in-game tooltip uses, no UI side effects.

local desc = C_Spell.GetSpellDescription(133)  -- Fireball Rank 1
-- "Hurls a fiery ball that causes 14 to 22 Fire damage and an additional
--  2 Fire damage over 4 sec."

No caster scaling. Values reflect the spell's base rank — caster level / spell power / talents are not applied, the same as calling it outside a unit context. If you need the "currently displayed" tooltip text with caster scaling, use GameTooltip:SetSpellByID and read line strings from there.

C_Spell.GetSpellDescriptionForItemLocation(spellIdentifier, itemLocation)

Returns the description of a spell as the tooltip of an item shows it. itemLocation is an ItemLocation object or table. An item tooltip shows the same spell text as C_Spell.GetSpellDescription. Thus the result is equal to C_Spell.GetSpellDescription(spellIdentifier). If itemLocation is not a table or a string, the function raises an error.

local loc = ItemLocation:CreateFromEquipmentSlot(13)
local desc = C_Spell.GetSpellDescriptionForItemLocation(spellID, loc)

C_Spell.GetSpellMechanicByID(spellID)

Returns the spell's mechanic as (mechanicID, name) — the standard WoW SpellMechanic ID and its English name. Works for any spellID, not just spells the player has learned.

Returns:

  • nothing (nil) if the spell ID is invalid / out of range
  • (0, nil) for a known spell that carries no mechanic
  • (mechanicID, name) otherwise

The name is always the enUS string (locale-independent), because the mechanic name is a stable token consumers string-match against ("rooted", "stunned", "polymorphed", …) — returning a localized value would break that matching across clients. It is nil only when the mechanic has no SpellMechanic.dbc record or no English name; the numeric ID is still returned in that case.

C_Spell.GetSpellMechanicByID(118)  -- Polymorph        → 17, "polymorphed"
C_Spell.GetSpellMechanicByID(339)  -- Entangling Roots  → 7,  "rooted"
C_Spell.GetSpellMechanicByID(133)  -- Fireball          → 0,  nil

This reads Spell.dbc's Mechanic field and resolves the name from SpellMechanic.dbc directly — both DBCs are resident from boot, so the call is synchronous with no caching or network round-trip (unlike item data). It replaces hand-maintained spellID → mechanic lookup tables that addons otherwise keep because there is no Lua reader for the field — e.g. crowd-control macro conditionals ([cc:stun], [cc:fear]) that need to know which mechanic a debuff applies.

The complete SpellMechanic.dbc set on 1.12 — all 27 rows (the name column is exactly what this function returns):

ID name ID name ID name
1 charmed 10 asleep 19 shielded
2 disoriented 11 ensnared 20 shackled
3 disarmed 12 stunned 21 mounted
4 distracted 13 frozen 22 seduced
5 fleeing 14 incapacitated 23 turned
6 clumsy 15 bleeding 24 horrified
7 rooted 16 healing 25 invulnerable
8 pacified 17 polymorphed 26 interrupted
9 silenced 18 banished 27 dazed

No mechanic 30. The table tops out at 27. Sap and Gouge report 14 (incapacitated) — the 30 ("sapped") value used by some addon tables has no row here.

C_Spell.GetSpellEffectInfo(spellID)

Returns one table per spell effect, holding that effect's raw Spell.dbc fields, as a 1-based array of three. nil for an invalid or out-of-range spell ID.

effects = C_Spell.GetSpellEffectInfo(spellID)

Each entry holds:

Field Meaning
effect The SPELL_EFFECT_* code. 0 when the slot is unused.
auraName Which aura the effect applies. 0 when it applies none.
basePoints The effect's stored magnitude.
baseDice Added to basePoints for the fixed magnitude.
dieSides Size of the magnitude's random range.
pointsPerLevel Magnitude gained per level. Fractional.
dicePerLevel Random range gained per level. Fractional.
miscValue The effect's parameter, such as which stat a stat-modifying aura applies to.

All three entries are always present. An unused effect slot has every field 0.

local fx = C_Spell.GetSpellEffectInfo(1243)  -- Power Word: Fortitude
-- fx[1].auraName == 29, fx[1].basePoints == 2, fx[1].baseDice == 1
-- so the effect grants 3 -- the +3 Stamina the tooltip shows

Working out an effect's magnitude. For most auras dieSides is 1 or 0 and pointsPerLevel is 0, and then the magnitude is simply basePoints + baseDice. The general form also scales with the caster's level, which needs the spell's level fields from GetSpellLevelInfo:

local spellLevel, _, maxLevel = C_Spell.GetSpellLevelInfo(spellID)
local cap = maxLevel > 0 and maxLevel or UnitLevel("player")
local level = math.max(spellLevel, math.min(UnitLevel("player"), cap)) - spellLevel

local points = fx.basePoints + level * fx.pointsPerLevel
local random = fx.dieSides + level * fx.dicePerLevel
local value = points + fx.baseDice   -- only valid while random <= 1

A dieSides above 1 means the magnitude is rolled. The value is picked when the spell is cast and is not available here, so treat such an effect as having no single magnitude rather than reporting the low end of its range.

The fields are the stored ones, so a magnitude worked out from them also leaves out what the caster brings to a cast: combo points, talent and gear modifiers, and the extra level scaling some spells are flagged for. Expect it to match a tooltip for a plain aura and to fall short for a damage or healing figure.

Reads Spell.dbc directly, so it covers every spell the client knows, not just the player's spellbook, with no caching or waiting on the server.

C_Spell.GetSpellEffectMechanics(spellID)

Returns the spell's three per-effect SpellMechanic ids (Spell.dbc EffectMechanic[3]) as a 1-based array table, or nil for an invalid / out-of-range spell ID. Each entry uses the same numbering as GetSpellMechanicByID (0 = that effect carries no mechanic).

C_Spell.GetSpellEffectMechanics(1822)  -- Rake  → { 0, 15, 0 }
C_Spell.GetSpellEffectMechanics(703)   -- Garrote → { 0, 0, 0 }
C_Spell.GetSpellEffectMechanics(133)   -- Fireball → { 0, 0, 0 }

Complements GetSpellMechanicByID, which reads only the spell-level Mechanic field (+0x14). A spell's mechanic is frequently stored on an effect instead — periodic damage such as bleeds is the common case. Garrote/Rupture/Rend/Rip tag bleeding (15) at the spell level, but Rake has spell-level 0 and EffectMechanic[2] = 15, so effect-mechanic-aware callers (e.g. bleed classification for immunity tracking) need this array:

local function IsBleed(spellID)
    if C_Spell.GetSpellMechanicByID(spellID) == 15 then return true end
    local em = C_Spell.GetSpellEffectMechanics(spellID)
    if em then for i = 1, 3 do if em[i] == 15 then return true end end end
    return false
end

Reads Spell.dbc directly (EffectMechanic[3] at +0x13C), so it covers every spell the client knows — not just the player's spellbook — with no caching or network round-trip.

C_Spell.GetSpellDispelType(spellID)

Returns the spell's dispel type from Spell.dbc: 1 = Magic, 2 = Curse, 3 = Disease, 4 = Poison. Returns 0 for a spell that nothing can dispel, and for an unknown spell.

C_Spell.GetSpellDispelType(118)   -- Polymorph → 1 (Magic)
C_Spell.GetSpellDispelType(980)   -- Curse of Agony → 2 (Curse)
C_Spell.GetSpellDispelType(133)   -- Fireball → 0 (none)

Use the number to show what removes an effect, or to filter auras by dispel type. The values match the DISPEL_* set the server uses.

C_Spell.GetSpellRadius(spellID) / GetSpellRadius(slot, bookType)

Returns (baseRadius, modifiedRadius) — the spell's AOE radius in yards, or nil for a non-AOE spell (no effect carries a radius). Works for any spell, not just ones the player has learned.

  • baseRadius — the raw SpellRadius.dbc value; caster-independent, what an AOE tracker watching other units wants.
  • modifiedRadius — baseRadius with the local player's talent / item modifiers applied (e.g. a Mage's Arctic Reach). Equal to baseRadius when the player has no matching modifier.
C_Spell.GetSpellRadius(122)    -- Frost Nova → 10, 10  (no Arctic Reach)
C_Spell.GetSpellRadius(122)    -- Frost Nova → 10, 12  (with Arctic Reach r2)
C_Spell.GetSpellRadius(133)    -- Fireball   → nil      (single-target)
GetSpellRadius(10, "spell")    -- 10th player-book slot, by spellbook position

Two signatures matching the dual-signature shape used elsewhere in this backport (GetSpellInfo, SpellHasRange, …): the namespaced form takes a spell identifier (numeric ID or name); the bare global takes the (slot, bookType) spellbook position, where bookType follows the GetSpellName convention — "pet" → pet book, "spell" (or any non-pet / omitted value) → player book.

baseRadius reads Spell.dbc's per-effect EffectRadiusIndex[3] (+0x160) and resolves each into SpellRadius.dbc; since a spell's effects can each carry their own radius, the largest base radius across the three effects is returned.

No caster-level scaling. The engine's internal radius helper also adds radiusPerLevel * casterLevel, but that needs a unit context; this reports the base value. Talent/item modifiers (the modifiedRadius return) are applied, via the engine's SpellMod system — but only the local player's, since the client tracks spell mods for the player alone (there's no way to know another caster's talents). All DBCs / mod tables are resident from boot, so the call is synchronous with no caching.

C_Spell.GetSpellPowerCost(spellIdentifier)

Returns an array of SpellPowerCostInfo tables, or nil if the spell isn't found or has no resource cost. Spells have exactly one power cost, so the array holds at most one entry.

C_Spell.GetSpellPowerCost(10187)   -- Blizzard, Mage with 3/3 Frost Channeling
-- { [1] = { type = 0, name = "MANA", cost = 1190, minCost = 1190,
--           costPercent = 0, costPerSec = 0, requiredAuraID = 0,
--           hasRequiredAura = false } }
-- base cost 1400, 15% reduction applied -> 1190
Field Meaning
type Enum.PowerType — 0 mana, 1 rage, 2 focus, 3 energy, 4 happiness (the spell's PowerType)
name power token ("MANA", "RAGE", …)
cost effective cost for the local player
minCost == cost (no optional-cost component)
costPercent the spell's %-of-base-resource cost, or 0 for a flat cost
costPerSec 0 (no per-second channel cost in these terms)
requiredAuraID 0 (no form/aura-conditional costs)
hasRequiredAura false

cost is the value the engine actually charges — base + level scaling + ManaCostPercent-of-resource + descriptor power-cost mods + the cost SpellMod (talents like Frost Channeling). It comes from the engine's own cost helper (FUN_006e31b0), so it stays in lockstep with what casting the spell deducts.

Effective vs. base. This returns the player-modified cost. C_Spell.GetSpellInfo's cost field is the base ManaCost (intrinsic spell data, caster-independent) — same base/modified split as GetSpellRadius.

C_Spell.GetSpellReagents(spellID)

Returns the spell's reagent list as an array of { itemID = N, count = M } tables, one entry per non-empty reagent slot. Returns an empty array {} for spells that consume no reagents, or nil for invalid spell IDs.

local list = C_Spell.GetSpellReagents(23028)  -- Arcane Brilliance
-- list = { { itemID = 17020, count = 1 } }   -- 1× Arcane Powder

-- Crafting recipe:
local list = C_Spell.GetSpellReagents(2538)
-- Cooking-style: { { itemID = ..., count = ... }, ... }

Reads directly from Spell.dbc at the documented reagent offsets (+0xA8 for itemIDs, +0xC8 for counts — verified empirically from the engine's tooltip-builder; the CMaNGOS-style schema documented in some emulator sources places these at +0x110 / +0x130, which is wrong for 1.12.1). Iteration stops at the first empty slot, matching how the engine walks its own reagent loop.

C_Spell.GetSpellCastCount(spellIdentifier)

Returns how many times the player can cast the spell with the reagents they carry. Returns 0 for a spell without reagents, and for an unknown spell.

C_Spell.GetSpellCastCount(23028)  -- Arcane Brilliance: your Arcane Powder count
C_Spell.GetSpellCastCount(133)    -- Fireball: 0 (no reagents)

The count is the smallest floor(carried / required) over the spell's reagents. "Carried" means equipped items and bags 0 to 4. Reagents in the bank do not count, because a cast cannot use them.

spellIdentifier accepts a spellID, a spell name, or a spell link, like C_Spell.GetSpellCooldown.

C_Spell.GetSpellSubtext(spellIdentifier)

Returns the localized "Rank N" / "Passive" / "Racial Passive" / similar string that appears below the spell's name in the spellbook. Read directly from Spell.dbc.Rank[9] at record +0x204 (same locale array shape as Name[9] two fields prior).

Accepts the full SpellIdentifier shape — spellID, name, name with rank ("Fireball(Rank 3)"), or |Hspell:N|h hyperlink. Returns nil for unrecognized input or spells with no rank text.

C_Spell.GetSpellSubtext(133)        -- "Rank 1"   (lowest-rank Fireball)
C_Spell.GetSpellSubtext(25306)      -- "Rank 12"  (max-rank Fireball)
C_Spell.GetSpellSubtext("Frost Armor")  -- "Rank 1"

IsPassiveSpell(spellID) / IsPassiveSpell(slot, bookType)

Returns true if the spell is passive (no cast bar — applies its effect as soon as it's learned/equipped), false otherwise. Returns nil for invalid spell IDs / out-of-range slots.

Reads Attributes bit 6 (SPELL_ATTR_PASSIVE = 0x40) directly off the Spell.dbc record. Used by aura libraries and talent code to decide whether a spell needs cast-bar tracking.

IsPassiveSpell(6603)   -- true  (Auto Attack)
IsPassiveSpell(133)    -- false (Fireball)

-- spellbook overload mirrors GetSpellInfo's
IsPassiveSpell(1, "spell")   -- true/false depending on slot 1

C_Spell.IsSpellPassive(spellID)

Table-namespace form of IsPassiveSpell. Same return semantics; doesn't accept the legacy (slot, bookType) shape.

C_Spell.IsSpellPassive(6603)   -- true (Auto Attack)

IsPlayerSpell(spellID)

Returns true if the player currently knows the given spellID, false otherwise. Covers everything granted by SMSG_LEARNED_SPELL:

  • Trained class abilities (the obvious case)
  • Racial abilities
  • Talent passives the player has invested in
  • Profession recipes — including ones from vendors / discovered via tradeskill — without needing to have the trade skill window open
  • Anything else the engine considers "known"
IsPlayerSpell(133)     -- true if you have Fireball
IsPlayerSpell(2963)    -- true for a tailor who knows Bolt of Linen
                       --   (works without opening the Tailoring window)

Only the current rank counts. For ranked spells (passives, trained ranks), only the spellID of the player's current rank returns true — lower-rank IDs return false even though the player conceptually "has" them: a player with 5/5 Unbreakable Will sees IsPlayerSpell(14791) (rank 5) as true but rank 4 / rank 3 / etc. as false.

This is by design — the engine's spell-knowledge bitmap stores one bit per spellID, and the highest-rank spellID is the one set when you train up. To check "do I have at least rank N", use the rank-N spellID specifically.

Reads a single bit from the engine's spell-knowledge bitmap at [0x00B710FC] — (bitmap[spellID >> 5] & (1 << (spellID & 31))) != 0. The same lookup the engine itself does internally. No spellbook walk, no talent walk, no profession-window dependency.

CanDualWield()

Returns true if the player can equip a weapon in the off hand.

if CanDualWield() then ... end

The value is true after the player learns Dual Wield. It is false again if the player unlearns it.

IsSpellKnown(spellID, [isPet])

Returns true if the given spellID is currently in the player's (or pet's) spellbook arrays — the same source the in-game spellbook UI displays. Strict semantics: only counts spells that have a spellbook button. isPet defaults to false.

IsSpellKnown(2050)         -- true if Lesser Heal is trained
IsSpellKnown(2649, true)   -- true if your hunter pet has Growl
IsSpellKnown(133)          -- false on a Priest (Fireball is a Mage spell)

Not the same as IsPlayerSpell. These two are deliberately split: IsSpellKnown is the strict "in the spellbook UI" check, IsPlayerSpell is the broad "any kind of known" query. The split matters because:

Spell type IsPlayerSpell IsSpellKnown
Trained class ability (Fireball) true true
Active talent grant (Mortal Strike) true true
Passive talent (Wand Spec, Imp Fireball) true false
Profession recipe (Bolt of Linen) true false

Use IsPlayerSpell for "do I have access to this effect at all"; use IsSpellKnown for "is this in my spellbook so I can put it on an action bar".

Implementation walks VAR_PLAYER_SPELLBOOK (0x00B700F0) when isPet=false or VAR_PET_SPELLBOOK (0x00B6F098) when isPet=true — a direct spellbook walk.

GetSpellBonusDamage(school)

Returns the local player's flat spell-damage bonus (spell power) for a given magic school, as a number. school is 1-based: 1 Physical, 2 Holy, 3 Fire, 4 Nature, 5 Frost, 6 Shadow, 7 Arcane. Raises a usage error for a missing / out-of-range school; returns 0 before you're in the world.

GetSpellBonusDamage(3)   -- your +Fire spell damage
GetSpellBonusDamage(6)   -- your +Shadow spell damage

GetSpellBonusHealing()

Returns the local player's flat healing bonus, as a number.

GetSpellBonusHealing()   -- your +Healing

Damage is an exact field read; healing is derived.

GetSpellBonusDamage reads the client's fully-computed value directly from the CGPlayer sub-struct (PLAYER_FIELD_MOD_DAMAGE_DONE_POS − _NEG per school). That field isn't in the broadcast descriptor — the server never sends it there, which is why it looks absent — but the client keeps a computed copy for its own use, with gear, enchants, buffs, talents, and set bonuses all baked in. So the value is exact and complete. (Same source nampower's GetSpellPower reads.)

GetSpellBonusHealing has no such field — there is no healing-done field at all (confirmed in-game: toggling a pure +healing item moved no field in the player struct) — so it's derived in two parts, both from Spell.dbc: (1) flat healing = the sum of every SPELL_AURA_MOD_HEALING_DONE off gear equip-effects, item enchants (permanent + weapon oils), random suffixes, and active buffs; plus (2) talent/buff stat-conversions, mirroring the server's own SpellBaseHealingBonusDone — the sum of SPELL_AURA_MOD_SPELL_HEALING_OF _STAT_PERCENT (aura 175) × total Spirit / 100 over your passive talents (Priest Spiritual Guidance) and active buffs. Part 2 reads the talent percent directly, so it's generic (any aura-175 talent/buff, including Turtle customs) and exact — e.g. Spiritual Guidance 5/5 with 182 Spirit adds int(182 × 25 / 100) = 45.

Residual: assumes talent ranks supersede (only the learned rank is known) and misses set-bonus healing granted via a set-completion spell.

GetCritChance() / GetRangedCritChance()

GetCritChance returns your chance to get a critical strike with melee attacks. GetRangedCritChance returns the same chance for ranged attacks. Each value is a percent, for example 21.2. Each function returns 0 before you are in the world.

The value includes gear, buffs, talents and forms. It changes when you change form, for example when a druid goes into Cat Form.

GetCritChance()         -- melee crit, for example 21.2
GetRangedCritChance()   -- ranged crit, for example 9.2

IsUsableSpell(spell) / IsUsableSpell(slot, bookType)

Returns (usable, noMana) for a spell. The result is the same verdict that IsUsableAction gives for an action slot that holds this spell, read live. Returns (1, nil) when you can cast the spell now, (nil, 1) when only power stops you, and (nil, nil) for any other block. The 1/nil pairs match IsUsableAction.

Two argument shapes:

  • IsUsableSpell(spellID) — a spellID. A spell that you do not know returns (nil, nil). A pet spell given by ID (Growl, Cower) is tested for the pet.
  • IsUsableSpell(slot, bookType) — a spellbook slot. bookType is "spell" for the player book or "pet" for the pet book.
IsUsableSpell(133)           -- Fireball: 1, nil with mana; nil, 1 without
IsUsableSpell(1, "spell")    -- player spellbook slot 1
IsUsableSpell(1, "pet")      -- pet spellbook slot 1

What the result includes. Every condition that greys an action button:

  • The player is alive, unless the spell is castable while dead.
  • The player has control. When control is lost (fear, charm), only spells that work in that state pass.
  • The required stance or form. Whirlwind in Battle Stance is not usable.
  • Reagents and totems in the bags.
  • An equipped weapon of the required type, and ammo for ranged spells.
  • Combo points for finishers.
  • Stealth-only and out-of-combat-only spells.
  • Aura states on the player and on the current target.
  • A toggle spell that is already active (Stealth while stealthed) is not usable.
  • Power. The cost of the spell, with your talents applied, against your current power. This is the only test that sets noMana.

What the result does not include. Cooldown, silence, and school lockouts. An action button greys for usability and swipes for cooldown as two separate states. This function keeps that split. Read cooldown with C_Spell.GetSpellCooldown and lockouts with C_LossOfControl.GetActiveLossOfControlData.

For a pet spell, the test is the current power of the pet against the spell cost, after the pet can act (not stunned, feared, or confused).

C_Spell.IsSpellUsable(spellID)

Table-namespace form. Same result as IsUsableSpell(spellID), but returns booleans (isUsable, insufficientPower) per the C_Spell.* convention. Accepts a spellID, a spell link, or a spell name.

local usable, noMana = C_Spell.IsSpellUsable(133)
-- usable=true, noMana=false  → cast it
-- usable=false, noMana=true  → drink up
-- usable=false, noMana=false → unknown spell, wrong stance, dead, or another block

C_Spell.GetSpellCooldown(spellIdentifier)

Returns a SpellCooldownInfo table for the given spell, or nil if the identifier doesn't resolve to a Spell.dbc row. Table-shape variant of the stock GetSpellCooldown(slot, bookType) — accepts a spellID directly without forcing the caller to resolve a spellbook slot first.

local info = C_Spell.GetSpellCooldown(1953)   -- Blink
-- on cooldown:   { startTime=728565.79, duration=15,  isEnabled=true, modRate=1, isOnGCD=false }
-- on the GCD:    { startTime=728580.12, duration=1.5, isEnabled=true, modRate=1, isOnGCD=true }
-- off cooldown:  { startTime=0,         duration=0,   isEnabled=true, modRate=1, isOnGCD=false }
Field Type Notes
startTime number Engine tick count (seconds) when the cooldown began. Same epoch as GetTime(), so info.startTime + info.duration is the absolute time the cooldown ends. 0 when no cooldown active.
duration number Cooldown length in seconds; 0 when no cooldown active.
isEnabled boolean false when the cooldown is "on hold" (e.g. some channeled abilities). true for normal cooldowns.
modRate number Always 1.0 — there is no haste-on-cooldown mechanic.
isOnGCD boolean true when startTime and duration are the global cooldown. false when they are the spell's own cooldown, or when no cooldown is active.

The activeCategory and timeUntilEndOfStartRecovery fields read as nil since they have no source here.

A spell in your pet's spellbook, and not in yours, reports your pet's cooldown.

Returns nil if the resolved spellID is 0 or doesn't have a Spell.dbc row.

GetSpellBaseCooldown(spellID)

Returns the base cooldown of a spell in milliseconds. Talents and items do not change this value. The value is the larger of the spell's own cooldown and its category cooldown.

/dump GetSpellBaseCooldown(12051)   -- Evocation: 480000
/dump GetSpellBaseCooldown(133)     -- Fireball: 0
/dump GetSpellBaseCooldown(6603)    -- Attack: nil

Returns 0 for a spell that has only the global cooldown. Returns nil if the spell does not exist, or if the spell has no cooldown and no global cooldown.

IsAttackSpell(slot, bookType)

Returns true if the spellbook slot holds the melee Attack spell. bookType is "spell" or "pet".

/dump IsAttackSpell(1, "spell")
/dump IsAttackSpell("Attack")

IsAutoRepeatSpell(slot, bookType)

Returns true if the spellbook slot holds the spell that repeats now, such as Auto Shot or Shoot. Pet spells always return false.

/dump IsAutoRepeatSpell("Auto Shot")

IsConsumableSpell(spellID) / IsConsumableSpell(slot, bookType)

Returns true if the spell uses up reagents. A single number is a spell ID. A number and a bookType is a spellbook slot.

/dump IsConsumableSpell(20484)   -- Rebirth: true
/dump IsConsumableSpell(133)     -- Fireball: false

These three functions accept a spell name in place of the slot. The name can end in a rank, such as "Fireball(Rank 1)". You can also give the rank as the second argument: IsAttackSpell("Fireball", "Rank 1"). If the name is not in your spellbook, the function returns nothing. A slot outside 1 to 1024 also returns nothing. A first argument that is not a number or a string raises an error.

C_Spell.GetSpellLossOfControlCooldown(spellIdentifier)

Returns startTime, duration for the loss-of-control effect that stops the player from casting this spell right now. Both values are seconds on the GetTime() clock, like C_Spell.GetSpellCooldown. Both are 0 when nothing blocks the spell. Returns nil for an unknown spell.

local start, duration = C_Spell.GetSpellLossOfControlCooldown(133)  -- Fireball
if duration > 0 then
    cooldownFrame:SetCooldown(start, duration)  -- the loss-of-control swipe
end

Which effects block which spells:

Effect Blocks
School lockout (Counterspell, Kick, ...) Spells of the locked school.
Stun, fear, confuse, charm, possess Every spell.
Silence Spells that a silence prevents (most casts).
Pacify Abilities that a pacify prevents (most melee and ranged abilities).
Root, disarm Nothing.

An effect does not block a spell that makes you immune to that effect. For example, Polymorph does not block a druid's shapeshift forms.

The function reports only effects with a known end time. See the timing note in LossOfControl: a school lockout always has one, and a control effect has one when ClassicAPI saw the cast that applied it. When only the end time is known, startTime is the current time and duration is the remaining time. When several effects block the spell, the function reports the one that ends last.

C_Spell.IsCurrentSpell(spellIdentifier)

Returns true if the spell is currently being cast, queued mid-GCD, or channeled by the player; false otherwise (including for unresolvable identifiers).

C_Spell.IsCurrentSpell(11366)              -- true while casting Pyroblast
C_Spell.IsCurrentSpell("Pyroblast")        -- same, via name
C_Spell.IsCurrentSpell(GetSpellLink(7620)) -- true while channeling Fishing

Useful for action-bar addons that want to highlight the active / queued button — action bars gate the "currently casting" glow on this. Reads three engine slots and returns true on any match:

  • VAR_CURRENT_CAST_SPELL (0x00CECA88) — cast-bar spellID. Written by FUN_006E4AD0 when a new cast begins; cleared on cast end.
  • VAR_QUEUED_CAST_SPELL (0x00CECAA8) — spell that was active when a new cast superseded it mid-GCD. Restored to current when the new cast ends, so checking it here covers the "queued to cast next" half of the documented semantics.
  • UNIT_FIELD_CHANNEL_SPELL (descriptor +0x228) on the player — covers channeled abilities (Fishing, Drain Soul, Ritual of Summoning, etc.).

C_Spell.IsSelfBuff(spellID)

Returns true if every active effect on the spell targets only the caster — every effect's implicit target (A and B) must be either TARGET_NONE (0) or TARGET_SELF (1).

C_Spell.IsSelfBuff(1006)   -- Inner Fire        → true
C_Spell.IsSelfBuff(7302)   -- Frost Armor       → true
C_Spell.IsSelfBuff(1459)   -- Arcane Intellect  → false (targets friendly)
C_Spell.IsSelfBuff(133)    -- Fireball          → false (targets enemy)

Walks Spell.dbc.EffectImplicitTargetA[3] and EffectImplicitTargetB[3]. Spells with no active effects (a degenerate case for normal content) return false.

C_Spell.SpellHasRange(spellIdentifier) / SpellHasRange(slot, bookType)

Returns true if the spell has a non-zero min or max range — i.e. it's a targeted ability you cast at a unit / location rather than a self-cast (which has range 0).

C_Spell.SpellHasRange(133)    -- Fireball       → true (35yd)
C_Spell.SpellHasRange(1006)   -- Inner Fire     → false (self buff)
SpellHasRange(10, "spell")    -- 10th spellbook slot in player book

It tells you whether a spell has a usable range. It uses the same range that the game uses when it checks distance, so bonuses that change range also change this answer. Spells that you cast on yourself return false. We expose both the namespaced form (any SpellIdentifier) and a positional SpellHasRange(slot, bookType) matching the dual-signature shape used elsewhere in this backport (GetSpellInfo, GetSpellLink, etc.). The bookType argument follows the same convention as GetSpellName(slot, bookType): "spell" (or any non-pet value) → player book, "pet" → pet book.

C_Spell.IsSpellInRange(spellIdentifier, targetUnit)

Returns true if the spell is in range of targetUnit, false if out of range, and nil when the range check doesn't apply — a rangeless spell (self buff), an unresolvable spell / unit, or a unit that the spell cannot be cast on.

spellIdentifier is a spellID, spell link, or the name of a spell in the player's spellbook. targetUnit is a unit token.

C_Spell.IsSpellInRange(133, "target")      -- Fireball → true / false
C_Spell.IsSpellInRange("Fireball", "target")
C_Spell.IsSpellInRange(1006, "target")     -- Inner Fire (self) → nil

Uses the engine's own geometric range core (the same one the action button range-out coloring uses via IsActionInRange): it derives the spell's min/max range — folding in the target's bounding radius — and compares center-to-center distance, so the boundary matches the client exactly, melee and min-range ("too close") spells included.

The target must be one that the spell can be cast on. If it is not, the result is nil. For example, a heal on an enemy, a damage spell on a friend, a creature-type spell on the wrong creature type, or a non-resurrect spell on a dead unit all return nil. Line of sight is not part of the check.

Some spells do not measure to targetUnit:

  • A spell that acts on your pet (for example Mend Pet) measures to your pet.
  • A resurrect on a released ghost measures to the ghost's corpse.
  • A spell from the pet spellbook that you do not know yourself measures from your pet, not from you.

Absent tokens (e.g. "target" with no target) return nil; a genuinely unrecognized token string raises a Lua error, same contract as UnitHealth("garbage").

C_Spell.IsAutoAttackSpell(spellID)

Returns true if the spell is the melee auto-attack — spell ID 6603 ("Auto Attack"), used by every class.

C_Spell.IsAutoAttackSpell(6603)              -- true
C_Spell.IsAutoAttackSpell(133)               -- false

See C_SpellBook.IsAutoAttackSpellBookItem for the spellbook-slot variant.

C_Spell.IsRangedAutoAttackSpell(spellID)

Returns true if the spell is one of the two ranged auto-attacks — spell 75 (Hunter "Auto Shot") or spell 5019 ("Shoot" wand).

C_Spell.IsRangedAutoAttackSpell(75)          -- true (Auto Shot)
C_Spell.IsRangedAutoAttackSpell(5019)        -- true (Shoot wand)
C_Spell.IsRangedAutoAttackSpell(6603)        -- false (melee)

Tests the auto-repeat flag in Spell.dbc.AttributesEx2 (bit 0x20). Only Auto Shot and Shoot carry it. The on-cast ranged abilities — Aimed Shot and Multi-Shot — set the general RANGED bit but not the auto-repeat flag, so they return false. The flag also covers any future auto-repeating spell a private server adds. Verified against Spell.dbc.

See C_SpellBook.IsRangedAutoAttackSpellBookItem for the spellbook-slot variant.

C_Spell.IsNextMeleeSpell(spellID)

Returns true if the spell is an "on next swing" melee ability. These abilities queue and land on your next weapon swing instead of an instant cast (Heroic Strike, Cleave, Maul, Raptor Strike).

C_Spell.IsNextMeleeSpell(78)     -- Heroic Strike → true
C_Spell.IsNextMeleeSpell(6807)   -- Maul → true
C_Spell.IsNextMeleeSpell(116)    -- Frostbolt → false

Tests the two on-next-swing bits in Spell.dbc.Attributes (0x04 and 0x400).

C_Spell.ResetsMeleeSwing(spellID)

Returns true if casting the spell resets your melee swing timer. Cast-time spells reset it when you cast them (Fireball, Frostbolt, Polymorph, wand Shoot). Instant abilities like Sinister Strike do not, so a rogue can weave them between swings.

C_Spell.ResetsMeleeSwing(133)    -- Fireball → true
C_Spell.ResetsMeleeSwing(1752)   -- Sinister Strike → false
C_Spell.ResetsMeleeSwing(1464)   -- Slam → false (flagged "no reset")

Mirrors the server's IsMeleeAttackResetSpell: true when the spell's InterruptFlags has the auto-attack bit (0x08) and its AttributesEx2 does not have the "no reset auto actions" bit (0x20000). Both fields come from Spell.dbc. Slam carries that second bit, so it keeps its swing.

IsHarmfulSpell(spell) / IsHelpfulSpell(spell)

Classify a spell as offensive (IsHarmfulSpell) or non-offensive (IsHelpfulSpell) without parsing its tooltip. Accept either form the spellbook-aware globals use:

IsHarmfulSpell(spellID)             -- by ID
IsHarmfulSpell(slot, "spell")       -- player spellbook slot
IsHarmfulSpell(slot, "pet")         -- pet spellbook slot
-- same calling shapes for IsHelpfulSpell

Examples:

IsHarmfulSpell(133)     -- true  (Fireball)
IsHarmfulSpell(118)     -- true  (Polymorph)
IsHelpfulSpell(2061)    -- true  (Flash Heal)
IsHelpfulSpell(1243)    -- true  (Power Word: Fortitude)
IsHarmfulSpell(2061)    -- false

Reads Spell.dbc.AttributesEx (record +0x1C) bit 0x80 — the same SPELL_ATTR_EX_NEGATIVE bit CMaNGOS uses to mark spells as debuffs server-side. IsHarmfulSpell is true iff that bit is set; IsHelpfulSpell is true iff the spell exists and the bit is NOT set. Both return false for invalid spellIDs.

There is no dedicated "positive" flag, so the helpful side is the rough complement of harmful. For utility spells with no clear orientation (Aspect of the Cheetah, Stealth, ground-targeted AOEs), both can read false elsewhere; we return true for helpful as a safer default for addons gating on "is this castable on me?" logic. Compute more precise semantics by also inspecting effect implicit targets if you need them.

C_Spell.IsSpellHarmful(spellID) / C_Spell.IsSpellHelpful(spellID)

Same classification logic as the globals above, exposed in the C_Spell namespace. Don't accept the legacy (slot, bookType) shape.

C_Spell.IsSpellHarmful(133)     -- true (Fireball)
C_Spell.IsSpellHelpful(2061)    -- true (Flash Heal)

Equivalent to C_Spell.IsSpellHarmful / C_Spell.IsSpellHelpful.

GetSpellSchool(spellID)

Returns (schoolID, schoolName) for any spell — known to the player or not. schoolID is 1-based; schoolName is the locale-independent English name.

schoolID schoolName
1 "Physical"
2 "Holy"
3 "Fire"
4 "Nature"
5 "Frost"
6 "Shadow"
7 "Arcane"

Reads directly from Spell.dbc record +0x04. School is stored as a single 0-based integer (no multi-school SchoolMask combinations), so a spell belongs to exactly one school.

Returns nil for invalid spellIDs (out of range or unpopulated Spell.dbc slot).

local id, name = GetSpellSchool(133)   -- 3, "Fire"   (Fireball)
local id, name = GetSpellSchool(116)   -- 5, "Frost"  (Frostbolt)
local id, name = GetSpellSchool(635)   -- 2, "Holy"   (Holy Light)

Useful for combat-log breakdown addons, dispel-eligibility checks, resistance-aware aura libraries, and damage-meter school tagging. Previously addons either maintained hardcoded spellID → school tables or scanned tooltips for the first-line color tag.

CastSpellByID(spellID [, target])

Casts the spell that has this spell ID. Each rank has its own spell ID, so CastSpellByID(133) casts Fireball Rank 1, not the highest rank that you know.

CastSpellByID(133)              -- Fireball Rank 1, at your current target
CastSpellByID(2050, "player")   -- Lesser Heal Rank 1, on yourself
CastSpellByID(2050, "party1")   -- on party1; your target does not change

The spell must be in your spellbook or in the spellbook of your pet.

  • target is a unit token. The spell goes to that unit, and your current target does not change.
  • Without target, the spell goes to your current target, as with CastSpellByName.
  • A ground-target spell shows the targeting circle for you to click. To place it at a unit, use C_Spell.CastAtUnit.
  • A string that is not a unit token causes an "Unknown unit" error.
Case Return
The cast attempt started true
You and your pet do not know the spell nothing
target names no unit (for example, "target" with no target) nothing

A true return does not prove that the spell cast. The game can still refuse it. For example, the target is out of range, or the spell is on cooldown.

A macro that calls CastSpellByID(<spellID>) shows that spell on its action button, as a /cast line does.

CastSpellNoToggle(name | spellID [, unit [, placeGroundSpell]])

Spam-safe variant of CastSpellByName that won't toggle off an already-active spell. This is what a /cast !Name line calls. Covers both kinds of toggle abilities:

  • Auto-repeat — Shoot, Auto-Shot, Wand. Tracked via the engine's active-auto-repeat global.
  • Self-aura toggles — shapeshift (Cat/Bear/Travel/Moonkin/ Shadowform), stance (Battle/Defensive/Berserker), aspect (Hunter), seal (Paladin), blessing-on-self, etc. Tracked via the unit descriptor's aura array.

If either toggle is already on for the requested spell, the call is a no-op — exactly what /cast !SpellName does, but expressed as a Lua call.

Engine state Behavior Return
Nothing toggled Starts the cast true if the spell is now active, else false
This spell already auto-repeating No-op true
This spell's aura already on (form / stance / aspect) No-op true
A different auto-repeat is active No-op false (don't disrupt the other auto-repeat)

Replaces both the action-bar scan-and-skip pattern and the form-already-active check:

-- Before (auto-repeat case)
for i = 1, 120 do
    if IsAutoRepeatAction(i) then return end
end
CastSpellByName("Shoot")

-- Before (shapeshift case)
if not (UnitBuff("player", "Cat Form") or GetShapeshiftFormID() == CAT_FORM) then
    CastSpellByName("Cat Form")
end

-- After (both cases)
CastSpellNoToggle("Shoot")
CastSpellNoToggle("Cat Form")

Accepts either a name string or a spellID number:

CastSpellNoToggle("Shoot")         -- by name (matches what CastSpellByName accepts)
CastSpellNoToggle(75)              -- by spellID (Auto Shot rank 1)
CastSpellNoToggle("Aspect of the Hawk")
CastSpellNoToggle("Battle Stance")

Optional unit target

A second argument casts the spell at a unit, without changing your current target:

CastSpellNoToggle("Auto Shot", "focus")       -- Auto Shot on your focus
CastSpellNoToggle("Shoot", "targettarget")    -- wand your target's target

The unit is any standard token — "focus", "targettarget", "party1", "mouseover", a player name, and so on. A unit-target or auto-repeat spell fires straight at that unit. A ground-target spell lands at the unit's feet. This is the same cast-at-unit path as C_Spell.CastAtUnit.

A third argument of false holds a ground-target spell back. The spell is cast on the unit, and the reticle comes up for you to click, exactly as the third argument of C_Spell.CastAtUnit works. Leave it out to place the spell at the unit.

CastSpellNoToggle("Auto Shot", "focus", false)

The toggle gates run first, so the unit only matters when the spell actually casts. If the spell is already toggled on, the call stays a no-op and ignores the unit. An unknown unit token raises the engine's standard "Unknown unit" error, the same as UnitHealth.

String input matches case-insensitively and tolerates a trailing (Rank N) suffix the same way CastSpellByName itself does — "Shoot" and "Shoot(Rank 1)" both compare equal to a Shoot that's already auto-repeating. A leading ! is accepted and dropped, so the macro form and the Lua form read the same.

Reads [VAR_ACTIVE_AUTO_REPEAT_SPELL] (0x00CEAC30) for the auto- repeat check, and the engine's FUN_SPELL_IS_TOGGLE_AURA_ACTIVE (0x004B36F0) for the aura-active check. Delegates to Script_CastSpellByName (0x004B4AB0) for the actual cast — same resolution semantics (rank picking, target rules, and more) as the engine's own global. With a unit argument, it dispatches through the same cast-at-unit path as C_Spell.CastAtUnit instead.

Using this from inside a macro action slot? See CastSpellNoToggle as a macro cast line below for the additional parser support that makes the slot tag correctly for action-bar UIs.

C_Spell.CastAtCursor(spellIDOrName)

Casts a ground-target spell at the world position under your cursor. This is the ClassicAPI form of /cast [@cursor] Blizzard. You do not click the reticle.

Accepts a spellID or a spell name:

C_Spell.CastAtCursor(10)                  -- Blizzard Rank 1 (exact spellID)
C_Spell.CastAtCursor(2120)                -- Flamestrike Rank 1
C_Spell.CastAtCursor("Blizzard")          -- highest known rank
C_Spell.CastAtCursor("Blizzard(Rank 6)")  -- that specific rank

A numeric spellID casts that exact spell. Every rank has its own spellID, so 10 casts Blizzard Rank 1, not the highest rank you know. A name can end in (Rank N). A name with no rank casts the highest rank you know.

Returns true when the spell lands at the cursor. Returns false when:

  • the cursor is not on the ground, for example over a frame or off the screen
  • the spell has no ground target
  • the spell needs another target in addition to the ground position
  • the spell is not in your spellbook

If the cursor is not on the ground, the reticle is cancelled and the spell is not cast. A spell with no ground target is cast normally, on your current target.

A reticle that is already up when you call this function is cancelled first. The function only places the reticle that its own call opens.

In a macro, C_Spell.CastAtCursor(10) or C_Spell.CastAtCursor("Blizzard") tags the action button with the spell, the same as /cast Blizzard.

The companion item version is C_Item.UseAtCursor.

C_Spell.CastAtUnit(spellIDOrName, unit [, placeGroundSpell])

Casts a spell at unit, whatever its target type — ClassicAPI's analog of /cast [@unit] Spell:

  • Ground-target spells (Flamestrike, Blizzard, Rain of Fire, …) are placed at the unit's feet, bypassing the AoE reticle click.
  • Unit-target / normal spells (Frostbolt, Renew, a buff, …) fire directly on the unit — regardless of your current selection, so you can buff "party1" or heal "player" while an enemy stays targeted.

Works with any unit token ("target", "player", "mouseover", "party1", "raid7", …). Returns true when the spell was cast at the unit (fired or placed); false for a spell not in your spellbook, a unit with no resolvable position, or a unit the engine won't accept as a target for that spell (wrong faction, out of range).

The first argument takes a spellID or a spell name, with the same exact-rank / (Rank N) semantics as CastAtCursor.

placeGroundSpell defaults to true, which is the behavior above. Pass false to cast a normal spell on the unit as usual but leave a ground-target spell's reticle for the player to click, instead of dropping it on the unit. That is what /cast [@unit] Spell does for every unit but yourself.

-- ground-target: dropped at the unit's feet
C_Spell.CastAtUnit(2120, "target")            -- Flamestrike at the target's feet
C_Spell.CastAtUnit("Blizzard(Rank 6)", "target")

-- unit-target / normal: cast straight on the unit, ignoring current target
C_Spell.CastAtUnit("Frostbolt", "target")
C_Spell.CastAtUnit("Renew", "player")         -- heal yourself mid-fight
C_Spell.CastAtUnit("Power Word: Fortitude", "party1")

If the unit is not a target that the spell accepts, the reticle is cancelled and the function returns false.

When placeGroundSpell is true, a reticle that is already up is cancelled first, so the function only places the reticle that its own call opens. When it is false, nothing is cancelled first. A second call while the reticle is up then closes it, as a second key press does.

A string that is not a unit token raises the standard "Unknown unit" error, as UnitHealth("garbage") does.

In a macro, C_Spell.CastAtUnit(...) tags the action button with its spell, for a spellID or a quoted name.

The companion item version is C_Item.UseAtUnit.

C_Spell.CancelSpellByID(spellID) / CancelSpellByName(name)

Cancel a buff on the player by spellID or by spell name. Both are player-only (the server only accepts CMSG_CANCEL_AURA for the local player's own auras).

C_Spell.CancelSpellByID(2378)      -- Cancel "Find Herbs"
CancelSpellByName("Cat Form")       -- Drop shapeshift
Function Arg Behavior
C_Spell.CancelSpellByID(spellID) spellID (number) Sends CMSG_CANCEL_AURA for spellID directly. No client-side check that the buff is actually present — server validates and silently no-ops if not.
CancelSpellByName(name) name (string, case-insensitive) Walks the player's buff range (UnitBuff slots 0..31) for the first aura whose Spell.dbc localized name matches, then cancels by its spellID. Buff-only — debuffs are skipped.

Calls FUN_006E7040 (0x006E7040) directly with the spellID. This bypasses Script_CancelPlayerBuff's client-side gates — the per-buff cancelable flag at [entry+0x0A] & 0x01 and the fallback AttributesEx & 0x04 check on the spell record. Trade-off: the server is now the sole authority on what's cancelable. Non-cancelable auras (proc-buffs, certain world buffs) still get rejected — just server-side instead of client-side.

For invalid input (non-positive spellID, OOR, or a Spell.dbc empty slot), both functions silently no-op. No lua_error, no event fired.

Spell name matching is case-insensitive via _stricmp. Rank-suffixed input (e.g. "Power Word: Fortitude(Rank 4)") is not parsed — pass the plain name. Multi-rank buffs match on first-found; if you have multiple ranks of the same buff active (unusual but possible with e.g. paladin blessings before talent merge), the first slot wins.

CancelUnitBuff(unit, index [, filter])

Cancels one of your own auras by its index. The function returns nothing.

CancelUnitBuff("player", 1)            -- Cancel your first buff
CancelUnitBuff("player", 2, "PLAYER")  -- Cancel the second buff that you cast
Arg Type Description
unit string A unit token for you, such as "player". For any other unit, the function does nothing.
index number Which aura to cancel, from 1. The index counts only the auras that filter selects.
filter string, optional Aura filter tokens. The default is "HELPFUL". The tokens are the same as for C_UnitAuras.GetAuraDataByIndex.

The index counts in the same order as C_UnitAuras.GetAuraDataByIndex and C_UnitAuras.UnitBuff with the same filter. The built-in UnitBuff can count in a different order. Do not use its index here.

The function cancels only an aura that you can cancel by a right-click on its icon. For other auras, it does nothing. To send the cancel request without this test, use C_Spell.CancelSpellByID.

If unit is not a string or index is not a number, the function raises the error Usage: CancelUnitBuff("unit", index[, "filter"]).

C_Spell.UnitCastingInfo(unit) / C_Spell.CastingInfo()

Returns the unit's in-progress regular cast (not a channel), or nil if it isn't casting. C_Spell.CastingInfo() is C_Spell.UnitCastingInfo("player") without the unit-token lookup.

Under C_Spell, not the global UnitCastingInfo. A global here would clobber addons that ship their own cast-tracking via the _G.UnitCastingInfo or <fallback> idiom — e.g. ShaguTweaks' libcast scrapes the combat log for remote/enemy casts the engine never exposes. If we occupied the global, those addons would adopt our player-only version and lose their (superior, for that case) fallback. So we cede the global names and register under C_Spell instead. Same for UnitChannelInfo/ChannelInfo.

name, displayName, textureID, startTimeMs, endTimeMs, isTradeskill,
  castID, notInterruptible, castingSpellID, castBarID, delayTimeMs
    = C_Spell.UnitCastingInfo(unit)
-- mid-cast:
C_Spell.UnitCastingInfo("player")
-- "Conjure Food", "Conjure Food", "Interface\\Icons\\INV_Misc_Food_...",
--  907928268, 907931268, false, nil, false, 28612, nil, 0

startTimeMs/endTimeMs share GetTime()'s epoch (GetTime()*1000), so progress is (GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs).

Works for any unit. The CGUnit stores no cast times (the cast bar is Lua-driven off SPELLCAST_START), so we source them two ways: the local player is self-tracked on a per-frame tick (VAR_CURRENT_CAST_SPELL, stamping startTimeMs = now, endTimeMs = now + effective cast time); other units come from a co-hook on SMSG_SPELL_START (Spell::Cast), the one packet carrying another unit's cast with a server-authoritative cast time, cached per caster GUID. So C_Spell.UnitCastingInfo("target") now backs enemy/target/focus/nameplate cast bars. Instant casts return nil (no cast bar). Aborted remote casts (interrupt, death, movement, cancel) clear the same tick — a co-hook on the engine's own CGUnit_C::ClearCastingSpell (the choke point every "unit stopped casting" path funnels through) evicts the cached cast the moment the engine stops the unit's cast visual, backstopped by co-hooks on the SMSG_SPELL_FAILURE / SMSG_SPELL_FAILED_OTHER packets for servers that broadcast them (Turtle's core doesn't — its interrupt propagation reaches observers through SuperWoW, which the choke-point hook catches too). Verified: Kick / Earth Shock / Counterspell each clear the victim's bar, while non-interrupting damage doesn't false-clear it. Best-effort for remote units, in three ways: only casts that began while the unit was in range are seen; the remote startTimeMs/endTimeMs are shifted later by roughly your latency (SMSG_SPELL_START carries only a single castTime, so we stamp start = now, end = now + castTime on receipt — the packet has no field to recover elapsed from, so the skew can't be removed); and remote pushback is invisible — the server sends SMSG_SPELL_DELAYED only to the affected caster, so another unit's bar can't stretch when they take damage. The local player is unaffected by all of this — its cast is detected client-side at the moment of cast, no packet involved, and its pushback is tracked.

isTradeskill is real — the spell's SPELL_ATTR_TRADESPELL flag, so profession recipe casts (smelting, cooking/enchanting recipes, …) return true while regular spells return false. endTimeMs reflects the effective cast time (the engine's own cast-time helper: base + level scaling + cast-time talents like Improved Frostbolt + the haste/cast-speed multiplier), so it matches the in-game cast bar — e.g. a Mage's talented Frostbolt reads 2.5s, not the 3.0s base. castID is the cast's castGUID — the exact string the UNIT_SPELLCAST_* events carry for this cast, so you can correlate the polled info with the events (works for the player and other units). delayTimeMs is the pushback the cast has taken so far, in milliseconds. castBarID has no source here and is always nil.

notInterruptible is true when no interrupt or silence that you know can stop the cast. The value is relative to your own abilities, so a rogue and a mage can read different values for the same cast. It is true in two cases: the spell can never be interrupted, or the caster has an aura that blocks every interrupt and silence you know (for example Divine Shield, Ice Block, or Banish). If you know no interrupt and no silence, the value is always false. Limits: a creature's built-in interrupt immunity is server-side data that the client never receives, so a boss with that immunity reads false while your interrupt still fails on it. When the value changes during the cast of your target or focus, UNIT_SPELLCAST_INTERRUPTIBLE or UNIT_SPELLCAST_NOT_INTERRUPTIBLE fires.

C_Spell.UnitChannelInfo(unit) / C_Spell.ChannelInfo()

Returns the unit's in-progress channel, or nil if it isn't channeling. C_Spell.ChannelInfo() is C_Spell.UnitChannelInfo("player") without the token lookup. (Under C_Spell rather than the global name for the same reason as C_Spell.UnitCastingInfo.)

name, displayName, textureID, startTimeMs, endTimeMs, isTradeskill,
  notInterruptible, spellID = C_Spell.UnitChannelInfo(unit)
C_Spell.ChannelInfo()
-- "Blizzard", "Blizzard", "Interface\\Icons\\Spell_Frost_IceStorm",
--  907894314, 907902314, false, false, 10187

The channeled spellID is broadcast for every unit (UNIT_FIELD_CHANNEL_SPELL), so C_Spell.UnitChannelInfo("target") works on other units. Timing now comes from the same SMSG_SPELL_START co-hook as casts: when we observed the channel begin, the remote unit returns full start/end times (validated against the live UNIT_FIELD_CHANNEL_SPELL so a stale cache entry never applies to a different/ended channel); otherwise it falls back to name/displayName/textureID/spellID with nil times. The player path is unchanged (full timing). notInterruptible follows the same rules as in C_Spell.UnitCastingInfo.

UnitCastingDuration(unit) / UnitChannelDuration(unit)

UnitCastingDuration(unit) returns a duration object for the regular cast of the unit. UnitChannelDuration(unit) returns one for the channel of the unit. Each function returns nothing when the unit has no cast or channel.

local duration = UnitCastingDuration("player")
if duration then
    print(duration:GetRemainingDuration())  -- 1.92
end
  • unit takes the same unit tokens as C_Spell.UnitCastingInfo.
  • The time span is the same as the startTimeMs and endTimeMs of C_Spell.UnitCastingInfo or C_Spell.UnitChannelInfo, in seconds. The same limits apply.
  • UnitChannelDuration returns nothing for a unit whose channel you did not see begin. C_Spell.UnitChannelInfo gives nil times for this channel.
  • Each call returns a new duration object. The object keeps the time span that it got at the call. It does not change when the cast gets pushback or when the cast ends. To get the new end time, call the function again on UNIT_SPELLCAST_DELAYED or UNIT_SPELLCAST_CHANNEL_UPDATE.

C_Spell.GetSpellLevelInfo(spellID)

Returns the raw Spell.dbc level fields for a spell:

spellLevel, baseLevel, maxLevel = C_Spell.GetSpellLevelInfo(spellID)
  • spellLevel — the rank's effective level (drives the target-level rule below and per-level scaling).
  • baseLevel — the spell's base level.
  • maxLevel — the level at which per-level scaling caps (0 = no cap).

Returns nil for an invalid / unpopulated spellID.

C_Spell.GetSpellLevelInfo(14752)   -- 30, 30, 40  (Divine Spirit Rank 1)
C_Spell.GetSpellLevelInfo(14818)   -- 40, 40, 50  (Divine Spirit Rank 2)

GetSpellRequiredTargetLevel(spellID)

Also C_Spell.GetSpellRequiredTargetLevel(spellID). Returns the minimum level a target must be for this exact spell (rank) to apply — e.g. Divine Spirit Rank 1 can only be cast on a target level 20 or higher. Returns 0 when the spell has no such restriction, or nil for an invalid spellID.

GetSpellRequiredTargetLevel(14752)   -- 20   (Divine Spirit Rank 1)
GetSpellRequiredTargetLevel(14818)   -- 30   (Divine Spirit Rank 2)
GetSpellRequiredTargetLevel(1243)    -- 0    (Fortitude Rank 1)
GetSpellRequiredTargetLevel(10060)   -- 0    (Power Infusion — single rank)
GetSpellRequiredTargetLevel(133)     -- 0    (Fireball — damage, not a buff)

Mirrors the server's SelectAuraRankForLevel rule: a ranked beneficial aura is castable when targetLevel + 10 >= spellLevel, so the minimum target level is spellLevel - 10. The restriction is applied only when the spell is a ranked positive aura — spellLevel > 10, not passive, part of a rank chain, and applying an aura to a friendly/party/raid target (or an APPLY_AREA_AURA_PARTY effect). Below the minimum, the lowest rank fails with SPELL_FAILED_LOWLEVEL; higher ranks auto-downrank.

The explicit MinTargetLevel / MaxTargetLevel spell fields the server also enforces are server-only columns — they aren't present in the 1.12 client's Spell.dbc, so the spellLevel rule is the only client-readable target-level mechanism.

Cast settings (autoStand, autoDismount, autoUnshift)

Three settings change what occurs when your state blocks a cast. Each one is 0 (the default) or 1. At 0, the cast fails with the usual error.

Setting At 1, before the cast
autoStand If you sit, you stand up.
autoDismount If you are mounted, you dismount.
autoUnshift If the spell fails with "You are in shapeshift form", you leave the form.

Each action occurs only when nothing else blocks the cast. autoDismount and autoUnshift do nothing on a flight path.

When autoUnshift is 1, an action button stays usable in a form that the cast can leave. For example, Healing Touch in Bear Form and Renew in Shadowform. A spell that needs a different form or stance ("Must be in ...") still fails, and you stay in your form.

/console autoDismount 1

SpellActivationOverlay

Spell alerts. The client tells a UI when to show an alert around the center of the screen, and when an action button glows. This system gives the events and one query function. It draws nothing. An addon draws the alerts and the glows.

Two kinds of change start an alert or a glow:

  • A spell becomes usable after a combat event. These are the spells that also fire COMBAT_TEXT_UPDATE with SPELL_ACTIVE: for example, Overpower, Revenge, Execute, Riposte, Counterattack, Mongoose Bite and Hammer of Wrath. While the spell is usable, it glows. These spells have no alert.
  • A proc aura lands on the player. Shadow Trance shows an alert, and Shadow Bolt glows. Clearcasting from Omen of Clarity shows an alert only.

The events are SPELL_ACTIVATION_OVERLAY_SHOW / SPELL_ACTIVATION_OVERLAY_HIDE for alerts and SPELL_ACTIVATION_OVERLAY_GLOW_SHOW / SPELL_ACTIVATION_OVERLAY_GLOW_HIDE for glows.

C_SpellActivationOverlay.IsSpellOverlayed(spellID) / IsSpellOverlayed(spellID)

Returns true while the spell glows, and false at other times. The two names are the same function.

local actionType, id = GetActionInfo(slot)
if actionType == "spell" and C_SpellActivationOverlay.IsSpellOverlayed(id) then
    MyGlow_Show(button)
end

Each rank of a spell has its own spell ID, and every rank in the spellbook glows. A glow continues through a /reload, and its SPELL_ACTIVATION_OVERLAY_GLOW_SHOW does not fire again. Thus, call this function each time that you draw a button.

Enum.ScreenLocationType

The positions of an alert, as SPELL_ACTIVATION_OVERLAY_SHOW gives them in locationType.

Field Value Field Value
Center 0 TopRight 6
Left 1 LeftOutside 7
Right 2 RightOutside 8
Top 3 LeftRight 9
Bottom 4 TopBottom 10
TopLeft 5 LeftRightOutside 11

LeftRight, TopBottom and LeftRightOutside are pairs: the art shows on both sides. The art on the Right, RightOutside and Bottom sides is a mirror image.

SpellBook

Functions keyed on the player or pet spellbook (slot indices, learn requirements, skill-tab membership). The underlying Spell.dbc data is exposed through the Spell section; what lives here is the spellbook-as-a-collection layer.

FindSpellBookSlotByID(spellID)

Inverse of 1.12's GetSpellName(slot, bookType). Given a spellID, returns the 1-based slot it occupies in the player or pet spellbook, along with the matching bookType so the result feeds directly into slot-and-bookType APIs (GetSpellName, GetSpellTexture, GameTooltip:SetSpell, etc.).

slot, bookType = FindSpellBookSlotByID(spellID)
  • Returns (slot, "spell") if the spell is in the player spellbook.
  • Returns (slot, "pet") if it's only in the pet spellbook.
  • Returns nil if the spellID isn't currently in either book.

Player book is searched first, so if a spell somehow appeared in both (unusual but possible for special pet-shared abilities), the player slot wins.

local slot, book = FindSpellBookSlotByID(133)
if slot then
    GameTooltip:SetSpell(slot, book)  -- shows full caster-scaled tooltip
end

Equivalent to the function of the same name (also known as FindSpellBookSlotBySpellID).

C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)

Returns a table describing the spell in a spellbook slot. slotIndex is 1-based across the whole book. spellBank picks the book — Enum.SpellBookSpellBank.Player (0) or Enum.SpellBookSpellBank.Pet (1).

local info = C_SpellBook.GetSpellBookItemInfo(1, Enum.SpellBookSpellBank.Player)
-- info.name, info.spellID, info.itemType, info.isPassive, info.iconID, ...

Table fields:

Field Type Notes
itemType Enum.SpellBookItemType Spell (1) for the player book, PetAction (3) for the pet book.
actionID number The spellID. Equals spellID — there is no spell-override system.
spellID number The spellID in the slot.
name string The localized spell name.
subName string The rank text (e.g. "Rank 3"), or "" when the spell has no rank.
iconID string The icon path — feed it straight to texture:SetTexture(...).
isPassive boolean true for a passive spell.
isOffSpec boolean Always false.

Returns nil for an empty or out-of-range slot.

Deviations forced by the data. iconID is a texture path, not a fileID (there is no fileID system) — the same deviation as C_Spell.GetSpellInfo. skillLineIndex is not returned (nil) — use C_SpellBook.GetSpellBookItemSkillLineIndex. isOffSpec is always false — there are no specializations.

C_SpellBook.GetNumSpellBookSkillLines()

Returns the number of tabs in the player spellbook: General, then one tab per skill line such as Fire or Fury.

C_SpellBook.GetSpellBookSkillLineInfo(skillLineIndex)

Returns a table describing spellbook tab skillLineIndex (1-based), or nil for an index outside 1 .. GetNumSpellBookSkillLines().

for i = 1, C_SpellBook.GetNumSpellBookSkillLines() do
    local tab = C_SpellBook.GetSpellBookSkillLineInfo(i)
    for slot = tab.itemIndexOffset + 1, tab.itemIndexOffset + tab.numSpellBookItems do
        local item = C_SpellBook.GetSpellBookItemInfo(slot, Enum.SpellBookSpellBank.Player)
        print(tab.name, item.name)
    end
end
Field Type Notes
name string The tab name ("General", "Fire", "Fury", ...).
iconID string The tab icon path — feed it to texture:SetTexture(...).
itemIndexOffset number This value + 1 is the first slotIndex of the tab.
numSpellBookItems number The number of spells in the tab.
isGuild boolean Always false.
shouldHide boolean Always false.
specID, offSpecID nil Always nil.

The name, icon, offset, and count are the same values GetSpellTabInfo(skillLineIndex) returns.

C_SpellBook.GetSpellBookItemSkillLineIndex(slotIndex, spellBank)

Returns the 1-based tab index that holds player-book slot slotIndex. Returns nil for a slot past the last spell, and for every pet-book slot (the pet book has no tabs).

C_SpellBook.GetSpellBookItemSkillLineIndex(1, Enum.SpellBookSpellBank.Player)  -- 1 (General)

C_SpellBook.GetSkillLineIndexByID(skillLineID)

Returns the 1-based tab index for a SkillLine.dbc ID, or nil when the player has no tab for that skill line. The General tab has no skill line ID.

local _, skillLineID = C_SpellBook.GetSpellSkillLine(133)   -- Fireball: "Fire", 8
local tab = C_SpellBook.GetSkillLineIndexByID(skillLineID)  -- the Fire tab, or nil on a non-mage

Paired with C_SpellBook.GetSpellSkillLine it answers "which of my tabs holds this spellID" without walking the book.

C_SpellBook.GetSpellBookItemCastCount(slotIndex, spellBank)

Spellbook-slot variant of C_Spell.GetSpellCastCount. Returns 0 for an empty slot.

C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo(slotIndex, spellBank)

Spellbook-slot variant of C_Spell.GetSpellLossOfControlCooldown, returned as a table. Returns nil for an empty slot.

Field Type Notes
startTime number Start of the blocking effect, in GetTime() seconds. 0 when nothing blocks the spell.
duration number Length of the blocking effect in seconds. 0 when nothing blocks the spell.
modRate number Always 1.
isActive boolean true while an effect blocks the spell.
shouldReplaceNormalCooldown boolean true when the effect outlasts the spell's own cooldown. A button then draws the loss-of-control swipe instead of the normal one.

C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration(slotIndex, spellBank)

The duration field above on its own. Returns nil for an empty slot.

C_SpellBook.IsClassTalentSpellBookItem(slotIndex, spellBank)

Returns true when the spell in the slot comes from a talent: the talent's own spell (Mortal Strike rank 1), or a higher rank of that ability that you trained (Mortal Strike rank 2). Returns false for every pet-book slot and for an empty slot.

local bank = Enum.SpellBookSpellBank.Player
local info = C_SpellBook.GetSpellBookItemInfo(slot, bank)
if C_SpellBook.IsClassTalentSpellBookItem(slot, bank) then
    print(info.name, "is a talent ability")
end

Higher ranks are found in two ways: through the next-rank links in the client data, and through the spell name. A spellbook spell with the same name as a talent's spell counts as a rank of that talent. Mind Flay is one chain that needs the second way.

C_SpellBook.ContainsAnyDisenchantSpell()

Returns true when the player knows a spell that disenchants items (Disenchant, from Enchanting).

C_SpellBook.GetSpellLevelLearned(spellID)

Returns the level at which a spell becomes available — the BaseLevel field in Spell.dbc (record +0x70). Direct read off the cached record; no class/race filtering.

C_SpellBook.GetSpellLevelLearned(133)    -- Fireball rank 1 → 4
C_SpellBook.GetSpellLevelLearned(25306)  -- Fireball rank 12 → 60
C_SpellBook.GetSpellLevelLearned(2061)   -- Flash Heal rank 1 → 20

Returns 0 for invalid spellIDs, spells with no level requirement (most non-class utility spells), or records the engine hasn't cached. Unknown / utility spells return 0 rather than nil.

C_SpellBook.GetCurrentLevelSpells([level])

Returns a 1-based table of spellIDs the player's class/race can learn at the given level. Without arguments, defaults to the player's current level.

C_SpellBook.GetCurrentLevelSpells()    -- spells trainable at your level
C_SpellBook.GetCurrentLevelSpells(20)  -- preview what's available at 20
C_SpellBook.GetCurrentLevelSpells(60)  -- preview at 60

Walks SkillLineAbility.dbc filtering by the player's class bit (1 << (classID - 1)) and race bit, respecting both the include masks (entries with mask = 0 are "all classes"/"all races", which also pass) and the exclude masks. For each surviving entry, looks up the spell's BaseLevel and includes it if it matches the queried level.

Spell learning is trainer-driven. Elsewhere GetCurrentLevelSpells returns auto-learned spells. Here you must visit a trainer to actually learn most class spells. We return the closest available analog: what's trainable at this level. Useful for "what's new this level" UI panels and level-preview tooling.

Class/race come from the local player — there's no (class, race, level) form because there's no clean class-string→classID lookup. Returns an empty table at character select / pre-login (no CGPlayer yet) and for levels where no class/race spells match.

C_SpellBook.GetPlayerSpellsByAura(auraName)

Returns every spell the player currently knows that has an effect applying the given aura, as a 1-based array of spell IDs in ascending order. Empty when none does.

spellIDs = C_SpellBook.GetPlayerSpellsByAura(auraName)

auraName is an aura code — the same number C_Spell.GetSpellEffectInfo reports in its auraName field. 0 means "applies no aura" and returns an empty array.

This is IsPlayerSpell asked the other way round. Instead of testing one spell, it reads the same set of known spells and reports the ones whose effects carry the aura. A talent is present at its current rank only, so summing the amounts of the result never counts two ranks of one talent.

-- everything the player knows that shortens the energy tick
for _, spellID in ipairs(C_SpellBook.GetPlayerSpellsByAura(217)) do
    local fx = C_Spell.GetSpellEffectInfo(spellID)
    -- read the amount from the effect whose auraName is 217
end

Known is not the same as active. For a passive — a talent, a racial — it is: known means in effect, and a passive never appears in the buff list. A castable buff in the result is only learned; whether it is up is a question for the buff list. And a buff another player puts on you is not a spell you know, so it is not here at all. Split the result on C_Spell.IsSpellPassive and check active buffs separately:

local mod = 0
for _, spellID in ipairs(C_SpellBook.GetPlayerSpellsByAura(217)) do
    if C_Spell.IsSpellPassive(spellID) then
        mod = mod + AmountOf(spellID)      -- in effect while known
    end
end
for i = 1, 32 do
    local spellID = select(10, C_UnitAuras.UnitAura("player", i, "HELPFUL"))
    if not spellID then break end
    mod = mod + AmountOf(spellID)          -- in effect while up
end

A list built this way follows the data: a retuned value, a new rank, or an added source shows up without a code change.

C_SpellBook.GetSkillLineName(skillLineID)

Returns the localized SkillLine.dbc name for a skill-line id — the id → name half of the bridge addons build by hand. A pure DBC read: it answers for any valid skill line (182 = Herbalism, 186 = Mining, the weapon/defense lines, profession lines) whether or not the player has learned it. nil for non-numeric / non-positive input or an id with no SkillLine.dbc row.

Pairs with GetSkillLineRank — name from the DBC, rank from the player's live skills.

C_SpellBook.GetSkillLineName(182)   -- "Herbalism"
C_SpellBook.GetSkillLineName(186)   -- "Mining"

C_SpellBook.GetSkillLineRank(skillLineID)

Returns (curRank, maxRank, modifier) — the local player's rank in a skill line by SkillLine.dbc id — or nil if the player hasn't learned it.

  • curRank — current skill value (base, no temp bonus)
  • maxRank — the line's cap at the player's level
  • modifier — temporary bonus (weapon oils, buffs; 0 if none)
local cur, max = C_SpellBook.GetSkillLineRank(182)  -- Herbalism: 285, 300 (nil if unlearned)
if C_SpellBook.GetSkillLineRank(186) then           -- has Mining at all?
    -- ...
end

The stock GetSkillLineInfo(index) only walks the skill window by position and returns skills by name, so an addon starting from a SkillLine.dbc id (a quest's requiredSkill) has to map id → localized name and then string-match every skill line to recover the rank. This reads the player's live skill list keyed by id directly — the returned ranks are the same values GetSkillLineInfo reports. Resolves the id to the player's skill slot via FUN_SKILL_LINE_TO_SLOT, then reads the CGPlayer skill table (the same storage C_Item.IsEquippableItem's require-skill gate uses). nil when the line isn't learned, for bad input, or before the player object is resident (character select).

C_SpellBook.GetSpellSkillLine(spellID)

Returns (name, skillID) — the SkillLine.dbc row that the given spell belongs to. The name is the user-facing skill/tab string in the engine's current locale: spellbook tab names for class spells ("Protection", "Fire", "Holy"), profession headers ("Tailoring", "Engineering"), weapon-skill rows ("Swords", "Bows"), etc.

C_SpellBook.GetSpellSkillLine(1671)   -- "Protection", 26 (Shield Bash)
C_SpellBook.GetSpellSkillLine(133)    -- "Fire", 8        (Fireball)
C_SpellBook.GetSpellSkillLine(2050)   -- "Holy", 56       (Lesser Heal)
C_SpellBook.GetSpellSkillLine(2480)   -- "Bows", 45       (Shoot Bow)
C_SpellBook.GetSpellSkillLine(3273)   -- "First Aid", 129 (Anesthetic)

Walks SkillLineAbility.dbc for any row whose spellID matches, then reads the localized Name[locale] field at offset +0x0C of the referenced SkillLine.dbc record. For spells with multiple SLA rows (race-locked variants, Turtle WoW's faction-specific entries) the lookup prefers a row whose class/race masks match the local player — so the result reflects "what tab is this spell in for me" when one exists. Falls back to the first matching row otherwise, so the function still answers for spells the local player can't actually use (inspecting other classes, parsing combat-log entries from unfamiliar specs).

Returns (nil, nil) for:

  • non-numeric or non-positive input
  • spellIDs with no SkillLineAbility.dbc row (most temporary auras, proc-only spells, item-on-use effects, GM-debug spells)
  • rows whose skillID doesn't resolve in SkillLine.dbc

The stock GetSpellTabInfo(tabIndex) enumerates spellbook tabs by 1-based index — it can't answer "what tab is this spellID in". Addons that need a spellID→tab mapping have historically had to walk every tab's slot range and string-match against GetSpellName. This function reads it directly off the DBC for any spellID, including spells the player hasn't learned.

C_SpellBook.IsAutoAttackSpellBookItem(slot, bookType)

Spellbook-slot variant of C_Spell.IsAutoAttackSpell. Resolves the spellbook slot to its spellID first, then tests == 6603 (melee auto-attack).

C_SpellBook.IsAutoAttackSpellBookItem(1, "spell")

C_SpellBook.IsRangedAutoAttackSpellBookItem(slot, bookType)

Spellbook-slot variant of C_Spell.IsRangedAutoAttackSpell. Resolves the slot to a spellID, returns true if it's 75 (Auto Shot) or 5019 (Shoot wand).

C_SpellBook.IsRangedAutoAttackSpellBookItem(10, "spell")
-- true if slot 10 is your wand Shoot

State

Player movement / visibility state queries, as no-arg globals. The stock client doesn't bind them to Lua despite the engine tracking the underlying state — broadcast in UpdateFields for some (mount, stealth visibility), local-only for others (falling, swimming).

IsMounted()

Returns true if the player is currently mounted, false otherwise.

Reads UNIT_FIELD_MOUNTDISPLAYID from the player's broadcast descriptor. The field holds a creature display ID (the model the engine renders under the player) when mounted, and 0 otherwise.

if not IsMounted() then
    CastSpellByName("Summon Dreadsteed")
end

Dismount()

Dismisses the player's currently summoned mount. No-op when the player isn't mounted.

if IsMounted() then Dismount() end

Walks the player's buff range for an aura whose Spell.dbc effects include SPELL_AURA_MOUNTED (78), then sends CMSG_CANCEL_AURA for that spellID via the same direct sender C_Spell.CancelSpellByID uses. By the time the server processes the cancel, IsMounted() will return false.

IsStealthed()

Returns true if the player is currently in Stealth (Rogue) or Prowl (Druid), false otherwise.

Reads bit 0x02 of the player visibility byte at descriptor +0x17C and AND-gates with MountDisplayID == 0 to disambiguate mount (which sets the same bit). Untested for Druid shapeshift forms — if you find a false-positive there, file an issue and we'll switch to walking the player's aura array for the actual stealth spell.

if IsStealthed() then
    -- defer the spell that would break stealth
end

IsFalling()

Returns true if the player is currently mid-jump or falling, false otherwise.

Reads the local CGPlayer movement-flags word at +0x9E8 and tests MOVEFLAG_FALLING | MOVEFLAG_FALLING_FAR (0x2000 | 0x4000). This is client-side state (outbound MSG_MOVE_* data, never visible for remote units), so the function only meaningfully applies to the local player.

if not IsFalling() then
    -- safe to bind ground-targeted spell
end

IsSwimming()

Returns true if the player is currently swimming (in liquid, with the swim animation/movement set), false otherwise.

Same movement-flags word as IsFalling, testing MOVEFLAG_SWIMMING (0x200000). Local-player only.

if IsSwimming() then
    -- breath bar logic, mount-failure suppression, etc.
end

IsIndoors()

Returns 1 if the local player is under a WMO roof — inside a building, cave, or instance interior — and nil otherwise. nil before the player object exists (pre-world).

Recomputed live from the engine's WMO geometry query, so it flips as you cross an interior threshold rather than tracking the current zone/area — you can be indoors and outdoors within the same subzone. Returns 1/nil (not true/false) to match the historical contract and SuperWoW 2.2, so both if IsIndoors() and IsIndoors() == 1 work.

if IsIndoors() then
    -- suppress a mount cast that would just fail
end

IsOutdoors()

Returns 1 if the local player is outdoors (open sky, or simply not inside a WMO interior) and nil otherwise. Exact complement of IsIndoors() for a resolvable player; both return nil pre-world.

if IsOutdoors() then
    CastSpellByName("Mount Up")
end

IsAssistingRitual()

Returns true if the local player is currently clicking a warlock summoning portal (the channel-on-GameObject state with no castbar UI, where movement breaks the channel), false otherwise.

This is distinct from spell channeling: the function fires for participants who clicked the portal, not the warlock who cast Ritual of Summoning. There is no stock Lua surface for this state — the engine's SPELLCAST_CHANNEL_* events don't fire and C_Spell.CastingInfo() returns nothing — so addons that want to react to the player being committed to a ritual (e.g. suppress autorun toggles, hide nameplate clicks, warn before movement) have no other way to detect it.

Detection uses two state slots:

  • UNIT_CHANNEL_SPELL (descriptor +0x228) — the warlock's channel spell ID (698) is mirrored onto every clicker for the duration of the ritual.
  • A CGPlayer-local pointer at +0xB4 — the engine's "current spell cast target GameObject" slot, set to the portal GO while the clicker is engaged.

Either signal alone is ambiguous (the warlock channeling matches the first; mining and other GO-targeted casts match the second); the conjunction is portal-clicker-specific.

The engine fires SPELLCAST_CHANNEL_START / SPELLCAST_CHANNEL_STOP on the portal click, even though there's no castbar — Blizzard's castbar UI filters out spell ID 698 (Ritual of Summoning), but the events themselves fire normally. Combine them with this function for ritual-specific triggers:

local frame = CreateFrame("Frame")
frame:RegisterEvent("SPELLCAST_CHANNEL_START")
frame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
frame:SetScript("OnEvent", function()
    if event == "SPELLCAST_CHANNEL_START" and IsAssistingRitual() then
        -- player just committed to a ritual portal
    elseif event == "SPELLCAST_CHANNEL_STOP" then
        -- channel ended; was it a ritual?
    end
end)

Local-player only — the +0xB4 pointer lives on the CGPlayer object, not in the broadcast descriptor, so the function can't report state for target / party* / etc. Returns false when called before the player object is initialized (login screen).

IsInGroup()

Returns true if the player is in a party or a raid, false otherwise. Shortcut over GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0.

Accepts an optional groupType argument (Enum.PartyCategory.Home / Instance). There is no LFG / LFD instance-group concept here, so the argument is accepted and ignored.

if IsInGroup() then
    SendChatMessage("ready check", "PARTY")
end

IsInRaid()

Returns true if the player is in a raid (specifically — parties return false), false otherwise. Same groupType argument story as IsInGroup.

local channel = IsInRaid() and "RAID" or "PARTY"
SendChatMessage("ready check", channel)

GetMirrorTimerInfo(index) / GetMirrorTimerProgress(timer)

These functions read the state of the mirror timer bars: EXHAUSTION (fatigue in deep water), BREATH (under water), and FEIGNDEATH.

GetMirrorTimerInfo(index) returns the state of one timer slot. index is 1, 2, or 3. Slot 1 is EXHAUSTION, slot 2 is BREATH, and slot 3 is FEIGNDEATH. If index is a different number, the function returns nothing.

Return Type Meaning
timer string "EXHAUSTION", "BREATH", or "FEIGNDEATH". "UNKNOWN" when the slot is empty
value number The value in milliseconds when the timer started. It does not change while the timer runs
maxValue number The full value in milliseconds
scale number The change of the value for each millisecond. A negative scale drains the bar (-1 under water). A positive scale fills it
paused number 1 when the timer is paused, else 0
label string The text of the bar, for example "Breath". When a spell started the timer, the name of that spell
spellID number The spell that started the timer, or 0

An empty slot returns "UNKNOWN" and 0 for each number.

GetMirrorTimerProgress(timer) returns the current value of a timer in milliseconds. timer is a timer name, in any letter case. The function calculates the value from value, scale, and the time since the timer started.

  • If the timer is not active, the function returns 0.
  • If the timer is paused, the function returns value.
  • If timer is not a timer name, the function returns nil.
  • The result can go below 0 or above maxValue before the server stops or changes the timer.

Both functions already return the new state in a MIRROR_TIMER_START or MIRROR_TIMER_STOP handler. When the player leaves the world (a logout or a loading screen), all slots become empty.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("MIRROR_TIMER_START")
f:SetScript("OnEvent", function()
    for i = 1, 3 do
        local timer, value, maxValue, scale, paused, label = GetMirrorTimerInfo(i)
        if timer ~= "UNKNOWN" then
            print(format("%s: %d / %d ms (%s)", label, value, maxValue,
                paused == 1 and "paused" or "active"))
        end
    end
end)

-- bar is a StatusBar with SetMinMaxValues(0, maxValue / 1000)
bar:SetScript("OnUpdate", function()
    bar:SetValue(GetMirrorTimerProgress("BREATH") / 1000)
end)

GetShapeshiftForm([requireForm])

Returns the stance-bar index of your active form or stance. This is the same 1-based index that GetShapeshiftFormInfo(index) takes. Returns 0 when no form or stance is active. GetShapeshiftForm(true) is the number that [stance:N] and [form:N] test.

local index = GetShapeshiftForm()
if index > 0 and index <= GetNumShapeshiftForms() then
    local icon, name = GetShapeshiftFormInfo(index)
    print("active:", name)
end
  • The index depends on which forms you know and on their order on the stance bar. The same form can have a different index on a different character. To identify a form, use GetShapeshiftFormID().
  • If you are in a form that is not on your stance bar, the result is the number of stance-bar spells plus 1.
  • If requireForm is true and you have no shapeshift form, the result is 0, even when a stance-bar spell that is not a form is active.

GetShapeshiftFormID()

Returns the player's current shapeshift form as the integer ID from SpellShapeshiftForm.dbc. Returns 0 when the player isn't shifted.

Class Form IDs
Druid 1 Cat, 2 Tree, 3 Travel, 4 Aquatic, 5 Bear, 8 Dire Bear, 31 Moonkin
Warrior 17 Battle, 18 Defensive, 19 Berserker
Shaman 16 Ghost Wolf
Rogue 30 Stealth
Priest 28 Shadowform, 32 Spirit of Redemption

Turtle WoW extends the DBC with custom rows that aren't present on the stock client:

ID Name Notes
9 Tree of Life Form Restoration druid endgame form
11 Swift Travel Form Mounted-speed travel form

Form numbering is client-specific. These IDs are the ones this client uses — e.g. 3 for Travel; Turtle uses 9 for Tree of Life. Other clients number the table differently. Don't copy named constants from another addon and expect them to match.

if GetShapeshiftFormID() == 0 then
    CastSpellByName("Cat Form")
end

Reads byte 2 of UNIT_BYTES_1 (descriptor +0x212) on the local player — the same byte the engine's Script_GetShapeshiftFormInfo compares against each form-spell's effect-encoded form ID to answer "is this form active". The engine updates the byte from SMSG_UPDATE_OBJECT aura/form packets, so the value is live without needing an event listener.

The stock client has only GetShapeshiftFormInfo(index) (1-based bar index); GetShapeshiftFormID exposes the DBC ID directly so callers can write if formID == CAT_FORM without iterating the bar.

CancelShapeshiftForm()

Cancels the player's current shapeshift form, dropping them back to caster (or whatever neutral state the class uses). No-op when the player isn't shifted.

if GetShapeshiftFormID() ~= 0 then
    CancelShapeshiftForm()
end

Covers druid forms (Cat / Bear / Travel / Aquatic / Moonkin / Dire Bear / Tree of Life), shaman Ghost Wolf, priest Shadowform / Spirit of Redemption, and rogue Stealth. No form table hardcoding — the implementation finds whichever buff currently provides the active form by scanning Spell.dbc effects.

Does not cover warrior stances. Warriors are always in a stance — the engine has no "no stance" state and right-clicking the active stance in the stance bar does nothing. The server rejects the cancel packet for stances, so this function is a no-op when called as a warrior. Switch stances with the normal stance spells instead.

Implementation: reads the current form byte (descriptor +0x212, same source as GetShapeshiftFormID()), walks the player's buff slots (0..31), looks up each spell's Spell.dbc[spellID] effect arrays, and on the first effect whose EffectApplyAuraName is SPELL_AURA_MOD_SHAPESHIFT (36) and whose EffectMiscValue matches the current form ID, sends CMSG_CANCEL_AURA via the engine's direct sender at FUN_006E7040.

Does an effect-array scan + form-id match before issuing its cancel packet. The engine has the machinery; it just lacks the public Lua surface.

GetSheathState()

Returns the weapon type that the player has drawn. Takes no arguments. The value is 1-based:

Value Meaning
1 None. The weapons are put away.
2 A melee weapon is drawn.
3 A ranged weapon is drawn.

Returns 1 before the player object exists (out of world).

if GetSheathState() == 3 then
    -- the ranged weapon is out
end

The engine has ToggleSheath() but no matching getter. GetSheathState adds the query, with 1-based values. To change the state, use the built-in ToggleSheath().

IsLoggingOut()

Returns true while a logout or quit countdown runs, false otherwise. Takes no arguments.

The value becomes true when Logout() or Quit() starts the countdown. It becomes false when the logout completes, when the player cancels it, or when the server refuses it. An instant logout (for example, in an inn) does not set it.

if IsLoggingOut() then
    CancelLogout()
end

IsPlayerInWorld()

Returns true when the player is in the world, false during a loading screen. Takes no arguments.

The value becomes false at the start of each loading screen (login, instance or continent change, logout). It becomes true again when the player enters the world, before PLAYER_ENTERING_WORLD fires.

if IsPlayerInWorld() then
    -- safe to read world state
end

SwingTimer

C_SwingTimer reports whether your current target is in range for a melee or ranged auto-attack. See PLAYER_SWING for the matching attack-timer event, and Enum.PlayerSwingType for the weapon values both use.

C_SwingTimer.EnableRangeCheck(swingType, enable)

Turns PLAYER_SWING_RANGE_UPDATE on or off for one swing type.

C_SwingTimer.EnableRangeCheck(Enum.PlayerSwingType.MainHand, true)

After you enable a swing type, call C_SwingTimer.IsTargetWithinSwingRange once to read the current range. The event fires only on a later change.

C_SwingTimer.IsTargetWithinSwingRange(swingType)

Returns whether your current target is in range for a swing type (an Enum.PlayerSwingType value).

local inRange = C_SwingTimer.IsTargetWithinSwingRange(Enum.PlayerSwingType.MainHand)

Returns nil when no range answer is possible. Reasons include no current target, an unattackable target, or no weapon equipped for that swing type. A nil value must not be read as out of range. Auto-attacks apply only to the current target, so no other unit can be queried.

System

Host/OS-level helpers that aren't tied to a game domain.

GetPhysicalScreenSize()

Returns (widthPixels, heightPixels): the size of the game window in pixels. In a window, this is the size of the window, and it changes when you resize the window. PixelUtil uses this function to convert pixels to UI units (768.0 / physicalHeight). With this conversion, addons can put frames on whole pixels, so borders stay sharp at all UI scales.

local w, h = GetPhysicalScreenSize()   -- for example 1920, 1080

GetScreenWidth() and GetScreenHeight() return UI units, not pixels.

The ClassicAPI addon supplies the PixelUtil table (PixelUtil.SetPoint, SetSize, SetStatusBarValue, and more), which uses this function.

CopyToClipboard(text [, removeMarkup])

Copies text to the Windows clipboard and returns the number of bytes copied. There is no stock clipboard access, so this is a ClassicAPI addition backed by the Win32 clipboard API. Available on both the in-game and login/character-select screens.

  • text — the string to copy (numbers are coerced, like Lua does).
  • removeMarkup — optional; when truthy, strip WoW UI escape sequences before copying so the clipboard gets plain text. The result is the same as Ctrl+C in an edit box:
    • |cAARRGGBB … |r color codes → removed
    • |H…|h text |h hyperlinks → keeps only the visible text
    • |T…|t textures and |A…|a atlases → removed
    • |n → newline, || → literal |
    • unknown |x sequences are left verbatim, so real text that merely contains a pipe survives
  • returns the byte length of the copied (possibly stripped) string — consistent with Lua's #s. 0 for an empty string or if the clipboard couldn't be opened.
CopyToClipboard("hello world")                              -- 11
local link = "\124cffff0000Red\124r"                        -- real single-pipe markup
CopyToClipboard(link, true)                                 -- 3   (clipboard gets "Red")
CopyToClipboard(link)                                       -- 15  (raw escapes copied)

The string is treated as UTF-8 (matching the credential and TTS modules) and written as CF_UNICODETEXT; Windows synthesizes CF_TEXT for legacy consumers.

Note when testing from the chat box: the edit box escapes every typed | into ||, so a literal |cff… you type is no longer a color code by the time it reaches Lua. Build real markup with \124 (as above) to exercise removeMarkup.

FlashClientIcon([briefly])

Flashes the client's taskbar button to get the player's attention (a retail backport). Does nothing while WoW is the foreground window, and returns nothing.

  • briefly — optional, default false. When falsy, flashes until the client is focused (Windows stops it on its own). When truthy, flashes three times and stops.
FlashClientIcon()       -- flash until focused (e.g. a battleground queue pops while alt-tabbed)
FlashClientIcon(true)   -- brief flash

Talent

GetTalentSpellID(tabIndex, talentIndex, [rank[, classID]])

Returns the spellID for the talent at the given (tabIndex, talentIndex) and rank, or nil if the talent index is out of range or the rank slot is empty. 1.12's GetTalentInfo returns (name, icon, tier, column, currentRank, maxRank, ...) with no spellID; this fills the gap so addons can chain into the spell APIs without maintaining their own (class, tab, idx) → spellID lookup tables.

GetTalentSpellID(1, 9)           -- 14751  (Inner Focus, single rank)
GetTalentSpellID(1, 1)           -- 14525  (Wand Specialization, current rank)
GetTalentSpellID(1, 1, 1)        -- 14524  (Wand Specialization rank 1)
GetTalentSpellID(1, 1, 2)        -- 14525  (Wand Specialization rank 2)
GetTalentSpellID(1, 1, 1, 8)     -- 11210  (Arcane Subtlety r1 — Mage, classID 8)

-- chains into the spell APIs cleanly
GameTooltip:SetSpellByID(GetTalentSpellID(1, 9))
print(C_Spell.GetSpellName(GetTalentSpellID(1, 9)))  -- "Inner Focus"

rank is optional. When omitted, defaults to the player's currentRank by delegating to the engine's existing GetTalentInfo (5th return) — that's the same player-state derivation the engine uses for talent UI rendering, including the spell-knowledge checks that account for talent-granted abilities. If currentRank is 0 (no points spent), falls back to rank 1 so the function still produces the talent's canonical spellID for unallocated talents.

When provided explicitly, returns the spellID at that rank regardless of how many points the player has invested. Useful for tooltip-on-hover scenarios where you want to preview "what rank 5 would do" without respec'ing.

classID is optional (4th arg; pass rank as nil to keep the default) and queries any class's talent tree via Talent.dbc / TalentTab.dbc — the same cross-class path as GetTalentIDByIndex, with identical (tab, idx) ordering. Because there's no "currentRank" for a class you aren't, a cross-class query with no explicit rank defaults to rank 1 (rather than the player-currentRank default of the same-class path).

Returns nil for:

  • non-numeric or non-positive tabIndex / talentIndex
  • tabIndex or talentIndex out of range for the class
  • explicit rank exceeding the talent's allocated max — e.g. asking for rank 5 on a 1-rank talent like Inner Focus
  • a classID that matches no class

Without classID, reads the engine's per-tab talent arrays at [0x00BDCD28] (populated at login from Talent.dbc filtered by the player's class); with it, reads the Talent.dbc flat array directly. Either way the SpellRank[] array lives at offset +0x10 of each record (stride 0x54), one spellID per rank — the engine populates indices 0..4 (ranks 1..5), the higher slots stay zero.

Equivalent to one of GetTalentInfo's extended returns — the talent's spellID.

GetTalentIDByIndex(tabIndex, talentIndex[, classID])

Returns the engine's hidden talentID — the primary key of the Talent.dbc row — for the talent at the given (tab, idx). Returns nil for out-of-range indices.

GetTalentIDByIndex(1, 9)      -- 174  (Inner Focus, Discipline tier 3)
GetTalentIDByIndex(1, 1)      -- 166  (first Discipline talent)
GetTalentIDByIndex(1, 1, 8)   -- 37   (first Arcane talent — Mage, classID 8)

The stock GetTalentInfo(tab, idx) returns (name, icon, tier, column, currentRank, maxRank, ...) but NOT the talentID. We add this getter so addons that key on talentIDs (used for GetTalentInfoByID, talent build sharing strings, etc.) work unmodified.

The optional classID (1-based: Warrior 1, Paladin 2, Hunter 3, Rogue 4, Priest 5, Shaman 7, Mage 8, Warlock 9, Druid 11) queries any class's talent tree, not just the local player's — useful for talent-build tooling and previewing other specs. When omitted or nil, it falls back to the player's own class.

  • Without classID — reads TalentEntry+0x00 from the per-tab talent arrays at [0x00BDCD28] (the engine's runtime state for the local player's class). Same struct walk as GetTalentSpellID.
  • With classID — reads the Talent.dbc / TalentTab.dbc flat arrays directly, replicating the engine's tree builder (FUN_004f2c00): tabs in DBC row order filtered by class-mask, then talents in DBC row order within each tab. This is the same ordering the runtime arrays are built from, so GetTalentIDByIndex(t, i) and GetTalentIDByIndex(t, i, <player's class>) return identical values.

Equivalent to the talentID return slot of GetTalentInfo (which the stock client doesn't expose).

Targeting

More target-selection functions, next to TargetNearestEnemy, TargetNearestFriend, TargetNearestPartyMember, TargetNearestRaidMember, TargetLastEnemy, TargetLastTarget, AssistUnit, TargetUnit and ClearTarget.

Not backported (no equivalent here — soft-target / action-camera features): TargetPriorityHighlightStart/End, IsTargetLoose, TargetToggle.

GetPlayerFacing()

Returns the player's facing as an angle in radians (0 … 2*pi, increasing counter-clockwise), or nothing off-world (glue / loading).

local facing = GetPlayerFacing()   -- e.g. 1.65

TargetDirectionEnemy(facing [, angle])

Targets the enemy in a direction on the screen. Use it for keys or a gamepad stick that move the target left, right, up or down.

  • facing is the direction in radians, measured clockwise on the screen. 0 is up (ahead of the camera), math.pi / 2 is right, math.pi is down and 3 * math.pi / 2 is left.
  • angle is the width of the cone in radians. The cone is never narrower than 22.5°.
TargetDirectionEnemy(math.pi / 2)  -- the next enemy to the right of the target

The search starts at your target when your target is on the screen, else at you. It picks the enemy closest on the screen inside the cone. When no enemy is inside the cone, it picks the one closest to the direction. The enemy must be on the screen, within targetNearestDistance yards of you (default 41), and not a critter.

After a call selects a target, later calls do nothing until you call TargetDirectionFinished(). The lock also ends when the selected unit leaves your view or you can no longer select it.

-- One step per key press:
TargetDirectionEnemy(math.pi / 2)
TargetDirectionFinished()

TargetDirectionFriend(facing [, angle])

Same as TargetDirectionEnemy, but for friendly units that you can assist.

TargetDirectionFinished()

Ends the lock from TargetDirectionEnemy or TargetDirectionFriend, so the next call can select a target again.

TargetNearest([reverse])

Cycles through the nearest units, of any kind. It works like TargetNearestEnemy:

  • Units in front of you come first, then units nearer to you.
  • Units in front count up to targetNearestDistance yards (default 41). Units behind you count up to targetNearestDistanceRadius yards (default 10).
  • Critters and units you cannot see are skipped.
  • Each call selects the next unit. reverse (a true value) goes the other way.
  • After 3 seconds without a call, the list is made again and the nearest unit is selected first.

TargetNearest, TargetNearestEnemyPlayer, TargetNearestFriendPlayer, TargetNearestEnemy and TargetNearestFriend use one list together. A call to a different function makes the list again.

TargetNearestEnemyPlayer([reverse])

Like TargetNearest, but only enemy players.

TargetNearestFriendPlayer([reverse])

Like TargetNearest, but only friendly players that you can assist.

InteractUnit(unit [, exactMatch])

Targets a unit or object and interacts with it. The result is the same as a right-click on it in the world. For example, it opens a vendor, loots a corpse, or opens a mailbox.

  • unit is a unit token, "mouseover" or a name.
  • "mouseover" is the unit or object under the cursor. This includes game objects and corpses, not only units.
  • A unit token that points at nothing finds nothing. The function does not search for that text as a name.
  • For any other text, the function finds the nearest unit or object with that name. A name that starts with the text is a match. If exactMatch is true, only the full name is a match.

Returns 1 if the interaction ran, or nil if nothing matched. The interaction can still fail, for example if you are too far away.

InteractUnit("target")
InteractUnit("mouseover")

Bindings (INTERACTTARGET / INTERACTMOUSEOVER)

The Key Bindings window shows Interact With Target and Interact With Mouseover in the Targeting group.

  • Interact With Target interacts with "anyinteract". This is the Interact Key. See Soft interact.
  • Interact With Mouseover interacts with the unit or object under the cursor. If there is nothing under the cursor, it interacts with "anyinteract".

Interact With Target does not attack a hostile target. Use Attack Target for that.

C_PlayerInteractionManager.InteractUnit(unit [, exactMatch [, looseTargeting]])

Does the same as InteractUnit, but returns a boolean.

  • unit and exactMatch are the same as for InteractUnit. Use "anyinteract" for the object that the Interact Key uses.
  • looseTargeting has no effect.

Returns true if the interaction ran, or false if nothing matched.

if not C_PlayerInteractionManager.InteractUnit("mouseover", false, false) then
    C_PlayerInteractionManager.InteractUnit("anyinteract")
end

Soft interact (softinteract / anyinteract)

Soft interact picks the nearest object in front of you that you can interact with. Bind Interact With Target to a key, then press it to interact with that object. This is the Interact Key.

Soft interact is on by default. It does nothing until you bind the key. To turn it off:

/console SoftTargetInteract 0

What it picks. It picks only an object that shows an interact cursor when you put the mouse over it. For example: an NPC with a service, a corpse that you can loot or skin, a mailbox, a chest, or an herb or mining node. It does not pick an enemy that you can only attack. The object must be on the screen, and inside the arc and the range of the settings below. If more than one object qualifies, it picks the nearest one.

Unit tokens.

  • "softinteract" is the object that soft interact picked. It is empty when nothing qualifies or when soft interact is off.
  • "anyinteract" is your target when you can interact with it. This is true also when the target is behind you or too far away. If you cannot interact with your target, "anyinteract" is the same as "softinteract".
  • "anyleftinteract" is the same as "anyinteract".

Both tokens can point at a game object or a corpse. UnitGUID returns the GUID of that object. Functions that read unit data, such as UnitName, return nothing for a game object or a corpse.

Settings.

cvar default values
SoftTargetInteract 3 0 off, 1 gamepad, 2 keyboard and mouse, 3 always. Only 2 and 3 turn soft interact on.
SoftTargetInteractArc 0 0: the object must be directly in front of the camera, within about 1.2 yards of a line from you in the direction of the camera. 1: the object must be within 30 degrees of the direction of the camera, on each side. 2: the object can be in any direction. The arc follows the camera, not the direction that your character faces.
SoftTargetInteractRange 10 The maximum distance in yards. The range cannot be more than the targetNearestDistance cvar.
SoftTargetInteractRangeIsHard 0 0: an object that you can interact with now is picked also when it is past the range. 1: the range always applies.

The icon. When SoftTargetIconInteract is 1, an icon shows over the picked object. For a game object, such as a mailbox, a chest or a node, SoftTargetIconGameObject must also be 1. The icon is the cursor image that the mouse shows over the object (see SetUnitCursorTexture). For a unit, the icon is above its name. For a game object or a corpse, the icon is above the top of the object. When you are too far away, the icon is the grey "Unable" form. The icon is hidden when the object is off the screen.

cvar default values
SoftTargetIconInteract 0 1 shows the icon, 0 hides it.
SoftTargetIconGameObject 0 1 shows the icon over a game object, 0 hides it. The icon also needs SoftTargetIconInteract.
SoftTargetNameplateSize 19 The size of the icon, and of the soft enemy and soft friend icons.

When the picked object changes, PLAYER_SOFT_INTERACT_CHANGED fires.

/dump UnitGUID("softinteract"), UnitGUID("anyinteract")

Soft enemy and soft friend (softenemy / softfriend / anyenemy / anyfriend)

Soft enemy picks the nearest enemy near you, and soft friend picks the nearest friendly unit. They do not change your target. An addon or a macro uses the picked unit through its token.

Both are off by default. To turn them on:

/console SoftTargetEnemy 3
/console SoftTargetFriend 3

What they pick. Soft enemy picks a unit that Tab targeting can select as an enemy: a living unit that you can attack. Soft friend picks a living unit that you can assist, for example a party member. Neither picks you. The unit must be on the screen, and inside the arc and the range of the settings below. If more than one unit qualifies, it picks the nearest one.

Locked and loose targets. A target that you choose (with a click, Tab, /target or TargetUnit) is a locked target. A target that soft targeting sets is a loose target. A click on a loose target makes it locked. IsTargetLoose() tells the two apart.

Your target. When SoftTargetMatchLocked is 1, a locked target is the soft unit when it qualifies. For example, when you target an enemy inside the range, "softenemy" is that enemy, also when another enemy is nearer. When SoftTargetWithLocked is 0, there is no soft enemy and no soft friend while you have a locked target. A loose target counts for neither setting.

Attacks. When you have no target, Attack Target, /startattack, AttackTarget() and StartAttack() attack "softenemy". Your target stays empty.

Spells. When you have no target, a harmful spell goes to "softenemy" and a helpful spell goes to "softfriend". A spell that only you can receive still goes to you. If the soft unit is out of the spell's range, the cast fails with "Out of range". When a harmful spell goes off, its unit becomes your loose target.

Loose targets follow. When SoftTargetForce is 1 and your target is loose, your target changes to each new "softenemy". When there is no soft enemy, the loose target is cleared. When SoftTargetForce is 2, the same happens with "softfriend". Soft targeting never changes a locked target, and it never selects a target when you have none.

Unit tokens.

  • "softenemy" is the unit that soft enemy picked. It is empty when nothing qualifies or when soft enemy is off.
  • "softfriend" is the unit that soft friend picked, in the same way.
  • "anyenemy" is the first of these that exists:
    1. your target, when it qualifies as a soft enemy
    2. "softenemy"
    3. your target
    4. "softfriend"
  • "anyfriend" is the same as "anyenemy", with enemy and friend changed around.

A target suffix also works, for example "softenemytarget".

Unit events. Every unit event fires with arg1 == "softenemy" for the soft enemy, the same as it does for "target". Every unit event fires with arg1 == "softfriend" for the soft friend in the same way. This includes UNIT_HEALTH, UNIT_AURA, UNIT_COMBAT, and UNIT_SPELLCAST_START. A unit that is your target and your soft enemy fires each event two times: one time with "target" and one time with "softenemy". "anyenemy" and "anyfriend" fire no unit events.

Settings.

cvar default values
SoftTargetEnemy 1 0 off, 1 gamepad, 2 keyboard and mouse, 3 always. Only 2 and 3 turn soft enemy on.
SoftTargetFriend 0 The same values, for soft friend.
SoftTargetEnemyArc / SoftTargetFriendArc 0 The same values as SoftTargetInteractArc.
SoftTargetEnemyRange / SoftTargetFriendRange 45 The maximum distance in yards. The range cannot be more than the targetNearestDistance cvar.
SoftTargetMatchLocked 1 1: your target is the soft unit when it qualifies. 0: the soft unit is always the nearest unit that qualifies.
SoftTargetWithLocked 1 0: no soft enemy and no soft friend while you have a locked target. 1 and 2: they are picked also while you have a locked target.
SoftTargetForce 1 1: a loose target follows "softenemy". 2: a loose target follows "softfriend". 0: a loose target stays.

The circles. A selection circle shows under "softenemy" and under "softfriend". It has the same color as the circle under your target, but it is smaller and dimmer. When the soft unit is also your target, the unit shows only the target circle. ObjectSelectionCircle 0 hides the soft circles with the target circle.

The names. The name of "softenemy" and of "softfriend" shows above the unit, the same as the name of your target. When the UnitName cvars hide the names of other units, the soft unit names also show. When a soft unit has a nameplate, the nameplate shows and the name does not, the same as for your target.

The icons. When SoftTargetIconEnemy is 1, an icon shows above the name of "softenemy". When SoftTargetIconFriend is 1, an icon shows above the name of "softfriend". The icon is the cursor image that the mouse shows over the unit, the same as the soft interact icon. A unit gets one icon, also when it is more than one soft unit. When the mouse shows only the normal pointer over the unit, for example over a party member, the icon is low priority. A low-priority icon shows only when SoftTargetLowPriorityIcons is 1. SoftTargetNameplateSize sets the size of the icons.

cvar default values
SoftTargetIconEnemy 0 1 shows the icon over "softenemy", 0 hides it.
SoftTargetIconFriend 0 1 shows the icon over "softfriend", 0 hides it.
SoftTargetLowPriorityIcons 0 1 shows the normal pointer as the icon, 0 shows no icon for it. This applies to all soft-target icons.

When a picked unit changes, PLAYER_SOFT_ENEMY_CHANGED or PLAYER_SOFT_FRIEND_CHANGED fires.

/dump UnitName("softenemy"), UnitName("anyenemy"), UnitName("softfriend")

IsTargetLoose()

Returns true when your target is loose, and false when it is locked. See Soft enemy and soft friend.

  • A spell that goes off on "softenemy" makes a loose target.
  • A target that you choose is locked.
  • After /cleartarget, the result is true while SoftTargetForce is 1 and soft enemy is on, or 2 and soft friend is on. Otherwise it is false.
if UnitExists("target") and IsTargetLoose() then
    print("soft targeting picked", UnitName("target"))
end

TaxiMap

Backports of the taxi-map node enumerators, reading TaxiNodes.dbc (static) and the open flight master's live session. Both also register the Enum.FlightPathFaction (Neutral=0, Horde=1, Alliance=2) and Enum.FlightPathState (Current=0, Reachable=1, Unreachable=2) tables, so code comparing against those enums ports unchanged.

Ship both because they answer different questions:

function source use
C_TaxiMap.GetTaxiNodesForMap([mapID]) TaxiNodes.dbc (static) every flight point (one continent, or all with no arg), faction-tagged, discovered or not — for a database / map overlay
C_TaxiMap.GetAllTaxiNodes([uiMapID]) live taxi session the nodes reachable from the open flight master, with per-node state + the slot index to fly — for a flight-map UI

C_TaxiMap.GetTaxiNodesForMap([mapID])

Returns an array of flight masters. A numeric mapID (a Map.dbc id — 0 Eastern Kingdoms, 1 Kalimdor, 30 Alterac Valley, … — the same identity C_Map.GetAreaTriggers([mapID]) uses) filters to that continent. Omitted / non-number returns every flight master on every continent — a one-call flight database (each entry carries its own mapID). This is a ClassicAPI extension to the required-argument signature.

Only real flight masters are returned. TaxiNodes.dbc also contains non-flight rows — transports/boats/zeppelins (no flight mount) and non-selectable markers like Northshire Abbey or standalone instance entries (no connecting flight path) — which are filtered out: a node is included only if it has a flight mount and is a TaxiPath.dbc endpoint (some flight path connects to it).

Each entry:

field meaning
nodeID TaxiNodes.dbc row id
name localized node name ("Stormwind, Elwynn Forest")
faction Enum.FlightPathFaction — Alliance-mount-only → Alliance, Horde-only → Horde, both → Neutral
reachable (ext) a flight path leads to this node (in-degree > 0). false = departure-only, reachable only by other means (druid Teleport → "Nighthaven, Moonglade"; a boat dock → "Stormwind Harbor")
position {x, y} 0–1 on the continent map
mapID (ext) continent Map.dbc id
x / y / z (ext) raw continent world coords
areaID (ext) the AreaTable zone, from the node's own name — its location if that's a mapped city ("Orgrimmar" → Orgrimmar city), else the zone suffix ("…, Western Plaguelands" → WPL). Authoritative; absent if unresolved
mapX / mapY (ext) 0–100 position within that zone (mapX horizontal, mapY vertical)

nodeID / name / faction / position are the standard fields; the rest are ClassicAPI extensions (marked (ext)). position is continent 0–1, while areaID + mapX/mapY let a zone-map addon pin directly. areaID comes from the node's name, matched against AreaTable — the location part when it's a mapped city ("Orgrimmar, Durotar" → Orgrimmar city, so a capital's master pins on the city map), otherwise the zone suffix ("Chillwind Camp, Western Plaguelands" → WPL). Each candidate is accepted only if its map rect contains the node, so this is right at borders a purely geometric guess gets wrong (Chillwind → WPL, not the Alterac box it sits in). Nodes whose name resolves to no containing zone fall back to the geometric landmass resolver. isUndiscovered is not wired (the client's known-node bitfield isn't read) — every node is returned regardless of discovery.

for _, n in ipairs(C_TaxiMap.GetTaxiNodesForMap(0)) do
    if n.faction == Enum.FlightPathFaction.Alliance and n.areaID then
        Pin(n.areaID, n.mapX, n.mapY, n.name)   -- zone-map pin
    end
end

Replaces hand-scraped flight tables (pfQuest's pfDB.meta.flight): draw a node per entry (filtered by faction, using areaID+mapX/mapY) exactly like SearchAreaTriggerID. Auto-covers custom-server flight points — no NPC-id curation.

C_TaxiMap.GetAllTaxiNodes([uiMapID])

Returns the live flight-master destination list — the nodes reachable from the currently open taxi map, with reachability state and the slot index needed to take the flight. Only meaningful while the taxi map is open (between TAXIMAP_OPENED and TAXIMAP_CLOSED); returns an empty table otherwise. uiMapID is accepted for signature parity but ignored (there is a single active taxi map).

Each entry:

field meaning
slotIndex the legacy 1..NumTaxiNodes() index — pass to TakeTaxiNode(slotIndex) to fly
name node name
position {x, y} 0–1 on the flight-map image
state Enum.FlightPathState — CURRENT→Current, REACHABLE→Reachable, DISTANT/none→Unreachable
nodeID TaxiNodes.dbc id (resolved from the slot's node record; 0 if absent)

No faction / isUndiscovered here — GetAllTaxiNodes doesn't carry them (reachability implies faction); that data lives on GetTaxiNodesForMap. The textureKit / useSpecialIcon cosmetic fields have no equivalent here and are omitted.

-- Fly to the first reachable destination:
for _, n in ipairs(C_TaxiMap.GetAllTaxiNodes()) do
    if n.state == Enum.FlightPathState.Reachable then
        TakeTaxiNode(n.slotIndex)
        break
    end
end

This is the interactive counterpart to GetTaxiNodesForMap: session-gated and player-relative (its whole point is the open master's reachable set), which is why a static database like pfQuest uses GetTaxiNodesForMap instead.

C_TaxiMap.GetTaxiPaths()

Returns every flight path (directed edge) in TaxiPath.dbc as an array of tables — the taxi routing graph. No flight master needed; reads the DBC directly.

local paths = C_TaxiMap.GetTaxiPaths()
-- paths[i] = { pathID = 94, fromNodeID = 8, toNodeID = 16, cost = 110 }
Field Type Meaning
pathID number TaxiPath.dbc id — pass to GetTaxiPathWaypoints
fromNodeID number source TaxiNodes.dbc id
toNodeID number destination TaxiNodes.dbc id
cost number flight cost in copper (the routing weight)

Directed — A→B and B→A are two separate entries with their own path IDs and geometry. Combine with GetTaxiNodesForMap (the node table) to build a weighted graph, and with GetTaxiPathWaypoints to measure real distances.

C_TaxiMap.GetTaxiPathWaypoints(pathID)

Returns the waypoint polyline of one flight path as an array of world coordinates, ordered by node index (flight order).

local wp = C_TaxiMap.GetTaxiPathWaypoints(94)
-- wp[i] = { x = -8851.66, y = 498.55, z = 111.26, mapID = 0 }
Field Type Meaning
x, y, z number world position (yards)
mapID number continent (Map.dbc id)

Returns nothing for an unknown pathID or an empty path. Sum the 3D distances between consecutive waypoints for the path's length; taxi flight speed is 32 yd/s, so length / 32 ≈ flight seconds for a single hop. (Multi-hop trips are shorter than the sum of their segments — the server cuts the corner at each intermediate flight master via data not present in any client file, so a client-side estimate of a chained route is an upper bound.)

C_TaxiMap.GetTaxiRoute(slotIndex)

Returns the node chain the client would actually fly to a taxi-map slot, as an array of TaxiNodes.dbc IDs in flight order ({ currentNodeID, …, destNodeID }). slotIndex is the legacy 1..NumTaxiNodes() index (the same one TaxiNodeName/TakeTaxiNode take). Only meaningful while the taxi map is open.

local route = C_TaxiMap.GetTaxiRoute(slotIndex)
-- route = { 8, 16, 43, 66 }  (Thelsamar -> Refuge Pointe -> Aerie Peak -> Chillwind)

This is the exact chain TakeTaxiNode(slotIndex) would send to the server — mirrors the client's own routing decision: a single direct hop when a TaxiPath edge exists ({ current, dest }), otherwise the client's precomputed multi-hop route (which, per character, may detour around flight points you haven't discovered). Returns nothing when the map is closed, the slot is out of range, it's the current node, or no route exists.

Pairs with GetTaxiPaths (each consecutive (from, to) names a pathID) and GetTaxiPathWaypoints (that path's geometry) to measure a chained trip without matching flight-map pixel coordinates back to nodes.

Texture

An atlas is a name for one piece of art. The name points to a texture file and to a rectangle inside that file. texture:SetAtlas(name) applies both in one step, so you never write the coordinates at the call site.

ClassicAPI ships a small set of built-in atlas names. Call C_Texture.GetAtlasElements() to list them. To use your own art, describe it once with C_Texture.RegisterAtlas and then set it by name like any other atlas.

Atlas names are not case sensitive.

The eight raid target markers are published as atlases, named UI-RaidTargetingIcon_1 through UI-RaidTargetingIcon_8. All eight markers share one sheet here, so an atlas is the only way to name a single marker.

markerTexture:SetAtlas("UI-RaidTargetingIcon_8", true)   -- the skull

C_Texture.GetAtlasInfo(atlasName)

Returns a table that describes the atlas, or nil when the name is not known.

Table fields:

Field Type Notes
elementName string The name in its canonical spelling.
width number Width of the art in pixels.
height number Height of the art in pixels.
leftTexCoord number Left edge of the rectangle, from 0 to 1.
rightTexCoord number Right edge of the rectangle, from 0 to 1.
topTexCoord number Top edge of the rectangle, from 0 to 1.
bottomTexCoord number Bottom edge of the rectangle, from 0 to 1.
tilesHorizontally boolean True when the art repeats across its width.
tilesVertically boolean True when the art repeats down its height.
filename string Path of the texture that holds the art.

Textures are named by path, so filename carries the texture and the file field is not set.

local info = C_Texture.GetAtlasInfo("Repair")
print(info.filename, info.width, info.height)

C_Texture.GetAtlasExists(atlasName)

Returns true when the name is known and false when it is not. This agrees with GetAtlasInfo at all times. A name that exists always returns a table.

C_Texture.GetAtlasID(atlasName)

Returns the id of the sheet that holds the atlas, or nil when the name is not known. A built-in atlas has a positive id. An atlas you add with C_Texture.RegisterAtlas gets a negative id, so the two kinds never collide.

C_Texture.GetAtlasElementID(atlasName)

Returns the id of this one piece of art, or nil when the name is not known. The same positive and negative rule applies.

C_Texture.GetAtlasElements()

Returns an array of every atlas name that is currently known, in alphabetical order. The list includes the names you registered yourself.

for _, name in ipairs(C_Texture.GetAtlasElements()) do
    print(name)
end

C_Texture.RegisterAtlas(name, file, width, height, left, right, top, bottom [, tilesHorizontally, tilesVertically])

A ClassicAPI extension. Adds an atlas name that points at your own art, so SetAtlas and atlas markup work with a sprite sheet you ship yourself.

  • name — the atlas name callers will use.
  • file — texture path, without the file extension.
  • width, height — size of the art in pixels, used by useAtlasSize.
  • left, right, top, bottom — the rectangle inside the file, from 0 to 1.

Registering a name a second time replaces its definition and keeps its ids, so a caller that stored an id keeps a valid one. Returns true.

-- One 32x32 cell from the top-left of a 128x128 sheet.
C_Texture.RegisterAtlas("myaddon-gem", "Interface\\AddOns\\MyAddon\\sheet",
                        32, 32, 0, 0.25, 0, 0.25)

myTexture:SetAtlas("myaddon-gem", true)

Atlas markup

|A:atlasName:height:width|a draws an atlas inside any text, next to the |T…|t form that draws a texture path. Use it in chat messages, tooltip lines, and font strings.

  • A height of 0 uses the atlas's own pixel height.
  • A width of 0 keeps the atlas's own shape at the height you asked for.
  • Sizes are relative to a 14-point font, as in |T…|t markup: the art grows and shrinks with the text. Below a UI scale of 1, the art is drawn smaller than the text by the UI scale.
  • Two more fields shift the art from the text, as |A:name:h:w:offsetX:offsetY|a. The values are screen pixels. A positive offsetY moves the art up.
print("Repair cost: |A:Repair:0:0|a")
print("Small marker |A:MinimapArrow:12:12|a here")

An unknown name draws nothing and leaves the rest of the line intact. Art that repeats is drawn once, so tilesHorizontally and tilesVertically have no effect in text.

Text typed by other players never draws atlas art. This matches how a typed texture path behaves.

Time

GetServerTime()

Returns the current server clock as a Unix epoch timestamp (seconds since 1970-01-01 UTC), or nil before login (while the engine's game-time struct is still BSS-zero).

local now = GetServerTime()
-- now = 1778260148 (Fri 2026-05-08 17:09:08 UTC)

A client can replace this global. Some clients declare their own GetServerTime function in FrameXML, which replaces the global with a different return value. Turtle WoW is one, and its version returns the server hour and the server minute. Call ClassicAPI.GetServerTime() to reach the timestamp on such a client.

The value is an instant, with second resolution. It is independent of the realm's timezone and of any per-zone time shift the realm applies, so it stays continuous when the player changes continents. That makes it the right call for log timestamps, sorting, and anything that counts down. Stock GetTime() measures the session instead, and cannot be compared against it.

To print the realm's clock digits rather than the instant, use C_DateAndTime.GetServerTimeLocal().

Right after login. The realm reports the instant on request, and the answer takes a moment to arrive. Until it does, this function reports the realm's wall clock instead, which is wrong by the realm's timezone offset. The value corrects itself once the answer lands, so a caller that starts at login can see it step once.

GetTimeCached()

Returns the same value as GetTime() — seconds on the engine's uptime clock — but frame-stable: every call within a single frame returns the identical value, refreshed once per frame. Same epoch as GetTime(), so the two are directly comparable.

Provides a frame-stable GetTime() (sampled once per frame in the main loop) under a distinct name, leaving the stock GetTime() untouched — GetTime() is live, recomputing the OS tick on every call, so two same-frame GetTime() calls can differ.

-- Frame-stable: consistent timestamps across a frame's work
local now = GetTimeCached()
if now - lastFire >= interval then
    lastFire = now
    ...
end

Not a performance feature. The underlying tick source is cheap (GetTickCount, a user-mode shared-page read, or rdtsc), and both functions pay the same Lua→C call overhead, which dominates. So GetTimeCached() is not meaningfully faster than GetTime() — its only advantage is the frame-stable semantics. To actually cut cost in a hot OnUpdate, cache the value in a Lua local (local now = GetTime()) and reuse it; that removes the per-call Lua dispatch, which a cached C function cannot.

The cache is refreshed from the shared once-per-frame Tick::WorldTick hook; before the first world tick (pre-login / glue) it falls back to a live sample, so it never returns 0.

GetTickTime()

Returns the time in seconds that the current frame advanced. The value is the same elapsed value that the OnUpdate scripts of this frame receive. The value changes one time in each frame, before the first OnUpdate script runs.

local elapsed = GetTickTime()

Code that runs before OnUpdate. Events and C_Timer callbacks run earlier in the frame than OnUpdate scripts. For these callers, the function returns the value of the previous frame.

C_Timer.After(seconds, callback)

Schedules callback to fire once after seconds. Returns nothing. C_Timer.After as an engine binding — a Script_* C function, not Lua.

C_Timer.After(0.5, function() print("half a second later") end)
C_Timer.After(0, function() print("next frame") end)

Errors in the callback are caught (via lua_pcall) and silently swallowed. One broken timer doesn't poison other timers on the same tick.

C_Timer.NewTimer(seconds, callback)

Like After, but returns a cancel-handle table:

local handle = C_Timer.NewTimer(10, function() print("never if I cancel") end)
handle:Cancel()           -- prevents the callback from firing
print(handle:IsCancelled()) -- true

Cancel() is idempotent; calling it on a timer that already fired or was previously cancelled is a no-op. IsCancelled() returns true after Cancel(), after natural expiry, or for an unknown timer ID. Both methods take no arguments other than self.

C_Timer.NewTicker(seconds, callback, [iterations])

Recurring variant. callback fires every seconds until cancelled or until iterations fires have happened. iterations is optional; omitted or non-numeric means forever.

local h = C_Timer.NewTicker(1, function() print("each second") end)
-- ...later
h:Cancel()

-- bounded:
C_Timer.NewTicker(0.25, function() ... end, 8)  -- fires 8 times, every 250ms

Minimum period is clamped to 0.0001 to prevent tight loops. Returns the same kind of cancel handle as NewTimer. The handle auto-cancels itself when the iteration count exhausts.

Implementation notes (apply to all three functions):

  • One unified timer queue walked once per frame from FUN_WORLD_TICK's subscription. Same hook the BAG_UPDATE_DELAYED flusher uses, so there's no per-feature hook overhead.
  • Resolution is one frame (~16ms at 60fps). A timer scheduled for 0.001 seconds will fire on the next tick, same as one scheduled for 0.
  • The timer queue uses a monotonic clock (QueryPerformanceCounter), so timers don't drift if the system clock changes mid-session.
  • Callbacks are pinned in the Lua registry under per-timer string keys so they survive GC between scheduling and firing.

C_DateAndTime overview

Backport of the C_DateAndTime namespace — calendar-style date math built on top of the server clock. The calendar functions exchange CalendarTime tables with these fields (matching Blizzard's TimeDocumentation.lua):

Field Range Notes
year full year, e.g. 2026
month 1..12 luaIndex (1-based)
monthDay 1..31 luaIndex (1-based)
weekday 1..7 luaIndex; 1 = Sunday
hour 0..23
minute 0..59

Two clocks. A realm sends the client both an instant and a wall clock, and they differ by the realm's timezone. GetServerTime and GetSecondsUntilDailyReset report the instant, so they are the ones to compare, sort, store, and count down with. GetCurrentCalendarTime and GetServerTimeLocal report the wall clock, for display, and always agree with GetGameTime().

Daily reset semantics. GetSecondsUntilDailyReset counts down to UTC midnight, which is where a realm's own day boundary sits. A realm can shift that boundary by a configured amount, and it never tells the client, so a realm that shifts it resets at a different moment.

Weekly reset not implemented. There is no server-broadcast weekly reset schedule, and Turtle WoW realm schedules vary, so C_DateAndTime.GetSecondsUntilWeeklyReset would have to hardcode a weekday/hour that's wrong on some realms. Addons that need it can compute their own value from the daily reset + a CVAR.

C_DateAndTime.GetCurrentCalendarTime()

Returns the realm's date and time as a CalendarTime table, or nothing before login. The hour and minute fields always agree with GetGameTime(). This is realm time, not the instant, so it does not match GetCalendarTimeFromEpoch(GetServerTime()) on a realm that is not on UTC.

local t = C_DateAndTime.GetCurrentCalendarTime()
-- t = { year=2026, month=5, monthDay=11, weekday=2, hour=14, minute=30 }

C_DateAndTime.GetCalendarTimeFromEpoch(epoch)

Converts a Unix epoch (seconds since 1970-01-01 UTC) to a CalendarTime table. Pure gmtime; no engine state touched.

C_DateAndTime.AdjustTimeByDays(date, days) / AdjustTimeByMinutes(date, minutes)

Returns a new CalendarTime offset from the input by the given number of days (or minutes). Negative deltas walk backwards. Out-of-range inputs (month=13, monthDay=32) normalize via the underlying _mkgmtime / gmtime round-trip.

local today = C_DateAndTime.GetCurrentCalendarTime()
local tomorrow = C_DateAndTime.AdjustTimeByDays(today, 1)
local yesterday = C_DateAndTime.AdjustTimeByDays(today, -1)

C_DateAndTime.CompareCalendarTime(lhs, rhs)

Returns -1 if lhs < rhs, 0 if equal, 1 if lhs > rhs. Compares by epoch conversion so denormalized inputs sort consistently.

C_DateAndTime.GetServerTimeLocal()

Returns GetServerTime() offset by the server's timezone. The result is a display value, not an instant: formatting it shows the realm's clock digits. If the realm reports 14:30, this returns the epoch for 14:30 UTC, so date("!%H:%M", t) prints 14:30. Do not compare it against a real timestamp.

C_DateAndTime.GetSecondsUntilDailyReset()

Returns seconds until the next server midnight (00:00:00 in server wall-clock time). Returns 0 if called before login.

See the overview's "Daily reset semantics" note — this uses server midnight, not UTC midnight (though they happen to coincide for Turtle WoW since the server reports UTC-aligned components).

Totem

GetTotemInfo(slot)

Returns haveTotem, totemName, startTime, duration, icon, modRate, spellID for one of the four shaman totem slots (1 Fire, 2 Earth, 3 Water, 4 Air).

local have, name, start, duration, icon = GetTotemInfo(1)   -- Fire slot
if start > 0 then                                          -- a totem is active
    local remaining = duration - (GetTime() - start)
end

When a totem is active in the slot:

Return Meaning
haveTotem true.
totemName The localized name of the totem creature. This is nil until the client sees the creature. PLAYER_TOTEM_UPDATE fires when the name arrives.
startTime The GetTime() value when you dropped the totem, in whole seconds.
duration The full duration of the totem, in whole seconds.
icon The icon texture path of the summon spell.
modRate Always 1. Totems have no haste.
spellID The ID of the summon spell.

When no totem is active in the slot:

Return Meaning
haveTotem true if you carry the totem tool of the slot (Fire Totem 5176, Earth Totem 5175, Water Totem 5177, Air Totem 5178).
totemName ""
startTime 0
duration 0
icon ""
modRate 1
spellID 0

To find out if a totem is active, test startTime > 0. Do not use haveTotem for this test, because it is also true when you only carry the tool.

startTime and duration are whole seconds. Thus, startTime + duration can be up to one second different from the real end time. For an exact countdown, use GetTotemTimeLeft.

A slot that is not a number causes the error Usage: GetTotemInfo(slot). A number that is not 1 to 4 causes the error Totem slot must be in range of 1 to 4.

All totems are found from their spell data, so custom totems also work. The slot becomes empty when the totem creature goes away (it dies, expires, is recalled, or moves out of view). The slot also becomes empty when you leave the world, but not on a UI reload. Slot changes fire PLAYER_TOTEM_UPDATE.

GetTotemTimeLeft(slot)

Returns the time, in whole seconds, until the totem in the slot expires. Returns 0 when the slot has no active totem. Slots: 1 Fire, 2 Earth, 3 Water, 4 Air.

local remaining = GetTotemTimeLeft(1)   -- Fire slot, e.g. 12

The value counts down from the full duration of the totem. It is 0 after the duration ends, until the totem goes away. The slot errors are the same as for GetTotemInfo.

GetTotemDuration(slot)

Returns a duration object for the active totem in the slot. Its time span starts when the totem appears and lasts the full duration of the totem. When the slot has no active totem, the duration object holds a zero time span. Each call returns a new duration object.

local duration = GetTotemDuration(1)
print(duration:GetTotalDuration())      -- e.g. 60
bar:SetTimerDuration(duration, Enum.StatusBarInterpolation.Immediate,
    Enum.StatusBarTimerDirection.RemainingTime)
  • The times are exact. GetTotemInfo gives the start time and the duration in whole seconds.

The slot errors are the same as for GetTotemInfo.

TargetTotem(slot)

Targets your totem in the slot (1 Fire, 2 Earth, 3 Water, 4 Air).

TargetTotem(2)   -- target your Earth totem

If the slot has no active totem, the function does nothing. It also does nothing until the client sees the totem creature. The slot errors are the same as for GetTotemInfo.

Tracking

The engine stores only the tracking spell that is active now. It does not keep a list of the tracking spells you know. These globals add that list. A minimap addon can then build a tracking menu without a hardcoded spell table.

A tracking spell is a spell in your spellbook that finds creatures, resources, or hidden units. This is the same test the engine uses to find the active tracker. The list covers Find Herbs, Find Minerals, Find Treasure, the Hunter Track spells, Sense Undead, Sense Demons, and Track Humanoids. It also covers server-added trackers, such as Turtle's Find Trees. You do not have to maintain a spell list.

GetNumTrackingTypes()

Returns the number of tracking spells in your spellbook.

GetNumTrackingTypes()   -- for example, 8 for a Hunter

GetTrackingInfo(index)

index starts at 1 and follows spellbook order. The function returns these five values, or nil if index is out of range:

# Value Type Notes
1 name string Localized spell name.
2 texture string Icon path. Give it to texture:SetTexture(...). The active tracker shows its "active" icon if the spell has one. nil if the icon is missing.
3 active number 1 when this tracker is the one now in effect, otherwise nil.
4 category string Always "spell". There are no non-spell trackers here.
5 spellID number The tracking spell's ID. This is a ClassicAPI extra.
local name, texture, active, category, spellID = GetTrackingInfo(1)

SetTracking([index])

Turns on the tracking spell at index. You select a tracker when you cast its spell. This function casts it. For an out-of-range index, it does nothing.

If you do not give a number, the function turns off the active tracker. This is the same as CancelTrackingBuff(). When the tracker turns off, PLAYER_AURAS_CHANGED fires.

for i = 1, GetNumTrackingTypes() do
    local name = GetTrackingInfo(i)
    if name == "Find Herbs" then
        SetTracking(i)
        break
    end
end

TradeSkillUI

A profession link (|Htrade:...|h[Blacksmithing]|h) shows the profession of a player and the recipes that the player knows. The link also carries the skill rank of the player.

When you click a received link in chat, the profession window opens with that profession. The window lists the recipes of the linked player. The difficulty colors use the rank of the linked player. The reagent counts come from your bags.

While the window shows a profession from a link, the window does not show its Create, Create All, and count controls. GetTradeSkillLine gives the rank of the linked player. When you open a profession from your spellbook, the window shows your own profession again.

Enchanting opens in the craft window. The craft window has two functions of its own: GetCraftListLink and IsCraftLinked. While the craft window shows a link, GetCraftName and GetCraftDisplaySkillLine give the linked profession and rank.

You can link every profession that opens from the spellbook. This includes Smelting, Poisons, and Beast Training.

The client must know the player who sent the link. If the client does not know the player, it asks the server for the name. The link opens when the answer arrives.

A link holds one bit for each recipe of the profession, in the order of the DBC files. If two clients have different DBC files, the link does not open, or it shows the wrong recipes.

Returns the link for your profession in the trade-skill window. The link has your skill rank and the recipes that you know. When the window is closed, the function returns nil.

local link = GetTradeSkillListLink()
-- "|cffffd000|Htrade:9785:300:300:2A6F1:Ug/E...|h[Blacksmithing]|h|r"
ChatFrameEditBox:Insert(link)

The trade-skill window has a button next to its title. The button puts this link in the chat box.

The link has this format:

|cffffd000|Htrade:<spellID>:<rank>:<maxRank>:<GUID>:<bits>|h[<profession>]|h|r
  • spellID: the profession spell that opened the window.
  • rank, maxRank: your skill rank and maximum rank, as GetTradeSkillLine gives them.
  • GUID: the GUID of your character, in hexadecimal.
  • bits: the recipes that you know, six recipes for each base64 character.

Returns the link for your profession in the craft window, for example Enchanting. When the craft window is closed, the function returns nil. This function works like GetTradeSkillListLink. This is a ClassicAPI extension.

local link = GetCraftListLink()   -- with Enchanting open
-- "|cffffd000|Htrade:13920:300:300:2A6F1:...|h[Enchanting]|h|r"

GetTradeSkillRecipeLink(index)

Returns the link for the recipe in row index of the trade-skill window. The link is an enchant: link to the recipe spell, so it carries the recipe's spell ID. GetTradeSkillItemLink links only the item that a recipe makes. The link text is <profession>: <recipe>. If the recipe has no profession for the player's race and class, the text is the recipe name only. A header row, or an index outside the list, returns nothing.

Click the link in chat, or give it to GameTooltip:SetHyperlink, to see the recipe's spell tooltip.

-- with Tailoring open, row 2 = Bolt of Linen Cloth
GetTradeSkillRecipeLink(2)   -- "|cffffd000|Henchant:2963|h[Tailoring: Bolt of Linen Cloth]|h|r"
local _, _, spellID = string.find(GetTradeSkillRecipeLink(2), "|Henchant:(%d+)")

The Craft window (Enchanting, pet training) already has this in GetCraftItemLink.

GetTradeSkillDescription(index)

Returns the description of the recipe in row index of the trade-skill window. The function fills in the values in the text, such as amounts and durations, as the spell tooltip does. Most recipes that make an item have no description. For these recipes, for a header row, and for an index outside the list, the function returns nil.

local description = GetTradeSkillDescription(GetTradeSkillSelectionIndex())
if description then
    print(description)
end

The Craft window (Enchanting, pet training) already has this in GetCraftDescription.

IsTradeSkillLinked()

When the trade-skill window shows a profession from a link, the function returns 1 and the name of the linked player. Otherwise, it returns nil and nil. If the client does not know the name, the second value is nil.

After the window closes, the function gives the same values until the window opens again. Addons use this function to keep a linked profession out of the recipe records of the player.

local isLinked, playerName = IsTradeSkillLinked()
if not isLinked then
    -- the rows are your own recipes
end

IsCraftLinked()

When the craft window shows a profession from a link, the function returns 1 and the name of the linked player. Otherwise, it returns nil and nil. This function works like IsTradeSkillLinked. This is a ClassicAPI extension.

Trainer

GetTrainerServiceItemLink(index)

Returns the link to the item that the recipe in row index of the trainer window makes. Only a profession trainer gives a link. For other trainers, for a header row, for a recipe that makes no item, and for an index outside the list, the function returns nil.

The function also returns nil if the item is not in the item cache. When you select a row, the trainer window gets the data for the item of that row.

-- at a profession trainer
local link = GetTrainerServiceItemLink(GetTrainerSelectionIndex())
if link then
    print(link)
end

UIColor

C_UIColor.GetColors()

Returns an array of rows, each shaped { baseTag = "FOO_COLOR", color = ColorMixin }, mirroring the function of the same name. The color field is a real ColorMixin instance (carries GetRGB, GenerateHexColorMarkup, etc.) — the DLL calls back into Lua's CreateColor(r,g,b,a) per row to construct it, so ColorMixin and CreateColor must already be defined when GetColors is called.

The companion addon !!!ClassicAPI/Util/Color.lua does both: it defines ColorMixin/CreateColor first, then loops the result the same way Blizzard_SharedXMLBase/Color.lua does — assigning each row as a global under its baseTag plus a _CODE variant holding the |c-prefixed hex markup:

for _, dbColor in ipairs(C_UIColor.GetColors()) do
    local color = CreateColor(dbColor.color.r, dbColor.color.g, dbColor.color.b, dbColor.color.a)
    _G[dbColor.baseTag] = color
    _G[dbColor.baseTag.."_CODE"] = color:GenerateHexColorMarkup()
end

After that runs, addons can use the standard color globals directly:

/dump ACHIEVEMENT_COLOR:GetRGB()       -- 1, 1, 0
/dump ITEM_EPIC_COLOR:GenerateHexColor()
ITEM_EPIC_COLOR_CODE .. "Legendary" .. "|r"

There is no GlobalColor.dbc here, so the data comes from a snapshot embedded in the DLL (see src/ui/ColorData.h). Duplicate baseTags in the source DBC (PURE_RED_COLOR, INVASION_*, etc.) are deduplicated keeping the higher-ID entry, matching what ipairs(DBColors) ends up assigning. Colors for Death Knight runes, Mythic+ medals, healing absorbs, glyphs, and the objective tracker aren't in the snapshot and so aren't surfaced — none have a use case here anyway.

If CreateColor happens not to be defined when GetColors is called (e.g. another DLL or addon manages to call us before !!!ClassicAPI loads), each color field falls back to a plain {r,g,b,a} table. The loop above tolerates both shapes — dbColor.color.r reads the same way either way — so consumers shouldn't notice.

Unit

UnitGUID(unit)

Returns the unit's 64-bit GUID formatted as a hex string "0xHHHHHHHHLLLLLLLL" (16 hex digits, hi dword first), or nil if the resolved unit's GUID is empty (e.g. "target" with nothing targeted, empty party/raid slot).

local guid = UnitGUID("player")  -- "0x0000000000000777" (low IDs are local-realm characters)
local guid = UnitGUID("target")  -- "0xF13000059A002553" (the F130... prefix tags creatures)
local guid = UnitGUID("party1")  -- works even if party1 is on a different continent

The returned string is itself accepted as a unit token — UnitName(guid), UnitHealth(guid), etc. all work — so a GUID round-trips as a stable handle. See Unit tokens (GUID literals).

Works for OOR party / raid members. Earlier versions of this function returned nil for "partyN" / "raidN" when the member's CGObject wasn't currently active in the client (e.g. on another continent, in a different zone phase). We now read from the engine's parallel group-roster GUID array (populated by SMSG_GROUP_LIST), which is independent of unit visibility.

GUID format. GUIDs here are plain 64-bit integers — there's no "Player-RealmID-CharacterID" / "Creature-0-0-MapID-..." prefix system. Addons written for the prefixed form will need to either accept the "0x..." form or extract the raw hex.

Errors on invalid unit tokens. Same behavior as the other unit-token functions (UnitAffectingCombat, UnitName, etc.) — passing a string that doesn't match a known unit ID raises a Lua error rather than returning nil. We match the engine's existing convention here. Unit tokens that resolve to "no current unit" (like "target" with nothing targeted) return nil cleanly via the GUID = 0 check.

UnitTokenFromGUID(guid)

Best-effort reverse lookup: given a GUID string in the "0xHHHHHHHHLLLLLLLL" format UnitGUID returns, walk the engine's known unit tokens and return the first one currently mapped to that GUID, or nil if none of them point at it.

The search order omits tokens the engine's resolver doesn't recognize (vehicle, arenaN, arenapetN, bossN). nameplateN, focus, softenemy, softfriend and softinteract are included — we hook the resolver so the engine recognizes these token forms. As with the other positional tokens, a returned "nameplate3" result is only valid at that instant; the slot may shift to a different unit between calls, so re-verify with UnitGUID(token) before reusing.

player → pet → party1..4 → partypet1..4 → raid1..40
       → raidpet1..40 → nameplate1..N → target → focus
       → npc → mouseover → softenemy → softfriend → softinteract
local name, guid = GameTooltip:GetUnitGUID()
local token = UnitTokenFromGUID(guid)
if token then
    -- can now feed `token` to UnitHealth / UnitClass / etc.
end

The return is inherently unstable. Multiple tokens can map to the same GUID at once (your target IS your party1, your pet IS your raidpet5 if you're in a raid), and the mapping changes every time SMSG_GROUP_LIST fires, the player tabs target, or a party member loads in. If you cache a result, re-verify with UnitGUID(token) == originalGuid before reusing it.

Returns nil for malformed GUID strings, zero GUIDs, and GUIDs that don't match any currently-resolvable token — including ex-targets, ex-mouseover units, distant players seen in the chat log, etc. The engine simply doesn't address those by token.

UnitTokenFromName(name [, exactMatch])

Finds the unit called name and returns a token for it, or nil when nobody around matches. This is what lets a character name stand in for a unit token, as [target=Feral] does in a macro.

local token = UnitTokenFromName("Feral")
if token then
    print(UnitHealth(token), UnitClass(token))
end

The search matches on the start of the name and prefers the nearest unit, the same rule TargetByName uses. Pass a true exactMatch to require the whole name. It looks through your party, then your raid, then every unit loaded around you, so it reaches units that no standard token names.

The result is a standard token such as party1 or target when one fits, and otherwise the unit's GUID, which the unit functions also accept.

Treat it as a snapshot. Which token names a unit changes as you retarget or as the group changes, so resolve it again rather than storing it.

ClassicAPI extension.

SetUnitCursorTexture(texture, unit [, style [, includeLowPriority]])

Sets texture to the cursor image that the mouse shows when it is over the unit. For example: the speak bubble for an NPC with gossip, the mail icon for a mailbox, or the attack sword for an enemy. When you are too far away, the image is the grey "Unable" form.

  • texture is a Texture object.
  • unit is any unit token. "softinteract" and "anyinteract" can also point at a game object or a corpse.
  • style has no effect.
  • When includeLowPriority is true, the normal pointer also counts as an image. When you do not give it, the SoftTargetLowPriorityIcons cvar sets it.

Returns true when it sets the texture. When the unit has no image, it returns false and clears the texture. Otherwise it returns false and does not change the texture.

local icon = UIParent:CreateTexture(nil, "OVERLAY")
icon:SetWidth(24)
icon:SetHeight(24)
icon:SetPoint("CENTER", 0, 100)
if SetUnitCursorTexture(icon, "softinteract") then
    icon:Show()
else
    icon:Hide()
end

UnitFactionGroup("player") while addons load

UnitFactionGroup("player") answers while addons load (at file scope, ADDON_LOADED and VARIABLES_LOADED), as UnitName, UnitRace and UnitClass already do and as later clients do. Stock 1.12 returns nil until PLAYER_LOGIN, and AceDB-3.0 builds its "<faction> - <realm>" profile key from it at file scope.

-- at file scope, on a Human character
UnitFactionGroup("player")   -- "Alliance", "Alliance" (stock 1.12: nil)

Before you enter the world, the answer comes from the character's race. The engine uses the same race-to-faction chain for a player it cannot see, and C_CreatureInfo.GetFactionInfo uses it for a race. After you enter the world, the result is the same as stock, nil included. Results for other units do not change.

UnitSubName(unit)

Returns the NPC's subtitle / title string (the small italic text under their name in the tooltip): "Innkeeper", "Stable Master", "<Master Engineer>", etc. Returns nil for players, unsynced NPCs, and NPCs with no subtitle.

/dump UnitSubName("target")
-- "Innkeeper"             (Stormwind innkeep)
-- "<Auction House>"       (auctioneer)
-- nil                     (a player, or a wolf in the woods)

The stock UnitName returns only one value, leaving no direct route to the subtitle. Addons that wanted it had to scrape GameTooltip text, which is fragile and required the unit to be hovered. UnitSubName reads it straight off the engine's creature cache ([unit + 0xB30] → [+0x10]), no tooltip round-trip.

Coverage caveat: the creature cache row is populated when an NPC becomes visible to the client AND the server has sent its CMSG_CREATURE_QUERY_RESPONSE. The first time a fresh NPC appears the row may be briefly NULL — usually only one or two frames. Subsequent calls succeed once the response lands.

UnitSelectionColor(unit [, useExtendedColors]) / UnitSelectionType(unit [, useExtendedColors])

UnitSelectionColor returns the colour of the selection circle of the unit as red, green, blue, alpha, each from 0 to 1. UnitSelectionType returns the number of that colour:

Type Colour Unit
0 red Hostile
1 orange Unfriendly
2 yellow Neutral
3 green Friendly, or a player flagged for PvP
4 blue A player that is not flagged for PvP
5 light blue #6060FF Same as 4, with extended colours
6 lavender #AAAAFF A party member, with extended colours
7 light green #AAFFAA A party member flagged for PvP, with extended colours
8 sky blue #53C8FF A player on your friends list, with extended colours
9 grey A dead NPC, with extended colours

When useExtendedColors is true, the result is the colour of the name above the head and the selection circle. When it is false or absent, party members and friends get the player colour, and a dead NPC gets its reaction colour.

/dump UnitSelectionColor("target")        -- 0, 1, 0, 1 (a friendly NPC)
/dump UnitSelectionType("party1", true)   -- 6
/dump UnitSelectionType("target", true)   -- 8 (a friend)

A party or raid member out of range also returns a colour. For this unit, the PvP flag comes from the group data. A token with no unit, for example "target" with no target, returns the player colour.

UnitCreatureFamilyID(unit)

Returns the numeric CreatureFamily id (the CreatureFamily.dbc row — 1 = Wolf, 2 = Cat, 3 = Spider, …) for a unit, or nil when it has no family (players, non-beast NPCs, unsynced NPCs).

/dump UnitCreatureFamilyID("pet")     -- e.g. 1 for a wolf pet
/dump UnitCreatureFamilyID("target")  -- 2 for a cat, nil for a humanoid

Stock UnitCreatureFamily(unit) returns only the localized name ("Wolf", "Chat") — awkward to compare and locale-dependent. This extension exposes the raw id addons actually want for stable checks (tameable-pet filtering, family-specific ability tables). It's the same id the engine's UnitCreatureFamily resolves internally ([unit + 0xB30] → [+0x1C]) before mapping it through CreatureFamily.dbc for the display name — this just stops at the id. nil semantics match UnitCreatureFamily (both return nothing for a family of 0), and it shares UnitSubName's creature-cache coverage caveat (a fresh NPC's row may be briefly NULL for a frame or two).

UnitCreatureTypeID(unit)

Returns the numeric CreatureType id (the CreatureType.dbc row — 1 = Beast, 3 = Demon, 7 = Humanoid, …) for a unit, or nil when it has no resolvable type. The raw-id twin of UnitCreatureFamilyID.

/dump UnitCreatureTypeID("target")   -- 7 for a humanoid, 1 for a beast
/dump UnitCreatureTypeID("player")   -- 7 (Humanoid, via the player's race)

Stock UnitCreatureType(unit) returns only the localized name ("Humanoid", "Humanoïde") — awkward to compare and locale-dependent. This exposes the id addons want for stable checks. It calls the same inner resolver the engine's UnitCreatureType uses, which handles every unit kind (NPCs via the creature cache, players via their race → Humanoid). Pair with C_CreatureInfo.GetCreatureTypeInfo to get the localized name back from the id.

UnitCreatureID(unit)

Returns the numeric creature id — the NPC / creature-template entry — for a unit, or nil. This is the unit-token twin of C_CreatureInfo.GetCreatureID: it does C_CreatureInfo.GetCreatureID(UnitGUID(unit)) for you.

creatureID = UnitCreatureID(unit)
/dump UnitCreatureID("target")   -- 1842 for Hogger, nil for a player
/dump UnitCreatureID("pet")      -- your pet's creature-template id

The id is read live from the unit, so it always matches the name you see, and it is the id Wowhead and Turtle's database show for that NPC. For a unit that is out of view, the same fallback as C_CreatureInfo.GetCreatureID applies. Returns nil for a player (a player carries no template), an unresolvable-but-valid token ("target" with nothing targeted, an empty "partyN" slot), an out-of-view pet, and any unit whose GUID isn't a creature or pet. Raises a Lua error on a garbage token — the standard UnitX behavior. Pair with C_CreatureInfo.GetCreatureInfoByID to get the name from the id.

GetUnitSpeed(unit)

Returns currentSpeed, runSpeed, flightSpeed, swimSpeed — all four in yards/second. There is no flying, so flightSpeed is always 0.

local current, run, _, swim = GetUnitSpeed("player")
-- e.g. 7.0, 7.0, 0, 4.722 for an unmounted player running normally
-- e.g. 14.0, 14.0, 0, 4.722 with a 100% mount active (run speed
--      reflects the modifier; swim ignores mount)
  • currentSpeed — speed the engine would apply to this frame's movement step. 0 when stationary; otherwise the walk/run/swim/ backward variant the unit is actually moving with. Read via the engine's own selector at 0x007C4C90 so movement-flag handling (walking, swimming, taxi paths) matches the engine exactly.
  • runSpeed — raw forward-run speed from MovementInfo +0x8C. Includes mount / buff / debuff multipliers — the engine maintains this field as the post-modifier value, updated by SMSG_FORCE_RUN_SPEED_CHANGE and friends.
  • flightSpeed — always 0.
  • swimSpeed — raw forward-swim speed from MovementInfo +0x94.

All four returns are 0 if the token doesn't resolve to a live CGUnit — empty "target", out-of-range party member, etc. Pushes 0.0 rather than nil for non-visible units.

Reads [CGUnit + 0x118] to get the MovementInfo pointer, then reads speed fields directly. Field offsets verified via the movement-prediction loop (FUN_005FC350 and the sprint-jump calculator at 0x00511920 both reach into the same struct).

UnitIsAFK(unit)

Returns true if the unit is currently AFK (toggled via /afk or auto-set after idle timeout). Works for any player-controlled unit — local self, target, party*, raid*, inspect targets. NPCs always return false.

UnitIsAFK("player")   -- true if you've /afk'd
UnitIsAFK("target")   -- true if the targeted player is AFK
UnitIsAFK("party1")   -- true if party member 1 is AFK
UnitIsAFK("npc")      -- always false

How it works under the hood. PLAYER_FLAGS isn't broadcast as a UpdateField here, but every nearby player's CGPlayer-side info struct at [unit + 0xE68] carries it at byte +0x08. Same struct the engine reads when rendering the <AFK> prefix above a player's head. Verified against the in-game nameplate behavior.

UnitIsDND(unit)

Returns true if the unit is currently in DND mode ("Do Not Disturb", toggled via /dnd). Same unit-coverage as UnitIsAFK — any player-controlled unit, false for NPCs.

UnitIsDND("player")
UnitIsDND("target")

UnitIsFeignDeath(unit)

Returns true if the unit is feigning death (Hunter's Feign Death).

UnitIsFeignDeath("target")   -- true if a feigning hunter

UnitIsInMyGuild(unitOrName)

Returns 1 if the unit/character shares the player's guild, nil otherwise. Accepts either a unit token ("player", "target", "party1", etc.) or a literal character name ("Bob").

UnitIsInMyGuild("player")    -- 1 if you're in any guild
UnitIsInMyGuild("target")    -- 1 if your target is a guildmate
UnitIsInMyGuild("party1")    -- 1 if party member 1 is a guildmate
UnitIsInMyGuild("Bob")       -- 1 if there's a guildmate named Bob

Resolution strategy:

  1. Calls UnitName(input) via lua_pcall to canonicalize the input. Tokens resolve cleanly; literal names hit the engine's "Unknown unit name" error which pcall swallows.
  2. For valid tokens, attempts a fast direct comparison against the player's guild-key field at [unit + 0xE68 + 0x0C] — same field GetGuildInfo reads, populated immediately on guild join for the local player and for any synced player-controlled unit. No roster fetch needed for nearby/loaded units.
  3. Falls back to walking the engine's guild roster array (same backing storage GetGuildRosterInfo reads) by name. Required for out-of-range party members, distant raid members, and literal name input — needs GuildRoster() to have been called and the server's response to have arrived.

Return convention is 1.0 / nil, not boolean. Both work for if UnitIsInMyGuild(x) then checks.

The slow path reads the engine's full roster count ([0x00B73118], the same value GetNumGuildMembers(true) returns), not the online-only count, so offline guildmates resolve too — the "show offline" toggle doesn't affect lookup. The only requirement is that GuildRoster() has been called and the server's SMSG_GUILD_ROSTER response has arrived.

UnitIsPossessed(unit)

Returns true if the unit is currently possessed (priest's Mind Control, warlock's Subjugate Demon). Reads UNIT_FIELD_FLAGS bit 24 (0x01000000) — the standard UNIT_FLAG_POSSESSED per emulator sources — directly off the unit's m_objectFields descriptor. Works for any unit token since UNIT_FIELD_FLAGS is broadcast in object updates.

UnitIsPossessed("target")   -- true if mind-controlled

Distinct from UnitIsCharmed: charm covers any charm-type effect (including pets summoned via mob-charm spells), possess is the specific spell-driven take-over effect.

UnitIsMinion(unit)

Returns true if unit is a minion of a player — a pet, guardian, totem, or charmed creature — false otherwise. Primarily owner-based: the unit's owner GUID (SummonedBy / CreatedBy / CharmedBy) must resolve to a player, with "player-controlled but not itself a player" as a fallback. Owner-based is needed because summoned guardians (shown as "X's Minion") carry an owner but not the PLAYER_CONTROLLED flag, so a flag-only check would miss them. Players, NPCs, and world creatures return false.

Built from the engine's own primitives. A bad or unresolved unit token returns false.

UnitIsMinion("pet")     -- your pet → true
UnitIsMinion("player")  -- you → false
UnitIsMinion("target")  -- another player's felhunter → true; a boar → false

UnitIsPet(unit)

Returns true if unit is a pet of a player (as opposed to a guardian/totem "minion"), false otherwise — the pet↔minion discriminator. Uses the engine's own classifier: owned by a player and the family-based pet/minion test that drives the UNITNAME_TITLE_PET vs UNITNAME_TITLE_MINION tooltip label. Neither the GUID (pets and guardians share the 0xF14… prefix on this client) nor UNIT_FLAG_PLAYER_CONTROLLED (a "Minion" can carry it) reliably separates them, so the creature-family classifier is the correct signal; the owner-is-player gate also excludes wild tameable beasts of the same family.

UnitIsMinion is the umbrella (pet + guardian + totem); UnitIsPet is the controllable subset. A ClassicAPI extension, not a stock WoW global. A bad or unresolved unit token returns false.

UnitIsPet("pet")                 -- your pet → true
-- another player's "X's Pet"    → UnitIsMinion true, UnitIsPet true
-- another player's "X's Minion" → UnitIsMinion true, UnitIsPet false (guardian, not controllable)

UnitIsOtherPlayersPet(unit)

Returns true if unit is a minion (pet, guardian, totem, or charmed creature) whose owner is a player other than you, false otherwise. Owner-based: it reads the unit's owner GUID (SummonedBy / CreatedBy / CharmedBy), requires that owner to be a player, and requires it to differ from you. World creatures and players (no owner) return false; your own minions (owner is you) return false. A bad or unresolved unit token returns false.

UnitIsOtherPlayersPet("target")  -- a party member's pet → true; your own pet → false

UnitOwnerGUID(unit)

Returns the GUID string ("0x…") of the unit's owner — the summoner of a pet/guardian/totem, or the charmer of a charmed unit. nil for an unresolved unit and for anything with no owner (players, world creatures).

local owner = UnitOwnerGUID("pettarget")   -- e.g. the enemy hunter's GUID
if owner == UnitGUID("player") then ... end  -- is it my minion?

Reads the same owner field the pet predicates use (CharmedBy, else CreatedBy in the unit's m_objectFields).

UnitCreatedBySpell(unit)

Returns the spell ID that summoned unit: the totem-drop spell for a totem, or the summon spell for a pet or guardian. Returns nil for an unresolved unit, and for a unit that no spell summoned (players and world creatures).

This is the summoning spell, not a spell that the unit casts later. The spell that a totem casts stays on the server and never reaches the client. The client receives this value for every summoned unit, so it works for any unit in range, not only your own summons. Use GetSpellInfo to get a readable name.

A ClassicAPI extension, not a stock WoW global.

UnitCreatedBySpell("pet")                -- your pet's summon spell id
local id = UnitCreatedBySpell("target")  -- target a totem → its totem spell id
if id then print(GetSpellInfo(id)) end   -- prints the name, like "Searing Totem"

UnitStandState(unit)

Returns the unit's standstate as an integer, matching the Enum.PlayerStandState values:

Value Meaning
0 STANDING
1 SITTING
2 SITTING_CHAIR
3 SLEEPING
4 SITTING_LOW_CHAIR
5 SITTING_MEDIUM_CHAIR
6 SITTING_HIGH_CHAIR
7 DEAD
8 KNEELING

Reads the low byte of UNIT_BYTES_1 (descriptor +0x210), a broadcast UpdateField — works for any synced unit (player, target, party*, raid*, mouseover, etc.). Returns 0 (STANDING) for unresolvable units (empty party* slot, no current target, etc.) returning a safe default.

UnitStandState("player")    -- 0 standing, 1 sitting, 5 chair-sit, …
UnitStandState("target")    -- works for any visible unit
UnitStandState("party1")    -- 0 if the slot is empty

The stock client has IsSitOrStanding() (local-player boolean) but no unit-token form; UnitStandState fills the gap and exposes the full enum.

UnitInRange(unit)

Returns (inRange, checkedRange) — whether unit is within 40 yards of the player, plus a flag indicating whether the range check could actually be performed.

While you control another unit (for example with Mind Control or Eyes of the Beast), the distance is measured from that unit. When the client moves no unit, the result is (false, false).

local inRange, checked = UnitInRange("party1")
if checked and not inRange then
    -- partymate is loaded but >40 yards away → skip the heal
end
Return Meaning
inRange true if the unit is within a 40-yard heal's reach of the player. false if out of range OR if checkedRange is false.
checkedRange true when a position-based range check was performed. false when the unit's position isn't available — either the token didn't resolve (empty partyN slot, no target, raid member in a different zone, etc.) or unit == "player" (see below).

Reads the world position via the CGObject::GetPosition vtable virtual (slot 5, offset +0x14) — same path CheckInteractDistance uses.

Reach-aware. The threshold is not a flat 40 yards on the raw center-to-center distance — it's 40 + playerReach + targetReach, mirroring the engine's own spell-range formula (FUN_006e3480), where each unit's reach is the bounding-radius float at [m_objectFields + 0x1F0] (the same size factor the interact-distance and loot-range checks add). A 40-yard heal lands on a target whose center is up to 40 + reach yards away, so a bare center-distance cap of 40 wrongly reports out-of-range for a target the client can actually heal (on a large target the reach gap is 2–3 yards). If a unit's descriptor isn't populated yet the reach term is 0 and the check degrades to a plain 40-yard cap. Note this is the one difference from UnitDistanceSquared, which stays raw center-to-center.

UnitInRange("player") returns (false, false) by design. The function is meant for healing-frame "is another unit in range" checks; querying yourself is meaningless. We short-circuit before the position read so the result is unambiguous (checkedRange=false).

Works for any synced unit (party/raid members in your zone, target, mouseover, etc.). For raid members in a different zone or otherwise not in the engine's sync window, the position virtual returns null and the function reports (false, false).

UnitDistanceSquared(unit)

Returns (distanceSquared, checkedPosition) — the squared world distance (yards²) from the player to unit, and a flag indicating whether both positions were available.

local distSq, checked = UnitDistanceSquared("target")
if checked and distSq <= 30 * 30 then
    -- target is within 30 yards
end
Return Meaning
distanceSquared Squared distance player→unit. 0 when checkedPosition is false (a placeholder — an "always a number" shape).
checkedPosition true when both positions were read. false when unit's position isn't available (empty partyN slot, no target, raid member outside the sync window, etc.).

The value is squared on purpose: nearly all distance logic is a threshold compare (distSq <= range * range) or a nearest-unit sort, neither of which needs the square root — so only the squared form is exposed, never a plain UnitDistance. Take math.sqrt(distanceSquared) only when you need a yard number to show a human.

Reads world positions via the CGObject::GetPosition vtable virtual (slot 5, offset +0x14) — the same path UnitInRange / CheckInteractDistance and our own UnitPosition use. Center-to-center, not reach-aware (that edge-to-edge nicety is UnitXP_SP3's niche); the value equals what you'd compute from the raw UnitPosition coordinates, and it's self-contained (no sibling-DLL dependency).

No self-quirk (unlike UnitInRange): UnitDistanceSquared("player") returns a legitimate (0, true). Because a real 0 (self, or two exactly co-located units) is indistinguishable by value from the miss placeholder, always branch on checkedPosition.

UnitPosition(unit)

Returns posY, posX, posZ, instanceID — the unit's world position, in the standard UnitPosition shape. nil when the unit has no known position.

local posY, posX, posZ, instanceID = UnitPosition("player")
Return Meaning
posY World Y coordinate (the west axis).
posX World X coordinate (the north axis).
posZ World Z coordinate (height).
instanceID Currently-loaded map id (Map.dbc row — e.g. 0 Eastern Kingdoms, 1 Kalimdor). Every visible unit shares the player's instance.

Standard field order — posY (west) first, then posX (north), then posZ, mirroring Blizzard's own UnitPosition. So an addon that does local py, px = UnitPosition(u) gets its coordinates labelled as expected. (World X is north/south, Y is west/east — WoW's long-standing convention.)

Not group-restricted. The standard UnitPosition returns nil for units outside your party/raid (a privacy guard that doesn't exist here). This backport reads any visible unit — "target", "mouseover", nameplate tokens, arbitrary party/raid members in sync range — so it's strictly more permissive, closer to SuperWoW's unit-position access. Units the client can't currently see (out of range, another zone) have no position → nil.

Reads through the same CGObject::GetPosition virtual as UnitDistanceSquared; the two are consistent (the squared delta of two UnitPosition reads equals UnitDistanceSquared).

SuperWoW interaction. SuperWoW also defines a global UnitPosition with a different return shape. Both register on the same Lua state, so if SuperWoW is loaded the last registrant wins — install order decides which shape is live. Without SuperWoW, this version is the one you get.

UnitFacing(unit)

Returns the direction that unit faces, as an angle in radians (0 … 2*pi). The angle is 0 at north and increases counter-clockwise. If the unit does not exist, the function returns nothing. For "player", the value is the same as GetPlayerFacing.

local facing = UnitFacing("target")   -- e.g. 3.11

On a boat or a zeppelin, the angle is the direction in the world, not the direction relative to the ship.

UnitInLineOfSight(unit)

Returns true if the player has clear line of sight to unit, false if world geometry (terrain or a building) blocks it, and nil when the check can't apply — an absent / unresolvable token, or a non-unit object. A ClassicAPI extension (not a stock WoW global).

if UnitInLineOfSight("target") then
    -- clear shot
end

The check uses one line from your unit to the other unit. Each end of the line is at the height of that unit's collision box. The server uses the same line for its line-of-sight check on spells. Works with or without UnitXP_SP3.

Notes and limits:

  • Terrain, buildings, doodads (trees, rocks, props) and game objects can block the line. The client tests only geometry that it has loaded.
  • The server can block a line that the client sees as clear when its position for a unit is different from your client's (for example, while the unit moves).
  • Center/eye-line, not a volume — a sliver of a unit peeking past a corner reads as blocked if the eye-line itself is occluded.
  • Pairs beyond ~150 yards report false (a guard against a long-segment crash in the underlying trace); such units are usually outside the client's object sync window anyway.
  • UnitInLineOfSight("player") is true (you always see yourself).

UnitClassBase(unit)

Returns (classFile, classID) — the locale-independent class token plus the numeric classID. The token is one of "WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "SHAMAN", "MAGE", "WARLOCK", "DRUID"; the classID matches the integer UnitClass's third return surfaces (1=Warrior, 2=Paladin, 3=Hunter, 4=Rogue, 5=Priest, 7=Shaman, 8=Mage, 9=Warlock, 11=Druid — 6 and 10 are unused here).

local token, id = UnitClassBase("player")    -- "WARRIOR", 1
local token = UnitClassBase("target")        -- works for any synced unit
local color = RAID_CLASS_COLORS[UnitClassBase("party1")]

Addons use the token for class detection because the UnitClass(unit) returns a localized first return (e.g. "Krieger" on a German client), which is fine for display but breaks any addon code that keys on if class == "WARRIOR". The classID second return is a ClassicAPI extension — the standard UnitClassBase returns the token only — but it saves callers from chaining UnitClass(unit) just to get the integer.

Reads the class byte from UNIT_FIELD_BYTES_0 (descriptor +0x79, the same byte UnitClass's general-token path reads) and looks up the english filename at ChrClasses.dbc::Filename (record +0x38). Both fields are broadcast / static, so remote players' class is current regardless of distance.

Returns (nil, nil) for:

  • target with no current target, empty partyN / raidN slots, or units whose descriptor isn't loaded yet
  • creatures and other non-player units (their class byte indexes a different table; the class-byte lookup naturally returns nil for out-of-range / zero values)

Throws a Lua error on missing / non-string unit argument — same shape as UnitClass itself.

UnitRaceBase(unit)

Returns (raceFile, raceID) — the locale-independent race token plus the numeric raceID. The token is one of "Human", "Orc", "Dwarf", "NightElf", "Scourge", "Tauren", "Gnome", "Troll"; the raceID is 1..8 for those values respectively.

local token, id = UnitRaceBase("player")    -- "Human", 1
local token = UnitRaceBase("target")        -- works for any synced unit

Sibling to UnitClassBase. Same problem (the UnitRace(unit) returns a localized name — "Mensch", "Orc", etc.); same solution (locale-independent token straight from the DBC). Reads byte 0 of UNIT_FIELD_BYTES_0 (descriptor +0x78) and looks up ChrRaces.dbc::Filename (+0x3C).

Returns (nil, nil) for unresolvable units and non-player units (creature race bytes don't index ChrRaces.dbc).

ClosestUnitPosition(creatureID)

Returns xPos, yPos, distance — the world position of the nearest creature with the given NPC (creature-template) ID, and its distance from the player in yards. Returns nothing (nil) when no matching creature is in range.

local x, y, dist = ClosestUnitPosition(3098)   -- nearest Kobold Vermin
if x then
    -- x, y = world coords; dist = yards from the player
end

Walks the client's visible-object manager, matches creatures whose GUID encodes creatureID (bits 24–47), and returns the closest one's position

  • center-to-center distance.

Behavior note. A ClosestUnitPosition that reads a static client-side spawn database only works for starting-zone mobs. There is no such database here, so this returns the nearest currently-visible creature of that entry (anywhere, not just starting zones). It can't point at un-synced spawns elsewhere in the zone the way a static spawn database can — but for "where's the nearest <mob> I can see" it's more general.

UnitHealthMissing(unit)

The health deficit — UnitHealthMax(unit) - UnitHealth(unit) — in one call. A convenience for healing addons (overheal math, "missing health" bars) that would otherwise call both engine functions and subtract in Lua every frame.

local missing = UnitHealthMissing("player")   -- 0 at full health

Returns 0 at full health and for a valid-but-absent unit (e.g. "target" with nothing targeted), matching UnitHealth's 0-for-missing convention; clamped so a transient current > max never returns negative.

Tracks the engine's own UnitHealth / UnitHealthMax exactly, including the percentage form for non-grouped units (where UnitHealthMax is 100): a target at 85/100 reports 15.

Direct descriptor reads — desc[+0x40] (HEALTH), desc[+0x58] (MAXHEALTH). No engine call, no Lua-stack roundtrip.

UnitPower(unit [, powerType [, unmodified]]) / UnitPowerMax(unit [, powerType [, unmodified]])

Multi-power-type getters. The stock client only ships UnitMana(unit) / UnitManaMax(unit) which return whichever primary power the unit happens to have (mana for casters, energy for rogues, rage for warriors, etc.); these add the explicit powerType arg so addons can read a specific power slot without caring what the unit's primary is.

local mana    = UnitPower("player", 0)   -- explicit mana
local energy  = UnitPower("player", 3)   -- explicit energy
local primary = UnitPower("player")      -- whatever the player's primary is
local maxMana = UnitPowerMax("target", 0)

powerType values:

value type
0 MANA
1 RAGE
2 FOCUS
3 ENERGY
4 HAPPINESS

These match the Enum.PowerType namespace published alongside — UnitPower("player", Enum.PowerType.Mana) is the idiomatic shape.

Omitting powerType (or passing -1 / any out-of-range value) falls back to the unit's primary power, read from UNIT_FIELD_BYTES_0 byte 3 (descriptor +0x7B) — the same source the engine uses internally.

Returns 0 for invalid units, unresolvable tokens, or power types outside the 0..4 range (Runes / Runic Power have no descriptor slots in the unit layout).

Display-divisor applied. Some power types are stored at a scaled value internally and divided before being exposed through Lua — same trick Script_UnitMana (0x00517670) uses, reading the divisor table at 0x0086F978:

type divisor
0 MANA 1
1 RAGE 10 (raw 0..1000 → display 0..100)
2 FOCUS 1
3 ENERGY 1
4 HAPPINESS 1000

So a fresh warrior reads UnitPower("player", 1) = 0..100, not 0..1000.

Pass a truthy unmodified (third arg) to bypass the divisor and get the raw internal value — UnitPower("player", 1, true) returns rage as 0..1000. The same third argument UnitPower / UnitPowerMax take.

Direct descriptor reads — desc[+0x44 + type*4] for current power, desc[+0x5C + type*4] for max, divided by the table entry for the type. No engine call, no Lua-stack roundtrip.

UNIT_POWER_UPDATE and UNIT_MAXPOWER fire when these values change.

UnitPowerMissing(unit [, powerType [, unmodified]])

The power deficit — UnitPowerMax(...) - UnitPower(...) for the given type — in one call. The power analogue of UnitHealthMissing, for energy/mana/rage "resource needed" checks. Same unit / powerType handling and invalid-unit-returns-0 semantics as UnitPower.

local energyNeeded = UnitPowerMissing("player", Enum.PowerType.Energy)
local rageMissing  = UnitPowerMissing("player")   -- primary power

unmodified (arg 3, any truthy value) skips the display divisor and returns the raw internal deficit — the same third argument UnitPower takes.

Computed as the difference of the two display values (each integer-divided by the per-type divisor), not by dividing the raw difference, so it equals UnitPowerMax(...) - UnitPower(...) exactly. Those diverge for rage: raw 1000/55 displays as 100/5, a deficit of 95, whereas (1000-55)/10 truncates to 94. Clamped at 0.

UnitPowerType(unit)

The stock version returns just the integer power type; this implementation extends it to the 2-tuple (powerType, powerToken). Strict-superset signature — addons that destructure only the first return are unaffected.

local pt, token = UnitPowerType("player")
-- e.g. (0, "MANA") for a paladin, (1, "RAGE") for a warrior

Token strings:

value token
0 "MANA"
1 "RAGE"
2 "FOCUS"
3 "ENERGY"
4 "HAPPINESS"
5 "RUNES"
6 "RUNIC_POWER"

5/6 are power types that can't appear on a unit's descriptor here; they're included in the table for symmetry in case a private server pushes one through.

Chains to the engine's original Script_UnitPowerType at 0x00517940 to preserve its full unit-resolution flow (object- manager lookup with pet / totem / vehicle fallbacks), then reads the just-pushed integer and appends the token string.

UnitSpellHaste(unit)

A spell-haste getter — there is no stock haste API. Returns the spell haste percentage: 0 for an unhasted unit, positive when casting is sped up, negative when slowed (Curse of Tongues).

local haste = UnitSpellHaste("player")   -- 0 for most vanilla casters

Reads UNIT_MOD_CAST_SPEED — the cast-time multiplier at descriptor +0x22c that the server folds into SpellEntry::GetCastTime (castTime *= modCastSpeed) — and converts it to a percentage: haste% = (1 / modCastSpeed - 1) * 100. So modCastSpeed 1.0 → 0, 0.5 (half cast time) → 100, >1.0 → negative.

This is the Blizzard-shaped surface over the field nampower exposes raw as GetUnitField(unit, "modCastSpeed"), so addons can drop that dependency. Consumers that need the raw multiplier back can recover it as 1 / (1 + UnitSpellHaste(unit) / 100). Returns 0 for invalid units. UNIT_SPELL_HASTE fires when the value changes.

UnitSpellTargetName(unit)

Returns the name of the unit that unit is casting or channeling a spell at, or nil.

targetName = UnitSpellTargetName(unit)
UnitSpellTargetName("target")   -- "Playername" while the mob casts at you
UnitSpellTargetName("player")   -- your current cast's target, or nil

Returns nil when:

  • unit is not casting or channeling.
  • The spell has no unit target. This covers a self-cast, a ground-target spell (Blizzard, Rain of Fire), and an item or lock cast.
  • The target's name can't be resolved (an off-screen stranger).

The target comes from the cast's SMSG_SPELL_START packet. ClassicAPI captures it for the player and for any remote unit whose cast you observed since it began. So the same limit as C_Spell.UnitCastingInfo applies: a remote unit's target is known only while you saw its cast. The name is resolved through the object manager (players and creatures), then the friends list, then the persistent name cache — the same chain as UnitNameFromGUID.

The player's target lands with the confirming packet, about one round-trip after the cast starts. So UnitSpellTargetName("player") can read nil for the first part of your own cast, then return the name.

UnitHasIncomingResurrection(unit)

hasIncomingResurrection = UnitHasIncomingResurrection(unit)

Returns true while somebody casts a resurrection spell at unit. Otherwise it returns false. If unit is a released player, the function also includes casts at the corpse of that player.

if UnitHasIncomingResurrection("party1") then
    print(UnitName("party1"), "has an incoming resurrection")
end

The value changes to true when the cast starts. It changes to false when the cast stops, for any reason. This includes a completed cast that the target did not accept yet.

You see a cast only when its caster is near you. If the caster goes out of range during the cast, the value changes to false.

INCOMING_RESURRECT_CHANGED fires at each change.

CanBeRaidTarget(unit)

Returns true if you can put a raid target icon on the unit. Otherwise it returns false.

These units are eligible:

  • you
  • a member of your party or raid, and the pet of a member
  • an NPC
  • a player of your faction

If you or the other player is charmed, that player is not eligible. A unit that does not exist returns false.

if CanBeRaidTarget("target") then
    SetRaidTarget("target", 8)
end

UnitAuras

Backport of the C_UnitAuras namespace. Returns AuraData-shaped tables instead of the UnitBuff / UnitDebuff multi-return tuples, so addon code that does local d = C_UnitAuras.GetAuraDataByIndex(unit, 1); if d.dispelName == "Magic" then ... works unchanged.

Reads primarily off the unit's m_objectFields descriptor — same data source UnitBuff / UnitDebuff use. The descriptor has 48 aura slots. Buffs sit in slots 0 to 31 and debuffs in slots 32 to 47.

Some servers, Turtle among them, put extra debuffs into free buff slots once the 16 debuff slots are full. HELPFUL and HARMFUL still select on the aura's real polarity there, so a debuff in a buff slot is still a debuff. The native UnitBuff and UnitDebuff split by slot range and report that debuff as a buff.

Aura order. The functions that count auras by index, or list them, put your own auras first. The order has three groups:

  1. Auras that you cast, or that a unit you control cast (your pet, a guardian, a totem, or a charmed creature).
  2. Auras that you could apply yourself (canApplyAura is true).
  3. All other auras.

In each group, the auras are in slot order. A HARMFUL walk visits slots 32 to 47 first, then any spilled debuffs in 0 to 31. Because of this order, an index is not the same index that the native UnitBuff or UnitDebuff uses. Pass a C_UnitAuras index only to the C_UnitAuras functions and to GameTooltip:SetUnitAura. For a party or raid member with no live unit object, the auras stay in slot order.

When a party/raid member has no live unit object at all (a different map, far out of range), there is no descriptor to read — but the server still transmits that member's current aura spell IDs, and these functions surface them, exactly as the built-in UnitBuff / UnitDebuff do. See Out-of-range group members.

AuraData table shape

Field Type Source / value
name string localized spell name from Spell.dbc
icon string icon path from SpellIcon.dbc
applications number stack count (engine stores stacks-1, we display +1)
spellId number spell ID from the descriptor's aura array
dispelName string "Magic" / "Curse" / "Disease" / "Poison" (from SpellDispelType.dbc), or "" if non-dispellable
isHelpful boolean true when the aura is a buff
isHarmful boolean true when the aura is a debuff. This is the aura's real polarity, so a debuff parked in a buff slot still reads harmful
duration number applied duration in seconds. For unit == "player", this is the duration the server applied, with diminishing returns and the caster's talents included. For other units, when the aura's cast was observed (in the Aura::Source cache), this is the caster-modified duration — talent/glyph extensions like Improved Shadow Word: Pain included — so it stays consistent with expirationTime (remaining ≤ duration). When no expiration time is known, duration is 0 too, so the two values are always given together. Returns 0 for spells flagged "no duration" (passives, paladin auras, infinite buffs)
expirationTime number for unit == "player", read from the engine's player-buff table at 0x00BC6040 (same data GetPlayerBuffTimeLeft returns). For any other unit, taken from the Aura::Source cache (cast time + duration captured from SMSG_SPELL_GO; see below). 0 when neither source has it, and 0 for one of your auras that lasts until you cancel it. expirationTime - GetTime() gives the true remaining time
sourceUnit string unit token of the caster ("player", "raid7", "nameplate1", …), resolved from the Aura::Source cache. nil if the cast wasn't observed or the caster maps to no current token
sourceGUID string caster's "0x…" GUID string from the same cache. A ClassicAPI extension — not a standard AuraData field. Set whenever a caster is known, including when sourceUnit is nil (caster left token range). Stable for the session, unlike the volatile nameplate token; doubles as a unit token under SuperWoW. nil on a cache miss
charges / maxCharges number always 0 — there are stacks, not charges
timeMod number always 1 — there are no haste-affected auras
isFromPlayerOrPlayerPet boolean true when a player — any player, not only you — or a player's pet applied the aura. Read from the cached caster GUID, so it is false when the cast was not seen this session. Totem buffs read false: a totem is a creature, not a pet
canApplyAura boolean true when you or your pet know the aura's spell, and the spell applies an aura
isRaid boolean true when the aura passes the RAID filter: a buff that you can cast on other players, or a debuff that you can dispel
isStealable boolean true when you can remove the buff from an enemy: you can attack the unit, the aura is a buff that is not passive, not an area aura and not channeled, and you or your pet know a spell that removes its dispel type from an enemy (for example Purge, Dispel Magic, Tranquilizing Shot, Devour Magic)
isBossAura, isNameplateOnly, nameplateShowAll, nameplateShowPersonal, shouldConsolidate boolean always false — UI concepts not present here
auraInstanceID number a number that identifies this aura on this unit. It stays the same while the aura stays on the unit. An aura that goes off and comes back gets a new number. nil for an aura that is not in the unit's aura list (a group member out of range, or an aura that only the cast cache knows)
points (absent) omitted from the table — Lua read yields nil for "field doesn't apply"

Filter parsing

The optional filter string is a pipe-separated set of tokens ("HELPFUL", "HARMFUL", "HELPFUL|PLAYER", and more). Case is not important. The full list of tokens is in GetAuraDataByIndex. The range tokens work as follows:

  • HELPFUL (default) / HARMFUL — pick buffs or debuffs by each aura's polarity flag. In GetUnitAuras, supplying neither returns both.

Caster & timing (Aura::Source)

sourceUnit, sourceGUID, and non-player expirationTime come from a client-side cache the engine itself can't provide: the unit aura array stores only spell IDs — never the caster, and no cast/expiration timing for anyone but the local player. Aura::Source fills the gap by co-hooking three engine functions and caching (targetGuid, spellId) → { casterGuid, expirationMs, durationMs }; Push keys into it by the unit's GUID + the aura's spell ID.

  • SpellGo (FUN_SPELL_GO, 0x006E7A70) — the cast packet, the only place the client sees an aura's caster + a server-authoritative, caster-modified duration. Fills sourceUnit/sourceGUID + timing for directly-cast auras.
  • OnAuraAdded (0x006123F0) — fires when any aura first occupies a slot, including proc/triggered auras that emit no SMSG_SPELL_GO (Shadow Weaving etc.). Stamps expirationMs = now + base duration (no caster).
  • OnAuraStacksChanged (0x00612450) — fires when an existing aura's stack count changes; re-stamps expiration so a climbing stacking debuff (e.g. Shadow Weaving 1→5) refreshes.

Application hooks never overwrite an entry SpellGo already owns, so a directly-cast aura keeps its talented timing + caster regardless of hook order.

Implications:

  • Best-effort. Only auras observed after login carry data; auras already up when you logged in leave the defaults (sourceUnit/sourceGUID nil, non-player expirationTime 0).
  • Max-stack refresh is a blind spot. When a stacking debuff is refreshed at max stacks (e.g. Shadow Weaving 5→5), no client-visible aura field changes (spellID, stacks, flags, level all identical), so the server sends no UpdateField and the engine fires no callback — there is no packet to hook (verified: the stack-change dispatcher FUN_00604ea0 calls OnAuraStacksChanged unconditionally, so its silence proves nothing arrived). The countdown therefore runs to 0 and the entry evicts; once evicted, expirationTime falls back to 0 (unknown) while the aura is still shown. Climbing stacks and single-stack refreshes (re-cast → fresh SpellGo) are unaffected.
  • No nampower dependency. We parse the engine functions ourselves; nampower (which hooks the same three) need not be loaded — when it is, we co-hook the sites.
  • Entries are evicted once their timed aura elapses, so the cache stays bounded; infinite-duration auras persist until overwritten under load.

Out-of-range group members

The descriptor and the Aura::Source cache both require the member's CGUnit to exist on the client. When a party/raid member is on a different map (or otherwise far enough out of range that the engine drops their object entirely), neither is available — yet the server keeps every group member's current auras flowing in SMSG_PARTY_MEMBER_STATS (GROUP_UPDATE_FLAG_AURAS for buffs, GROUP_UPDATE_FLAG_AURAS_NEGATIVE for debuffs), and the client stashes them in the group-member roster structs. Every C_UnitAuras.* function falls back to that array when the unit has no object — the same source the built-in UnitBuff / UnitDebuff read out of range — so a cross-map GetUnitAuras("raid7") returns the member's real buffs/debuffs instead of an empty table.

This path is spell-ID only, because that is all the packet carries:

  • applications is always 1 — stack counts aren't transmitted.
  • expirationTime / sourceUnit / sourceGUID are present only if you cast the aura (the Aura::Source SMSG_SPELL_GO entry is merged in when it matches); otherwise the usual defaults.
  • duration is 0 unless the expiration time is known too.
  • Spell IDs are truncated to 16 bits on the wire, so a custom aura with a spell ID above 65535 comes through wrong — an inherent limitation of the packet that UnitBuff shares.

Distinct from the in-range descriptor-drop cases (rogue stealth, nearby range fluctuation), where the object still exists and the Aura::Source cache is the fallback — see Caster & timing.

C_UnitAuras.GetAuraDataByIndex(unit, index [, filter])

Returns the AuraData table for the index-th aura (1-based) matching filter, or nil if the unit has fewer than index matching auras. Empty / non-visible descriptor slots are skipped during iteration, so consecutive indices return consecutive active auras. The order puts your own auras first. See Aura order.

local d = C_UnitAuras.GetAuraDataByIndex("player", 1, "HELPFUL")
if d then
    print(d.name, d.spellId, d.dispelName, d.applications)
end

Filter tokens. filter uses the AuraFilters format: tokens separated by | or spaces, each token optionally negated with a leading !. Case is not important: "harmful" is the same as "HARMFUL". This build honors:

  • HELPFUL / HARMFUL — buffs / debuffs, by each aura's polarity flag rather than its slot number. With neither token the query returns both (the indexed getter defaults to helpful).
  • PLAYER / !PLAYER — only auras that you cast, or that a unit you control cast (your pet, a guardian, a totem, or a charmed creature). With !, only the other auras. Caster data comes from casts this session, so an aura present before you saw it cast has no caster and counts as not-player. A totem or guardian aura counts only while that unit is in sight.
  • RAID — for buffs, only buffs that you can cast on other players. For debuffs, only debuffs that you can dispel. This is the same test that the third argument of UnitBuff and UnitDebuff makes.
  • CANCELABLE / NOT_CANCELABLE — only auras that the player can cancel, or only auras that the player cannot cancel. Debuffs are never cancelable.
  • DISPELLABLE / !DISPELLABLE — only auras that can be dispelled, purged, or stolen (dispel type Magic, Curse, Disease, or Poison), or only auras that cannot. This is "can it be removed at all", not "can you remove it".
  • CROWD_CONTROL / !CROWD_CONTROL — only auras that are a crowd- control effect (stun, fear, silence, root, charm, confuse, disarm, and movement slows), or only auras that are not. Shares the loss-of-control classifier with C_LossOfControl, plus snares (which are crowd control but not loss of control).

All other AuraFilters tokens (INCLUDE_NAME_PLATE_ONLY, MAW, and the rest) are accepted and ignored — there is no data for them. Token matching is whole-token, so RAID_PLAYER_DISPELLABLE is not read as PLAYER.

C_UnitAuras.GetBuffDataByIndex(unit, index) / GetDebuffDataByIndex(unit, index)

Convenience wrappers locking the filter to HELPFUL or HARMFUL respectively. Equivalent to GetAuraDataByIndex(unit, index, "HELPFUL") / "HARMFUL".

C_UnitAuras.UnitAura(unit, index [, filter])

Returns the same aura as GetAuraDataByIndex, but as 15 positional values instead of a table — so it allocates nothing. Use it in an OnUpdate loop that scans many auras, where one table per aura strains Lua's garbage collector. Prefer AuraUtil.ForEachAura for iteration.

The values, in order:

name, icon, count, dispelType, duration, expirationTime, source, isStealable,
nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer,
nameplateShowAll, timeMod

Returns a single nil when the unit has fewer than index matching auras. dispelType is the dispel-type string ("Magic", "Curse", "Disease", "Poison", or nil). source is the caster's unit token, or nil when the cast was not seen this session. castByPlayer is true when a player (any player, not only you) applied the aura. It is false for an aura from a pet, and false when the cast was not seen this session. filter takes the same tokens as GetAuraDataByIndex.

The values are the GetAuraDataByIndex fields in the classic UnitAura order, with two differences from the table. Position 12 is isBossDebuff (the table field is isBossAura), and it is always false. Position 13 is castByPlayer. The table has isFromPlayerOrPlayerPet in its place, which is also true for an aura from a pet.

C_UnitAuras.UnitBuff(unit, index [, filter]) / UnitDebuff(unit, index [, filter])

The UnitAura positional form with the range locked to HELPFUL or HARMFUL. The filter still honors all the other tokens, for example PLAYER and RAID. Namespaced under C_UnitAuras so they never clash with the native global UnitBuff / UnitDebuff (which keep their texture-first return).

C_UnitAuras.GetAuraSlots(unit [, filter [, maxSlots [, continuationToken]]])

Returns a continuation token, then the slot ids of the auras on unit that match filter. A slot id is an opaque number. Pass it to GetAuraDataBySlot or UnitAuraBySlot to read that aura.

local token, slot1, slot2 = C_UnitAuras.GetAuraSlots("target", "HARMFUL", 2)

The slot ids come in the order that the by-index getters visit the auras. maxSlots limits how many ids one call returns. With nil or 0, one call returns all of them. When more auras remain, the first return value is a token. Pass this token back as continuationToken to get the next batch. When the batch reaches the last aura, the first return value is nil.

Use this function instead of a loop over GetAuraDataByIndex. The by-index getters walk the aura array from the start on every call, so a loop over them repeats that walk for each index. One GetAuraSlots call plus one by-slot fetch per aura walks the array once. AuraUtil.ForEachAura uses this path.

A slot id is valid in the frame that returned it. Do not store slot ids across frames. filter takes the same tokens as GetAuraDataByIndex. The range is HELPFUL unless the filter has HARMFUL.

Fill a table instead (ClassicAPI extension). Pass a table as the fifth argument. The function writes the slot ids into it as t[1] to t[n], clears old entries after n, sets t.n, and returns continuationToken, n. Use this form in code that runs every frame. On this client's Lua, a call to a function with ... in its parameter list allocates a table. So a Lua helper that receives the normal return list allocates once per call, and the fill form does not.

local slots = {}
local token, n = C_UnitAuras.GetAuraSlots("target", "HARMFUL", nil, nil, slots)
for i = 1, n do
    local name = C_UnitAuras.UnitAuraBySlot("target", slots[i])
end

C_UnitAuras.GetAuraDataBySlot(unit, slot) / UnitAuraBySlot(unit, slot)

GetAuraDataBySlot returns the AuraData table for the aura that slot id slot names on unit. It returns nil when the slot id no longer names an aura. UnitAuraBySlot returns the same aura as the 15 positional values of C_UnitAuras.UnitAura, so it allocates nothing. Get slot ids from GetAuraSlots.

local token, slot = C_UnitAuras.GetAuraSlots("player", "HELPFUL", 1)
if slot then
    local d = C_UnitAuras.GetAuraDataBySlot("player", slot)
    print(d.name, d.applications)
end

C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID [, buf])

Returns the AuraData table for the aura on unit that has the instance ID auraInstanceID. Returns nil when no aura on the unit has this ID. An ID stops working when its aura goes off the unit.

If you give buf, the function writes the aura into buf and returns buf. This form makes no new table. It is the correct form for a UNIT_AURA_DELTA handler that reads each changed ID. When the aura has no sourceUnit or sourceGUID, the function sets that field of buf to nil. The other keys of buf do not change.

local buf = {}
local d = C_UnitAuras.GetAuraDataByAuraInstanceID("target", id, buf)
if d then
    print(d.name, d.applications)
end

C_UnitAuras.GetUnitAuraBySpellID(unit, spellID [, filter])

Searches unit's auras for one whose spellId matches and returns its AuraData, or nil if not found. If more than one aura matches, your own aura comes first (see Aura order). Without a filter, searches both ranges (helpful first, then harmful).

-- Is the player blessed with Wisdom right now?
local d = C_UnitAuras.GetUnitAuraBySpellID("player", 25290)
if d then print("yes, with", d.applications, "stacks") end

C_UnitAuras.GetPlayerAuraBySpellID(spellID)

Shorthand for GetUnitAuraBySpellID("player", spellID) — the most common consumer pattern (WeakAuras-style aura tracking).

C_UnitAuras.GetAuraDataBySpellName(unit, spellName [, filter])

Searches unit's auras for one whose locale-resolved name matches spellName. If more than one aura matches, your own aura comes first. Case is not important: "inner fire" finds Inner Fire. Returns the AuraData for that slot or nil if not found. Without a filter, searches both ranges (helpful first, then harmful).

local d = C_UnitAuras.GetAuraDataBySpellName("player", "Inner Fire")
if d then print(d.applications, "stacks of Inner Fire") end

-- restrict to debuffs:
local poison = C_UnitAuras.GetAuraDataBySpellName(
    "target", "Mind-numbing Poison", "HARMFUL")

Uses the same name field the other C_UnitAuras.* functions populate (locale-applied Spell.dbc name). If the player is on a non-English client, pass the localized name — addons that hard-code English names should use GetUnitAuraBySpellID instead for portability.

C_UnitAuras.RegisterComboDuration(spellID, baseSeconds, maxSeconds)

ClassicAPI extension — you normally never call this. Overrides the combo-point duration data for one spellID, for servers whose values differ from (or are missing in) the client's SpellDuration.dbc. Returns true on success; re-registering a spellID replaces its entry.

Background: combo-point finishers (Rupture, Kidney Shot, Slice and Dice, …) get their real duration computed server-side at cast time as base + (max − base) × comboPoints / 5, and 1.12 never transmits the result for debuffs the player puts on other units (verified with a whole-protocol packet capture). ClassicAPI therefore mirrors the computation automatically: it snapshots your combo points the instant the cast request leaves the client and applies the same formula from the spell's own SpellDuration.dbc row, so duration/ expirationTime in every AuraData are combo-accurate out of the box — Rupture at 4 CP reads 14s, not the 6s base. Your duration talents apply on top, in the server's order. No registration involved.

The override exists only for spells whose client DBC data is missing or wrong on a specific server. The known case — Turtle's reworked Rip (8s + 2s per combo point), whose SpellDuration row exists on the server only — is built into ClassicAPI and gated on the client-data bug itself: the fallback applies only while the spell's DurationIndex points at a SpellDuration row the client doesn't have (a condition unique to Rip on Turtle's client; on a stock client Rip's index resolves normally, and if Turtle ever ships the missing row, the DBC takes over automatically). No realm detection involved. This function is the escape hatch for other custom servers with the same class of gap — call it from your own config or addon; a registered override beats both the DBC and the built-in values.

C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])

ClassicAPI extension for server-side DoT-duration changes on another unit that the client is never told about. Returns true on success.

expirationTime for a debuff on another unit is computed from the cast packet (SMSG_SPELL_GO) at cast time. When the server later changes that debuff's remaining duration — a refresh, or a partial consume — 1.12 sends no packet an observing caster can see (verified in the server source: the change runs through SetAuraDuration/RefreshHolder, and the only duration packet is self-scoped to the aura-bearer; on a mob it isn't even built). So the cached expirationTime would go stale.

The client does always see the triggering cast, though. Register a rule and ClassicAPI mirrors the server's edit on the cached aura when that trigger lands:

Arg Meaning
triggerFamily SpellFamilyName of the triggering cast (e.g. 6 priest).
triggerSchool School index the trigger must be (0 physical … 2 fire, 5 shadow, 6 arcane), or < 0 for any. Matching by family + school covers a whole class of spells at once — every rank, plus server-added ones — rather than a named ability.
affectedFamily SpellFamilyName of the affected aura (e.g. 5 warlock, 11 shaman, 6 priest).
affectedFamilyFlags A SpellFamilyFlags bitmask; the affected aura matches if it overlaps. Family plus flag is rank-proof, so it covers every rank at once. Pass 0 to select on the icon alone.
affectedIcon SpellIconID the affected aura must have, or 0 to match any. Give this when the aura has no SpellFamilyFlags, which is common for a server's custom spells. An icon is shared by a spell's whole rank ladder, so it stays rank-proof.
op "refresh" (reset to full duration), "reduce" (subtract valueSeconds, removing the aura if it would go non-positive), "set" (to valueSeconds), "remove".
valueSeconds Amount for reduce/set; ignored otherwise.

Give at least one of affectedFamilyFlags and affectedIcon. A rule with neither would match every aura of the class, so it is rejected.

The rule only fires for the aura cast by the same unit as the trigger (these mechanics act on the caster's own DoT), and misses are excluded for free (a missed trigger isn't in the packet's hit list).

Rules keyed to an exact trigger spellID (rather than a family + school category) are registered inside the DLL instead. !!!ClassicAPI's built-in Turtle mods — Conflagrate shaving 3s off the caster's Immolate, Molten Blast refreshing the caster's Flame Shock, and Bestial Wrath stretching the pet's Scent of Blood to its own length — live in src/turtle/DurationMods.cpp, and Carnage's roll-gated Rip/Rake refresh in src/turtle/Carnage.cpp.

The one rule registered from Lua (Util/AuraDurationModifiers.lua) is Shadow Weaving: for a 5/5 priest (talent 15334, where the proc is 100%), any shadow-school priest cast refreshes the target's Shadow Vulnerability (15258). Below 5/5 it's roll-gated and deliberately not inferred — the client can't see the server's roll, so inferring it would show wrong timers. One accepted best-effort caveat: DoT ticks (SW:P / Devouring Plague) also refresh it server-side but emit no cast packet, so a pure DoT-only phase under-counts slightly.

-- Flags are hex; Lua 5.0 has no 0x literals, so use tonumber(hex, 16).
-- Any priest (6) shadow-school (5) cast refreshes Shadow Vulnerability
-- (affected priest 6, flag 0x4000000, icon 9):
C_UnitAuras.RegisterAuraDurationModifierByTrigger(6, 5, 6, tonumber("4000000", 16), 9, "refresh")

C_UnitAuras.GetUnitAuras(unit [, filter])

Returns a numerically-indexed array of AuraData tables for every populated aura on unit. Without a filter the array is helpful auras followed by harmful; with a filter it's restricted to one range. Empty array ({}) if the unit has no auras or doesn't resolve.

for _, aura in ipairs(C_UnitAuras.GetUnitAuras("target")) do
    print(aura.spellId, aura.dispelName, aura.isHarmful)
end

C_UnitAuras.GetAuraDispelTypeColor(dispelName)

Returns a {r, g, b, a} table for the given dispel-type name, matching FrameXML's DebuffTypeColor values:

dispelName r g b
"Magic" 0.20 0.60 1.00
"Curse" 0.60 0.00 1.00
"Disease" 0.60 0.40 0.00
"Poison" 0.00 0.60 0.00
"Enrage" 1.00 0.55 0.00
(anything else, including "") 0.80 0 0

Returns a ColorMixin instance — the C function pcalls Lua's CreateColor(r, g, b, a) (defined in !!!ClassicAPI/Util/Color.lua) and returns whatever table it builds. So the returned value carries the mixin methods (GetRGB, GenerateHexColorMarkup, etc.) in addition to the .r/.g/.b/.a fields. Falls back to a plain {r,g,b,a} table if CreateColor isn't loaded yet (shouldn't happen — !!!ClassicAPI loads first thanks to the triple-! prefix).

VoiceChat

A text-to-speech surface — the C_VoiceChat and C_TTSSettings namespaces — backed by Windows SAPI (the OS speech engine). All playback is local (rendered on your own machine); there is no voice-chat transport, so there is nothing "remote" to transmit synthesized speech into.

Coexistence with VanillaTTS: the same surface also ships as the standalone VanillaTTS DLL, which adds a software-synth backend for Wine. If VanillaTTS.dll is loaded in the same client, this built-in module stands down automatically (registers none of the functions, events, or cvars below) and VanillaTTS provides them instead — the Lua-visible API is identical either way, so addons don't need to care which one is active.

SAPI is created lazily on first use. If SAPI is unavailable (e.g. running the client under Wine, or a stripped Windows), every call degrades gracefully: GetTtsVoices returns an empty table, SpeakText fires VOICE_CHAT_TTS_PLAYBACK_FAILED and does nothing, and the client stays stable — there is no crash. Install more voices under Windows Settings → Time & Language → Speech, then call C_TTSSettings.RefreshVoices().

C_VoiceChat.GetTtsVoices() / C_VoiceChat.GetRemoteTtsVoices()

Return a numerically-indexed array of voice tables — one per SAPI voice installed on the machine:

for _, v in ipairs(C_VoiceChat.GetTtsVoices()) do
    print(v.voiceID, v.name)   -- e.g.  0  "Microsoft David Desktop - English (United States)"
end

Each entry is { voiceID = <0-based index>, name = <display name> }. The voiceID is the index you pass to SpeakText and store via C_TTSSettings.SetVoiceOption. GetRemoteTtsVoices is an alias returning the same local list (kept for API parity — there is no voice chat). Calling either refreshes the cached voice list and fires VOICE_CHAT_TTS_VOICES_UPDATE if it changed.

C_VoiceChat.SpeakText(voiceID, text [, destination, rate, volume])

Speaks text aloud using the SAPI voice at index voiceID.

C_VoiceChat.SpeakText(
    C_TTSSettings.GetSpeechVoiceID(),         -- use the configured voice
    UnitName("target") or "no target")
  • voiceID (number, required) — index into GetTtsVoices(). Note this is an explicit argument — SpeakText does not read the ttsVoice cvar itself, matching Blizzard's contract. Pass C_TTSSettings.GetSpeechVoiceID() to honor the configured default.
  • text (string, required) — UTF-8; spoken as-is.
  • destination (number, optional, default 1) — 1 = LOCAL_PLAYBACK, 4 = QUEUED_LOCAL_PLAYBACK. Accepted for parity; both queue FIFO through SAPI (no app-side queue).
  • rate (number, optional, default 0) — speech rate, clamped to [-10, 10].
  • volume (number, optional, default 100) — clamped to [0, 100].

Speech is asynchronous. The call returns immediately and fires VOICE_CHAT_TTS_PLAYBACK_STARTED when SAPI begins the utterance and VOICE_CHAT_TTS_PLAYBACK_FINISHED when it completes (or ..._PLAYBACK_FAILED if the voice/engine is unavailable). Raises a Lua error if voiceID isn't a number or text isn't a string.

C_VoiceChat.StopSpeakingText()

Stops and purges everything currently playing or queued.

C_TTSSettings — getters & setters

The settings store (separate from SpeakText, which takes its parameters explicitly). All three values are backed by CVars (see below), so they persist across sessions and are clamped to valid ranges on write.

Function Returns / effect
C_TTSSettings.GetSpeechRate() configured rate, [-10, 10] (default 0)
C_TTSSettings.GetSpeechVolume() configured volume, [0, 100] (default 100)
C_TTSSettings.GetSpeechVoiceID() configured voice index (default 0)
C_TTSSettings.GetVoiceOptionName() display name of the configured voice, or ""
C_TTSSettings.SetSpeechRate(rate) set rate (clamped)
C_TTSSettings.SetSpeechVolume(volume) set volume (clamped)
C_TTSSettings.SetVoiceOption(voiceID) set the voice by index (clamped to installed count)
C_TTSSettings.SetVoiceOptionByName(name) set the voice by display name (case-insensitive exact match)
C_TTSSettings.SetDefaultSettings() reset to voice 1 (if present, else 0), rate 0, volume 100
C_TTSSettings.RefreshVoices() re-enumerate SAPI voices; fires VOICE_CHAT_TTS_VOICES_UPDATE if the list changed
C_TTSSettings.SetVoiceOptionByName("Microsoft Zira Desktop - English (United States)")
C_TTSSettings.SetSpeechVolume(150)              -- clamps to 100
print(C_TTSSettings.GetVoiceOptionName())       -- "Microsoft Zira Desktop - ..."

TTS events

Registered like any engine event (frame:RegisterEvent("VOICE_CHAT_TTS_PLAYBACK_STARTED")).

Event Args
VOICE_CHAT_TTS_PLAYBACK_STARTED numConsumers (number), utteranceID (number), durationMS (number, always 0), destination (number)
VOICE_CHAT_TTS_PLAYBACK_FINISHED numConsumers (number), utteranceID (number), destination (number)
VOICE_CHAT_TTS_PLAYBACK_FAILED status (string — e.g. "EngineAllocationFailed", "InternalError"), utteranceID (number), destination (number)
VOICE_CHAT_TTS_VOICES_UPDATE (none) — the installed-voice list changed
VOICE_CHAT_TTS_SPEAK_TEXT_UPDATE reserved for parity; not currently fired

utteranceID correlates the START/FINISHED/FAILED events for a given SpeakText call. PLAYBACK_FINISHED fires only when playback actually completes, so it's the signal to start the next utterance in a queue.

TTS CVars

The C_TTSSettings values are stored as engine CVars, persisted to WTF\Config.wtf. You can also read/write them with the standard GetCVar/SetCVar (or /console set); writes are clamped by the same validation:

CVar Default Range Backing setting
ttsVoice 0 [0, voiceCount-1] GetSpeechVoiceID / SetVoiceOption
ttsSpeed 0 [-10, 10] GetSpeechRate / SetSpeechRate
ttsVolume 100 [0, 100] GetSpeechVolume / SetSpeechVolume

XMLUtil

C_XMLUtil.GetTemplates()

Returns an array of every registered virtual XML template — the virtual="true" frames that inherits= targets — one entry per template:

for _, t in ipairs(C_XMLUtil.GetTemplates()) do
    print(t.name, t.type)   -- e.g.  "UIPanelButtonTemplate"  "Button"
end

Each entry is { name = <string>, type = <frameType> }, where type is the XML element tag the template was declared with ("Frame", "Button", "CheckButton", "StatusBar", …).

The list is read straight from the engine's virtual-template registry — the same store the XML loader fills for inherits= — so it covers every template currently loaded from both Blizzard FrameXML and addon .xml files, and is rebuilt on /reload. Font templates live in a separate registry and are not included (GetTemplates returns frame templates). Returns an empty table if no template has registered yet.

Note: the standard XMLTemplateListInfo carries only the name and type fields, both provided here. There's no filtering — every virtual template is returned, in hash-table order (not load or alphabetical order).

C_XMLUtil.GetTemplateInfo(name)

Returns an XMLTemplateInfo table for the named template, or nil if no template of that name is registered:

local info = C_XMLUtil.GetTemplateInfo("StatFrameTemplate")
-- { type = "Frame", width = 104, height = 13, keyValues = {}, inherits = nil }
Field Type Notes
type string The template's frame type — its XML element tag ("Frame", "Button", …).
width / height number The size statically declared on the template itself via a <Size> element, or 0 if it declares none (including when the size comes from an inherited template rather than a direct <Size>).
keyValues table The template's own <KeyValues> entries, in file order. Each entry is { key, keyType, type, value }, with the text as written in the file. keyType and type are "string" when the file leaves them out. Entries from inherited templates are not included. The table is empty when the template has no entries.
inherits string? The template's inherits= attribute (a comma-delimited list), or nil if it inherits nothing.

Lookup is by name through the engine's own template registry — the same resolution inherits= uses — so it's case-insensitive and covers both Blizzard FrameXML and addon templates.

Not provided: the sourceLocation field (file:line where the template was defined); no source location is recorded for XML nodes, so the field is omitted (reads as nil).

C_XMLUtil.DoesTemplateExist(name)

Returns true if a virtual template of that name is registered, false otherwise:

if C_XMLUtil.DoesTemplateExist("MyAddonRowTemplate") then
    button = CreateFrame("Button", nil, parent, "MyAddonRowTemplate")
end

Case-insensitive, resolved through the same registry as inherits=.

inherits with more than one template

A frame element can name a list of templates in inherits. Commas or spaces divide the names. The templates apply from left to right, so a later template overrides an earlier one. The element's own attributes and children apply last.

<Button name="MyButton" inherits="UIPanelButtonTemplate, MyButtonTemplate"/>

If a name in the list is not a template, FrameXML.log gets the line Couldn't find inherited node: <name>. The other templates still apply.

A frame element without a parent attribute gets its parent from the templates. The first template in the list that names a parent gives it.

Textures and font strings use only one template.

parentKey in XML

parentKey="Name" on a frame, texture or font string element sets parent.Name to that object, so code reaches unnamed children as self.Icon or self.Close:

<Frame name="MyFrame">
    <Layers>
        <Layer level="ARTWORK">
            <Texture parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark"/>
            <FontString parentKey="Label" inherits="GameFontNormal"/>
        </Layer>
    </Layers>
    <Frames>
        <Button parentKey="Close" inherits="UIPanelCloseButton"/>
    </Frames>
    <Scripts>
        <OnLoad>this.Label:SetText("Hello")</OnLoad>
    </Scripts>
</Frame>

Keys are set while the parent is being built, so the parent's OnLoad already sees them, and a parentKey inside a virtual template is set on every frame built from it. A button's <NormalTexture parentKey="..."> works the same way.

KeyValues in XML

A <KeyValues> element sets fields on the Lua table of the element that holds it. Each <KeyValue> inside it is the same as object[key] = value:

<Frame name="MyFrame">
    <KeyValues>
        <KeyValue key="layoutIndex" value="3" type="number"/>
        <KeyValue key="title" value="Options"/>
        <KeyValue key="tooltipFrame" value="GameTooltip" type="global"/>
    </KeyValues>
    <Scripts>
        <OnLoad>print(this.layoutIndex, this.title, this.tooltipFrame:GetName())</OnLoad>
    </Scripts>
</Frame>

Frames, textures, font strings, <AnimationGroup> and animation elements can hold a <KeyValues> element.

type sets how value is read. keyType sets how key is read. Both are string if you leave them out:

Type Result
string The text as written.
number The text as a number.
boolean true if the text starts with t, y or a digit from 1 to 9 (any case), or is on or enabled. Otherwise false.
nil nil. This type clears a field that a template set.
global The global variable with that name, _G[value]. A name with dots, such as ScrollControllerMixin.Directions.Decrease, reads one field at each dot.

Numbers keep about seven significant digits, so integers up to 16777216 are exact.

A template's entries apply first, so an element's own entries replace entries with the same key. The fields are set before OnLoad runs, and a <KeyValues> element inside a virtual template applies to every frame built from it. A global value reads the global when the element loads, so the global must exist by then.

If an entry has no key, has no value, or has an unknown type, FrameXML.log gets a warning and the entry is skipped.

mixin in XML

A mixin attribute names one or more global tables. Commas or spaces divide the names. Each table's fields are copied onto the element's Lua table, the same as Mixin(object, ...):

<Frame name="MyFrame" mixin="MyFrameMixin">
    <Scripts>
        <OnLoad method="OnLoad"/>
        <OnEvent method="OnEvent"/>
    </Scripts>
</Frame>

Frames, textures, font strings and <AnimationGroup> elements take a mixin attribute. The tables must exist when the XML loads, so define them in a Lua file that loads before the XML file.

The mixins of the templates in inherits apply first, in template order. Then the element's own mixins apply, so a later mixin replaces fields of an earlier one. All mixins apply before the element's <KeyValues> and scripts load, so a <KeyValue> replaces a default that a mixin gives. Frames from CreateFrame with a template get the template's mixins too.

If a name is not a global table, FrameXML.log gets the warning Couldn't find mixin and the other names still apply.

intrinsic frame types in XML

A top-level frame element with intrinsic="true" makes a new frame type. The element does not make a frame. Its name becomes the name of the type:

<Frame name="MyEventFrame" mixin="MyEventFrameMixin" intrinsic="true">
    <Scripts>
        <OnLoad method="OnLoad_Intrinsic"/>
        <OnShow method="OnShow_Intrinsic" intrinsicOrder="postcall"/>
    </Scripts>
</Frame>

Use the name as an XML element, or as the type in CreateFrame:

<MyEventFrame name="MyFrame" parent="UIParent"/>
local f = CreateFrame("MyEventFrame", nil, UIParent)
  • The frame has the type of the element that made the type. In the example, GetObjectType() returns "Frame".
  • The content of the type applies first. Then the templates in inherits apply, then the element's own content. The mixins of the type also apply before all other mixins.
  • The scripts of the type are its intrinsic scripts. They always run, also after the frame gets a different handler. An element with intrinsicOrder="postcall" runs after the frame's handler. All other intrinsic scripts run before it.
  • A type cannot inherit from templates. FrameXML.log gets a warning for an unnamed type, a type with inherits, and a type whose name is already a template.

ClassicAPI defines four types: EventFrame, EventButton, EventEditBox and EventScrollFrame. Each one is a CallbackRegistryMixin that sends a callback event for some of its scripts. For example, an EventFrame sends OnShow, OnHide and OnSizeChanged:

local f = CreateFrame("EventFrame", nil, UIParent)
f:RegisterCallback("OnShow", function() print("shown") end, f)

ScrollBox and scroll bars

ClassicAPI has the ScrollBox system of the later clients: scroll boxes, their views, data providers, ScrollUtil and scroll bars. A scroll box shows its frames only inside its own area.

Template Use
WowScrollBoxList A list or a grid of frames from a data provider.
WowScrollBox A scroll box with one frame that you make, such as a long text.
WowClassicScrollBar A vertical scroll bar with the client's own scroll bar art.
WowTrimScrollBar, MinimalScrollBar The same as WowClassicScrollBar.
WowTrimHorizontalScrollBar A horizontal scroll bar. It uses the art of WowClassicScrollBar, turned on its side.
ScrollingEditBoxTemplate, ScrollingFontTemplate An edit box or a font string in a scroll box.
local box = CreateFrame("Frame", nil, UIParent, "WowScrollBoxList")
box:SetSize(200, 200)
box:SetPoint("CENTER")
local bar = CreateFrame("EventFrame", nil, UIParent, "WowClassicScrollBar")
bar:SetPoint("TOPLEFT", box, "TOPRIGHT")
bar:SetPoint("BOTTOMLEFT", box, "BOTTOMRIGHT")

local view = CreateScrollBoxListLinearView()
view:SetElementExtent(20)
view:SetElementInitializer("Button", function(button, data)
    if not button.text then
        button.text = button:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        button.text:SetAllPoints()
    end
    button.text:SetText(data)
end)
ScrollUtil.InitScrollBoxListWithScrollBar(box, bar, view)
box:SetDataProvider(CreateDataProvider({"one", "two", "three"}))

The scroll bar arrows do not play a click sound.

BackdropTemplate

ClassicAPI has the BackdropTemplate template and the BackdropTemplateMixin mixin. A frame that inherits BackdropTemplate gets these methods:

Method Use
frame:SetBackdrop(backdropInfo) Sets the backdrop and keeps the table in frame.backdropInfo. If the frame already uses this table, nothing changes and the colors stay. nil, or a table without bgFile and edgeFile, removes the backdrop.
frame:ApplyBackdrop() Sets the backdrop from frame.backdropInfo. The colors change to white.
frame:ClearBackdrop() Removes the backdrop.
frame:HasBackdropInfo(backdropInfo) Returns true if the frame uses this table.
frame:GetEdgeSize() Returns the edge size of the backdrop. If the table has no edgeSize, it returns the default size.

GetBackdrop, SetBackdropColor, GetBackdropColor, SetBackdropBorderColor and GetBackdropBorderColor are the same on all frames.

When the frame loads, it applies these <KeyValues>:

Key Type Value
backdropInfo global The backdrop table, for example BACKDROP_DIALOG_32_32.
backdropColor global A color object for the background.
backdropColorAlpha number The alpha of the background.
backdropBorderColor global A color object for the border.
backdropBorderColorAlpha number The alpha of the border.
<Frame name="MyPanel" parent="UIParent" inherits="BackdropTemplate">
    <Size x="200" y="100"/>
    <Anchors><Anchor point="CENTER"/></Anchors>
    <KeyValues>
        <KeyValue key="backdropInfo" value="BACKDROP_TUTORIAL_16_16" type="global"/>
        <KeyValue key="backdropBorderColor" value="NORMAL_FONT_COLOR" type="global"/>
    </KeyValues>
</Frame>
local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
f:SetBackdrop(BACKDROP_DIALOG_32_32)

The BACKDROP_* tables are available. These tables name textures that are not in the game files, and those parts of the backdrop do not show: BACKDROP_ACHIEVEMENTS_0_64, BACKDROP_ARENA_32_32 (border), BACKDROP_DARK_DIALOG_32_32 (background), BACKDROP_WATERMARK_DIALOG_0_16, BACKDROP_TOAST_12_12, BACKDROP_CALLOUT_GLOW_0_16 and BACKDROP_CALLOUT_GLOW_0_20.

frame:SetBorderBlendMode and the backdropBorderBlendMode key are not available.

Menus and dropdowns

ClassicAPI has the menu system: Menu, MenuUtil, MenuTemplates, MenuConstants, MenuResponse, MenuVariants and the DropdownButton frame type. Menus and dropdowns use the game's own dropdown art.

These dropdown templates are available:

Template Use
WowStyle1DropdownTemplate A dropdown that shows the selected options as its text. Its menu has the dialog border.
WowStyle1ThinDropdownTemplate The same dropdown. Its menu has less space at the sides, for short options.
WowStyle1ArrowDropdownTemplate A dropdown that is only an arrow button.
WowStyle1FilterDropdownTemplate A red button with fixed text, "Filter" by default. Its menu has the tooltip border.

A menu description has these functions to add elements: CreateButton, CreateTitle, CreateCheckbox, CreateRadio, CreateDivider, CreateSpacer, CreateColorSwatch, CreateFrame and CreateTemplate. An element that has elements of its own opens them as a submenu.

local dropdown = CreateFrame("DropdownButton", nil, UIParent, "WowStyle1DropdownTemplate")
dropdown:SetPoint("CENTER")
dropdown:SetWidth(150)

local selected = 1
dropdown:SetupMenu(function(dropdown, rootDescription)
    rootDescription:CreateTitle("Size")
    local names = { "Small", "Medium", "Large" }
    for i = 1, table.getn(names) do
        rootDescription:CreateRadio(names[i],
            function(index) return selected == index end,
            function(index) selected = index end, i)
    end
end)

MenuUtil.CreateContextMenu(owner, generator) opens a menu at the cursor. The menu closes when owner is hidden. A context menu has the tooltip border.

frame:SetScript("OnMouseUp", function(self, button)
    if button == "RightButton" then
        MenuUtil.CreateContextMenu(self, function(owner, rootDescription)
            rootDescription:CreateButton("Say hello", function() print("Hello") end)
        end)
    end
end)

The ColorSwatchTemplate template is also available.

MenuUtil.CreateHighlightRadio, WowStyle2DropdownTemplate and WowStyle2IconButtonTemplate are not available.

Atlases in XML

A <Texture> element takes an atlas attribute and a useAtlasSize attribute. They are the same as texture:SetAtlas(atlas, useAtlasSize):

<Texture parentKey="Middle" atlas="UI-ClassicScrollBar-Center" useAtlasSize="true"/>

The atlas applies after the other content of the element. An atlas that ClassicAPI does not know keeps the texture that the element set.

A <Size> element, an <Anchor> or an <Offset> can give only x or only y. The value that it does not give is 0.

Masks in XML

A <Texture> element takes a mask attribute. It is the same as texture:SetMask(path):

<Texture parentKey="Portrait" file="Interface\Icons\INV_Misc_QuestionMark"
         mask="Interface\CharacterFrame\TempPortraitAlphaMask"/>

A <Layer> also takes a <MaskTexture> element. It is the same as frame:CreateMaskTexture: the mask loads like a texture (file, inherits, parentKey, <Size>, <Anchors>) and never draws itself. Each <MaskedTexture> inside <MaskedTextures> attaches the mask to one texture, the same as texture:AddMaskTexture(mask):

  • childKey="Icon" names the texture at parent.Icon, where parent is the frame that holds the layer.
  • target="$parentIcon" names a texture by its global name. $parent at the start stands for the name of that frame.
<Frame name="MyFrame">
    <Layers>
        <Layer level="ARTWORK">
            <Texture parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark"/>
            <MaskTexture file="Interface\CharacterFrame\TempPortraitAlphaMask">
                <Anchors>
                    <Anchor point="TOPLEFT" relativeTo="$parent" x="4" y="-4"/>
                    <Anchor point="BOTTOMRIGHT" relativeTo="$parent" x="-4" y="4"/>
                </Anchors>
                <MaskedTextures>
                    <MaskedTexture childKey="Icon"/>
                </MaskedTextures>
            </MaskTexture>
        </Layer>
    </Layers>
</Frame>

A <MaskedTexture> can name a texture from any layer of the same frame. If it names no texture, the load log gets a warning and the mask is not attached.

function and method on XML script handlers

A script element with function="Name" uses the global function Name as that script's handler. It is the same as frame:SetScript("OnLoad", MyFrame_OnLoad):

<Scripts>
    <OnLoad function="MyFrame_OnLoad"/>
    <OnEvent function="MyFrame_OnEvent"/>
</Scripts>

A script element with method="Name" uses the frame's own field self.Name as the handler. The field usually comes from a mixin:

<Scripts>
    <OnLoad method="OnLoad"/>
</Scripts>

Animation groups and animations take function and method the same way.

The function is looked up when the element loads, so it must be defined by then. A bound OnLoad runs at the usual point, once the frame is built, and with positional script arguments on (the default) the function gets self and the script's arguments like any other handler.

style on button font elements

A button's font element can name its font with style:

<Button name="MyButton" text="Click">
    <NormalFont style="GameFontNormal"/>
    <HighlightFont style="GameFontHighlight"/>
    <DisabledFont style="GameFontDisable"/>
</Button>

A NormalFont, HighlightFont or DisabledFont with style and no inherits gets the named font object, through the same setters as SetTextFontObject, SetHighlightFontObject and SetDisabledFontObject. An element with inherits loads as before. CheckButtons get the same.

alpha, scale and registerForDrag in XML

A <Texture> or <FontString> element takes an alpha attribute. It is the same as region:SetAlpha(alpha). The value goes from 0 to 1. Values outside this range are clamped. The alpha applies after the <Color> of the element:

<Texture parentKey="Glow" file="Interface\Buttons\UI-ActionButton-Border" alphaMode="ADD" alpha="0.5"/>

A frame element takes a scale attribute. It is the same as frame:SetScale(scale). A negative scale changes nothing. On a <Model> and the model types that come from it, scale stays the scale of the model:

<Frame name="MySmallFrame" scale="0.8"/>

A frame element takes a registerForDrag attribute. It is the same as frame:RegisterForDrag(...) with the button names in the attribute. Spaces or commas separate the names:

<Frame name="MyMovableFrame" movable="true" enableMouse="true" registerForDrag="LeftButton"/>