Wowpedia
Advertisement

This guide describes how to make a simple HelloWorld addon, use slash commands and store user settings.

Getting started

Hello World

Running scripts

You can execute Lua scripts from the chat window or in a macro with the /run or /script command. There is no difference between them.

/run print("Hello World!")

To quickly turn scripts like this into an addon, just remove the "/run" part and paste it into https://addon.bool.no/

Creating an AddOn

An addon consists of Lua/XML files and a TOC file. We won't be using XML since most things that are possible in XML can also be done in Lua.

Go to your AddOns folder and create a new folder with the following files:
World of Warcraft\_retail_\Interface\AddOns\HelloWorld

HelloWorld.lua

print("Hello World!")

HelloWorld.toc

## Interface: 90205
## Version: 1.0.0
## Title: Hello World
## Notes: My first addon
## Author: YourName

HelloWorld.lua
  • The name of the TOC file must match the folder name or the addon won't be detected by the game.
  • The TOC Interface metadata 90205 as returned by GetBuildInfo() tells which version of the game the addon was made for. If they don't match then the addon will be marked out-of-date in the addon list.

Load up World of Warcraft, the addon should show up in the addon list and greet you upon login.

Development tips

See also: Lua Coding Tips

  • When updating addon code use /reload to test the new changes, you may want to put it on a macro hotkey; as well as temporarily disabling any unnecessary addons that would increase loading time.
  • Get an error reporting addon like BugSack or turn on /console scriptErrors 1
  • There is the /dump slash command for general debugging, /etrace for showing events and /fstack for debugging visible UI elements.
  • Export, clone, download or bookmark Blizzard's user interface code a.k.a. the FrameXML. If you don't know what a specific API does it's best to just reference it in FrameXML. Not everything is documented so we generally look through the code from Blizzard or other addons.
  • For VS Code the Lua extension by Sumneko adds IntelliSense features like code completion.

Responding to events

Main article: Handling events

Almost every action in the game is an Event which tells the UI that something happened. For example CHAT_MSG_CHANNEL fires when someone sends a message in a chat channel like General and Trade.

To respond to events you create a frame with CreateFrame() and register the events to it.

Event payload in the chat window and /etrace

local function OnEvent(self, event, ...)
	print(event, ...)
end

local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_CHANNEL")
f:SetScript("OnEvent", OnEvent)

Another example, to play a sound on levelup with PlaySoundFile() or PlayMusic() you register for the PLAYER_LEVEL_UP event.

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LEVEL_UP")
f:SetScript("OnEvent", function()
	PlayMusic(642322) -- sound/music/pandaria/mus_50_toast_b_hero_01.mp3
end)

Handling multiple events

When registering multiple events it can be messy if there are a lot of them.

local function OnEvent(self, event, ...)
	if event == "ADDON_LOADED" then
		local addOnName = ...
		print(event, addOnName)
	elseif event == "PLAYER_ENTERING_WORLD" then
		local isLogin, isReload = ...
		print(event, isLogin, isReload)
	elseif event == "CHAT_MSG_CHANNEL" then
		local text, playerName, _, channelName = ...
		print(event, text, playerName, channelName)
	end
end

local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("CHAT_MSG_CHANNEL")
f:SetScript("OnEvent", OnEvent)

Which can be refactored to this:

local f = CreateFrame("Frame")

function f:OnEvent(event, ...)
	self[event](self, event, ...)
end

function f:ADDON_LOADED(event, addOnName)
	print(event, addOnName)
end

function f:PLAYER_ENTERING_WORLD(event, isLogin, isReload)
	print(event, isLogin, isReload)
end

function f:CHAT_MSG_CHANNEL(event, text, playerName, _, channelName)
	print(event, text, playerName, channelName)
end

f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("CHAT_MSG_CHANNEL")
f:SetScript("OnEvent", f.OnEvent)

Slash commands

Main article: Creating a slash command
FrameXML: RegisterNewSlashCommand()

Slash commands are an easy way to let users interact with your addon. Any SLASH_* globals will automatically be registered as a slash command.

Using a slash command

-- increment the index for each slash command
SLASH_HELLOW1 = "/helloworld"
SLASH_HELLOW2 = "/hw"

-- define the corresponding slash command handler
SlashCmdList.HELLOW = function(msg, editBox)
	local name1, name2 = strsplit(" ", msg)
	if #name1 > 0 then -- check for empty string
		print(format("hello %s and also %s", name1, name2 or "Carol"))
	else
		print("Please give at least one name")
	end
