The .toc file
The folder name and the .toc name must match exactly:
Interface/AddOns/MyAddon/MyAddon.toc. Everything before the blank line is metadata;
everything after is the file list, loaded in the order written. That order is the
only dependency mechanism inside an addon — a file can use what earlier files defined, nothing more.
## Interface: 16001
## Title: MyAddon
## Notes: Short description shown in the AddOns list
## Author: Name
## Version: 1.0.0
## SavedVariables: MyAddonDB
## IconTexture: Interface\Icons\INV_Misc_QuestionMark
## Category: Other
Locales.lua
Widgets.lua
Core.lua
| Directive | What it does |
|---|---|
## Interface: | Client build this addon targets. Wrong number = flagged out of date. Get the real one in game with /dump select(4, GetBuildInfo()) — never hard-code a number you have not checked. |
## Title: | Name in the AddOns list. Colour escapes work here. |
## Notes: | One-line description. Localise with ## Notes-deDE: and friends — there is no continuation directive, so keep it to one line. |
## Version: | Your version string. Read it back with C_AddOns.GetAddOnMetadata. |
## SavedVariables: | Global table names persisted per account. Space-separated for several. |
## SavedVariablesPerCharacter: | Same, but stored under each character. |
## Dependencies: | Hard requirement. Your addon will not load without it. |
## OptionalDeps: | Load these first if present. This is how you use a library without requiring it. |
## LoadOnDemand: 1 | Do not load at startup; wait for C_AddOns.LoadAddOn. |
## DefaultState: disabled | Ships switched off. |
## IconTexture: | Icon in the AddOns list. |
## Category: | Grouping in the AddOns list. |
Interface version. 16001 is major·10000 + minor·100 + patch,
so patch 1.60.1. It changes every patch. If the client refuses to load the addon, this is the first
thing to check.
Shape of the API
6,577 functions is not something you learn. It is something you look things up in. What helps
is knowing which corner of the API a problem lives in, because the answer is nearly always inside
one C_Something namespace, and the namespaces are grouped by what they do.
Three practical consequences. UI is the biggest area by far — a quarter of the whole API is the widget system you draw with. Chat and comms punch above their weight in events: 235 events against 345 functions, because that corner is something that happens to you. And the long tail is genuinely long; if a feature has a Blizzard panel, it has a namespace.
Lifecycle
Four moments matter. Get these right and nothing else about startup will surprise you.
PLAYER_ENTERING_WORLD is
isInitialLogin, isReloadingUi — use it to tell a real login from a UI reload.1 · Skeleton
This is the shape of every addon. One frame, one handler table, one loop to register. Adding an
event later is one function, not one more if branch.
local addonName, ns = ... -- ns is shared between this addon's files
local frame = CreateFrame("Frame")
local handlers = {}
function handlers.ADDON_LOADED(name)
if name ~= addonName then return end
MyAddonDB = MyAddonDB or {} -- SavedVariables exist from here on
frame:UnregisterEvent("ADDON_LOADED")
end
function handlers.PLAYER_ENTERING_WORLD(isInitialLogin, isReloadingUi)
end
frame:SetScript("OnEvent", function(_, event, ...) handlers[event](...) end)
for event in pairs(handlers) do frame:RegisterEvent(event) end
One trap. That last loop registers exactly the events you wrote handlers
for. If an event fires with no handler, handlers[event] is nil and you get
a call-a-nil error — so never RegisterEvent outside this table.
Slash commands
SLASH_MYADDON1 = "/myaddon"
SLASH_MYADDON2 = "/mya"
SlashCmdList.MYADDON = function(msg)
local cmd, rest = msg:lower():match("^%s*(%S*)%s*(.-)%s*$")
print(cmd, rest)
end
Timers
C_Timer.After(5, function() print("once, five seconds later") end)
local ticker = C_Timer.NewTicker(1, function() --[[ every second ]] end)
The reference documents C_Timer.NewTicker as returning a TickerCallback
with no documented methods. If you need to stop a ticker, prefer a flag you check inside the
callback over a method the reference does not describe.
2 · Widget tree
Widgets inherit their methods in layers. Knowing which layer a method lives on tells you which
reference file to open — and that is most of the battle, because SetPoint is not in
the Frame file at all.
Simple*API.md file. A
CheckButton has 6 of its own and every one of Button's 42 besides.3 · A panel
Everything below is drawn with CreateTexture and SetColorTexture
instead of Blizzard art. That means no texture paths to guess and no templates to inherit — it works
on any client, and you can recolour it by changing four numbers.
local PAD = 12
-- Draw order is BACKGROUND < BORDER < ARTWORK < OVERLAY, so a "border" is
-- just a slightly bigger rectangle one layer down.
local function paintBox(frame, inset, edge, fill)
local e = frame:CreateTexture(nil, "BACKGROUND")
e:SetPoint("TOPLEFT", -inset, inset)
e:SetPoint("BOTTOMRIGHT", inset, -inset)
e:SetColorTexture(unpack(edge))
local f = frame:CreateTexture(nil, "BORDER")
f:SetAllPoints()
f:SetColorTexture(unpack(fill))
end
function Widgets.Panel(name, title, width, height)
local f = CreateFrame("Frame", name, UIParent)
f:SetSize(width, height)
f:SetPoint("CENTER")
f:SetFrameStrata("DIALOG")
f:SetMovable(true)
f:EnableMouse(true)
f:SetClampedToScreen(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", f.StartMoving)
f:SetScript("OnDragStop", f.StopMovingOrSizing)
paintBox(f, 1, {0.35,0.35,0.45,1}, {0.05,0.05,0.07,0.95})
f.title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
f.title:SetPoint("TOPLEFT", PAD, -PAD)
f.title:SetText(title)
return f
end
Stacking rows
Hand-anchoring every control to the one above it is where panels rot. One tiny layout object fixes it, and it is the piece you will reuse most.
function Widgets.Layout(panel, startY)
return {
y = startY or -(PAD + 22),
Add = function(self, widget, height)
widget:SetPoint("TOPLEFT", panel, "TOPLEFT", PAD, self.y)
self.y = self.y - (height or widget:GetHeight()) - PAD
return widget
end,
}
end
4 · Controls
Checkbox
A CheckButton keeps its own state. SetCheckedTexture takes a
TextureAsset, and a texture object counts — so you never need an art path.
function Widgets.CheckBox(parent, labelText, onChange)
local box = CreateFrame("CheckButton", nil, parent)
box:SetSize(20, 20)
paintBox(box, 1, {0.35,0.35,0.45,1}, {0.10,0.10,0.13,1})
local tick = box:CreateTexture(nil, "ARTWORK")
tick:SetPoint("TOPLEFT", 4, -4)
tick:SetPoint("BOTTOMRIGHT", -4, 4)
tick:SetColorTexture(0.35, 0.75, 1, 1)
box:SetCheckedTexture(tick)
box:SetScript("OnClick", function(self)
onChange(self:GetChecked() and true or false)
end)
return box
end
Slider
A Slider needs three things or it will not behave: an orientation, a min/max, and a thumb
texture. SetObeyStepOnDrag(true) is what makes dragging snap to the step.
local slider = CreateFrame("Slider", nil, holder)
slider:SetOrientation("HORIZONTAL")
slider:SetMinMaxValues(0, 100)
slider:SetValueStep(5)
slider:SetObeyStepOnDrag(true)
local thumb = slider:CreateTexture(nil, "ARTWORK")
thumb:SetSize(10, 16)
thumb:SetColorTexture(0.35, 0.75, 1, 1)
slider:SetThumbTexture(thumb)
slider:SetScript("OnValueChanged", function(self, value)
readout:SetText(("%d"):format(value))
onChange(value)
end)
Text field
EditBox is the largest widget in the API — 79 methods. Four of them matter on day
one, and the two escape scripts are not optional: without them the box traps your keyboard.
local box = CreateFrame("EditBox", nil, holder)
box:SetAutoFocus(false) -- or it steals focus the moment it appears
box:SetFontObject("GameFontHighlight")
box:SetTextInsets(6, 6, 0, 0)
box:SetMaxLetters(60)
box:SetScript("OnEnterPressed", function(self)
self:ClearFocus()
onCommit(self:GetText())
end)
box:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
Scrolling list
A ScrollFrame is a window onto a taller child frame. You move the window, not the
content. Reuse the FontStrings instead of creating new ones — that is the difference between a list
that scrolls and one that stutters.
local scroll = CreateFrame("ScrollFrame", nil, parent)
local content = CreateFrame("Frame", nil, scroll)
content:SetSize(width, height)
scroll:SetScrollChild(content)
scroll:EnableMouseWheel(true)
scroll:SetScript("OnMouseWheel", function(self, delta)
local range = self:GetVerticalScrollRange()
local target = self:GetVerticalScroll() - delta * 16
if target < 0 then target = 0 elseif target > range then target = range end
self:SetVerticalScroll(target)
end)
5 · Saving what the user did
There is no save function. You declare a global in the .toc, the client hands it
back at ADDON_LOADED, and writes whatever is in it at logout. That is the entire
mechanism.
-- MyAddon.toc
## SavedVariables: MyAddonDB
New settings appear as your addon grows, so fill in what is missing rather than overwriting what the user chose. This recursive default-filler is the one you will paste into every addon you write:
local DEFAULTS = {
enabled = true,
threshold = 40,
label = "my first addon",
history = {},
}
local function applyDefaults(target, defaults)
for key, value in pairs(defaults) do
if type(value) == "table" then
target[key] = target[key] or {}
applyDefaults(target[key], value)
elseif target[key] == nil then
target[key] = value
end
end
end
Two traps, both of which will bite you.
pairs() skips nil values, so a default written as
foo = nil does nothing at all — it is a comment with extra steps.
And migrations must run before applyDefaults, normalisation
after. Read a setting before its default exists and you get
attempt to compare nil with number.
Wiring a control to the database is then two lines — read on build, write on change:
f.check = layout:Add(Widgets.CheckBox(f, "Enabled", function(checked)
db.enabled = checked -- write
end), 20)
f.check:SetChecked(db.enabled) -- read
Libraries
Not in the reference. Everything else on this page is checked against Blizzard's generated documentation. Libraries are community code and appear in none of it — treat the names below as a map of the territory and check each project's own docs for current versions.
A library is just Lua files you ship inside your own folder. What makes them libraries is LibStub, a tiny version registry: each library registers under a name and a revision, and the highest revision loaded wins. That is why five addons can ship five copies of the same library without fighting.
local AceGUI = LibStub("AceGUI-3.0")
local LSM = LibStub("LibSharedMedia-3.0", true) -- true = do not error if absent
| Library | What you reach for it for |
|---|---|
LibStub | The registry everything else is built on. Ship it first. |
AceAddon-3.0 | Addon object with OnInitialize / OnEnable, plus modules. |
AceDB-3.0 | SavedVariables with profiles, defaults and per-character scopes. Replaces the hand-rolled defaults above once an addon has real settings. |
AceConfig-3.0 | Describe options as a table; get an options panel and slash commands out. |
AceEvent-3.0 | Event registration without owning a frame. |
AceGUI-3.0 | Widget toolkit, if you would rather not write section 4 yourself. |
LibSharedMedia-3.0 | Shared pool of fonts, textures and sounds across addons. |
LibDataBroker-1.1 | Publish a data feed that any display addon can show. |
LibDBIcon-1.0 | Puts a LibDataBroker feed on the minimap. |
Referencing them in the .toc
Two ways, and the difference matters.
Embedded — the common case
The library lives in your folder and loads before your code. Nothing external is required, so your addon cannot be broken by a user deleting something else.
## Interface: 16001
## Title: MyAddon
## SavedVariables: MyAddonDB
Libs\LibStub\LibStub.lua
Libs\AceAddon-3.0\AceAddon-3.0.xml
Core.lua
Ace packages ship an .xml that pulls in their own files, which is why you see
.xml entries in a .toc that is otherwise all Lua. Many projects collect
these into a single embeds.xml and list that one line instead.
External — when the library is its own addon
## Dependencies: Ace3
## OptionalDeps: LibSharedMedia-3.0
Dependencies means your addon will not load at all without it.
OptionalDeps only guarantees load order — the other addon loads first if it is
installed, and your code must cope when it is not. That is what the second argument to
LibStub is for.
Secret values
This is the part of the API that is specific to this era, and the part that breaks addons ported
from older clients. Some functions return secret values: you can hold one and pass
it on, but you cannot look at it. Comparisons, arithmetic, string operations,
tostring and using one as a table key all fail or throw.
| Flag on a function | What it means for you |
|---|---|
| SecretValue | This return may be secret. Guard it with issecretvalue(v) before you touch it. |
| SecretReturns | All returns may be secret. |
| SecretWhenInCombat | Readable out of combat, secret in it. Same pattern for PvP, restricted maps, auras, cooldowns. |
| IsProtectedFunction | Cannot be called from addon code while tainted or in combat. |
| HasRestrictions | There are conditions. The call can succeed and still do nothing. |
| SecretArguments=AllowedWhenTainted | You may pass a secret straight into this one — this is how you display a value you cannot read. |
| Environment: SecureOnly | Not available to addons at all. |
if issecretvalue(hp) then
bar:SetValue(hp) -- widgets accept secrets
else
text:SetText(("%d%%"):format(hp / maxHp * 100))
end
The design rule. Display secrets, do not compute on them. Status bars, cooldown frames and the text-binding APIs all take secret input directly. Building addon logic — decisions, rotation hints, alerts — on a value the client has made secret is exactly what the restrictions exist to prevent, so it will not work no matter how you approach it.
Looking things up
The habit that matters more than anything on this page: never guess an API. Every
C_ function, widget method and event is written down. The quickest way in is the
API reference: fuzzy search across every system, function, widget
method, event, enum and structure, right in the browser. Four more ways in:
| You have | Do this |
|---|---|
| A function name | grep -rn "### FunctionName" references/api/ |
| An event name | grep "^EVENT_NAME" references/EVENTS.tsv → open the file it names |
| A namespace | Look it up in references/INDEX.md |
| A widget method | It is in one of the 40 Simple*API.md files |
In the game
/api | The in-game browser, same source data as the files |
/dump expr | Inspect any value |
/etrace | Watch events fire live — the fastest way to find the right event |
/fstack | Name the frames under your cursor |
/console scriptErrors 1 | Show Lua errors. Off by default, which turns a crash into "nothing happened" |
When the reference is not enough. Blizzard's own UI code — FrameXML,
mixins, and templates like UIPanelButtonTemplate and
ScrollBoxListLinearView — is not generated documentation and is not in the reference.
Clone it and grep:
git clone --depth 1 -b forever --filter=blob:none \
https://github.com/Gethe/wow-ui-source