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”

Players asking for 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.