Why Does Halo PC Race Only Support 32 Checkpoints?
While working on the custom Race checkpoint system, I wanted to confirm exactly where the 32-checkpoint limit comes from.
Several different numbers appear throughout old Halo documentation, while a scenario can contain far more than 32 netgame flags. That made the limit difficult to place. Was it enforced by the engine, or simply inherited from older scripts?
Well, it turns out the limit is real.
After examining the retail PC dedicated server in Ghidra, I found that Race tracks checkpoint progress with a 32-bit bitmask. Each checkpoint gets one bit, which gives us checkpoint indices 0 through 31.
That sounds conclusive, but I wanted proof.
Race checkpoint state
The main Race state begins at:
0x639FA0 Its first DWORD contains the required checkpoint mask, with one bit representing each checkpoint on the current track. For example, checkpoints 0 through 3 produce this value:
00000000 00000000 00000000 00001111 Each player has another DWORD recording which checkpoints they have already reached during the current lap. Once that value matches the required mask, Halo considers the lap complete.
So, 32 already makes sense here. A DWORD only contains 32 bits.
Still, another structure could theoretically have supported additional checkpoints elsewhere, so I followed the code that builds the mask itself.
Building the Race checkpoints
The relevant function begins at:
0x0048D500 I have named it:
ProcessRaceTrackFlagIndices This function walks through the scenario's netgame flags, looking for the entries that should become Race checkpoints. Race checkpoints use type 3, while the value at offset +0x12 supplies the checkpoint index.
The important part is straightforward. The server accepts only indices below 32, and anything greater than or equal to 32 gets skipped.
The field itself can store larger values. Race simply ignores them.
For every accepted checkpoint, Halo converts the index into a bit and adds it to the required mask:
required_mask |= 1 << checkpoint_index; That gives us the following mapping:
Checkpoint 0 -> bit 0
Checkpoint 1 -> bit 1
Checkpoint 2 -> bit 2
...
Checkpoint 31 -> bit 31 And that is the problem. There is no bit at index 32 in a 32-bit value.
It checks again during the race
The same restriction appears again when a player actually touches a checkpoint during a live Race game.
That check happens in the function at:
0x0048DCB0 I have named this one:
CheckRaceCheckpointIsNext It receives the checkpoint index and rejects anything greater than or equal to 32 before performing the bit shift. Even if I forced a 33rd checkpoint through the initial setup, it would still fail at this second check.
Sequential, any-order, and rally Race all rely on the same 32-bit checkpoint state. There is no separate path with a larger array waiting elsewhere.
The waypoint table is also 32 entries
The HUD waypoint table starts at:
0x670F40 Each waypoint occupies 0x20 bytes, and the table contains exactly 32 entries. That places its end at:
0x671340 Of course, that address is not unused space. It marks the beginning of the loaded gametype variant.
The memory layout therefore looks like this:
0x670F40
GameWaypoint[0]
GameWaypoint[1]
...
GameWaypoint[31]
0x671340
Loaded GameVariant There is no spare waypoint after entry 31. Treating the existing table as though it held 33 entries would write directly into the gametype data, which is hardly a useful solution.
Race scoring assumes 32 too
Race scoring provides another independent confirmation that the limit is built into the surrounding game systems.
Halo calculates each player's internal score using:
laps * 33 + popcount(checkpoint_mask) The multiplier of 33 looked unusual at first, but it makes sense once the checkpoint mask is understood. A player can hold at most 32 completed checkpoint bits, so the values progress like this:
0 laps + 32 checkpoints = 32
1 lap + 0 checkpoints = 33 That prevents a complete set of checkpoints from overlapping with the first score value for the following lap.
Honestly, this was one of the clearest confirmations. The 32-checkpoint limit was not an isolated guard; Race's scoring model was designed around it as well.
Networking uses the same masks
Race networking does not offer an escape route either.
The network state copies the same DWORD masks used by the server, including both the required mask and each player's progress. As a result, this cannot be extended only on the server, because every connected client also receives just 32 bits of checkpoint state.
What about the other limits?
The conflicting numbers were why I wanted to investigate this properly instead of trusting the existing scripts.
A scenario may contain more than 32 netgame flags, but those flags are not all Race checkpoints. The engine filters them by type and then accepts only checkpoint indices from 0 through 31.
The checkpoint-index field can hold values above 31. That works perfectly well at the data level, but the Race engine ignores those values when building its state.
Older documentation also mentions a limit of 15 Race checkpoints, although that is not the runtime limit used by the retail PC dedicated server.
I also found these strings inside haloded.exe:
NETGAME MAP FAILURE: duplicate race track flag
NETGAME MAP FAILURE: missing race flag They initially looked promising. Unfortunately, neither string appears to be referenced by this dedicated-server path, so they do not explain or enforce the 32-checkpoint limit.
Duplicate checkpoint indices are a little broken too
While following the setup code, I found a small bug in the way Halo handles duplicate checkpoint indices.
When two Race flags share an index, Halo attempts to find a free replacement and write that value back to the duplicate flag. The strange part comes immediately afterwards.
The function updates its local mask using the old index instead of the replacement value. This means the flag gets reassigned, while the newly chosen bit is not properly claimed in that local mask.
My checkpoint code manages its own indices, so the bug does not currently affect custom tracks. For now, it is simply an interesting side effect uncovered while tracing the setup function.
Can the limit be increased?
In principle, perhaps. Not through a simple data edit.
So far, I have deliberately kept this project within memory that Halo has already allocated. Moving checkpoints, changing their indices, adjusting gametype values, and updating waypoint data all fit comfortably within that approach.
Going beyond 32 checkpoints does not.
At minimum, an implementation would need to replace or modify:
The index < 32 checks
The required checkpoint mask
Each player's checkpoint mask
Rally checkpoint handling
The waypoint table
Race scoring
The Race network state
The waypoint table makes the problem particularly awkward because it sits directly beside the loaded gametype variant. Expanding it would require moving or replacing existing memory rather than merely adding another entry.
The laps * 33 scoring calculation would also need to change once a lap could contain more than 32 checkpoints. Networking would then require compatible client-side changes, since the existing protocol sends the same fixed-width masks.
At that point, I would no longer be editing Race data. I would be patching several interconnected parts of the engine, which falls outside the scope I set for this project.
So, 32 it is
The 32-checkpoint limit does not come from the HEK, the scenario block size, or the width of the checkpoint-index field. It is embedded throughout Race itself.
Progress uses a 32-bit mask. The waypoint table contains 32 entries. Scoring reserves space for 32 checkpoint bits, and networking transmits those same fixed-width masks.
There is no hidden setting to increase.
For now, 32 is the limit.
Which is fine.
I have already managed to use all 32 checkpoints on Boarding Action and Sidewinder.