Archive

UI Mod Guide - Chapter 4

Updated Thu, Feb 21, 2008 by bpirkle

Krimm's Guide
to World of Warcraft Addon Design

Part 4
Tables

Part 4 Index

Download Part 4 Example Files!


Are you bored with your battle cry yet?  I know I am.  I can use our slash commands to change it, or turn it on and off, but it would be nice to have some variety without that much effort.  After all, I still have four more parts to this guide left to write (counting this one).  So as I test the code, I’m gonna be seeing my battle cry a lot.

Here’s an idea:  let’s let the user enter a bunch of battle cries and have the AddOn randomly display one of them when a fight starts.  Not only will this improve our AddOn, but it is a good time to talk about Lua tables in more depth.  And we’ll need to know tables for the changes we’ll be making in Part Five and Part Six.

SO, WHAT ARE Lua TABLES?

I promised in Part One of this guide that I wouldn’t try to teach you Lua coding.  And I mean it, I really do.  But some concepts are too fundamental to avoid.  Trying to learn how to cleanly write a complex AddOn without knowing tables would be kinda like trying to spec your Feral Druid before you know what Talents are.  (And if you don’t know that, close this guide and go play some WoW already!)

Lua tables are incredibly flexible data structures.  If you’ve used arrays in strongly typed languages such as C++, you’re in for a treat.  Lua tables are entirely different beasts, with mind-bending capabilities. 

At a basic level, a Lua table is just a place to store a bunch of stuff, combined with convenient ways to find the stuff later.  Recall that in Part Two we said a Lua variable was like a big empty box?  Lua tables are like a bunch of big empty boxes, all connected in a row.  These boxes (I’ll also call them slots or entries) can contain just about anything:  numbers, strings, functions, other tables, etc.  And they don’t all have to contain the same kind of thing.  Within a single table, one box could contain a number, one a string, and one a function. 

Now here comes the really cool part.  Imagine each box has an index card taped to it.  On each index card is written something that identifies the box.  This identifier (or key) can be anything:  a number, a string, a function, another table, etc.  And like the box contents, the keys don’t have to all be the same type.  Wow.  (Pun intended. J)

Let’s see how tables look in code.  Here’s how to create an empty table in Lua, then assign it some values:

MyTable = {};
MyTable[1] = 1;
MyTable["cow"] = "two";
MyTable[5] = 3.5;

Notice that we’re mixing types for both keys and values, and even when we used numbers as keys, they didn’t have to be consecutive. 

When we want to access the values (peek in the boxes to see what they hold) we do this:

someVariable1 = MyTable[1];
someVariable2 = MyTable["cow"];
someVariable3 = MyTable[5];
someVariable4 = MyTable["oops"];

Oops, we didn’t store a value with a key of “oops”.  This will cause someVariable4 to end up with the special value nil, which means “no value” or “not present”.

MULTIPLE BATTLE CRIES

Next, let’s use tables to store a set of battle cries.  We can create our table like this:

TthAog4MyBattleCries = {
  "Not in the face!",
  "Not in the groin!",
  "Not in the kneecaps!"
};

This is just a shorthand way of creating the table and assigning it some values all in one statement.  When values are added to the table this way, they automatically use consecutive integers for keys, starting at 1.
 
Now that we have multiple battle cries, our old slash command isn’t good enough any more.  Instead, we’ll have two slash commands.  One command will add a battle cry to the table, and the other command will clear all battle cries.  (In Part Seven we’ll create a more powerful interface to make up for things these slash commands obviously lack, such as the ability to delete a specific battle cry.)  We looked atslash commands in detail in Part Two, so I won’t bore you with all the details.  Take a look at the code listing at the end if you like.  Suffice it to say that we’ll end up with four things the user can type to manage our battle cries:

/addnewbattlecry4
/anbc4
/clearallbattlecries4
/cabc4

The interesting part happens in the functions that are called in response to these slash commands:

function TthAog4AddNewBattleCry(msg)
  -- Add the new battle cry, for use later.
  local newKey = table.getn(TthAog4MyBattleCries) + 1;
  TthAog4MyBattleCries[newKey] = msg;

  -- Echo the new battle cry for confirmation.
  local echo = TTH_AOG4_BATTLE_CRY_ADDED .. msg;
  TthAog4OutputToChat(echo);
end

This function gets the number of entries in the table, adds one to it, and inserts a new battle cry in the new slot.  Then it echoes the new battle cry (TTH_AOG4_BATTLE_CRY_ADDED is a constant string we’ll declare at the top of our file.)  Notice that the table size isn’t fixed when it is created.  In keeping with Lua’s trademark flexibility, all we have to do to expand the table to hold a new entry is assign a value to the entry.  The table grows automatically to accommodate the new data.  Also notice this function call:

