Skip to main content
  1. Writing/

OpenSpartan And The Quirks Of Halo Infinite API

This blog post is part of a series on exploring the Halo game API.

Overview #

Time for another update on my “whenever I have free time” project - OpenSpartan. I’ve done quite a few changes to the code base in preparation for the first public alpha release and there is now a final burndown of things to address before I can push the big red button and make the binaries available. And before I go deeper into the explanations here - keep in mind that I do not work on Halo. I do not know what the rationale for certain API decisions is. All I am doing is extrapolating some insights from my own observations that I’ve seen through the public Halo Infinite build and the associated web properties.

Alright, with that out of the way - part of the work I’ve been focused on has been dealing with the peculiarities of the Halo Infinite API and how it is split between game itself and Halo Waypoint API surfaces. When I talked about my discovery process for the Halo Infinite API I was talking about how I used Wireshark and Fiddler to try and “sniff” the requests that the game was sending to Halo Infinite services to get the necessary data. What many folks don’t know is that there are actually two distinct APIs - one that is used by the game itself, and the other one that you can access through the Halo Waypoint web experience. And quite interestingly, there are differences between the same data in Waypoint and game-specific APIs.

Dude, where are my medals? #

One such example is medals (that are already introduced in OpenSpartan). For example, let’s say that I play an Infection match. Depending on how well (or not so well) I perform, I will get a set of Infection-related medals.

Image of post-match results for an Infection round

Or, let’s say I did something unique in some of the latest games, where I acquired the “Blind Fire” medal, awarded for shooting an enemy from inside a Shroud Screen. I can also see it in my match results:

Image of post-match results containing the Blind Fire medal

None of these medals, however, show up when I go to Halo Waypoint:

Medals as seen rendered on the Halo Waypoint site

By the way, I am sad that the same can be said about the “Monopoly” medal. Seems like a pretty clear disparity between the two experiences. Let’s try to explain why this is happening.

Wait, but isn't the game storing the medal records? If they are missing from Halo Waypoint, does it also mean that I lost all the progress on them?

Indeed, the game is storing all the medals for your profile - it just so happens that Halo Waypoint doesn’t quite know about the missing medals yet. They are most definitely not lost. To prove this, let’s take a look at how we acquire match stats and the medals awarded after the end of the game. When you finish a match, you can acquire the match stats through the halostats endpoint, like this:

https://halostats.svc.halowaypoint.com
	/hi
	/matches
	/bf6e6746-c535-4cdf-bc2d-cbcfd5a75cba
	/stats

The GUID in this context is the match ID. The output will be a breakdown of all kinds of team and individual player stats, that will also include the list of awarded medals (I cut down chunks of the JSON blob below for brevity).

{
    "PlayerId": "MY_XUID",
    "PlayerType": 1,
    "BotAttributes": null,
    "LastTeamId": 3,
    "Outcome": 3,
    "Rank": 2,
    "ParticipationInfo": {
        [...]
    },
    "PlayerTeamStats": [
        {
            "TeamId": 3,
            "Stats": {
                "CoreStats": {
                    [...]
                    "Medals": [
                        {
                            "NameId": 88914608,
                            "Count": 1,
                            "TotalPersonalScoreAwarded": 0
                        },
                        {
                            "NameId": 87172902,
                            "Count": 1,
                            "TotalPersonalScoreAwarded": 0
                        },
                        {
                            "NameId": 2717755703,
                            "Count": 1,
                            "TotalPersonalScoreAwarded": 0
                        },
                        {
                            "NameId": 1025827095,
                            "Count": 1,
                            "TotalPersonalScoreAwarded": 0
                        }
                    ],
                    "PersonalScores": [
                        {
                            "NameId": 1024030246,
                            "Count": 9,
                            "TotalPersonalScoreAwarded": 900
                        },
                        {
                            "NameId": 4294967295,
                            "Count": 10,
                            "TotalPersonalScoreAwarded": 75
                        },
                        {
                            "NameId": 638246808,
                            "Count": 1,
                            "TotalPersonalScoreAwarded": 50
                        }
                    ],
                    [...]
                },
                "InfectionStats": {
                    "AlphasKilled": 2,
                    "SpartansInfected": 4,
                    "SpartansInfectedAsAlpha": 0,
                    "KillsAsLastSpartanStanding": 3,
                    "LastSpartansStandingInfected": 0,
                    "RoundsAsAlpha": 0,
                    "RoundsAsLastSpartanStanding": 1,
                    "RoundsFinishedAsInfected": 8,
                    "RoundsSurvivedAsSpartan": 0,
                    "RoundsSurvivedAsLastSpartanStanding": 0,
                    "TimeAsLastSpartanStanding": "PT9.2S",
                    "InfectedKilled": 4
                }
            }
        }
    ]
}

