docs/API.md is 18.7k lines and its anchors are signature slugs, so editing a signature breaks the 281 internal and 88 README links into it. Blizzard solved the same problem by shipping the API as data plus a browser addon, and warcraft.wiki.gg renders those same tables. One set of descriptors can feed all three. Each registration now carries one next to it: typed arguments and returns, nilability, defaults, and a sentence. Events, enumerations and returned table shapes are described the same way. No Script_* body changed. Game.h gains namespace Game::Doc (Field with Req/Opt/Vararg, FieldList, Function, Method, Structure, Event) and a trailing descriptor argument on all six Game::Lua::Register* plus Event::Custom::AutoReserve. api/Documentation.cpp records every registration on the first pass of each Lua state and seals it, so /reload re-registers but records nothing and the environment comes from which pass saw the name. Export is lazy: nothing is materialized until C_APIDocumentation.GetSystem asks for one system, because eager export would build thousands of tables per reload for a slash command. _classicapi_UndocumentedAPI lists what still has no descriptor. AddOns/!!!ClassicAPI/APIDocumentation/ ports Blizzard_APIDocumentation's eight files. Named /classicapi, not /api: the browser holds only what ClassicAPI adds, so /api would have implied the whole 1.12 surface. Two deliberate differences from the retail source, both noted in the file headers: a system search also matches its namespace, since that is the name people type; and an undocumented function says so, because an empty signature otherwise reads as a real one. LinkUtil gains the SetItemRef to LinkUtil.ProcessLink dispatch. That handler registry was dead code; a new link type now registers a handler instead of stacking another SetItemRef override. Also defines assertsafe there, which was undefined and only mattered once the function went live. Documented: the C_Spell and C_SpellBook surface, the spell globals, both spellbook enumerations, the 13 UNIT_SPELLCAST_* events, GameTooltip as the widget-method example and C_Glue as the glue and mixed-environment example. 66 of 692 function registrations. tools/New-ApiDocSkeletons.ps1 emits paste-ready skeletons to make the rest of the sweep tractable. Verified in game: the browser lists, searches, and prints detail; links render and click through; right-click copies and shift-click dumps.
794 KiB
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
-
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.GetAddOnLocalTable(name)- Conditional and multi-flavor TOC loading
- SavedVariables loaded first
-
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)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)
-
C_Container.GetContainerItemID(bagIndex, slotIndex)C_Container.GetContainerItemInfo(containerIndex, slotIndex)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)
-
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()
-
- 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_EventUtils.IsEventValid(eventName)GetFramesRegisteredForEvent(event)PLAYER_ENTERING_WORLDpayload (isInitialLogin,isReloadingUi)PLAYER_TOTEM_UPDATEeventBAG_UPDATE_DELAYEDeventPLAYER_EQUIPMENT_CHANGEDeventUPDATE_INVENTORY_DURABILITYeventHEARTHSTONE_BOUNDevent- Player input-state events (
PLAYER_STARTED_MOVING/LOOKING/TURNING+STOPPED_*) GLOBAL_MOUSE_DOWN/GLOBAL_MOUSE_UPeventsAUCTION_MULTISELL_START/AUCTION_MULTISELL_UPDATE/AUCTION_MULTISELL_FAILUREeventsCURSOR_CHANGEDeventEQUIPMENT_SETS_CHANGEDeventEQUIPMENT_SWAP_PENDINGeventEQUIPMENT_SWAP_FINISHEDeventFACTION_STANDING_CHANGEDeventLOOT_HISTORY_ROLL_CHANGED/LOOT_HISTORY_ROLL_COMPLETE/LOOT_HISTORY_FULL_UPDATEeventsLEARNED_SPELL_IN_SKILL_LINEeventLOOT_SCAN_COMPLETEDeventLOSS_OF_CONTROL_ADDED/LOSS_OF_CONTROL_UPDATEeventsMODIFIER_STATE_CHANGEDeventNAME_PLATE_CREATED/NAME_PLATE_UNIT_ADDED/NAME_PLATE_UNIT_REMOVEDeventsPLAYER_FOCUS_CHANGEDeventQUEST_ACCEPTEDeventQUEST_REMOVEDeventQUEST_TURNED_INeventSOUNDKIT_FINISHEDeventUNIT_FACTIONevent (fire-coverage fix)UPDATE_MOUSEOVER_UNITevent (loss-fire fix)UPDATE_SHAPESHIFT_FORMeventUNIT_SPELLCAST_*events
-
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()
-
region:SetPoint("point")(one-argument form)region:SetSize(width, height)/region:GetSize()region:IsMouseOver([topOffset, bottomOffset, leftOffset, rightOffset])region:GetRect()region:IsDragging()GetMouseFoci()frame:SetShown(shown)fontstring:GetStringHeight()fontstring:GetUnboundedStringWidth()fontstring:GetWrappedWidth()fontstring:GetNumLines()fontstring:GetLineHeight()fontstring:IsTruncated()fontstring:SetMaxLines(maxLines)/fontstring:GetMaxLines()fontstring:SetFormattedText(format [, ...])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:SetDesaturation(amount)texture:GetDesaturation()- Texture size and shape
fontstring:SetRotation(angle [, cx, cy])editBox:SetCursorPosition(position)editBox:GetCursorPosition()editBox:GetUTF8CursorPosition()editBox:ClearHighlightText()editBox:HasFocus()editBox:HasText()editBox:SetHighlightColor(r, g, b [, a])editBox:GetHighlightColor()editBox:ClearHistory()frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])frame:HookScript(scriptType, handler)frame:IsEventRegistered(event)frame:RegisterUnitEvent(event, ...units)frame:GetEffectiveAlpha()frame:SetAttribute/SetAttributeNoHandler/ClearAttribute/GetAttribute(+ unit-frame mouseover)SetModernScriptArgs(enable)/GetModernScriptArgs()SecureCmdOptionParse(options [, quiet])RegisterStateDriver/UnregisterStateDriverRegisterAttributeDriver/UnregisterAttributeDriverRegisterUnitWatch/UnregisterUnitWatch/UnitWatchRegisteredSecureButton_GetAttribute/SecureButton_GetUnitPreClick/PostClickbutton scriptsGetClickFrame(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)
-
GameTooltip:SetSpellByID(spellID)GameTooltip:AddSpellByID(spellID)GameTooltip:SetTalentByID(talentID)GameTooltip:SetInventoryItemByID(itemID)GameTooltip:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])GameTooltip:IsEquippedItem()OnTooltipSet*scripts (Item / Spell / Unit / GameObject)GameTooltip:SetItemByGUID(itemGUID)GameTooltip:SetEquipmentSet(name)GameTooltip:SetTotem(slot)GameTooltip:GetItem()GameTooltip:GetSpell()GameTooltip:HasItem()/GameTooltip:HasSpell()GameTooltip:GetUnitGUID()/GameTooltip:HasUnit()GameTooltip:GetGameObject()/GameTooltip:HasGameObject()GameTooltip:GetOwner()
-
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_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.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)C_Item.GetItemStats(itemLink)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)C_Item.IsEquippableItem(item)IsUsableItem(item)/C_Item.IsUsableItem(item)C_Item.IsEquippedItem(item)C_Item.IsItemDataCachedByID(item)/C_Item.IsItemDataCached(itemLocation)C_Item.IsItemGUIDInInventory(itemGUID)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)C_Item.UseItemByName(item [, unit])Get*ItemID— companions to the engine'sGet*ItemLinkfamilyGetAverageItemLevel()GetInventoryItemDurability(invSlot)GetInventoryItemID(unit, slot)GetInventoryItemRepairCost(invSlot)GetInventoryItemsForSlot(slot, returnTable [, transmogrify])GetItemIcon(itemID)/C_Item.GetItemIcon(itemLocation)/C_Item.GetItemIconByID(item)OffhandHasWeapon()
-
- Lua 5.1 syntax
- Upvalue limit
- String methods (
s:upper(),s:format(...)) getfenv/setfenvenvironment protectionselect(index, ...)unpack(list [, i [, j]])xpcall(f, msgh [, arg1, ...])collectgarbage(opt [, arg])table.wipe(t)table.count(tbl)table.maxn(t)- Stale table lengths (5.1 healing)
Mixin(object, ...)/CreateFromMixins(...)string.match/string.gmatchstring.gsubtable replacementstrsplit(sep, str [, pieces])strjoin(delimiter, ...)strtrim(str [, chars])strreplace(str, find, replace)string.reverse(s)/strrev(s)math.fmod(x, y)math.modf(x)math.hugecoroutine.create(fn)coroutine.resume(co, ...)coroutine.yield(...)coroutine.status(co)coroutine.wrap(fn)coroutine.running()- Async pattern (RunAsync + C_Timer.After)
-
/castand/use- More commands with
[conditions] #showtooltipand#show- Numeric spellIDs in
/castandCastSpellByName CastSpellNoToggleas a macro cast lineGetMacroSpell(macroSlot)GetMacroItem(macroSlot)GetMacroIcons/GetMacroItemIcons/GetLooseMacroIcons/GetLooseMacroItemIconsC_Macro.CreateMacro/C_Macro.EditMacroC_Macro.GetMacroIcon(macroSlot)C_Macro.SetMacroDisplay(macroSlot, value)
-
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)USER_WAYPOINT_UPDATEDevent
-
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])
-
PlaySound(soundKitID)/PlaySound(soundName)C_Sound.PlaySound(soundKitID [, channel, forceNoDuplicates, runFinishCallback])C_Sound.PlaySoundWithOptions(params)C_Sound.GetSoundScaledVolume(soundHandle)C_Sound.IsPlaying(soundHandle)C_Sound.PlayItemSound(soundType, item)C_Sound.PlayVocalErrorSound(vocalErrorSoundID)MuteSoundFile(file)/UnmuteSoundFile(file)C_Sound.GetRecentSoundFiles()
-
C_Spell.DoesSpellExist(spellID)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.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()IsUsableSpell(spell)/IsUsableSpell(slot, bookType)C_Spell.IsSpellUsable(spellID)C_Spell.GetSpellCooldown(spellIdentifier)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)CastSpellNoToggle(name | spellID [, unit [, placeGroundSpell]])C_Spell.CastAtCursor(spellIDOrName)C_Spell.CastAtUnit(spellIDOrName, unit [, placeGroundSpell])C_Spell.CancelSpellByID(spellID)/CancelSpellByName(name)C_Spell.UnitCastingInfo(unit)/C_Spell.CastingInfo()C_Spell.UnitChannelInfo(unit)/C_Spell.ChannelInfo()C_Spell.GetSpellLevelInfo(spellID)GetSpellRequiredTargetLevel(spellID)
-
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)
-
GetServerTime()GetTimeCached()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])UnitSubName(unit)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)UnitInLineOfSight(unit)ClosestUnitPosition(creatureID)UnitHealthMissing(unit)UnitPower(unit [, powerType [, unmodified]])/UnitPowerMax(unit [, powerType [, unmodified]])UnitPowerMissing(unit [, powerType [, unmodified]])UnitPowerType(unit)UnitSpellHaste(unit)UnitSpellTargetName(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.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)
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 nil for empty slots.
local actionType, id, subType = GetActionInfo(1)
| Slot contents | Returns |
|---|---|
| empty | nil |
| spell | "spell", spellID, "spell" |
| macro | "macro", macroSlot |
| item (by-itemID) | "item", itemID |
| item (by bag-instance) | "item", nil |
The 120-slot action table at 0x00BC6980 packs each entry as a
uint32 where the top 4 bits are the type tag:
0x0— spell action. Entry is the spellID. The third return is always"spell"for entries on this table; the engine helper hardcodes pet flag to 0 here. Pet-bar actions live in a separate table — usePetHasActionBar+ the pet-action helpers for pet slots.0x4— macro or bag-item. Ambiguous tag, disambiguated by walking the 36-entry macro-slot map at0x00BDCC60and checking whetherentry & 0xBFFFFFFFmatches any of those macro IDs. On a hit, the slot is a macro and the matching index becomes the 1-basedmacroSlotreturn. On a miss, the slot is a bag-item reference; we surface the type as"item"but returnnilfor theid— extracting the itemID from the bag-action hash struct needs more reversing than we've done.0x8— item by itemID.entry & 0x7FFFFFFFis the itemID.
This is the same discrimination SuperWoWhook uses in
its replacement of GetActionText (which returns name, type, id).
Without SuperWoWhook the engine's stock GetActionText only handles
items in bags; ClassicAPI's GetActionInfo provides the full
name, type, id shape independently of any other DLL patches.
Subtype is always "spell" for spell entries (no pet differentiation on this
table) and the bag-item itemID path is currently incomplete — see
the comment in src/action/Info.cpp.
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("DebugTools") -- "DebugTools"
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("DebugTools") -- "UI Debug Tools"
C_AddOns.GetAddOnNotes(indexOrName)
Returns the ## Notes: from the .toc. nil for missing
addons or addons without notes.
C_AddOns.GetAddOnNotes("DebugTools")
-- "Tools for developing addons (backport of Blizzard_DebugTools 3.3.5 to 1.12.1 / Lua 5.0)"
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("DebugTools") -- 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("DebugTools") -- 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("DebugTools") -- 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("DebugTools") -- 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.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("DebugTools") -- 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.
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()
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.
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.)
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
GetCurrentChatGUID()
Returns the sender's GUID for whichever CHAT_MSG_* event is
currently being dispatched, or nil if called outside a chat
event or for a synthetic chat with no real sender (e.g.
CHAT_MSG_SYSTEM).
Format matches UnitGUID: "0xHHHHHHHHLLLLLLLL" (16 hex digits,
hi dword first).
local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_CHANNEL")
f:RegisterEvent("CHAT_MSG_SAY")
f:RegisterEvent("CHAT_MSG_WHISPER")
f:SetScript("OnEvent", function()
local guid = GetCurrentChatGUID()
if guid then
local _, class, _, race = GetPlayerInfoByGUID(guid)
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"%s: %s [%s %s]", arg2, arg1, class or "?", race or "?"))
end
end)
The CHAT_MSG_* events don't include the sender GUID in their
payload. Addons that need to identify chatters reliably (rather than by
sender name,
which is locale-fragile and ambiguous across realms) currently
strcmp against an addon-maintained name cache. This function lets
them skip that work.
Implementation: hooks the engine's chat dispatcher at
FUN_CHAT_DISPATCH (0x0049A870), which receives the sender
GUID as args 11/12 of a 12-arg __fastcall. The hook stashes the
GUID into a static global before forwarding to the original
function; the engine then fires the appropriate CHAT_MSG_*
event synchronously inside that window, so GetCurrentChatGUID()
called from the addon's OnEvent sees the right GUID. The global
is restored to its prior value (not cleared) on hook return, so
nested chat dispatch (e.g. an addon calling SendChatMessage
from its handler) doesn't lose the outer context's GUID.
The function returns nil rather than "0x0000000000000000"
for synthetic chat (system messages, login banners, etc.) where
the GUID args are zero — matches the idiomatic
if GetCurrentChatGUID() then check.
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.
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. If there is no valid target, nothing happens.
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
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.
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.
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)
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.
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 | Always false — there is no bag search-filter system. |
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
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 equivalent 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?" — same as GetItemCount(water, false, true)
local total = 0
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
if C_Container.GetContainerItemID(bag, slot) == waterID then
total = total + C_Container.GetContainerItemCharges(bag, slot)
end
end
end
A ClassicAPI extension (addons otherwise read charges off tooltip
text). Useful when you need a per-slot number rather than
GetItemCount's rollup — e.g. to display "X charges" on the slot UI
for a wand without re-walking every other matching item.
C_Container.GetContainerNumFreeSlots(bagID)
Returns the number of empty slots in the given bag, plus the bag's
BagFamily bitfield (the type of items the bag is restricted to).
numberOfFreeSlots, bagType = C_Container.GetContainerNumFreeSlots(bagID)
bagID = 0— the player's main backpack. Always reports(freeCount, 0)— the backpack is unfamilied (general-purpose).bagID = 1..4— the player's equipped bag slots. Slot count and bagType come from the bag item's cachedItemStats_Crecord —m_containerSlotsandm_bagFamilyrespectively. If no bag is equipped (or the item somehow isn't cached):(0, 0).- Other
bagIDvalues (bank, keyring, out-of-range):(0, 0). Always returns two values, never nil.
local free, bagType = C_Container.GetContainerNumFreeSlots(0)
-- free = number of empty slots in backpack, bagType = 0
for bag = 0, 4 do
local free = C_Container.GetContainerNumFreeSlots(bag)
-- ...
end
bagTypeis derived for specialty bags. Bags leave the raw BagFamily field empty in Turtle's data (Soul Bag, Herb Bag, quiver, ammo pouch, … all read 0). We fall back to deriving the family from the bag's class + subclass, so a Soul Bag reports0x4here, a quiver0x1, an ammo pouch0x2— the same valuesC_Item.GetItemFamilyreports for both the bag and the items it holds. This also meansCalculateTotalNumberOfFreeBagSlotsnow correctly excludes specialty bags from the general-purpose free-slot total (it previously counted them, since the raw field was 0). Note the Turtle custom families (Meat/Fish/Leather/Mining,0x200–0x1000) collide with the standard bit meanings — see theGetItemFamilyfamily table for the caveat.
The bitmask encoding.
bagTypeis the bit position (1 << (familyID - 1)), not the raw stored familyID, so callers can bitwise-AND with itemFamily values fromC_Item.GetItemFamilydirectly. We convert internally — see that function's notes for the encoding-shift story.
Implementation walks the bag using
Item::Location::ResolveBag
internally and counts slots that resolve to a null CGItem * (i.e.,
empty). Cross-checked in-game against a manual
C_Container.GetContainerItemID walk; counts match.
C_Container.GetContainerFreeSlots(bagID)
Returns which slots in a bag are empty, as a table of slot numbers in ascending order.
freeSlots = C_Container.GetContainerFreeSlots(bagID)
bagID = 0— the player's main backpack.bagID = 1..4— the player's equipped bag slots. If no bag is equipped there, the call returns nothing.- Other
bagIDvalues (bank, keyring, out of range) return nothing.
A full bag returns an empty table. A bag that is not there returns nothing at all, so an empty table and a missing bag stay easy to tell apart:
local freeSlots = C_Container.GetContainerFreeSlots(bagID)
if freeSlots then
for _, slot in ipairs(freeSlots) do
-- slot is empty
end
end
Use C_Container.GetContainerNumFreeSlots
when you only need how many there are.
C_Container.GetContainerItemQuestInfo(containerIndex, slotIndex)
Returns the quest state of one bag slot, as a single table.
questInfo = C_Container.GetContainerItemQuestInfo(containerIndex, slotIndex)
The table holds three fields:
isQuestItem—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 joined with", ", in the order the sets are stored.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 — backpack plus any equipped bags 0..4 whose
BagFamily is 0. Specialty bags (quivers, soul bags, herb bags, …) are
excluded, matching FrameXML's implementation:
local total = 0
for bag = 0, 4 do
local free, family = C_Container.GetContainerNumFreeSlots(bag)
if family == 0 then total = total + free end
end
-- total == C_Container.CalculateTotalNumberOfFreeBagSlots()
Takes no arguments.
Same sparse-
BagFamilycaveat asGetContainerNumFreeSlots: because the server data leaves most bags' family field at 0, specialty bags that would normally be excluded are counted here. The result stays consistent with the per-bagGetContainerNumFreeSlotsvalues it sums.
C_Container.IsContainerItemOpenable(containerIndex, slotIndex)
Positional-arg wrapper for C_Item.IsItemOpenable
against a bag/slot pair. Same (isOpenable, canOpen) tuple — see
the linked section for full semantics. Both returns are nil for
empty slots or items whose data hasn't been cached yet.
-- Sweep main backpack for openables
for slot = 1, GetContainerNumSlots(0) do
if C_Container.IsContainerItemOpenable(0, slot) then
-- ...
end
end
Same cache-warming caveat as the underlying C_Item.IsItemOpenable —
a freshly-seen item may report false for one call while the
SMSG_ITEM_QUERY_SINGLE round-trips. Bag-resident items are almost
always already cached.
C_Container.PlayerHasHearthstone()
Returns the itemID of the hearthstone if one is in the player's bags,
or nil otherwise.
itemID = C_Container.PlayerHasHearthstone()
if C_Container.PlayerHasHearthstone() then
-- player can hearth
end
Match logic. An item counts as a hearthstone if either:
- 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, best quality first
- 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.
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. The sort leaves such an item alone, 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.
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()
Stacks combine less reliably here than in your bags. To combine stacks inside the bank, the client has to work out what fits, and that needs each item's max stack size. Until an item's data arrives from the server that number is unknown, so a stack the client cannot measure is left alone. Sorting your bags does not have this limit.
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.
Creature
C_CreatureInfo.GetCreatureID(guid)
Extracts the creature template / NPC ID from a unit GUID. The entry ID is packed directly into bits 24-47 of the 64-bit GUID for the types that carry one; this function does the shift and mask so addons don't have to.
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)
Accepts creature GUIDs (0xF130xxxx…) and pet GUIDs
(0xF140xxxx…). Returns nil for:
- non-string input or malformed GUIDs
- player GUIDs — the low 32 bits hold a player ID, not a 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
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.
fontHeight sets the icon height in UI pixels. When omitted (or 0),
each coin sizes itself to the font of the fontstring that shows the
string — the default.
GetCoinTextureString(123456) -- "12g 34s 56c" with coin icons
GetCoinTextureString(5) -- "5c" with a copper icon
GetCoinTextureString(5, 24) -- same, with a 24px coin
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.
CVar
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.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), 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 (equipment slot, action bar, trade, mail) | "item", itemID |
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 |
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 availability
Returned only when the cursor holds a bag item (type 1) —
PickupContainerItem / PickupInventoryItem from a bag slot, mail
or trade detach. The cursor stores the item's GUID in this case, so
we resolve back to the live CGItem * and feed it to the engine's
link builder, which produces the full per-instance form including
enchant ID and random-suffix decoration:
|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
Not returned for drag-source items (equipment slot, action bar
slot, trade slot, mail attachment — types 5/6/7/9). In those cases
the cursor only stores the itemID (no GUID, no live CGItem *), so
the engine's link builder isn't reachable. Derive a basic link from
the itemID via GetItemInfo if you need one for display:
local kind, itemID, link = GetCursorInfo()
if kind == "item" and not link then
link = (select(2, GetItemInfo(itemID))) or ("item:" .. itemID)
end
Types we don't surface
The engine has cursor types beyond what GetCursorInfo advertises —
pet action drag, trade-slot drag, mail-attach drag, ability-bar drag
from PickupAction, etc. These don't map cleanly to any GetCursorInfo
string, so we return nil for them. Consumers needing to detect those
can use the engine-native CursorHasItem / CursorHasSpell /
CursorHasMoney and the source-side PickupX calls.
Changes to what the cursor holds are reported by the
CURSOR_CHANGED event.
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, snapshotted at CreateEquipmentSet /
SaveEquipmentSet time. UseEquipmentSet searches every player-owned
container for those GUIDs and dispatches pickup→equip pairs for items
that aren't already where they belong. The search reads the
underlying GUID arrays directly (same trick C_Item.GetItemCount
uses for its includeBank=true path), so bank items resolve even
without the bank window being open — the bank gate at
VAR_BANK_GATE_GUID only suppresses GetItemBySlot, not the
underlying data.
One limitation worth knowing about:
- Equipping from the bank is not supported by the protocol
—
UseEquipmentSetskips bank items rather than try and fail. Retrieve the items first, then re-run.
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 true unconditionally. There is no banker/feature gate on
equipment-set storage; the feature ships for every character.
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
isEquipped is true when every resolvable item in the set is in
its target slot (missing items don't disqualify — useful so a set
that includes a bank-stored cloak still shows as equipped after you
swap in the rest). numItems excludes ignored slots; numIgnored
counts them separately. 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 hash table { [slot] = itemID } for every set slot whose
item is currently resolvable. Missing items (GUID stored but client
can't find a CGItem) are omitted because the engine doesn't keep an
itemID separate from the live CGItem record.
C_EquipmentSet.GetItemLocations(setID)
Returns a hash table { [slot] = locationCode }. 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 (1-based) within the container |
| 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..28 | `BANK | slot` |
| Bank bag 5..10 | `BANK | BAGS |
Special values returned in the table entry:
1— slot is ignored (GetIgnoredSlotslists these)-1— item is missing (was in the set, can't find now)
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.
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.
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)
Walks the set and dispatches one atomic server-side swap per item
that isn't already in its target slot. Items in the bank are skipped
silently (the protocol can't equip from bank). Missing items are skipped
silently. Returns true if the call ran (the set existed), false
otherwise.
Implementation uses the same FUN_INVENTORY_SWAP primitive
C_Item.EquipItemByName
uses for its explicit-slot path. Each swap is a single
CMSG_SWAP_INV_ITEM (or CMSG_AUTOEQUIP_ITEM) packet that the server
applies atomically — the two-cycle "ring A in slot 11, ring B in
slot 12, set swaps them" case resolves in one packet because the
opcode swaps both slots in a single server transaction. The cursor
is never touched; an item held on the cursor when
UseEquipmentSet is called stays on the cursor.
Longer dependency chains (3+ items rotating) are rare with 1.12's slot set — the realistic conflicts are paired slots (rings 11/12, trinkets 13/14, weapons 16/17), all 2-cycles.
CURSOR_UPDATEfires per item moved. The engine's swap primitive (FUN_005E0C40) runs a generic cursor-state cleanup at the end of each call, which firesCURSOR_UPDATEregardless of whether the cursor was actually touched. So oneUseEquipmentSetcall that moves N items will fire NCURSOR_UPDATEevents in quick succession. Addons that react toCURSOR_UPDATE(cursor-attached tooltips, drag-state tracking) should debounce — or checkCursorHasItem()before doing work — since most of those fires won't reflect a real cursor change. This is engine behavior, not a bug in the implementation; the old cursor-based path actually fired more (one per pickup, one per equip, one per cursor-clear).
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.
PLAYER_TOTEM_UPDATE event
Fires with arg1 = totemSlot (1 Fire, 2 Earth, 3 Water,
4 Air) whenever a totem is dropped, expires, or is destroyed early
(killed / Totemic Recall) — 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)
-- re-read the changed slot
end)
Driven by the same GetTotemInfo tracker: the drop
is detected from SMSG_SPELL_GO, removal from the WorldTick object-manager
scan, and the event is flushed from the tick (so handlers run in a safe
context, not mid-packet). Removal detection carries up to the tracker's
scan interval (~250 ms) of latency; drops fire on the next frame.
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 an equipped item's durability changes:
combat damage, resurrection penalty, repairs. The standard consumer
pattern works unchanged: rescan the 19 slots with
GetInventoryItemDurability on each fire.
local f = CreateFrame("Frame")
f:RegisterEvent("UPDATE_INVENTORY_DURABILITY")
f:SetScript("OnEvent", RefreshMyDurabilityHUD)
Event-driven, no polling: piggybacks on the engine's own
inventory-alerts recompute — the routine behind
UPDATE_INVENTORY_ALERTS (the red/yellow "broken armor man"), which
the engine runs whenever an owned item's fields change, on equip
changes, and at enter-world. On each recompute the DLL diffs a
per-slot {item, durability} snapshot and fires once when anything
actually changed — a death dinging all your armor produces one event,
not nineteen. Also fires when the set of equipped items changes
(swapping to a differently-damaged item updates durability UI, so
consumers want the refresh anyway). Bag-carried items don't trigger
it: durability UI is for equipped gear only.
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.
Note: the server re-sends the bind packet on map/zone transitions (behind the loading screen). Those resyncs are suppressed by gating on the engine's in-world flag, so the event fires only for an actual innkeeper bind, never 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.
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
Fires on every raw mouse-button press or release while WoW has focus. Payload is the button identifier string.
local f = CreateFrame("Frame")
f:RegisterEvent("GLOBAL_MOUSE_DOWN")
f:RegisterEvent("GLOBAL_MOUSE_UP")
f:SetScript("OnEvent", function(_, event, button)
print(event, button) -- "GLOBAL_MOUSE_DOWN", "LeftButton"
end)
| Payload | Source |
|---|---|
"LeftButton" |
VK_LBUTTON |
"RightButton" |
VK_RBUTTON |
"MiddleButton" |
VK_MBUTTON |
"Button4" |
VK_XBUTTON1 |
"Button5" |
VK_XBUTTON2 |
Distinct from per-frame OnMouseDown scripts — these fire even
when the click misses every UI frame (clicks in the world / on
nothing). Distinct from PLAYER_STARTED_LOOKING /
PLAYER_STARTED_TURNING — those are camera-rotation events;
these are raw input events.
Focus-gated. Polls Win32 GetAsyncKeyState per frame on the
shared Tick::WorldTick registry. DOWN transitions are only
honored when WoW is the foreground window — clicking in another
app while alt-tabbed doesn't fire. UP transitions always fire,
even if the user alt-tabbed mid-click, so an addon never gets
left in a "button is held" state.
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, or a money amount in copper.0when the cursor was empty.
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 a single payload arg — setID — at the start of
UseEquipmentSet, right after the set-exists check passes and
before any pickup/equip work begins. Addons use this to
gate "swap in progress" UI state (grey out the set button, show
a spinner, etc.) until EQUIPMENT_SWAP_FINISHED arrives.
Doesn't fire if UseEquipmentSet is called with an unknown
setID — in that case only EQUIPMENT_SWAP_FINISHED(false, setID)
fires.
EQUIPMENT_SWAP_FINISHED event
Fires at the end of every UseEquipmentSet call with two payload
args: success (1 if the set existed and we dispatched the swap, 0
if the setID was unknown) and setID. Note this is "we ran the
dispatch" success — not "every item ended up in its target slot."
Items that were in the bank or that couldn't complete a swap cycle
in one pass still report success=1. Listen for this if you want to
re-paint the character pane / refresh tooltips after a swap.
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.
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 rolled or passed on an item. |
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 reconstruction and 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.
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.
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)
Fires for every path that adds a quest to the local log — NPC accept,
party-shared quest accept, auto-grant from quest items — by hooking
the single engine chokepoint (FUN_QUEST_LOG_REBUILD at 0x004DE510)
that rebuilds the Lua-visible quest log from the player's
authoritative slot data after any quest state change.
Does not fire on initial login / character entry, even though the
same engine function runs the bulk-sync there. Suppression is
heuristic: if a single rebuild call adds more than one quest, it's
treated as a resync and skipped. Human input speed can't accept two
quests within the same engine tick, so single-add is always a real
user accept. A brand-new character's very first quest accept
(0 → 1 entries) fires correctly.
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)
Synthesized from the same FUN_QUEST_LOG_REBUILD pre-/post-snapshot
diff as QUEST_ACCEPTED — the removal side of the delta. For
turn-ins, QUEST_REMOVED fires after QUEST_TURNED_IN: the
SMSG_QUESTGIVER_QUEST_COMPLETE packet doesn't
touch the log itself; the removal arrives in the follow-up quest-log
update packets, whose rebuild triggers the diff. The observed turn-in
sequence is QUEST_TURNED_IN → UNIT_QUEST_LOG_CHANGED →
QUEST_LOG_UPDATE (fired inside the rebuild) → QUEST_REMOVED.
Does not fire on login / character-switch resyncs. Same
suppression rule as QUEST_ACCEPTED (a user action removes at most
one quest per rebuild), plus one extra gate: a rebuild that removes
one quest while adding several is a cross-character resync shape, not
gameplay, and stays silent.
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.
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. |
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.
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.
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. Thirteen events are provided;
six also fire for non-player units:
| 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 (kick, movement, LoS) — never for channels | all | same |
UNIT_SPELLCAST_FAILED |
a cast is rejected before it starts (range, mana, cooldown) with an error shown | 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 (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.
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: a spell that never started (out of
range, not enough mana, on cooldown, LoS to a target) fires FAILED, except
for a fixed whitelist of "quiet" SpellCastResult codes that fire
FAILED_QUIET instead — SPELL_IN_PROGRESS (casting while already casting /
a spell-queue rejection), DONT_REPORT (fake fails, a cancelled ground
reticle), and CHARMED. A spell that was already casting and gets stopped
(an enemy kick, moving to cancel, breaking LoS mid-cast) fires INTERRUPTED.
Holding the cast key while running fires INTERRUPTED repeatedly (once per
retry), each reusing the interrupted cast's castGUID.
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.
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
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.
Focus
A focus-target system: a single sticky GUID that addons can pin to a
unit and address via the "focus" unit token across the entire UnitX
API surface. Backed by a hook on FUN_TOKEN_TO_GUID (shared with the
nameplateN tokens — see
unit/TokenExtensions.cpp).
State is session-only — drops on /reload, /logout, and zone
loads that recreate Lua state. Addons that want persistence have to
re-FocusUnit from SavedVariables at ADDON_LOADED.
Auto-clear on despawn. When the focused unit leaves the client's
object table — out of rendering range, full despawn, dies and
decays — focus drops and PLAYER_FOCUS_CHANGED
fires. The unit re-entering range does NOT auto-refocus.
Implementation: per-tick ObjectByGUID(g_focusGUID) probe; on null
result, Set(0).
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". UNIT_HEALTH, UNIT_MANA,
UNIT_AURA, UNIT_LEVEL, UNIT_MODEL_CHANGED, … fire with
arg1 == "focus" whenever the focused unit's corresponding descriptor
field changes — even when it isn't your target, so a focus frame keeps
updating while you fight something else. Backed by Unit::TokenObserver,
which registers the engine's own unit-event descriptor-field observers
for the focus unit (the exact watch the engine gives target/party/raid).
A unit that is simultaneously your target and focus fires both
"target" and "focus". (OnEvent handlers read the arg1
global, not a function parameter.)
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
The direct-action and temporary override binding APIs. The permanent helpers write action command strings into the native binding table; the override helpers add a separate, session-only layer in front of that table.
Permanent direct-action bindings
| Function | Equivalent command |
|---|---|
ok = SetBindingSpell(key, spell) |
SetBinding(key, "SPELL " .. spell) |
ok = SetBindingItem(key, item) |
SetBinding(key, "ITEM " .. item) |
ok = SetBindingMacro(key, macroNameOrIndex) |
SetBinding(key, "MACRO " .. macroNameOrIndex) |
ok = SetBindingClick(key, buttonName [, mouseButton]) |
SetBinding(key, "CLICK " .. buttonName .. (mouseButton and ":" .. mouseButton or "")) |
key is any binding string accepted by the client, such as "CTRL-F",
"SHIFT-BUTTON4", or "F8". spell, item, macroNameOrIndex, and
buttonName identify the action to perform. macroNameOrIndex accepts either
a saved macro's name or its decimal index. mouseButton is passed to the
button's click handler and defaults to "LeftButton"; when supplied, it is
stored as the :MouseButton suffix of the CLICK command.
Each helper returns 1 if the stock SetBinding changed the binding and
nil otherwise. It changes whichever binding set is currently loaded, fires
the normal UPDATE_BINDINGS notification, and is not persisted until
the addon calls SaveBindings. Use SetBinding(key) to unbind a key;
the typed helpers always require an action argument.
The command hook also understands manually constructed SPELL, ITEM,
MACRO, and CLICK strings passed directly to SetBinding.
Temporary override bindings
SetOverrideBinding(owner, isPriority, key [, command])
SetOverrideBindingSpell(owner, isPriority, key, spell)
SetOverrideBindingItem(owner, isPriority, key, item)
SetOverrideBindingMacro(owner, isPriority, key, macroNameOrIndex)
SetOverrideBindingClick(owner, isPriority, key, buttonName [, mouseButton])
ClearOverrideBindings(owner)
owner must be a frame and isPriority must be a Boolean. The typed helpers
construct the same four action strings shown above. The generic
SetOverrideBinding also accepts ordinary Bindings.xml command names, which
are delegated to the engine's command executor. Override setters and
ClearOverrideBindings return no values.
Passing no command (or nil) to SetOverrideBinding removes that owner's
override for key. ClearOverrideBindings(owner) removes every override
belonging to that owner. Overrides never change or save the native binding
table and are cleared whenever the FrameScript Lua state is recreated, such
as on /reload or logout.
Resolution order is:
- Priority override bindings.
- Non-priority override bindings.
- The normal native binding.
When multiple owners set the same key at the same priority, the most recently set override wins. Removing it immediately reveals the next matching override or the unchanged native binding.
key is matched case-insensitively, and its modifiers are accepted in any
order: "ctrl-alt-F", "ALT-CTRL-F", and "Alt-Ctrl-F" all bind the same
key. The override layer normalizes the key to the engine's own keypress form
(ALT-CTRL-SHIFT- before the base key) before matching, so the override fires
regardless of how the caller spelled it.
A generic SetOverrideBinding command that is an ordinary Bindings.xml command
runs through the engine's own command executor with the same execution context
a native keypress establishes, so it behaves like the equivalent permanent
binding.
Action execution
| Command | Execution path |
|---|---|
SPELL name |
Calls the stock CastSpellByName(name). The usual /cast spell-name syntax is accepted. |
ITEM item |
Calls C_Item.UseItemByName(item), accepting the names, item strings, and item IDs supported by that API. |
MACRO nameOrIndex |
Resolves a saved macro by name or decimal index. An addon-provided RunMacro is used when available; otherwise ClassicAPI resolves it with GetMacroInfo and dispatches its body through ChatEdit_ParseText. |
CLICK ButtonName[:MouseButton] |
Resolves the named global frame and calls its ordinary :Click(mouseButton) method. The default is LeftButton. |
SetBindingMacro and SetOverrideBindingMacro take a saved macro identifier,
not literal macro text. To bind literal macro text, bind a named button whose
ClassicAPI attributes describe a macro action:
local button = CreateFrame("Button", "MyMacroTextBindingButton")
button:SetAttribute("type", "macro")
button:SetAttribute("macrotext", "/say Bound macro text")
SetBindingClick("CTRL-F7", button:GetName())
1.12 compatibility notes
- There is no combat lockdown or secure execution. These functions remain
callable in combat, and
CLICKinvokes the frame's ordinary click path. - Direct actions run on key-down. The corresponding key-up is consumed; a click binding does not deliver separate mouse-down and mouse-up events.
- The override layer is not reflected by
GetBindingAction. TheGetBindingAction(key, true)override query is not backported. - Override ownership is explicit. Call
ClearOverrideBindings(owner)when the addon's owner is no longer needed; all remaining overrides are also cleared when the Lua state is recreated.
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()
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.
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.
Uses the engine's own rect getters and cursor position, dividing the
cursor by the region's effective scale — the exact computation addons
have long open-coded as MouseIsOver(). Returns false for a
region with no resolved rect (unpositioned / zero-size).
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+GetWidth+GetHeight).
Composed from the engine's own getters, so all UI-scale conversion is its
code. 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: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
GetMouseFoci()
Returns a table of the mouse-enabled frames under the cursor, from top
to bottom. Index 1 is the frame that GetMouseFocus() returns, so
GetMouseFocus() == GetMouseFoci()[1] is always true.
local foci = GetMouseFoci()
-- foci[1] == GetMouseFocus()
for i = 1, table.getn(foci) do
print(foci[i]:GetName() or "<anonymous>")
end
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).
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.
Editbox text is not adjusted — an editbox shows and measures raw markup.
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: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: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.
This is the way to make a truncating label. Fontstrings always wrap
at spaces, and there is no SetWordWrap. So SetMaxLines(1) gives a
single-line label that ends in ... when the text is too wide, even
inside a tall box.
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:SetFormattedText(format [, ...])
Sets the text to string.format(format, ...) — a convenience over
SetText(format(...)). A bad format string raises a normal Lua error.
The set goes through the same engine path as SetText, so escape
handling is identical.
f:SetFormattedText("%d/%d (%.1f%%)", cur, max, cur / max * 100)
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
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.
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.
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.
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: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
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.
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
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.
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: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.
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:HookScript(scriptType, handler)
Adds HookScript to every frame. It chains: the previously-set handler
runs first, globals-style (no arguments — it reads the
this/event/argN globals like every handler), then handler is
invoked with positional arguments: (frame, event, arg1..arg9) for
OnEvent scripts and (frame, arg1..arg9) for everything else. So
handlers written as function(self, ...) ... end work unmodified.
Globals-style handlers passed to HookScript also keep working — the
globals are set as usual and extra arguments are simply ignored.
button:HookScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText("hello")
GameTooltip:Show()
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 — its own GetAlpha() multiplied
by every ancestor's, i.e. the opacity WoW actually composites at render
time (fading UIParent fades all of its children). The engine exposes only
GetAlpha (a frame's own alpha); this walks self → parent → … via the
engine's own GetAlpha/GetParent, so it sees exactly what the engine
does — including the 8-bit quantization of alpha (SetAlpha(0.5) stores
127/255 ≈ 0.498, and effective alpha is the product of those).
UIParent:GetEffectiveAlpha() -- 1
-- with UIParent at 0.5 and Minimap's own alpha 1:
Minimap:GetEffectiveAlpha() -- ~0.498 (0.5 truncates to 127/255)
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
SetAttributeNoHandler sets the value without firing OnAttributeChanged;
SetAttribute fires it (see below).
Unit-frame mouseover — the headline use. Setting a string unit
attribute makes the frame a mouseover source: while the cursor is over it,
the mouseover unit token resolves to that frame's unit. 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.
How it works, and why it's robust: rather than installing OnEnter/OnLeave
on the frame (which an addon's own SetScript("OnEnter", …) would overwrite —
pfUI sets unit before its scripts, for instance), this mirrors SuperWoW's
SetMouseoverUnit. The engine's mouse-focus frame is watched once
per frame; when a hovered frame carries a unit attribute, the engine's real
mouseover setter is invoked for that unit — 1:1 with hovering the unit's 3D
model: the model highlights, the mouseover tooltip builds, and
UPDATE_MOUSEOVER_UNIT fires, in addition to the GUID slot being set. So
everything that reads mouseover — the resolver's mouseover branch,
GameTooltip:SetUnit("mouseover"), UnitX("mouseover"), mouseover-cast — sees
it natively. Because nothing touches the frame's scripts, an addon setting its
own handlers can't break it; and it's stomp-proof (the engine's 3D-hover setter
is event-driven, so while the cursor is over UI nothing overwrites the slot). It
follows live token changes (a unit="target" frame tracks your current target
while hovered). 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 macro text. To give the macro a unit, the handler selects the clicked unit, runs the macro, then restores the target from before the click. It prefers an addon RunMacro (for example, SuperCleveRoidMacros — named macros and extended conditionals). If no addon RunMacro exists, it runs the text natively, line by line, through the stock ChatEdit_ParseText. |
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. As with all frame scripts, the
handler takes no parameters and reads its context from globals: this = the
frame, arg1 = the (lowercased) attribute name, arg2 = the new value.
f:SetScript("OnAttributeChanged", function()
if arg1 == "unit" then
MyFrame_Update(this, arg2) -- react to unit changes
end
end)
f:SetAttribute("unit", "party1") -- fires the handler (arg1="unit", arg2="party1")
f:SetAttributeNoHandler("unit", "party2") -- sets it silently, no handler
It's implemented the same way as the tooltip-side OnTooltipSet* scripts — a
co-hook on the base-frame script-name resolver that hands out an external
per-frame handler slot for this one name (frames are never destroyed, so
the slot never goes stale). Recursion-guarded, so a handler may itself call
SetAttribute.
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),OnValueChanged(self, value),OnUpdate(self, elapsed). OnEvent:(self, event, arg1..argN).
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. 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)
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 current button is N (defaults to the left button; 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, subtype, or slot is equipped (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.
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 OnClick. They are
real scripts: set them with SetScript, read them with GetScript, and hook
them with HookScript.
One click runs the three handlers in this order:
PreClick -> OnClick -> PostClick
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).
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)
The button must also have an OnClick handler. PreClick and PostClick
fire around OnClick. A button with no OnClick set fires neither. This serves
the normal use: run code just before or after a button's click action.
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")
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:SetSpellByID(spellID)
Renders a spell tooltip for any spellID, including spells the player has
not learned. The stock 1.12 GameTooltip:SetSpell(slot, bookType) only works
for entries in the player's spellbook (or pet book). SetSpellByID bypasses
the spellbook indirection and calls WoW's internal tooltip builder directly.
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetSpellByID(133) -- Fireball
GameTooltip:Show()
GameTooltip:AddSpellByID(spellID)
The append counterpart to SetSpellByID:
adds a spell's full tooltip (name, cast time, cost, range, description) to
the current tooltip without clearing the existing lines. Works for any
spellID, learned or not. Lets you compose tooltips — e.g. a header line
plus a spell block.
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:AddLine("Rank 3 grants:")
GameTooltip:AddSpellByID(133) -- Fireball, appended below the header
GameTooltip:Show()
Unlike SetSpellByID, this does not update what
GetSpell reports — the appended spell isn't the
tooltip's "primary" spell, so GetSpell keeps reflecting whatever SetX
call (if any) built the base tooltip. Silent no-op for spellID <= 0 or an
unknown spell.
The engine's tooltip builder only exposes "append" via its internal talent "next rank" preview, which emits a
Next rank:header instead of the spell name; we overwrite that header line with the real name so the appended block reads normally.
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:GetUnitGUID() / GameTooltip:HasUnit()
GetUnitGUID() returns (name, guidString) for whichever unit the
tooltip is currently displaying, or nothing if it isn't showing a
unit. Return order mirrors GameTooltip:GetUnit() (name
first) — so addons porting from
local name, unit = ttip:GetUnit() can swap to GetUnitGUID and
keep their existing destructuring. name is the unit's display name
— the same string that appears in the tooltip header, or one of the
engine's "UNKNOWNOBJECT" / "Unknown Being" fallbacks for a remote
unit whose info hasn't been queried yet. guidString is the
canonical "0xHHHHHHHHLLLLLLLL" format returned by
UnitGUID(unit).
HasUnit() is a boolean companion — returns true if the tooltip is
currently displaying a unit.
GameTooltip:SetUnit("target")
local name, guid = GameTooltip:GetUnitGUID()
-- name = "Hogger"
-- guid = "0xF130001234..." (Creature) or "0x000000...ABC123" (Player)
if GameTooltip:HasUnit() then
-- cheap predicate, no name-resolution work
end
Why
GetUnit()returns a GUID, not a token. A(name, unitToken)form would return the exact"target"/"focus"/"mouseover"/ etc. string passed toSetUnit. But the engine drops the token at theScript_GameTooltip_SetUnitboundary — it converts the token to a 64-bit GUID and discards the original string. Reconstructing a plausible token by walking known tokens and reverse-matching by GUID is possible but lossy (multiple tokens can refer to the same GUID —"target"and"raid1"simultaneously, for instance), so we expose the GUID directly instead, which is what addons actually need for cross-referencing withUnitGUID, the NameCache, etc.
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: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:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])
Fills the tooltip with the item currently equipped in the slot the
given item would occupy — a grey Currently Equipped header, the
equipped item's own tooltip, then (when shiftButton is set) a
green/red per-stat delta breakdown of how the given item compares.
This is the method behind item-comparison ("shopping") tooltips;
FrameXML's GameTooltip_ShowCompareItem drives it.
Call it on the tooltip that should show the comparison (typically
ShoppingTooltip1/ShoppingTooltip2, but any GameTooltipTemplate
frame works).
| Arg | Meaning |
|---|---|
itemLink |
The item being compared (chat link, item: string, or bare itemID). Optional if comparisonTooltip is given. |
offset |
1-based slot selector for two-slot items — rings, trinkets, and one-hand weapons expose offset 1 and 2 (Finger1/2, Trinket1/2, MainHand/OffHand). Default 1. |
shiftButton |
Gates the stat-change breakdown. Deltas show by default (true/1/nil/omitted) and are hidden only for an explicit false/0 (header + equipped item only). Both the boolean and 1/nil conventions are accepted. |
comparisonTooltip |
Optional. When itemLink is omitted, the compared item is taken from whatever this tooltip is displaying (a tooltip-to-tooltip call). |
Returns the number of comparison slots for the item (1, or 2 for
rings/trinkets/one-hand weapons; 0 if it isn't equippable, isn't
cached, or nothing is equipped in the chosen slot) — so a caller knows
whether a second offset is worth querying.
The stat deltas cover the same keys as
C_Item.GetItemStats (base stats,
resistances, weapon DPS, and on-equip-spell bonuses like crit / attack
power / spell power), colored with the client's
INCREASE_STAT_COLOR / DECREASE_STAT_COLOR. The method only
populates the tooltip; the caller shows/anchors it (as
SetHyperlinkCompareItem does).
-- Compare a hovered bag item against what's equipped:
local link = GetContainerItemLink(bag, slot)
ShoppingTooltip1:SetOwner(GameTooltip, "ANCHOR_NONE")
local slots = ShoppingTooltip1:SetHyperlinkCompareItem(link, 1, true)
ShoppingTooltip1:Show()
if slots == 2 then -- ring/trinket/1H: also compare the other slot
ShoppingTooltip2:SetOwner(GameTooltip, "ANCHOR_NONE")
ShoppingTooltip2:SetHyperlinkCompareItem(link, 2, true)
ShoppingTooltip2:Show()
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
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) |
The engine ships only OnTooltipAddMoney / OnTooltipCleared /
OnTooltipSetDefaultAnchor; these four 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.
Item classification reuses Locations::FindGUID (the same walk
GetItemCount uses), so items in the bank count as "in inventory"
without requiring the bank window to be 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.
GameTooltip:SetTotem(slot)
Fills the tooltip with the shaman totem currently active 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)
Two lines: the totem name (NORMAL_FONT_COLOR) and the time remaining
(white). Built natively with the engine's own per-tooltip clear and raw
add-line — the same path SetSpellByID and SetHyperlinkCompareItem
use — and shows itself at the end, so no trailing :Show() is
required. Set the owner/anchor before the call.
Silent no-op when the slot has no active totem, so a totem-button
OnEnter can call it unconditionally.
Localization. The time line mirrors SecondsToTimeAbbrev
(raw seconds under a minute, minutes rounded up above) and formats
through the FrameXML GlobalStrings SPELL_TIME_REMAINING_SEC /
SPELL_TIME_REMAINING_MIN (English %d Sec / %d Min fallbacks for
servers stripped of those keys). The totem name comes from the summon
spell's localized Spell.dbc name. Slot data is the same tracker behind
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.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.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 the two types
GetCursorInfo returns nil for — no type string
fits them, but the enum names them, so the event reports them.
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.
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.
Implemented in pure C: builds a Lua C closure with (orig, callback)
as upvalues; the wrapper calls orig with LUA_MULTRET, then callback,
then returns orig's full result list. No return-count cap — works
correctly for functions returning any number of values.
Errors via lua_error on:
- Non-string
name - Non-function
callback target[name]not resolvable to a function (covers typos and hooking unknown frame methods)- 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.
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).
State is maintained from the same WH_GETMESSAGE hook that drives
GLOBAL_MOUSE_DOWN / GLOBAL_MOUSE_UP, so press/release transitions
register at exactly the moment the corresponding event fires. Hook
scopes to WoW's message queue — clicks made while the game is
alt-tabbed out don't deliver, which means the state can stay
"down" if a button was held while focus left the window. Most uses
of IsMouseButtonDown are gated behind a frame that just received
mouse focus, so this rarely matters in practice.
if IsMouseButtonDown("RightButton") then
-- right-button-held bindings
end
GetMouseButtonClicked()
Returns the name of the button responsible for the mouse handler
currently running ("LeftButton", "RightButton", "MiddleButton",
"Button4", "Button5"), or nil when no click is being handled.
Addons call this inside a mouse handler (OnClick,
OnMouseDown, OnMouseUp, OnDragStart, …) to learn which button
drove it — an alternative to reading arg1, and readable
from nested helper functions where arg1 isn't in scope. The value
is captured by the same WH_GETMESSAGE hook behind
GLOBAL_MOUSE_DOWN / GLOBAL_MOUSE_UP, then evicted a couple of
frames later so — matching the real API — it reads nil outside a
handler rather than lingering as the last click forever.
The button stays readable for the whole frame its click lands in (so
multiple addons hooking the same OnClick all see it), and while a
button is physically held (so OnDragStart reads it). It is a
best-effort replica: WoW dispatches these handlers deferred from the
OS message, so eviction is time-based (per-frame) rather than exactly
bracketed around the handler. Values can therefore
linger ~2 frames past a click — harmless, since it's only meaningful
inside a mouse handler.
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.
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 straight off the engine's inventory manager (location
form) and item cache (ID form). No GetItemInfo chaining — both
functions read directly from the structures they need.
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)—trueiff the cache currently has data foritem. Cache miss returnsfalseand kicks off the network query in the background; a follow-up call afterGET_ITEM_INFO_RECEIVEDwill succeed. Accepts a numericitemID, a bare"item:NNN..."string, or a full chat link.
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 player's total count of itemInfo — equipped items +
bags, and optionally bank.
count = C_Item.GetItemCount(itemInfo [, includeBank [, includeUses]])
itemInfo— a numericitemID, a string that contains"item:NNN"(full chat links work), or an item name. A name is compared, without regard to case, with the full name of each item that you carry (the suffix included, so"Foo of the Owl"matches and"Foo"does not).includeBank(optional, default false) — also walk bank slots (bag-1for the main bank, bags5..10for bank-bag slots).includeUses(optional, default false) — whentrue, multiplies each match by the item's spell-charges count. A wand with 50 charges contributes 50 instead of 1. Items without charges pass their plain stack count through unchanged, so the flag is a no-op for them.
local n = C_Item.GetItemCount(2589) -- Linen Cloth in bags + equipped
local n = C_Item.GetItemCount(2589, true) -- + bank
local n = C_Item.GetItemCount("item:2589") -- string form works too
local n = C_Item.GetItemCount("Linen Cloth") -- by name
-- Equipped items count toward the total:
local trinketID = GetInventoryItemID("player", INVSLOT_TRINKET1)
C_Item.GetItemCount(trinketID) -- 1
-- includeUses multiplies by charges for charged items:
C_Item.GetItemCount(wandID, false, true) -- 50 for one 50-charge wand
C_Item.GetItemCount(linenID, false, true) -- same as stack count (no charges)
Bank works cold — no banker visit required. The server sends bank inventory at login alongside the rest of the player's data; only the engine's own
GetItemBySlotgates bank slots until the window opens. We bypass that gate by reading the GUID array directly out of the player invMgr and resolving each entry via the engine's object resolver — same pathGetItemBySlotwould take internally if the gate let us through. Counts are correct from session start.
Walks the 19 equipment slots via
Item::Location::ResolveEquipmentSlot, then bags via the same
Item::Location::ResolveBag chain
C_Container.GetContainerItemID
uses. Slot counts come from the engine's Script_GetContainerNumSlots
so custom server bag sizes work transparently. Stack counts read
directly off the CGItem's m_objectFields descriptor at +0x20
(ITEM_FIELD_STACK_COUNT, verified by decoding
Script_GetContainerItemInfo at 0x004F9670).
Equivalent to the global GetItemCount and C_Item.GetItemCount.
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. Same bit C_Item.IsItemOpenable reads. |
isLootable |
bool | Flag bit 0x10 — has loot generators (right-click loot). |
isWrapper |
bool | Flag bit 0x200 — gift-wrappable. |
maxStackSize |
number | m_stackable. |
maxCount |
number | m_maxCount — 0=unlimited, 1=unique, otherwise per-character cap. |
containerSlots |
number | m_containerSlots — bag slot count, 0 for non-bags. |
bagFamily |
number | m_bagFamily converted to a bitmask (1 << (id-1)). |
buyPrice |
number | Vendor buy price in copper. |
sellPrice |
number | Vendor sell price in copper. |
itemLevel |
number | Base ilvl from ItemSparse. |
requiredLevel |
number | Minimum character level. |
requiredSkill |
number | SkillLine.dbc row, 0=none. |
requiredSkillRank |
number | Skill rank required. |
requiredSpell |
number | Spell.dbc row required to learn / use, 0=none. |
requiredHonorRank |
number | PvP honor rank required, 0=none. |
requiredCityRank |
number | Reserved; always 0 here. |
requiredFaction |
number | Faction.dbc row, 0=none. |
requiredFactionRank |
number | Reputation tier (Friendly/Honored/…). |
allowableClass |
number | Class bitmask, -1 = all classes. |
allowableRace |
number | Race bitmask, -1 = all races. |
armor |
number | Armor value. |
block |
number | Shield block value. |
maxDurability |
number | 0 for items without durability. |
stats |
table | Sparse { [statType] = statValue, … }. Empty for items without stat allocations. |
resistanceHoly |
number | Holy resistance. |
resistanceFire |
number | Fire resistance. |
resistanceNature |
number | Nature resistance. |
resistanceFrost |
number | Frost resistance. |
resistanceShadow |
number | Shadow resistance. |
resistanceArcane |
number | Arcane resistance. |
damageMin |
table | 5-element float array — weapon min damage per damage slot. |
damageMax |
table | 5-element float array. |
damageType |
table | 5-element integer array — damage school per slot. |
delay |
number | Weapon swing time in ms. |
ammoType |
number | Ammo subclass for ranged weapons. |
rangedModRange |
number | Range modifier for bows/guns. |
spells |
table | Sparse array of {id, trigger, charges, cooldown, category, categoryCooldown} records — one per non-empty spell slot (up to 5). |
useSpellID |
number | nil | Convenience — the spellID of the first slot with trigger=ON_USE (0). Omitted when the item has none. Same value C_Item.GetItemSpell returns. |
lockID |
number | Lock.dbc row for the item's lock, 0=none. |
itemSet |
number | ItemSet.dbc row, 0=not part of a set. |
pageText |
number | PageText.dbc row for readable items. |
pageMaterial |
number | Book page material. |
languageID |
number | Language for in-game-readable books. |
startQuest |
number | Quest row started by right-clicking this item. |
material |
number | Material type (cloth/leather/metal etc. for hit sounds). |
sheath |
number | Weapon sheath style. |
randomProperty |
number | Random property template id. |
area |
number | AreaTable.dbc — bound area for area-locked items, 0=none. |
map |
number | Map.dbc — bound map for map-locked items, 0=none. |
local data = C_Item.GetItemDataByID(6948) -- Hearthstone
-- data.name == "Hearthstone"
-- data.quality == 1, data.classID == 15, data.subclassID == 0
-- data.bindType == 1 (BoP)
-- data.useSpellID == 8690 (the recall spell)
-- data.maxStackSize == 1, data.maxCount == 1
local sword = C_Item.GetItemDataByID(2092) -- Worn Shortsword
-- sword.equipLoc == "INVTYPE_WEAPONMAINHAND"
-- sword.damageMin[1] == 1.0, sword.damageMax[1] == 2.0, sword.delay == 1900
-- sword.stats == {} (no stat allocations on the starter sword)
Cache state. The function reads the same DBCache_ItemStats
network cache every other C_Item.* reader consults. Items already in
the player's inventory are typically populated by the engine's natural
prefetch before PLAYER_ENTERING_WORLD returns; for other items
(quest rewards, AH items, link hovers, etc.) the data lands once a
SMSG_ITEM_QUERY_SINGLE_RESPONSE arrives. Use
C_Item.RequestLoadItemDataByID if you specifically need to trigger
the load.
Stats encoding. The stats table is keyed by the ItemModType
enum:
| Key | Meaning |
|---|---|
| 0 | Mana |
| 1 | Health |
| 3 | Agility |
| 4 | Strength |
| 5 | Intellect |
| 6 | Spirit |
| 7 | Stamina |
Higher-numbered slots (consumable per-second-regen, defense rating,
hit/crit/dodge/parry chance, weapon/spell-power) only appear on items
the server actually carries — items here mostly use 0..7. Empty
slots (both type and value zero) are omitted, so iterating stats
yields only the stats actually allocated.
A ClassicAPI-only reader — GetItemInfo returns its tuple piecemeal and
C_Item.GetItemInfo adds a few more. GetItemData is a single-call
superset useful for tooltip addons and item-data caches.
C_Item.GetItemFamily(item)
Returns the BagFamily bitmask for an item — i.e., what kind of
specialty bag it belongs in. 0 means "general-purpose" (no specialty
bag preference). Returns nil if the item isn't in the cache.
familyBitmask = C_Item.GetItemFamily(item)
item accepts a numeric itemID or a string containing "item:NNN"
(the bare shorthand and full chat links both parse correctly), same as
C_Item.GetItemInfoInstant.
C_Item.GetItemFamily(2512) -- 1 (Wooden Arrow → quiver-family)
C_Item.GetItemFamily(2516) -- 2 (Light Shot → ammo pouch-family)
C_Item.GetItemFamily(6265) -- 4 (Soul Shard → soul bag-family)
C_Item.GetItemFamily(2447) -- 32 (Peacebloom → herb bag-family)
C_Item.GetItemFamily(6948) -- 0 (Hearthstone → general-purpose)
Bitmask values follow the ItemBagFamily.dbc IDs converted to
the bitmask form (1 << (familyID - 1)). The IDs 1–9 are the standard
families; 10–13 are Turtle WoW custom families (the stock client
stops at Keys):
| Bit | Value | Raw ID | Family | Notes |
|---|---|---|---|---|
| 0 | 1 | 1 | Quiver (arrows) | |
| 1 | 2 | 2 | Ammo Pouch (bullets) | |
| 2 | 4 | 3 | Soul Bag | |
| 5 | 32 | 6 | Herb Bag | |
| 6 | 64 | 7 | Enchanting Bag | |
| 7 | 128 | 8 | Engineering Bag | |
| 8 | 256 | 9 | Keyring | |
| 9 | 512 | 10 | Meat Bag (Turtle) | ⚠ collides with the standard Gem Bag bit (0x200) |
| 10 | 1024 | 11 | Fish Bag (Turtle) | ⚠ collides with the standard Mining Bag bit (0x400) |
| 11 | 2048 | 12 | Leather Bag (Turtle) | no standard equivalent bit |
| 12 | 4096 | 13 | Mining Bag (Turtle) | ⚠ not the standard Mining Bag bit (0x400) |
Turtle custom families & the bit collision. Turtle added four cooking/gathering bag families (Meat, Fish, Leather, Mining) as
ItemBagFamily.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.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)
Returns a table of the per-stat difference between two items,
computed as item2 - item1 — a positive value means the second item has
more of that stat. Only stats whose delta is non-zero appear.
-- Compare an equipped ring against one in your bags:
local delta = C_Item.GetItemStatDelta(
C_Item.GetItemLink({equipmentSlotIndex = 11}),
C_Item.GetItemLink({bagID = 0, slotIndex = 3}))
-- delta = { ITEM_MOD_INTELLECT_SHORT = 6, ITEM_MOD_SPIRIT_SHORT = 2 }
-- (the bag ring has +6 Int / +2 Spirit relative to the equipped one)
Same key set, accepted input forms, random-suffix handling, and caching
caveat as C_Item.GetItemStats — see
there for the details. Returns nil if either item is uncached.
C_Item.GetItemStats(itemLink)
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.
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 | MOD_DAMAGE_DONE (13) |
ITEM_MOD_SPELL_HEALING_DONE_SHORT |
Healing | MOD_HEALING_DONE (135) |
ITEM_MOD_MANA_REGENERATION |
mp5 (mana) | MOD_POWER_REGEN (85) |
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) |
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 nil if the item isn't cached yet — warm it via
GetItemInfo and retry.
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 = 1800000, charges = 0, enchantID = 2629
-- Apply Instant Poison instead:
-- has = true, expireMs = 1800000, charges = 40, enchantID = 323
An oil runs on time alone and reports charges = 0. A poison reports a
charge count as well as a time.
Reads the temporary enchant slot (ITEM_FIELD_ENCHANTMENT
slot 1 at descriptor +0x4C) — the same slot oils, sharpening
stones, and poisons populate and the engine drains as they expire.
This is what GetWeaponEnchantInfo measures.
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.
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)
Returns true if the item is a consumable, false otherwise. An item
counts as consumable when its class is Consumable (0) or its
inventory type is INVTYPE_AMMO (24) or INVTYPE_THROWN (25) — ammo
and thrown weapons being consumed in use. Registered as both the bare
global and the namespaced C_Item form, like IsUsableItem.
item is an itemID number or "item:N..." link. Item names aren't
accepted (there is no name→ID resolver). Returns false for uncached
items (no async load fired) — same cache contract as
C_Item.IsEquippableItem.
IsConsumableItem(118) -- Minor Healing Potion → true (class Consumable)
C_Item.IsConsumableItem(1251) -- Linen Bandage → true (class Consumable)
C_Item.IsConsumableItem(2512) -- Rough Arrow → true (INVTYPE_AMMO)
C_Item.IsConsumableItem(6948) -- Hearthstone → false
C_Item.IsConsumableItem(18820) -- Talisman of Ephemeral Power → false (on-use trinket)
This is a class/ammo check, not a "has a usable effect" check. An
on-use trinket with a numeric Use: effect (Talisman of Ephemeral
Power) returns false, while class-Consumable items (potions, bandages,
the class-0 "Faintly Glowing Skull") and ammo return true.
C_Item.IsEquippableItem(item)
Returns true if item can be equipped in any character-pane slot,
false otherwise. Reads m_inventoryType from the cached ItemStats
record — INVTYPE_NON_EQUIP (value 0) is the only "not equippable"
value, so any non-zero inventory type passes (head, neck, weapon,
shield, holdable, …).
item is an itemID number or "item:N..." link. Item names aren't
accepted — there is no name→ID resolver, and equippability is an
itemID-keyed property anyway.
Returns false for uncached items (no async load fired). If you
need it to wait for the cache, call C_Item.RequestLoadItemDataByID
first and re-check on ITEM_DATA_LOAD_RESULT.
C_Item.IsEquippableItem(12640) -- Lionheart Helm → true
C_Item.IsEquippableItem(6948) -- Hearthstone → false
IsUsableItem(item) / C_Item.IsUsableItem(item)
Returns (usable, noMana) for an item's on-use ability — the item
analog of IsUsableSpell.
This asks "can I click this item right now?", not "can I equip it".
Armor proficiency, class-restricted gear, and the like are not this
function's concern (that's the tooltip's red requirement lines); only an
item's usable/right-click effect matters here.
The global IsUsableItem(item) returns 1/nil pairs (matching stock
IsUsableAction); C_Item.IsUsableItem(item) returns proper booleans
per the C_Item.* convention. item is an itemID number or
"item:N..." link.
IsUsableItem(6948) -- Hearthstone → 1, nil
local usable, noMana = C_Item.IsUsableItem(13446) -- Major Healing Potion
-- usable=true, noMana=false → click it
-- usable=false, noMana=false → no on-use effect, below required level,
-- class/race-restricted, dead, on a form
-- that blocks it, etc.
What this function checks:
- The item has an on-use spell (an
ItemStatsspell slot with triggerON_USE). Items with no usable effect — plain gear, materials, on-equip procs — return(nil, nil).- The item's use requirements are met: RequiredLevel, and the AllowableClass / AllowableRace masks. (These gate activation — a too-low-level potion, a class-specific trinket — so they belong here, unlike equip-only requirements.)
- The on-use spell is castable now — delegated to the engine's own spell-castability helper (
FUN_SPELL_IS_USABLE,0x006E3D60), which folds in shapeshift/form, mechanic immunities, aura state, and the power check. Insufficient power is the only condition that setsnoMana=true.Like
C_Item.IsEquippableItem, this is a synchronous cache peek: uncached items return(nil/false, nil/false)with no async load fired. CallC_Item.RequestLoadItemDataByIDfirst and re-check onITEM_DATA_LOAD_RESULTif you need it to wait.The on-use spell lookup is shared with
C_Item.GetItemSpell(Item::Spell::FindOnUseSpellIDInRecord).
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.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. Range-only like the spell
version: it ignores line of sight and doesn't reject wrong-faction
targets. 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 type is right-click-openable (sack, clam, simple chest, quest box, lockbox, etc. — anything whose tooltip could show<Right Click to Open>). Intrinsic to the item type. FromItemStats.Flags & 0x4.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).
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
Implementation: isOpenable reads ItemStats.Flags & 0x4 from the
client-side ItemSparse cache. canOpen adds a check that either
ItemStats.LockID == 0 (no lock) or the CGItem instance's
ITEM_FIELD_FLAGS & 0x4 (UNLOCKED) bit is set. Same conditions the
engine's tooltip builder uses at 0x0052E323 to gate the
ITEM_OPENABLE line.
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 player's current cursor world position —
ClassicAPI's [@cursor] analog for ground-target on-use items
(Iron Grenade, Bombling, demolition charges, etc.). Returns true
when the cursor-placement leg landed (the item fires at terrain);
false for items that aren't ground-target (the item still fires
normally with no implicit target), unparseable input, items you do
not have, cursor over UI / off-screen, etc.
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 })
When the item fires a non-ground-target spell, the cursor leg no-ops
and returns false; the item still uses normally (any implicit
target — current selection, etc. — applies).
Cancels placement automatically when the cursor isn't on terrain — the item-use packet is never sent, so an off-screen click doesn't waste the grenade.
C_Item.UseAtUnit(item, unit)
Unit-position analog of
C_Item.UseAtCursor: uses item at
unit's feet rather than the cursor. ClassicAPI's [@unit] for
ground-target on-use items. item accepts the same two forms as
UseAtCursor — an item reference or an item location; unit is any
unit token ("player", "target", "mouseover", "party1", …).
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")
Returns true when the placement landed at the unit. Returns false for
unparseable input, an item you do not carry, or a unit that cannot be
resolved. The unit is resolved before the item fires, so an absent unit
fails without consuming the item.
An item with no ground effect also returns false, and is used on the
unit: the item fires with that unit as its target rather than with
whatever you have selected. So this covers both kinds of on-use item, the
same way C_Spell.CastAtUnit covers
both kinds of spell.
A unit token that names nothing right now, such as "party3" while solo,
returns false. A string that is not a unit token at all raises the
engine's standard "Unknown unit" error, as UnitHealth("garbage") does and
as C_Spell.CastAtUnit does. That differs from
C_Item.UseItemByName, whose contract
is to no-op on anything 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).
avgItemLevelEquipped is the arithmetic mean over
the player's currently-worn equipment slots; avgItemLevel is the
same metric but extended to include best-per-slot upgrades found
anywhere in the player's bags and bank.
Slots considered: INVSLOT_HEAD..INVSLOT_TABARD (1..19) minus
shirt (4) and tabard (19) = 17 candidate slots. Bag-walk uses the
same invType → slotMask mapping as GetInventoryItemsForSlot.
local overall, equipped = GetAverageItemLevel()
-- overall: best-per-slot ilvl across equipped + bags + bank
-- equipped: ilvl of currently-worn items only
Fixed denominator. Empty slots count toward the denominator
(contributing 0 to the sum). Removing a piece of gear always
lowers avgItemLevelEquipped. (A populated-only count would keep the
average up when a slot empties; this implementation deliberately
differs.)
Max-of-two-divisors fairness. A 2H weapon wielder has an intentionally empty offhand (slot 17) — counting it as 0 in a 17-divisor sum would unfairly penalize them. We compute a second candidate excluding slot 17 from both numerator and denominator (16 slots), and return the max. Sword+shield characters typically win the all-17 path; 2H wielders win the no-OH path.
No double-counting. Each bag/bank item is assigned to one candidate slot via greedy fit — a trinket in your bag fills one trinket slot (the empty/lower-best one), not both. Without this gate, moving a trinket from equipped to bag would inflate the "overall" count and drag the average down (because the same item would count for both trinket slots).
Bank is walked even if the bank window has never been opened
this session — the GUID array at invMgr + 0x04 is populated
from server data at login. Bypasses the bank-window gate on
GetItemBySlot the same way C_Item.GetItemCount does.
Limitations:
- Items not yet in the ItemStats cache are skipped from the
running totals and a warmup is queued; the next call after
ITEM_DATA_LOAD_RESULTlands picks them up. - A full weapon
qsortdance — handling 2H-equipped vs. 1H + OH-in-bag comparisons — is not replicated. The edge case where you'd notice a difference is narrow.
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.
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.
OffhandHasWeapon()
Returns true if the player has a one-handed weapon (or off-hand-only
weapon) equipped in the off-hand slot, false otherwise. Used by
dual-wield checks and weapon-equipment logic.
Returns false for:
- Empty off-hand
- Shields (
INVTYPE_SHIELD) - Held items — tomes, orbs, librams (
INVTYPE_HOLDABLE) - Off-hand item data not yet cached (typically only on first login, before the engine has the item record; warms up after a peek)
Returns true for INVTYPE_WEAPON (any one-handed weapon — sword,
axe, mace, dagger, fist) and INVTYPE_WEAPONOFFHAND (off-hand-only
weapons). Two-handers occupy the main-hand slot exclusively, so they
never apply.
if OffhandHasWeapon() then
-- Apply mainhand+offhand poison, refresh dual-wield rotation, etc.
end
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.
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.
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.
- (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 or the world isn't loaded. 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.
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 or the world isn't loaded — 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, extract the payload form via
string.match(link, "|H(item:[^|]+)|h") — SetHyperlink
requires the literal "item:" prefix and rejects full |cff...|Hitem...|h
input.
LootHistory
The C_LootHistory namespace — group-loot roll history. There is no
loot-history store, but the client does receive the live group-loot roll
traffic (SMSG_LOOT_ROLL per player, SMSG_LOOT_ROLL_WON,
SMSG_LOOT_ALL_PASSED) — which the stock UI only turns into chat lines. This
reconstructs the history client-side: packet co-hooks accumulate a ring of
rolled items with per-player Need/Greed/Pass results and the winner, exposed
through the namespace. Rollers' name and class come from the engine
NameCache captured at roll time, so they resolve even for a player who has
left the group by the time their roll arrives.
History is in-memory (last 128 rolled items) and resets on /reload or logout.
The master-loot management functions (SetExpiration / GiveMasterLoot /
CanMasterLoot) are intentionally omitted — they drive server support this
build doesn't have.
Progress is signalled by the LOOT_HISTORY_ROLL_CHANGED /
LOOT_HISTORY_ROLL_COMPLETE events — see the
Events
section.
C_LootHistory.GetNumItems()
Returns the number of rolled items currently in the history.
C_LootHistory.GetItem(itemIndex)
1-based. Returns rollID, itemLink, numPlayers, isDone, winnerIdx, timestamp
(timestamp is a ClassicAPI extension):
rollID— a stable monotonic ID for the roll. UnlikeitemIndex(which shifts as the 128-item ring saturates),rollIDnever changes, so key "keep this row expanded" / "highlight this roll" state on it.itemLink—item:<id>:0:<suffix>:<unique>(valid forGetItemInfo/GameTooltip:SetHyperlink), carrying the roll's random suffix so "of the X" items keep their affix.numPlayers— how many players have rolled/passed so far.isDone—trueonce the item is decided (won or all-passed).winnerIdx— the winner's1-based player index, forC_LootHistory.GetPlayerInfo(itemIndex, winnerIdx)(name, class token, winning roll).nilif undecided / all passed. The winner is guaranteed to be in the player list even if their individual roll packet never arrived.timestamp— the roll's creation time in seconds, directly comparable toGetTime()(e.g.GetTime() - timestampfor "N seconds ago"). A ClassicAPI extension.
Returns nothing for an out-of-range index.
C_LootHistory.GetPlayerInfo(itemIndex, playerIndex)
Both 1-based. Returns name, class, rollType, roll, isWinner, isMe:
name— the roller's name (from the NameCache).class— the class token ("WARRIOR","MAGE", …) forRAID_CLASS_COLORS, ornilif unknown.rollType—0= pass,1= need,2= greed.roll— the1–100roll, or0if they passed.isWinner—trueif this player won the item.isMe—trueif this roller is the local player.
Returns nothing for an out-of-range item or player index.
C_LootHistory.Clear()
Wipes the accumulated roll history (returns nothing). A ClassicAPI extension,
for a "clear history" button. Fires LOOT_HISTORY_FULL_UPDATE so any open
display rebuilds empty.
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. Present for SCHOOL_INTERRUPT and for CC whose applying cast ClassicAPI observed; nil otherwise. |
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 is best-effort —
startTime/timeRemaining/durationare filled when ClassicAPI observed the cast that applied the aura (theAura::Sourcecache) and arenilotherwise. These fields are nullable. auraInstanceIDis omitted (nil) — no equivalent here.
The LOSS_OF_CONTROL_ADDED / LOSS_OF_CONTROL_UPDATE events fire as these
effects change — see 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 five numbers: the chunks loaded, their bytes, the chunks that needed a full token pass, those bytes, and the milliseconds the rewrite has used since the client started.
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).
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. - 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 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 protect the environment table it gives to restricted code.
Put a metatable on that table with an __environment field:
local view = setmetatable({}, { __environment = "protected" })
setfenv(restrictedFn, view)
From then on, code inside the sandbox sees the protection:
getfenv()returns the__environmentvalue, not the real table. The restricted code cannot reach or change the true environment.setfenv()on that environment raisescannot change a protected environment.
This is the Lua 5.1 form. 1.12 already protected environments, but it read
a raw __fenv field on the table itself. ClassicAPI adds the 5.1
metatable form and keeps __fenv as a fallback. The change is additive:
an environment with neither marker behaves as before.
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.
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.
Note that error("...") does not reach the handler. WoW replaces Lua's
error with one that reports through geterrorhandler() and then returns
normally, so f looks like it finished and you get true with the message
printed to chat. Real runtime errors — indexing or calling nil, arithmetic
on a non-number — do reach the handler. To abort into it deliberately, cause
one of those: (nil)() is the shortest.
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)
Removes every key from t, leaving it empty but preserving its
internal hash and array capacity. Returns t for chaining.
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.
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). Three 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. 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.gsub table replacement
Lua 5.1 allows a table as gsub's replacement: every match looks up
the first capture (the whole match when the pattern declares no
captures) as a key and substitutes the value. A nil or false value
keeps the original match. The stock 5.0 gsub accepts only a string or
function replacement and errors on a table
(string or function expected).
("$name the $class"):gsub("%$(%w+)", { name = "Bob", class = "Druid" })
-- "Bob the Druid", 2
("$name and $unknown"):gsub("%$(%w+)", { name = "Bob" })
-- "Bob and $unknown", 2 (a missing key keeps the match, as in 5.1)
Both string.gsub and the bare gsub global have the upgrade; string
and function replacements behave exactly as before. One residual: when
the pattern itself contains a %1..%9 back-reference, a missing table
key removes the match instead of keeping it.
strsplit(sep, str [, pieces])
The WoW global; splits str on any character in
sep and returns the pieces as multiple values.
sep— a set of delimiter characters (not a pattern); each character is a delimiter.","splits on commas;" -"splits on spaces and dashes.pieces(optional) — caps the number of results. 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).
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.
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"
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"
math.fmod(x, y)
The floating-point remainder of x / y (the quotient truncated toward zero),
same as C's fmod. A pure 5.0→5.1 rename: Lua 5.0 ships the identical C
function as math.mod, and 5.1 renamed the Lua-facing name to math.fmod.
Registered as a direct alias — math.mod remains available too, so code
written for either name works.
math.fmod(7, 3) -- 1
math.fmod(-7, 3) -- -1 (result takes the sign of the dividend)
math.fmod(5.5, 2) -- 1.5
math.modf(x)
Returns (integral, fractional) — the integral part truncated toward zero
and the fractional part carrying x's sign. Lua 5.1 function, genuinely
missing from 1.12's math library (not just renamed like fmod).
math.modf(3.7) -- 3, 0.7
math.modf(-3.7) -- -3, -0.7
math.modf(5) -- 5, 0
math.huge
Positive infinity, as the numeric constant math.huge (Lua 5.1). Handy as a
"larger than anything" sentinel in min/max scans.
local best = math.huge
for _, v in ipairs(costs) do best = math.min(best, v) end
coroutine.*
Restores Lua 5.0's stripped coroutine.* library. The C-level
coroutine machinery (lua_resume, lua_yield, lua_newthread,
and the thread type at tag 8) is linked into the engine, but
the script-facing library was never registered as a global table.
ClassicAPI rewires the standard five entries on coroutine, plus
Lua 5.1's coroutine.running, matching Lua 5.0 semantics with two
WoW-specific quirks 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:- Coroutine isn't suspended — e.g. resuming a dead one;
errMsgis a fixed string like"cannot resume dead coroutine". - The body hit a Lua VM error;
errMsgis the formatted error string with file:line prefix.
- Coroutine isn't suspended — e.g. resuming a dead one;
Caveat — error() doesn't propagate. WoW replaces Lua's
error() global with a soft handler that calls
geterrorhandler()(msg) and returns normally instead of
longjmp'ing. So error("msg") inside a coroutine body looks like
a clean return to resume — the message gets printed to chat as
a side effect and resume returns true with no values. Real VM
errors (calling nil, indexing nil, etc.) still propagate
through the standard false, msg path.
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. |
"dead" |
co's body has returned or errored. |
The standard Lua "normal" state — a coroutine that resumed a
deeper coroutine and is now an ancestor on the call chain — is
collapsed into "suspended". Detecting it cleanly requires
walking the global thread list, and the case is near-useless for
addon-level logic.
coroutine.wrap(fn)
Equivalent to coroutine.create(fn) followed by returning a
closure that calls coroutine.resume(co, ...) on each invocation
— but propagates errors as raised exceptions (via lua_error)
instead of returning false, msg. 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
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.
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. |
/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]
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 on the first line of a macro. 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, 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/castand/uselines 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.- 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.
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) -- not yet — macro parser only recognizes the string form
The numeric form works for the cast itself (via the function's
runtime resolution), but the macro parser only scans for the string
form when tagging. To get the slot to highlight when using the
spellID, write CastSpellNoToggle("Shoot") instead.
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.
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's
#showtooltip or #show directive resolved to.
Returns nothing when the macro has no directive, or when the directive
resolved to a spell. 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"
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 the user dropped into Interface\Icons\
on disk) vs mpq (icons baked into the game's MPQ archives), crossed
with Spell (basenames starting with Ability_ / Spell_) vs
Item (basenames starting with INV_). We hook
the engine's three scan callbacks
(FUN_MACRO_ICON_CB_DISK / *_USER_MPQ / *_INSTALL_MPQ) and tag
each captured filename by source + prefix.
Quirk worth noting: the engine's main icon DB (what
GetMacroIconInfo(i) returns) is filtered down to Ability_* /
Spell_* only — INV_* filenames flow through the scan callbacks
(~2,500+ per session in the Octo client) but never land in the DB
because something downstream of the callbacks rejects them. We
capture before that rejection, so GetMacroItemIcons works even
though no engine-level GetMacroItemIconInfo(i) exists.
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 a
#showtooltip resolved 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)
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, 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.
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
first line of the macro is #showtooltip. 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. 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
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)
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)
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 six C_MerchantFrame.* calls addons use when
interacting with a vendor. All entry points read the engine's
merchant/buyback storage directly — no Lua-roundtrip through
GetMerchantItemInfo / GetBuybackItemLink etc. — and the
SellAllJunkItems dispatcher bypasses UseContainerItem entirely
by calling the engine's internal MerchantSellItem packet builder.
A "merchant frame is currently open" gate (VAR_MERCHANT_NPC_GUID_*
non-zero) applies to every function.
GetNumJunkItems returns 0 away from a vendor and the sell
functions are no-ops; the per-slot getters return nil.
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 itemID of the merchant's buyback slot at the given
1-based index (1..12), or nil if the slot is empty / no merchant
is open.
for slot = 1, 12 do
local id = C_MerchantFrame.GetBuybackItemID(slot)
if id then
-- buybackable item at slot
end
end
Resolution chain (engine-direct, no Lua-side GetBuybackItemLink
roundtrip):
- Read the buyback slot's stored invMgr index from
VAR_BUYBACK_SLOTS + (slot-1)*4. - Look up that index in the player invMgr's flat GUID array (the engine keeps sold items alive in the player's inventory storage, not in a separate buyback pool).
- Resolve the GUID to a
CGItem*viaItem::Location::ResolveByGUID— same engine helperC_EquipmentSet.GetItemLocationsuses. - Read the itemID from the CGItem's instance block at
+0x08 + 0x0C.
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 identical per-overlay table shape (see
GetMapOverlays); these two just filter it. "Explored" is computed
per-areaID from the player's explored-areas bitfield (the same source the
engine's own GetNumMapOverlays/GetMapOverlayInfo gate on), so the
areaID argument works for any zone — not only the one currently on the
world map. With no argument, both default to 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.
Model
model:SetDisplayInfo(creatureDisplayID)
Points a Model frame at a creature by its display ID. This is the
alternative to Model:SetModel(path), which needs a raw model file path.
The engine resolves the display ID to a model file through two DBC tables:
CreatureDisplayInfo, then CreatureModelData. It loads the model the same
way SetModel does, then applies the creature's skin textures. So the model
shows the correct look for that display, not an untextured base.
A Model frame provides no light of its own. Call SetLight after
SetDisplayInfo, or the model renders as a black silhouette. Call
SetCamera(0) to frame it.
An unknown or missing display ID falls back to the engine's placeholder model.
A character-based display (an NPC on the shared Character\<Race>\<Sex>
base model) is dressed the same way a spawned NPC is: baked body texture,
hair, face, facial hair, and equipment, including the attached helm and
shoulder models. The dressing runs when the model file finishes loading,
a moment after the call. Weapons do not show: the server sends NPC
weapons only for spawned units, and no client table carries them.
This is the single-creature form. An optional second argument
mountDisplayID that shows the creature on a mount is not supported
here: a mount needs a second, attached model, and the Model frame holds
one model.
local m = CreateFrame("Model", "PreviewModel", UIParent)
m:SetSize(200, 260)
m:SetPoint("CENTER")
m:SetDisplayInfo(330) -- a human NPC
m:SetCamera(0)
m:SetLight(1, 0, 0, -1, -1, 1, 1, 1, 1) -- else it is a black silhouette
model:SetCreature(creatureID)
Points a Model frame at a creature by its entry ID — the creature's
database ID, not a display ID. This is the companion to SetDisplayInfo.
The engine looks the entry ID up in the client creature cache, reads the display
ID, then loads the model exactly like SetDisplayInfo. The same light and camera
notes apply.
Limitation. The lookup only works for a creature the client has cached. A
creature caches after you see it, target it, or mouse over it, or from
creaturecache.wdb at login. There is no client-side entry-to-display table,
so an uncached creature ID is a no-op — the frame keeps its previous model.
For an uncached creature, request its data first, then set it when the data arrives:
local creatureID = 448 -- the creature's entry (database) ID
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)
model:SetCreature(creatureID)
model:SetCamera(0)
model:SetLight(1, 0, 0, -1, -1, 1, 1, 1, 1)
end
end)
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).
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". Like party/raid tokens,
UNIT_HEALTH / UNIT_AURA / UNIT_LEVEL / … fire with
arg1 == "nameplateN" when a nameplated unit's descriptor field changes
(the engine only watched its own target/party/raid units). Backed by
Unit::TokenObserver — observers are registered per plate on
NAME_PLATE_UNIT_ADDED and torn down on _REMOVED; the changed GUID is
resolved to its current index at fire time, since indices shift as
plates vanish. Note the per-plate observer cost in very large scenes.
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". UNIT_HEALTH / UNIT_MANA /
UNIT_AURA / UNIT_LEVEL / … fire with arg1 == "markN" when the marked
unit's descriptor field changes — the engine only watched its own
target/party/raid units, so a marked mob that isn't otherwise one of those
fired nothing:
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)
Backed by Unit::TokenObserver (the same mechanism as focus /
nameplateN). A per-frame watcher diffs the engine's 8-slot marker table
and attaches/detaches an observer per marked unit — deferring until the
unit is in range, since observers can only bind to a live object and a
marked unit may be marked while out of range and enter later. The idle
case (no markers set) costs a handful of comparisons per frame and no
object lookups. A marker on a non-unit is simply never observed.
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, adds an opt-in persistent layer that
survives /reload and full client restarts, and (separately
toggleable) sweeps the engine's visible-object list to populate
entries the SMSG path would never reach on its own.
Two toggles, each independent:
C_PlayerCache.SetEnabled— turn on the on-disk cache. Off by default. Without this, the module is read-only against the engine's transient in-memory cache.C_PlayerCache.SetScanEnabled— turn on the visible-object sweep. Off by default. Requires the on-disk cache to be enabled to have any effect.
Three population sources feed the cache when fully enabled:
SMSG_NAME_QUERY_RESPONSEhook (always active when the on-disk cache is on). Every name-query response the engine processes is mirrored to disk. Covers chat, group/raid sync, guild updates, visible-object resolution — anything the engine itself issues a name query for.C_PlayerCache.RememberPlayer(always available, no-op when the cache is off). Lets addons feed in sources the engine never sees:/whoresults, etc.- Visible-object sweep (active when both toggles are on).
Walks the engine's live visible-object list every ~10 seconds
on the
Frame::RegisterEventhook, resolving each player and feeding name + class. Picks up nearby players whether or not the engine ever queried them.
Storage:
WTF\Account\<account>\ClassicAPI.txt— account-level settings (PersistentNameCacheEnabled,NameCacheScanEnabled). The opt-in toggles stay per-account.WTF\ClassicAPI\<realmList>\<realm>\ClassicAPI_NameCache.txt— per-realm cache, shared across every account on the same server. Keyed by the realmlist address (unambiguous server identity, can't collide across unrelated private servers) plus the realm name — so alts on different accounts pool into one file rather than each rebuilding their own. Tab-separated text, ~30 bytes/entry, hand-editable. On first load with no shared file yet, a pre-existing per-account cache (WTF\Account\<account>\<realm>\ClassicAPI_NameCache.txt) is migrated in automatically.
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 | Realm name. Single-realm, so usually the local 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. On engine cache miss, GetPlayerInfoByGUID falls
back to the persistent cache and returns name, class, race,
and sex from storage (realm comes back as "" since this is
single-realm and we don't carry per-player realm names).
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 three 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 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 always "" — the engine doesn't populate
per-player realm names and there is no cross-realm interaction.
We push the empty string rather than nil to match the convention
of the other player-info accessors; addons can gate on 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 in the persistent NameCache, or nil if the name
isn't cached. Companion to GetPlayerInfoByGUID for the case where
an addon has a player's name but not their GUID — e.g., when
GetCurrentChatGUID() returns nil for a chat event the engine
didn't tag with the sender GUID (system notifications, some channel
messages, etc.).
local _, class, _, race, _, _, _, guid = C_PlayerCache.GetPlayerInfoByName("Gedwyr")
-- "MAGE", "Human", "0x000000003B9ADAA7"
C_PlayerCache.GetPlayerInfoByName("NeverSeen") -- nil (not cached)
Match is case-sensitive, exact — server-stored names
are case-stable ("Gedwyr" won't match "gedwyr").
Returns (vs. GetPlayerInfoByGUID):
| Slot | Value | Notes |
|---|---|---|
| 1–7 | same as GetPlayerInfoByGUID |
see above |
| 8 | guid |
"0xHHHHHHHHLLLLLLLL" — the cached player's full GUID, so name-based callers can chain into GUID-keyed APIs. Absent from GetPlayerInfoByGUID since the caller already has it. |
Only reads from the persistent NameCache — does not hit the
engine's in-memory NAME_QUERY cache (that's keyed by GUID and has
no name index). So this requires C_PlayerCache.SetEnabled(true)
to have been opted into; entries get there via the engine's
NAME_QUERY response hook, the visible-object scan, and
C_PlayerCache.RememberPlayer.
Implementation: O(1) lookup via a name → guid index map
maintained in lockstep with the GUID-keyed entry map. Eviction
keeps the index consistent: when a name's character is deleted and
the name is recycled to a different GUID, the prior entry is
removed from both maps.
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.
guid—"0xHHHHHHHHLLLLLLLL"string (same formatUnitGUIDreturns). 8-hex"0xLLLLLLLL"is also accepted (hi-dword zero).name— 1–12 ASCII chars (the character-name range). Tabs, newlines, and high bytes are stripped.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; theSMSG_NAME_QUERY_RESPONSEhook handles direct assignment).
-- 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 gets a different cache 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\Account\<account>\<realm>\ClassicAPI_NameCache.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.SetScanEnabledbecomes effective if also turned on.
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
C_PlayerCache.SetScanEnabled(enabled)
Opts into the visible-object sweep: an opportunistic walk of
the engine's live visible-object list that feeds every player in
render range (whose object is currently loaded by the client) into
the persistent name cache. Throttled to once per ~10 seconds, on
the existing Frame::RegisterEvent hook — no per-frame overhead,
no extra hooks installed.
enabled is a boolean (numeric 0/1 also accepted). Persists to
WTF\Account\<account>\ClassicAPI.txt as
NameCacheScanEnabled=1/0.
C_PlayerCache.SetEnabled(true) -- prerequisite
C_PlayerCache.SetScanEnabled(true)
-- Now: standing in Stormwind for a minute pre-populates the cache
-- with every visible player's name and class.
Independent of the cache toggle. Turning this on alone has no
effect — the sweep relies on Remember() which silently no-ops
when the on-disk cache is disabled. Turning the cache off (without
also turning the scan off) preserves the scan setting for the next
time you re-enable the cache.
Players only. NPCs and pets aren't cached — their GUIDs are ephemeral and can get reused across sessions, so caching them is worse than useless.
Implementation: walks ClntObjMgrEnumVisibleObjects
(0x00468380) — the same engine iterator
VanillaMinimapTracking
uses for blip rendering — filtering each GUID through
ClntObjMgrObjectPtr (0x00468460) with TYPEMASK_PLAYER. Name
comes from the CGObject vftable's GetName slot; class is the
byte at [m_objectFields + 0x79] (UNIT_FIELD_BYTES_0 byte 1, same
field Script_UnitClass reads).
C_PlayerCache.IsScanEnabled()
Returns the current visible-object scan setting as a boolean. Independent of the cache toggle — this only reflects the scan- specific setting, not whether the scan is effectively running (which also requires the cache to be on).
if C_PlayerCache.IsScanEnabled()
and C_PlayerCache.IsEnabled() then
-- visible-object sweeps are running every ~10s
end
NewItems
The "new item glow" bookkeeping — the sparkle a bag UI shows on items you've just picked up but haven't looked at yet. There is no DBC, server data, or engine bit for it; it's pure client-side tracking over your bag contents, so ClassicAPI adds it.
Newness is keyed on each item's instance GUID (read off the CGItem),
not on (bagID, slotIndex). A flag therefore survives the player
rearranging a bag, and is impossible for a pure-Lua
addon (no per-instance GUID is exposed to Lua). Only bag items are
tracked (bags 0–4); equipment and bank are out of scope.
The feed is entirely client-side C++ (no addon Lua), and
event-driven rather than polled: it hangs off the engine's own BAG_UPDATE
fire sites, so a bag rescan runs only on frames where a bag actually
changed. Each rescan diffs the resident item GUIDs against the previous one,
flags anything newly acquired, and prunes flags for items that leave.
BAG_NEW_ITEMS_UPDATED (no payload) fires whenever the new-item set
changes. A one-time baseline taken ~1.5s after login (re-armed on character
switch) means items already owned at login aren't flagged.
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 (e.g. once the player has
seen it). 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.
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 row triage: was that a player kill or a mob kill?
local function OnCombatLogEvent(_, _, eventType, srcGUID, ...)
if eventType == "PARTY_KILL" then
if C_PlayerInfo.GUIDIsPlayer(srcGUID) 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.
Quest
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. |
completionText |
string | The "now turn it in" text shown on the reward UI panel after objectives are met. Often empty for simple quests; populated for narrative-heavy ones. Also exposed as GetQuestLogCompletionText. |
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.
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 the category already
has as many sounds as it allows.
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 sound category, 0 to 12. Leave it out for 0, which is what the client uses for interface sounds and music. A channel name such as "SFX" is accepted and ignored: the categories here do not match those names. |
forceNoDuplicates |
Accepted and ignored. |
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. |
uiSoundSubType |
Accepted and ignored. |
forceNoDuplicates |
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.
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.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. All read directly from Spell.dbc (with
SpellIcon.dbc, SpellCastTimes.dbc, and SpellRange.dbc for the
indirected fields). Cast time is in milliseconds; ranges are floats in
yards. isFunnel is a real boolean (true/false). Returns nil if
the spell ID is out of range.
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 | Base cast time in milliseconds, or 0 for instant |
minRange |
number | Yards, or 0 if not applicable |
maxRange |
number | Yards, or 0 if not applicable |
spellID |
number | Echo of the input |
rank |
string | Localized rank (e.g. "Rank 1") — an extra field |
cost |
number | Base ManaCost — an extra field |
isFunnel |
boolean | True for funnel-channeled spells — 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.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 -- true for a warrior/rogue past level 10, etc.
Server-side this is a plain flag (Player::m_canDualWield) that starts
false and is turned on only by the SPELL_EFFECT_DUAL_WIELD spell
effect — no class has it innately. Exactly one spell in the 1.12 client's
Spell.dbc carries that effect: 674 ("Dual Wield"), the trained
warrior / rogue / hunter passive, which lands in the known-spell bitmap
like any learned spell. So CanDualWield() is precisely
IsPlayerSpell(674) — there is no separate client-visible capability
field to read.
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.
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 }
-- off cooldown: { startTime=0, duration=0, isEnabled=true, modRate=1 }
| 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. |
The activeCategory, timeUntilEndOfStartRecovery, and
isOnGCD fields read as nil since they have no source here.
Returns nil if the resolved spellID is 0 or doesn't have a
Spell.dbc row.
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. |
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 (40yd)
C_Spell.SpellHasRange(1006) -- Inner Fire → false (self buff)
SpellHasRange(10, "spell") -- 10th spellbook slot in player book
Looks up Spell.dbc.RangeIndex (+0x90) → SpellRange.dbc row,
then tests minRange > 0 or maxRange > 0. The stock client doesn't ship
this function. 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), or an unresolvable spell / unit.
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.
Range-only: it ignores line of sight, and it does
not reject wrong-faction targets (a friendly-only heal still
returns a range answer against an enemy). Check target validity
separately if you need it. 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.
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 player's current cursor world
position, bypassing the manual click on the AoE reticle the engine
would otherwise require. ClassicAPI's analog of
/cast [@cursor] Blizzard. Returns true when the cursor-placement
leg landed; false for non-ground-target spells (cast still fires
normally on the current target), unknown spells, cursor over UI /
off-screen, etc.
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 is its own
spellID, so 10 casts Blizzard Rank 1 (not the highest known rank). A
name goes through the engine's own resolver, which parses a trailing
(Rank N); a bare name casts the highest rank you know.
Two-stage internally:
-
Initiates the cast: a numeric spellID resolves to its exact spellbook slot (
Spell::Lookup::FindSpellbookSlot, so the requested rank is the one that fires), a name goes throughFUN_RESOLVE_SPELL_NAME_TO_SLOT; either way it dispatches through the engine's cast dispatcher. If the spell needs a ground target, the engine enters placement mode and arms the AoE reticle. -
Refreshes the cursor's screen→world raycast via
FUN_REFRESH_CURSOR_RAYCAST(the same internal the engine fires on every mouse move when placement is active), confirms the cursor hit terrain (not a UI frame or a unit), and commits placement viaFUN_COMMIT_PLACEMENT_COORDSwith the resolved(x, y, z). Cancels placement cleanly viaFUN_STOP_PLACEMENTwhen the cursor isn't on terrain or when the spell wanted a unit-target rather than ground.
For non-placement spells (regular target casts, self-buffs, etc.)
the cast fires normally; the placement-resolve no-ops since the
engine never set the placement flag, and we return false.
The companion item version is
C_Item.UseAtCursor — same chain via
the item-use path for grenades / on-use ground-target items.
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")
The unit is resolved once to its object; the cast is dispatched with the
unit's GUID as the implicit target (the engine substitutes it exactly
where it would the current selection), so a unit-target spell fires
immediately at that unit. A ground spell ignores the GUID and enters
placement mode instead, which is then committed at the unit's world
position (read from its GetPosition virtual — the same one
CheckInteractDistance / UnitInRange use) via
FUN_COMMIT_PLACEMENT_COORDS. The engine can't be left armed in placement
mode on failure: if no placement is active after dispatch the cast already
went out, and a non-ground placement (an unaccepted unit target) is
cancelled. A genuinely unrecognized unit-token string raises the engine's
standard "Unknown unit" error, matching UnitHealth("garbage").
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.
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.
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.
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.
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(label)
Readers for the BREATH (drowning) / EXHAUSTION
(off-map) / FEIGNDEATH (hunter Feign Death) bar state. The
engine fires the MIRROR_TIMER_START / _PAUSE / _STOP events
with the full packet payload but doesn't cache anything internally;
ClassicAPI hooks the SMSG handler at 0x005E7990 and builds a
3-slot side cache so these accessors work.
GetMirrorTimerInfo(index) — index is 1, 2, or 3 (slot id). Returns
(timer, value, maxValue, scale, paused, label) for an active slot,
or nothing if that slot is empty:
| Return | Type | Meaning |
|---|---|---|
timer |
string | Engine type-name: "EXHAUSTION", "BREATH", or "FEIGNDEATH" |
value |
number | The snapshot value from the last server packet (ms). Not live-interpolated — see GetMirrorTimerProgress for that |
maxValue |
number | Timer's max (ms) |
scale |
number | Server-set rate. Negative = depleting (the server sends -1 for breath, draining 60000 ms over 60 s of real time); positive = filling |
paused |
boolean | true if the engine has frozen the timer |
label |
string | Localized display label, e.g. "Breath". For FEIGNDEATH, the spell name |
GetMirrorTimerProgress(label) — label is the engine type-name
("BREATH" etc.). Returns the live interpolated value in
milliseconds, computed each call from the snapshot + elapsed
real-time ticks. Returns 0 when no matching timer is active.
local f = CreateFrame("Frame")
f:RegisterEvent("MIRROR_TIMER_START")
f:SetScript("OnEvent", function()
-- pull whichever slot the engine just populated
for i = 1, 3 do
local timer, value, maxv, scale, paused, label = GetMirrorTimerInfo(i)
if timer then
DEFAULT_CHAT_FRAME:AddMessage(format("%s: %d / %d (%s)",
label, value, maxv, paused and "paused" or "running"))
end
end
end)
-- Smooth bar updates: poll progress in OnUpdate
local function OnUpdate(self)
self:SetValue(GetMirrorTimerProgress("BREATH"))
end
The off-map timer is
"EXHAUSTION", not"FATIGUE". The engine type-name string for the off-map timer is"EXHAUSTION"— we surface what the engine actually returns rather than translating the name. Addons porting"FATIGUE"-keyed code need to handle either string.
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().
System
Host/OS-level helpers that aren't tied to a game domain.
GetPhysicalScreenSize()
Returns (widthPixels, heightPixels) — the display's physical
resolution in pixels. FrameXML's PixelUtil builds its entire
pixel↔UI-unit conversion on it (768.0 / physicalHeight), which is what
lets addons snap frames to whole pixels for crisp borders at fractional
UI scales.
local w, h = GetPhysicalScreenSize() -- e.g. 1920, 1080
The stock client has no such native — and no pre-parsed pixel-dimension
global. GetScreenWidth()/GetScreenHeight() return UI-space units
(derived from the UIParent aspect ratio at [UIParent + 0x7c]), not
pixels. The engine only ever holds the current mode as the
gxResolution CVar string ("WIDTHxHEIGHT"); both SetScreenResolution
(0x0048bfd0) and the display-init path sscanf it on demand rather
than caching dimensions anywhere. So we read the gxResolution CVar
directly through the engine's by-name lookup (FUN_FIND_CVAR, value at
+0x20) and parse it — the same source of truth the engine uses.
Returns 1024, 768 (→ factor 1.0, making PixelUtil a 1:1 no-op) when
the CVar is missing or unparseable. Caveat: in windowed mode
gxResolution reflects the configured render resolution, which may
differ from the OS window's client size — there is no separate
window-pixel query.
The companion PixelUtil table (PixelUtil.SetPoint/SetSize/
SetStatusBarValue/…) is provided Lua-side by the ClassicAPI addon and
consumes 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:|cAARRGGBB … |rcolor codes → removed|H…|h text |hhyperlinks → keeps only the visible text|T…|ttextures → 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.
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
Additional TargetScript selection functions. The engine
already ships TargetNearestEnemy/Friend/PartyMember/RaidMember,
TargetLastEnemy/Target, AssistUnit, TargetUnit, ClearTarget; these
fill in the rest. They're built on the engine's own
tab-targeting core — the same visible-unit enumeration, per-mode validity
predicate (reaction/attackability/alive), and selection commit the native
TargetNearestEnemy uses — so hostility semantics match the client exactly.
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).
The stock client doesn't ship this (only character-create/select facing
exist), but it's the companion the direction-target functions below need
— pass it to aim "in front of me."
local facing = GetPlayerFacing() -- e.g. 1.65
Read from the player unit's movement-block orientation field
(player + 0x9C4).
TargetDirectionEnemy(facing [, coneAngle])
Targets the nearest attackable unit within a cone centered on facing
(an absolute world angle in radians, as returned by GetPlayerFacing()).
coneAngle is the full cone width in radians; omitted, it defaults to
pi/2 (90° — 45° to either side).
-- Target whatever enemy you're looking at:
TargetDirectionEnemy(GetPlayerFacing())
-- Wider 180° sweep in front:
TargetDirectionEnemy(GetPlayerFacing(), math.pi)
Does nothing if no enemy falls inside the cone. "Attackable" uses the
engine's own reaction check, so it matches what TargetNearestEnemy would
consider a valid enemy (hostile, alive, not a critter).
TargetDirectionFriend(facing [, coneAngle])
Same as TargetDirectionEnemy, but selects the nearest friendly
(assistable) unit in the cone.
TargetNearest([reverse])
Cycles the nearest unit you can attack or assist. Repeated calls within ~1s
step through candidates nearest→farthest (a frozen snapshot, so the order
is stable mid-cycle); a longer gap or an externally-changed target rebuilds
and re-targets the nearest. reverse (truthy) steps the other way.
TargetNearestEnemyPlayer([reverse])
Like TargetNearest, but restricted to hostile players (PvP targeting).
Same cycle behavior and reverse flag.
TargetNearestFriendPlayer([reverse])
Like TargetNearestEnemyPlayer, but restricted to friendly players.
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. - Two more fields shift the art from the text, as
|A:name:h:w:offsetX:offsetY|a.
print("Repair cost: |A:Repair:0:0|a")
print("Small marker |A:MinimapArrow:12:12|a here")
An unknown name draws nothing and leaves the rest of the line intact. Art that
repeats is drawn once, so tilesHorizontally and tilesVertically have no effect
in text.
Text typed by other players never draws atlas art. This matches how a typed texture path behaves.
Time
GetServerTime()
Returns the current server clock as a Unix epoch timestamp (seconds since
1970-01-01 UTC), or nil before login (while the engine's game-time
struct is still BSS-zero).
local now = GetServerTime()
-- now = 1778260148 (Fri 2026-05-08 17:09:08 UTC)
A client can replace this global. Some clients declare their own
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.
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 _, name, start, duration, icon = GetTotemInfo(1) -- Fire slot
if name ~= "" then -- a totem IS active
local remaining = duration - (GetTime() - start)
-- name = "Searing Totem", icon = "Interface\\Icons\\...", etc.
end
| Return | Meaning |
|---|---|
haveTotem |
true if the player carries the slot's totem tool (Fire/Earth/Water/Air Totem item) — the documented meaning, not whether a totem is summoned. |
totemName |
The active totem spell's localized name ("" when no totem is out — this is how you test "is a totem active"). |
startTime |
GetTime() value when the totem was cast (0 when none). |
duration |
Total duration in seconds (0 when none). |
icon |
The active totem spell's icon texture path, or nil when none. |
modRate |
Always 1 — there is no per-totem haste. |
spellID |
The active summon spell's ID (0 when none). |
haveTotemis tool presence, not summon state. Matching the live API:haveTotemreflects whether you hold the slot's totem tool item (Fire Totem5176,Earth Totem5175,Water Totem5177,Air Totem5178) in bags/equipped, independent of whether a totem is currently deployed. The tool item is the totem spell'sTotem[0]requirement (a tool, not a consumed reagent), read from the spell — not hardcoded. To check for an active totem, testtotemName ~= ""(orstartTime > 0).
There is no client-side totem tracking here (no totem bar,
PLAYER_TOTEM_UPDATE, or GetTotemInfo), so this is self-tracked but
data-driven: the slot is read from the summon spell's Spell.dbc
effect (SUMMON_TOTEM_SLOT1..4 = Fire/Earth/Water/Air),
duration from SpellDuration.dbc, the totem's creature entry (for death
detection) from the effect's EffectMiscValue, and the haveTotem tool
item from the spell's Totem field (the totem the spell requires) —
resolved from the player's spellbook, not a hardcoded table. Casts are observed
via the SMSG_SPELL_GO hook; a slot clears when its duration elapses OR
when the totem creature disappears early (killed, Totemic Recall, or
destroyed) — detected by scanning the object manager for the player-owned
totem creature. Because it's data-driven, Turtle-custom totems are tracked
automatically. Slot changes fire
PLAYER_TOTEM_UPDATE.
GetTotemTimeLeft(slot)
Returns the seconds remaining before the slot's totem is auto-destroyed,
or 0 when no totem is active in that slot. Slots: 1 Fire, 2 Earth,
3 Water, 4 Air.
local remaining = GetTotemTimeLeft(1) -- Fire slot, e.g. 12.4
Computed from the same tracker as GetTotemInfo
(startTime + duration − now), so it shares that data's caveats — most
notably it reflects the totem's full duration from cast, unaffected by
any server-side early expiry the client isn't told about (the WorldTick
scan still clears the slot when the totem creature actually disappears).
GetTotemDuration(slot)
Returns the slot's active totem's total duration in seconds (its full
lifetime from cast), or 0 when no totem is active. The companion to
GetTotemTimeLeft (remaining time) — same value
as GetTotemInfo's 4th return.
local total = GetTotemDuration(1) -- e.g. 60
local frac = GetTotemTimeLeft(1) / GetTotemDuration(1) -- bar fill
TargetTotem(slot)
Targets the player's totem in the given slot (1 Fire, 2 Earth, 3
Water, 4 Air). No-op when the slot has no active totem or the totem
creature isn't currently visible in the client.
TargetTotem(2) -- target your Earth totem
Looks up the totem creature's live GUID (object-manager scan for the
player-owned creature of the slot's tracked entry) and sets the target
through the same engine path TargetUnit uses (CMSG_SET_SELECTION).
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(...). nil if the icon is missing. |
| 3 | active |
boolean | true when this tracker is the one now in effect. |
| 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. To turn tracking off, use the built-in
CancelTrackingBuff().
for i = 1, GetNumTrackingTypes() do
local name = GetTrackingInfo(i)
if name == "Find Herbs" then
SetTracking(i)
break
end
end
TradeSkillUI
The trade-skill list link — the shareable
|Htrade:...|h[Profession]|h link that shows a player's profession and
which recipes they know. The engine has no such link type (its
hyperlink parser only knows item: and enchant:), so both the link and the
click-to-view UI are synthesized. Clicking a received link opens a
crafting-window-style frame (the real TradeSkillFrame parchment art) listing
the recipes the linker knows — difficulty-coloured and sorted high-craft-first
— with a skill-rank bar and an "X of Y recipes known" summary.
The "known recipes" data is a bitfield over the skill line's canonical
recipe list — every SkillLineAbility.dbc row for the skill line that is a
craftable recipe, in ascending record-ID order. That ordering is identical on
every client with the same DBC, so the sender and reader agree on the
bit-index space with no handshake. Whether the sender knows a given recipe
comes from the same player-spell bitmap IsPlayerSpell reads (which covers
profession recipes), so the link is self-contained — the reader needs neither
the sender online nor the profession itself.
C_TradeSkillUI.GetTradeSkillListLink()
Returns the |Htrade:...|h link for the currently-open trade skill
window, or nil if no profession window is open.
-- with a profession window open:
local link = C_TradeSkillUI.GetTradeSkillListLink()
-- "|cffffd000|Htrade:164:300:300:Bob:eE0C...|h[Blacksmithing]|h|r"
ChatEdit_InsertLink(link) -- or SendChatMessage(link, "GUILD"), etc.
Built entirely in the DLL — no addon wiring. The window only needs to be open long enough to identify the profession; the recipe data is read from the spell bitmap, not the window's server list. Wire format (a ClassicAPI-private shape modeled on the 2.x link, with the linker's name in the slot 2.x used for the GUID):
|cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
skillLineID is the SkillLine.dbc row (164 = Blacksmithing); curRank /
maxRank are the linker's skill level (cosmetic); linkerName is the linking
character's name (shown in the viewer title); the base64 tail packs 6 recipes
per character, LSB-first. Links from before this field was added (no
linkerName) still decode — the viewer just omits the name from its title.
Clicking a trade: link is handled automatically (an override of
SetItemRef) — it opens the recipe viewer. No addon wiring needed.
C_TradeSkillUI.GetCraftListLink()
Same as GetTradeSkillListLink, but for the Craft window — the
separate profession frame used by Enchanting and pet training. Returns nil
unless CraftFrame is shown. The two windows have separate storage and can be
open simultaneously, so they get separate builders; the resulting link is
the same |Htrade:...|h format and opens the same viewer.
local link = C_TradeSkillUI.GetCraftListLink() -- with the enchanting window open
-- "|cffffd000|Htrade:333:300:300:Bob:...|h[Enchanting]|h|r"
C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)
Decodes a link's bitfield against a skill line's canonical recipe list. Returns an array table:
local recipes = C_TradeSkillUI.GetTradeSkillListRecipes(164, "eE0C...")
-- recipes[i] = { spellID, isKnown, trivialLevel, greenLevel, createdItem }
for i = 1, table.getn(recipes) do
local name = GetSpellInfo(recipes[i].spellID)
print(name, recipes[i].isKnown and "known" or "unknown")
end
trivialLevel / greenLevel are the skill levels the recipe turns grey /
green at — the recipe's difficulty band, coloured against the linker's skill
exactly as the default UI does (the required-skill field is uselessly 1 for
nearly every trainer-taught recipe). createdItem is the crafted item's ID
(0 for enchants), which the viewer uses to build its subclass filter.
The array is in canonical recipe order (one entry per possible recipe in the
skill line, not just the known ones). Turn a spellID into a name/icon with
GetSpellInfo. Recipes past the end of a short/garbled
bits string decode as not-known rather than erroring.
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, softenemy,
softfriend, softinteract). nameplateN and focus are
included — we hook the resolver so the engine recognizes both 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
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.
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.
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 entry id is packed into the unit's GUID (bits 24-47), so this reads it
straight from the resolved GUID — no creature cache lookup. Returns nil for a
player (a player GUID carries no template), an unresolvable-but-valid token
("target" with nothing targeted, an empty "partyN" slot), 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).
Reads UNIT_FIELD_FLAGS bit 29 (0x20000000) from the broadcast
descriptor — works for any unit since UNIT_FIELD_FLAGS is broadcast
in object updates.
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.
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.
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
Traces a ray between the two units through the client's world-collision
geometry (CWorld::Intersect, flags terrain + WMO). Endpoints are
raised by each unit's collision-box height so a foot-to-foot ray doesn't
false-block on terrain, and the trace runs in two passes (level look,
then look up/down) for height mismatches — the same approach UnitXP_SP3
uses, implemented natively so it works with or without that addon (a
call routes harmlessly through UnitXP_SP3's hook when present).
Notes and limits:
- Terrain + buildings (WMO) only — M2 doodads (trees, small props) are not tested (that flag combination crashes in some dungeons). This matches server-side LoS, which also ignores most doodads.
- 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 matches
the integer UnitClass's third return surfaces (1=Warrior,
2=Paladin, 3=Hunter, 4=Rogue, 5=Priest, 7=Shaman, 8=Mage,
9=Warlock, 11=Druid — 6 and 10 are unused here).
local token, id = UnitClassBase("player") -- "WARRIOR", 1
local token = UnitClassBase("target") -- works for any synced unit
local color = RAID_CLASS_COLORS[UnitClassBase("party1")]
Addons use the token for class detection because the
UnitClass(unit) returns a localized first return (e.g.
"Krieger" on a German client), which is fine for display but
breaks any addon code that keys on if class == "WARRIOR". The
classID second return is a ClassicAPI extension — the standard
UnitClassBase returns the token only — but it saves callers
from chaining UnitClass(unit) just to get the integer.
Reads the class byte from UNIT_FIELD_BYTES_0 (descriptor +0x79,
the same byte UnitClass's general-token path reads) and looks up
the english filename at ChrClasses.dbc::Filename (record +0x38).
Both fields are broadcast / static, so remote players' class is
current regardless of distance.
Returns (nil, nil) for:
targetwith no current target, emptypartyN/raidNslots, or units whose descriptor isn't loaded yet- creatures and other non-player units (their class byte indexes a different table; the class-byte lookup naturally returns nil for out-of-range / zero values)
Throws a Lua error on missing / non-string unit argument — same
shape as UnitClass itself.
UnitRaceBase(unit)
Returns (raceFile, raceID) — the locale-independent race token
plus the numeric raceID. The token is one of "Human", "Orc",
"Dwarf", "NightElf", "Scourge", "Tauren", "Gnome",
"Troll"; the raceID is 1..8 for those values respectively.
local token, id = UnitRaceBase("player") -- "Human", 1
local token = UnitRaceBase("target") -- works for any synced unit
Sibling to UnitClassBase. Same problem
(the UnitRace(unit) returns a localized name — "Mensch",
"Orc", etc.); same solution (locale-independent token straight
from the DBC). Reads byte 0 of UNIT_FIELD_BYTES_0 (descriptor
+0x78) and looks up ChrRaces.dbc::Filename (+0x3C).
Returns (nil, nil) for unresolvable units and non-player units
(creature race bytes don't index ChrRaces.dbc).
ClosestUnitPosition(creatureID)
Returns xPos, yPos, distance — the world position of the nearest
creature with the given NPC (creature-template) ID, and its distance from
the player in yards. Returns nothing (nil) when no matching creature is in
range.
local x, y, dist = ClosestUnitPosition(3098) -- nearest Kobold Vermin
if x then
-- x, y = world coords; dist = yards from the player
end
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.
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.
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.
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. Indices stay stable either way. A HARMFUL
walk visits slots 32 to 47 first, then any spilled debuffs in 0 to 31.
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. 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). On a cache miss it falls back to the base Spell.dbc → SpellDuration.dbc value with level scaling. 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. 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 |
isStealable, isBossAura, isNameplateOnly, nameplateShowAll, nameplateShowPersonal, canApplyAura, shouldConsolidate, isRaid |
boolean | always false — UI concepts not present here |
auraInstanceID, 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 upper-case
tokens ("HELPFUL", "HARMFUL",
"HELPFUL|PLAYER", etc.). Honored:
HELPFUL(default) /HARMFUL— pick buffs or debuffs by each aura's polarity flag. InGetUnitAuras, supplying neither returns both.PLAYER— restrict to auras the local player cast, via theAura::Sourcecaster cache (sourceGUID ==player GUID). Combines with the range tokens ("HARMFUL|PLAYER"= your debuffs only). Because the caster is best-effort, an aura whose cast we didn't observe is treated as not-player-cast and excluded — soPLAYERcan under-report auras that predate login.
Other tokens (RAID / CANCELABLE / INCLUDE_NAME_PLATE_ONLY) are
accepted but no-op — they need engine systems (raid-dispel relevance,
nameplate visibility flags) not present here.
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 theSpell.dbcbase with level scaling.- 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 same way UnitBuff does.
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 !. 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 the local player cast, or only auras the player did not cast. Caster data comes from casts this session, so an aura present before you saw it cast has no caster and counts as not-player.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 (RAID, CANCELABLE,
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 — or a player's pet applied the aura, 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 field-name differences from the table: position 12 is isBossDebuff
(the table field is isBossAura) and position 13 is castByPlayer (the table
field is isFromPlayerOrPlayerPet, the same value). The boss field is
always false; castByPlayer is reported truthfully.
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 the PLAYER, DISPELLABLE, and CROWD_CONTROL
predicates. 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.GetUnitAuraBySpellID(unit, spellID [, filter])
Linear-searches unit's aura array for the first populated slot
whose spellId matches and returns its AuraData, or nil if not
found. 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])
Linear-searches unit's aura array for the first populated slot
whose locale-resolved name matches spellName exactly. Returns
the AuraData for that slot or nil if not found. Case-sensitive.
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 | Always an empty table — the XML schema has no <KeyValues> element, so no template can carry key/value pairs. Present for API parity. |
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=.