From One Halo PC Lua Script to a Race Framework
The first custom Halo PC Race implementation lived in one large Lua script.
That was useful while the problem was still one problem: move some checkpoints, watch the Race engine rebuild them, and see whether the idea worked.
Then the script learned about player starts, vehicle layouts, navigation commands, portals, flingers, lap limits, marker objects, and fall damage.
It stopped being one problem.
Where the monolith failed
A single file made early experiments fast, but every new feature shared the same callbacks and state.
Changing checkpoint behaviour risked breaking chat commands. Adding vehicle logic made round-start ordering harder to understand. Reusing one feature on another track meant copying unrelated code with it.
The project needed boundaries that matched the game systems being modified.
One module, one concern
The current framework separates responsibilities into modules:
race-checkpoints manages checkpoint flags and waypoint slots
checkpoint-progress reads player masks and reports the next checkpoint
gametype-vehicles controls packed vehicle counts
vehicle-portals moves occupied vehicles
vehicle-flingers applies launch velocity
dynamic-race-laps adjusts the score limit
object-table monitors shared capacity
Each module owns its configuration and internal state.
A track file mostly selects modules and provides data.
Centralising Phasor callbacks
Phasor expects global callback functions. Multiple independent modules cannot all define OnNewGame and expect every version to run.
TrackHost installs the global hooks once and dispatches events to registered module instances.
The registration order is intentional. Fire-and-forget events visit every module, while hooks capable of vetoing an action stop when one module returns false.
That design keeps Phasor's global interface at the edge of the project instead of spreading it through every feature.
Why instances matter
Several tracks can use the same module with different settings. Instance-based modules keep those settings separate and make dependencies explicit.
They also expose lifecycle methods such as on_script_load, on_new_game, and on_script_unload. A module can snapshot original memory, apply its values at the correct time, and restore the snapshot later.
This matters when writing the loaded Halo PC gametype, where script load and round start close different timing gaps.
The next duplication problem
The framework is not finished.
Checkpoint carets, portal markers, and flinger rings each contain versions of the same marker lifecycle: create objects, deny pickup, retry failures, freeze or respawn markers, and cope with drift.
Those copies have begun to diverge.
A marker-lifecycle module is planned, but it should be extracted only when the shared behaviour is understood. Moving unstable duplication into a shared abstraction merely makes the instability harder to isolate.
What the framework changed
The important result is not fewer lines of Lua.
A new Halo PC track can now combine existing capabilities without copying their implementations. Features can be tested separately, callback order is visible, and each module documents the memory it owns.
The original script proved that custom Race checkpoints were possible.
The framework makes them reusable.