How to Make an Auto-Inviting Mage Portal AddOn for World of Warcraft Classic
Published
Picture the scene… You’re a mage in Kargath and you want to make some money creating portals for people heading to Orgimmar and elsewhere. But you’re not the only mage hoping to make some quick cash from your services. You have competition! You apply your entire marketing knowledge and yell your best sales pitch to the continuous stream of adventurers bundling out of the inn.
“WTS Portals”
Nobody’s biting. Despite adventurers yelling words like “Org”, “UC”, “TB”, “Og” and “Undersh*tty”, nobody is coming to you for those portals. Every time you invite one of those gold-laden, potential customers to a group, you’re informed that they’re already in one! It seems that the other portal-selling mages are getting all the business. They smile smugly to themselves as they continuously vacuum up gold from everyone around them and yet you’re not getting a single coin.
What’s going on?
You probably already know if you’ve ever been seeking portals from a mage yourself. As soon as you utter a phrase which indicates that you’re looking for a portal then you’ll be immediately invited to a group. No human could possibly react as fast so it must be automated and indeed it is. The other mages are running AddOns to quickly create groups and get the business.
So here’s how to make a World of Warcraft AddOn to do the same thing yourself. Soon you’ll be on a level playing field with the competition!
The Basics
I’m not going to prattle on about Lua and why people use AddOns just to get my word count up and hope for a better SEO ranking. Instead, I’m going to assume that you already know these things or you wouldn’t be here in the first place.
Choose a Name
First, we need a unique name for our AddOn. Something catchy but relevant to its function. Hmmm… PortalWhore PortalWhere!
Create a Folder
Every AddOn needs its own folder created in a specific location in order for World of Warcraft to find it. On my PC the AddOns folder is located at:
# The location of WoW's AddOns
C:\Program Files (x86)\World of Warcraft\_classic\Interface\AddOns
You’ll know you’re in the right place because you’ll likely see other AddOns in the folder but if you’re one of those rare freaks of nature that has never installed an AddOn before then you might need to create this folder yourself.
You’ll then need to create your new folder inside of this one so in my case I will be working at:
# The Addon's path
C:\Program Files (x86)\World of Warcraft_classic\Interface\AddOns\PortalWhere
Create Some Files
Every AddOn must have a .toc
file in order for World of Warcraft to recognise it. This file contains everything the game needs to know about your AddOn, including its name, description, the version of WoW required and what Lua and XML files to load to make it all work. The main thing to remember is that the .toc
file must be named the same as its parent folder and it must use the .toc
extension. So ours will be called PortalWhere.toc
.
## Interface: 11302
## Title: PortalWh*re
## Notes: A portal mage's best friend
Core.lua
The Interface
line tells WoW which version this AddOn is for. Since we’re creating this for Classic then we’ll tell it that we require version 11302
.
The Title
line is the name that will show up in the AddOns list in game, and the Notes
line specifies the description shown when you hover your mouse over it.
Underneath the ##
headers is a list of files that the AddOn will load. PortalWhere is going to be a relatively simple AddOn so we’ll be able to code it all using only one Lua file – All of our code will be in Core.lua
. So create that file and we’ll move on to the good stuff – actual code!
A Strong Foundation
It’s time to create some code and make something work.
World of Warcraft AddOns are event driven. That means that the game informs them when something happens and the AddOns can choose to perform actions in response.
Events are sent to frames and since events are at the core of how everything works, we will actually base all of our code around a single frame by creating that frame and then adding class methods to it.
First we create our frame. A frame is bit like the equivalent of a window in Windows. It’s a core UI element that can be a window, a button, or an image etc. Our frame won’t be displayed on-screen but it will be hidden away in the background, receiving and responding to game events.
PortalWhere = CreateFrame("Frame")
Then we define a new method called Print
which accepts a varying number of arguments and passes them all to the WoW’s built-in print
function after prepending [PortalWh*re]
to the text. This isn’t necessary but it’s nice to know which AddOn printed text to the chat box.
function PortalWhere:Print(...)
print("[PortalWh*re] " .. ...)
end
The next method we define is Boot
. Here we call SetScript
to tell the frame how we’d like to handle received events. We pass a lambda function to it which which cleverly looks at the name of the event we’re receiving and will call a method on our class with the exact same name and pass all of the arguments to it.
function PortalWhere:Boot()
self:SetScript("OnEvent", function(self, event, ...)
self[event](self, ...)
end)
self:RegisterEvent("ADDON_LOADED")
end
We then also call RegisterEvent
to let the game know that we want to be informed about the ADDON_LOADED
event. This is an event that’s fired every time an AddOn has finished loading. It’ll fire once for every loaded AddOn and we’re going to use this as a place to start our initialisation.
Next we define the ADDON_LOADED
method which will be called automagically by our little lambda call that we passed to SetScript
. It receives the name of the AddOn that just loaded. We’ll make sure it was us that loaded and if so we call OnBoot
, which (for now) just prints a message to the console to show that our freshly created AddOn has been successfully loaded.
function PortalWhere:ADDON_LOADED(name)
if name == "PortalWhere" then
self:OnBoot()
end
end
function PortalWhere:OnBoot()
self:Print("Loaded.")
end
Then all we need to do to get things going is actually call our Boot
method. The Core.lua
file now looks like this:
PortalWhere = CreateFrame("Frame")
function PortalWhere:Print(...)
print("[PortalWh*re]", ...)
end
function PortalWhere:Boot()
self:SetScript("OnEvent", function(self, event, ...)
self[event](self, ...)
end)
self:RegisterEvent("ADDON_LOADED")
end
function PortalWhere:ADDON_LOADED(name)
if name == "PortalWhere" then
self:OnBoot()
end
end
function PortalWhere:OnBoot()
self:Print("Loaded.")
end
PortalWhere:Boot()
Congratulations, you have a working AddOn. Sure, it’s not very useful right now but it’s a strong foundation to build on.
Looking for Punters
Now that we’ve got a basic AddOn working, we can get down to the nitty-gritty and create the functionality that’s going to get some of that sweet, sweet Kargath coin.
On and Off
We’re going to need a way to turn our auto-inviter on and off. There’s nothing more annoying than asking for a portal in Orgrimmar and being auto-invited by an AFK mage so hopefully you’ll remember to turn this off when you’re taking a break from filling your pockets with gold.
Let’s create a method that will register a slash command with WoW so that we can type /portalwhere on
and /portalwhere off
and also allow us to use /pw
as well since /portalwhere
takes longer to type!
function PortalWhere:RegisterSlashCommand()
SLASH_PORTALWHERE1 = "/pw"
SLASH_PORTALWHERE2 = "/portalwhere"
SlashCmdList["PORTALWHERE"] = function(msg)
local _, _, command, args = string.find(msg, "%s?(%w+)%s?(.*)")
if command then
self:OnSlashCommand(command, args)
end
end
end
function PortalWhere:OnBoot()
self:RegisterSlashCommand()
self:Print("Loaded.")
end
Registering chat commands with WoW seems a bit hacky. You have to add your command to a predefined global table called SlashCmdList
and then create some matching global variables. It’s a bit ugly but that’s how it works. Now whenever /pw
or /portalwhere
is entered in to the chat window, our lambda function will get called with the text that follows those commands. So if I typed /pw hello there
then our function will receive the text “hello there”.
We’re then parsing that text. We take the first word we see and assume that’s a command and that all the following text are arguments.
function PortalWhere:OnSlashCommand(command, args)
command = string.lower(command)
if command == "on" then
self:On()
elseif command == "off" then
self:Off()
else
self:Print("Unknown command.")
end
end
We use string.lower()
to convert the command to lowercase so we can easily test it without worrying about case sensitivity. Now it you type /pw on
(or /portalwhere on
) then our On()
method will get called. If you type /pw off
(or /portalwhere off
) then the Off()
method will get called. We haven’t created On()
and Off()
yet so let’s get to it.
Monitoring Chat
When our AddOn is in the on state, we want to watch the say, yell and whisper channels and when it’s off then we want to stop watching those channels.
function PortalWhere:On()
self:RegisterEvent("CHAT_MSG_SAY")
self:RegisterEvent("CHAT_MSG_YELL")
self:RegisterEvent("CHAT_MSG_WHISPER")
self:Print("Looking for punters...")
end
function PortalWhere:Off()
self:UnregisterEvent("CHAT_MSG_SAY")
self:UnregisterEvent("CHAT_MSG_YELL")
self:UnregisterEvent("CHAT_MSG_WHISPER")
self:Print("All done. Time for breakfast.")
end
We’re then going to respond to say, yell and whisper in exactly the same way by routing it all to the same OnChat()
method.
function PortalWhere:CHAT_MSG_SAY(...)
self:OnChat(...)
end
function PortalWhere:CHAT_MSG_YELL(...)
self:OnChat(...)
end
function PortalWhere:CHAT_MSG_WHISPER(...)
self:OnChat(...)
end
function PortalWhere:OnChat(text, playerName, _, _, shortPlayerName, _, _, _, _, _, _, guid)
end
Before we flesh out OnChat()
, let’s go to the top of the file and define the chat keywords that we’ll be looking for against the possible destinations. We’ll define them just under our CreateFrame()
call.
PortalWhere = CreateFrame("Frame")
PortalWhere.matchWords = {
["Undercity"] = {
"Undercity",
"UC",
"Undershitty",
},
["Orgrimmar"] = {
"Orgrimmar",
"Orgrimar",
"Org",
"Orgri",
"Ogri",
"Ogr",
"Og",
},
["Thunderbluff"] = {
"Thunderbluff",
"TB",
}
}
We’ll then convert these to lowercase in our OnBoot()
method so we can do case insensitive matching.
function PortalWhere:MakeLowercaseMatchWords()
for destination, wordList in pairs(self.matchWords) do
for index, word in ipairs(wordList) do
self.matchWords[destination][index] = string.lower(word)
end
end
end
function PortalWhere:OnBoot()
self:MakeLowercaseMatchWords()
self:RegisterSlashCommand()
self:Print("Loaded.")
end
At this stage we know what people are saying and we have a list of words to look for. Time to join the dots! Let’s create a WantsPortal()
method that will determine if the player talking is looking for a portal and where they want to go.
function PortalWhere:WantsPortal(playerName, guid, message)
if playerName == UnitName("player") then
return false
end
local _, playerClass = GetPlayerInfoByGUID(guid)
if playerClass == "MAGE" then
return false
end
for word in string.gmatch(message, "%a+") do
local match, destination = self:MatchWordToDestination(word)
if match then
return match, destination
end
end
return false
end
Notice that we’re first making sure that it isn’t us talking. Secondly, we’re also making sure that the player talking isn’t a mage since other mages aren’t likely to need a teleport but are very likely to trigger our keyword matching when they advertise their services. Really the first test isn’t needed since we’re also a mage but I like to be explicit. The guid
parameter is something we receive with the chat message. It’s a unique identifier for the character and we use it to determine their class.
Another thing to note here is that we’re looking for whole words with %a+
. I’ve been in Kargath before and found other bots do a terrible job at this. For example I could type “Let’s go to the truck-stop” and be automatically invited by a lesser quality AddOn since “truck-stop” contains the letters “uc”.
We’ll also need a couple of helper functions to make this work.
function PortalWhere:MatchWordToDestination(word)
word = string.lower(word)
for destination, wordList in pairs(self.matchWords) do
if self:ArrayHas(word, wordList) then
return true, destination
end
end
return false
end
function PortalWhere:ArrayHas(item, array)
for index, value in pairs(array) do
if value == item then
return true
end
end
return false
end
MatchWordToDestination()
receives a word and performs a case-insensitive search in our matchWords
table. If it finds a match then it returns true
along with the matched destination.
ArrayHas()
is a helper method that we use to determine if an array contains a value. It’s a method that you may find useful when creating other AddOns too.
Sending an Invite
We have everything we need and now for the easy bit! We’re going to piece everything together in our OnChat()
method and invite potential customers to our group.
function PortalWhere:OnChat(text, playerName, _, _, shortPlayerName, _, _, _, _, _, _, guid)
if self:WantsPortal(playerName, guid, text) then
InviteUnit(playerName)
end
end
Job Done
There you have it – a working auto-inviter AddOn for portal mages. Congratulations on making it to the end of this article but your work doesn’t have to stop here. There are many improvements that could be made to this AddOn. You could create a GUI with a real On / Off button and maybe even keep a history of how many times you’ve teleported players. You can also make it automatically open up a trade window to prompt the punter to give you the gold or welcome them if they join the group – everyone loves a spammy mage. I’ll leave those things as an exercise to the reader.
A Humble Request
You’re free to do whatever you like with this code (under the MIT license) but I’d like to make a humble request – Please don’t just copy & paste this code and upload it as it is to Curse or another AddOn site. At least add something to it so we don’t end up with 10 copies of the same AddOn in the Twitch app. Oh, and please credit the author!
It’s on Github
You can find the complete source code on Github.