
Moxnatiy/OpenBattlefield2

A C++20 clean-room reimplementation of the Refractor 2 engine behind Battlefield 2, an engine rather than a game; no game content is redistributed and there is no anti-cheat.

Moxnatiy/OpenBattlefield2

A C++20 clean-room reimplementation of the Refractor 2 engine behind Battlefield 2, an engine rather than a game; no game content is redistributed and there is no anti-cheat.
Why it is listed: Clean-room interoperability and preservation work that ships no assets.
See the original source for installation instructions.
A clean-room reimplementation of DICE's Refractor 2 engine — the engine behind Battlefield 2 (2005) — in C++20.
It is an engine, not a game: it ships no content and reads the files of your own installed copy of Battlefield 2. Without that copy there is nothing to run.
The goal is a one-to-one port, not a game "inspired by" the original.
Every rule in src/ names where it came from — an address in the original
binary, a line in the game's data, or a measured frame of the original — and
where the original's behaviour is not known yet it is written down as debt
instead of being filled in with a guess.
A research project in progress. What works today, against an original Battlefield 2 1.5 installation and an original dedicated server:
mainMenu.swf, played by
Ruffle with a bridge that answers the
calls the movie makes into the engine: localisation, the player's profiles
from Documents/Battlefield 2, the installed maps from each level's .desc.openbf2 --connect <host> does the original's
handshake, loads the level the server names, and stays in the game. Every
record the server's ghost stream sends is read to exactly its length:
players (score, ping, team, squad, kit, alive), spawn groups, soldiers
(position, angles, pose, the weapon out, a dead soldier's ragdoll) and
vehicles' positions..con node tree, animated by the
MemeFile graph from Menu/Ingame: the spawn screen with the server's own
spawn points, the countdown to the next spawn, the minimap, the scoreboard
with the server's numbers.--hosted).What is not there yet, among much else (the full list is in CLAUDE.md,
"Debt"): firing and the weapons in your own hands, entering and driving
vehicles, sound, the commander and squad screens. Bink video is not decoded,
there is no anti-cheat, and no game content is redistributed.
Everything below is our engine on macOS/arm64, reading an original Battlefield
2 1.5 installation. Every frame was made by the command printed under it, with
no hands on the keyboard. --screenshot writes a BMP; the images here are the
same frames as PNG.
The game's own mainMenu.swf, under Ruffle with our bridge:

openbf2 --frames 700 --click-at 300:490:285 --click-at 400:813:630 --screenshot menu-main.bmp
The spawn screen on Dalian Plant — kits, the minimap, control points, all
built from the game's own .con node tree:

openbf2 --level Dalian_plant --frames 8 --screenshot spawn-dalian.bmp
In the world after DONE — terrain, static meshes, the combat HUD:

openbf2 --level Dalian_plant --frames 120 \
--click-at 20:730:585 --click-at 60:1110:833 --screenshot ingame.bmp
Strike at Karkand from a fixed camera, the interface off:

openbf2 --level Strike_at_Karkand --camera -134/175/-250 --angles 20 -8 \
--no-hud --frames 8 --screenshot karkand.bmp
These three come from an original Linux dedicated server with bots, so what
is in the frame is whatever the bots were doing; the commands are the ones
that took them. --watch-soldier holds the camera beside another player's
soldier, --watch-weapon <item> beside one with that item out,
--watch-ragdoll beside a dead one.
A bot running with his launcher on his back, animated by the game's own animation system from what his ghost carries:

A bot crouched behind his pistol — the pose (crouch) and the weapon out (item 2, the pistol) both read from his state:

A dead bot, laid down by the engine's ragdoll: the server sends four of the body's particles, the client simulates the rest:

