Unlocking All Halo Infinite Content In Your Build

Why do things manually when you can spend a day writing a PowerShell script.

By Den in Hackery

July 25, 2022

NOTE

This post is part of a series about the Halo Infinite Web API.

You can read more about the authentication process to get started with the API endpoints right away.

You can also explore the .NET wrapper for the API that makes endpoint interaction a bit easier.

If you’ve been following some of my recent work, you might’ve caught my latest blog post on enabling hidden game modes and maps in Halo Infinite. Well, clearly my curiosity got the best of me, because this post is very much a continuation of that story.

The gist of my realization at the time was that you can actually enable the content that is available in the build (or even not in the build, but you need to know the magic GUID combinations) by bookmarking or copying it into your file browser. The process that I came up with boiled down to creating a Microsoft Bond envelope that mimics the API requests issued by the game, with GUIDs being substituted for the desired content.

The process would be very tedious if you’d ever want to, let’s say, enable literally everything that is declared in the build manifest. There are about 256 game variants and 95 map variations enabled in the latest production build. That’s a lot of one-off envelope re-creation and POST-ing to the “Save A Copy” API endpoint. What if I could automate this process?

And automate this process I did! I decided to write a PowerShell script for several reasons:

  1. Code is easy to modify without programming tools installed.
  2. Code is easy to understand at a glance.
  3. No need to deal with compilation and binaries.
  4. I can still wrap all the .NET niceties for Bond serialization.
  5. User get to avoid the need to download and run binaries from random people on the Internet (myself).

The script is very simple in that it only does a few things:

  1. Reads your Spartan V4 token as an argument. The easiest way to get the token (I did not write the authentication flow into the script) that is by looking at the Halo Waypoint site through the lens of your browser’s Network Inspector and copying the value of the x-343-authorization-spartan header. That way you don’t have to deal with Fiddler or other tools to inspect game traffic (although you can if you want to).
  2. Reads your Xbox User ID (XUID) as an argument. Your XUID is effectively a string identifier in the format of xuid(NUMERIC_ID) that represents your account on the Xbox Live network. There are tools online that allow you to convert your gamertag into a XUID, and there are also Xbox Live APIs for this purpose, that I will dive into in another post (it’s a lengthy process to describe).
  3. Reads the build ID as an argument. The build ID is shown in the game when you launch it (it’s in the right corner). Skip the trailing .0 when using it with the script.
  4. For each game variant (in UgcGameVariantLinks) and map (in MapLinks) stores a copy in your files. You will see your in-game file browser populate with all the game modes and maps that are available for the build.

Now, do keep in mind that there is a lot of test and unfinished content that is likely not stable or doesn’t work as well as you’d expect. The entire point of this is to make exploration easier at the expense of occasionally running into some glitch. Like this one, with bots following me aimlessly:

So, what’s the script? You can copy and paste the snippet below into a file and run on your Windows machine (you can also find the same script on GitHub Gist). Use script arguments in the following format:

1
.\halobuff.ps1 -SpartanV4Token YOUR_SPARTAN_TOKEN_STARTING_WITH_V4 -BuildId 6.10022.13411 -Xuid "xuid(YOUR_XUID_ID)"

I call the script halobuff.ps1 because it enables the hidden “buffs” to experiment with in the content, but you can cal the file whatever you really want - it makes no difference when it executes. Anyway, here is the code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# Script by Den - Written in July 2022
# Enables all modes and maps that are available in a given build manifest.
# For details, refer to https://den.dev/blog/halo-infinite-enable-all-content

param (
    # Spartan V4 token, used for authentication against the Halo Infinite API.
    [Parameter(Mandatory=$true)]
    [string]$SpartanV4Token,
    
    # Build ID accessible to the user which is authenticating.
    [Parameter(Mandatory=$true)]
    [string]$BuildId,

    # User XUID used for inclusion in the binary blob
    [Parameter(Mandatory=$true)]
    [string]$Xuid
)

function ProcessAsset([Guid]$AssetId, [Guid]$AssetVersionId, [string]$AssetName) {
    Write-Host "Processing asset with version ${AssetId} and version ${AssetVersionId}"
    Write-Host "Asset name:" $AssetName
        
    $reverseAssetGuid = $assetId.ToByteArray()
    $reverseVersionGuid = $versionId.ToByteArray()

    $properAssetId = [RequestData.Helper]::GetRawGuid($reverseAssetGuid)
    $properVersionId = [RequestData.Helper]::GetRawGuid($reverseVersionGuid)

    $entity = [RequestData.Entity]::new()
    $entity.Admin = $Xuid
    $entity.Dummy = [RequestData.DummyEntity]::new()
    $entity.AssetDefinition = New-Object System.Collections.Generic.List[RequestData.Asset]

    $asset = [RequestData.Asset]::new()
    $asset.AssetId = $properAssetId
    $asset.AssetVersion = $properVersionId

    $entity.AssetDefinition.Add($asset)
    $entity.Metadata = New-Object System.Collections.Generic.List[string]
    $entity.Metadata.Add($AssetName)

    Write-Host "Writing data to file..."
    [RequestData.Helper]::WriteToFile($entity)
}

# Only download a new NuGet binary if one doesn't already exist in the current folder.
if (-Not(Test-Path nuget.exe -PathType Leaf)) {
    Write-Host "Downloading NuGet (PowerShell package management is messy)..."
    Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile nuget.exe
} else {
    Write-Host "NuGet.exe already exists in current folder."
}

Write-Host "Installing the Microsoft Bond helper package..."
.\nuget.exe install Bond.Core.CSharp -OutputDirectory . -Version 9.0.5