You can see this is an Infection match by the presence of the InfectionStats entity. Because I didn’t do too hot, I only earned a few medals in this match. Nonetheless, let’s pay close attention to NameId - this is the unique identifier of the medal that we can try and query from the medal metadata endpoint:

https://gamecms-hacs.svc.halowaypoint.com
	/hi
	/Waypoint
	/file
	/medals
	/metadata.json

This is where things start to get interesting. The medal metadata endpoint is actually not used by the game at all. It’s a convenient affordance for the web experience to get the list of medals, but it’s not the source of truth for the medals in the game. This might be a bit confusing, because if you look through the JSON response (I highlighted it in one of the previous blog posts) you will see a wealth of details about every available medal that you can imagine. Notably, however, some of the latest medals that were introduced since seasons 3 and 4 are missing.

From the match stats JSON response, I have four kinds of medals. Here is how they map to known medal names:

Name ID Medal Name Description Metadata Availability
88914608 N/A N/A
87172902 Odin’s Raven Detect 3+ enemies with a single Threat Sensor
2717755703 N/A N/A
1025827095 N/A N/A

Three out of four medals are missing metadata. Let’s see what that looks like if we take a peek at the post-game carnage report (PGCR):

Full example of medals shown in the post-game carnage report in Halo Infinite

The missing medals are Blight, Culling, and Sole Survivor. A bit of digging through experimentation and analysis of other games (look at what patterns emerge by playing the same game mode, some of them with more medals, some with less), we land on the following breakdown:

Name ID Medal Name Description Metadata Availability
88914608 Blight Kill 2 Survivors in quick succession
87172902 Odin’s Raven Detect 3+ enemies with a single Threat Sensor
2717755703 Sole Survivor Be the final Survivor in an Infection round
1025827095 Culling Kill 2 Infected in quick succession

OK, so it appears that the medals are not in the metadata API endpoint, but I see that other unofficial online resources that show my Spartan record show the medals. What gives?

It is very likely that said resources went through the exact same set of experiments to get the medal IDs! That is, they did not get them through an API endpoint but rather have manually indexed them from in-game experiences. The proper medal assets are embedded in the game and seems like not all of them are exposed through the metadata API. Given the fact that the Waypoint API is not really consumed by the local application, the direct impact on players is minimal, albeit it’s a bit inconvenient when it comes to tracking your own progress outside the game.

With OpenSpartan I’ve made the explicit decision to map one-to-one to the API - that is, you will only see medals there that are exposed through the medal metadata endpoint and its associated spritesheets.

A horse with no nameplate #

Another interesting aspect when it comes to “dual surface” APIs is related to player appearance, and specifically - emblems and nameplates. I ran into this problem fairly recently when testing OpenSpartan. I had my nameplate set to the NERF Needler one (along with the emblem, that comes in tandem):

Example of the nameplate and emblem

The correct emblem/nameplate combo is displayed in the bottom right, but that’s the game itself - it’s bound to display things correctly. OpenSpartan displayed the combo correctly too from the earliest builds:

Example of OpenSpartan displaying the player record

Then, the new set of events rolled in and I started playing Ranked Tactical. I was somewhat decent at it and climbed to Diamond 2, which awarded the respective emblem/nameplate combo:

Diamond nameplate and emblem

And that’s where OpenSpartan started failing. I was a bit baffled at first, especially considering that I haven’t really changed anything in the code. Then I started debugging step-by-step the service record acquisition process and spotted that the nameplate and emblem combo could not be found! How could it be? To get the visual representation of these assets, I followed the steps below.

First, get the player customization set up. This is done through a request to an Economy endpoint:

https://economy.svc.halowaypoint.com
	/hi
	/customization?players=PLAYER_XUID

This enables me to get all the currently equipped artifacts, including the emblem. Pay close attention to the Emblem entity below:

