Thursday, September 30, 2010

A Pair of Warcraft Addons

I've been poking around with Lua a little bit lately, working with Addons for World of Warcraft.

TOTTler and tottler

Original TOTTler addon:

My tottler friendly fork/utter rewrite:

To understand why an addon such as tottler exists, you must first understand the rogue ability Tricks of the Trade (also just tricks or tott). Tricks is an ability which a rogue can cast on a friendly target. When the rogue makes a special attack on a creature after casting tricks, the threat which is generated from that attack, and any threat generated for the next six seconds, is passed on to the friendly target. This allows a rogue to help a tank keep their threat high, and it can assist in AOE pulls of many monsters at once.

In addition to the threat transfer, tricks also gives a damage boost to the friendly target for the duration of the transfer.

As if that wasn't enough fun, the tier 10 two-piece bonus makes tricks also cost no energy, and instead gives energy back to the rogue. This makes the ability a massively important tool, as energy is commonly the limiting factor in what a rogue can do.

Tricks of the Trade works as three separate affects. When it is originally cast, the rogue gains a buff that lasts for 30 seconds. After the first special attack happens, that buff disappears, and another buff (which lasts for 6 seconds) appears. This second buff is the one that actively transfers threat to the tricks target.

At the time of the first special attack, third buff is placed upon the tricks target, and this buff grants them their damage boost.

It is possible to cancel the rogue's threat transfer buff. Doing so does not effect the target's damage boost buff.

In essence, this means it is possible to use tricks, get the energy refund from the 2pc t10, give the damage boost to a non-tank (such as another rogue), and NOT give the friendly target any additional threat.

There was a fair amount of consideration over this in the forums, and the original author of TOTTler wrote an addon that would automatically cancel the threat transfer.

What's most interesting is that the original TOTTler also was clever, and permitted the player from deciding ahead of time whether the tricks target should or should not get the threat. For instance, if the target was the tank, you'd definitely want the threat transfer. If the target was a mage, you really wouldn't want to give them even more threat (unless you were trying to get them killed, which is fine by me).

I picked up TOTTler, but I found that its command interface was terribly broken, so I took a look at the code and found a lot of bits of ugly (nothing declared local, unnecessarily using an XML file for the frame description, etc). I decided that I should make myself useful, and contribute some patches back to the original author.

The problem is that the original author isn't very responsive (to me at least). Also, the code for TOTTler wasn't kept in a source control system that I could find.

Luckily, TOTTler was licensed to be in the public domain! So I set about re-writing it as a friendly fork, calling it simply "tottler"

I cleaned up the handling of events and commands. I also added the ability to have the addon notify you when another rogue is giving you the damage-increasing buff.

There are three parts of tottler that I am particularly happy with, and will likely re-use in other addons that I write in the future.

First, and I have already re-used this, I have a very short pattern for creating local functions for subscribing to and handling events. I create a table with keys matching the event names, and values referencing the local function to be called. I then create an empty Frame, and set its On_Event handler to a short function which simply looks up the event in the table and executes it's function with all of the remaining arguments to the event.

The table-lookup can be significantly faster than a if/then/else block checking the string over and over again, and it's much more terse and easier to add new events to... you just put the event name and its handler in the event_handler table and it'll work! See lines 702 through 718.

Secondly came handling the command-line interface for the addon. The original TOTTler would throw an exception of you specified an incorrect number of arguments, or an unknown command. This was something that I really wanted to fix.

I set up a table listing each of the commands, the function that should handle the command, and some short help text. The dispatcher would then unpack all of the arguments into a list, find the correct command (and fail gracefully), and pass the arguments to the correct function.

Again, table lookup being faster than if/then/else, this also permitted me to very easily write a help command that would present all of the known commands and their arguments and descriptions. Adding a new command involves updating the table and writing a handler function. I will definitely be re-using this code in the future. See lines 471 through 566 for the main command handler and commands table.

Finally, I needed a reliable way to detect the application of multiple buffs. For this, I put together a handler for the UNIT_AURA event. Each time the UNIT_AURA event fires, the various buffs on the unit are collected into a table for caching. For each buff that was added to the cache, but which was not in the previous cache, a table is checked. This table is keyed by spell_ids, the value is a handler function to be called when that spell_id is found, but only the first time! So we if we have an aura on the player which wasn't found the last time UNIT_AURA was called, we know it is newly applied. We check if we are interested in this aura (by seeing if there's a function associated with that spell_id), and if so, we'll call the handler function. At the end of the event, the new cache is stored, and the previous cache is destroyed. See lines 594 through 647.

Unfortunately come patch 4.0.1, it will no longer be possible for an addon to cancel a unit's buffs automatically while in combat. This will kill the primary purpose of this addon. It's entirely possible that addons like this are the very reason for the change.

Whose Aura Is This?

One of the things that often annoyed me when looking at my buffs and debuffs is that I could not easily tell who had applied what spell on whom. The information is obviously available to our game clients (if you watch the combat log, you'll see the name of the original buff casters when the affects are applied and when they fade), why isn't it in the user interface?

So I wrote the simple whoseaura addon, which simply binds to the GameTooltip and watches for the SetUnitAura call. Whenever this is triggered, whoseaura uses the UnitAura function to get detailed information such as the unit_id of the aura's caster.

Now, unfortunately, a unit_id is not always terribly useful. A unit_id will be something like, "player" or "target" or "party3" rather than an actual name. One must use the GetName function to convert a unit_id into an actual meaningful name, rather than a relationship.

Problematically, if there's no ready unit_id to describe the relationship between the caster and the player, UnitAura will return nil for that field! For example: a paladin you are not in party with casts Kings on you. If you target the paladin and mouse-over the Kings tooltip, you'll see their name as the caster. However, if you un-target them, the caster will now display as "Unknown" because UnitAura wasn't able to describe a relationship to that player!

A possible fix for this would be to instead monitor the combat log, which does not use a unit_id, but which instead will always provide the full name (and server) of the casting player or creature. However, this would require some caching and a lot of filtering.

For now, I am alright with whoseaura operating with the limitations of the UnitAura function, because it keeps the addon utterly tiny.