Wednesday, October 16, 2019

Blizzard API

I play World of Warcraft and I wanted an easy way to spot battle pet auctions for battle pets I don't currently own. Blizzard has an extensive API for querying data from their games which is well documented. But I couldn't find an end-to-end walkthrough. Here it is.

You start by creating a battle.net account which is easy to get if you already have a Google or Facebook account, but they're not mandatory. If you already play one of their games you already have an account so you can skip the next step.

Create a battle.net account:

Create a Blizzard client:

  • Browse to https://develop.battle.net/access/clients
  • Logon with your Battle.Net account
  • Under Manage Your Clients, click Create New Client
  • Enter the client name - I used AuctionHouseAlerts
  • Check the "I do not have a service URL for this client" checkbox
  • Enter the intended use of the client. I used "Alert me when certain auctions become available"
  • Click [Create]

Manage the client:

  • Back at https://develop.battle.net/access/clients your client is listed.
  • Click on it. Make a note of your client id
  • Click [GENERATE NEW SECRET] - this is a temporary password
  • Specify how long you want the secret to last. I set mine to 12 months.
  • Click [GENERATE]
  • Make a note of your secret. The client id and secret are your user id and password for authentication.

Start new WPF, C# project

Call the project AuctionHouseAlerts, target any framework after 4.0 and use C#.
Using NuGet, install Newtonsoft.json.

Step #1 - oAuth2

All calls to the Blizzard APIs require a Token which is obtained from Blizzard using your client id and secret (see Manage the client, above). It's probably possible to use HttpRequest/Response to get the token, but it's easier to use curl. Curl is a command line utility that can be run in a process. It returns json which can be captured from stdout and parsed to extract the token.

Start by making MainWindow.xaml.cs look like this. Make sure you populate ClientID and Secret from your Blizzard client information. Note I am defining class members as strings unless I really need something else - this reduces potential json parsing issues and this isn't a json blog entry.

using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Windows;
using System.IO;
using System.Net;
using System.Collections.Generic;
using System.Linq;

namespace AuctionHouseAlerts
{
     public partial class MainWindow : Window
    {
        private const string ClientID = "Your Client ID";
        private const string Secret = "Your secret";

        public class cToken
        {
            public String access_token;
            public String token_type;
            public String expires_in;
        }

        public MainWindow()
        {
            String Token = GetToken();
            InitializeComponent();
        }

        public String GetToken()
        {
            String json = "";
            String command = string.Format("-u {0}:{1} -d grant_type=client_credentials https://us.battle.net/oauth/token", ClientID, Secret);
            Process process = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    FileName = "curl",
                    Arguments = command,
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    CreateNoWindow = true
                }
            };

            process.Start();
            while (!process.StandardOutput.EndOfStream)
            {
                json += process.StandardOutput.ReadLine();
            }

            cToken root = JsonConvert.DeserializeObject<cToken>(json);
            return root.access_token;
        }
    }
}

The GetToken method uses curl to get a token from Blizzard and parses the token value out of the returned json. If you put a break point on InitializeComponent you will see the token is a string of random characters.

Step #2 - Getting Auctions