{
    "PlayerCustomizations": [
        {
            "Id": "PLAYER_XUID",
            "ResultCode": "Success",
            "Result": {
                "ArmorCores": {
                    "ArmorCores": [...]
                },
                "SpartanBody": {...},
                "Appearance": {
                    "LastModifiedDateUtc": {
                        "ISO8601Date": "2023-09-09T05:05:56.803Z"
                    },
                    "ActionPosePath": "Inventory/Spartan/ActionPoses/101-000-menu-stance-s-53a3641a.json",
                    "BackdropImagePath": "Inventory/Spartan/BackdropImages/103-000-ui-s2-backgro-81f964da.json",
                    "Emblem": {
                        "EmblemPath": "Inventory/Spartan/Emblems/104-001-s4-diamondsig-45a23b3a.json",
                        "ConfigurationId": 5502794
                    },
                    "ServiceTag": "UKRN",
                    "IntroEmotePath": null,
                    "PlayerTitlePath": null
                },
                "WeaponCores": {...},
                "AiCores": {...},
                "VehicleCores": {...}
            }
        }
    ]
}

The next step was to obtain the emblem setup from the emblem mapping endpoint:

https://gamecms-hacs.svc.halowaypoint.com
	/hi
	/Waypoint
	/file
	/images
	/emblems
	/mapping.json

A GET call to this endpoint returns all available emblem/nameplate combinations and the associated configurations, like this:

{
    "104-001-343other-343-cf7e1ba1": {
        "-582136935": {
            "emblemCmsPath": "images/emblems/104-001-343other-343-cf7e1ba1_n582136935.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-343-cf7e1ba1_n582136935.png",
            "textColor": "#FFFFFF"
        },
        "-968910011": {
            "emblemCmsPath": "images/emblems/104-001-343other-343-cf7e1ba1_n968910011.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-343-cf7e1ba1_n968910011.png",
            "textColor": "#000000"
        },
        "-2095749787": {
            "emblemCmsPath": "images/emblems/104-001-343other-343-cf7e1ba1_n2095749787.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-343-cf7e1ba1_n2095749787.png",
            "textColor": "#000000"
        },
        "1398757569": {
            "emblemCmsPath": "images/emblems/104-001-343other-343-cf7e1ba1_1398757569.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-343-cf7e1ba1_1398757569.png",
            "textColor": "#000000"
        },
        "-1708548354": {
            "emblemCmsPath": "images/emblems/104-001-343other-343-cf7e1ba1_n1708548354.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-343-cf7e1ba1_n1708548354.png",
            "textColor": "#000000"
        }
    },
    "104-001-343other-adre-5d4ea595": {
        "-1300612690": {
            "emblemCmsPath": "images/emblems/104-001-343other-adre-5d4ea595_n1300612690.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-adre-5d4ea595_n1300612690.png",
            "textColor": "#FFFFFF"
        },
        "-252222833": {
            "emblemCmsPath": "images/emblems/104-001-343other-adre-5d4ea595_n252222833.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-adre-5d4ea595_n252222833.png",
            "textColor": "#FFFFFF"
        },
        "-1391772538": {
            "emblemCmsPath": "images/emblems/104-001-343other-adre-5d4ea595_n1391772538.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-adre-5d4ea595_n1391772538.png",
            "textColor": "#FFFFFF"
        },
        "2092759023": {
            "emblemCmsPath": "images/emblems/104-001-343other-adre-5d4ea595_2092759023.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-adre-5d4ea595_2092759023.png",
            "textColor": "#FFFFFF"
        },
        "-1546654162": {
            "emblemCmsPath": "images/emblems/104-001-343other-adre-5d4ea595_n1546654162.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-adre-5d4ea595_n1546654162.png",
            "textColor": "#FFFFFF"
        },
        "802727239": {
            "emblemCmsPath": "images/emblems/104-001-343other-adre-5d4ea595_802727239.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-adre-5d4ea595_802727239.png",
            "textColor": "#FFFFFF"
        },
        "824330229": {
            "emblemCmsPath": "images/emblems/104-001-343other-adre-5d4ea595_824330229.png",
            "nameplateCmsPath": "images/nameplates/104-001-343other-adre-5d4ea595_824330229.png",
            "textColor": "#FFFFFF"
        }
    },
	[...]
}

The parent key contains the identity of the combination. In my case, that would be 104-001-s4-diamondsig-45a23b3a. Helpfully, every child item would be represented by a ConfigurationId that determines the color scheme (many can be available for a combo). Problem solved, right? Since from this point on I have all the .png files that I need. Well, as it turns out that was a bad assumption. This is yet another case of Halo Waypoint APIs being behind content-wise from what the game actually sees. 104-001-s4-diamondsig-45a23b3a, along with many other special emblem/nameplate combos (i.e., those you earn through ranking) are not available there.

