WoW:Parsing event messages

HOWTOs

Event messages are things like combat log messages, loot messages, online/offline notifications, and so on.

If you are writing an AddOn that wants to take some sort of action when a certain event happens (for example, tracking your honor from PvP kills), you need to find the correct event and register to listen for that event. Some events provide you the information about that event in compact easy-to-use arguments. For example, the REPLACE_ENCHANT event has two arguments, arg1 (the name of the enchant) and arg2 (the name of the item). Other events don't provide you the arguments in such an easy-to-use form. For example, the CHAT_MSG_COMBAT_HONOR_GAIN event has one big long argument, arg1, that looks something like "Joe dies, honorable kill Rank: Commander (Estimated Honor Points: 10)". What you really want to know, though, is

  1. who died
  2. what their rank was, and
  3. how many estimated honor points it was worth.

In order to get this information, you need to parse the big long string.

Parsing messages the quick wayEdit

What's the quick-and-dirty way to parse an event message?

Well, you can look at how the event message looks, and you can manually write a string.find over that string to extract the information you need. For example,

function MyHonorTracker_OnEvent()
  if(event == "CHAT_MSG_COMBAT_HONOR_GAIN") then
    start, end, killed, rank, honor =
      string.find(arg1, "(.+) dies, honorable kill Rank: (.+) %(Estimated Honor Points: (.+)%)");
    if(start) then
      MyHonorTrackingFunction(killed, rank, honor);
    end
  end
end
function MyHonorTracker_OnEvent()
  if(event == "CHAT_MSG_COMBAT_HONOR_GAIN") then
    if(GetLocale() == "enUS") then
      pattern = "(.+) dies, honorable kill Rank: (.+) %(Estimated Honor Points: (.+)%)";
    elseif(GetLocale() == "deDE") then
      pattern = "(.+) german-for-dies, german-words: (.+) %(more-german: (.+)%)";
    elseif(GetLocale() == "frFR") then
      pattern = "french-words (.+) french-words (.+) french-words (.+)";
    elseif(GetLocale() == "koKR") then
      pattern = "(.+) korean-for-dies, korean-words: (.+) %(more-korean: (.+)%)"; 
    else
      pattern = "who knows";
    end
    start, end, param1, param2, param3 = string.find(arg1, pattern);
    if(GetLocale() == "frFR") then
      -- correct the order of the parameters
      killed, rank, honor = param2, param1, param3;
    else
      killed, rank, honor = param1, param2, param3;
    end
    if(start) then
      MyHonorTrackingFunction(killed, rank, honor);
    end
  end
end

Parsing messages the clean wayEdit

What's the clean-and-easy way?

Well, first, you should extract GlobalStrings.lua out of your WoW install. Then, you will want to look through that file for the variable that contains the template of the message you're looking for. In our case, it is

COMBATLOG_HONORGAIN = "%s dies, honorable kill Rank: %s (Estimated Honor Points: %d)";

This variable is the key to this method. Basically, Blizzard has already done the work of creating translations for every language that WoW supports, and they have provided this GlobalStrings.lua to map those translations to common variable names. We just need to figure out how to harness this information.

Harnessing this information yourself can be quite a task. You need to revise your code to look at this variable, transform the value of this variable into a value suitable for your pattern variable, run your pattern, and then be able to reorder the parameters correctly based on the value of this variable.

However, each AddOn author should not have to do nearly this much work. There's a library called MarsMessageParser. I don't have a link to that file directly, but you will find it as a separate file inside of this AddOn. You can copy MarsMessageParser.lua out of MarsNeedyGreedy and put it in your AddOn, say MyHonorTracker. Make sure MarsMessageParser.lua is listed before MyHonorTracker.lua in your MyHonorTracker.toc file or your MyHonorTracker.xml file. Then, all of the above code can be trimmed down to just this:

function MyHonorTracker_OnEvent()
  if(event == "CHAT_MSG_COMBAT_HONOR_GAIN") then
    MarsMessageParser_ParseMessage("MyHonorTracker", arg1);
  end
end

Well, that isn't exactly true: MarsMessageParser also needs to be initialized. Fortunately, this can be done with just one line (put this line somewhere in your initialization area):

MarsMessageParser_RegisterFunction("MyHonorTracker", COMBATLOG_HONORGAIN, MyHonorTrackingFunction);

The parameters to the RegisterFunction function are as follows:

  1. The first parameter is your AddOn name and needs to match the name you pass in to the ParseMessage function.
  2. The second parameter is the name of the template variable from GlobalStrings.lua for the message you want to parse.
  3. The third parameter is the function you want called with the parameters extracted from the event messages.
  4. An optional fourth parameter (not shown) specifies how MarsMessageParser should handle item links if they are present (either collapsing them or leaving them in pieces). Since there are no items links in an honor function, we omit the fourth parameter. For more details, see the API description at the top of MarsMessageParser.lua.

The parameters to the ParseMessage function are as follows:

  1. The first parameter is your AddOn name and needs to match the name you passed in to the RegisterFunction function.
  2. The second parameter is the big long string you got from WoW (typically arg1).

MarsMessageParser will take care of reading the GlobalStrings.lua, creating the correct pattern for the current locale, re-ordering the parameters accordingly, and finally calling MyHonorTrackingFunction with three parameters: killed, rank, and honor.

Parsing messages the right wayEdit

What's the Clean-And-Not-So-Easy way?

If you don't want to download the MarsMessageParser and include it in your addon, the thing you're left to do is find a way to do it yourself. But rather than develop a ton of code, you just want to "hardwire" your addon for just the string you want to use, rather than the infinite amount of infinite variables in every string in GlobalStrings.lua.

Well, let's look at FACTION_STANDING_INCREASED. According to GlobalStrings.lua, this says "Reputation with %s increased by %d." Well, to make it work like the strings at the top, we'd need to replace %s and %d with (.+). But how do we do that? Here's how:

newpattern = string.gsub(string.gsub(FACTION_STANDING_INCREASED, "(%%s)", "(.+)"), "(%%d)", "(.+)")
start, end, faction, amount = string.find(string, newpattern)

This example will properly parse the string, and return all the information we need. How was this achieved? By using a % in front of the %s, it tells it to look for %s in stead of a space, which is what %s would normally be looking for. This way, even though we have to hardcode in the strings we want, we can also don't have to bother with translations.