Skip to main content
  1. Writing/

Enabling Hidden Maps And Game Modes In Halo Infinite

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

You might’ve seen over the course of the past couple of months since the Halo Infinite release some news and rumors about upcoming game modes in the game. I wondered how those folks got their hands on the new experience and just assumed that it’s typical data mining in game files to look for new assets.

That was until I really got hooked on the Halo Infinite story, started exploring the undocumented Halo Infinite API, and realized that the answer to my question was here all along! Behold, the key to hidden maps and game modes:

https://discovery-infiniteugc.svc.halowaypoint.com
  /hi
  /manifests
  /builds
  /6.10022.13411
  /game

Wait, what? This is it? A user generated content discovery endpoint that points to the current game manifest? Why yes indeed!

I should preface this by saying that everything you will read below relies on data made widely available by Halo Infinite’s very own API, and nothing below relies on you having to modify the client (the installed game) or “trick” any service in any capacity. This is not a vulnerability - you’re merely accessing public resources that already exist on the backend, just like any other game asset, that are not visible in the Halo Infinite user interface (UI). To access the API you need to use your own Xbox Live credentials, and you will only see results that your account is allowed to see.

Back to our discovery endpoint. This endpoint is special in that it returns lists of supported maps, game modes, and their associated engine variants. Let’s look at the condensed version of the response that one might get if they sent an authenticated GET request to that URL.

A screenshot showing a response to the GET request against the Halo Infinite manifest API

On its own, it’s not that interesting. Sure, it lists a number of game modes that are not available out-of-the-box, like Extraction, VIP, Infection, Grifball, Assault, Escalation, Paintball, and even the beloved Forge but that’s about it. We know they exist through this manifest.

Little did I know that getting to these game modes is entirely possible from the game. But first, let’s talk through the different entity types that are covered here. We have:

  • MapLinks - a bit self-explanatory, but this is basically a collection of maps that are supported in the game.
  • UgcGameVariantLinks - “UGC” is a fancy acronym for “user-generated content,” so this stands for authored game modes.
  • PlaylistLinks - I am assuming that this is for game playlists, but so far the results have been returning as an empty list.
  • EngineGameVariantLinks - engine script code that powers different game modes.

It didn’t click with me until I started exploring the rating and favoriting API that there is an opportunity to meld the two APIs for something fun. Specifically, the fun starts when it comes to maps and game variants.

Recall that in my earlier blog post I mentioned how when you favorite (or bookmark) an asset (a map or game variant) you need to pass two properties - an asset ID and its version, sending a PUT request like this:

https://authoring-infiniteugc.svc.halowaypoint.com
      /hi
      /players
      /xuid(PLAYER_XUID)
      /favorites
      /UgcGameVariants
      /f96f57e2-9f15-45c5-83ac-5775a48d2ba8

In the aforementioned request, you’d include the version in the body:

{
    "AssetVersionId": "2674c887-7aa1-42ab-a6cd-4a2c60611d0e"
}

Not only that, but as I was exploring a completely different API surface, I noticed that Halo Infinite is sending another interesting request when saving a copy of a game to my files.

Look at this:

When you actually save a copy, a POST request is sent to the following endpoint:

https://authoring-infiniteugc.svc.halowaypoint.com
  /hi
  /UgcGameVariants

The request contains binary data, encoded in Bond format, that contains the asset GUID, asset version GUID, admin user (which is your XUID - the Xbox User Identification string). I haven’t yet figured out the JSON representation, the same way I did for the rating and favoriting API, but that’s OK as it’s not relevant for now.

As I looked at this API request, along with my earlier research, I realized that maybe with the help of these request I can enable any of those game modes that are listed in the manifest without them being available in the game UI. Recall that by default, Halo Infinite does not expose the game modes that I am talking about above (Extraction, VIP, Infection, Grifball, Assault, Escalation, Paintball, and Forge):

The list above is not comprehensive, as 343 has developed more modes if you look at their author page, but you get the idea. The set is currently limited and nowhere near the volume of game modes you see in the manifest.