This actually explains why I see this when I go to Halo Waypoint:

Example of a missing ranked nameplate and signum on Halo Waypoint

As a matter of fact, all other services that provide insights into a Halo Infinite service record are plagued by the same problem - there is simply no asset that they can show when the player’s appearance uses special emblems and nameplates.

Alright, so is this yet another case of the game having the assets, displaying them correctly, but it just so happens that the exact same assets are not listed through the API?

Yes and no! Yes in that the Halo Waypoint API doesn’t contain references to those assets. But there’s a better approach, and here we can do something that the game uses - rely on game CMS assets by reference. That is, follow the trail of breadcrumbs we’re given from the set of player customizations and then follow it to the right assets.

In the example above, the path to my emblem is Inventory/Spartan/Emblems/104-001-s4-diamondsig-45a23b3a.json. This is a reference to a configuration JSON file that I can acquire like this:

https://gamecms-hacs.svc.halowaypoint.com
	/hi
	/progression
	/file
	/Inventory
	/Spartan
	/Emblems
	/104-001-s4-diamondsig-45a23b3a.json

This request will give us something like this (the JSON is also cut down for brevity):

{
    [...]
    "AvailableConfigurations": [
        {
            "ConfigurationId": 5502794,
            "ConfigurationPath": "Configuration/Emblems/Coatings/1000-000-c3023945.json"
        }
    ],
    "CommonData": {
        "Id": "104-001-s4-diamondsig-45a23b3a",
        "HideUntilOwned": true,
        "Title": {
            "status": "Ready",
            "value": "Diamond Signum S4",
            "translations": {...}
        },
        "Description": {
            "status": "Ready",
            "value": "Champions thrive in the ebb and flow of a hard-fought battle.",
            "translations": {...}
        },
        [...]
        "DisplayPath": {
            "Width": 1024,
            "Height": 1024,
            "Media": {
                "MediaUrl": {
                    "AuthorityId": "",
                    "Path": "progression/Inventory/Emblems/Skill_S4_DiamondSignum_emblem.png",
                    "RetryPolicyId": "",
                    "TopicName": "",
                    "AcknowledgementTypeId": "NoAcknowledgement",
                    "AuthenticationLifetimeExtensionSupported": false,
                    "ClearanceAware": false
                },
                "MimeType": "image/png",
                "Caption": {
                    "status": "Test",
                    "value": ""
                },
                "AlternateText": {
                    "status": "Test",
                    "value": ""
                },
                "FolderPath": "progression/Inventory/Emblems",
                "FileName": "Skill_S4_DiamondSignum_emblem.png"
            }
        },
        [...]
    }
}

Oh what is that? Another path to a PNG inside MediaUrl? progression/Inventory/Emblems/Skill_S4_DiamondSignum_emblem.png seems like a promising lead. Let’s see what it yields.

Diamond signum for Season 4 of Halo Infinite

Bingo! We’ve successfully managed to acquire the right emblem. The problem around the nameplate still remains, though, and that’s not something that we can address at this time, unfortunately. As it turns out, nameplates are only available in the API through the Halo Waypoint mapping. If there is no entry in the mapping file, there is no nameplate. This is also made slightly more inconvenient by the fact that not every seasonal ranked emblem even has a PNG in the asset storage. That is, some of them aren’t shown even if you go down the path of /hi/progression/file/ because MediaUrl would be empty.

In OpenSpartan, I am mitigating that by showing a blank default:

Blank default nameplate as shown in OpenSpartan

It’s not ideal, but there is little to be done about this particular absence of assets.

Support for battle pass data #

I’ve rambled enough about API differences, so where does that put OpenSpartan progress? I’ve been optimizing more logic around data storage and querying (all local SQLite) and also added support for Battle Pass tracking. I haven’t yet implemented the differentiation between active and inactive Battle Passes, as well as what items you earned and are yet to earn, but that’s coming next.

Browsing the Battle Pass items in OpenSpartan

Next, it’s all about polish and optimizing some of the performance rough edges - like one memory leak caused by frame transitions. As I mentioned before, if you want to be among the first people to try the app when it hits production, go to the OpenSpartan homepage and put your email in the waitlist form.