|
|
Friday, December 12, 2008 |
Using the WoW Armory to determine class talents
Posted: 5:11:00 PM
|
First of all, my apologies for not posting in over two months. My main focus has been working on the Six Minutes To Release website and related functions. Believe me when I say I've poured a lot of time into this.
On the heels of my LibBeImba project, I needed a more robust way to access the World of Warcraft Armory than I have been on the website. My research led me to the fact that they are using XML and XLT files to create their web pages, thus leaving the XML data exposed. In addition, using Fiddler, I have been able to find additional files behind the scenes that allow me to mine even more XML data. Serializing the XML in lieu of an XSD and then presenting the information in a developer-friendly format has led me to create the LibWowArmory project, still in development.
So today I went to take a look at the talents, figuring they were in XML as well. Well, this:
<talentTree value="503501523201322531025012511400200000000000000000000050000000000000000000000000"/>
isn't exact useful data. Turns out that string is parsed via JavaScript, and the talent calculator, instead of being created by XML and XLT, is generated on the client side via JavaScript. This included all of the static data, including the names of the talent trees, the individual talents, and the actual tooltip text you get when you mouse over the talents on the page. Very unArmorylike. And very inaccessable from VB.Net. At least, initially.
A while back I had done a bit of work with compiling .Net assemblies on the fly, and had began to wonder if I could somehow take the JavaScript that Blizzard was using, convert it to some .Net-equivalent language, and mine the data through reflection. After looking into JScript.Net and doing a refresher on Reflection, it only took me a couple of hours to come up with this code:
Public Sub Talents(ByVal strClass As String)
' Gather the source code
Dim lstSource As New List(Of String)
lstSource.Add("var jsLoaded; var document; var pointsTier = new Array(); var templateString; var reqTalentID; var reqTalentPoints; var variableIsSite; var maxRank; var theUpdatedRank; var showTip; var hideTip; var textNextRank; var textPoint; var textPoints;")
Using wcClient As New System.Net.WebClient
lstSource.Add(wcClient.DownloadString("http://www.wowarmory.com/shared/global/talents/includes/variables-live.js"))
lstSource.Add(wcClient.DownloadString("http://www.wowarmory.com/shared/global/talents/includes/functions.js"))
lstSource.Add(wcClient.DownloadString(String.Format("http://www.wowarmory.com/shared/global/talents/{0}/data.js", strClass.ToLower.Replace(" ", ""))))
lstSource.Add(wcClient.DownloadString(String.Format("http://www.wowarmory.com/shared/global/talents/{0}/donotlocalize.js", strClass.ToLower.Replace(" ", ""))))
End Using
' Compile the code
Dim cdpProvider As CodeDomProvider = CodeDomProvider.CreateProvider("JScript")
Dim cpParams As New CompilerParameters()
cpParams.GenerateInMemory = True
cpParams.GenerateExecutable = True
Dim crResults As CompilerResults = cdpProvider.CompileAssemblyFromSource(cpParams, lstSource.ToArray())
' Run the main module of the assembly
Dim lstArgs = New List(Of Object)
lstArgs.Add(Nothing)
Dim objReturn As Object = crResults.CompiledAssembly.EntryPoint.Invoke(Nothing, BindingFlags.Static, Nothing, lstArgs.ToArray(), Nothing)
' Get the type in the assembly that contains the data we want
Dim tType As Type = (From t In crResults.CompiledAssembly.GetTypes() Where t.Name = "JScript 0").First()
' Get the tree names
Dim fiTrees As FieldInfo = (From f In tType.GetFields() Where f.Name = "tree").First()
Dim lstTrees As List(Of Object) = CType(Unbox(fiTrees.GetValue(Nothing)), List(Of Object))
' Get the talents
Dim fiTalents As FieldInfo = (From f In tType.GetFields() Where f.Name = "talent").First()
Dim lstTalents As List(Of Object) = CType(Unbox(fiTalents.GetValue(Nothing)), List(Of Object))
' Get the rank descriptions (some of these are in HTML format)
Dim fiRanks As FieldInfo = (From f In tType.GetFields() Where f.Name = "rank").First()
Dim lstRanks As List(Of Object) = CType(Unbox(fiRanks.GetValue(Nothing)), List(Of Object))
' Get the non localizable tree names... used to mine images like so:
' String.Format("http://www.wowarmory.com/shared/global/talents/{0}/images/armory/{1}/background.jpg", strClass.ToLower().Replace(" ", ""), strTree.ToLower().Replace(" ", ""))
Dim fiNLTrees As FieldInfo = (From f In tType.GetFields() Where f.Name = "nltree").First()
Dim lstNLTrees As List(Of Object) = CType(Unbox(fiNLTrees.GetValue(Nothing)), List(Of Object))
' Get the non localizable talents... used to mine images like so:
' String.Format("http://www.wowarmory.com/shared/global/talents/{0}/images/armory/{1}/{2}.jpg", strClass.ToLower().Replace(" ", ""), strTree.ToLower().Replace(" ", ""), strTalent.ToLower().Replace(" ", ""))
' String.Format("http://www.wowarmory.com/shared/global/talents/{0}/images/armory/{1}/{2}-off.jpg", strClass.ToLower().Replace(" ", ""), strTree.ToLower().Replace(" ", ""), strTalent.ToLower().Replace(" ", "").Replace(":", ""))
Dim fiNLTalents As FieldInfo = (From f In tType.GetFields() Where f.Name = "nltalent").First()
Dim lstNLTalents As List(Of Object) = CType(Unbox(fiNLTalents.GetValue(Nothing)), List(Of Object))
' TODO: Cache lists here for future use
End Sub
Private Function Unbox(ByVal obj As Object) As Object
If TypeOf obj Is ArrayObject Then
Dim aoObj = CType(obj, ArrayObject)
If CInt(aoObj.length) = 0 Then
Return New List(Of Object)
Else
Dim arr As Array = CType(Microsoft.JScript.Convert.ToNativeArray(aoObj, Type.GetTypeHandle(New Object)), Array)
Dim lstReturn As New List(Of Object)
For Each o As Object In arr
lstReturn.Add(Unbox(o))
Next
Return lstReturn
End If
Else
Return obj
End If
End Function
There's a bit to talk about in here, so I'll go ahead and explain the code.
First, JScript.Net differs from JavaScript in that all variables must be defined. That's essentially what I'm doing with that big long line just after declaring lstSource. All of those variables need to be declared, otherwise the code will not compile. Interesting to note that "document" needs to be declared, since we're not running from within the scope of a web page. Fortunately, I'm not doing anything with the document object, so I'm not going to run into any runtime errors accessing the document.
Next, I download all of the necessary code from Blizzard. The second one has a lot of useless functions, but it does have a couple functions that are used in the last two. Those two files contain all of the talents for the specified class.
The next section compiles the code using the JScript CodeDomProvider. The following section actually runs the main module of the assembly, instantiating all the variables and allowing the functions to run. Most of Blizzard's code is written in the global scope, so most of that code runs. This is where the data from the data.js and donotlocalize.js files are put into memory.
Now we have to use reflection to get the type. After some debugging, I found that the type named "JScript 0" is the one with the data I'm interested in. So, using LINQ, I grab that type from the assembly.
Now we get to pull the data we want. I get the FieldInfo object from the type, again using reflection to make it easy to cherry pick the correct field. Then I can use FieldInfo.GetValue to grab my value and...
Not so fast.
Because the assembly I'm pulling from is in JScript.Net, arrays are not of type System.Array, but rather Microsoft.JScript.ArrayObject. This is a monolithic array type that really doesn't let you play around much with it. Trying to LINQ from it doesn't give you the data, you actually have to call the data through the Item property.
Fortunately, JScript.Net provides a way to change ArrayObjects to normal Arrays, via the ToNativeArray function. I packaged this all up into a recursive function called Unbox. This takes an object and spits the object back out if it's not an ArrayObject. However, if it is an ArrayObject, it converts it into a List(Of Object) using some trickery. Note that it tries to unbox each object that it inserts into the list. This is because Blizzard likes nesting arrays, so I needed to make sure that every ArrayObject inside is put into a more friendly list object.
The idea is to cache this data so I don't have to spam Blizzard for their JavaScript files every time I need to access a player's talents, something I already have a bit of a framework setup to do in LibWowArmory.
The result is I can now take that very ugly talentTree XML node, split the string up into individual characters, and assign each of those numbers to each of the character's talents to learn exactly what their build looks like in code.
All in all, I'm pretty happy with my work on this, and plan to release LibWowArmory soon with item search and lookup, as well as player search, lookup, and talent inspection. Further, I am now able to apply the power of the Armory to my own website in an easy-to-use library in what should be a write-and-forget library, much like LibBeImba turned out to be.
I would be doing this post an injustice if I didn't talk about how the guild is doing. So far, we've cleared about half of Naxxramas 10 as people are still working on leveling up. We've also taken out Sartharion 10 with no drakes, and are farming Archavon 10 when we can as well. Wrath of the Lich King brought some great content into the game, succeeding in many places where, in my opinion, Burning Crusade just failed miserably. I'm really happy with this expansion so far, as is Kathy, and it's something we continue to enjoy together, something that's more important to us than the game itself.Labels: Coding, Fiddler, Gaming, VB.NET, World of Warcraft
0 Comments
|
|