In the manifest, however, we have two key pieces of information that we could, at least in theory, use with the saving and favoriting API requests - the asset GUID and the version GUID. The challenge stems from the fact that we still have no idea what the data structure is that we need to send over the wire, just how it was decoded. Well, luckily for us, the data that is Bond-encoded is actually trivial to re-compose because the contents are not encrypted and the format used is tag-less - that is, we don’t need to name properties and have exact schema to be able to serialize or deserialize it.

Saving the POST request body I saw in fiddler to the /UgcGameVariants endpoint and passing it through my Bond decoder yielded the following result:

Example output for the Bond decoder with information passed from the Halo Infinite API

Because we have the data type, field ID, and the type of data captured in each field, we can now replicate this with the help of our good friends - C# and the Bond.CSharp package.

To get started, I’ll start with a very basic console app that contains the scaffolding for our Bond “envelope”:

[Bond.Schema]
class Entity
{
    [Bond.Id(0), Bond.Type(typeof(wstring))]
    public string Admin { get; set; }
    [Bond.Id(1), Bond.Type(typeof(Bond.Tag.structT))]
    public DummyEntity Dummy { get; set; }
    [Bond.Id(2)]
    public List<Asset> AssetDefinition { get; set; }
    [Bond.Id(6), Bond.Type(typeof(List<Bond.Tag.wstring>))]
    public List<string> Metadata { get; set; }
}

[Bond.Schema]
class DummyEntity
{
}

[Bond.Schema]
class Asset
{
    [Bond.Id(0)]
    public RawGuidDefinition AssetId { get; set; }
    [Bond.Id(1)]
    public RawGuidDefinition AssetVersion { get; set; }
}

[Bond.Schema]
class RawGuidDefinition
{
    [Bond.Id(0)]
    public UInt32 FragmentA { get; set; }
    [Bond.Id(1)]
    public UInt16 FragmentB { get; set; }
    [Bond.Id(2)]
    public UInt16 FragmentC { get; set; }
    [Bond.Id(3)]
    public UInt64 FragmentD { get; set; }
}

What the Microsoft Bond folks did really nicely in their C# wrapper for the protocol is expose all the schema customizations out of the box, so that I can define a class with the right attributes based solely on what I saw in my decoded Bond example. That’s what I did above.

I know that there is a “container” class, that is represented by Entity (I don’t actually know what that class is called on the Halo Infinite service side). Then said class carries the Admin property that maps to a Bond BT_WSTRING type - this property contains the XUID. There is a dummy container, as you can notice in the earlier screenshot that there is an empty BT_STRUCT. Once again - no idea what that is at this point, but it doesn’t matter. Then there is a list of what seems like asset definitions, so I called it respectively (AssetDefinition) that in turns contains the deconstructed GUIDs, where every chunk is represented by its own unsigned integer variation. Lastly, there seems to be an array of strings that contains the copied asset name, so I am referring to it as Metadata - again, names don’t matter here because in the serialized form they will be removed anyway.

In the case above, all properties are also mapped to their respective field IDs that I saw in the Bond-encoded request. So far so good - it seems that we have a close representation of what this needs to be.

To validate this assumption, I can now create a sample Entity and use a new function - SerializeData, to write everything to a file:

static void SerializeData()
{
    Entity entity = new Entity();
    entity.Admin = "xuid(MY_XUID)";
    entity.Dummy = new DummyEntity();
    entity.AssetDefinition = new List<Asset>();

    var asset = new Asset();

    var assetGuid = new Guid("2c66ea9a-7bb2-449b-8971-28522e2d0d8d");
    var reverseAssetGuid = assetGuid.ToByteArray();

    var versionGuid = new Guid("d2074ddb-62db-4680-b6df-f5c859318024");
    var reverseVersionGuid = versionGuid.ToByteArray();

    var assetId = GetRawGuid(reverseAssetGuid);
    var assetVersion = GetRawGuid(reverseVersionGuid);

    asset.AssetId = assetId;
    asset.AssetVersion = assetVersion;

    entity.AssetDefinition.Add(asset);

    entity.Metadata = new List<string>();
    entity.Metadata.Add("some-test-name-you-want");

    using (var stream = new FileStream("example.bin", FileMode.Create))
    {
        var output = new Bond.IO.Unsafe.OutputStream(stream);
        CompactBinaryWriter<Bond.IO.Unsafe.OutputBuffer> writer = new Bond.Protocols.CompactBinaryWriter<Bond.IO.Unsafe.OutputBuffer>(output, 2);
        Serialize.To(writer, entity);
        output.Flush();
    }
}

