WoW:Earth (AddOn)/Using earth to create a QuestLog

Earth (AddOn), a FrameXML library, is useful for rapidly creating Frames in WoW.

By using Earth, it is possible to create a fancy quest log rather quickly. I'll go through the simple steps here. I will assume you already know how to create an addon and fill-in an example .toc file. You should also know how to specify a dependency in the .toc. You should also know how to use getglobal().

SummaryEdit

The goal of this tutorial is to create a fancier quest log. Using the tools provided by Earth's Tree template, along with the functions available in Sea and Chronos (AddOn), we'll be able to show a complete list of the player's quests with very little trouble.

  • Coding Time: 1 hour
  • Tutorial Length: 2-3 hours

Creating the XML fileEdit

The first step is to create the XML file. Normally, I just copy my favorite AddOn's Ui tag, then work from there:

<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/ ..\FrameXML\UI.xsd">
 </Ui>

Once you have that, the next stage is to setup your localization and code files. Having an independent localization.lua file will make it exceptionally easier for the lovable Frenchies and swanky Swedes to translate your AddOn into their mother tongue.

This goes inside the Ui tag:

<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/ ..\FrameXML\UI.xsd">
 	<!-- Localization -->
 	<Script file="localization.lua"/>
 
 	<!-- Source -->
 	<Script file="PartyQuests.lua"/>
 	
 	<!-- All Future XML goes here! -->
 </Ui>

Now, because that looks ugly, I'm going to stop copy and pasting all of that and assume you can follow me. Good luck!

The next stage is to setup the window for the frame! Because we're inheriting from an Earth frame, we don't need to specify the default background texture. This works great for simple mods, like this quest log.


	<!-- Party Quests Frame -->

	<Frame name="PartyQuestsFrame" inherits="EarthFrameTemplate">
		<Size>
			<AbsDimension x="384" y="490"/>
		</Size>
		<Anchors>
			<Anchor point="CENTER">
				<Offset>
					<AbsDimension x="0" y="0"/>
				</Offset>
			</Anchor>
		</Anchors>
	</Frame>
 
The empty frame was created from that bit of code.

At this point, our Frame looks like this:

Woo hoo! Wasn't that easy? Okay, next we need to add an EarthTree. This will let us render information from a table to a nice, collapsable list (up to a decent limit).

First, we add a <Frames> tag to the PartyQuestsFrame. Then we stick the EarthTree frame inside of it.

	<Frames>
		<!-- Main Data View -->
		<Frame name="$parentTree" inherits="EarthTreeTemplate">
			<Anchors>
				<Anchor point="TOPLEFT" relativeTo="PartyQuestsFrame" relativePoint="TOPLEFT">
					<Offset>
						<AbsDimension x="20" y="-75"/>
					</Offset>
				</Anchor>
			</Anchors>
		</Frame>
	</Frames>

You may have noticed that I placed it at a very specific x/y set. I got those through experimentation. However, if you find yourself getting frustrated, I recommend you try the following trick:

	<Layers>
		<Layer level="BACKGROUND">
			<Texture name="$parentBackground" setAllPoints="true">
				<Color r="0" g="1" b="0" a="1.0" />
			</Texture>
		</Layer>
	</Layers>

Place that code snippet inside of the frame you are trying to position. This will force the entire frame to be colored green. You can then use this green box to help position the frame correctly and ensure your hit rectangles are 100% correct. You can disable it by setting a="0.0".

Now, for the next stage, we need to specify something to load the actual data into the EarthTree. So, first things first, we add the event handler to the PartyQuestsFrame.

		<Scripts>
			<OnLoad>
				PartyQuests_Frame_OnLoad();
			</OnLoad>
		</Scripts>

Starting the CodeEdit

Then, going into our PartyQuests.lua file, we create:

function PartyQuests_Frame_OnLoad()	
	local ebf = getglobal(this:GetName().."Tree".."ExpandButtonFrame");
	ebf:SetPoint("TOPLEFT", ebf:GetParent():GetName(), "TOPLEFT", 50, 1); 

	getglobal(this:GetName().."ExitButton"):Hide();
	getglobal(this:GetName().."TitleText"):SetText(PARTYQUESTS_TITLE_TEXT);
end

First, we move the +All frame a bit to the right, then hide the Exit Button and set the Title text. This just makes it look nicer. Next, we add the dummy function:

