Grunt Release On NuGet

Header for the blog post, with a Halo grunt in the background and text that says “Grunt Release On NuGet”

I’ll get to the point - Grunt is now officially available on NuGet. After some time analyzing the Halo Infinite APIs, I’ve finally put together what I think is a good wrapper that encapsulates most (not quite all, but more on that later) of the web APIs that are publicly available from the game, and you can use those today if you are a .NET developer.

NuGet download link for OpenSpartan.Grunt

NuGet download link for OpenSpartan.Grunt with download counter

All you need to do is run the following command from your PowerShell terminal:

Install-Package OpenSpartan.Grunt -Version 0.1.1

Or, if you are using the dotnet CLI:

dotnet add package OpenSpartan.Grunt --version 0.1.1

Does this work cross-platform? It absolutely does. Whether you are building on Linux, Windows, or macOS - it should just work.

The package currently only supports Halo Infinite, but I am aiming to add support for all major releases of Halo that are available on PC and that have a networking API.

And what’s great is that I am dogfooding the API myself as well! If you’ve seen my OpenSpartan Data Snapshots project, you might’ve noticed that I am regularly collecting play data around existing maps and matches. Well, the underlying tool that does the data collection is using Grunt!

Getting Started#

With the API client wrapped in HaloInfiniteClient, you can use it to access all available APIs that are covered by the settings endpoint.

Here is a C# example, that I am also using in the sample application that I lovingly call Grunt.Zeta to go through the entire authentication flow and get match stats:

ConfigurationReader clientConfigReader = new();
var clientConfig = clientConfigReader.ReadConfiguration<ClientConfiguration>("client.json");

XboxAuthenticationClient manager = new();
var url = manager.GenerateAuthUrl(clientConfig.ClientId, clientConfig.RedirectUrl);

HaloAuthenticationClient haloAuthClient = new();

OAuthToken currentOAuthToken = null;

var ticket = new XboxTicket();
var haloTicket = new XboxTicket();
var extendedTicket = new XboxTicket();

var xblToken = string.Empty;
var haloToken = new SpartanToken();

if (System.IO.File.Exists("tokens.json"))
{
    Console.WriteLine("Trying to use local tokens...");

    // If a local token file exists, load the file.
    currentOAuthToken = clientConfigReader.ReadConfiguration<OAuthToken>("tokens.json");
}
else
{
    currentOAuthToken = RequestNewToken(url, manager, clientConfig);
}

Task.Run(async () =>
{
    ticket = await manager.RequestUserToken(currentOAuthToken.AccessToken);
    if (ticket == null)
    {
        // There was a failure to obtain the user token, so likely we need to refresh.
        currentOAuthToken = await manager.RefreshOAuthToken(
            clientConfig.ClientId,
            currentOAuthToken.RefreshToken,
            clientConfig.RedirectUrl,
            clientConfig.ClientSecret);

        if (currentOAuthToken == null)
        {
            Console.WriteLine("Could not get the token even with the refresh token.");
            currentOAuthToken = RequestNewToken(url, manager, clientConfig);
        }
        ticket = await manager.RequestUserToken(currentOAuthToken.AccessToken);
    }
}).GetAwaiter().GetResult();

Task.Run(async () =>
{
    haloTicket = await manager.RequestXstsToken(ticket.Token);
}).GetAwaiter().GetResult();

Task.Run(async () =>
{
    extendedTicket = await manager.RequestXstsToken(ticket.Token, false);
}).GetAwaiter().GetResult();

if (ticket != null)
{
    xblToken = manager.GetXboxLiveV3Token(haloTicket.DisplayClaims.Xui[0].Uhs, haloTicket.Token);
}

Task.Run(async () =>
{
    haloToken = await haloAuthClient.GetSpartanToken(haloTicket.Token);
    Console.WriteLine("Your Halo token:");
    Console.WriteLine(haloToken.Token);
}).GetAwaiter().GetResult();

HaloInfiniteClient client = new(haloToken.Token, extendedTicket.DisplayClaims.Xui[0].Xid);

// Test getting the clearance for local execution.
string localClearance = string.Empty;
Task.Run(async () =>
{
    var clearance = (await client.SettingsGetClearance("RETAIL", "UNUSED", "222249.22.06.08.1730-0")).Result;
    if (clearance != null)
    {
        localClearance = clearance.FlightConfigurationId;
        client.ClearanceToken = localClearance;
        Console.WriteLine($"Your clearance is {localClearance} and it's set in the client.");
    }
    else
    {
        Console.WriteLine("Could not obtain the clearance.");
    }
}).GetAwaiter().GetResult();

// Try getting actual Halo Infinite data.
Task.Run(async () =>
{
    var example = await client.StatsGetMatchStats("21416434-4717-4966-9902-af7097469f74");
    Console.WriteLine("You have stats.");
}).GetAwaiter().GetResult();

This might look a little overly-complicated, but keep in mind that this sample goes from absolutely zero to getting match stats. Ideally, you would store the credentials, refresh them periodically, and just use the API to get all the fun Halo game stuff.

Moving forward, once you go through the authentication dance, you can call the API like this:

// Test getting the season data.
Task.Run(async () =>
{
    var seasonData = await client.GameCmsGetSeasonRewardTrack(
        "Seasons/Season7.json",
        localClearance);
    Console.WriteLine("Got season data.");
}).GetAwaiter().GetResult();

Whether the call was successful or not, you will get back an instance of HaloApiResultContainer that contains either the object with the data or an error object that will contain debug information.

Providing Feedback#

Because this is the 0.1.0 release, I very much am looking at getting as much feedback as possible. How easy is it to use the API? What’s missing? What could be done differently or better?

Let me know on GitHub or Twitter.

What’s Next#

This release is just the beginning. I have some plans on what I am going to focus on next, not in any order yet:

  • Node.js and Python implementation. This will enable a broader developer base for the API.
  • Better documentation. Right now it’s pretty rough, as it mostly contains comments from code that are included in the auto-generated pages. I aim to add more tutorials and samples.
  • Putting the API to use on OpenSpartan. Specifically, I want to find a better way to present the match data, medals, and player stats.
  • Better automation. I want to validate API structures and content returned for each endpoint quickly, so that I am not reliant on customer input to detect breaking changes.