Wowpedia

We have moved to Warcraft Wiki. Click here for information and the new URL.

READ MORE

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: 100207
## 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 100207 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.

Tutorial CreateAddOn 5

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.

Tutorial CreateAddOn 6

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: 100207
## 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; this essentially completely resets your savedvariables.

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:

Tutorial CreateAddOn 8

Minimal example

Tutorial CreateAddOn 7

Multiple options with reset button

Note
Note: This example is up to date for patch 10.0.0 and Classic
local f = CreateFrame("Frame")

local defaults = {
	someOption = true,
}

function f:OnEvent(event, addOnName)
	if addOnName == "HelloWorld" then
		HelloWorldDB = HelloWorldDB or CopyTable(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")
	-- there already is an existing OnClick script that plays a sound, hook it
	cb:HookScript("OnClick", function(_, btn, down)
		self.db.someOption = cb:GetChecked()
	end)
	cb:SetChecked(self.db.someOption)

	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)
	InterfaceOptionsFrame_OpenToCategory(f.panel)
end

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

Multiple options with reset button  

GitHub source

HelloWorld.toc

## Interface: 100207
## Version: 1.0.2
## 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)
	InterfaceOptionsFrame_OpenToCategory(HelloWorld.panel_main)
end

Options.lua

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

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

function HelloWorld:CreateCheckbox(option, label, parent, updateFunc)
	local cb = CreateFrame("CheckButton", nil, parent, "InterfaceOptionsCheckButtonTemplate")
	cb.Text:SetText(label)
	local function UpdateOption(value)
		self.db[option] = value
		cb:SetChecked(value)
		if updateFunc then
			updateFunc(value)
		end
	end
	UpdateOption(self.db[option])
	-- there already is an existing OnClick script that plays a sound, hook it
	cb:HookScript("OnClick", function(_, btn, down)
		UpdateOption(cb:GetChecked())
	end)
	EventRegistry:RegisterCallback("HelloWorld.OnReset", function()
		UpdateOption(self.defaults[option])
	end, cb)
	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
		EventRegistry:TriggerEvent("HelloWorld.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

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
	HelloWorld.mushroom:SetShown(value)
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: 100207
## 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 simply 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

Conclusion[]

You know how to write a simple addon from scratch! Go and publish it on CurseForge (guide), WoWInterface (guide) and/or wago.io.
If you want to cheat and rather start with a complete example it's available here: HelloWorld.zip

Follow-up: Ace3 for Dummies


See also

Advertisement