function PartyQuests_Frame_OnLoad()	
	local ebf = getglobal(this:GetName().."Tree".."ExpandButtonFrame");
	ebf:SetPoint("TOPLEFT", ebf:GetParent():GetName(), "TOPLEFT", 50, 1); 
 
	getglobal(this:GetName().."ExitButton"):Hide();
	getglobal(this:GetName().."TitleText"):SetText(PARTYQUESTS_TITLE_TEXT);
 
	local testTable = {1,2,3, {4, 5, {6,7}, 8}, 9};
	EarthTree_LoadTable(
		getglobal(this:GetName().."Tree"),
		testTable,
		{ onClick = function(a) Sea.io.printTable(a); end; }
	);
	EarthTree_UpdateFrame(
		getglobal(this:GetName().."Tree")
	); 
end

EarthTable_LoadTable will put the data into the tree. Once the data is inside we don't need to call LoadTable unless the data changes. Then EarthTree_UpdateFrame will draw the information to the screen. For the sake of speed, I'm going to pass on screenshotting this. Try it out!

Parsing the Player's Quest ListEdit

The next step is to create a table which contains all of the Quests a player has. This part is kind of complicated, but I am probably going to commit my function to Sea, so you don't have to worry about the details too much.

First thing, write the function to get all of the information from the game.

-- Generates the current player's quest list --
function PartyQuests_GetPlayerQuestTree()	
	-- Expand everything
	ExpandQuestHeader(0);

	-- Build our quest list
	local numEntries, numQuests = GetNumQuestLogEntries();

	local questList = {};
	local activeTable = nil;

	for i=1, PARTYQUESTS_QUESTS_DISPLAYED, 1 do
		local questIndex = i; 
		if ( questIndex <= numEntries ) then 
			local title, level, questTag, isHeader, isCollapsed, isComplete =  GetQuestLogTitle(questIndex);
			local color;
			if ( title ) then 
				if ( isHeader ) then
					questList[title] = {};
					activeTable = questList[title]; 
				else
					if ( activeTable == nil ) then 
						activeTable = questList; 
					end

					local entry = {}; 
					entry.title = title; 
					entry.level = level;
					entry.tag = questTag; 
					entry.complete = isComplete;

					table.insert(activeTable, entry);
				end
			end
		end
	end

	return questList;
end

Everything looks great, right? Uh oh! Whats this? We had to call ExpandQuestHeader to ensure we got all of the quests. This means the player's collapsed quests will be expanded. He won't like that, so first thing we do is create a function to keep track of all of his collapsed quests and a function to re-collapse them when done.

We also want to make sure no other function tries call us for an update while we're still parsing the information. (For you advanced programmers, I'm making sure my code is thread-safe). We add a bit called "PartyQuests_CurrentlyGettingQuestData" to ensure we can't call this funcion before it is finished.

-- Generates the current player's quest list --
function PartyQuests_GetPlayerQuestTree()
	if ( PartyQuests_CurrentlyGettingQuestData ) then 
		return true; 
	end 
	PartyQuests_CurrentlyGettingQuestData = true; 

 	-- Store the collapsed
 	local collapsed = PartyQuests_StoreCollapsedQuests();
	
	-- Expand everything
	ExpandQuestHeader(0);

	-- Build our quest list
	local numEntries, numQuests = GetNumQuestLogEntries();
	
	<SNIP>

	-- Collapse again
	PartyQuests_CollapseStoredQuests(collapsed);

	-- Unlock the thread
	PartyQuests_CurrentlyGettingQuestData = false;

	return questList;
end

Whew! Okay. Next we implement those two quick functions:

-- Stores the currently collapsed quests --
function PartyQuests_StoreCollapsedQuests()
	local collapsed = {};
	for i=1, PARTYQUESTS_QUESTS_DISPLAYED, 1 do
		local title, level, questTag, isHeader, isCollapsed, isComplete = GetQuestLogTitle(i);
		if ( isCollapsed ) then 
			table.insert(collapsed, title);
		end
	end
	
	return collapsed; 
end
-- Collapse the quests we stored --
function PartyQuests_CollapseStoredQuests(collapseThese) 
	for i=1, PARTYQUESTS_QUESTS_DISPLAYED, 1 do
		local title, level, questTag, isHeader, isCollapsed, isComplete = GetQuestLogTitle(i);
		if ( Sea.list.isInList(collapseThese, title) ) then 
			CollapseQuestHeader(i);
		end
	end
end

Displaying the Quest ListEdit