end

We can also add a shorter /reload command.

SLASH_NEWRELOAD1 = "/rl"
SlashCmdList.NEWRELOAD = ReloadUI

SavedVariables

Main article: Saving variables between game sessions

To store data or save user settings, set the SavedVariables in the TOC which will persist between sessions. You can /reload instead of restarting the game client when updating the TOC file.

## Interface: 90205
## Version: 1.0.0
## Title: Hello World
## Notes: My first addon
## Author: YourName
## SavedVariables: HelloWorldDB

HelloWorld.lua

SavedVariables are only accessible once the respective ADDON_LOADED event fires. This example prints how many times you logged in (or reloaded) with the addon enabled.

local function OnEvent(self, event, addOnName)
	if addOnName == "HelloWorld" then -- name as used in the folder name and TOC file name
		HelloWorldDB = HelloWorldDB or {} -- initialize it to a table if this is the first time
		HelloWorldDB.sessions = (HelloWorldDB.sessions or 0) + 1
		print("You loaded this addon "..HelloWorldDB.sessions.." times")	
	end
end

local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", OnEvent)

This example initializes the SavedVariables with default values. It also updates the DB when new keys are added to the defaults table.
The CopyTable() function is defined in FrameXML. GetBuildInfo() is an API function.

local defaults = {
	sessions = 0,
	someOption = true,
	--someNewOption = "banana",
}

local function OnEvent(self, event, addOnName)
	if addOnName == "HelloWorld" then
		HelloWorldDB = HelloWorldDB or {}
		self.db = HelloWorldDB -- makes it more readable and generally a good practice
		for k, v in pairs(defaults) do -- copy the defaults table and possibly any new options
			if self.db[k] == nil then -- avoids resetting any false values
				self.db[k] = v
			end
		end
		self.db.sessions = self.db.sessions + 1
		print("You loaded this addon "..self.db.sessions.." times")
		print("someOption is", self.db.someOption)

		local version, build, _, tocversion = GetBuildInfo()
		print(format("The current WoW build is %s (%d) and TOC is %d", version, build, tocversion))
	end
end

local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", OnEvent)

SLASH_HELLOW1 = "/hw"
SLASH_HELLOW2 = "/helloworld"

SlashCmdList.HELLOW = function(msg, editBox)
	if msg == "reset" then
		HelloWorldDB = CopyTable(defaults) -- reset to defaults
		f.db = HelloWorldDB 
		print("DB has been reset to default")
	elseif msg == "toggle" then
		f.db.someOption = not f.db.someOption
		print("Toggled someOption to", f.db.someOption)
	end
end

Tips for troubleshooting tables:

  • /dump HelloWorldDB or /tinspect HelloWorldDB or /run for k, v in pairs(HelloWorldDB) do print(k, v) end shows the contents of a (global) table.
  • /run wipe(HelloWorldDB) or /run for k in pairs(HelloWorldDB) do HelloWorldDB[k] = nil end empties the table.
  • /run HelloWorldDB = nil; ReloadUI() removes the table reference and reloads the UI.

Options Panel

Main article: Using the Interface Options Addons panel

It would be more user-friendly to provide a graphical user interface. This example prints a message when you jump, if the option is enabled. It also opens the options panel with the slash command.

FrameXML:

Options Panel 1

Options Panel 2

local f = CreateFrame("Frame")

f.defaults = {
	someOption = true,
}

function f:OnEvent(event, addOnName)
	if addOnName == "HelloWorld" then
		HelloWorldDB = HelloWorldDB or CopyTable(self.defaults)
		self.db = HelloWorldDB
		self:InitializeOptions()
		hooksecurefunc("JumpOrAscendStart", function()
			if self.db.someOption then
				print("Your character jumped.")
			end
		end)
	end
end

f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", f.OnEvent)

function f:InitializeOptions()
	self.panel = CreateFrame("Frame")
	self.panel.name = "HelloWorld"

	local cb = CreateFrame("CheckButton", nil, self.panel, "InterfaceOptionsCheckButtonTemplate")
	cb:SetPoint("TOPLEFT", 20, -20)
	cb.Text:SetText("Print when you jump")
	cb.SetValue = function(_, value)
		self.db.someOption = (value == "1") -- value can be either "0" or "1"
	end
	cb:SetChecked(self.db.someOption) -- set the initial checked state

	local btn = CreateFrame("Button", nil, self.panel, "UIPanelButtonTemplate")
	btn:SetPoint("TOPLEFT", cb, 0, -40)
	btn:SetText("Click me")
	btn:SetWidth(100)
	btn:SetScript("OnClick", function()
		print("You clicked me!")
	end)

	InterfaceOptions_AddCategory(self.panel)
