This guide describes how to make a simple HelloWorld addon, use slash commands and store user settings.
Getting started[]
- You need a basic understanding of Lua, otherwise see Introduction to Lua or other tutorials.
- A simple text editor like VS Code or Notepad++.
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: 110005 ## 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
110005
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.
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.
-- 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: 110005 ## 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:
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: 110005 ## 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: 110005 ## 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