Hey, that wasn't so bad! Just a couple of loops. We've saved a lot of frustration from our user's end, too! Okay, so now we have a fancy function to get all of the quests for the current player. Let's use this instead of our dummy data.

function PartyQuests_Frame_OnLoad()	
	local ebf = getglobal(this:GetName().."Tree".."ExpandButtonFrame");
	ebf:SetPoint("TOPLEFT", ebf:GetParent():GetName(), "TOPLEFT", 50, 1); 
 
	getglobal(this:GetName().."ExitButton"):Hide();
	getglobal(this:GetName().."TitleText"):SetText(PARTYQUESTS_TITLE_TEXT);
 
	local questTree =  PartyQuests_GetPlayerQuestTree();
	EarthTree_LoadTable(
		getglobal(this:GetName().."Tree"),
		questTree,
		{ onClick = function(a) Sea.io.printTable(a); end; }
	);
	EarthTree_UpdateFrame(
		getglobal(this:GetName().."Tree")
	); 
end
Ick!! What's this? The questTree didn't load? Oh no! What happened?
A) The quest log isn't always available OnLoad, so we need to wait for a bit before getting the info.

We'll do this by moving all of the update code into another function:

function PartyQuests_LoadGui ()
	local frame = PartyQuestsFrame;
	local tree = PartyQuests_GetPlayerQuestTree();
	
	-- Load up the data
	EarthTree_LoadTable(
		getglobal(frame:GetName().."Tree"), 
		eTree
		); 

	-- Draw the data
	EarthTree_UpdateFrame(
		getglobal(frame:GetName().."Tree")
		); 
end
 
A table-accurate parsing of the data

Then we'll add something to the PartyQuests_Frame_OnLoad() function:

-- Event Handlers --
function PartyQuests_Frame_OnLoad()
	Chronos.afterInit(PartyQuests_LoadGui);
	
	local ebf = getglobal(this:GetName().."Tree".."ExpandButtonFrame");
	ebf:SetPoint("TOPLEFT", ebf:GetParent():GetName(), "TOPLEFT", 50, 1);

	getglobal(this:GetName().."ExitButton"):Hide();
	getglobal(this:GetName().."TitleText"):SetText(PARTYQUESTS_TITLE_TEXT);
end

Chronos.afterInit is a really nice tool, that lets us do something after the game-world has started. In our case, its populating our quest list and refreshing. Using code like this helps ensure we run into fewer errors, but we need to change all of the this references into frame references.

So reload and you'll see that will cause the EarthTree to parse our tree by type, as well as create sublists for all of the members of this table. Check out the result!

Not bad, eh?

Rendering Complex TreesEdit

However, we're no slaves to simplicity, we're real coders, out to make life easier for our users. So we persist on, to make it even better. The first step is to convert our data-tree into an enhanced tree. The enhanced tree format for Earth (AddOn) is pretty complicated, but you can find out more about it by reading the EarthTree.lua file.

Let's write a function to make an enhanced tree out of the simple one:

-- Converts Quest Trees into Enhanced Tree --
function PartyQuests_ConvertQuestTreeToEnhancedTree(questTree, funcList)
	local enhancedTree = {};
	for zone,questList in questTree do
		local zoneQuests = {};
		zoneQuests.title = zone; 
		zoneQuests.children = {};
		
		for k,quest in questList do 
			local entry = {};

			entry.title = quest.title; 
			entry.right = quest.tag;
			entry.tooltip = quest.level;
			entry.onClick = function (a) Sea.io.printTable(a) end;
			
			table.insert(zoneQuests.children,entry);
		end		
		
		zoneQuests.right = table.getn(zoneQuests.children); 
 		zoneQuests.tooltip = table.getn(zoneQuests.children);  

		table.insert(enhancedTree,zoneQuests);
	end 

	return enhancedTree;
end
 
Wow, it looks like a real quest log.

Then, change the function to use the LoadEnhanced call:

function PartyQuests_OnQuestClick(a) 
	-- Do something here
end
function PartyQuests_LoadGui ()
	local frame = PartyQuestsFrame;
	local tree = PartyQuests_GetPlayerQuestTree();
	local eTree = PartyQuests_ConvertQuestTreeToEnhancedTree(tree,
 	       {onClick=PartyQuests_OnQuestClick}
	);
	
	-- Load up the data
	EarthTree_LoadEnhanced(
		getglobal(frame:GetName().."Tree"), 
		eTree
		);

	-- Draw the data
	EarthTree_UpdateFrame(
		getglobal(frame:GetName().."Tree")
		); 
