Blizzard UI Systems Deep Dive¶
Blizzard ships dozens of UI systems that addon developers can hook, extend, or integrate with. Midnight (12.0) didn't replace these systems — it hardened them with Secret Values and consolidated APIs into namespaces. Understanding what Blizzard provides is the first step to building addons that cooperate with the game instead of fighting it.
This page covers every major Blizzard UI system relevant to addon development, with real API signatures, working code examples, and the pitfalls that will waste your afternoon if you don't know about them.
Prerequisites
This page assumes you've read Coding for Midnight for the philosophy of hooking vs. replacing, and Security Model for taint fundamentals. Here we focus on what Blizzard provides and how to interact with it.
Cooldown Manager¶
Blizzard's built-in Cooldown Manager (also called "Essential Cooldowns") was added in Patch 11.1.5 and significantly expanded through 11.2.5. In Midnight, cooldown data flows through Secret Values — making this system critical to understand.
The Cooldown Widget¶
The Cooldown frame type renders clock-sweep animations. Every action button, buff icon, and item slot uses one.
-- Basic cooldown display
local cooldown = CreateFrame("Cooldown", nil, parentFrame, "CooldownFrameTemplate")
cooldown:SetAllPoints(parentFrame)
-- Pre-12.0 way (still works outside restricted contexts)
cooldown:SetCooldown(startTime, duration)
-- Read back cooldown times
local start, duration = cooldown:GetCooldownTimes()
Midnight 12.0: Duration Objects¶
The old SetCooldown(start, duration) breaks when those values are Secret Values. Blizzard's solution: Duration Objects — opaque containers that carry cooldown timing without exposing the numbers.
-- Create a reusable Duration Object
local duration = C_DurationUtil.CreateDuration()
-- Three ways to populate it:
duration:SetTimeSpan(startTime, endTime)
duration:SetTimeFromStart(startTime, durationSecs)
duration:SetTimeFromEnd(endTime, durationSecs)
-- Pass it straight to the cooldown widget — no math needed
cooldownFrame:SetCooldownFromDurationObject(duration)
New Cooldown widget methods in 12.0:
| Method | Purpose |
|---|---|
SetCooldownFromDurationObject(duration [, clearIfZero]) | Secret Values-compatible cooldown display |
SetCooldownFromExpirationTime(expTime, dur [, modRate]) | Expiration-based alternative |
SetPaused(paused) | Pause/resume the sweep animation |
GetCountdownFontString() | Access the countdown text overlay |
Spell Cooldown APIs¶
-- 11.0+ standard API (returns SpellCooldownInfo table)
local info = C_Spell.GetSpellCooldown(spellID)
-- info.startTime, info.duration, info.isEnabled, info.modRate
-- GCD check: use spell ID 61304
local gcdInfo = C_Spell.GetSpellCooldown(61304)
New 12.0 Duration-returning APIs (return Secret Values in restricted contexts):
| Function | Purpose |
|---|---|
C_Spell.GetSpellCooldownDuration() | Spell cooldown as Duration Object |
C_Spell.GetSpellChargeDuration() | Charge cooldown as Duration Object |
C_Spell.GetSpellLossOfControlCooldownDuration() | LoC cooldown as Duration Object |
C_SpellBook.GetSpellBookItemCooldownDuration() | Spellbook item cooldown |
The Built-in Cooldown Viewer¶
Blizzard's own cooldown tracker (the Edit Mode "Cooldown Viewer" frame):
- Disabled by default — players enable it in Options > Advanced Options
- Requires level 10+
- Categories: Essential, Utility, Hidden by Default (for cooldowns), Icons/Bars/Hidden (for buffs)
- Positioned via Edit Mode using
EditModeCooldownViewerSystemMixin - 11.2.5+: Advanced drag-and-drop configuration, per-spec display, category reassignment
Don't duplicate what Blizzard already provides
Before building a custom cooldown tracker, check whether the built-in Cooldown Viewer already does what you need. Many players now use it, and addons that duplicate its functionality create visual clutter. Consider supplementing it instead — add cooldowns Blizzard doesn't track, or provide a different layout.
Complete Cooldown Pattern (12.0+)¶
local addonName, ns = ...
-- Reusable Duration Object (avoid creating per-frame)
local durationObj = C_DurationUtil.CreateDuration()
local function UpdateCooldownDisplay(cooldownFrame, spellID)
-- Try the Duration API first (Secret Values-safe)
local duration = C_Spell.GetSpellCooldownDuration(spellID)
if duration then
cooldownFrame:SetCooldownFromDurationObject(duration)
return
end
-- Fallback for non-restricted contexts
local info = C_Spell.GetSpellCooldown(spellID)
if info and info.duration > 0 then
cooldownFrame:SetCooldown(info.startTime, info.duration, info.modRate)
else
cooldownFrame:Clear()
end
end
ActionButton cooldown split
In 12.0, ActionButtonTemplate split its single cooldown frame into three children: cooldown, chargeCooldown, and lossOfControlCooldown. If your addon skins action buttons by accessing the cooldown child, update your code to handle all three. The global function ActionButton_ApplyCooldown() handles this automatically using secure delegates.
Edit Mode System¶
Edit Mode (added in 10.0) lets players rearrange HUD elements by dragging them. It's the system behind the grid-snapping, layout-saving UI that appears when you press Escape > Edit Mode.
Core Architecture¶
EditModeManagerFrame (DIALOG strata, global singleton)
├── Registered system frames (action bars, minimap, chat, etc.)
├── EditModeSystemSettingsDialog (per-system settings panel)
└── Layout storage (C_EditMode namespace)
C_EditMode Namespace¶
| Function | Purpose |
|---|---|
C_EditMode.GetLayouts() | Get all saved layouts |
C_EditMode.SaveLayouts(layoutInfo) | Persist layout changes |
C_EditMode.SetActiveLayout(index) | Switch active layout |
C_EditMode.GetAccountSettings() | Account-wide settings |
C_EditMode.SetAccountSetting(setting, value) | Set account-wide setting |
C_EditMode.ConvertLayoutInfoToString() | Serialize layout for sharing |
Events¶
| Event | Payload |
|---|---|
EDIT_MODE_LAYOUTS_UPDATED | layoutInfo, reconcileLayouts |
EventRegistry "EditMode.Enter" | — |
EventRegistry "EditMode.Exit" | — |
EventRegistry "EditMode.SavedLayouts" | — |
Detecting Edit Mode State¶
-- React when Edit Mode opens/closes
EventRegistry:RegisterCallback("EditMode.Enter", function()
-- Player entered Edit Mode — show configuration UI, unlock frames, etc.
MyAddon:EnterEditMode()
end)
EventRegistry:RegisterCallback("EditMode.Exit", function()
-- Player left Edit Mode — lock frames, save positions
MyAddon:ExitEditMode()
end)
System Mixins¶
Every draggable element in Edit Mode uses a specialized mixin inheriting from EditModeSystemMixin:
| Mixin | System |
|---|---|
EditModeActionBarSystemMixin | Action Bars 1–8 |
EditModeCastBarSystemMixin | Cast Bar |
EditModeMinimapSystemMixin | Minimap |
EditModeChatFrameSystemMixin | Chat Frame |
EditModeUnitFrameSystemMixin | Unit Frames |
EditModeAuraFrameSystemMixin | Buffs/Debuffs |
EditModeBagsSystemMixin | Bags |
EditModeMicroMenuSystemMixin | Micro Menu |
EditModeObjectiveTrackerSystemMixin | Objective Tracker |
EditModeCooldownViewerSystemMixin | Cooldown Viewer |
Settings are controlled by: EditModeSettingDropdownMixin, EditModeSettingSliderMixin, EditModeSettingCheckboxMixin.
Registering Custom Frames (The Taint Problem)¶
Blizzard provides no official API for addons to register custom frames with Edit Mode. Calling EditModeManagerFrame:RegisterSystemFrame() from addon code introduces taint and can break the entire Edit Mode UI.
Do NOT call RegisterSystemFrame() directly
Community libraries that solve this problem:
| Library | Approach | Usage |
|---|---|---|
| LibEditMode (p3lim) | Parallel selection/dialog system alongside Blizzard's | lib:AddFrame(frame, callback, default, name) |
| EditModeExpanded (teelolws) | Extensive feature registration with full settings | lib:RegisterFrame(frame, name, db) |
| LibEditModeOverride (plusmouse) | Programmatic layout manipulation | Uses C_EditMode.GetLayouts() / SaveLayouts() |
Source Files¶
All in Interface/AddOns/Blizzard_EditMode/Shared/:
| File | Purpose |
|---|---|
EditModeManager.lua | Frame registration, layout management |
EditModeSystemTemplates.lua | All system mixins |
EditModeDialogs.lua | Settings dialog |
EditModeSettingDisplayInfo.lua | System-to-setting mapping |
EditModeUtil.lua | Magnetism/snapping logic |
Settings Panel API¶
The modern Settings Panel (added in 10.0, breaking changes in 11.0.2) replaces the old InterfaceOptions_AddCategory system. Your addon's settings appear in the game's Settings > AddOns tab.
Category Registration¶
-- Vertical layout: auto-stacking controls (recommended for most addons)
local category = Settings.RegisterVerticalLayoutCategory("My AddOn")
-- Subcategory under a parent
local sub = Settings.RegisterVerticalLayoutSubcategory(category, "Advanced")
-- Canvas layout: fully custom frame
local frame = CreateFrame("Frame")
local category = Settings.RegisterCanvasLayoutCategory(frame, "My AddOn")
-- Register in the AddOns tab
Settings.RegisterAddOnCategory(category)
Setting Registration (11.0.2+ Signatures)¶
11.0.2 broke the old signatures
Patch 11.0.2 added variableKey and variableTbl parameters to RegisterAddOnSetting. Code written for 10.0–11.0.1 will error. The signatures below are current through 12.0.1.
| Function | Purpose |
|---|---|
Settings.RegisterAddOnSetting(category, variable, variableKey, variableTbl, variableType, name, defaultValue) | Direct table read/write |
Settings.RegisterProxySetting(category, variable, variableType, name, defaultValue, getValue, setValue) | Custom getter/setter |
Settings.RegisterCVarSetting(category, variable, variableType, name) | Bind to a CVar |
Control Creation¶
| Function | Purpose |
|---|---|
Settings.CreateCheckbox(category, setting, tooltip) | Boolean toggle |
Settings.CreateSlider(category, setting, options, tooltip) | Numeric slider |
Settings.CreateSliderOptions(min, max, step) | Slider range config |
Settings.CreateDropdown(category, setting, getOptions, tooltip) | Dropdown selector |
Settings.CreateControlTextContainer() | Build dropdown options |
Complete Working Example¶
-- ## SavedVariables: MyAddOn_DB
MyAddOn_DB = {} -- populated by WoW from SavedVariables
local category = Settings.RegisterVerticalLayoutCategory("My AddOn")
-- Checkbox
do
local setting = Settings.RegisterAddOnSetting(category,
"MyAddOn_Toggle", -- unique variable name
"toggle", -- key in SavedVariables table
MyAddOn_DB, -- the SavedVariables table
type(false), -- variableType ("boolean")
"Enable Feature", -- display name
false -- default value
)
Settings.CreateCheckbox(category, setting, "Enables the main feature.")
end
-- Slider
do
local setting = Settings.RegisterAddOnSetting(category,
"MyAddOn_Scale", "scale", MyAddOn_DB,
type(1), "UI Scale", 100
)
local options = Settings.CreateSliderOptions(50, 200, 5)
options:SetLabelFormatter(MinimalSliderWithSteppersMixin.Label.Right)
Settings.CreateSlider(category, setting, options, "Adjust scale percentage.")
end
-- Dropdown
do
local function GetOptions()
local container = Settings.CreateControlTextContainer()
container:Add(1, "Small")
container:Add(2, "Medium")
container:Add(3, "Large")
return container:GetData()
end
local setting = Settings.RegisterAddOnSetting(category,
"MyAddOn_Size", "size", MyAddOn_DB,
type(1), "Frame Size", 2
)
Settings.CreateDropdown(category, setting, GetOptions, "Select frame size.")
end
Settings.RegisterAddOnCategory(category)
Reading and Writing Settings¶
-- Read
local value = Settings.GetValue("MyAddOn_Toggle")
-- Write
Settings.SetValue("MyAddOn_Toggle", true)
-- Get the setting object for callbacks
local setting = Settings.GetSetting("MyAddOn_Toggle")
setting:SetValueChangedCallback(function(setting, value)
print("Toggle changed to:", value)
end)
-- Open the settings panel to your category
Settings.OpenToCategory(category:GetID())
Canvas Layout (Custom Frame)¶
For complex settings UIs that don't fit the vertical layout:
local frame = CreateFrame("Frame")
frame:SetSize(600, 400)
-- These three functions are REQUIRED for Canvas layouts
function frame.OnCommit()
-- Called when the player clicks "Okay" or "Apply"
-- Save your settings here
end
function frame.OnDefault()
-- Called when the player clicks "Defaults"
-- Reset your settings here
end
function frame.OnRefresh()
-- Called when the panel is shown
-- Update your controls to match current settings
end
local category = Settings.RegisterCanvasLayoutCategory(frame, "My AddOn")
Settings.RegisterAddOnCategory(category)
Migration from Pre-10.0¶
| Old System | New System |
|---|---|
InterfaceOptions_AddCategory(panel) | Settings.RegisterAddOnCategory(category) |
InterfaceOptionsFrame_OpenToCategory(name) | Settings.OpenToCategory(category:GetID()) |
InterfaceOptionsCheckButtonTemplate | Settings.CreateCheckbox() |
OptionsSliderTemplate | Settings.CreateSlider() |
UIDropDownMenu_Initialize | Settings.CreateDropdown() |
Addon Compartment¶
The Addon Compartment (added in 10.0, TOC registration in 10.1) is a unified dropdown on the minimap that replaces the old pattern of each addon creating its own minimap button. It's the small addon icon button next to the minimap.
TOC Registration (Recommended)¶
The simplest approach — no Lua code required for basic presence:
## AddonCompartmentFunc: MyAddon_OnCompartmentClick
## AddonCompartmentFuncOnEnter: MyAddon_OnCompartmentEnter
## AddonCompartmentFuncOnLeave: MyAddon_OnCompartmentLeave
## IconTexture: Interface\Icons\INV_Misc_QuestionMark
Only AddonCompartmentFunc is required. The callback functions must be global.
Callback Signatures¶
-- Click handler (TOC-registered)
function MyAddon_OnCompartmentClick(addonName, buttonName)
if buttonName == "LeftButton" then
Settings.OpenToCategory(MyAddon.categoryID)
elseif buttonName == "RightButton" then
MyAddon:ToggleMainWindow()
end
end
-- Tooltip on hover
function MyAddon_OnCompartmentEnter(addonName, menuButtonFrame)
GameTooltip:SetOwner(menuButtonFrame, "ANCHOR_LEFT")
GameTooltip:SetText("My Addon")
GameTooltip:AddLine("Left-click: Settings", 1, 1, 1)
GameTooltip:AddLine("Right-click: Toggle window", 1, 1, 1)
GameTooltip:Show()
end
function MyAddon_OnCompartmentLeave(addonName, menuButtonFrame)
GameTooltip:Hide()
end
Programmatic Registration¶
For addons that need to register at runtime:
AddonCompartmentFrame:RegisterAddon({
text = "My Addon",
icon = "Interface\\Icons\\INV_Misc_QuestionMark",
func = function(button, menuInputData, menu)
local mouseButton = menuInputData.buttonName
-- Handle click
end,
funcOnEnter = function(button, menuInputData, menu)
GameTooltip:SetOwner(button, "ANCHOR_LEFT")
GameTooltip:SetText("My Addon")
GameTooltip:Show()
end,
funcOnLeave = function(button, menuInputData, menu)
GameTooltip:Hide()
end,
notCheckable = true,
})
Compartment pitfalls
IconTexturetakes priority overIconAtlasif both are set- There is no
UnregisterAddonAPI — once registered, it's permanent until reload - The dropdown does not live-update while open
- In 11.0.0, TOC-based registrations lost the
menuButtonFrameparameter in click handlers; programmatic registrations receive a table as the second argument instead - No compartment-specific changes in 12.0
ScrollBox & DataProvider¶
The ScrollBox/DataProvider system (added in 10.0) replaced FauxScrollFrame and HybridScrollFrame. It's data-driven and pool-based — you provide data, it handles virtualization and recycling.
Architecture¶
DataProvider (data store)
│
▼
ScrollView (layout + element factory)
│
▼
ScrollBox (viewport frame) ◄──► ScrollBar (scroll control)
| Component | How to Create |
|---|---|
| ScrollBox | CreateFrame("Frame", nil, parent, "WowScrollBoxList") |
| ScrollBar | CreateFrame("EventFrame", nil, parent, "MinimalScrollBar") |
| ScrollView | CreateScrollBoxListLinearView() |
| DataProvider | CreateDataProvider() |
Complete Setup¶
-- 1. Create the visual components
local scrollBox = CreateFrame("Frame", nil, parentFrame, "WowScrollBoxList")
scrollBox:SetPoint("TOPLEFT", 4, -4)
scrollBox:SetPoint("BOTTOMRIGHT", -24, 4)
local scrollBar = CreateFrame("EventFrame", nil, parentFrame, "MinimalScrollBar")
scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 4, 0)
scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 4, 0)
-- 2. Configure the ScrollView with fixed row height
local scrollView = CreateScrollBoxListLinearView()
scrollView:SetElementExtent(24)
-- 3. Define how each row renders
scrollView:SetElementInitializer("UIPanelButtonTemplate", function(button, data)
button:SetText(data.name)
button:SetScript("OnClick", function()
print("Clicked:", data.name)
end)
end)
-- 4. Wire everything together
ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, scrollBar, scrollView)
-- 5. Populate with data
local dataProvider = CreateDataProvider()
for i = 1, 100 do
dataProvider:Insert({ name = "Item " .. i })
end
scrollBox:SetDataProvider(dataProvider)
Element Initializer and Resetter¶
The initializer runs when a row becomes visible. The resetter runs when it scrolls out of view — use it to clean up scripts and references.
scrollView:SetElementInitializer("MyItemTemplate", function(frame, data)
frame.Name:SetText(data.name)
frame.Icon:SetTexture(data.icon)
frame:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(data.tooltip)
GameTooltip:Show()
end)
frame:SetScript("OnLeave", GameTooltip_Hide)
end)
scrollView:SetElementResetter(function(frame, data)
frame:SetScript("OnEnter", nil)
frame:SetScript("OnLeave", nil)
end)
Variable-Height Rows¶
scrollView:SetElementExtentCalculator(function(dataIndex, data)
if data.isHeader then return 32 end
return 20
end)
Multiple Templates (Element Factory)¶
scrollView:SetElementFactory(function(factory, node)
local data = node:GetData()
factory(data.Template, function(frame, node)
frame:Init(node)
end)
end)
DataProvider Operations¶
dataProvider:Insert({ name = "New Item" })
dataProvider:InsertTable({ {name = "A"}, {name = "B"} })
dataProvider:RemoveIndex(3)
dataProvider:RemoveByPredicate(function(d) return d.name == "B" end)
dataProvider:Flush() -- clear all data
dataProvider:SetSortComparator(function(a, b) return a.name < b.name end)
dataProvider:Sort()
dataProvider:GetSize()
dataProvider:ForEach(function(data) print(data.name) end)
dataProvider:FindByPredicate(function(d) return d.name == "A" end)
Retaining Scroll Position¶
-- Keeps the scroll position when replacing data
scrollBox:SetDataProvider(dataProvider, ScrollBoxConstants.RetainScrollPosition)
Selection Behavior¶
local selectionBehavior = ScrollUtil.AddSelectionBehavior(scrollBox)
selectionBehavior:RegisterCallback(
SelectionBehaviorMixin.Event.OnSelectionChanged,
function(self, elementData, selected)
local button = scrollBox:FindFrame(elementData)
if button then
if selected then
button:SetText("[" .. elementData.name .. "]")
else
button:SetText(elementData.name)
end
end
end
)
Search / Filter Pattern¶
local AllData = {}
for i = 1, 100 do
tinsert(AllData, { name = "Item " .. i })
end
local dataProvider = CreateDataProvider(AllData)
scrollBox:SetDataProvider(dataProvider)
searchBox:HookScript("OnTextChanged", function(editBox)
local query = editBox:GetText():lower()
local filtered = {}
for _, entry in ipairs(AllData) do
if entry.name:lower():find(query, 1, true) then
tinsert(filtered, entry)
end
end
scrollBox:SetDataProvider(CreateDataProvider(filtered))
end)
Tree DataProvider (Hierarchical Lists)¶
local treeView = CreateScrollBoxListTreeListView()
local treeDataProvider = CreateTreeDataProvider()
treeView:SetElementInitializer("UIPanelButtonTemplate", function(button, node)
local data = node:GetData()
local depth = node:GetDepth()
button:SetText(string.rep(" ", depth) .. data.name)
button:SetScript("OnClick", function() node:ToggleCollapsed() end)
end)
ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, scrollBar, treeView)
scrollBox:SetDataProvider(treeDataProvider)
local parent = treeDataProvider:Insert({ name = "Category A" })
parent:Insert({ name = "Sub-item 1" })
parent:Insert({ name = "Sub-item 2" })
Hooking Existing Blizzard ScrollBoxes¶
-- React when Blizzard populates a ScrollBox
hooksecurefunc(someBlizzardScrollBox, "SetDataProvider", function(self, dp)
-- Hook the DataProvider to track insertions
hooksecurefunc(dp, "Insert", function(dataProvider, data)
-- React to new data entries
end)
end)
Managed ScrollBar visibility
Hide the scrollbar when content fits without scrolling:
ScrollUtil.AddManagedScrollBarVisibilityBehavior(
scrollBox, scrollBar,
-- Anchors when scrollbar is visible:
{ CreateAnchor("TOPLEFT", 4, -4), CreateAnchor("BOTTOMRIGHT", scrollBar, -13, 4) },
-- Anchors when scrollbar is hidden:
{ CreateAnchor("TOPLEFT", 4, -4), CreateAnchor("BOTTOMRIGHT", -4, 4) }
)
Action Bar System¶
Midnight's biggest namespace consolidation. All action bar functions moved into C_ActionBar — over 45 functions replacing scattered globals.
Frame Names and Slot Numbers¶
| Bar | Frame Name | Action Slots |
|---|---|---|
| Action Bar 1 (page 1) | ActionButton[1-12] | 1–12 |
| Action Bar 1 (page 2) | ActionButton[1-12] | 13–24 |
| Action Bar 4 | MultiBarRightButton[1-12] | 25–36 |
| Action Bar 5 | MultiBarLeftButton[1-12] | 37–48 |
| Action Bar 3 | MultiBarBottomRightButton[1-12] | 49–60 |
| Action Bar 2 | MultiBarBottomLeftButton[1-12] | 61–72 |
| Bars 6–8 (added 10.0) | — | 145–180 |
Slots 73–120 are class stance bars. Slots 121–132 are the possession bar.
C_ActionBar Namespace (12.0)¶
Legacy globals like ActionHasRange(), GetActionAutocast(), and IsItemAction() were removed. Use the namespace equivalents:
| Function | Purpose |
|---|---|
C_ActionBar.GetActionCooldown() | Cooldown info (Secret Values in combat) |
C_ActionBar.GetActionCharges() | Charge mechanics |
C_ActionBar.GetActionDisplayCount() | Visual counts |
C_ActionBar.IsActionInRange() | Range validation |
C_ActionBar.GetActionBarPage() | Current bar page |
C_ActionBar.SetActionBarPage() | Switch bar page |
C_ActionBar.GetActionTexture() | Action icon |
C_ActionBar.GetActionText() | Action text |
C_ActionBar.IsUsableAction() | Usability check |
C_ActionBar.IsAttackAction() | Auto-attack check |
C_ActionBar.IsAutoRepeatAction() | Auto-repeat check |
C_ActionBar.RegisterActionUIButton() | Register button (replaces SetActionUIButton()) |
C_ActionBar.HasBonusActionBar() | Bonus bar check |
C_ActionBar.HasVehicleActionBar() | Vehicle bar check |
ActionButton Cooldown Split (12.0)¶
The single cooldown frame on ActionButtonTemplate was split into three:
| Child Frame | Purpose |
|---|---|
cooldown | Standard ability cooldown |
chargeCooldown | Charge-based cooldown |
lossOfControlCooldown | Loss of control overlay |
-- Old (pre-12.0): one cooldown frame
local cd = actionButton.cooldown
-- New (12.0): three cooldown frames
local cd = actionButton.cooldown
local charge = actionButton.chargeCooldown
local loc = actionButton.lossOfControlCooldown
The global function ActionButton_ApplyCooldown() handles all three using secure delegates for Secret Values.
SecureActionButtonTemplate¶
Still functional in 12.0 with additions. Supports 20+ action types: spell, item, macro, action, pet, target, focus, assist, cancelaura, and more.
12.0 additions:
SetRaidTarget()now usable via the template- New secure action delegates for housing APIs (
VisitHouse,TeleportHome,ReturnAfterVisitingHouse) - New
raidTargetdelegate for setting/clearing/toggling raid targets
Migrating action bar addons
The migration is mostly mechanical: replace each removed global with its C_ActionBar equivalent. The Patch 12.0.0 API changes page on Warcraft Wiki has a complete mapping. Most action bar addons (Bartender4, Dominos, etc.) needed minimal changes.
Secret Values System¶
The defining change of Midnight. Secret Values are opaque containers that addon code cannot inspect, compare, or do arithmetic on. They exist to prevent automation addons from reacting to combat data faster than humans can.
When Secrets Apply¶
Secret Values are enforced during:
- Mythic keystone runs
- PvP matches
- Instance encounters (boss fights)
- Player combat (general)
Outside these contexts, values are accessible normally.
What Returns Secrets¶
| Data | Status |
|---|---|
Spell cooldown info (C_ActionBar.GetActionCooldown(), C_Spell.GetSpellCooldown()) | Secret in restricted contexts |
| Enemy unit names, GUIDs, and IDs | Secret in instances (regardless of combat) |
| Aura data contents | Secret (but AuraInstanceIDs themselves are NOT secret) |
| Combat log events | Removed entirely from addon access |
What Does NOT Return Secrets¶
| Data | Reason |
|---|---|
| Player secondary resources (Combo Points, Runes, Soul Shards, Holy Power, Chi, Arcane Charges, Essence) | Explicitly exempted |
| Profession and Skyriding spells | Flagged "never secret" |
UnitIsUnit() comparisons with target/focus/mouseover/softenemy/softinteract/softfriend | Allowed |
| Specific auras: Maelstrom Weapon, Ebon Might | Explicitly exempted |
Detection APIs¶
-- Check if a specific value is secret
if issecretvalue(someValue) then
-- Can't read this right now — pass it to a widget or bail
return
end
-- Check if restrictions are currently active
if C_Secrets.HasSecretRestrictions() then
-- We're in restricted content
end
-- Specific restriction checks
C_Secrets.ShouldSpellCooldownBeSecret() -- cooldown restriction
C_Secrets.ShouldAurasBeSecret() -- aura restriction
C_Secrets.ShouldUnitAuraInstanceBeSecret() -- unit aura restriction
Testing Secret Values¶
Use these CVars to force Secret Values outside restricted content (useful for development):
/console secretAurasForced 1
/console secretCooldownsForced 1
/console secretUnitIdentityForced 1
/console secretSpellcastsForced 1
Set to 0 to disable.
Widget Support¶
Widgets can detect and handle Secret Values:
-- Check if a widget contains secret values
frame:HasSecretValues()
frame:HasSecretAspect()
-- Prevent a widget from accepting secret values (will error instead)
frame:SetPreventSecretValues()
String operations (string.format(), .., string.join()) all work with Secrets — they produce secret output. You can concatenate secret values into strings for display, but you can't read the result back in Lua.
The Pattern: Pass Through, Don't Inspect¶
-- WRONG: trying to read and branch on secret values
local info = C_Spell.GetSpellCooldown(spellID)
if info.duration > 0 then -- ERROR: can't compare secret value
cooldown:SetCooldown(info.startTime, info.duration)
end
-- RIGHT: use Duration Objects that pass through to widgets
local duration = C_Spell.GetSpellCooldownDuration(spellID)
if duration then
cooldown:SetCooldownFromDurationObject(duration)
end
Boolean-Driven Visuals (12.0)¶
New widget methods for showing/hiding based on Secret Values without branching:
-- Instead of: if someSecretBool then region:SetAlpha(1) else region:SetAlpha(0) end
region:SetAlphaFromBoolean(someSecretBool)
-- Instead of: if someSecretBool then region:SetVertexColor(1,0,0) else ... end
region:SetVertexColorFromBoolean(someSecretColor)
Combat Log is gone
COMBAT_LOG_EVENT_UNFILTERED is no longer available to addons in 12.0. The new C_CombatLog namespace provides IsCombatLogRestricted() and filter management, but raw combat log parsing is over. Addons like Details! and Recount must use C_DamageMeter for session data or the new C_EncounterTimeline for mechanic tracking.
Finding Blizzard Source Code¶
You can't effectively hook Blizzard systems without reading their source code. Here's how to find it.
Exporting Interface Files¶
At the login/character select screen, open the console and run:
This creates BlizzardInterfaceCode/ and BlizzardInterfaceArt/ in your WoW install directory.
BlizzardInterfaceCode/
Interface/
SharedXML/ -- Shared utilities (math, tables, colors, events)
FrameXML/ -- Core UI framework (UIParent, templates)
AddOns/
Blizzard_AchievementUI/
Blizzard_ActionBar/
Blizzard_AuctionHouseUI/
Blizzard_APIDocumentationGenerated/
... (dozens of Blizzard_ addons)
GitHub Mirrors¶
| Repository | Description |
|---|---|
| Gethe/wow-ui-source | Canonical community mirror (live, ptr, beta branches) |
| Gethe/wow-ui-textures | UI texture assets |
| Ketho/wow-ui-source-midnight | Midnight Beta FrameXML |
| Ketho/BlizzardInterfaceResources | API dumps, enums, templates, mixins, atlas info |
Online Browsers¶
| Tool | URL | Description |
|---|---|---|
| Townlong Yak | townlong-yak.com/framexml/ | Browse and diff FrameXML across builds |
| Wago.tools | wago.tools | Data mining, DB2 tables, file browser |
Documentation¶
| Resource | URL |
|---|---|
| World of Warcraft API | warcraft.wiki.gg/wiki/World_of_Warcraft_API |
| Widget API | warcraft.wiki.gg/wiki/Widget_API |
| Events | warcraft.wiki.gg/wiki/Events |
| FrameXML Functions | warcraft.wiki.gg/wiki/FrameXML_functions |
| ScrollBox Guide | warcraft.wiki.gg/wiki/Making_scrollable_frames |
In-Game Debugging¶
| Command | Purpose |
|---|---|
/dump <expr> | Print any Lua expression (tables, frames, API return values) |
/eventtrace | Real-time event viewer with filtering |
/framestack | Show all frames under mouse cursor with hierarchy |
Key Files to Know¶
| File | Purpose |
|---|---|
FrameXML/FrameXML.toc | Master load order |
FrameXML/UIParent.lua | Root UIParent frame, core events |
SharedXML/Mixin.lua | Mixin(), CreateFromMixins() |
SharedXML/TableUtil.lua | Table utilities |
SharedXML/EventRegistry.lua | Callback-based event system |
SharedXML/PixelUtil.lua | Pixel-perfect positioning |
AddOns/Blizzard_APIDocumentationGenerated/ | Generated API docs with signatures |
How to Trace a Function¶
- Use
/framestackto identify the frame under your cursor - Search the wow-ui-source repo for that frame name — find its XML definition and Lua scripts
- Follow the
inheritschain in XML for template hierarchy - Follow the
Mixinchain in Lua —CreateFromMixins(SomeMixin) - Check
RegisterEvent/EventRegistry:RegisterCallbackfor events the frame listens to - Use
/dump SomeFrame:GetPoint(1)to inspect live positioning
IDE Support¶
Ketho's WoW API VS Code extension provides IntelliSense via LuaLS. Auto-activates when it detects a .toc file in your project.
Essential Development Addons¶
| Addon | Purpose |
|---|---|
| BugSack + BugGrabber | Captures Lua errors with full stack traces |
| TextureAtlasViewer | Browse atlas names and coordinates |
| TextureViewer | Display any texture by file path |
Interface Version Numbers¶
Your TOC file's ## Interface: line determines compatibility. Addons without 120000 or higher will not load without a player override.
| Expansion | Patch | Interface |
|---|---|---|
| Dragonflight | 10.0.0 | 100000 |
| The War Within | 11.0.0 | 110000 |
| Midnight | 12.0.0 | 120000 |
| Midnight | 12.0.1 | 120001 |