end

SLASH_HELLOW1 = "/hw"
SLASH_HELLOW2 = "/helloworld"

SlashCmdList.HELLOW = function(msg, editBox)
	-- https://github.com/Stanzilla/WoWUIBugs/issues/89
	InterfaceOptionsFrame_OpenToCategory(f.panel)
	InterfaceOptionsFrame_OpenToCategory(f.panel)
end

This reference example has a couple of checkboxes (with related functionality) and a reset button.

Options Panel example 2  

GitHub source

HelloWorld.toc

## Interface: 90205
## Version: 1.0.0
## Title: Hello World
## Notes: My first addon
## Author: YourName
## SavedVariables: HelloWorldDB

Core.lua
Options.lua

Core.lua

HelloWorld = CreateFrame("Frame")

function HelloWorld:OnEvent(event, ...)
	self[event](self, event, ...)
end
HelloWorld:SetScript("OnEvent", HelloWorld.OnEvent)
HelloWorld:RegisterEvent("ADDON_LOADED")

function HelloWorld:ADDON_LOADED(event, addOnName)
	if addOnName == "HelloWorld" then
		HelloWorldDB = HelloWorldDB or {}
		self.db = HelloWorldDB
		for k, v in pairs(self.defaults) do
			if self.db[k] == nil then
				self.db[k] = v
			end
		end
		self.db.sessions = self.db.sessions + 1
		print("You loaded this addon "..self.db.sessions.." times")

		local version, build, _, tocversion = GetBuildInfo()
		print(format("The current WoW build is %s (%d) and TOC is %d", version, build, tocversion))

		self:RegisterEvent("PLAYER_ENTERING_WORLD")
		hooksecurefunc("JumpOrAscendStart", self.JumpOrAscendStart)

		self:InitializeOptions()
		self:UnregisterEvent(event)
	end
end

function HelloWorld:PLAYER_ENTERING_WORLD(event, isLogin, isReload)
	if isLogin and self.db.hello then
		DoEmote("HELLO")
	end
end

-- note we don't pass `self` here because of hooksecurefunc, hence the dot instead of colon
function HelloWorld.JumpOrAscendStart()
	if HelloWorld.db.jump then
		print("Your character jumped.")
	end
end

function HelloWorld:COMBAT_LOG_EVENT_UNFILTERED(event)
	-- it's more convenient to work with the CLEU params as a vararg
	self:CLEU(CombatLogGetCurrentEventInfo())
end

local playerGUID = UnitGUID("player")
local MSG_DAMAGE = "Your %s hit %s for %d damage."

function HelloWorld:CLEU(...)
	local timestamp, subevent, _, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags = ...
	local spellId, spellName, spellSchool
	local amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand
	local isDamageEvent

	if subevent == "SWING_DAMAGE" then
		amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand = select(12, ...)
		isDamageEvent = true
	elseif subevent == "SPELL_DAMAGE" then
		spellId, spellName, spellSchool, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand = select(12, ...)
		isDamageEvent = true
	end

	if isDamageEvent and sourceGUID == playerGUID then
		-- get the link of the spell or the MELEE globalstring
		local action = spellId and GetSpellLink(spellId) or MELEE
		print(MSG_DAMAGE:format(action, destName, amount))
	end
end

SLASH_HELLOW1 = "/hw"
SLASH_HELLOW2 = "/helloworld"

SlashCmdList.HELLOW = function(msg, editBox)
	-- https://github.com/Stanzilla/WoWUIBugs/issues/89
	InterfaceOptionsFrame_OpenToCategory(HelloWorld.panel_main)
	InterfaceOptionsFrame_OpenToCategory(HelloWorld.panel_main)
end

Options.lua

HelloWorld.defaults = {
	sessions = 0,
	hello = false,
	mushroom = false,
	jump = true,
	combat = true,
	--someNewOption = "banana",
}

local callbacks = {}

local function CreateIcon(icon, width, height, parent)
	local f = CreateFrame("Frame", nil, parent)
	f:SetSize(width, height)
	f.tex = f:CreateTexture()
	f.tex:SetAllPoints(f)
	f.tex:SetTexture(icon)
	return f
end

