Making Replay Ghost Vehicles Non-Collidable in Halo Combat Evolved
I have recently been working on a replay system for Halo Combat Evolved.
The replay system records a vehicle's position and rotation during a race, then spawns a local vehicle which follows that recorded path. This gives the player a visual "ghost" of a previous lap.
The visual side was working well, but there was one fairly significant problem: Halo still treated the replay ghost as a real vehicle.
Driving into it caused the player's vehicle to bounce away, while the replay ghost could also be woken by the collision and pushed away from its recorded path.
The goal was therefore fairly simple:
the player should be able to drive through the replay ghost;
the ghost should not be pushed by real vehicles;
normal collision with the map, players and other objects should continue working;
normal gameplay should be restored when replay playback stops.
The eventual fix turned out to be only five bytes.
Finding the correct five bytes, however, took considerably more work.
The initial approach
Halo objects have a number of flags which control how they interact with the game.
One option was to make the replay vehicle completely non-collidable. This worked to an extent, but it disabled more than just vehicle physics. The vehicle would stop participating correctly in other collision and trace systems as well.
That was too broad.
I instead wanted to disable only the physical response between vehicles.
The first promising path was a swept-collision function at:
FUN_00505880 This function receives a mask describing what kinds of geometry and objects should be tested.
It was called from the vehicle physics code with:
0xC0A1 The original assumption was that Halo transformed this into:
0xFFFA1 That resulting mask would include vehicle objects, which suggested a fairly easy fix: clear the vehicle bit from the mask and allow all locally simulated vehicles to pass through one another.
The relevant call looked like this:
00509A6A push 0xC0A1
...
00509ADE call 0x00505880 At first glance, patching the immediate value passed by the push instruction appeared to be the simplest solution.
Checking the mask properly
Before patching it, I walked through the mask logic in the decompiled function.
The important condition was:
if ((mask & 0xFFF00) == 0) {
mask |= 0xFFF00;
} This means Halo adds the default object-type bits only when the caller has not explicitly supplied any.
The problem was that:
0xC0A1 & 0xFFF00 = 0xC000
The result is non-zero.
Halo therefore does not add 0xFFF00. The working mask remains:
0xC0A1
Breaking the value down further showed that its object-type bits enabled only scenery and device-machine objects.
It did not include the vehicle bit at all.
The original theory was therefore wrong: this collision cast was never checking other vehicles.
Worse, replacing it with the proposed mask would have enabled collision checks against a large number of additional object types, including weapons, equipment and projectiles.
Rather than narrowing the cast, the patch would have significantly broadened it.
Fortunately, this was caught before anything was changed.
Following the vehicle hull code
At this point, the more useful question became:
If this cast does not handle vehicle-to-vehicle collision, what does?
The next step was to find the function responsible for testing the physical hull of a vehicle:
collision_test_vehicle_hull This function is located at:
0x005074B0 A vehicle hull is essentially the invisible shape Halo uses when determining whether two vehicle bodies overlap.
Tracing the callers of this function exposed a separate vehicle collision system:
vehicle_physics_integrate
↓
per-vehicle physics step
↓
vehicle_resolve_object_collisions
↓
nearby-object broad phase
↓
collision_test_vehicle_hull
↓
vehicle_apply_pair_contact_impulse
Some of these names were added during the reverse-engineering process to make the decompilation easier to follow. Where the exact role is still inferred rather than fully proven, that is documented in Ghidra rather than presented as certainty.
The key functions in the confirmed vehicle-pair chain are:
0x00508A10 vehicle_resolve_object_collisions
0x005074B0 collision_test_vehicle_hull
0x005090C0 vehicle_apply_pair_contact_impulse Unlike the earlier swept cast, this path did not use an object-type mask.
Instead, it performed the following sequence:
Find nearby objects using a broad bounding-box search.
Read the type of each nearby object.
If the object is a biped, use the biped collision path.
If the object is another vehicle, test the two vehicle hulls.
If the hulls overlap, apply a contact impulse between them.
In simplified form, the decompiled logic looked roughly like this:
for each nearby_object {
if (nearby_object.type == BIPED) {
handle_vehicle_biped_contact();
}
else if (
nearby_object.type == VEHICLE &&
nearby_object != self
) {
if (vehicle_hulls_overlap()) {
apply_vehicle_pair_contact_impulse();
}
}
}
The actual vehicle contact response was applied by:
vehicle_apply_pair_contact_impulse at:
0x005090C0 This was the function I had been looking for.
Proving the replay ghost used this path
Static analysis suggested that this was the correct subsystem, but I still wanted to prove that the replay ghost actually passed through it.
Two non-pausing software breakpoints were added:
0x00508AE8 This is where Halo calls collision_test_vehicle_hull.
The second was:
0x00508B17 This is immediately before Halo proceeds to vehicle_apply_pair_contact_impulse.
The debugger was configured so these breakpoints logged information without stopping the process. Pausing a multiplayer Halo client for too long causes the server to assume it has crashed and disconnect it, so all runtime probes had to continue automatically.
During a collision with the replay ghost, the logs captured:
VVCONTACT CAND=E2BD0043 The datum was resolved through Halo's object table and validated:
the salt matched;
the object type was vehicle;
the internal object type was vehicle;
the tag matched the Warthog used by the replay system.
This confirmed that the local replay ghost was being accepted by the vehicle hull system and sent to vehicle_apply_pair_contact_impulse.
It also exposed another useful detail: the ghost's datum was not stable.
The replay system destroys and recreates the ghost as needed, so its salt and even its object-table index can change. An older recorded datum had already become invalid and its object-table slot had been reused by a weapon.
Any solution based on a permanently hard-coded ghost datum would therefore be unreliable.
What the contact resolver does
vehicle_apply_pair_contact_impulse applies the physical response between two overlapping vehicles.
It calculates separating impulses from the overlapping vehicle mass points, then adds linear and angular impulse values to the vehicles.
The relevant fields include:
object + 0x508 linear impulse accumulator
object + 0x514 angular impulse accumulator
It then clears the physics-asleep flag:
object + 0x10, bit 0x20 and marks the vehicle as having made contact during the current frame.
The same response can be applied to the candidate vehicle.
This explained both symptoms:
the real vehicle was pushed away from the replay ghost;
the replay ghost was woken and pushed away from its recorded position.
Both behaviours came from the same function call.
The physics-asleep flag is also checked elsewhere in the vehicle physics chain. The gate at:
0x005714FB sits inside the function now named:
vehicle_physics_integrate The exact full responsibility of that larger function is still partly inferred from its surrounding behaviour, but the asleep gate itself was confirmed at runtime.
Earlier testing showed that forcing the ghost to remain asleep could prevent the ghost from integrating the contact response.
That only solved one side of the problem, however. The player's real vehicle would still detect the ghost and bounce away from it.
Skipping the pair-contact impulse solved both sides at their shared source.
The patch
The call to vehicle_apply_pair_contact_impulse occurs at:
00508B20 call 0x005090C0
The original instruction bytes are:
E8 9B 05 00 00
Ghidra cross-references showed that vehicle_apply_pair_contact_impulse has exactly one caller: this vehicle branch inside vehicle_resolve_object_collisions.
That made the call a particularly useful patch point.
Replacing it does not alter an otherwise widely shared physics helper. It removes the one invocation responsible for resolving vehicle-to-vehicle contact from this branch.
For the test, the call was replaced with five NOP instructions:
90 90 90 90 90
A NOP is an instruction which performs no operation.
With the call removed, Halo still:
finds nearby vehicles;
identifies them as vehicles;
performs the vehicle hull overlap test;
advances through the normal candidate loop.
It simply does not apply the physical contact impulse.
In other words:
Halo detects the overlap
↓
Halo skips the push-apart calculation
↓
the vehicles pass through one another locally
The patch was applied live through x32dbg without pausing the game.
The bytes were verified before and after the write, and the original call bytes were kept ready for immediate restoration.
Testing the result
The first test was driving directly into the replay ghost.
The player's vehicle passed through it without bouncing, and the ghost was no longer pushed or woken by the vehicle-pair contact resolver.
Vehicle collision with the map remained normal because that is handled by separate vehicle-to-world collision paths.
The exact BSP sweep helper within that wider system has not yet been confidently named. One function initially suspected of being the BSP sweep, FUN_00502060, was later checked and found to be a much smaller sweep-result initialisation helper, so it was deliberately left unnamed.
Vehicle-to-biped contact also remained normal because bipeds branch into a different function before reaching the patched call.
The relevant branch is cleanly separated:
candidate type == biped
→ biped contact path
candidate type == vehicle
→ vehicle hull test
→ vehicle_apply_pair_contact_impulse
Only the second path was affected.
The only noticeable compromise involved real server-owned vehicles.
The local client now predicts that vehicles can pass through one another, while the server still considers real vehicles solid. When colliding with another real vehicle, the server can correct the local client's predicted position.
This can produce minor:
jitter;
snapping;
delayed collision response;
rubber-banding.
In testing, the result was slightly glitchy but acceptable.
More importantly, the patch is only active while watching a replay. Normal vehicle collision is restored as soon as replay playback ends.
Applying it from Chimera Lua
Chimera's unlocked Lua scripts can read and write process memory.
That means the final replay script does not need a custom Chimera build, native plugin or complex trampoline hook. It can patch the five-byte call directly.
The implementation first verifies that the expected original bytes are present:
local address = 0x00508B20
local original = {
0xE8,
0x9B,
0x05,
0x00,
0x00
}
local patched = {
0x90,
0x90,
0x90,
0x90,
0x90
} When replay playback starts, the script replaces the call with NOPs.
When playback stops, it restores:
E8 9B 05 00 00 The script should also restore the instruction when:
the script unloads;
the map changes;
replay playback fails or is cancelled;
the user disables the replay system.
The byte verification is important because this address and instruction sequence are specific to the tested Halo executable. If the expected bytes are not present, the script should refuse to patch anything.
The patch should also guard against being applied twice and verify that the expected NOP bytes are still present before restoring the original call.
Final behaviour
The finished behaviour is:
Replay starts
↓
disable local vehicle-to-vehicle contact resolution
↓
real vehicles can pass through the replay ghost
Replay stops
↓
restore the original contact-resolution call
↓
normal local vehicle collision resumes
A note is shown to users explaining the trade-off:
While a replay is active, local vehicle-to-vehicle collision handling is temporarily disabled so vehicles can pass through the replay ghost. Because real vehicles remain server-controlled, minor visual glitches, jitter or position corrections may occur when colliding with non-ghost vehicles during replay playback.
Conclusion
The final patch is small, but getting there required correcting a fairly convincing initial theory.
The first function was part of vehicle physics and did perform collision casts, but it handled vehicle-to-world and selected object collision rather than vehicle-to-vehicle contact.
Carefully checking the mask arithmetic prevented a patch which would have enabled a large number of unintended collision types.
Tracing collision_test_vehicle_hull led to a completely separate, mask-free collision path:
vehicle_resolve_object_collisions
↓
nearby-object broad phase
↓
candidate type == vehicle
↓
collision_test_vehicle_hull
↓
vehicle_apply_pair_contact_impulse
Runtime logging then confirmed that the replay ghost passed through that path and reached the dedicated vehicle contact resolver.
Ghidra's cross-references also confirmed that vehicle_apply_pair_contact_impulse has only one caller, making the five-byte call replacement significantly more targeted than the earlier mask-based idea.
Disabling that one call removed both sides of the unwanted response:
the real vehicle is no longer pushed away;
the replay ghost is no longer woken and shoved.