table.getn(TthAog4MyBattleCries)

This is our first encounter with using “dot notation” to call a function, but it won’t be our last.  Lua provides a library named “table”, which contains a bunch of functions related to tables.  One of these functions is “getn”, which returns the number of items in the table.  By writing “table.getn” we’re saying “call the getn function in the table library.  We’ll do something very similar with our own functions when we add packages to our AddOn in Part Five. 

Next let’s look at the other slash command function:

function TthAog4ClearAllBattleCries()
  --  assign to an empty table for quick and easy clearing.
  TthAog4MyBattleCries = {};

  -- Echo the new battle cry for confirmation.
  TthAog4OutputToChat(TTH_AOG4_BATTLE_CRIES_CLEARED);
end

This one clears all existing battle cries and echoes the constant string contained in the TTH_AOG4_BATTLE_CRIES_CLEARED variable message to chat for confirmation.  To clear the table, all we did was assign a new, empty table to the TthAog4MyBattleCries variable.  Boom, all the old battle cries are gone in one simple statement.  If you’re coming to Lua from a language that requires manual memory management, be aware that Lua is garbage collected so there is no need to deallocate the old table.  If that sentence caused your eyes to glaze over and roll back in your head, just skip it and move along.

The TthAog4Event function has grown a bit.  Here’s how it looks now:

function TthAog4Event()
  if (event == "ADDON_LOADED") then
    if (arg1 == TTH_AOG4_ADDON_NAME) then
      TthAog4OutputToChat(TTH_AOG4_BATTLE_CRIES_ARE);
      for i=1,table.getn(TthAog4MyBattleCries) do
        TthAog4OutputToChat(TthAog4MyBattleCries[i]);
      end
    end
  elseif (event == "PLAYER_REGEN_DISABLED") then
    -- Send a random message to chat (if there are any to send)
    local numBattleCries = table.getn(TthAog4MyBattleCries);
    if (numBattleCries > 0) then
      local randomKey = math.random(numBattleCries);
      SendChatMessage(TthAog4MyBattleCries[randomKey], "SAY");
    end
  end
end

We’ve changed the handling of both ADDON_LOADED and PLAYER_REGEN_DISABLED.
You can probably guess what the changes do in both cases, but we’ll look at them quickly just to be sure.  First, ADDON_LOADED:

      for i=1,table.getn(TthAog4MyBattleCries) do
        TthAog4OutputToChat(TthAog4MyBattleCries[i]);
      end

Now that we have multiple battle cries, we need to echo each one when the AddOn loads.  The for loop is just a way to execute a block of code multiple times.  The TthAog4OutputToChat function will be called once for each element in the table.  The variable i will take on values starting at 1 and ending at the last index in the table.  The way this works in that before calling TthAog4OutputToChat, Lua checks to see if “i” is bigger than the number of elements in the table, as returned by the table.getn function.  If the table is empty, “i” starts out bigger, so we never attempt to output any battle cries.  Good enough. 

Next, let’s look at PLAYER_REGEN_DISABLED:

    local numBattleCries = table.getn(TthAog4MyBattleCries);
    if (numBattleCries > 0) then
      local randomKey = math.random(numBattleCries);
      SendChatMessage(TthAog4MyBattleCries[randomKey], "SAY");
    end

This block first gets the number of elements in the table.  It then checks to see if there actually are any, or if the table is empty.  If the table is empty, there’s no battle cry to output so we’re done.  But if there are elements in the table, we call the math.random function.  This function returns a random value ranging from one to the value of the variable that we pass to the function, in this case the number of battle cries.  We then use this random value as an index into the table, to get the battle cry associated with that index.  Done.

There’s one last thing to mention.  Our old variable to hold the battle cry is gone now, having been replaced with the table, so we need to modify our .toc file accordingly.  All we have to do is change one line:

## SavedVariablesPerCharacter: TthAog4MyBattleCries

This causes the entire table to be saved to and restored from the per-character saved variable file.  We don’t have to do anything to handle individual table elements.  Lua and WoW handle all of that.  Here’s what our saved variable file looks like after I used our slash commands to clear the default battle cries then add two new ones:

TthAog4MyBattleCries = {
       [1] = "To Battle!",
       [2] = "ARRRGH!",
}

We’re done!  The complete code listings follow.  The files are also available for download <here>.

Here’s the .toc file.  The only notable change is the saved variables line.

## Interface: 11000
## Title: TenTonHammer.com Guide Part 4
## Author:  TenTonHammer.com
## Notes:  Makes your character scream a randomized battle cry when combat begins
## SavedVariablesPerCharacter: TthAog4MyBattleCries
TthAog4.xml