# Loading the Bond assembly, that will be necessary to serialize the data
$bondLibaryPath = Resolve-Path ".\Bond.Core.CSharp.9.0.5\lib\net46\Bond.dll"
$bondAttributesLibaryPath = Resolve-Path ".\Bond.Core.CSharp.9.0.5\lib\net46\Bond.Attributes.dll"
$bondIOLibaryPath = Resolve-Path ".\Bond.Core.CSharp.9.0.5\lib\net46\Bond.IO.dll"
$bondReflectionLibaryPath = Resolve-Path ".\Bond.Core.CSharp.9.0.5\lib\net46\Bond.Reflection.dll"

[String[]]$paths = $bondLibaryPath, $bondAttributesLibaryPath, $bondIOLibaryPath, $bondReflectionLibaryPath

Add-Type -Path $bondLibaryPath
Add-Type -Path $bondAttributesLibaryPath
Add-Type -Path $bondIOLibaryPath
Add-Type -Path $bondReflectionLibaryPath

Write-Host $paths

$entityDefinition = @"
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Bond;
using Bond.Protocols;
using Bond.Tag;

namespace RequestData
{
    [Bond.Schema]
    public 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]
    public class DummyEntity
    {

    }

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

    [Bond.Schema]
    public 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; }
    }

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

        
        public static void WriteToFile(Entity entity)
        {
            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();
            }
        }
    }
}
"@

Try {
    Add-Type -TypeDefinition $entityDefinition -ReferencedAssemblies $paths
} Catch {
    Write-Host "Looks like the type is already loaded."
}

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("X-343-Authorization-Spartan", $SpartanV4Token)
$headers.Add("Accept", "application/json")

# Try and get build metadata.
Try {
    $url = "https://discovery-infiniteugc.svc.halowaypoint.com/hi/manifests/builds/${buildId}/game"
    Write-Host "Trying to work with the following URL:" $url
    $response = Invoke-RestMethod $url -Method 'GET' -Headers $headers

    $gameVariants = $response.UgcGameVariantLinks
    $maps = $response.MapLinks

    Write-Host "Response contains $($maps.Count) map links and $($gameVariants.Count) game variants."

    # Process game variant links
    $counter = 1
    foreach($gameVariant in $gameVariants){
        $assetId = [Guid]::new($gameVariant.AssetId)
        $versionId = [Guid]::new($gameVariant.VersionId)
    
        ProcessAsset $assetId $versionId $gameVariant.PublicName
    
        Write-Host "Preparing to store..."
    
        $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
        $headers.Add("X-343-Authorization-Spartan", $SpartanV4Token)
        $headers.Add("Accept", "application/json")
        $headers.Add("Content-Type", "application/x-bond-compact-binary")
    
        $body = [System.IO.File]::ReadAllBytes('example.bin')
        $fullPath = [System.IO.Path]::GetFullPath('example.bin')
        Write-Host $fullPath
    
        $response = Invoke-WebRequest 'https://authoring-infiniteugc.svc.halowaypoint.com/hi/UgcGameVariants' -Method 'POST' -Headers $headers -Body $body
        
        Write-Host "[${counter}] Storage routine ended with HTTP status code" $response.StatusCode
    
        $counter = $counter + 1
    }

    Write-Host "Going to be processing map links..."

    $counter = 1
    foreach($map in $maps){
        $assetId = [Guid]::new($map.AssetId)
        $versionId = [Guid]::new($map.VersionId)

        ProcessAsset $assetId $versionId $map.PublicName

        Write-Host "Preparing to store..."

        $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
        $headers.Add("X-343-Authorization-Spartan", $SpartanV4Token)
        $headers.Add("Accept", "application/json")
        $headers.Add("Content-Type", "application/x-bond-compact-binary")

        $body = [System.IO.File]::ReadAllBytes('example.bin')
        $fullPath = [System.IO.Path]::GetFullPath('example.bin')
        Write-Host $fullPath

        $response = Invoke-WebRequest 'https://authoring-infiniteugc.svc.halowaypoint.com/hi/maps' -Method 'POST' -Headers $headers -Body $body
        
        Write-Host "[${counter}] Storage routine ended with HTTP status code" $response.StatusCode

        $counter = $counter + 1
    }
} Catch {
    Write-Host "Error processing build metadata."
    Write-Warning $Error[0]
}

This script is a horrific implementation for anyone that is familiar with PowerShell and/or C#. I am clearly Frankenstein-ing the code here by mixing in C# pieces that are dealing with Bond serialization and attributes (I haven’t gotten an elegant solution for that in PowerShell) with .NET-style instantiation and management of entities. But, as hacky of a piece it is - it works. Broken down step-by-step, the script:

  1. Downloads NuGet.exe from the official NuGet website so that I can install the right packages for Bond data handling.
  2. Install the Bond.Core.CSharp package that enables me to handle Bond data.
  3. Get the paths for Dynamic Links Libraries (DLLs) included in the aforementioned package.
  4. Load the libraries in the script context.
  5. Translate my C# entity code into PowerShell-managed classes.
  6. Get the build metadata through the Halo API.
  7. Parse the build metadata to get the available maps and game variants.
  8. For each available game variant, create a Bond-formatted envelope and send it to the “Save A Copy” Halo API endpoint.
  9. For each available map, create a Bond-formatted envelope and send it to the “Save A Copy” Halo API endpoint.

That’s about it! Once the script execution completes, you should see all the maps and modes available when you go to Community and then My Files.

I am not sure how useful this actually is to the broader community per-se, but it at least enables me and friends to play Vampire Oddball and try to launch Forge (despite Forge assets not being in the current build).

Want to get more notes like the above? Subscribe to The Den!

A monthly newsletter about product management, engineering, and tinkering with code.

Feedback

Have any thoughts? Let me know over email by sending a note to hi followed by the domain of this website or on Mastodon.