Unlocking All Halo Infinite Content In Your Build
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:
- Code is easy to modify without programming tools installed.
- Code is easy to understand at a glance.
- No need to deal with compilation and binaries.
- I can still wrap all the .NET niceties for Bond serialization.
- 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:
- 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). - 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). - 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. - For each game variant (in
UgcGameVariantLinks
) and map (inMapLinks
) 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:
.\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:
# 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:
- Downloads NuGet.exe from the official NuGet website so that I can install the right packages for Bond data handling.
- Install the
Bond.Core.CSharp
package that enables me to handle Bond data. - Get the paths for Dynamic Links Libraries (DLLs) included in the aforementioned package.
- Load the libraries in the script context.
- Translate my C# entity code into PowerShell-managed classes.
- Get the build metadata through the Halo API.
- Parse the build metadata to get the available maps and game variants.
- For each available game variant, create a Bond-formatted envelope and send it to the “Save A Copy” Halo API endpoint.
- 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.