end

There. This is a lot to soak up, but basically, every entry in the Earth EnhancedTree format has an entry called "children", which determines what elements go inside of it. "title" is the main text, "right" is the extra text on the right, "tooltip" is what appears when you mouse it over and onClick is what happens when you click it! Here's the result:

Ooh! That was nice. However, it was kind of bland. All that white. So lets spice it up with some color. We're going to use the function the default QuestLog uses to set its colors. That will ensure we don't need to change our code if Blizzard changes theirs.

 
Description
-- Converts Quest Trees into Enhanced Tree --
function PartyQuests_ConvertQuestTreeToEnhancedTree(questTree, funcList)
	local enhancedTree = {};

	-- Loop through the zone list
	for zone,questList in questTree do
		local zoneQuests = {};
		zoneQuests.title = zone; 
		zoneQuests.children = {};
		
		-- Loop through the quest list
		for k,quest in questList do 
			local entry = {}; 

			entry.title = quest.title; 
			entry.right = quest.tag;
			entry.tooltip = quest.level;

			-- Lets add some color
			entry.titleColor = GetDifficultyColor(quest.level);
			
			-- Add the event handlers
			entry.onClick = function (a) Sea.io.printTable(a) end;
			
			table.insert(zoneQuests.children,entry);
		end		
		
		zoneQuests.right = table.getn(zoneQuests.children); 
		zoneQuests.tooltip = table.getn(zoneQuests.children); 

		table.insert(enhancedTree,zoneQuests);
	end 

	return enhancedTree;
end

Doesn't that look pretty? But it still not enough. We need to complete this masterpiece by improving upon the original. Let's make use of that spiffy localization.lua file. Open it up and add the following:

PARTYQUESTS_QUEST_TITLE = "\[%s\] %s";
PARTYQUESTS_QUEST_TITLE_TAG = "(%s)";

These are a couple of formatting strings that will help us make the Enhanced Tree look even better. "string.format" is another really nice function, that you can find more about over at http://www.lua.org, check out their documentation. These strings allow us to format those tags we'd like changed very quickly. Plus, if a different country prefers to use a different format, we can update it just by changing the localization.lua file.

 
Beautiful. Simply Beautiful!
-- Converts Quest Trees into Enhanced Tree --
function PartyQuests_ConvertQuestTreeToEnhancedTree(questTree, funcList)
	local enhancedTree = {}; 

	-- Loop through the zone list
	for zone,questList in questTree do
		local zoneQuests = {};
		zoneQuests.title = zone; 
		zoneQuests.children = {};
		
		-- Loop through the quest list
		for k,quest in questList do 
			local entry = {}; 
			
			-- Add the format strings here:
			
			-- Format it into [12] Quest of Danger
			entry.title = string.format(PARTYQUESTS_QUEST_TITLE, quest.level,  quest.title); 

			-- Pretty up the (Elite)
			if ( quest.tag ) then 
				entry.right = string.format(PARTYQUESTS_QUEST_TITLE_TAG, quest.tag); 
			end

			-- Add a simple tooltip
			entry.tooltip = quest.level;

			-- Lets add some color
			entry.titleColor = GetDifficultyColor(quest.level);
			
			-- Add the event handlers
			entry.onClick = function (a) Sea.io.printTable(a) end;
			
			table.insert(zoneQuests.children,entry);
		end		
		
		zoneQuests.right = table.getn(zoneQuests.children); 
		zoneQuests.tooltip = table.getn(zoneQuests.children); 

		table.insert(enhancedTree,zoneQuests);
	end

	return enhancedTree;
end

Beautiful. A work of art and it only took us about an hour to do. If you'd like to extend this work, you can set all of those onClick entries to do something more advanced than just print out a message. Likewise, you can make checkboxes show up by adding "checked=true" to each entry.

Rendering the Actual Quest TextEdit

If you wish to see how to create the actual QuestLog, check out HOWTO: Use Earth to create a QuestLog/Part 2. Its a bit more grizzly than this tutorial.

Wrap-upEdit

Well, that's all for this time. Stay tuned for next time, when I hope to tackle the mysteries of sending messages between two users via Sky (AddOn).

The code I used to create this tutorial can be found at: http://www.cosmosui.org/tutorials/EarthQuestLog_Example.zip