# the running bot (frame 1600) and the body (frame 4400) are from one run
openbf2 --connect <host> --frames 6000 --no-hud --watch-ragdoll \
--screenshot-at 1600:live-soldier.bmp --screenshot-at 4400:live-ragdoll.bmp
openbf2 --connect <host> --width 800 --height 600 --frames 1900 --no-hud \
--watch-weapon 2 --screenshot-at 800:live-pistol.bmp
--mod, default Game Files/mods/bf2.windows-x64 preset is kept
building but is not exercised.reference/ruffle (see docs/research/11-ruffle-menu.md). Without them the
obf2_flash module is skipped and everything else still builds.cmake --preset macos-arm64-debug
cmake --build --preset macos-arm64-debug
ctest --test-dir build/macos-arm64-debug --output-on-failure
Some tests read your installation (Game Files/) and pass with a note when it
is not there; the network tests run on captures of a live server kept in
tests/data/.
Every push to master is built and tested by GitHub Actions on macOS arm64
(.github/workflows/ci.yml), and the run keeps openbf2 with its SDL3 beside
it as an artifact, openbf2-macos-arm64-<commit>.tar.gz, for 30 days. That
build has no Flash menu — Ruffle is not part of it — so it is started with
--level or --connect.
# The menu (the game's own movie), then singleplayer from there
./build/macos-arm64-debug/src/app/openbf2
# Straight into a level
./build/macos-arm64-debug/src/app/openbf2 --level Dalian_plant
# A local server and client, with the round's settings from the game's data
./build/macos-arm64-debug/src/app/openbf2 --hosted --level Strike_at_Karkand
# Join an original dedicated server. No `--level`: the server names the level
# it runs, and the client mounts that one
./build/macos-arm64-debug/src/app/openbf2 --connect <host>
The switches that measure rather than play — every one of them leaves a frame or a number behind:
| switch | what it gives |
|---|---|
| --frames N, --screenshot f.bmp, --screenshot-at N:f.bmp | run N frames and keep a frame |
| --click-at N:x:y, --exec-at N:line | a click or a console line at frame N |
| --camera x/y/z --angles yaw pitch, --no-hud | a fixed view, for comparing against the original (tools/bf2_run.sh puts the original in the same spot) |
| --hud-rects, --hud-vars, --hud-screen <name> | what the HUD drew, which of its variables nobody fills, one screen of it |
| --watch-soldier, --watch-pose N, --watch-weapon N, --watch-ragdoll | the camera beside another player's soldier, one in a pose, with a weapon out, or dead |
| --record f.bin | the server's packets, for the tests and capture_scan |
| --calibrate f.bin | the server's template numbers matched to names |
At the end of a run the client prints its report: how every record type was read, how other players were drawn, how many ragdoll frames had a particle through a wall, how long the HUD's rebuilds took.
| directory | what it is |
|---|---|
| src/ | the engine, one module per subject |
| tests/ | one test per parsed subject; nothing counts as understood without one |
| tools/ | scripts and small programs that pull facts out of the binaries and the game's data |
| docs/ | what was reversed, and where it came from |
| third_party/ | vendored single-file libraries: miniz, stb |
| module | what it does |
|---|---|
| core | platform, paths, maths, the loading phases' thread pool |
| vfs | the game's archives, mounted the way the original's fileManager does |
| con | lexer and interpreter for the .con language |
| texture, mesh, anim | .dds; meshes, skeletons, skinning, collision; the animation systems and the ragdoll |
| font, loc | .dif fonts, .utxt localisation |
| game | the ObjectTemplate registry, the scene, a soldier's model |
| level | terrain, water, placement, GamePlayObjects.con |
| gfx | window and GPU (SDL3 + SDL_GPU) — the only place with graphics |
| hud, meme | the hudBuilder node tree and its states; the MemeFile animation graph |
| net | BitStream, the protocol, events, the ghost stream's records |
| server | local server, soldier physics, the collision world |
| session | the live link to a server: the handshake, the world it keeps, our action stream |
| engine | console, settings, key bindings |
| flash | the Flash menu: Ruffle behind a C ABI |
| app | the openbf2 executable: command line, scene, renderer, menu, the world on screen |
Where the notes are:
docs/functions/ — one file per subject reversed from the binary, every
claim with its address: the network events and records
(network-events.md, player-state.md, spawn.md), the soldier's physics
and model (soldier-physics.md, soldier-model.md, ragdoll.md), the HUD
(hud-*.md), the menu bridge (menu-bridge.md);docs/formats/ — the file formats: .con, meshes, skeletons, animation,
levels, the HUD's graph, the network protocol;docs/research/ — prior art, the first passes, frame dumps of the original.Two directories are not in git and must be provided locally:
Game Files/ (your own installation) and reference/ (third-party sources
used for comparison; their licences differ from ours).
Reversing the binary goes through Ghidra over MCP. That configuration is
nothing but absolute paths to one machine, so it is not in git either: copy
.mcp.json.example to .mcp.json and fill in your own.
One pass is a measure first (a command, a number, a screenshot), then the
answer from the binary or the game's data, then the notes and the code, then
the measure again. A constant with no source is not written down; it is
recorded as "not measured". CLAUDE.md states the rules the project holds
itself to and keeps the list of what is still not reversed; docs/ holds the
notes each of them refers to.
MIT — see LICENSE. This covers our code only. Battlefield 2 and its
content belong to their respective owners; nothing from the game is included
here.