The .xml file had no changes of substance this round.

<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/">
       <Script file="TthAog4.lua"/>
       <Frame name="TthAog4Core">
              <Scripts>
                     <OnLoad>TthAog4Load();</OnLoad>
                     <OnEvent>TthAog4Event();</OnEvent>
              </Scripts>
       </Frame>
</Ui>

Finally, the .lua file, where most of the changes were made.

--  Constant strings
local TTH_AOG4_ADDON_NAME = "TthAog4";
local TTH_AOG4_BATTLE_CRY_ADDED = "Battle cry added:  ";
local TTH_AOG4_BATTLE_CRIES_ARE = "Battle cries are:  ";
local TTH_AOG4_BATTLE_CRIES_CLEARED = "Battle cries cleared.";
 
--  This table holds our battle cries.
TthAog4MyBattleCries = {
  "Not in the face!",
  "Not in the groin!",
  "Not in the kneecaps!"
};

--  Called when the AddOn loads.
function TthAog4Load()
  -- Register for events.
  this:RegisterEvent("PLAYER_REGEN_DISABLED");
  this:RegisterEvent("ADDON_LOADED");

  --  Create the slash commands.
  SlashCmdList["TTH_AOG4_ADDNEWBATTLECRY"] = TthAog4AddNewBattleCry;
  SLASH_TTH_AOG4_ADDNEWBATTLECRY1 = "/addnewbattlecry4";
  SLASH_TTH_AOG4_ADDNEWBATTLECRY2 = "/anbc4";

  SlashCmdList["TTH_AOG4_CLEARALLBATTLECRIES"] = TthAog4ClearAllBattleCries;
  SLASH_TTH_AOG4_CLEARALLBATTLECRIES1 = "/clearallbattlecries4";
  SLASH_TTH_AOG4_CLEARALLBATTLECRIES2 = "/cabc4";
end

--  Utility function for printing to chat area
function TthAog4OutputToChat(msg)
  if (DEFAULT_CHAT_FRAME) then
    DEFAULT_CHAT_FRAME:AddMessage(msg);
  end
end

--  Called when the associated slash command executes.
function TthAog4AddNewBattleCry(msg)
  -- Add the new battle cry, for use later.
  local newKey = table.getn(TthAog4MyBattleCries) + 1;
  TthAog4MyBattleCries[newKey] = msg;

  -- Echo the new battle cry for confirmation.
  local echo = TTH_AOG4_BATTLE_CRY_ADDED .. msg;
  TthAog4OutputToChat(echo);
end

--  Called when the associated slash command executes.
function TthAog4ClearAllBattleCries()
  --  assign to an empty table for quick and easy clearing.
  TthAog4MyBattleCries = {};

  -- Echo the new battle cry for confirmation.
  TthAog4OutputToChat(TTH_AOG4_BATTLE_CRIES_CLEARED);
end

--  Called to handle the events for which we’ve registered.
function TthAog4Event()
  if (event == "ADDON_LOADED") then
    if (arg1 == TTH_AOG4_ADDON_NAME) then
      TthAog4OutputToChat(TTH_AOG4_BATTLE_CRIES_ARE);
      for i=1,table.getn(TthAog4MyBattleCries) do
        TthAog4OutputToChat(TthAog4MyBattleCries[i]);
      end
    end
  elseif (event == "PLAYER_REGEN_DISABLED") then
    -- Send a random message to chat (if there are any to send)
    local numBattleCries = table.getn(TthAog4MyBattleCries);
    if (numBattleCries > 0) then
      local randomKey = math.random(numBattleCries);
      SendChatMessage(TthAog4MyBattleCries[randomKey], "SAY");
    end
  end
end

Now our AddOn is a little more interesting, but it is also a little bigger.  It is time to look at packages to help manage the AddOn’s growing complexity.  So when you’re ready, move on to Part Five!

Go to Part 5 - Packages!

WarlockWallpaper.jpg
Five classes that would be excellent additions to World of Warcraft.
Features
Fri, Jun 20, 2014
Mem
HotSAlphaLoad.jpg
Five things players should expect during the Alpha test of Warlords of Draenor.
Features
Fri, Jun 13, 2014
Mem
GG_25_02

I really don't understand racism in the real world. People are what people are, regardless of skin pigmentation or where their ancestors came from. There's really only one real-world race - the Human Race - and I loathe everyone equally.

Opinions
Mon, Jun 09, 2014
gunky
GarrisonTownHall.jpg
A basic guide to Garrisons in Warlords of Draenor.
Basics, Features, Guides
Fri, Jun 06, 2014
Mem

News from around the 'Net