WoW Forever · interface 16001 · build 1.60.1.69913

Writing addons for WoW Forever

A working handbook: what goes in a .toc, how the API is shaped, how to build a panel out of the widgets everyone actually uses, and how to keep what the user typed. Every count and signature on this page comes from Blizzard's own generated documentation, not from memory.

611systems
6,577functions
1,802events
282C_ namespaces
40widget types

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
DirectiveWhat 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: 1Do not load at startup; wait for C_AddOns.LoadAddOn.
## DefaultState: disabledShips 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.

Functions per area of the WoW Forever API UI & widgets Everything else Player & units Items & bags System & addons Chat & comms Economy & professions Guild & groups Instances & PvP Quests & progression Spells & auras Map & world 1689 fn · 53 ev · Simple*API 1352 fn · 411 ev 878 fn · 290 ev · C_Traits 523 fn · 160 ev · C_Item 358 fn · 71 ev · C_AddOns 345 fn · 235 ev · C_ChatInfo 330 fn · 155 ev · C_AuctionHouse 279 fn · 129 ev · C_GuildInfo 272 fn · 82 ev · C_PvP 237 fn · 130 ev · C_QuestLog 230 fn · 63 ev · C_Spell 84 fn · 23 ev · C_Map 0 800 1,689 functions
Every system in the reference index, bucketed by name. “Everything else” is the long tail — housing, collections, individual Blizzard UI panels. Counts are exact; the bucketing is a reading aid, not an official taxonomy.

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.

Addon lifecycle from load to logout ADDON_LOADED PLAYER_LOGIN PLAYER_ENTERING_WORLD logout · /reload SavedVariables exist all addons loaded world is there SavedVariables written read and default your database here fires once per addon; check the name argument build frames, start timers here fires on every loading screen, not just login guard it, or you get a second ticker per zone There is no save call. Whatever sits in your SavedVariables table when the client writes the file is what you get back. Nothing else survives.
The payload of 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.

Widget method sets and where they are documented Object 5 Region 16 ScriptRegion 55 Frame 128 SetPoint · SetSize live on ScriptRegionResizing, not Frame Button 42 CheckButton 6 Slider 19 EditBox 79 ScrollFrame 9 StatusBar 28 FontString 57 Texture 4 CreateFrame types purple: you make them with CreateFrame orange: created from a frame, with CreateFontString / CreateTexture
Numbers are the documented methods in each 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
LibraryWhat you reach for it for
LibStubThe registry everything else is built on. Ship it first.
AceAddon-3.0Addon object with OnInitialize / OnEnable, plus modules.
AceDB-3.0SavedVariables with profiles, defaults and per-character scopes. Replaces the hand-rolled defaults above once an addon has real settings.
AceConfig-3.0Describe options as a table; get an options panel and slash commands out.
AceEvent-3.0Event registration without owning a frame.
AceGUI-3.0Widget toolkit, if you would rather not write section 4 yourself.
LibSharedMedia-3.0Shared pool of fonts, textures and sounds across addons.
LibDataBroker-1.1Publish a data feed that any display addon can show.
LibDBIcon-1.0Puts 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 functionWhat it means for you
SecretValueThis return may be secret. Guard it with issecretvalue(v) before you touch it.
SecretReturnsAll returns may be secret.
SecretWhenInCombatReadable out of combat, secret in it. Same pattern for PvP, restricted maps, auras, cooldowns.
IsProtectedFunctionCannot be called from addon code while tainted or in combat.
HasRestrictionsThere are conditions. The call can succeed and still do nothing.
SecretArguments=AllowedWhenTaintedYou may pass a secret straight into this one — this is how you display a value you cannot read.
Environment: SecureOnlyNot 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 haveDo this
A function namegrep -rn "### FunctionName" references/api/
An event namegrep "^EVENT_NAME" references/EVENTS.tsv → open the file it names
A namespaceLook it up in references/INDEX.md
A widget methodIt is in one of the 40 Simple*API.md files

In the game

/apiThe in-game browser, same source data as the files
/dump exprInspect any value
/etraceWatch events fire live — the fastest way to find the right event
/fstackName the frames under your cursor
/console scriptErrors 1Show 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