Halo: Combat Evolved's multiplayer on a Nintendo 64 with the Expansion Pak, split screen for up to four players, with bots and a Firefight mode. It is Halo's own game code (the decompiled source of the halo-ce-universal project: objects, physics, collision, weapons, game types, the HUD, first-person weapons, Halo's pathfinding) compiled for the N64's VR4300, with a new renderer, menu, HUD drawing and sound on libdragon and tiny3d, and the maps converted from the Xbox game's files.
You need your own copy of the Xbox game. The build converts its maps; nothing from the retail game is in this repository. A ROM you build contains data converted from the game: keep it to yourself.
port/n64/HISTORY.md is the development log (what
changed when, and why) and port/n64/NOTES.md the
working notebook.
![]() |
![]() |
| Blood Gulch, one player and seven bots | Battle Creek, four-player split screen |
![]() |
![]() |
| Hang 'em High, two players | Firefight: the Covenant in waves on Blood Gulch |
(Taken in the ares emulator.)
- All 13 multiplayer maps: Battle Creek, Sidewinder, Damnation, Rat Race, Prisoner, Hang 'em High, Chill Out, Derelict, Boarding Action, Blood Gulch, Wizard, Chiron TL-34 and Longest, with their vehicles, weapons, teleporters and water.
- 1 to 4 players in split screen, each on their own controller, each joining a team or playing free for all.
- Game types: Slayer, Team Slayer, Slayer Pro and Team Slayer Pro (everyone starts with a pistol and an assault rifle), Capture the Flag, King of the Hill, Oddball and Race (each also as a team game), with the score to win chosen in the menu.
- Bots: up to 8 AI players at four skill levels (easy to legendary). They fight, pick up better weapons, find their way with Halo's own pathfinding and play on teams. Each game they take names at random from a list of about 300 Mario, Zelda, GoldenEye and Rare N64 characters.
- A pause menu (START): the game stops and a scoreboard shows every player and bot (up to 12) with score, kills and deaths, and a button back to the main menu.
- Firefight: Grunts, Jackals and Elites attack in waves on the multiplayer maps; 1 to 4 players, shared lives, four difficulties.
- Halo's HUD (shields, health, ammunition, motion sensor, messages, scores), Halo's weapon and vehicle sounds, the main menu's music, and the N64's own explosions and particles.
- A main menu at 640x480 (interlaced) over the scene behind Halo's own title, the ring in space, with the HALO 64 logo.
- Controls for each player: four schemes (standard, bumper jumper, GoldenEye, north), toggle or hold crouch, inverted look, fire and grenade swapped, and separate look speeds for the stick and the C buttons.
- Rumble Pak: Halo's own rumble (shots, hits, explosions, vehicles) on the Rumble Pak, each player's on or off.
- Settings for the picture and speed (below). Settings and controls are saved on the cartridge (EEPROM).
- The ground in colour: the natural ground's colour map in tiles of its own, read from the cartridge as you come near (a megatexture: the grass, dirt and paths of Halo's maps), and Blood Gulch's blue sky.
- A campaign cartridge (
halo-campaign.z64, a ROM of its own): the level "Halo" with its Covenant driven by the N64's simplified AI (Firefight's; no Marines), squads and waves placed as you come near and put away when you have gone far on, checkpoints, four difficulties, a warthog by the Marine base, and lighter scenery. No cutscenes or scripts yet, and the level's second half (its other structure BSP) is not on it yet.
No networking.
Halo64RomBuilder.exe makes the ROMs without any of the tools below: drop
your Halo: Combat Evolved Xbox disc image (.iso, trimmed or whole) or
its maps folder on its window, tick the cartridges (multiplayer and the
campaign's "Halo"), choose where they go and press Build ROMs. It
converts your maps on your PC (a first build takes a while: every sound is
converted once, then kept for the next) and writes halo-n64.z64 and
halo-campaign.z64. It needs about 3 GB of free memory and 3 GB of disk
for its work folder (%LOCALAPPDATA%\Halo64RomBuilder). No Halo data comes
with it: it carries the game's program, built from this repository, and
the tools that turn your maps into the cartridge's files.
How it is made: tools/n64/rombuilder.py runs the same conversions as the
ninja build below with each cartridge's prebuilt program (the build's
halo.elf.stripped and halo.elf.sym), tools/n64/xiso.py reads the disc
image, tools/n64/rombuilder_gui.py is the window, tools/n64/ build_windows_tools.sh builds libdragon's mksprite, audioconv64, mkdfs,
n64tool and ed64romconfig for Windows (MinGW-w64, in WSL or on Linux) and
tools/n64/package_rombuilder.py, run with a Windows Python that has
pillow tkinterdnd2 sv-ttk pyinstaller, packages it all with PyInstaller.
On Linux the builder runs from the repository too:
python3 -m tools.n64.rombuilder <disc image or maps folder> -o <folder>.
The build runs on Linux (or on Windows in WSL, below). It takes ~10 minutes
the first time (13 maps are converted) and makes build/n64/halo.z64, about
63 MB.
Everything except the repository lives in ~/n64 (none of it needs root):
| What | Where | How |
|---|---|---|
| libdragon's MIPS toolchain | ~/n64/toolchain (N64_INST) |
the gcc-toolchain-mips64-x86_64.deb from libdragon's toolchain-continuous-prerelease release, unpacked with dpkg-deb -x and its opt/libdragon moved to ~/n64/toolchain |
libdragon, preview branch |
~/n64/libdragon |
with this repository's port/n64/devtools/libdragon-rspq-buffer.patch applied (git apply: the RSP queue's buffer size becomes a variable the game sets; stock libdragon works, only slower), then make -j install tools-install |
| tiny3d | ~/n64/tiny3d (TINY3D) |
make -j (the build links build/libt3d.a) |
| Invader | ~/n64/invader |
only its tag definitions are read (src/tag/hek/definition/*.json); a shallow clone is enough |
| The Xbox game's maps | build/xbox/disc/maps/ in this repository |
from your own disc: the 13 multiplayer maps, ui.map (the menu) and a30.map (the Covenant for Firefight) |
| Python 3 with Pillow, ninja | apt install python3-pil ninja-build |
|
| Optional: the fan-made Halo font | ~/n64/fonts/Halo.ttf (HALO_FONT) |
Will Turnbow's "Halo" from dafont.com: the logo's 64 is drawn in it; without it the title shows Halo's own logo |
The maps (*.map) are in the maps folder of the Xbox disc. Copy an
image of your disc with your own tools and extract it (for example with
extract-xiso).
git clone https://github.com/JackTulli/halo-64 ~/halo-64
export N64_INST=~/n64/toolchain
mkdir -p ~/n64 && cd ~/n64
# the toolchain (see the table for the .deb)
dpkg-deb -x gcc-toolchain-mips64-x86_64.deb toolchain-deb && mv toolchain-deb/opt/libdragon toolchain
# libdragon, patched, into the toolchain
git clone -b preview https://github.com/DragonMinded/libdragon
cd libdragon
git apply ~/halo-64/port/n64/devtools/libdragon-rspq-buffer.patch
make -j install tools-install
cd ..
# tiny3d and Invader's tag definitions
git clone https://github.com/HailToDodongo/tiny3d && make -C tiny3d -j
git clone --depth 1 https://github.com/SnowyMouse/invader
# the maps, then the ROM
mkdir -p ~/halo-64/build/xbox/disc/maps
cp /path/to/your/xbox/maps/*.map ~/halo-64/build/xbox/disc/maps/
cd ~/halo-64
python3 tools/n64_build.py # writes build/n64/build.ninja
ninja -f build/n64/build.ninja # build/n64/halo.z64The build converts the maps listed in port/n64/port.json ("maps"),
compiles the game and the platform layer, links, and packs the ROM with the
converted data on its file system. Later builds only redo what changed.
Converting maps and linking take a lot of memory: on a machine with less
than 16 GB, run ninja with -j 4 or so.
port/n64/devtools/build.sh does the same two steps in a memory-capped
scope (9 GB, through systemd-run, so that a heavy build can't freeze the
machine) and prints the errors or the ROM.
The same build runs in WSL 2 (Ubuntu 24.04 is what it's built on):
- Install WSL and Ubuntu (
wsl --install -d Ubuntuin an administrator PowerShell), open Ubuntu andsudo apt install git python3-pil ninja-build build-essential. - Clone the repository inside WSL's own file system (not under
/mnt/c: building there is many times slower) and follow the Linux steps above. - Copy the maps in from Windows: your C: drive is
/mnt/cin WSL. - Copy the ROM out to Windows to play it, for example
cp build/n64/halo.z64 /mnt/c/Users/<you>/n64/roms/.
WSL gets half the PC's memory by default; the map conversion wants ~8 GB
(set memory= in %UserProfile%\.wslconfig if the build is killed).
The game needs the Expansion Pak (8 MB). It is developed and tested in the ares emulator (v148):
- Turn the Expansion Pak on in ares (its
Nintendo64/ExpansionPaksetting) and loadhalo.z64as a Nintendo 64 game. The game's settings are saved to the cartridge's EEPROM (4 kbit, declared in the ROM's header), which ares keeps next to the ROM. - On Windows, from a command prompt:
ares.exe --system "Nintendo 64" --setting Nintendo64/ExpansionPak=true halo.z64 - On Linux, with ares from Flathub:
flatpak run dev.ares.ares --system "Nintendo 64" --setting Nintendo64/ExpansionPak=true build/n64/halo.z64
At the title, MULTIPLAYER sets up a game: each controller presses A to join (Z changes team in team games, B leaves), then the game type, the map, the score and the bots (left and right for how many, A for their skill), and START. FIREFIGHT is the same with a difficulty instead of a game type. After the scores, A goes back to the menu.
During a game, any player's START pauses it: the scoreboard (every player and bot, the best first, the leading team's first in a team game) with one button, MAIN MENU (A), back to the title. START or B goes on with the game.
The standard scheme (each player can choose another on the CONTROLS page):
| N64 | Halo |
|---|---|
| Stick | look and turn |
| C buttons | move (up/down) and strafe (left/right) |
| Z | fire |
| R | throw a grenade |
| A | jump |
| B | reload, action (pick up, enter a vehicle) |
| L | melee |
| D-pad up | switch weapons |
| D-pad down | crouch |
| D-pad left | zoom |
| D-pad right | switch grenades |
| START | pause: the scoreboard, and MAIN MENU |
The other schemes:
- Bumper jumper: R jumps, A throws a grenade.
- GoldenEye: the stick turns and walks, C left/right strafe, C up/down look.
- North: the stick walks and strafes, the C buttons look.
Each player also chooses: crouch held or toggled, look normal or inverted, fire on Z or R (grenades on the other), the look speed of the stick and of the C buttons (1-10), and the Rumble Pak on or off (it buzzes when turned on, to show it's there).
Rumble Pak. A controller with a Rumble Pak shakes with Halo's own rumble: firing, taking hits, explosions, vehicles. Halo drove two motors at any speed; the Rumble Pak has one that is on or off, so it is switched on for a share of the frames as strong as Halo's rumble. In ares, choose the Rumble Pak as the controller's pak (it passes the rumble on to a PC gamepad that has motors).
| Setting | What it does |
|---|---|
| 3D RESOLUTION | chunky (160 wide, each pixel doubled; the HUD stays sharp), medium (the 3D drawn 256 wide and widened), standard (320 wide) or high detail: 480i on the maps with the memory for it, else 640x240 at 240p, twice the pixels across as Perfect Dark's high resolution. Lower is faster. |
| ANIMATIONS | full or half rate: half poses the characters, vehicles and your gun every other tick (halved again with 3 or 4 players), and the bots think less often. Faster. |
| PARTICLES | muzzle flashes, sparks, dust and blood on or off |
| VIEW MODEL | your gun and arms in low, medium or high detail, or off in split (low, and not drawn with 3 or 4 players: the biggest saving there) |
| TERRAIN DETAIL | full (the detail maps everywhere: the ground's at every distance, rock and earth's drawn over them), high (the ground's detail fades out a few steps away, before it can shimmer), medium (high without rock and earth's second pass) or low (no detail maps: the ground's colour alone). Lower is faster. |
| WIDESCREEN | off, or anamorphic 16:9 for a television that stretches the picture |
| HUD INSET | moves the HUD in from the edges, for televisions that cut them off |
| FPS COUNTER | frames a second and timings at the top of the screen |
A multiplayer game's CONTENT screen (game setup) sets the WEAPONS players and bots start with, the VEHICLES (map default, or none at all), IMPACT FX (original or enhanced), BIG HEAD and PAINTBALL.
The N64 has a 93.75 MHz CPU and 8 MB of memory with the Expansion Pak; the Xbox had a 733 MHz CPU and 64 MB. What it takes to fit:
- Memory. Each map is converted to a big-endian image of only the tags the game needs, relocated at load. The game state is trimmed to what it uses, models and textures are streamed from the cartridge into a cache, animations are streamed, and textures are 4-bit (CI4/I4) with mipmaps and detail textures where they matter. High detail's 480i needs 1.8 MB for its buffers, so it is only used where the map leaves room; elsewhere it is 640x240 at 240p (1.2 MB).
- Drawing. The level is drawn from tiny3d batches with the light baked into the vertices, culled by Halo's own potentially visible set and the view; objects are skinned from Halo's node matrices; draws are sorted by material; each view's HUD is recorded once and replayed for a few frames.
- The game's ticks. Halo runs at 30 ticks a second whatever the frame rate. Objects that can't change (items at rest, still scenery) update every fourth tick; poses of far, unseen or bot characters every second or third; vehicle physics every other tick; bots and Covenant think every few ticks and find paths a slice at a time; effects no one can see or hear aren't made.
The frame rate depends on the map, the number of views and the bots: the FPS COUNTER setting shows it.
tools/n64_build.py: the ninja file. Game sources (source/) are the decomp project's own (itsconfig/config.json: the sources, defines and include folders), less whatport.jsonleaves out (Halo's actor AI except its pathfinding, networking, scripts, Halo's sound engine, the Xbox renderer and menus).tools/n64_stubs.pywrites stand-ins for what the rest still calls (port/n64/game/n64_stubs_generated.c, regenerate withdevtools/restub.sh);tools/n64_link_check.pyfails a link where a call to a missing inline body would go to address 0.port/include/,port/linux/(two files and their headers) andport/third_party/musl-math: what the N64 build shares with the decomp project's PC ports (the Xbox SDK's headers, the MSVC string functions, the linker's common symbols, musl's maths).port/n64/include/halo_n64_prefix.h: the MSVC/Xbox environment for gcc (big-endian MIPS, 32-bit pointers and longs, 16-bit wchar_t). Changes to Halo's own code are under#ifdef HALO_N64.tools/n64/: the map converter.convert.pyrepacks the tags into one big-endian image with relocations (layouts from Invader's definitions) and streams animation frames tomaps/<map>.anim;level.pyconverts the BSP's render geometry into tiny3d batches lit from each material's lighting summary;models.pyconverts the objects' models (and the first-person ones) into skinned batches;hud.pyexports the HUD's bitmaps as half-size sprites;bitmaps.pydecodes the Xbox textures;effects.pythe particle emitters and explosions;soundbank.pyandmusic.pythe sounds and the menu's music;merge.pybrings the Covenant froma30.mapinto the multiplayer maps for Firefight;ui.pytakes the menu's things fromui.map(the sky, the ring, the camera path, Halo's fonts, the logo and the pictures);prune.pydrops shared files no map uses any more.port/n64/src/: the platform layer.xdk_*.cimplement the Xbox SDK calls (files, time, XInput over the N64 pads, with the control schemes) with the game's ABI;n64_*.care libdragon code:n64_render.c(the renderer),n64_hud.c(the HUD's quads, meters, text and blips),n64_particles.c(particles and explosions),n64_sound.c(the mixer's voices),n64_anim.c(animation streaming),n64_menu.c(the menu) andn64_font.c(Halo's fonts),n64_exceptions.c(x86 behaviour the game relies on: unaligned access, NaN arithmetic, float-to-integer overflow),n64_profile.c(a sampling profiler).n64_main.cgives the game a deeper stack (192 KB, below the heap's top) than libdragon's 64 KB.port/n64/game/: game-side glue:n64_game.c(the menu's choices made into a game; map loading, memory),n64_render_game.c(the renderer as the game sees it),n64_hud_game.c(the rasterizer calls Halo's HUD code makes),n64_sound_game.c(Halo's sound calls on the N64's voices),n64_bots.c(the bots) andn64_firefight.c(Firefight's waves and the Covenant's behaviour).
Cartridge flags are files on the ROM's file system, listed in port.json
"flags" (keep it [] for a ROM to play):
- Straight into a game, without the menu:
debug_map=<map>(withdebug_players=<1-4>,debug_bots=<n>,debug_bot_skill=<0-3>,debug_game_type=<variant>,debug_firefight=<difficulty>),autotest(scripted controllers),debug_menu=<script>(drives the menu: a press every 20 frames,u d l r a b z son port 1, upper case on port 2,.none),debug_title_time=<seconds>(the title's camera path from there),debug_cycle=<seconds>(after so many seconds a game goes back to the menu, which starts the next map by itself: every map in turn, through the menu),debug_high_detail(3D RESOLUTION high detail without the menu),debug_view_model=<0-3>. - Drawing:
debug_chase(view 1 follows its player),debug_overview,debug_untextured,debug_no_level,debug_no_objects,debug_no_first_person,debug_no_hud(a plain crosshair instead),debug_hud_inset=<pixels>,debug_gpu_time(each render phase waits for the RSP/RDP: their times),debug_cull_front,debug_level_parts=<1|2>,debug_explosions,debug_fp_nodraw(the first-person weapon posed, not drawn),debug_rdpq(libdragon's RDP validator: slow; it names the commands that would hang or misdraw on a console). - Measuring:
debug_stats(frame, tick, render, HUD and object timings in the log),profile(samples at the vertical blank from 20 to 80 s;tools/n64_profile.py <log> build/n64/halo.elf),profile_zone,debug_ray_callers,debug_sound,debug_no_sound,profile_sound,debug_game_state(Halo's game-state allocations at boot),debug_firefight_log,debug_bots,debug_heap_dump(each block in use past the maps' home when the menu comes back: what a game left behind). The log also names each FPU instruction the port had to emulate ("emulated ... at pc"): a NaN or a denormal, thousands of cycles each.
devtools/n64_timing.c times chosen game functions through --wrap.
devtools/ares-run.sh and ares-shots.sh run a ROM in ares on a hidden
Xvfb display (:97) with the debug log captured; ares-gdb.sh and
rsp.py read the CPU's state through ares' GDB server. In WSL,
devtools/bench-wsl.sh <label> <seconds> <scenes> builds and runs the
benchmark scenes, bench-summary.py adds the game's speed and the stack's
depth. A crash shows libdragon's inspector with a backtrace: the ROM keeps
its symbols.
- The halo-ce-universal decompilation of Halo: Combat Evolved, which this port is built on.
- libdragon and tiny3d, the N64 libraries it runs on.
- Invader's tag definitions, read by the map converter.
- ares, the emulator it is developed in.
- musl's maths functions (
port/third_party/musl-math, MIT licence). - Will Turnbow's fan-made Halo font, used (optionally) for the logo's 64.
The code is under CC0 (LICENSE.md), as the decompilation project's is;
port/third_party/musl-math keeps musl's MIT licence (COPYRIGHT) and
tools/ninja_syntax.py its Apache 2.0 licence (in the file).
This is a fan project, not affiliated with or endorsed by Microsoft, Bungie or 343 Industries. Halo is a trademark of Microsoft. No game data is included: building it needs your own copy of the Xbox game, and the ROM it makes contains data converted from that game, so don't share it.