static RawGuidDefinition GetRawGuid(byte[] data)
{
    return new RawGuidDefinition()
    {
        FragmentA = BitConverter.ToUInt32(data.Take(4).ToArray()),
        FragmentB = BitConverter.ToUInt16(data.Skip(4).Take(2).ToArray()),
        FragmentC = BitConverter.ToUInt16(data.Skip(6).Take(2).ToArray()),
        FragmentD = BitConverter.ToUInt64(data.Skip(8).Take(8).ToArray())
    };
}

Let’s unpack what is going on here. I am creating a new Entity class and setting the appropriate properties (Admin for XUID and Metadata for the copied asset name), along with the “deconstructed” asset ID and version ID GUIDs for an asset I’ve discovered through the manifest. For the latter, I’ve built a rough helper function called GetRawGuid that will extract pieces from the GUID byte array and create unsigned integer variations.

Keep in mind that the API we’re using here is very picking about using the correct UgcGameVariant GUIDs and if you made a mistake it will make it very clear that it doesn’t like what you gave it.

Last but not least, I am serializing data to a file (example.bin). That’s it. That’s the secret.

I can validate the data by running the following snippet in my console application:

var ib = new Bond.IO.Unsafe.InputBuffer(File.ReadAllBytes(@"example.bin"));
var cbr = new CompactBinaryReader<Bond.IO.Unsafe.InputBuffer>(ib, 2);

ReadData(cbr);

This will parse the file with the version 2 of the Bond protocol (used by Halo Infinite services). You can read my earlier Bond blog post to see what ReadData does. Indeed, the result seems to match exactly what I saw in the response, albeit with different GUID values, since I am using a different asset.

Now to the fun part - how do we pass it to the service? Postman (or any other REST API client of your choice) to the rescue! Do this:

  1. Create a new request.
  2. Set the request type to POST.
  3. Set the request URL to:
    https://authoring-infiniteugc.svc.halowaypoint.com
      /hi
      /UgcGameVariants
    
  4. Set the X-343-Authorization-Spartan header to your Spartan token. You can read more about obtaining those in another blog post I wrote.
  5. Set the Accept header to application/json so that you can get a readable JSON blob containing information about the result of the API call.
  6. Set the Content-Type header to application/x-bond-compact-binary - we’re sending over Bond-encoded binary data.
  7. For the request body, select the format as binary and attach the example.bin file that we just created with the sample C# application.

With everything set up, and assuming that your Spartan token is valid and your generated Bond-encoded file contains all correct GUIDs for a UgcGameVariant, once you click on Send you should see a JSON response confirming that your asset was indeed created.

Requesting the Halo Infinite service to save a copy of an asset through Postman

Now, if you go to Halo Infinite, then Community and check My Files you should see the asset listed there.

From there, you can also start a Custom Game and even invite your Xbox friends to it!

Not every mode will work - for example, it seems that Forge in all its variants is no longer available to play through this method (while you still have access to other in-development modes).

What I also found interesting is that you can only add game variants that have a corresponding EngineGameVariant associated with it, which you can validate by using the content discovery endpoint, sending a GET request (authenticated with your own Spartan token, of course) such as:

https://discovery-infiniteugc.svc.halowaypoint.com
  /hi
  /ugcGameVariants
  /1e8cd10b-1496-423b-8699-f98f6f5db67e
  /versions
  /4155142e-b867-4030-a8a0-d5f44f9dce60

This, in turn, should produce a result such as the JSON snippet below, containing information about the associated EngineGameVariant:

