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.
- Tools Used:
- 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>
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
- Author's Note: I've now added more advanced versions of these two functions to Sea.
- Check out Sea.wow.questLog.protectQuestLog() and Sea.wow.questLog.unprotectQuestLog().
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
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
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.
-- 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.
-- 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