UnitClassBase and UnitRaceBase now follow stock Script_UnitClass (0x00518350) and Script_UnitRace (0x00518200) branch for branch. "player" reads the selected-character record; any other token goes through GetGUIDFromName + ClntObjMgrObjectPtr(TYPEMASK_UNIT) and reads the held object's descriptor byte (+0x79 class, +0x78 race). The NameCache record (+0x140 / +0x138, null callback) is peeked only when no object is held. Before, the NameCache was also peeked when a held object's byte was 0. That never changed a result (creatures are not in the NameCache) but resolved the token twice and did not match the engine's branch. The comments and API.md also claimed creatures return nil, that class IDs run 1-9, and that UnitClass has a third return. None of that is true: creatures go through the same ChrClasses/ChrRaces lookup as in UnitClass/UnitRace, Druid is 11, and stock returns exactly two values.
1.1 MiB
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
-
GetActionInfo(slot)IsItemAction(slot)HasBonusActionBar()C_ActionBar.ShouldShowKeyring()C_ActionBar.EnableActionRangeCheck(slot, enable)C_ActionBar.GetSpell(actionID)C_ActionBar.GetItemActionOnEquipSpellID(actionID)IsStackableAction(slot)/C_ActionBar.IsStackableAction(actionID)C_ActionBar.IsMacroActionWithShowTooltip(actionID)C_ActionBar.FindSpellActionButtons(spell [, setToSearch])C_ActionBar.HasSpellActionButtons(spell [, setToSearch])C_ActionBar.IsOnBarOrSpecialBar(spell [, setToSearch])Enum.ActionBarSet
-
C_AddOns.GetAddOnName(indexOrName)C_AddOns.GetAddOnTitle(indexOrName)C_AddOns.GetAddOnNotes(indexOrName)C_AddOns.IsAddOnLoadable(indexOrName)C_AddOns.IsAddOnLoaded(indexOrName)C_AddOns.LoadAddOn(indexOrName)C_AddOns.GetAddOnSecurity(indexOrName)C_AddOns.DoesAddOnExist(indexOrName)C_AddOns.GetAddOnOptionalDependencies(indexOrName)C_AddOns.GetAddOnInterfaceVersion(indexOrName)C_AddOns.GetAddOnLocalTable(name)- Conditional and multi-flavor TOC loading
- SavedVariables loaded first
<Script file>paths in XML files
-
C_AutoComplete.GetAutoCompleteResults(name, numResults, cursorPosition, allowFullMatch, includeFlags, excludeFlags)C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags)C_AutoComplete.GetAutoCompletePresenceID(name)/C_AutoComplete.GetAutoCompleteRealms()Enum.AutoCompleteEntryFlagEnum.AutoCompletePriority- Name completion settings
-
C_ColorUtil.ConvertRGBToHSV(r, g, b)C_ColorUtil.ConvertHSVToRGB(h, s, v)C_ColorUtil.ConvertHSVToHSL(h, s, v)C_ColorUtil.ConvertHSLToHSV(h, s, l)C_ColorUtil.ConvertHSLToRGB(h, s, l)C_ColorUtil.GenerateTextColorCode(color)C_ColorUtil.WrapTextInColor(text, color)C_ColorUtil.WrapTextInColorCode(text, colorCode)
-
ConsoleExec(command)ConsoleAddMessage(message)ConsoleEcho(message [, colorType])ConsolePrintAllMatchingCommands(prefix)ConsoleIsActive()ConsoleGetColorFromType(colorType)ConsoleGetFontHeight()SetConsoleKey(keyCode)CalculateStringEditDistance(a, b)ConsoleGetAllCommands()ExportInterfaceFiles art|code(console command)ExportDBCFiles(console command)ExportSoundFiles [subpath](console command)pitchLimit degrees(console command)AppendLogToFile [filename] [numLines](console command)- Tab completion in the console
-
C_Container.GetContainerItemID(bagIndex, slotIndex)C_Container.GetContainerItemInfo(containerIndex, slotIndex [, buf])C_Container.SetItemSearch(searchString)C_Container.IsContainerFiltered(containerIndex)C_Container.HasContainerItem(bagIndex, slotIndex)GetItemCooldown(itemInfo)/C_Container.GetItemCooldown(itemID)C_Container.GetContainerItemDurability(containerIndex, slotIndex)C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)C_Container.GetContainerItemCharges(containerIndex, slotIndex)C_Container.GetContainerNumFreeSlots(bagID)C_Container.GetContainerFreeSlots(bagID)C_Container.GetContainerItemQuestInfo(containerIndex, slotIndex)C_Container.GetContainerItemEquipmentSetInfo(containerIndex, slotIndex)C_Container.CalculateTotalNumberOfFreeBagSlots()C_Container.IsContainerItemOpenable(containerIndex, slotIndex)C_Container.PlayerHasHearthstone()C_Container.UseHearthstone()C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)C_Container.MoveItem(srcBag, srcSlot, dstBag, dstSlot, count)C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])C_Container.SortBags()C_Container.SortBankBags()C_Container.GetSortBagsRightToLeft()/C_Container.SetSortBagsRightToLeft(enable)C_Container.GetBackpackAutosortDisabled()/C_Container.SetBackpackAutosortDisabled(disable)C_Container.GetBankAutosortDisabled()/C_Container.SetBankAutosortDisabled(disable)SetBagPortraitTexture(texture, bagIndex)/C_Container.SetBagPortraitTexture(texture, bagIndex)C_Container.GetContainerNumSlots(containerIndex)/C_Container.PickupContainerItem(containerIndex, slotIndex)ContainerFrame_GetContainerNumSlots(bagID)/NUM_TOTAL_BAG_FRAMES
-
C_CreatureInfo.GetCreatureID(guid)C_CreatureInfo.GetCreatureInfoByID(creatureID)C_CreatureInfo.RequestLoadCreatureByID(creatureID)C_CreatureInfo.GetRaceInfo(raceID)C_CreatureInfo.GetClassInfo(classID)C_CreatureInfo.GetCreatureFamilyInfo(creatureFamilyID)C_CreatureInfo.GetCreatureFamilyIDs()C_CreatureInfo.GetFactionInfo(raceID)C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)C_CreatureInfo.GetCreatureTypeIDs()
-
GetCVar(name)GetCVarDefault(name)C_CVar.SetCVar(name [, value])C_CVar.GetCVarInfo(name)C_CVar.DoesCVarExist(name)C_CVar.AreCVarsLoaded()C_CVar.GetCVarBitfield(name, index)/C_CVar.SetCVarBitfield(name, index, value)C_CVar.SetTempCVar(name, value)/C_CVar.RemoveTempCVar(name)C_CVar.GetCVarBool(cvar)- The interface memory limit
-
- Overview & file format
C_EquipmentSet.CanUseEquipmentSets()C_EquipmentSet.GetNumEquipmentSets()C_EquipmentSet.GetEquipmentSetIDs()C_EquipmentSet.GetEquipmentSetID(name)C_EquipmentSet.GetEquipmentSetInfo(setID)C_EquipmentSet.GetIgnoredSlots(setID)C_EquipmentSet.GetItemIDs(setID)C_EquipmentSet.GetItemLocations(setID)C_EquipmentSet.CreateEquipmentSet(name [, icon])C_EquipmentSet.SaveEquipmentSet(setID [, icon])C_EquipmentSet.ModifyEquipmentSet(setID, newName)C_EquipmentSet.DeleteEquipmentSet(setID)C_EquipmentSet.IgnoreSlotForSave(slot)/UnignoreSlotForSave/IsSlotIgnoredForSave/ClearIgnoredSlotsForSaveC_EquipmentSet.EquipmentSetContainsLockedItems(setID)C_EquipmentSet.UseEquipmentSet(setID)C_EquipmentSet.PickupEquipmentSet(setID)
-
C_EventUtils.IsEventValid(eventName)GetFramesRegisteredForEvent(event)PLAYER_ENTERING_WORLDpayload (isInitialLogin,isReloadingUi)CHAT_MSG_*argumentslineID(arg11) andguid(arg12)PLAYER_TOTEM_UPDATEeventBAG_UPDATE_DELAYEDeventPLAYER_EQUIPMENT_CHANGEDeventUPDATE_INVENTORY_DURABILITYeventPLAYER_AVG_ITEM_LEVEL_READYeventHEARTHSTONE_BOUNDeventDYNAMIC_GOSSIP_POI_UPDATEDevent- Player input-state events (
PLAYER_STARTED_MOVING/LOOKING/TURNING+STOPPED_*) GLOBAL_MOUSE_DOWN/GLOBAL_MOUSE_UPeventsAUCTION_MULTISELL_START/AUCTION_MULTISELL_UPDATE/AUCTION_MULTISELL_FAILUREeventsACTION_RANGE_CHECK_UPDATEeventACTION_USABLE_CHANGEDeventCOMBAT_LOG_EVENT_UNFILTERED/COMBAT_LOG_EVENTeventsCURSOR_CHANGEDeventEQUIPMENT_SETS_CHANGEDeventEQUIPMENT_SWAP_PENDINGeventEQUIPMENT_SWAP_FINISHEDeventWEAR_EQUIPMENT_SETeventFACTION_STANDING_CHANGEDeventINCOMING_RESURRECT_CHANGEDeventINVENTORY_SEARCH_UPDATEeventLOOT_HISTORY_ROLL_CHANGED/LOOT_HISTORY_ROLL_COMPLETE/LOOT_HISTORY_FULL_UPDATEeventsLEARNED_SPELL_IN_SKILL_LINEeventLOOT_SCAN_COMPLETEDeventLOSS_OF_CONTROL_ADDED/LOSS_OF_CONTROL_UPDATEeventsMAP_EXPLORATION_UPDATEDeventMODIFIER_STATE_CHANGEDeventNAME_PLATE_CREATED/NAME_PLATE_UNIT_ADDED/NAME_PLATE_UNIT_REMOVEDeventsPLAYER_FOCUS_CHANGEDeventPLAYER_MOUNT_DISPLAY_CHANGEDeventPLAYER_SOFT_ENEMY_CHANGED/PLAYER_SOFT_FRIEND_CHANGEDeventsPLAYER_SOFT_INTERACT_CHANGEDeventPLAYER_SWINGeventPLAYER_SWING_RANGE_UPDATEeventQUEST_ACCEPTEDeventQUEST_QUERY_COMPLETEeventQUEST_REMOVEDeventQUEST_TURNED_INeventSELF_RES_SPELL_CHANGEDeventSOUNDKIT_FINISHEDeventSPELL_ACTIVATION_OVERLAY_SHOW/SPELL_ACTIVATION_OVERLAY_HIDEeventsSPELL_ACTIVATION_OVERLAY_GLOW_SHOW/SPELL_ACTIVATION_OVERLAY_GLOW_HIDEeventsUI_SCALE_CHANGEDeventUNIT_AREA_CHANGEDeventUNIT_AURA_DELTAeventUNIT_CONNECTIONeventUNIT_DISTANCE_CHECK_UPDATEeventUNIT_FACTIONevent (fire-coverage fix)UNIT_IN_RANGE_UPDATEeventUNIT_POWER_UPDATE/UNIT_POWER_FREQUENT/UNIT_MAXPOWEReventsUNIT_SPELL_HASTEeventUNIT_TARGETeventUNIT_TARGETABLE_CHANGEDeventUPDATE_MOUSEOVER_UNITevent (loss-fire fix)UPDATE_SHAPESHIFT_FORMeventUNIT_SPELLCAST_*eventsUSER_WAYPOINT_UPDATEDeventWEAPON_SLOT_CHANGEDevent
-
GetFactionIDByIndex(factionIndex)GetFactionInfoByID(factionID)GetFactionParentID(factionID)C_Reputation.GetFactionStandings()C_Reputation.GetWatchedFactionData()C_Reputation.GetFactionDataByID(factionID)C_Reputation.GetFactionDataByIndex(factionSortIndex)C_Reputation.SetSelectedFactionByID(factionID)C_Reputation.SetWatchedFactionByID(factionID)C_Reputation.ToggleFactionAtWarByID(factionID)C_Reputation.IsFactionActive(factionSortIndex)/C_Reputation.IsFactionActiveByID(factionID)C_Reputation.SetFactionInactiveByID(factionID)/C_Reputation.SetFactionActiveByID(factionID)C_Reputation.GetLastStandingChange()ExpandAllFactionHeaders()/CollapseAllFactionHeaders()
-
region:SetPoint("point")(one-argument form)region:SetSize(width, height)/region:GetSize([ignoreRect])region:GetWidth([ignoreRect])/region:GetHeight([ignoreRect])region:GetUnscaledWidth([ignoreRect])/region:GetUnscaledHeight([ignoreRect])/region:GetUnscaledSize([ignoreRect])region:GetScale()/region:GetEffectiveScale()region:IsMouseOver([topOffset, bottomOffset, leftOffset, rightOffset])region:GetRect()region:GetScaledRect()region:IsRectValid()region:GetPoint([index])region:GetPointByName(point)region:ClearPoint(point)region:AdjustPointsOffset(x, y)/region:ClearPointsOffset()region:IsDragging()region:GetDebugName([preferParentKey])region:GetSourceLocation()region:SetToDefaults()AddSourceLocationExclude(fileName)GetMouseFoci()CreateFrame(frameType [, name, parent, template, id])CreateFrame("Cooldown" [, name, parent, template])frame:SetShown(shown)- A frame moved to a new parent takes its children along
- A ScrollFrame keeps clipping its content when it moves
fontstring:GetStringHeight()fontstring:GetUnboundedStringWidth()fontstring:SetTextToFit([text])fontstring:GetWrappedWidth()fontstring:GetNumLines()fontstring:GetLineHeight()fontstring:GetFontHeight([calculated])/fontstring:SetFontHeight(height)fontstring:IsTruncated()fontstring:SetMaxLines(maxLines)/fontstring:GetMaxLines()fontstring:SetIndentedWordWrap([indent])/fontstring:GetIndentedWordWrap()fontstring:SetWordWrap([wrap])/fontstring:CanWordWrap()fontstring:GetFieldSize()fontstring:SetFormattedText(format [, ...])/button:SetFormattedText(format [, ...])- The
|4plural code texture:SetRotation(angle [, cx, cy])texture:SetVertexOffset(vertexIndex, offsetX, offsetY)texture:SetMask(path)frame:CreateMaskTexture([name, layer, ...])texture:AddMaskTexture(mask)texture:RemoveMaskTexture([mask])texture:GetNumMaskTextures()texture:GetMaskTexture(index)texture:SetColorTexture(colorR, colorG, colorB [, a])texture:SetAtlas(atlas [, useAtlasSize])texture:GetAtlas()texture:ResetTexCoord()texture:SetSpriteSheetCell(cell, numRows, numColumns)texture:SetTexture(file [, wrapModeHorizontal, wrapModeVertical])texture:SetHorizTile(tiling)/texture:SetVertTile(tiling)texture:SetDesaturation(amount)texture:GetDesaturation()IsDesaturateSupported()- Texture size and shape
fontstring:SetRotation(angle [, cx, cy])editBox:SetCursorPosition(position)editBox:GetCursorPosition()editBox:GetUTF8CursorPosition()editBox:ClearHighlightText()editBox:HasFocus()GetCurrentKeyBoardFocus()editBox:HasText()editBox:SetHighlightColor(r, g, b [, a])editBox:GetHighlightColor()editBox:SetCountInvisibleLetters(count)/editBox:IsCountInvisibleLetters()editBox:ClearHistory()editBox:Enable()/editBox:Disable()/editBox:IsEnabled()slider:Enable()/slider:Disable()/slider:IsEnabled()OnEnableandOnDisable(scripts)OnMinMaxChanged(self, min, max)(script)statusBar:SetReverseFill(isReverseFill)/statusBar:GetReverseFill()statusBar:SetRotatesTexture(rotatesTexture)/statusBar:GetRotatesTexture()statusBar:SetFillStyle(fillStyle)/statusBar:GetFillStyle()statusBar:SetTimerDuration(duration [, interpolation [, direction]])/statusBar:GetTimerDuration()statusBar:SetValue(value [, interpolation])/statusBar:SetMinMaxValues(min, max [, interpolation])statusBar:GetInterpolatedValue()/statusBar:IsInterpolating()/statusBar:SetToTargetValue()statusBar:SetColorFill(colorR, colorG, colorB [, a])statusBar:SetStatusBarDesaturation(desaturation)/statusBar:GetStatusBarDesaturation()editBox:IsInIMECompositionMode()OnCharComposition(self, text)(script)OnTextChanged(self, userInput)(script)- Edit box caret fixes
frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])frame:SetClampRectInsets(left, right, top, bottom)/frame:GetClampRectInsets()button:SetNormalFontObject(font)/button:GetNormalFontObject()button:SetMotionScriptsWhileDisabled(enabled)/button:GetMotionScriptsWhileDisabled()button:SetEnabled(enabled)region:IsProtected()/region:CanChangeProtectedState()frame:CanChangeAttribute()/frame:AllowAttributeChanges()frame:SetPropagateKeyboardInput([propagate])/frame:GetPropagateKeyboardInput()frame:SetPropagateMouseMotion(propagate)/frame:CanPropagateMouseMotion()frame:IsMouseMotionFocus()frame:SetMouseClickEnabled(enabled)/frame:SetMouseMotionEnabled(enabled)frame:SetPropagateMouseClicks(propagate)/frame:CanPropagateMouseClicks()frame:SetDontSavePosition([dontSave])/frame:GetDontSavePosition()frame:SetUsingParentLevel(usingParentLevel)/frame:IsUsingParentLevel()frame:SetClipsChildren(clipsChildren)/frame:DoesClipChildren()frame:SetAlphaGradient(index, gradient)/frame:ClearAlphaGradient()/frame:HasAlphaGradient()frame:DesaturateHierarchy(desaturation [, excludeRoot])frame:GetBoundsRect()frame:HookScript(scriptType, handler)- Script binding types
smf:SetHyperlinksEnabled(enabled)/smf:GetHyperlinksEnabled()smf:GetMessageInfo(index)smf:BackFillMessage(text, r, g, b, ...)smf:RemoveMessagesByPredicate(predicate)smf:SetScrollOffset(offset)/smf:GetScrollOffset()/smf:GetMaxScrollRange()/smf:ScrollByAmount(amount)smf:SetInsertMode(mode)/smf:GetInsertMode()smf:AddOnDisplayRefreshedCallback(callback)frame:IsEventRegistered(event)frame:RegisterUnitEvent(event, ...units)frame:GetEffectiveAlpha()- Frame alpha multiplies into child frames
OnSizeChangedsizes are in frame unitsframe:SetAttribute/SetAttributeNoHandler/ClearAttribute/GetAttribute(+ unit-frame mouseover)SetModernScriptArgs(enable)/GetModernScriptArgs()- XML script bodies see
selfand their arguments SecureCmdOptionParse(options [, quiet])RegisterStateDriver/UnregisterStateDriverRegisterAttributeDriver/UnregisterAttributeDriverRegisterUnitWatch/UnregisterUnitWatch/UnitWatchRegisteredSecureButton_GetAttribute/SecureButton_GetUnitPreClick/PostClickbutton scriptsButton:RegisterForClicks("AnyUp" | "AnyDown" | ...)button:RegisterForMouse(...)GetClickFrame(name)
-
C_FriendList.SendWhoQueryByName(name)C_FriendList.IsWhoQueryPending()C_FriendList.GetNumWhoResults()C_FriendList.GetWhoInfo(index)C_FriendList.IsFriend(guid)C_FriendList.IsIgnored(token)C_FriendList.IsIgnoredByGuid(guid)C_FriendList.GetNumFriends()C_FriendList.GetNumOnlineFriends()C_FriendList.GetFriendInfo(name)C_FriendList.GetFriendInfoByIndex(index)C_FriendList.SetFriendNotes(name, notes)C_FriendList.SetFriendNotesByIndex(index, notes)AddOrRemoveFriend(name[, notes])
-
GameTooltip:AddTexture(texture)GameTooltip:SetSpellByID(spellID [, isPet [, showSubtext]])GameTooltip:AddSpellByID(spellID [, isPet [, showSubtext]])GameTooltip:SetTalentByID(talentID)GameTooltip:SetInventoryItemByID(itemID)GameTooltip:SetHyperlink(link)GameTooltip:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])GameTooltip:IsEquippedItem()GameTooltip:SetFrameStack([showHidden, showRegions, highlightIndexChanged])OnTooltipSet*scripts (Item / Spell / Unit / GameObject / FrameStack)GameTooltip:SetItemByGUID(itemGUID)GameTooltip:SetEquipmentSet(name)- Equipment sets on item tooltips
GameTooltip:SetTotem(slot)GameTooltip:GetItem()GameTooltip:GetSpell()GameTooltip:HasItem()/GameTooltip:HasSpell()GameTooltip:GetUnit()/GameTooltip:GetUnitGUID()/GameTooltip:HasUnit()GameTooltip:GetGameObject()/GameTooltip:HasGameObject()GameTooltip:GetOwner()GameTooltip:SetAnchorType(anchorType [, xOffset [, yOffset]])ANCHOR_TOPandANCHOR_BOTTOMGameTooltip:GetMinimumWidth()/GameTooltip:GetPadding()
-
C_GossipInfo.GetText()C_GossipInfo.GetOptions()C_GossipInfo.GetAvailableQuests()C_GossipInfo.GetActiveQuests()C_GossipInfo.GetNumOptions()/GetNumAvailableQuests()/GetNumActiveQuests()C_GossipInfo.SelectOption(gossipOptionID)/SelectOptionByIndex(orderIndex)C_GossipInfo.SelectAvailableQuest(questID)C_GossipInfo.SelectActiveQuest(questID)C_GossipInfo.CloseGossip()C_GossipInfo.GetPoiForUiMapID(uiMapID)C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)GetNumGossipAvailableQuests()/GetNumGossipActiveQuests()GetNumGossipOptions()
-
C_Item.DoesItemExist(itemLocation)/C_Item.DoesItemExistByID(item)C_Item.EquipItemByName(item [, dstSlot])C_Item.GetCurrentItemLevel(itemLocation)/C_Item.GetDetailedItemLevelInfo(item)C_Item.GetItemCount(itemInfo, [includeBank], [includeUses])C_Item.GetItemData(itemLocation)/C_Item.GetItemDataByID(item)C_Item.GetItemFamily(item)C_Item.GetItemGUID(itemLocation)C_Item.GetItemID(itemLocation)C_Item.GetItemIDForItemInfo(itemInfo)C_Item.GetItemInfo(itemInfo)C_Item.GetItemInfoInstant(item)C_Item.GetItemInventorySlotInfo(inventorySlot)C_Item.GetItemInventorySlotKey(inventorySlot)C_Item.GetItemInventoryType(itemLocation)/C_Item.GetItemInventoryTypeByID(item)C_Item.GetItemLink(itemLocation)C_Item.GetItemLocation(itemGUID)C_Item.GetItemMaxStackSize(itemLocation)/C_Item.GetItemMaxStackSizeByID(item)C_Item.GetItemName(itemLocation)/C_Item.GetItemNameByID(item)C_Item.GetItemQuality(itemLocation)/C_Item.GetItemQualityByID(item)C_Item.GetItemSellPrice(itemLocation)/C_Item.GetItemSellPriceByID(item)C_Item.GetItemSetID(itemLocation)/C_Item.GetItemSetIDByID(item)C_Item.GetItemSetInfo(setID)C_Item.GetItemSpell(item)C_Item.GetItemStatDelta(itemLink1, itemLink2 [, statTable])C_Item.GetItemStats(itemLink [, statTable])GetItemClassInfo(classID)GetItemSubClassInfo(classID, subClassID)/C_Item.GetItemSubClassInfo(classID, subClassID)C_Item.GetItemUniqueness(itemLocation)/C_Item.GetItemUniquenessByID(item)C_Item.GetStackCount(itemLocation)C_Item.GetWeaponEnchantInfo()C_Item.GetItemTempEnchantInfo(itemLocation)C_Item.GetEnchantInfo(enchantID)C_Item.IsBound(itemLocation)IsConsumableItem(item)/C_Item.IsConsumableItem(item)IsEquippableItem(item)/C_Item.IsEquippableItem(item)IsUsableItem(item)/C_Item.IsUsableItem(item)IsEquippedItemType(type)IsCurrentItem(item)IsDressableItem(item)IsHelpfulItem(item)/IsHarmfulItem(item)C_Item.IsEquippedItem(item)C_Item.IsItemDataCachedByID(item)/C_Item.IsItemDataCached(itemLocation)C_Item.IsItemGUIDInInventory(itemGUID)C_Item.ItemHasRange(item)/ItemHasRange(item)C_Item.IsItemInRange(item, targetUnit)C_Item.IsItemOpenable(itemLocation)C_Item.IsLocked(itemLocation)C_Item.LockItem(itemLocation)C_Item.LockItemByGUID(itemGUID)C_Item.PickupItem(itemInfo)C_Item.RequestLoadItemDataByID(item)/C_Item.RequestLoadItemData(itemLocation)C_Item.UnlockAllItems()C_Item.UnlockItem(itemLocation)C_Item.UseAtCursor(item)C_Item.UseAtUnit(item, unit [, placeGroundSpell])C_Item.UseItemByName(item [, unit])Get*ItemID— companions to the engine'sGet*ItemLinkfamilyGetAverageItemLevel()GetInventoryItemDurability(invSlot)GetInventoryItemID(unit, slot)GetInventoryItemRepairCost(invSlot)GetInventoryItemsForSlot(slot, returnTable [, transmogrify])SetInventoryPortraitTexture(texture, unit, slot)GetItemIcon(itemID)/C_Item.GetItemIcon(itemLocation)/C_Item.GetItemIconByID(item)IsDualWielding()Itemobjects
-
ItemUtil.GetItemDetails(itemLink, quantity)/ItemUtil.GetItemHyperlink(itemID)ItemUtil.PickupBagItem(itemLocation)ItemUtil.IterateBagSlots(bag, callback)/ItemUtil.IterateInventorySlots(firstSlot, lastSlot, callback)ItemUtil.IteratePlayerInventory(callback)/ItemUtil.IteratePlayerInventoryAndEquipment(callback)ItemUtil.FilterOwnedItems(itemIDs)ItemUtil.TransformItemIDsToItems(itemIDs)and related functionsItemUtil.GetValidatedItemLocation(equipSlot)/ItemUtil.GetEquipSlotTexture(equipSlot)ItemUtil.GetEmptyEquipSlotTooltip(equipSlot)/ItemUtil.GetEmptyEquipSlotTooltipForSlotName(slotName)ItemUtil.DisplayEquipSlotTooltip(frame, tooltip, equipSlot, suppressComparison, checkRelic)
-
- Lua 5.1 syntax
- Upvalue limit
forloop variables per pass- String methods (
s:upper(),s:format(...)) getfenv/setfenvenvironment protectionselect(index, ...)unpack(list [, i [, j]])newproxy([proxy])scrub(...)difftime(time2 [, time1])xpcall(f, msgh [, arg1, ...])GetCallstackHeight()/GetErrorCallstackHeight()/SetErrorCallstackHeight([height])debugstack(thread [, start [, count1 [, count2]]])debuglocals([level [, skipFunctionsAndUserdata]])collectgarbage(opt [, arg])table.wipe(t)/wipe(t)table.count(tbl)table.maxn(t)table.removemulti(t [, pos [, count]])- Stale table lengths (5.1 healing)
Mixin(object, ...)/CreateFromMixins(...)string.match/string.gmatchstring.gsubreplacementsstrsplit(sep, str [, pieces])strjoin(delimiter, ...)strtrim(str [, chars])strreplace(str, find, replace)string.reverse(s)/strrev(s)string.byte(s [, i [, j]])/strbytestrlenutf8(s)strcmputf8i(string1, string2)strsplittable(sep, str [, pieces])strconcat(...)/string.concat(...)math.fmod(x, y)math.modf(x)math.hugemath.cosh(x)/math.sinh(x)/math.tanh(x)coroutine.create(fn)coroutine.resume(co, ...)coroutine.yield(...)coroutine.status(co)coroutine.wrap(fn)coroutine.running()- Async pattern (RunAsync + C_Timer.After)
-
/castand/use/castsequence/castrandomand/userandom- More commands with
[conditions] #showtooltipand#show- Numeric spellIDs in
/castandCastSpellByName CastSpellNoToggleas a macro cast lineStopMacro()RunMacroText(text [, button])GetRunningMacro()GetRunningMacroButton()- Macro names in
GetMacroInfo/EditMacro/PickupMacro/DeleteMacro GetMacroBody(macro)GetMacroSpell(macroSlot)GetMacroItem(macroSlot)SetMacroSpell(macro, spell [, target])/SetMacroItem(macro, item [, target])GetMacroIcons/GetMacroItemIcons/GetLooseMacroIcons/GetLooseMacroItemIconsC_Macro.CreateMacro/C_Macro.EditMacroC_Macro.GetMacroIcon(macroSlot)C_Macro.SetMacroDisplay(macroSlot, value [, unit])
-
- The closed world map follows the player's zone
C_Map.CanSetUserWaypointOnMap(uiMapID)C_Map.GetAreaInfo(areaID)C_Map.GetAreas()C_Map.GetAreaTriggerInfo(triggerID)/C_Map.GetAreaTriggers([mapID])C_Map.GetBestMapForUnit(unitToken)C_Map.GetFallbackWorldMapID()C_Map.GetMapAreaIDs()C_Map.GetMapArtLayers(uiMapID)C_Map.GetMapArtLayerTextures(uiMapID, layerIndex)C_Map.GetMapChildrenInfo(uiMapID)C_Map.GetMapInfo(uiMapID)C_Map.GetMapInfoAtPosition(uiMapID, x, y)C_Map.GetMapOverlays([areaID])C_Map.GetMapPosFromWorldPos(continentID, worldPosition[, overrideUiMapID])C_Map.GetMapRectOnMap(uiMapID, topUiMapID)C_Map.GetMapWorldSize([areaID])C_Map.GetPlayerMapPosition(uiMapID, unitToken)C_Map.GetUserWaypoint()/C_Map.HasUserWaypoint()/C_Map.ClearUserWaypoint()C_Map.GetUserWaypointHyperlink()/C_Map.GetUserWaypointFromHyperlink(hyperlink)C_Map.GetUserWaypointPositionForMap(uiMapID)C_Map.GetWorldPosFromMapPos(uiMapID, mapPosition)C_Map.MapHasArt(uiMapID)C_Map.SetUserWaypoint(uiMapPoint)
-
model:SetDisplayInfo(displayID)model:SetCreature(creatureID [, displayID])model:SetItem(itemID [, appearanceModID, itemVisualID])model:SetPortraitZoom(zoom)model:SetCamDistanceScale(scale)model:RefreshCamera()model:SetRotation(rotation [, animate])model:SetUnit(unit)model:CanSetUnit(unit)model:GetDisplayInfo()model:SetAnimation(animation [, variation])model:FreezeAnimation(animation, variation, frame)model:HasAnimation(animation)model:SetDoBlend([doBlend])model:GetDoBlend()model:SetKeepModelOnHide(keepModelOnHide)model:GetKeepModelOnHide()model:ApplySpellVisualKit(spellVisualKitID [, oneShot])DressUpModel:UndressSlot(slot)DressUpModel:TryOn(item [, handSlotName, spellEnchantID])DressUpModel:IsGeoReady()DressUpModel:SetSheathed([sheathed, hideWeapons])DressUpModel:GetSheathed()DressUpModel:SetAutoDress([enabled])DressUpModel:GetAutoDress()Model:SetGlow(value)
-
UpdateAddOnMemoryUsage()/GetAddOnMemoryUsage(index or "name")scriptProfileGetFunctionCPUUsage(func [, includeSubroutines])UpdateAddOnCPUUsage()/GetAddOnCPUUsage(index or "name")GetFrameCPUUsage(frame [, includeChildren])GetEventCPUUsage([event])GetScriptCPUUsage()ResetCPUUsage()_classicapi_LuaHeapAudit()/_classicapi_GcTagOf(value)
-
C_QuestLog.GetInfo(questLogIndex)C_QuestLog.GetQuestLogTitle(questLogIndex)C_QuestLog.GetQuestIDForLogIndex(index)C_QuestLog.GetLogIndexForQuestID(questID)C_QuestLog.GetHeaderIndexForQuest(questID)C_QuestLog.RequestLoadQuestByID(questID)C_QuestLog.IsOnQuest(questID)C_QuestLog.IsUnitOnQuest(unit, questID)C_QuestLog.GetTitleForQuestID(questID)C_QuestLog.GetQuestDetails(questID)C_QuestLog.GetNumQuestObjectives(questID)C_QuestLog.IsQuestDataCachedByID(questID)GetQuestLogLeaderBoardID(objectiveIndex [, questIndex])GetQuestLogCompletionText([questLogIndex])- Completed quests
QueryQuestsCompleted()GetQuestsCompleted([questTable])C_QuestLog.IsQuestFlaggedCompleted(questID)C_QuestLog.GetAllCompletedQuestIDs()- A turn-in always prints "quest completed."
-
PlaySound(soundKitID)/PlaySound(soundName)C_Sound.PlaySound(soundKitID [, channel, forceNoDuplicates, runFinishCallback])C_Sound.PlaySoundWithOptions(params)C_Sound.GetSoundScaledVolume(soundHandle)C_Sound.IsPlaying(soundHandle)StopSound(soundHandle [, fadeoutTime])PlaySoundFile(file [, channel])C_Sound.PlayItemSound(soundType, item)C_Sound.PlayVocalErrorSound(vocalErrorSoundID)MuteSoundFile(file)/UnmuteSoundFile(file)C_Sound.GetRecentSoundFiles()
-
C_Spell.DoesSpellExist(spellID)C_Spell.IsSpellDataCached(spellIdentifier)C_Spell.RequestLoadSpellData(spellIdentifier)C_Spell.GetSchoolString(schoolMask)GetSpellInfo(spellID)/GetSpellInfo(slot, bookType)C_Spell.GetSpellInfo(spellID)C_Spell.GetSpellName(spellID)C_Spell.GetSpellTexture(spellID)GetSpellLink(spellID)/GetSpellLink(slot, bookType)C_Spell.GetSpellLink(spellID)C_Spell.GetSpellDescription(spellID)C_Spell.GetSpellDescriptionForItemLocation(spellIdentifier, itemLocation)C_Spell.GetSpellMechanicByID(spellID)C_Spell.GetSpellEffectInfo(spellID)C_Spell.GetSpellEffectMechanics(spellID)C_Spell.GetSpellDispelType(spellID)C_Spell.GetSpellRadius(spellID)/GetSpellRadius(slot, bookType)C_Spell.GetSpellPowerCost(spellIdentifier)C_Spell.GetSpellReagents(spellID)C_Spell.GetSpellCastCount(spellIdentifier)C_Spell.GetSpellSubtext(spellIdentifier)IsPassiveSpell(spellID)/IsPassiveSpell(slot, bookType)C_Spell.IsSpellPassive(spellID)IsPlayerSpell(spellID)CanDualWield()IsSpellKnown(spellID, [isPet])GetSpellBonusDamage(school)GetSpellBonusHealing()GetCritChance()/GetRangedCritChance()IsUsableSpell(spell)/IsUsableSpell(slot, bookType)C_Spell.IsSpellUsable(spellID)C_Spell.GetSpellCooldown(spellIdentifier)GetSpellBaseCooldown(spellID)IsAttackSpell(slot, bookType)IsAutoRepeatSpell(slot, bookType)IsConsumableSpell(spellID)/IsConsumableSpell(slot, bookType)C_Spell.GetSpellLossOfControlCooldown(spellIdentifier)C_Spell.IsCurrentSpell(spellIdentifier)C_Spell.IsSelfBuff(spellID)C_Spell.SpellHasRange(spellIdentifier)/SpellHasRange(slot, bookType)C_Spell.IsSpellInRange(spellIdentifier, targetUnit)C_Spell.IsAutoAttackSpell(spellID)C_Spell.IsRangedAutoAttackSpell(spellID)C_Spell.IsNextMeleeSpell(spellID)C_Spell.ResetsMeleeSwing(spellID)IsHarmfulSpell(spell)/IsHelpfulSpell(spell)C_Spell.IsSpellHarmful(spellID)/C_Spell.IsSpellHelpful(spellID)GetSpellSchool(spellID)CastSpellByID(spellID [, target])CastSpellNoToggle(name | spellID [, unit [, placeGroundSpell]])C_Spell.CastAtCursor(spellIDOrName)C_Spell.CastAtUnit(spellIDOrName, unit [, placeGroundSpell])C_Spell.CancelSpellByID(spellID)/CancelSpellByName(name)CancelUnitBuff(unit, index [, filter])C_Spell.UnitCastingInfo(unit)/C_Spell.CastingInfo()C_Spell.UnitChannelInfo(unit)/C_Spell.ChannelInfo()UnitCastingDuration(unit)/UnitChannelDuration(unit)C_Spell.GetSpellLevelInfo(spellID)GetSpellRequiredTargetLevel(spellID)- Stand, dismount and form settings (
autoStand,autoDismount,autoUnshift)
-
FindSpellBookSlotByID(spellID)C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)C_SpellBook.GetNumSpellBookSkillLines()C_SpellBook.GetSpellBookSkillLineInfo(skillLineIndex)C_SpellBook.GetSpellBookItemSkillLineIndex(slotIndex, spellBank)C_SpellBook.GetSkillLineIndexByID(skillLineID)C_SpellBook.GetSpellBookItemCastCount(slotIndex, spellBank)C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo(slotIndex, spellBank)C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration(slotIndex, spellBank)C_SpellBook.IsClassTalentSpellBookItem(slotIndex, spellBank)C_SpellBook.ContainsAnyDisenchantSpell()C_SpellBook.GetSpellLevelLearned(spellID)C_SpellBook.GetCurrentLevelSpells([level])C_SpellBook.GetPlayerSpellsByAura(auraName)C_SpellBook.GetSkillLineName(skillLineID)C_SpellBook.GetSkillLineRank(skillLineID)C_SpellBook.GetSpellSkillLine(spellID)C_SpellBook.IsAutoAttackSpellBookItem(slot, bookType)C_SpellBook.IsRangedAutoAttackSpellBookItem(slot, bookType)
-
IsMounted()Dismount()IsStealthed()IsFalling()IsSwimming()IsIndoors()IsOutdoors()IsAssistingRitual()IsInGroup()IsInRaid()GetMirrorTimerInfo(index)/GetMirrorTimerProgress(timer)GetShapeshiftForm([requireForm])GetShapeshiftFormID()CancelShapeshiftForm()GetSheathState()IsLoggingOut()IsPlayerInWorld()
-
GetPlayerFacing()TargetDirectionEnemy(facing [, angle])TargetDirectionFriend(facing [, angle])TargetDirectionFinished()TargetNearest([reverse])TargetNearestEnemyPlayer([reverse])TargetNearestFriendPlayer([reverse])InteractUnit(unit [, exactMatch])C_PlayerInteractionManager.InteractUnit(unit [, exactMatch [, looseTargeting]])- Soft interact (
softinteract/anyinteract) - Soft enemy and soft friend (
softenemy/softfriend/anyenemy/anyfriend) IsTargetLoose()
-
C_Texture.GetAtlasInfo(atlasName)C_Texture.GetAtlasExists(atlasName)C_Texture.GetAtlasID(atlasName)C_Texture.GetAtlasElementID(atlasName)C_Texture.GetAtlasElements()C_Texture.RegisterAtlas(name, file, width, height, left, right, top, bottom [, tilesHorizontally, tilesVertically])- Atlas markup
SetPortraitTextureFromCreatureDisplayID(textureObject, creatureDisplayID)
-
GetServerTime()GetTimeCached()GetTickTime()C_Timer.After(seconds, callback)C_Timer.NewTimer(seconds, callback)C_Timer.NewTicker(seconds, callback, [iterations])C_DateAndTimeoverviewC_DateAndTime.GetCurrentCalendarTime()C_DateAndTime.GetCalendarTimeFromEpoch(epoch)C_DateAndTime.AdjustTimeByDays(date, days)/AdjustTimeByMinutes(date, minutes)C_DateAndTime.CompareCalendarTime(lhs, rhs)C_DateAndTime.GetServerTimeLocal()C_DateAndTime.GetSecondsUntilDailyReset()
-
UnitGUID(unit)UnitTokenFromGUID(guid)UnitTokenFromName(name [, exactMatch])SetUnitCursorTexture(texture, unit [, style [, includeLowPriority]])UnitFactionGroup("player")while addons loadUnitSubName(unit)UnitSelectionColor(unit [, useExtendedColors])/UnitSelectionType(unit [, useExtendedColors])UnitCreatureFamilyID(unit)UnitCreatureTypeID(unit)UnitCreatureID(unit)GetUnitSpeed(unit)UnitClassBase(unit)UnitRaceBase(unit)UnitIsAFK(unit)UnitIsDND(unit)UnitIsFeignDeath(unit)UnitIsInMyGuild(unitOrName)UnitIsPossessed(unit)UnitIsMinion(unit)UnitIsPet(unit)UnitIsOtherPlayersPet(unit)UnitOwnerGUID(unit)UnitCreatedBySpell(unit)UnitStandState(unit)UnitInRange(unit)UnitDistanceSquared(unit)UnitPosition(unit)UnitFacing(unit)UnitInLineOfSight(unit)ClosestUnitPosition(creatureID)UnitHealthMissing(unit)UnitPower(unit [, powerType [, unmodified]])/UnitPowerMax(unit [, powerType [, unmodified]])UnitPowerMissing(unit [, powerType [, unmodified]])UnitPowerType(unit)UnitSpellHaste(unit)UnitSpellTargetName(unit)UnitHasIncomingResurrection(unit)CanBeRaidTarget(unit)
-
C_UnitAuras.GetAuraDataByIndex(unit, index [, filter])C_UnitAuras.GetBuffDataByIndex(unit, index)/GetDebuffDataByIndex(unit, index)C_UnitAuras.UnitAura(unit, index [, filter])C_UnitAuras.UnitBuff(unit, index [, filter])/UnitDebuff(unit, index [, filter])C_UnitAuras.GetAuraSlots(unit [, filter [, maxSlots [, continuationToken]]])C_UnitAuras.GetAuraDataBySlot(unit, slot)/UnitAuraBySlot(unit, slot)C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID [, buf])C_UnitAuras.GetUnitAuraBySpellID(unit, spellID [, filter])C_UnitAuras.GetPlayerAuraBySpellID(spellID)C_UnitAuras.GetAuraDataBySpellName(unit, spellName [, filter])C_UnitAuras.RegisterComboDuration(spellID, baseSeconds, maxSeconds)C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])C_UnitAuras.GetUnitAuras(unit [, filter])C_UnitAuras.GetAuraDispelTypeColor(dispelName)
-
C_XMLUtil.GetTemplates()C_XMLUtil.GetTemplateInfo(name)C_XMLUtil.DoesTemplateExist(name)inheritswith more than one templateparentKeyin XMLKeyValuesin XMLmixinin XMLintrinsicframe types in XML- ScrollBox and scroll bars
- BackdropTemplate
- Menus and dropdowns
- Atlases in XML
- Masks in XML
functionandmethodon XML script handlersstyleon button font elementsalpha,scaleandregisterForDragin XMLC_TTSSettings— getters & setters- TTS events
- TTS CVars
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. Returnstrueon success,falseif name or password is empty, no realmlist is set, or the OS rejects the write. -
DeleteAccount(name)— remove the saved entry. Returnstrueif 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 namestring Account name as saved. lastUsednumber Unix epoch seconds of the last write to the vault entry. Refreshed automatically by LoginWithSavedAccount, so this functions as a "last used" timestamp.0if 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. Returnstrueif credentials were found and dispatched,falseif no such entry exists. Plaintext is never returned to Lua. Equivalent to whatDefaultServerLogin(name, password)does, but with the password fetched from the vault rather than passed in. Also re-saves the credential (same value) to refresh thelastUsedtimestamp.
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)
C_ActionBar.GetSpell(actionID)
Returns the ID of the spell that a 1-based action-bar slot casts. The answer depends on what the slot holds:
- A spell gives its own spell ID.
- A macro gives the spell that its button shows. The button uses this spell for its cooldown, usable state and range.
- An item gives the spell that the item casts when you use it.
If the slot is empty, has no spell, or is not in the range 1 to 120, the
function returns 0. If the client does not have the data of an item yet,
the function also returns 0 for that item.
local actionType, id = GetActionInfo(1)
if actionType == "item" then
id = C_ActionBar.GetSpell(1)
end
C_ActionBar.GetItemActionOnEquipSpellID(actionID)
Returns the ID of the spell that the item of a 1-based action-bar slot gives while you wear it. The item is the item in the slot, or the item that a macro in the slot shows.
If the slot has no item, or the item has no such spell, the function returns nil. If the client does not have the data of the item yet, the function also returns nil.
local spellID = C_ActionBar.GetItemActionOnEquipSpellID(1)
if spellID then
print("Equip effect", GetSpellInfo(spellID))
end
IsStackableAction(slot) / C_ActionBar.IsStackableAction(actionID)
Tells if the item of a 1-based action-bar slot stacks. The item is the item in the slot, or the item that a macro in the slot shows. A potion stacks. A sword does not stack.
IsStackableAction returns 1 or nil. C_ActionBar.IsStackableAction
returns true or false. A slot without an item gives nil or false.
if C_ActionBar.IsStackableAction(1) then
print("Count", GetActionCount(1))
end
C_ActionBar.IsMacroActionWithShowTooltip(actionID)
Returns true if a 1-based action-bar slot holds a macro with a
#showtooltip line. A macro with only #show gives false. Any other
slot gives false.
if C_ActionBar.IsMacroActionWithShowTooltip(1) then
print("The button shows the tooltip of the macro spell or item")
end
C_ActionBar.FindSpellActionButtons(spell [, setToSearch])
Returns a table of the 1-based action-bar slots that hold a spell. If no slot holds the spell, the function returns nothing.
spell is a spell ID, a spell link, or the name of a spell in your
spellbook. A name gives the highest rank that you know.
A slot counts only when it holds the spell itself. A macro or an item that casts the spell does not count. Each rank is a different spell. Thus, a slot with Fireball rank 11 does not count for Fireball rank 10.
setToSearch is a value of Enum.ActionBarSet. The default is
Enum.ActionBarSet.All. The client has only the Mkb set of slots. Thus,
Enum.ActionBarSet.Gamepad finds no slot.
local slots = C_ActionBar.FindSpellActionButtons(133)
if slots then
for _, slot in ipairs(slots) do
print("Fireball on slot", slot)
end
end
C_ActionBar.HasSpellActionButtons(spell [, setToSearch])
Returns true if one or more action-bar slots hold a spell. The function
uses the same rules as
FindSpellActionButtons.
print(C_ActionBar.HasSpellActionButtons("Fireball"))
C_ActionBar.IsOnBarOrSpecialBar(spell [, setToSearch])
Returns true if a spell is on an action-bar slot, on the shapeshift bar,
or on the pet bar. The action-bar slots use the same rules as
FindSpellActionButtons.
setToSearch has an effect only on the action-bar slots.
The shapeshift bar holds stances, forms, auras and stealth. The pet bar counts only while you have a pet bar.
-- Bear Form is on the shapeshift bar.
print(C_ActionBar.IsOnBarOrSpecialBar(5487))
Enum.ActionBarSet
The sets of action-bar slots, for setToSearch.
| Field | Value |
|---|---|
None |
0 |
Mkb |
1 |
Gamepad |
2 |
All |
3 |
All 120 action-bar slots are in the Mkb set. No slot is in the Gamepad
set.
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—1when the addon loaded, or was already loaded.nilotherwise.value— a locale-independent reason the load failed, such as"DISABLED", or"UNKNOWN_ERROR"when no specific reason is available.nilon 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_LOADEDhandler getstrue, 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:
MyAddonis loaded, andMyAddon's.tocfile 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
## metadatalines 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 inGetNumAddOns()/GetAddOnInfo(), firesADDON_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/reloadapplies it.GetAddOnMetadatareturns 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:
INstarts slow,OUTends slow andIN_OUTdoes 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
trueif 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
Alphaanimation 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
Alphaanimation addschange × progress. While the group plays,GetAlphareturns the animated value. - When the last group with an
Alphaanimation on the region, or on a frame around it, stops or finishes, the stored alpha comes back. - A value you set with
SetAlphawhile 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.
SetChangemakes 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,ScaleorRotationanimation 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
Rotationafter aTranslationturns around the moved texture. - On a font string, the text scales and turns around the point where its
first line starts, and
SetOrigindoes not change that point. A font string uses only the x value ofSetScale. When more than oneScaleorRotationanimation 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.
SetParentmoves 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 forinheritsand for thetemplateNamearguments. - Animations without an
orderplay one after another. <Scripts>bodies seeselfand their script's arguments (elapsed,requested,loopState). A script element can name a global function withfunction=.
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.
0stops 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;slotIndex1-based).duration—1/2/3for 2h/8h/24h (the three durations); the raw minute values120/480/1440are 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):
funcreceives the 15 positional values ofC_UnitAuras.UnitAuraBySlot. No table is built per aura. usePackedAuratrue:funcreceives theAuraDatatable fromGetAuraDataBySlot.
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
InGroupbit. - The player is in your guild roster. The entry has the
InGuildbit. - The player is on your friends list. The entry has the
Friendbit. - You whispered the player, or the player whispered you, after you logged
in. The entry has the
InteractedWithbit.
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. At0or less, you get all of them.cursorPosition: the position of the cursor inname, in characters, aseditBox:GetUTF8CursorPosition()gives it. A value less than1or more than the length ofnameis the end of the text.allowFullMatch: if this isfalse, a name equal tonameis not an entry.includeFlags:Enum.AutoCompleteEntryFlagbits. Each entry has one or more of them.excludeFlags:Enum.AutoCompleteEntryFlagbits. 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:
- The players that you whispered, the most recent first.
- 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 forrealmas a string, or""if none has been saved. Nevernil.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/.xsdfiles toBlizzardInterfaceCode\…ExportInterfaceFiles art— writes.blp/.tgafiles toBlizzardInterfaceArt\…
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 underInterface\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:
- the MPQ
(listfile)underDBFilesClient\— catches files that ship in the archives without a loader (e.g.wowerror_strings.dbc); - a
.textscan 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 notSound\. The client prefers a loose file over the archived copy of the same path, so exporting intoSound\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.
filenameis relative to the client's working directory (next toWoW.exe). The default isConsoleLogs\Log<Timestamp>.txt. The command makes missing folders. If the file exists, the command adds the lines to the end of it.numLineslimits 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_resetorcvar_defaultcompletes 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_resetorcvar_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'sPackBagSlotrejects 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 returnsniland 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
isFilteredagain onBAG_UPDATEand onINVENTORY_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—0backpack,1..4bags,-1bank,5..10bank bags,-2keyring.
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 sharedItem::Arghelper, same chainC_Item.GetItemCountetc. 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 sharedItem::Arg::Resolvefor 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 a bag, and the bag family of that bag. The bag family is a bitmask that shows which items the bag accepts.
numFreeSlots, bagFamily = C_Container.GetContainerNumFreeSlots(bagID)
bagID = 0: the backpack.bagFamilyis always0, because the backpack accepts all items.bagID = 1..4: the equipped bags.bagID = -1: the main bank.bagFamilyis always0.bagID = 5..10: the bank bags.
The main bank and the bank bags are available only while the bank window
is open. If the bag is not there, the function returns only 0, and
bagFamily is nil. This includes the keyring, an empty bag slot, and
the bank while the bank window is closed.
local free, bagFamily = C_Container.GetContainerNumFreeSlots(1)
if bagFamily == 0 then
-- a general-purpose bag
end
Specialty bags report their family. A Soul Bag reports
0x4, a quiver reports0x1, and an ammo pouch reports0x2. These are the same values thatC_Item.GetItemFamilygives for the bag and for the items that go in it. The Turtle bags (Meat, Fish, Leather, Mining) use0x200to0x1000. These bits have a different meaning in other clients. See theGetItemFamilyfamily table.
The encoding.
bagFamilyis a bitmask (1 << (familyID - 1)), so you can usebit.bandto compare it with a value fromC_Item.GetItemFamily.
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)
bagIDtakes the same values asC_Container.GetContainerNumFreeSlots. The main bank and the bank bags are available only while the bank window is open.- If the bag is not there, the call returns 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—truewhen the item is a quest item.questID— the quest the item begins, ornilwhen it begins no quest.isActive—truewhen the quest inquestIDis 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_RECEIVEDor the nextBAG_UPDATEand 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—truewhen the item is part of at least one equipment set.setList— the set names, in the order the sets are stored, joined withPLAYER_LIST_DELIMITER(", "in English).nilwhen 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: the backpack, plus each equipped bag (1..4) whose bag family 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.
The result is the sum of the per-bag
GetContainerNumFreeSlotsvalues for the bags whose bag family is 0.
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:
- Its itemID is
6948(the standard Hearthstone — fast path, no cache lookup needed), or - 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_Crecord 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
truedoesn't guarantee the cast started. The return reflects "we had a hearthstone to try with and called the engine'sUseContainerItemon it." Whether the cast actually starts depends on cooldown, combat, movement, etc. — same downstream rules as callingUseContainerItem(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— backpack1..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
0x10DCMSG_SWAP_INV_ITEMwith the two linear slot bytes. - Cross-container (e.g. backpack ↔ bag1, bag2 ↔ bank, bank ↔ bank bag)
sends opcode
0x10CCMSG_SWAP_ITEMwith(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 bankEncodeBagSlotpath returns false until then. Same constraint asC_Container.GetContainerItemInfoon bank IDs.
ClassicAPI-only. There is no other direct swap-two-slots call — addons otherwise drive the cursor with two
PickupContainerItemcalls in sequence. This single-call form bypasses the cursor entirely (same pathC_EquipmentSet.UseEquipmentSetuses 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:
dstempty → placescountthere (clean split).dstholds the same item &dstCount + count ≤ maxStack→ mergescountinto dst.dstholds a different item, or the merge would overflow → server rejects entirely (source untouched,SMSG_INVENTORY_CHANGE_FAILUREfires).
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 returnsniluntil 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:
- Hearthstone
- Weapons and armor
- Consumables
- Reagents
- Trade goods
- Quest items
- Everything else, best quality first
- 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_CreatureInfodoesn'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.5becomes"1.5". - If
valueisnilor missing, the cvar gets"0". - For a read-only cvar, it returns
falseand the value does not change. - If no cvar has that name, it returns no values, so the result reads as
nil. - If
nameis not a string or a number, orvalueis 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
SetTempCVartwo times on one cvar,RemoveTempCVarputs back the value from before the first call. - If
SetCVarchanges a cvar that has a temp value, theSetCVarvalue replaces the temp value. The game saves it, andRemoveTempCVarthen does not change the cvar. - If
valueisnil(or another value that is not a string or a number), the cvar gets"0", as withSetCVar. - 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 aSetCVarvalue would. It is not saved. RemoveTempCVardoes 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
itemLink
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.
Expanded cursors (expandedCursors)
When the expandedCursors setting is 1 and you point at an NPC or an
object, the cursor shows what you can do with it. The default is 0. You can
also change the setting in the ClassicAPI options, on the Interface page.
These NPC services get new images:
| The NPC has | The cursor |
|---|---|
| A quest that you can accept, or a quest that you did not complete | A yellow ! |
| A quest that you can turn in | A yellow ? |
| A repeatable quest that you can turn in | A blue ? |
| Repairs | An anvil |
| An inn | A hearthstone |
| A stable | A horseshoe |
When an NPC has more than one service, the cursor shows the first service in this list:
- A quest
- Repairs
- An inn
- Flights
- Training
- Resurrection at a spirit healer
- A bank
- Guild charters, tabards and battlegrounds
- An auction house
- A stable
- Goods for sale
- Gossip
The other services keep their usual images. For example, a vendor that also has gossip shows the vendor image, not the speech bubble.
Your fishing bobber shows a fish hook, not the gear.
When you cannot use the NPC or the object, the cursor shows the gray form of the image. For example, this occurs when you are too far away.
The soft-interact icon and
SetUnitCursorTexture
show the same images as the cursor. For a quest, they show no image.
/console expandedCursors 1
ClassicAPI extension.
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.
modRatemust be more than 0. It changes only the results forEnum.DurationTimeModifier.BaseTime.modifieris a value ofEnum.DurationTimeModifier.RealTime(0) is the default. WithBaseTime(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:GetTimerDurationreturns a new Lua object for the time span that you gave tostatusBar: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])andFormatTotalDuration(formatter [, modifier])return the text that aNumericRuleFormattermakes from the duration.- The
Evaluate…methods andSetClockare 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 empty1— slot is ignored (GetIgnoredSlotslists 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_Sone time. - A missing item is skipped. The error frame shows
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_Sone 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_FINISHEDreports a failure, and the error frame showsERR_EQUIPMENT_MANAGER_BAGS_FULL.
CURSOR_UPDATEfires 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 toCURSOR_UPDATEshould checkCursorHasItem()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:
ModifyEquipmentSetandSaveEquipmentSetupdate the name and the icon.DeleteEquipmentSetremoves 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—trueon a fresh login (including logging out to char-select and back in),nilotherwise.isReloadingUi—truewhen the fire is caused by a/reload,nilotherwise.- Both
nilon 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 have0.arg12—guid. The sender's GUID, in the same format asUnitGUID("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,
GetTotemInforeturnsnilfor 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 at0x004F9xxx) — each just sets ag_pendingflag, no fireFUN_0066FD50(engine's per-frame world-subsystem update at0x0066xxxx, 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__thiscallwith an awkward register-arg shape. Keyring updates currently won't triggerBAG_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 asGetInventoryItemLink("player", slot)).hasCurrent—1when the slot now holds an item,nilwhen it's now empty (writeif 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:
- 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's0, this is the sync and we suppress the event. - 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
OnMouseDownorOnMouseUp. - If the game window loses the mouse while you hold a button,
GLOBAL_MOUSE_UPfires 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) —truewhen the target is in range. Do not use this value whenchecksRangeisfalse.checksRange(boolean) —falsewhen 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_MISSEDRANGE_DAMAGE,RANGE_MISSED(Auto Shot, Shoot, Throw)SPELL_DAMAGE,SPELL_MISSED,SPELL_HEAL,SPELL_ENERGIZESPELL_PERIODIC_DAMAGE,SPELL_PERIODIC_MISSED,SPELL_PERIODIC_HEAL,SPELL_PERIODIC_ENERGIZE,SPELL_PERIODIC_DRAIN,SPELL_PERIODIC_LEECHDAMAGE_SHIELD,DAMAGE_SHIELD_MISSED,DAMAGE_SPLITENVIRONMENTAL_DAMAGESPELL_CAST_START,SPELL_CAST_SUCCESS,SPELL_CAST_FAILEDSPELL_DRAIN,SPELL_LEECH,SPELL_INTERRUPT,SPELL_EXTRA_ATTACKS,SPELL_INSTAKILL,SPELL_RESURRECT,SPELL_DURABILITY_DAMAGE,SPELL_DURABILITY_DAMAGE_ALLSPELL_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_DOSESPELL_DISPEL,SPELL_DISPEL_FAILEDUNIT_DIED,UNIT_DESTROYED(a totem or another summoned unit),PARTY_KILL
Some values have fixed contents:
critical,glancing,crushingandisOffHandare1ornil.resisted,blockedandabsorbedin a damage suffix arenilwhen they are 0.missTypeis 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.
overkillis the damage above the health of the target. It is 0 when the target lives.overhealingis 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. Theabsorbedof a heal is always 0.alternatePowerTypeis alwaysnil.DAMAGE_SHIELDnames its spell when the owner of the shield has one damage-shield aura of that school, for example Thorns. OtherwisespellIdis 0,spellNameisnil, andspellSchoolis the school of the damage.DAMAGE_SHIELD_MISSEDwith themissType"IMMUNE"fires for a target that is immune to the damage of a damage shield or of a periodic effect.- The
COMBATLOG_OBJECT_MAINTANKandCOMBATLOG_OBJECT_MAINASSISTbits are never set. SPELL_CAST_STARTfires for a spell with a cast time, and has no target.SPELL_CAST_SUCCESSfires for an instant spell. A spell with a cast time gets noSPELL_CAST_SUCCESS.SPELL_CAST_FAILEDfires 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
amountof an_AURA_APPLIED_DOSEor_AURA_REMOVED_DOSEis the number of stacks after the change. SPELL_AURA_REFRESHfires 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_DISPELandSPELL_DISPEL_FAILEDname the dispelling spell when its cast was seen, for example Dispel Magic or Purge. A dispel from an aura, for example Abolish Poison, hasspellId0 andspellNamenil. The extra spell is the aura that the dispel removed, or did not remove.SPELL_INSTAKILLhas 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_APPLIEDandENCHANT_REMOVEDdo not fire.- On some servers,
SPELL_INTERRUPTdoes 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—1when the cursor is now empty,nilwhen it is holding something (writeif isDefault then— the event dispatcher cannot push real booleans).newCursorType(number) — what the cursor holds now, as anEnum.UICursorTypevalue.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.0when 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/IsAltKeyDownbottom out at the synchronousGetKeyState, 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. PreferIsModifierKeyDown/ 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 — soarg1atCREATEDtime is a bare frame with no addon-side decorations yet. For unit-specific work after the addon has decorated, useNAME_PLATE_UNIT_ADDED(fires next tick at the latest) or fetch the current frame on-demand viaGetNamePlateForUnit(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, callUnitGUID(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.
arg1is the GUID of the previous object, ornil.arg2is the GUID of the new object, ornil.
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— anEnum.PlayerSwingTypevalue: 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— theEnum.PlayerSwingTypevalue this update is for.isInRange—1when the target is in range,nilwhen it is not. Ignore this value whenchecksRangeisnil.checksRange—1when a range check was possible,nilwhen 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 stockQUEST_FINISHEDfires 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. |
textureis a texture path that names the art of the alert. An addon that draws the alert supplies a texture file for each path.locationTypeis anEnum.ScreenLocationTypevalue.scaleis a size multiplier, andr,gandbare 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
uiScalesetting changes whileuseUiScaleis on, - the
useUiScalesetting 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 exampleparty1orraid12. A member that has more than one token (for exampleparty1andraid3) 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 exampletargetandparty1) 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
updateInfotable 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.
isFullUpdateistruewhen 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 exampleparty1orraid12. A member that has more than one token (for exampleparty1andraid3) gets one event for each token.isConnected(boolean) —truewhen the member is online,falsewhen 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 exampleparty1orraid12. A member that has more than one token (for exampleparty1andraid3) gets one event for each token.isInDistance(boolean) —truewhen the client can see the member,falsewhen 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_FACTIONshould 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 ofUnitInRange(unitTarget).
The frames that are registered for the event select the units that the client checks:
- A frame that uses
RegisterUnitEventadds its units. You can use any unit token, for exampletargetorfocus. - A frame that uses
RegisterEventaddspet, 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 exampletargetandfocus) 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 exampletargetandfocus) 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 exampletargetandfocus) 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 exampletargetandfocus) 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:
SENTnever fires (only your own outgoing casts are visible).DELAYED/CHANNEL_UPDATEnever fire (pushback is sent only to the caster, so another unit's bar can't stretch/shrink from damage).FAILEDnever fires (a pre-cast requirement failure is client-local).- A remote cast/channel only has timing from the moment its
SMSG_SPELL_STARTwas 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):
castUIDis 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):castUIDis 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) andCHARMED.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-lessSPELLCAST_START/SPELLCAST_CHANNEL_UPDATE/ … still fire as before; theseUNIT_-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
0for header / category rows ("Other","Inactive", etc.). - Returns
nilif 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 flagsnil. - 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 returnnilfor 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.
ownermust be a frame.isPriorityis any value. A true value makes a priority override.keyis matched without regard to case. Write the modifiers in the order the game uses:ALT-CTRL-SHIFT-X.- A
commandofnilor""removes the owner's override forkey. 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)returns1when the action's keys are held. It also takes modifier names:IsModifiedClick("SHIFT"),IsModifiedClick("LCTRL"). With no argument, it returns1when any modifier key is held.SetModifiedClickchanges the current binding set. CallSaveBindingsto keep the change.bindingis modifier names joined with-, optionally followed byBUTTON1toBUTTON5, or"NONE". An unknown action or binding raises an error.- A
<ModifiedClick action="NAME" default="BINDING"/>line in an addon'sBindings.xmladds 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
preferParentKeyistrue, 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
MEDIUMand 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
NORMALstate. 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
BLENDand its draw layer becomesBACKGROUND. - A font string loses its text and its font.
GetFont()returnsnil, 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 becomesCENTERandMIDDLE.
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
CreateFramemade returns the script file and the line of that call, asfile:line. The file name is the chunk name, as in error messages. A/runorloadstringchunk 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. Ifenableisnilor0, orstartordurationis not more than 0, it callsCooldownFrame_Clear.CooldownFrame_Clear(cd)callsClear().CooldownFrame_SetDisplayAsPercentage(cd, percentage)pauses the frame and shows the sweep atpercentage(0 to 1).CooldownFrame_SetTimer(cd, start, duration, enable)callsSetCooldown. Ifstart,duration, orenableis 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.
calculatedistruewhen you omit it. A string that draws at the pixel size of its font then returns that pixel size.- When
calculatedisfalse, the result is always equal to the second return value ofGetFont.
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.
indentistruewhen you omit it.GetIndentedWordWrapreturns1ornil.- 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.
wrapistruewhen you omit it.CanWordWrapreturns1when the text wraps, elsenil.- 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 followstexture:SetRotation. It also combines with masks fromtexture: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 is1. 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"ortruerepeats the image."CLAMP",falseornilstretches 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
OnMouseDownandOnMouseUpscripts, andslider: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
OnValueChangedruns after it. - A status bar whose
minis more than itsmaxgetsmaxfor 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.Reversehas 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
Centerstyle ignoresstatusBar: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 andGetMinMaxValues()returns 0, 0. The timer does not run theOnValueChangedscript. SetValuedoes not remove the timer.SetMinMaxValuesremoves 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,SetReverseFillandSetRotatesTexture. The bar texture moves with the fill, as it does forSetValue. interpolationis a value ofEnum.StatusBarInterpolation. The default isImmediate. WithExponentialEaseOut, the fill eases from the fill that the bar showed before, and then follows the timer with the same ease (seestatusBar:SetValue). A jump in the time span then eases too.- A
durationthat 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), withelapsedin 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.OnValueChangedruns one time, at the call, with the new value.- A new eased value continues from the fill that the bar shows. An
Immediatevalue 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
interpolationvalue other thanExponentialEaseOutworks asImmediate. 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.SetTextcan give a box such a text. - In a single-line box, the caret moves over
|nas 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.
propagateisfalsewhen you omit it.GetPropagateKeyboardInputreturnstrueorfalse.- A frame that passes a key on does not get the
OnKeyUpscript 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.
CanPropagateMouseMotionreturnstrueorfalse. The default isfalse.- Clicks do not pass on with motion. For clicks, use
frame:SetPropagateMouseClicks. - In XML,
propagateMouseInput="Motion"orpropagateMouseInput="Both"on a frame sets the value totrue.
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
OnEnterandOnLeave. A click on it goes to the next frame under the cursor that takes clicks. - A frame that takes only clicks never gets
OnEnterorOnLeave. 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.
CanPropagateMouseClicksreturnstrueorfalse. The default isfalse.- In XML,
propagateMouseInput="Clicks"orpropagateMouseInput="Both"on a frame sets the value totrue.
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.
dontSaveistruewhen you omit it.GetDontSavePositionreturns1ornil.
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.
IsUsingParentLevelreturnstrueorfalse. The default isfalse.- 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.
DoesClipChildrenreturnstrueorfalse. The default isfalse.- 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.
indexis0or1. Index0is the left and top edges. Index1is the right and bottom edges.gradientis a vector fromCreateVector2D(x, y).xis the length of the fade in from the left or right edge.yis the length of the fade in from the top or bottom edge. The lengths use the scale of the frame. A length of0gives 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 (
SetDesaturationabove0) or that has a mask does not fade. - With the OpenGL renderer (
gxApiset toopengl), 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,
handlerbecomes the handler, andGetScriptreturns it. - An error in
handleris 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 samedoesn't have a "<type>" scripterror asSetScriptfor 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 returnsnilwhen the script has only intrinsic scripts.GetScript(scriptType, bindingType)returns one part, ornil.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.
textis the text that the frame keeps, with its color codes.r,gandbare the color that the frame shows for the message. AfterUpdateColorByIDchanges the color, these values also change. The frame keeps each color value in steps of 1/255, so0.5returns as0.50196….smf:AddMessage(text, r, g, b, ...)keeps all the values afterb.GetMessageInforeturns 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,band the extra values work as inAddMessage. Withoutr,gandb, the message takes the color of the frame's font.GetMessageInforeturns 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
predicatefor all messages before it removes any message. Ifpredicategives 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 tooffset. An offset less than 0 or more thanGetMaxScrollRange()changes to the nearest limit.ScrollByAmount(amount)addsamountto 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 thanGetMaxScrollRange(). The function also shows the faded lines again, also when the offset does not change.- If
offsetoramountis 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
callbackis 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.
RegisterEventon a unit-filtered registration keeps the filter.RegisterUnitEventon a plain registration keeps it plain.RegisterUnitEventon a unit-filtered registration replaces the units. To switch kinds, callUnregisterEventfirst. - 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. UnregisterEventandUnregisterAllEventsremove 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):
prefix..name..suffix"*"..name..suffixprefix..name.."*""*"..name.."*"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>
typeisstring(the default),number,booleanornil. Every type exceptnilneeds avalue.- The attributes are set before the frame's
OnLoadruns, and they do not fireOnAttributeChanged. - 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,PreClickandPostClick:downis true for a click on a mouse press and false for a click on a release. Forbutton:Click(name, down), it is the value ofdown.OnEnterandOnLeave:(self, motion).motionis 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
noprefix negates a condition. A:a/b/csuffix adds arguments, which are OR'd. - A
@unitortarget=unitpiece 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] Rejuvenationheals the player called Feral. The name matches on its start and picks the nearest match, and it has to be someone around you. SeeUnitTokenFromName. - A group whose only piece is a
@unitmatches only while that unit exists. So[@mouseover][] Spelluses 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. @noneand@cursorname no unit, so they always match.@noneasks for no unit:/targetclears your target, and the other commands act with no unit named.@cursornames the position under your mouse, which/castand/useuse 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
AnyDownandAnyUp. - Each call replaces the previous one. A call with no argument removes all buttons.
OnMouseUpruns only when the down action of the same button is also registered.- This method changes only
OnMouseDownandOnMouseUp. Clicks still followRegisterForClicks, and drags still followRegisterForDrag.
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, ornilwhen unknown.classStr— localized class name, ornilwhen unknown.filename— locale-independent class token ("WARRIOR","MAGE", …), ornilwhen unknown. This is the key forRAID_CLASS_COLORS. The stockGetWhoInfodoes 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—truewhen the friend is online.level— the friend's level, or0when unknown.className— localized class name, ornilwhen unknown.classFilename— locale-independent class token ("WARRIOR","MAGE", …), ornilwhen unknown. Present wheneverclassNameis. This is the key forRAID_CLASS_COLORSand 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, ornilwhen unknown.guid— the friend's GUID string.notes— your note for this friend, ornilif none. Set it withSetFriendNotes.afk/dnd— the friend's status flags.mobile/referAFriend/rafLinkType— alwaysfalse/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 bySMSG_ITEM_QUERY_SINGLE_RESPONSEonly 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 endThis caching behavior matches what
C_Item.GetItemInfoInstantdocuments — same underlying cache. The same caveat applies withC_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 forSetItemByID/SetHyperlinkwhich 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.
nameis 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".unitTokenis the string that was given toSetUnit, 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 aunitattribute, 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 theSetUnittoken, 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_TOPputs the bottom edge of the tooltip on the top edge of the owner.ANCHOR_BOTTOMputs 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:
talentIDdoesn't match any record inTalent.dbctalentIDisnil, 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()
GameTooltip:SetHyperlink(link)
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.
GameTooltip:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])
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:
offsetcounts only the filled slots. If only the second ring slot has a ring,offset1 shows that ring. - One-hand weapons and two-hand weapons: if both hands hold an item,
offset1 shows the off hand andoffset2 shows the main hand. If only one hand holds an item,offset1 shows it. - Main-hand-only weapons:
offset1 shows the main hand. - Shields, off-hand weapons, and held items:
offset1 shows the off hand. If the off hand is empty and the main hand holds a two-hand weapon,offset1 shows that weapon. - All other items:
offset1 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 thatGetPoint(index)returns. If the anchor is relative to the screen, the line is<index> point x y.SOURCE, then the location thatGetSourceLocation()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
callbackis shown like any other script error, through the error handler (geterrorhandler()). The original's return values still reach the caller. namecan be a number. It is looked up as its string form.- The hook is written directly into the table. A
__newindexmetamethod on the table does not run. The original is read normally, so a method the table inherits through__indexcan be hooked. - Works in the world and on the login screen.
Errors:
Usage: hooksecurefunc([table,] "function", hookfunc)whennameis not a string or number, orcallbackis not a function.hooksecurefunc(): <name> is not a functionwhentable[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(errorhooksecurefunc: 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_Gas 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, ...)callsfunc(...)and returns its results.funccan also be the name of a global function. After an error,securecallreturns nothing.securecallfunction(func, ...)is the same, butfuncmust be a function.securecallmethod(object, method, ...)callsobject[method](object, ...)and returns its results. The lookup ofmethodignores the metatable ofobject. Ifobjectis not a table, ormethodis not a function, the error goes to the error handler.secureexecuterange(tbl, func, ...)callsfunc(key, value, ...)for each entry oftbl. 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 fromMap.dbc.instanceType—"none"(open world),"party"(5-man dungeon),"raid","pvp"(battleground), or"arena"(unused).difficultyID— always1. There is no heroic mode.difficultyName— always"Normal".maxPlayers— type-default cap:5for dungeons,40for raids,40for battlegrounds,0for open world. See caveat below.dynamicDifficulty— always0.isDynamic— alwaysfalse.instanceID— current map ID. The field is named "instanceID" but it's really theMap.dbcrow ID.instanceGroupSize— mirrorsmaxPlayers(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.
isArenaistruewhile the player is in an arena match. It becomestruewhen the arena map loads. The open-world free-for-all arenas (for example the Gurubashi Arena) are not arena matches, and returnfalse.isRegistered(a rated match) is alwaysfalse. 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
itemLocationargument shapesEvery
C_Item.*function on this page that takesitemLocationaccepts any of three forms — theItemLocationmixin 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.GetItemGUIDTable 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)—trueiff the equipment-slot or(bagID, slotIndex)location resolves to a populated inventory slot on the active player. Empty slots, missing bags, or malformed tables returnfalse.C_Item.DoesItemExistByID(item)—truefor 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.IsItemDataCachedByIDtells you if the data is cached.C_Item.RequestLoadItemDataByIDloads it. Accepts a numericitemID, a bare"item:NNN..."string, or a full chat link. Other input returnsfalse.
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 shapesC_Item.IsEquippedItemtakes. 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 predicateC_Item.IsEquippedItemuses. - An item location:
{bagID = B, slotIndex = S},{equipmentSlotIndex = N}, or an item GUID string — the shapesC_Item.GetItemIDtakes. 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 ownFUN_INVENTORY_SWAP(0x005E0C40) — the same primitiveScript_EquipCursorItemdispatches 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 +AutoEquipCursorItempath 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 numericitemID, 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
includeBankis 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 | The bag family bitmask. The same value as C_Item.GetItemFamily, bags included. |
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.dbcrows 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 through1 << (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-codesfamily == 0x200to 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 report0x200, which is all the "does this item fit this bag" test needs) — just don't assume the standard bit meanings for anything above0x100.
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 viabitmask = 1 << (rawID - 1), so callers see the bitmask encoding they expect — addons canband(family, FAMILY_BAG_HERB_BAG)directly.
Legacy Blizzard bags derive from
(class, subClass). The stock bags shipped with an emptym_bagFamilyfield — 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 is0we recover the family from its class + subclass — the same signal the"24 Slot Soul Bag"tooltip line is built from. A Felcloth Bag reports0x4(Soul Shards), a quiver0x1(arrows), an ammo pouch0x2(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 readsubClassyourself. (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.)
nilvs0. We returnnil(not0) 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 as0for routing-logic purposes; the distinction helps debugging.
Auto-warmup on cache miss. First call for an uncached item returns
nilAND triggers the engine's cache fill in the background. Listen forGET_ITEM_INFO_RECEIVED(itemID, success)and retry once the matchingitemIDarrives — "nil first call, value second call." We fireGET_ITEM_INFO_RECEIVEDwhen 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/itemSubTypeare the localized class / subclass names (e.g."Weapon"/"One-Handed Swords"), read fromItemClass.dbcandItemSubClass.dbc.itemEquipLocis the"INVTYPE_*"constant (e.g."INVTYPE_HEAD"), or""for non-equippable items.iconis 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/subClassIDare the raw enum integers (e.g.2,7for 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
GetItemInfoorC_Item.GetItemNameByID,GetItemInfoInstantdoes not trigger a network query on a cache miss. The "Instant" name is contractual — callers that need the cache populated should useC_Item.RequestLoadItemDataByIDexplicitly, or just callGetItemInfo(which warms via theScript_GetItemInfohook).
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.
C_Item.GetItemStats(itemLink [, statTable])
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:
- Base record — the stored attributes, armor, resistances, and (for weapons) DPS.
- 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. - Random suffix — the link's third
item:id:enchant:SUFFIX:uniquefield (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 = NandITEM_MOD_SPELL_HEALING_DONE_SHORT = N. An item with "+N Fire damage" reports onlyITEM_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
*_RATINGkey — e.g. Krol Blade's "+1% crit" isITEM_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_SHORTkey subsumes it, soITEM_MOD_RANGED_ATTACK_POWER_SHORTshows 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 localizedItemSubClass.dbcname (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 theItemSubClass.dbcflags bit0x200.
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:
- You carry the item: equipped or in bags 0 to 4. An item that you do not have is not usable.
- 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.
- If the item has no on-use effect, it is usable while you control your character.
- Otherwise, the result is the same test that
IsUsableSpelluses for the on-use spell.noManaistrueonly 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, callC_Item.RequestLoadItemDataByIDfirst, then check again onITEM_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—trueif 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—trueif the player can right-click this specific instance and trigger the open action right now. For unlocked-by- default items (clams, sacks) this matchesisOpenable. 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_OBJECTconfirms 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.
successis1ornil. The engine's printf-style event dispatcher has no%btoken, so we encode the boolean as1for success /nilfor failure (leaning onlua_pushstring(NULL)→lua_pushnil).if success then …distinguishes them correctly —nilis falsy,1is truthy. Same encoding asGET_ITEM_INFO_RECEIVEDandQUEST_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.
RequestLoadItemDatadoes 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 ownSMSG_ITEM_QUERY_SINGLE, while our early query gets dropped. Instead we track the itemID and hook the engine'sSMSG_ITEM_QUERY_SINGLE_RESPONSEhandler at0x0055BDB0: when the engine fills any entry, we sweep tracked items and fireITEM_DATA_LOAD_RESULTfor matches. For items the engine doesn't prefetch (quest rewards, AH browse targets, etc.) we escalate after ~60 ticks by callingCacheFetchourselves — by then engine state has settled and our query goes through normally. Hard timeout at ~1200 ticks (~20–40s) firesITEM_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 shapesC_Item.UseItemByNametakes. The first matching item in your bags is used. - An item location:
{bagID = B, slotIndex = S},{equipmentSlotIndex = N}, or an item GUID string — the shapesC_Item.GetItemIDtakes. 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 shapesC_Item.EquipItemByNametakes. 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 predicateC_Item.IsEquippedItemuses. - An item location:
{bagID = B, slotIndex = S},{equipmentSlotIndex = N}, or an item GUID string — the shapesC_Item.GetItemIDtakes. 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.
Get*ItemID — companions to the engine's Get*ItemLink family
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.
GetQuestItemIDreads the active quest offer (the QuestFrame you see when accepting/turning in a quest);GetQuestLogItemIDreads the currently-selected entry in the quest log. The two cover different states intentionally — the same split asGetQuestItemLink/GetQuestLogItemLink.
GetTradeSkillItemIDvsGetCraftSpellID. 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).GetTradeSkillItemIDreturns the produced itemID;GetCraftSpellIDreturns the recipe spellID — because there's no produced item to point at.
GetAverageItemLevel()
Returns avgItemLevel, avgItemLevelEquipped.
avgItemLevelis 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.avgItemLevelEquippedis 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.
GetInventoryItemDurabilitytakes 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 —maxis the discriminator, matching the engine's ownGetInventoryItemBrokenlogic.
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 rangeGetItemBySlotaccepts 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 onUnitPlayerControlledto avoid the engine crash thatGetInventoryItemLinkitself 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/WEAPONOFFHANDconfined 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
GetItemBySlotgates 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
unitmust be a player. For other units, the texture is cleared.slotis an inventory slot number, the same number thatGetInventoryItemTexturetakes.0is the ammo slot.- If the slot is empty, the texture is cleared.
- A
slotthat is not an inventory slot causes an error. The error messages spell the name asSetInventoryPortaitTexture.
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 afileID:numberelsewhere. There is no fileID system here — same situation asC_Spell.GetSpellTextureandC_Spell.GetSpellInfo'siconIDfield. 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(itemLink, quantity) / ItemUtil.GetItemHyperlink(itemID)
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.IterateBagSlotswalks one bag. Ifcallbackstopped the walk, it returnstrue. Otherwise it returnsfalse.ItemUtil.IterateInventorySlotswalks the equipment slots fromfirstSlottolastSlot. 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
ItemUtil.TransformItemIDsToItems(itemIDs) and related functions
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 asItemUtil.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:
- Fast path — the corpse's loot window is already open for the
requested target. Synchronously finds the slot, sends the take
packet, returns
trueif the slot was found /falseif not. - Async path — opens the corpse's window first (same wire as
C_Loot.LootUnit), waits for the response, then sends the take packet automatically. Returnstrueonce the request is queued. No "did it succeed" event; observeBAG_UPDATE/ITEM_PUSHto confirm.
false immediately when:
- Args missing or wrong types (raises an error, not returns false).
- Another
LootUnitItemis 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. TheitemIndexof an item changes when the history is full and drops its oldest item. TherollIDdoes 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 toGetItemInfoandGameTooltip: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—truewhen the item has a winner, or when all players passed.winnerIdx— the player index of the winner, forC_LootHistory.GetPlayerInfo(itemIndex, winnerIdx).nilwhen the item has no winner yet, or when all players passed. The winner is always in the player list.isMasterLoot— alwaysfalse.isCurrency— alwaysfalse.
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) forRAID_CLASS_COLORS.nilwhen the class is not known.rollType—0= pass,1= need,2= greed.roll— the dice roll,1to100.nilwhen 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—truewhen this player won the item.isMe—truewhen 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_INTERRUPTis derived from the server's own lockout packet (Player::ProhibitSpellSchoolsends a singleSMSG_SPELL_COOLDOWNlisting every spell of the interrupted school at one uniform duration), so the school and duration are authoritative. The interrupting spell itself isn't transmitted, hencespellID = 0anddisplayText = "Interrupted".- CC timing comes from the server's timer for your debuff, the same timer
GetPlayerBuffTimeLeftuses. It includes diminishing returns and the caster's talents.startTime/timeRemaining/durationarenilwhen the debuff has no timer. auraInstanceIDis the instance ID of the CC debuff (seeAuraData). It isnilforSCHOOL_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:
#xgives the length of a string, or a border of a table (the value Lua 5.1#returns). It ignorestable.setn.a % buses the Lua 5.1 resulta - floor(a/b)*b, which takes the sign ofb. This differs frommath.mod(Cfmod, truncated toward zero):-1 % 3is2, whilemath.mod(-1, 3)is-1....as an expression yields all the varargs. The...in a function parameter list stays as the vararg declaration.0xFF00becomes its decimal value (65280) — the same number Lua 5.1 produces. The 5.0 lexer rejects0xliterals, so without this an addon needstonumber("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
__lenand__modare 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), wherenameis"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()andstring.upper(s). -
Every function in the
stringtable is reachable as a method. That includes the ClassicAPI additions (match/gmatch,reverse) and any function an addon adds tostring. -
Reading an unknown key gives
nil, the Lua 5.1 result:("x").nopeisnil, and("x"):nope()fails withattempt to call, notattempt 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__indexis thestringtable. You can change the metatable, butsetmetatablecannot 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 positionnonward.nmay be negative to count back from the end (-1is 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.
newproxy([proxy])
Returns a new empty userdata. The argument sets its metatable:
falseor no argument: the userdata has no metatable.true: the userdata gets a new empty metatable. Usegetmetatableto get this metatable and add metamethods to it.- A userdata from
newproxy(true): the new userdata shares the metatable of that userdata.
Any other argument causes the error boolean or proxy expected.
local proxy = newproxy(true)
getmetatable(proxy).__gc = function() print("collected") end
local twin = newproxy(proxy) -- same metatable as proxy
scrub(...)
Returns its arguments. Each argument that is not a boolean, a number or a
string becomes nil.
scrub(1, "a", true, {}, print) -- 1, "a", true, nil, nil
difftime(time2 [, time1])
Returns the number of seconds from time1 to time2. time1 is 0 by
default. Both values are times in seconds, as time() returns.
local start = time()
-- ...
difftime(time(), start) -- the seconds since start
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 callsGetCallstackHeightis level 1. C functions count as levels, the same as indebugstack.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,GetErrorCallstackHeightreturns the value thatSetErrorCallstackHeightset, or nil.SetErrorCallstackHeight(height)sets the height for an error handler that code calls directly, with no error.SetErrorCallstackHeight(nil)removes the value.CallErrorHandlerandassertsafeuse 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.
debugstack(thread [, start [, count1 [, count2]]])
With a coroutine as the first argument, debugstack returns the call stack
of that coroutine. start is 0 by default, which is the innermost level of
the coroutine. Without a coroutine, debugstack works as before.
local co = coroutine.create(function() coroutine.yield() end)
coroutine.resume(co)
print(debugstack(co))
debuglocals([level [, skipFunctionsAndUserdata]])
Returns the local variables and the upvalues of a call level as one string.
An error handler uses it to show the values in the code that caused the
error. level counts the same as in debugstack, and is 1 by default.
Each variable is one name=value line. A string value is in quotes. A
table shows the name of its frame, or <table>, then its fields one space in,
then }. Fields of tables inside a table do not show. A function shows the
file and line where it is defined.
If skipFunctionsAndUserdata is true, function and userdata values do not
show. The text stops at 4095 bytes.
debuglocals works only while an error handler runs. At other times it
returns nothing.
seterrorhandler(function(message)
local level = GetCallstackHeight() - GetErrorCallstackHeight() + 1
print(debuglocals(level, true))
end)
The error window shows these values under Locals.
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 (thegcinfo()value)."collect"— runs a full garbage-collection cycle. Returns0."step"— accepted no-op returningtrue. The 5.0 collector cannot step incrementally;truemeans "cycle finished", sorepeat until collectgarbage("step")loops exit immediately."stop"/"restart"/"setpause"/"setstepmul"— accepted no-ops returning0. 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), or0if 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.
table.removemulti(t [, pos [, count]])
Removes count elements from t, starting at position pos, and returns
the removed elements in order. The elements after them move down to fill the
gap. pos is the last position by default, and count is 1 by default.
If t is empty, the function returns no values. If the range is not inside
the table, the function causes the error parameters out of bounds.
local t = { "a", "b", "c", "d", "e" }
table.removemulti(t, 2, 3) -- "b", "c", "d"
-- t is now { "a", "e" }
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
nfield (the varargargcontract) 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 nexttable.insertadds 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.setnstill works; the heal only changes the answer when the stored length points past the last value and the nil was not put there bytable.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 ontoobject(later arguments win on key collisions) and returnsobject. 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, ornilwhen it doesn't match. Same pattern engine asstring.find— it'sfindreturning the captures instead of thestart, endindices, so it reuses the engine's own matcher rather than reimplementing it.string.gmatch(s, pattern)— the match iterator, forfor x in string.gmatch(s, pat) do ... end. This is exactly Lua 5.0'sstring.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.gmatch is also the
global gmatch.
string.gsub replacements
string.gsub(s, pattern, repl [, n]) replaces each match of pattern in
s. The replacement repl can be a string, a number, a table, or a
function.
- String or number: the text replaces the match. In this text,
%0is the whole match and%1to%9are the captures. If the pattern has no captures,%1is also the whole match.%%is one%character. - Table: the first capture is the key. If the pattern has no captures, the whole match is the key. The value at that key replaces the match.
- Function: the function gets the captures as arguments. If the pattern has no captures, it gets the whole match. Its first return value replaces the match.
If a table value is nil or false, the original match stays in the
string. Any other table value that is not a string or a number causes the
error invalid replacement value (a <type>).
If a function returns a value that is not a string or a number, the match is
removed. This includes nil and false. To keep a match, return the match
itself.
If repl is not one of the four types, the call fails with the error
string/function/table expected.
("hello world"):gsub("%w+", "<%0>")
-- "<hello> <world>", 2
("$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 b c"):gsub("%a", function(c) if c == "b" then return "B" end end)
-- " B ", 3 (the function returns nil for "a" and "c")
string.gsub, the global gsub, and the method form s:gsub(...) are the
same function.
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. Afterpieces - 1splits the rest of the string (delimiters and all) is returned as the final piece.0or omitted = unlimited.1returns 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"
string.byte(s [, i [, j]]) / strbyte
Returns the numeric codes of the bytes of s from position i to position
j, as separate values. i is 1 by default, and j is i by default. A
negative position counts back from the end of the string: -1 is the last
byte. Positions outside the string are clamped to it. If the range is empty,
the function returns no values.
The range can have at most about 2,000 bytes. A longer range causes the
error stack overflow (string slice too long).
string.byte("ABC") -- 65
string.byte("ABC", 2) -- 66
string.byte("ABC", 1, -1) -- 65, 66, 67
strbyte("ABC", -2, -1) -- 66, 67
string.byte("ABC", 4) -- (no values)
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
math.cosh(x) / math.sinh(x) / math.tanh(x)
The hyperbolic cosine, sine and tangent of x.
math.cosh(0) -- 1
math.sinh(0) -- 0
math.tanh(1) -- 0.76159415595576
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, ...valueson success, wherevaluesare whatever the coroutine yielded (mid-execution) or returned (on completion).false, errMsgon error. Two error sources:- The coroutine isn't suspended.
errMsgis"cannot resume <status> coroutine", where<status>isrunning,normalordead. - The body raised an error: a Lua VM error, or a call to
error.errMsgis the error message, usually with a file:line prefix.
- The coroutine isn't suspended.
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 example0 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 (1to23), orbag slot. It also takesspell: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:1539and#showtooltip 1539both 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/castlines, 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.
#showtooltipwith 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),/castrandomand/userandom(the first entry), and/equipand/equipslot(items only).#showtooltip noneor#show noneshows 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] Rejuvenationalone shows?with nothing under the cursor, and[@mouseover][] Rejuvenationshows 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.IsActionInRangethen 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:
GetMacroInforeturns nils.PickupMacroandDeleteMacrodo nothing.EditMacrochanges 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
CastSpellNoToggleand were last edited under stock 1.12) will have a stale0cache — 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 theInterface\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
indexdoes 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-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.
attachmentIndexis anything other thannilor1.- 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:
messageIndexis out of range (< 1or> GetInboxNumItems()).- The message slot is empty / has no attached item.
attachmentIndexis anything other thannilor1.
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
WorldMapAreaname, which matchesGetMapInfo()'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"), soGetMapAreaIDs()[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
tilesexists (don't derive the grid yourself). The canonical layout isceil(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; trustingceilshears 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/textureHeightremain 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— theMap.dbcmap the zone sits on (0Eastern Kingdoms,1Kalimdor, or an instance map).worldPosition— aVector2DMixinof the world coordinates (.xnorth,.ywest).
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— theAreaTable.dbczone the point falls in.mapPosition— aVector2DMixinwith.x/.yin0..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
C_Map.GetUserWaypointHyperlink() / C_Map.GetUserWaypointFromHyperlink(hyperlink)
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, ornilfor a missing / out-of-range id.GetAreaTriggers([mapID])— an array of every trigger's info table, optionally filtered to a singleMap.dbcid (0Eastern Kingdoms,1Kalimdor,30Alterac Valley,489Warsong 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/mapYline up withpfQuest-turtle's scrapedareatrigger-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 dropGetAreaTriggers()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— aCreateVector2D(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 [, displayID])
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. If you give displayID, the model shows that
display in place of the creature's own display.
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:SetItem(itemID [, appearanceModID, itemVisualID])
Shows the model of one item: a weapon, a shield, a helm, a shoulder, a
quiver, or ammo. The item has its skin texture and its glow, if it has one.
appearanceModID has no effect.
itemVisualID is an ItemVisuals ID, for example the glow of a weapon
enchant. The model shows it only when the item has no glow of its own.
A helm model is made for one race and sex. The model shows the helm for the race and sex of your character.
An item that has no model of its own, for example a chest item, shows nothing. If the item is not in the client item cache, the model shows nothing at first. The client requests the item, and the model shows it when the data arrives.
The model shows the item until the next SetItem, SetDisplayInfo,
SetCreature, or SetUnit call. This is also true when you hide the frame
and show it again.
PreviewModel:SetItem(19019) -- Thunderfury
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)
model:SetUnit(unit)
Shows a unit on the model. Returns true when the unit exists, and false
when it does not. An unknown unit token is an error.
if not PreviewModel:SetUnit("target") then
PreviewModel:SetUnit("player")
end
model:CanSetUnit(unit)
Returns true when the unit exists, so SetUnit can show it. The call
does not change the model. An unknown unit token is an error, the same as
in SetUnit.
model:GetDisplayInfo()
Returns the display ID (a CreatureDisplayInfo ID) that the model shows:
- After
SetDisplayInfoorSetCreature, the display that the model got. - After
SetUnit, the display that the unit has now. For a shapeshifted druid, this is the display of the form.
Returns 0 when the model shows nothing or an item, or when its unit no longer exists.
model:SetAnimation(animation [, variation])
Sets the animation that the model plays. animation is an AnimationData
ID, for example 0 Stand, 4 Walk, 5 Run, 60 EmoteTalk, 67 EmoteWave, 69
EmoteDance. variation selects one of the model's versions of the
animation. The default is 0. The value -1 selects a version at random.
The model plays the animation again each time it ends. The model goes back
to Stand when the frame loads a new model: after SetUnit, RefreshUnit,
SetDisplayInfo, SetCreature, SetItem, or DressUpModel:Dress(), and
when the
frame shows again after it was hidden (see SetKeepModelOnHide). To go
back to Stand at any time, call SetAnimation(0).
If the model does not have the animation, it plays the fallback animation
that AnimationData gives, or Stand. Use HasAnimation to find out first.
While the animation is not Stand, SetRotation turns the model without the
step animation.
Call SetAnimation after SetUnit, SetDisplayInfo, or SetCreature. The
animation starts when the model finishes loading.
PreviewModel:SetUnit("player")
PreviewModel:SetAnimation(69) -- dance
model:FreezeAnimation(animation, variation, frame)
Holds the model still in one pose of an animation. animation and
variation are the same as in SetAnimation. frame is the time into the
animation, in milliseconds. A time past the end of the animation wraps to
the start.
The model holds the pose until the frame loads a new model, the same as
with SetAnimation. To make the model move again, call SetAnimation.
PreviewModel:FreezeAnimation(69, 0, 500) -- dance pose, 0.5 seconds in
model:HasAnimation(animation)
Returns true when the model has the animation (an AnimationData ID).
Returns false until the model finishes loading. A model file that the
client already loaded in this session is ready at once.
local anim = PreviewModel:HasAnimation(69) and 69 or 0
PreviewModel:SetAnimation(anim)
model:SetDoBlend([doBlend])
Sets how the model starts the animation that SetAnimation set. If
doBlend is true, the model blends from the animation that plays now into
that animation. If doBlend is false or not given, the animation starts
at once. The default is false.
model:GetDoBlend()
Returns the value that SetDoBlend set.
model:SetKeepModelOnHide(keepModelOnHide)
Sets whether the frame keeps its model while the frame is hidden. The
default is false: the frame removes the model when it hides, and loads
the model again from its unit, display, or item when it shows.
If keepModelOnHide is true, the model comes back as it was when the
frame shows again, with its animation and the items that TryOn put on
it. If you call SetUnit, RefreshUnit, SetDisplayInfo, SetCreature,
SetItem, or DressUpModel:Dress() while the frame is hidden, the frame loads the
new model when it shows, the same as with false.
DressUpModel:SetKeepModelOnHide(true)
model:GetKeepModelOnHide()
Returns the value that SetKeepModelOnHide set.
model:ApplySpellVisualKit(spellVisualKitID [, oneShot])
Shows the effects of a spell visual kit (a SpellVisualKit ID) on the
model, for example the glow on the hands while a spell is cast. Each effect
attaches to its point on the model: head, chest, base, hands, breath, or
weapons. An effect whose point the model does not have does not show.
If oneShot is true, each effect plays one time and then fades out. If
oneShot is false or not given, the effects play again and again until
the model changes. The model changes after SetUnit, SetDisplayInfo,
SetCreature, SetItem, and when the frame shows again after it was
hidden.
A kit can also change the color or the transparency of the model. This
change stays until the model changes. The kit does not play its animation
or its sound. A kit ID that is not in SpellVisualKit does nothing.
PreviewModel:SetUnit("player")
PreviewModel:ApplySpellVisualKit(99) -- holy glow on both hands
PreviewModel:ApplySpellVisualKit(154, true) -- Holy Light flash, one time
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 [, handSlotName, spellEnchantID])
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".
handSlotName sets the hand that holds a weapon, a shield, or an off-hand
item. Give "MAINHANDSLOT" or "SECONDARYHANDSLOT". Upper and lower case
are the same. If the item in the other hand cannot be held with the new
item, the model removes it. Armor ignores handSlotName.
If handSlotName is nil or another name, the model chooses the hand. A
one-hand weapon goes into the off hand only when the main hand holds a
one-hand weapon and the unit can dual wield.
spellEnchantID is an enchant ID from SpellItemEnchantment.dbc, for
example 1900 (Crusader). The weapon shows the glow of that enchant. If
spellEnchantID is nil, the weapon shows the glow of the enchant in the
item link. An item that has its own glow keeps its own glow.
If the item is not in the item cache, the model wears it when the data arrives. But then the model chooses the hand, and the weapon shows no enchant glow.
TryOn returns an Enum.ItemTryOnReason value:
Success(0): the model wears the item.NotEquippable(2): the model cannot wear the item. Necklaces, rings, trinkets, and items that are not equipment give this result. Armor gives this result when the model shows a shapeshifted unit or a creature.DataPending(3): the item is not in the item cache. The game requests the item. When the data arrives, the model wears the item.
WrongRace (1) is in the enum, but TryOn does not return it.
local result = DressUpModel:TryOn("|cff1eff00|Hitem:2077:0:0:0|h[Magician Staff]|h|r")
if result == Enum.ItemTryOnReason.DataPending then
print("The item loads now.")
end
DressUpModel:IsGeoReady()
Returns true when the model file and all item models on it have
loaded. Otherwise it returns false. A model that the frame does not show
also gives false.
DressUpModel:SetSheathed([sheathed, hideWeapons])
Moves the main-hand and off-hand weapons of the model. If sheathed is
true, the weapons go to their sheath positions. If sheathed is false
or nil, the weapons go into the hands. If hideWeapons is true, the
model shows no main-hand or off-hand weapon.
The setting stays on the frame. It applies to weapons that TryOn adds
later. It also applies when the model is built again: when you call
SetUnit or Dress, or when the frame shows again. SetSheathed does not
move a ranged weapon. The weapons keep their enchant glow.
Before the first SetSheathed call, the model holds its weapons the same
way as its unit. Weapons that TryOn adds then go into the hands.
DressUpModel:SetSheathed(true) -- weapons on the back and hips
DressUpModel:SetSheathed(false, true) -- no weapons
DressUpModel:SetSheathed() -- weapons in the hands
DressUpModel:GetSheathed()
Returns the sheathed value of the last SetSheathed call on the frame.
Before the first call, it returns false.
DressUpModel:SetAutoDress([enabled])
Sets whether the model wears the equipment of its unit. If enabled is
true, SetUnit shows the unit with its equipment. If enabled is
false or nil, SetUnit shows the unit with no equipment. The default
is true.
The setting applies the next time that the model is built: when you call
SetUnit, or when the frame shows again. Dress() always dresses the model
in the equipment of its unit.
DressUpModel:SetAutoDress(false)
DressUpModel:SetUnit("player") -- no equipment
DressUpModel:TryOn(2077) -- only the staff
DressUpModel:GetAutoDress()
Returns the value of the last SetAutoDress call on the frame. Before the
first call, it returns true.
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:
- Name-query responses. Every
SMSG_NAME_QUERY_RESPONSEthe 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. C_PlayerCache.RememberPlayer(no-op when the cache is off). Lets addons add sources the engine never sees, such as/whoresults.- 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:
- The object manager — any currently-synced unit (a player you can see, your target, a party or raid member, an NPC in range).
- 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.
- The engine name cache — every player name that the client got in this session, in range or not.
- 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:
- Your party and raid members. This includes members that are out of range or in a different zone.
- Players near you.
- Your friends list, including offline friends.
- 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.
- 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 formatUnitGUIDreturns). 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 againstChrClasses.dbcfilename 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, againstChrRaces.dbc. Omitted/unknown → leaves prior race alone.sex(optional) —0(male) or1(female), matching the wire-format convention the engine cache stores.UnitSexuses2/3; passUnitSex - 2if 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_RESPONSEthe engine processes is also written toWTF\ClassicAPI\NameCache-<realm>@<realmList>.txt. - Lua-side
C_PlayerCache.RememberPlayercalls become effective (they're no-ops when the cache is disabled). GetPlayerInfoByGUIDgains 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:
- 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. - RequiredLevel ≤ player level.
- AllowableClass / AllowableRace masks include the player.
- 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)"). - 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.
- RequiredHonorRank / RequiredCityRank — PvP-rank gates (the player's honor rank and earned PvP-medal bitmask).
- 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 returnsfalsewith no async load fired. Warm the cache (C_Item.RequestLoadItemDataByID) and re-check onITEM_DATA_LOAD_RESULTif 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:
unitandguid. 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.GetNameuses the polymorphic object name getter (the same path asUnitNameFromGUID);GetSexreads theUNIT_FIELD_BYTES_0gender byte and reports it asUnitSex's 2/3;IsConnectedtreats a currently-synced object as online and otherwise consults the group-member roster's online bit, exactly asUnitIsConnecteddoes.IsConnectedwith no argument defaults to the local player. GetRace/GetClassreturn the numeric ids thatUnitRace/UnitClassdon't (those give only names).classIDround-trips withC_CreatureInfo.GetClassInfoandraceIDwithC_CreatureInfo.GetRaceInfo;GetClass'sclassName/classFilenameare 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/UnitClassdirectly.
PvP
SetPVP(enable) / C_PvP.SetPVP(enable)
Asks the server to turn the PvP preference of the player on or off.
enableistrueor a nonzero number to turn the preference on.false,nilor0turns 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.yielddoes 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.
isHeaderandisCollapsedaretrueorfalse.isCompleteis1when the objectives are done,-1when the quest failed, andnilfor all other cases.suggestedGroupis always0.frequencyis alwaysEnum.QuestFrequency.Default.questIDis0for a header row.startEventand all values afterquestIDare alwaysfalse.
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
0for header rows (zone / category dividers). - Returns
nilif 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.
successis1ornil. The engine's printf-style event dispatcher has no%btoken, so we encode the boolean as1for success /nilfor failure.if success then ...works as expected (nilfalsy,1truthy). Same encoding asITEM_DATA_LOAD_RESULTandGET_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_RESULTdoesn't fire withsuccess=0either. Addons that need to handle "request timed out" should use their own timer (the Lua polyfill at!!!ClassicAPI/Util/QuestUtil.luauses 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_RESPONSEpacket 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 (3120Verdant Sigil — Night Elf Druid only), a dungeon quest (914), and a timed delivery (3364Scalding 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. Thepoifield 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-basedobjectiveIndexcounts 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.
questLogIndexis 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.RequestLoadQuestByIDfor 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
QueryQuestsCompletedcall. - After each quest turn-in.
- When an addon sends the
.queststatuseschat 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 numericspellID. The first argument can be:
- number — spellID, used directly.
|Hspell:N|hhyperlink — extracted by parsing for the embeddedspell:Npayload, soC_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 chainCastSpellByNameuses). 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
nilrather than raising a Lua usage error — the permissiveC_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 theC_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 (
0x04is a syntax error); pass mask values as decimals, or viatonumber("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.
costis the cost you pay, with your talents and gear. Rage is in whole points (Heroic Strike reports15).isFunnelistruewhen the spell costs power every second, such as Health Funnel or Drain Soul. Thencostis the cost per second.castTimeis your cast time in milliseconds, with your talents and cast speed. It is0for an instant or passive spell.minRangeandmaxRangeare your range in yards, with your talents. Both are0for 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 stockGetSpellName(slot, bookType).slotis 1-based,bookTypeis"spell"(player) or"pet". The slot is resolved to a spellID via the engine's spellbook array, then the same DBC reads run. Returnsnilfor 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 returnsnil(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 sameSpellName(Rank N)formCastSpellByNameaccepts. A space before the parenthesis is allowed ("Mind Blast (Rank 8)"). A rank you do not know returnsnil.GetSpellInfo("|Hspell:ID|h[Name]|h")— a spell hyperlink. ThespellIDinside 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
spellIDat 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.
iconIDis a path string. This value can be afileID:numberelsewhere. There is no fileID system here — assets are referenced by path strings. We surface the icon path so it's directly usable withtexture: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 inSpell.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:numberelsewhere. There is no fileID system here — see the same note onC_Spell.GetSpellInfo'siconIDfield. Pass directly totexture: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:SetSpellByIDand 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 at27. Sap and Gouge report14(incapacitated) — the30("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
dieSidesabove1means 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 rawSpellRadius.dbcvalue; caster-independent, what an AOE tracker watching other units wants.modifiedRadius—baseRadiuswith the local player's talent / item modifiers applied (e.g. a Mage's Arctic Reach). Equal tobaseRadiuswhen 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 (themodifiedRadiusreturn) 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'scostfield is the baseManaCost(intrinsic spell data, caster-independent) — same base/modified split asGetSpellRadius.
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 returnfalseeven though the player conceptually "has" them: a player with 5/5 Unbreakable Will seesIsPlayerSpell(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:IsSpellKnownis the strict "in the spellbook UI" check,IsPlayerSpellis the broad "any kind of known" query. The split matters because:
Spell type IsPlayerSpellIsSpellKnownTrained class ability (Fireball) truetrueActive talent grant (Mortal Strike) truetruePassive talent (Wand Spec, Imp Fireball) truefalseProfession recipe (Bolt of Linen) truefalseUse
IsPlayerSpellfor "do I have access to this effect at all"; useIsSpellKnownfor "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.
GetSpellBonusDamagereads the client's fully-computed value directly from the CGPlayer sub-struct (PLAYER_FIELD_MOD_DAMAGE_DONE_POS − _NEGper 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'sGetSpellPowerreads.)
GetSpellBonusHealinghas 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 fromSpell.dbc: (1) flat healing = the sum of everySPELL_AURA_MOD_HEALING_DONEoff gear equip-effects, item enchants (permanent + weapon oils), random suffixes, and active buffs; plus (2) talent/buff stat-conversions, mirroring the server's ownSpellBaseHealingBonusDone— the sum ofSPELL_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 addsint(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.bookTypeis"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.GetSpellCooldownand lockouts withC_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 byFUN_006E4AD0when 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
truefor 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.
targetis 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 withCastSpellByName. - 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 globalUnitCastingInfo. A global here would clobber addons that ship their own cast-tracking via the_G.UnitCastingInfo or <fallback>idiom — e.g. ShaguTweaks'libcastscrapes 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 underC_Spellinstead. Same forUnitChannelInfo/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, stampingstartTimeMs = now,endTimeMs = now + effective cast time); other units come from a co-hook onSMSG_SPELL_START(Spell::Cast), the one packet carrying another unit's cast with a server-authoritative cast time, cached per caster GUID. SoC_Spell.UnitCastingInfo("target")now backs enemy/target/focus/nameplate cast bars. Instant casts returnnil(no cast bar). Aborted remote casts (interrupt, death, movement, cancel) clear the same tick — a co-hook on the engine's ownCGUnit_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 theSMSG_SPELL_FAILURE/SMSG_SPELL_FAILED_OTHERpackets 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 remotestartTimeMs/endTimeMsare shifted later by roughly your latency (SMSG_SPELL_STARTcarries only a singlecastTime, so we stampstart = now, end = now + castTimeon receipt — the packet has no field to recoverelapsedfrom, so the skew can't be removed); and remote pushback is invisible — the server sendsSMSG_SPELL_DELAYEDonly 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.
isTradeskillis real — the spell'sSPELL_ATTR_TRADESPELLflag, so profession recipe casts (smelting, cooking/enchanting recipes, …) returntruewhile regular spells returnfalse.endTimeMsreflects 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.castIDis the cast's castGUID — the exact string theUNIT_SPELLCAST_*events carry for this cast, so you can correlate the polled info with the events (works for the player and other units).delayTimeMsis the pushback the cast has taken so far, in milliseconds.castBarIDhas no source here and is alwaysnil.
notInterruptibleistruewhen 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 istruein 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 alwaysfalse. Limits: a creature's built-in interrupt immunity is server-side data that the client never receives, so a boss with that immunity readsfalsewhile your interrupt still fails on it. When the value changes during the cast of your target or focus,UNIT_SPELLCAST_INTERRUPTIBLEorUNIT_SPELLCAST_NOT_INTERRUPTIBLEfires.
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
unittakes the same unit tokens asC_Spell.UnitCastingInfo.- The time span is the same as the
startTimeMsandendTimeMsofC_Spell.UnitCastingInfoorC_Spell.UnitChannelInfo, in seconds. The same limits apply. UnitChannelDurationreturns nothing for a unit whose channel you did not see begin.C_Spell.UnitChannelInfogivesniltimes 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_DELAYEDorUNIT_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/MaxTargetLevelspell fields the server also enforces are server-only columns — they aren't present in the 1.12 client'sSpell.dbc, so thespellLevelrule is the only client-readable target-level mechanism.
Stand, dismount and form settings (autoStand, autoDismount, autoUnshift)
Three settings change what occurs when your state blocks a cast or another
action. Each one is 0 (the default) or 1. At 0, the action 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. For example, if the
spell needs a target and you have none, you stay mounted. If the spell shows
the targeting cursor, the action occurs when you select the target. 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.
The settings also apply to these actions:
| Action | autoStand |
autoDismount |
autoUnshift |
|---|---|---|---|
| Talk to an NPC | — | — | You leave the form. |
| Use an object | — | You dismount. You stay mounted at a mailbox. | You leave the form. |
| Loot a creature | You stand up. | You dismount. | — |
| Loot a corpse | You stand up. | — | — |
| Use an item | — | — | You leave the form. |
| Attack | — | You dismount. | — |
| Open the flight map | You stand up. | You dismount. | You leave the form. |
A form that can talk and use objects, for example Shadowform, stays on when
you talk to an NPC or use an object. While autoUnshift is 1, the cursor
over an NPC or an object shows its usual icon in such a form. While
autoDismount is 1, the cursor shows the attack icon over an enemy while
you are mounted.
You dismount to attack only when the target is in melee range. Farther away, the attack does not start and "Out of range." shows. With click-to-move on, no error shows and you walk to the target; click the target again when you arrive.
To open a locked object, such as a chest, you cast a spell. Thus the cast rules above apply.
/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_UPDATEwithSPELL_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
nilif 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.
iconIDis a texture path, not afileID(there is no fileID system) — the same deviation asC_Spell.GetSpellInfo.skillLineIndexis not returned (nil) — useC_SpellBook.GetSpellBookItemSkillLineIndex.isOffSpecis alwaysfalse— 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
GetCurrentLevelSpellsreturns 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 levelmodifier— temporary bonus (weapon oils, buffs;0if 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.dbcrow (most temporary auras, proc-only spells, item-on-use effects, GM-debug spells) - rows whose
skillIDdoesn't resolve inSkillLine.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
timeris not a timer name, the function returnsnil. - The result can go below
0or abovemaxValuebefore 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
requireFormistrueand you have no shapeshift form, the result is0, 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.
3for Travel; Turtle uses9for 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 … |rcolor codes → removed|H…|h text |hhyperlinks → keeps only the visible text|T…|ttextures and|A…|aatlases → removed|n→ newline,||→ literal|- unknown
|xsequences 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.0for 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 synthesizesCF_TEXTfor 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 exerciseremoveMarkup.
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, defaultfalse. 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 tabIndexortalentIndexout of range for the class- explicit
rankexceeding the talent's allocated max — e.g. asking for rank 5 on a 1-rank talent like Inner Focus - a
classIDthat 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— readsTalentEntry+0x00from the per-tab talent arrays at[0x00BDCD28](the engine's runtime state for the local player's class). Same struct walk asGetTalentSpellID. - With
classID— reads theTalent.dbc/TalentTab.dbcflat 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, soGetTalentIDByIndex(t, i)andGetTalentIDByIndex(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.
facingis the direction in radians, measured clockwise on the screen.0is up (ahead of the camera),math.pi / 2is right,math.piis down and3 * math.pi / 2is left.angleis 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
targetNearestDistanceyards (default 41). Units behind you count up totargetNearestDistanceRadiusyards (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.
unitis 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
exactMatchis 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.
unitandexactMatchare the same as forInteractUnit. Use"anyinteract"for the object that the Interact Key uses.looseTargetingistrueif you do not give it. When it istrueand you have a target, the interaction does not change your target. When it isfalse, or when you have no target, a unit that you interact with becomes your target.
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 now. This is true also when the target is behind you. When your target is too far away,"anyinteract"is the same as"softinteract". If"softinteract"is empty,"anyinteract"is your target. When 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:- your target, when it qualifies as a soft enemy
"softenemy"- your target
"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 istruewhileSoftTargetForceis1and soft enemy is on, or2and soft friend is on. Otherwise it isfalse.
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 byuseAtlasSize.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
heightof0uses the atlas's own pixel height. - A
widthof0keeps the atlas's own shape at the height you asked for. - Sizes are relative to a 14-point font, as in
|T…|tmarkup: 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 positiveoffsetYmoves 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.
SetPortraitTextureFromCreatureDisplayID(textureObject, creatureDisplayID)
Shows the round portrait of a creature on a texture, by its display ID (a
CreatureDisplayInfo ID). A unit is not necessary. The function returns
nothing.
The texture changes at once. The portrait is transparent until the creature model loads, and then it shows, a short time after the call.
A character-based display gets the same appearance as a spawned NPC of that display. Like the portrait of a unit, the portrait does not show helms, shoulders, or weapons.
A display ID that is not in CreatureDisplayInfo does not change the
texture. Some models have no portrait view. For these models, the portrait
stays transparent.
Each display ID is drawn one time. Later calls for the same display ID show the finished portrait at once. The portraits stay until you log out.
local frame = CreateFrame("Frame", nil, UIParent)
frame:SetWidth(64)
frame:SetHeight(64)
frame:SetPoint("CENTER", 0, 0)
local portrait = frame:CreateTexture(nil, "ARTWORK")
portrait:SetAllPoints(frame)
SetPortraitTextureFromCreatureDisplayID(portrait, 3254)
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
GetServerTimefunction 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. CallClassicAPI.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, orrdtsc), and both functions pay the same Lua→C call overhead, which dominates. SoGetTimeCached()is not meaningfully faster thanGetTime()— its only advantage is the frame-stable semantics. To actually cut cost in a hotOnUpdate, 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 andC_Timercallbacks run earlier in the frame thanOnUpdatescripts. 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 theBAG_UPDATE_DELAYEDflusher uses, so there's no per-feature hook overhead. - Resolution is one frame (~16ms at 60fps). A timer scheduled for
0.001seconds will fire on the next tick, same as one scheduled for0. - 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.
GetServerTimeandGetSecondsUntilDailyResetreport the instant, so they are the ones to compare, sort, store, and count down with.GetCurrentCalendarTimeandGetServerTimeLocalreport the wall clock, for display, and always agree withGetGameTime().Daily reset semantics.
GetSecondsUntilDailyResetcounts 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.GetSecondsUntilWeeklyResetwould 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.
GetTotemInfogives 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.
GetTradeSkillListLink()
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, asGetTradeSkillLinegives them.GUID: the GUID of your character, in hexadecimal.bits: the recipes that you know, six recipes for each base64 character.
GetCraftListLink()
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_LISTfires, the player tabs target, or a party member loads in. If you cache a result, re-verify withUnitGUID(token) == originalGuidbefore reusing it.Returns
nilfor 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.
textureis a Texture object.unitis any unit token."softinteract"and"anyinteract"can also point at a game object or a corpse.stylehas no effect.- When
includeLowPriorityistrue, the normal pointer also counts as an image. When you do not give it, theSoftTargetLowPriorityIconscvar 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.
A quest cursor (see Expanded cursors)
has no image, also when includeLowPriority is true.
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.0when stationary; otherwise the walk/run/swim/ backward variant the unit is actually moving with. Read via the engine's own selector at0x007C4C90so 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 bySMSG_FORCE_RUN_SPEED_CHANGEand friends.flightSpeed— always0.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:
- Calls
UnitName(input)vialua_pcallto canonicalize the input. Tokens resolve cleanly; literal names hit the engine's "Unknown unit name" error whichpcallswallows. - For valid tokens, attempts a fast direct comparison against the
player's guild-key field at
[unit + 0xE68 + 0x0C]— same fieldGetGuildInforeads, populated immediately on guild join for the local player and for any synced player-controlled unit. No roster fetch needed for nearby/loaded units. - Falls back to walking the engine's guild roster array (same
backing storage
GetGuildRosterInforeads) by name. Required for out-of-range party members, distant raid members, and literal name input — needsGuildRoster()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 real0(self, or two exactly co-located units) is indistinguishable by value from the miss placeholder, always branch oncheckedPosition.
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
UnitPositionwith 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")istrue(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 is
1=Warrior, 2=Paladin, 3=Hunter, 4=Rogue, 5=Priest, 7=Shaman,
8=Mage, 9=Warlock, 11=Druid. IDs 6 and 10 are not used.
local token, id = UnitClassBase("player") -- "WARRIOR", 1
local token = UnitClassBase("target") -- works for any synced unit
local color = RAID_CLASS_COLORS[UnitClassBase("party1")]
Use the token for class checks. The first return of
UnitClass(unit) is localized (for example, "Krieger" on a
German client). It is good for display, but a check such as
if class == "WARRIOR" fails on other locales. The classID is a
ClassicAPI extension: the standard UnitClassBase returns only
the token.
UnitClassBase finds the unit and its class the same way as
UnitClass, so the two functions always agree:
"player"gives a result before the player is in the world (for example, during addon load).- Party and raid members that are out of range give a result if the client has their name data.
- Creatures give the same class as
UnitClassgives for them.
Returns (nil, nil) when UnitClass returns (nil, nil). For
example, "target" with no target, or an empty partyN / raidN
slot.
Throws a Lua error if unit is missing or is not a string, the
same as UnitClass.
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
Use the token for race checks. The first return of
UnitRace(unit) is localized (for example, "Mensch" on a German
client).
UnitRaceBase finds the unit and its race the same way as
UnitRace, so the two functions always agree. The notes for
UnitClassBase about "player",
out-of-range group members and creatures also apply here.
Returns (nil, nil) when UnitRace returns (nil, nil). Throws a
Lua error if unit is missing or is not a string.
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
creatureID(number) — the NPC ID (the valueC_CreatureInfo.GetCreatureID(guid)returns, and the entry packed into a creature's GUID).
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
ClosestUnitPositionthat 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:
unitis 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:
- Auras that you cast, or that a unit you control cast (your pet, a guardian, a totem, or a charmed creature).
- Auras that you could apply yourself (
canApplyAurais true). - 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. InGetUnitAuras, 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. FillssourceUnit/sourceGUID+ timing for directly-cast auras.OnAuraAdded(0x006123F0) — fires when any aura first occupies a slot, including proc/triggered auras that emit noSMSG_SPELL_GO(Shadow Weaving etc.). StampsexpirationMs = 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/sourceGUIDnil, non-playerexpirationTime0). - 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_00604ea0callsOnAuraStacksChangedunconditionally, so its silence proves nothing arrived). The countdown therefore runs to 0 and the entry evicts; once evicted,expirationTimefalls back to 0 (unknown) while the aura is still shown. Climbing stacks and single-stack refreshes (re-cast → freshSpellGo) 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:
applicationsis always1— stack counts aren't transmitted.expirationTime/sourceUnit/sourceGUIDare present only if you cast the aura (theAura::SourceSMSG_SPELL_GOentry is merged in when it matches); otherwise the usual defaults.durationis 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
UnitBuffshares.
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 ofUnitBuffandUnitDebuffmakes.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 withC_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.dllis 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 intoGetTtsVoices(). Note this is an explicit argument —SpeakTextdoes not read thettsVoicecvar itself, matching Blizzard's contract. PassC_TTSSettings.GetSpeechVoiceID()to honor the configured default.text(string, required) — UTF-8; spoken as-is.destination(number, optional, default1) —1=LOCAL_PLAYBACK,4=QUEUED_LOCAL_PLAYBACK. Accepted for parity; both queue FIFO through SAPI (no app-side queue).rate(number, optional, default0) — speech rate, clamped to[-10, 10].volume(number, optional, default100) — 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
XMLTemplateListInfocarries only thenameandtypefields, 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
sourceLocationfield (file:line where the template was defined); no source location is recorded for XML nodes, so the field is omitted (reads asnil).
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
inheritsapply, 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.loggets a warning for an unnamed type, a type withinherits, 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 atparent.Icon, whereparentis the frame that holds the layer.target="$parentIcon"names a texture by its global name.$parentat 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"/>