-- if `update` is passed, call it when the option is initialized and when clicked
function HelloWorld:CreateCheckbox(savedvar, name, parent, update)
	local cb = CreateFrame("CheckButton", nil, parent, "InterfaceOptionsCheckButtonTemplate")
	cb.Text:SetText(name)
	cb.SetValue = function(_, value)
		if type(value) == "string" then
			value = (value == "1")
		end
		self.db[savedvar] = value
		cb:SetChecked(self.db[savedvar])
		if update then
			update(value)
		end
	end
	cb:SetValue(self.db[savedvar]) -- init
	self:RegisterCallback("OnReset", function()
		cb:SetValue(self.defaults[savedvar])
	end)
	return cb
end

function HelloWorld:InitializeOptions()
	-- main panel
	self.panel_main = CreateFrame("Frame")
	self.panel_main.name = "HelloWorld"

	local cb_hello = self:CreateCheckbox("hello", "Do the |cFFFFFF00/hello|r emote when you login", self.panel_main)
	cb_hello:SetPoint("TOPLEFT", 20, -20)

	local cb_mushroom = self:CreateCheckbox("mushroom", "Show a mushroom on your screen", self.panel_main, self.UpdateIcon)
	cb_mushroom:SetPoint("TOPLEFT", cb_hello, 0, -30)

	local cb_jump = self:CreateCheckbox("jump", "Print when you jump", self.panel_main)
	cb_jump:SetPoint("TOPLEFT", cb_mushroom, 0, -30)

	local cb_combat = self:CreateCheckbox("combat", "Print when you damage a unit", self.panel_main, function(value)
		self:UpdateEvent(value, "COMBAT_LOG_EVENT_UNFILTERED")
	end)
	cb_combat:SetPoint("TOPLEFT", cb_jump, 0, -30)

	local btn_reset = CreateFrame("Button", nil, self.panel_main, "UIPanelButtonTemplate")
	btn_reset:SetPoint("TOPLEFT", cb_combat, 0, -40)
	btn_reset:SetText(RESET)
	btn_reset:SetWidth(100)
	btn_reset:SetScript("OnClick", function()
		HelloWorldDB = CopyTable(HelloWorld.defaults)
		self.db = HelloWorldDB
		self:FireCallbacks("OnReset")
	end)

	InterfaceOptions_AddCategory(HelloWorld.panel_main)

	-- sub panel
	local panel_shroom = CreateFrame("Frame")
	panel_shroom.name = "Shrooms"
	panel_shroom.parent = self.panel_main.name

	for i = 1, 10 do
		local icon = CreateIcon("interface/icons/inv_mushroom_11", 32, 32, panel_shroom)
		icon:SetPoint("TOPLEFT", 20, -32*i)
	end

	InterfaceOptions_AddCategory(panel_shroom)
end

-- crappy callback handler
function HelloWorld:RegisterCallback(name, func)
	callbacks[name] = callbacks[name] or {}
	callbacks[name][func] = true
end

function HelloWorld:FireCallbacks(name)
	for func in pairs(callbacks[name]) do
		func()
	end
end

function HelloWorld.UpdateIcon(value)
	if not HelloWorld.mushroom then
		HelloWorld.mushroom = CreateIcon("interface/icons/inv_mushroom_11", 64, 64, UIParent)
		HelloWorld.mushroom:SetPoint("CENTER")
	end
	if value then
		HelloWorld.mushroom:Show()
	else
		HelloWorld.mushroom:Hide()
	end
end

-- a bit more efficient to register/unregister the event when it fires a lot
function HelloWorld:UpdateEvent(value, event)
	if value then
		self:RegisterEvent(event)
	else
		self:UnregisterEvent(event)
	end
end

AddOn namespace

Main article: Using the AddOn namespace

The addon namespace is a private table shared between Lua files in the same addon. This way you can avoid leaking variables to the global environment.

HelloWorld.toc

## Interface: 90205
## Version: 1.0.0
## Title: Hello World

FileA.lua
FileB.lua

FileA.lua

local _, ns = ...
ns.foo = "Banana"

FileB.lua

local addonName, ns = ...
print(addonName, ns.foo) -- prints "HelloWorld" and "Banana"


Or you can use a unique global variable.

FileA.lua

MyAddon = {}
MyAddon.value = 0

function MyAddon:DoSomething(increment)
	self.value = self.value + increment
end

MyAddon:DoSomething(2)

FileB.lua

MyAddon:DoSomething(3)
print(MyAddon.value) -- 5

Publishing

Addons can be published on CurseForge (guide) and/or WoWInterface (guide).

Follow-up: Ace3 for Dummies


Other guides

Advertisement