{
    "CustomData": {
        "KeyValues": {}
    },
    "Tags": [
        "343i",
        "Arena",
        "Slayer"
    ],
    "EngineGameVariantLink": {
        "AssetId": "1cfee22b-513f-418d-a1d7-2648f1a575e0",
        "VersionId": "cc9603ca-63ce-4cd2-9071-0f966ce98d54",
        "PublicName": "Slayer",
        "Description": "Control Power Weapons to Eliminate the Enemy.",
        "Files": {
            "Prefix": "https://blobs-infiniteugc.svc.halowaypoint.com/ugcstorage/enginegamevariant/1cfee22b-513f-418d-a1d7-2648f1a575e0/cc9603ca-63ce-4cd2-9071-0f966ce98d54/",
            "FileRelativePaths": [],
            "PrefixEndpoint": {
                "AuthorityId": "iUgcFiles",
                "Path": "/ugcstorage/enginegamevariant/1cfee22b-513f-418d-a1d7-2648f1a575e0/cc9603ca-63ce-4cd2-9071-0f966ce98d54/",
                "QueryString": null,
                "RetryPolicyId": "linearretry",
                "TopicName": "",
                "AcknowledgementTypeId": 0,
                "AuthenticationLifetimeExtensionSupported": false,
                "ClearanceAware": false
            }
        },
[...More content following...]

The EngineGameVariant has a number of binary files tied to it that contain the scripting data regarding the mode along with all the asset information, that can be viewed if you request another piece of data from the content discovery endpoint:

https://discovery-infiniteugc.svc.halowaypoint.com
  /hi
  /engineGameVariants
  /0e5db09d-cc7f-4554-a402-860225b4bce7

The result is as follows:

{
    "CustomData": {
        "SubsetData": {
            "StatBucketGameType": 0,
            "EngineName": "",
            "VariantName": ""
        },
        "LocalizedData": {}
    },
    "Tags": [
        "343i"
    ],
    "AssetId": "0e5db09d-cc7f-4554-a402-860225b4bce7",
    "VersionId": "55fee0fb-cf7f-46e9-8885-94053f8c8b79",
    "PublicName": "Forge",
    "Description": "Forge your own maps!",
    "Files": {
        "Prefix": "https://blobs-infiniteugc.svc.halowaypoint.com/ugcstorage/enginegamevariant/0e5db09d-cc7f-4554-a402-860225b4bce7/55fee0fb-cf7f-46e9-8885-94053f8c8b79/",
        "FileRelativePaths": [
            "Default.bin",
            "Default.br",
            "Default.chs",
            "Default.cht",
            "Default.de",
            "Default.dk",
            "Default.en",
            "Default.fi",
            "Default.fr",
            "Default.it",
            "Default.jpn",
            "Default.kor",
            "Default.mx",
            "Default.nl",
            "Default.no",
            "Default.pl",
            "Default.pt",
            "Default.ru",
            "Default.sp",
            "Default_guid.txt"
        ],
[...More content following...]

Notice the .bin and localized files? That’s what I am talking about. It seems that once you enable a “hidden” game mode, the missing mode files are dynamically downloaded both for you and any of your Xbox Live friends that join your game. Truly living up to the “Halo-as-a-Service” moniker, where assets are dynamically fetched:

And this is the key: Halo Infinite will change with time. The team at 343 hopes they have begun a war that will last a decade. This is Halo-as-a-service, says Ross.

Now, you can’t add maps to your saved files, unfortunately, but we do have a bunch of MapLinks in the manifest we requested from the very beginning. To add them to your personal game collection, you can use the favoriting API and pass in the asset ID and version ID to have it listed in your bookmarks. Again - this requires you to use your very own Spartan token associated with your account.

Hidden maps showing in bookmarks

And that’s kind of it! Not all modes will work, and they are hidden for a reason - they are likely unfinished and buggy (for example, Infection doesn’t act like Infection just yet, and neither does Grifball), but VIP works a-OK. It’s a hit or miss, but that wasn’t the point of this exploration. I was mostly curious to see what I can do with this public, yet hidden in the UI game information. and I gotta say - I love the comments that developers set for the different game engines, such as this:

Description for the Infection game engine

Keep up the good work, folks at 343 (in case you’re reading this). I know it’s not an easy job maintaining a live-service game, but these little fun nuggets really make me enjoy the game beyond just playing the story and the base multiplayer.