Before we can get the auction items we need to know what URL to call. We do that by calling another URL (yes, it's a bit confusing).

After the definition of the secret const, add the realm name we are searching on. For example...
private const string Realm = "Icecrown";

In the MainWindow constructor, add the line.
String AuctionURI = GetAuctionURI(Token);
After the definition of cToken, add the definition of the auction URL.

public class cFile
{
    public String url;
    public String lastModified;
}

public class cAuctionURL
{
    public List<cFile> files;
}

and add the GetAuctionURI method definition in the class somewhere.

 public String GetAuctionURI(String token)
{
    String URI = string.Format("https://us.api.blizzard.com/wow/auction/data/{0}?locale=en_US&access_token={1}", Realm, token);
    HttpWebRequest request = WebRequest.CreateHttp(URI);
    String json;

    using (Stream s = request.GetResponse().GetResponseStream())
    {
        using (StreamReader sr = new StreamReader(s))
        {
            json = sr.ReadToEnd();
        }
    }

    cAuctionURL root = JsonConvert.DeserializeObject<cAuctionURL>(json);
    return root.files[0].url;
}

If you run to your breakpoint now you will see the auction URL looks a bit like this.
http://auction-api-us.worldofwarcraft.com/auction-data/04167a8875c2972895944a696810c74e/auctions.json

Now we can make the actual call to get the auctions. I only defined getters on the members I plan on binding to.

Add a new class called cAuction.

        public class cAuction
        {
            public String auc;
            public String item { get; set; }
            public string petName { get; set; }
            public String owner { get; set; }
            public String ownerRealm;
            public long bid { get; set; }
            public long buyout { get; set; }
            public long quantity { get; set; }
            public String timeleft { get; set; }
            public String rand;
            public String seed;
            public String context;
            public String petSpeciesId { get; set; }
            public int petBreedId { get; set; }
            public String petBreed { get; set; }
            public String petLevel { get; set; }
            public int petQualityId { get; set; }
            public String petQuality { get; set; }
            public String gender;
        }

        public class cRealm
        {
            public String name;
            public string slug;
        }

        public class cAuctions
        {
            public List<cRealm> realms { get; set; }
            public List<cAuction> auctions { get; set; }
        }

In the MainWindow class, add a property that defines a list of cAuction. The name will make more sense later.

public List<cAuction> uniquePetAuctions { get; set; }

Populate uniquePetAuctions in the constructor.

uniquePetAuctions = GetAuctions(AuctionURI).auctions;


Write the GetAuctions method.

        public cAuctions GetAuctions(String AuctionURI)
        {
            cAuctions auctions = new cAuctions();

            HttpWebRequest request = WebRequest.CreateHttp(AuctionURI);
            String json;

            using (Stream s = request.GetResponse().GetResponseStream())
            {
                using (StreamReader sr = new StreamReader(s))
                {
                    json = sr.ReadToEnd();
                }
            }

            cAuctions root = JsonConvert.DeserializeObject<cAuctions>(json);

            return root;
        }
If you run to your breakpoint now you will see that uniquePetAuctions contains a large number of auction details. It's time to bind a datagrid.

Replace MainWindow.XAML with this.

<Window x:Class="AuctionHouseAlert.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace: AuctionHouseAlert "
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <DataGrid ItemsSource="{Binding uniquePetAuctions}" IsReadOnly="True"/>
    </Grid>
</Window>

Raw auction house data
The next step is to only list battle pet auctions - the easiest way to do this is to filter where ItemId=82800 (battle pet cage).

Step #3 - Filtering Battle Pet Auctions

Change the call to GetAuctions to this...
uniquePetAuctions = GetAuctions(AuctionURI).auctions.FindAll(a => a.item=="82800");

Step #4 - Filtering out Battle Pets our account already has

We need to get a list of all Battle Pets our account already has. We do this by passing one of our character's names to the correct URL.

Add another constant containing our character's name

private const string Character = "Gandalf";

Add the collected pets class definitions

        public class cPetStats
        {
            public string speciesId;
            public string breedId;
            public string petQualityId;
            public string level;
            public string health;
            public string power;
            public string speed;
        }

        public class cCollectedPet
        {
            public string name;
            public string spellId;
            public string creatureId;
            public string itemId;
            public string qualityId;
            public string icon;
            public cPetStats stats;
            public string battlePetGuid;
            public string isFavorite;
            public string isFirstAbilitySlotSelected;
            public string isSecondAbilitySlotSelected;
            public string isThirdAbilitySlotSelected;
            public string creatureName;
            public string canBattle;
        }

        public class cPetCollection
        {
            public string numCollected;
            public string numNotCollected;
            public List<cCollectedPet> collected;
        }

        public class cCharacterPets
        {
            public string lastModified;
            public string name;
            public string realm;
            public string battlegroup;
            public string @class;
            public string race;
            public string gender;
            public string level;
            public string achievementPoints;
            public string thumbnail;
            public string calcClass;
            public string faction;
            public cPetCollection pets;
        }

In the main window constructor add a list of pet ids we already own and call a method to populate it.
List<String> CollectedPets = GetPetCollection(Token);

The method is defined as...
public List<String> GetPetCollection(String token)
{
    String URI = string.Format("https://us.api.blizzard.com/wow/character/{0}/{1}?locale=en_US&access_token={2}&fields=pets", Realm, Character, token);
    HttpWebRequest request = WebRequest.CreateHttp(URI);
    String json;
    cCharacterPets cp;

    using (Stream s = request.GetResponse().GetResponseStream())
    {
        using (StreamReader sr = new StreamReader(s))
        {
            json = sr.ReadToEnd();
        }
    }

    cp = JsonConvert.DeserializeObject<cCharacterPets>(json);
    return cp.pets.collected.Select(p => p.stats.speciesId).ToList();
}

Now we have a list of pet ids our character (account) has already collected, we can filter them out of the results. While we're at it, we can sort the results into ascending buyout order. Change the GetAuctions line to look like this.

uniquePetAuctions = GetAuctions(AuctionURI).auctions.FindAll(a => a.item=="82800" && !CollectedPets.Any(cp => cp == a.petSpeciesId)).OrderBy(a => a.buyout).ToList();

Battle pet auctions for pets I don't own - cheapest first

Step #5 - Populating pet names, replacing IDs with descriptions, and formatting currency

There is a simple call that returns a list of species ids and names which we can use to populate the petName column.

Define cPetName and add a line to the constructor.

        public class cPetName
        {
            public string id;
            public string name;
        }

        public class cPetIndex
        {
            public List<cPetName> pets;
        }

List<cPetName> PetNames = GetPetNames(Token);

GetPetNames is defined thus.

public List<cPetName> GetPetNames(string token)
{
    String URI = string.Format("https://us.api.blizzard.com/data/wow/pet/index?namespace=static-us&locale=en_US&access_token={0}", token);
    HttpWebRequest request = WebRequest.CreateHttp(URI);
    String json;
    cPetIndex petIndex;

    using (Stream s = request.GetResponse().GetResponseStream())
    {
        using (StreamReader sr = new StreamReader(s))
        {
            json = sr.ReadToEnd();
        }
    }

    petIndex = JsonConvert.DeserializeObject<cPetIndex>(json);
    return petIndex.pets;
}

We need to replace Quality and Breed Ids with descriptions. Rather than the commonly used B/B format I have chosen to indicate the weights of health, power, and speed explicitly ie 50H/50P/50S. While we're doing this we will take the opportunity to only list the pet with the cheapest buyout for each unique pet/breed.

Replace the contents of the MainWindow constructor with this.

public MainWindow()
{
    String Token = GetToken();
    String AuctionURI = GetAuctionURI(Token);
    List<String> CollectedPets = GetPetCollection(Token);
    List<cPetName> PetNames = GetPetNames(Token);
    List<cAuction> petAuctions;
    List<String> PetQuality = new List<string>() { "Poor", "Common", "Uncommon", "Rare" };
    List<String> PetBreed = new List<string>() { "", "", "", "50H/50P/50S", "200P", "200S", "200H", "90H/90P", "90P/90S", "90H/90S", "45H/90P/45S", "45H/45P/90S", "90H/45P/45S" };

    petAuctions = GetAuctions(AuctionURI).auctions.FindAll(a => a.ownerRealm == Realm && a.petLevel != null && !CollectedPets.Any(cp => cp == a.petSpeciesId)).OrderBy(a => a.buyout).ToList();
    uniquePetAuctions = new List<cAuction>();
    foreach (cAuction auction in petAuctions)
    {
        if (auction.petBreedId > 12)
        {
            auction.petBreedId -= 10;
            auction.gender = "F";
        }
        else
            auction.gender = "M";

        if (!uniquePetAuctions.Any(p => p.petSpeciesId == auction.petSpeciesId && p.petBreedId == auction.petBreedId))
        {
            auction.petName = PetNames.First(p => p.id == auction.petSpeciesId).name;
            auction.petQuality = PetQuality[auction.petQualityId];
            auction.petBreed = PetBreed[auction.petBreedId];
            uniquePetAuctions.Add(auction);
        }
    }

    InitializeComponent();
}

Now we need to convert the bid and buyout values from copper to gold/silver/copper. We will do this with a converter so they will still sort correctly.

Add a new class to the project called Converters and change the code to this...

using System;
using System.Globalization;
using System.Windows.Data;

namespace AuctionHouseAlerts
{
    class CurrencyConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            // coppers to gsc
            long totalCoppers = long.Parse(value.ToString());
            long c = totalCoppers % 100;
            long totalSilvers = (totalCoppers - c) / 100;
            long s = totalSilvers % 100;
            long g = (totalSilvers - s) / 100;
            string result = g + "g ";
            if (c > 0 || s > 0)
            {
                result += s + "s ";
                if (c > 0)
                    result += c + "c ";
            }
            return result;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Now lets clean up the XAML and use our new converter. Change the XAML to this...

<Window x:Class="AuctionHouseAlerts.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AuctionHouseAlerts"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:CurrencyConverter x:Key="CurrencyConverter"/>
    </Window.Resources>
    <Grid>
        <DataGrid ItemsSource="{Binding uniquePetAuctions}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Pet Name" Binding="{Binding petName}"/>
                <DataGridTextColumn Header="Owner" Binding="{Binding owner}"/>
                <DataGridTextColumn Header="Bid" Binding="{Binding bid, Converter={StaticResource CurrencyConverter}}"/>
                <DataGridTextColumn Header="Buyout" Binding="{Binding buyout, Converter={StaticResource CurrencyConverter}}"/>
                <DataGridTextColumn Header="Quantity" Binding="{Binding quantity}"/>
                <DataGridTextColumn Header="Time Left" Binding="{Binding timeleft}"/>
                <DataGridTextColumn Header="Pet Breed" Binding="{Binding petBreed}"/>
                <DataGridTextColumn Header="Pet Level" Binding="{Binding petLevel}"/>
                <DataGridTextColumn Header="Pet Quality" Binding="{Binding petQuality}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Battle pet auctions that I don't have showing names, quality, breed, and formatted bid and buyouts


No comments:

Post a Comment