Delivering Snail Mail
Recovering the source of a 2004 game nobody owns, then proving it plays the same
In May 2017, a player found the creator of a forgotten 2004 game hiding behind a different one. The developer of Chromadrome 2 had just shipped on Steam, and someone in the comments asked: did you make Snail Mail?
He did. And he still had it.
I still have all the code and assets for Snail Mail.
— Alpha72 Games, Steam discussions, May 2017
He owned the game, he explained, but not its title, its art, or its music. He wasn’t sure who did. The publisher had been bought, the buyer had folded, and for a while some company had collected the royalties without paying him his share.
So the source code for Snail Mail exists. It’s on someone’s disk, legally stuck. This post is about the other way to get it: rebuilding it from the shipped executable, one function at a time, until the compiler from 2004 produces the same bytes. Then compiling it for the web and proving it plays exactly like the original.
You can play it now. The rest of this post explains why you should believe it’s the real thing.

A snail with a cannon
Snail Mail came out on November 15, 2004. You play Turbo, a snail delivering parcels for the Intergalactic Postal Service along tracks that float in space. You steer with the mouse. His shell has a cannon that upgrades to lasers and then rockets. Slugs try to stop you. Salt is a hazard, for obvious reasons. It cost $19.99, the going rate for a casual game, and you could try it first with a 7 MB download.
The launch press release from Sandlot Games had a subtitle in the spirit of the postal motto: neither salt nor asteroids nor slugs nor the gloom of black holes stops Turbo.1
1 “Sandlot Games Ships And Delivers With ‘Snail Mail™’”, Bothell, WA, 2004.
2 alpha72.com, April 2004. The domain is now a WordPress “Coming Soon” page.
The credits are short. Programming and original game design: Richard Evans, of Alpha72 Games, a studio he founded in 2004 to make “original, quality 3D shareware games.”2 His earlier game, Chromadrome, was rough, he said later, but it got him a shot at Snail Mail. Graphics: Israel Evans. Game and level design: Andrew Lum. Sound and music: Daniel Bernstein, who also happened to be Sandlot’s founder and CEO. In his own press release he praised the game’s “outstanding soundtrack.”
I can confirm the soundtrack is catchy. I heard it several hundred times while building this, and at some point I added a mute switch for my own sanity.

Sandlot sold it on its own site and through casual portals like Reflexive Arcade. In December 2006, a Reflexive reviewer who signed as “Snail” wrote a review addressed to Santa, asking for Snail Mail 2.3 Santa didn’t deliver.
3 Reflexive Arcade reviews, archived 2008.
The game did have a long second life, though:
- iPhone, 2008. Tilt to steer. IGN’s reviewer gave it an 8.0 and put it in his top ten of the year, and Sandlot’s App Store page claimed IGN’s “Best Use of Accelerometer” award.
- iPad, 2010. Up to four players in split screen.
- Android and Palm Pre, 2011. The Palm port is missing from most databases.
- WiiWare, 2011. Nintendo Life gave it a 6, with a headline I can’t improve on: “Not quite worth its salt.”4 The voice credit changed from “Dano!” to Mark Lund.
4 Nintendo Life review, June 2011.
5 US serial 77019139. Filed 2006, registered 2007, cancelled January 31, 2014.
Then the corporate part. Digital Chocolate bought Sandlot in August 2011, laid off around 180 people the next year, and shut down in 2014. The SNAIL MAIL trademark changed hands twice and was cancelled in January 2014, because nobody filed the declaration that you’re still using it.5 The iOS apps are gone from the App Store. sandlotgames.com became a casino spam site that, as late as 2019, still had a page at /games/snailmail. Today it redirects to Indonesian slots.
The creator has the code but not the name. The name belongs to nobody. The game is abandonware, and it doesn’t run well on a modern PC. Players on that Steam thread trade compatibility-mode tips.
The specimen
The build I work from is the Windows release, linked on December 4, 2004, three weeks after launch. It’s a Visual C++ 6 program on Direct3D 8, with BASS 2.0 for sound and the usual early-2000s statically linked zlib, libpng and libjpeg. libjpeg even brought its own error string: “Sorry, there are legal restrictions on arithmetic coding.”
Before you can study it, you have to take it out of its wrapper. Reflexive Arcade shipped games inside a DRM launcher, which I’ve documented in my Reflexive preservation project. SnailMail.exe starts the real program, SnailMail.RWG, suspended, decrypts its code section in memory with a keystream from a seeded PRNG, then resumes it. For Snail Mail, the seed collapses to zero. So the decryption is fully reproducible, and uv run snail unwrap gives you the same 724 KB gameplay executable every time.
The assets live in a 27 MB archive, SnailMail.dat, with 603 files XOR-masked by their absolute offset. The meshes are a text format derived from DirectX .x. The tracks are ASCII art: each segment is a grid ten characters wide, where | is a wall turret, J is a jetpack, and ( is a trampoline. I find it charming that a 3D game’s levels are designed in Notepad.
Score files use a different mask, a plain index XOR. I learned that the hard way. My first parser used the archive’s mask for score files, and its round-trip tests passed for months, because a wrong mask applied twice is still a round trip. Then real save files from the original decoded to garbage. Real data from the original, not your own round trips, is what catches a wrong guess. That became the theme of the whole project.
The first port, 58,000 lines later
I started in March by doing what I’d done with Crimsonland: read the decompile, write the game again by hand, compare. This time in Zig, on raylib.
It went fast and it went wrong. By June I had a 58,537-line port and a document called the invalidation ledger, with 25 dated entries. Each one was a model I had confidently written and later had to overturn: the score bank mask, a struct I’d taken for a global, how the snail interpolates its pose on track attachments. Errors compounded. One +0.27 error in z at a track exit changed collisions and speed for the rest of the level.
The score files turned out to be the best test I had. Snail Mail records your best runs as replays, and the score banks keep them. I wrote a replay oracle that fed recorded runs into my port. On the first run, the port drifted more than one unit from the recording at tick 28. By the end of the run, it was off by 917 units.
The lesson wasn’t that I needed to transcribe more carefully. A hand-written port is a second copy of the game logic, and a second copy drifts. raylib didn’t help: its batching cached GL state behind the renderer, and at one point the font’s glyph atlas showed up as a texture on every mesh.
So I stopped writing a second copy. In August I deleted all 58,537 lines.
Matching decompilation
Matching decompilation is the opposite discipline. You write C++ that compiles, with the original compiler and flags, into exactly the same bytes as the shipped function. When it matches, there’s no interpretation left. That function is the source, or something indistinguishable from it.
The workflow is mostly a loop between the decompiler and a diff. I used Binary Ninja, IDA and Ghidra as different opinions on the same code. Agents drive Binary Ninja through bn, my command-line tool for it. VC6 runs under wibo, and objdiff shows exactly which instructions differ. Progress is published on decomp.dev.
Three things made this game tractable.
The other ports kept their names. The Windows binary has no symbols. The Android build has 1,082 named C++ functions, and the iPhone build another 970, with source file records. Matching them up gave most Windows functions their original names. It also kept the original spelling: the quaternion class is tQuaternian, and the snail respawns in cRSubGoldy::RessurectInit.
The Wii port kept its structure. The Wii build has no names, but its PowerPC code kept control flow that the x86 optimizer had folded away. The quaternion’s axis-selection tree was stuck at 92.47% on Windows. It went to 100% once the Wii build showed what the source looked like.
The compiler was identified from an error message. I spent months assuming VC6 Service Pack 5. Most functions compiled identically with any VC6 build, so nothing complained. The Rich header (the linker’s record of which tools built each object) says otherwise. 56 of the game’s C++ objects come from backend build 8447. That backend refuses to work with any newer frontend, failing every compile with C1900: Il mismatch between 'P1' version and 'P2' version. That pins it to VC6 SP3. Two of my “exact” matches lost their credit, because I’d fitted them to a compiler that didn’t exist when the game shipped.
Beyond that, it’s a long tail of VC6 folklore. Redundant parentheses aren’t redundant: they emit a rounding node that shifts the instruction scheduler’s window. Removing one named temporary can swap three registers across a whole function. At one point I was reverse engineering C2.DLL, the compiler’s own backend, to understand its register allocator. The Binary Ninja database for the compiler ended up with about 1,200 recovered names.
Where it stands today:
| today | |
|---|---|
| functions with recovered source | 785 |
| port-relevant functions byte-identical | 637 of 662 |
| proof-grade bytes | 71.5% |
| fuzzy match, overall | 99.3% |
| recovered C++ | ~50,000 lines, 159 headers |
The 25 that don’t match yet are semantically complete and fuzzy-match between 77% and 100%. They’re the biggest functions in the game, like the 23 KB asset loader and the snail’s update.
The port, take two
This time the port compiles the recovered source itself, unchanged, and only the platform layer is new. There’s no second copy of the game logic to drift.
The target is wasm32. It has the same 32-bit data layout the original’s struct-size assertions expect: 4-byte pointers, 8-byte-aligned doubles. Every size check holds, down to the game’s 19.8 MB cRGame object. Underneath it are two emulators:
- Direct3D 8. A small subset, emulated on the CPU, with presenters for WebGL2 in the browser and SDL3 GPU on macOS.
- BASS 2.0. Emulated too, with Web Audio and SDL3_mixer playing the archive’s original OGG files.
The game’s own renderer, audio code and loaders run as they did in 2004.
The constant data comes straight from the original executable: its .rdata and .data sections become assembly, with every known name as a label and every pointer as a relocation.
WebAssembly turned out to be a great place to put old C++. It’s strict where x86 was forgiving. Every call has to agree with its definition’s signature, so the build surfaced every recovered declaration that disagreed with its definition. Negative offsets into arrays trap instead of wrapping around.
The native build runs the same .wasm file. wasm2c turns it into C, and SDL3 provides the window.
It went from first link to the v0.2.0 release in 49 hours: headless, then the browser, sound, saves, a native macOS host, hosting, two releases.
Proving it
Compiling the original’s source doesn’t prove the port behaves like the original. A float can round differently, or a platform call can be emulated slightly wrong. So I tested it two ways.
Replays. The same score banks that sank my first port now test this one. The shipped game hides its Replay buttons but keeps their code, so the oracle presses them. It plays back all 20 recorded high-score runs, 57,817 ticks, and compares the snail’s position to the recording at every sample. All 20 match.
Sessions. Replays only record the snail’s path. For everything else, I set up a Windows machine and wrote a Frida script that records a play session of the original. It captures every tick’s input state as the game loop sees it, a snapshot after every tick, the RNG state, the render cadence, and a screenshot every 120 frames. The port replays the inputs and compares its state against the snapshot after every tick.
The first sessions diverged, and each divergence was a floating-point story.
- The compiler reassociates float sums. VC6 computed
a + b + cas(a + c) + bin one spot. Writing it in that order gives byte-identical VC6 output and makes clang round the same way. Byte matching can’t see this class of difference. - The sine tables were built at the wrong precision. Direct3D 8 drops the x87 FPU to 24-bit precision when it creates its device. But the math library fills its 8,192-entry sine table before that, at 53-bit precision, with the angles held in registers. More than half the entries differ by one ULP if you compute them in float.
- Denormals round twice. When a decaying velocity underflows, the x87 rounds to 24 bits first, then again to a denormal on the store. WebAssembly rounds once. A tiny helper now rounds twice, at the two places it matters.
To check the precision theory, I needed the FPU control word from inside the running original. Reading one register took five attempts:
- Frida’s built-in C compiler faulted on the game thread.
- Hand-assembled instructions, but my edit accidentally deleted eleven helper functions.
- The capture machine wouldn’t run code written into memory.
- Hooking inside the C runtime’s own math code corrupted the x87 register stack and crashed the game whenever a postal level started.
- Reading the thread’s saved context from function-entry hooks, where the FPU stack is empty. That one worked.
Now all 10 captured sessions match bit for bit on every tick: 56,977 ticks of menus, tutorial, postal, challenge and time trial play.
Frame by frame
State can match perfectly while the picture is still wrong. So the native host can now replay a captured session and render any frame the capture took, at the same tick.
The first comparison found turrets that weren’t there.



Those lavender pillars are the turrets, the | in the track ASCII art. In the port, you only saw their lasers. The cause was a calling-convention mismatch.
- The game positions the pillar model by multiplying each vertex by a matrix. It calls the multiply through a declaration that returns a pointer to the result, which is how MSVC hands back a struct: the caller passes a hidden slot after
this, and the function returns the slot’s address. - WebAssembly passes the hidden slot before
this. - My link step had generated a bridge between the two declarations, and it got the order backwards. The product overwrote the source vertex, and the caller read its result from a garbage address.
Every pillar vertex became zero. A hand-written shim fixed it. The link step now leaves alone any function that has a hand-written shim.
Fog was the opposite case. I was sure the original had fog, and the port didn’t. The game asks for linear vertex fog from 30 to 50 units, just short of its 52-unit far plane, so the track fades out instead of ending. On paper the formula uses eye-space depth. The game’s camera is right-handed, so everything in front of it has negative depth, and the documented formula never fogs anything.
Real drivers use the absolute value. Wine’s Direct3D test suite pins it down: “Vertex fog abs() behavior is the same on all GPUs.” A one-line fix brought the fog back. Amusingly, my captures from a modern Windows machine don’t show fog either, so for fog the 2004 hardware is the reference, not today’s Windows.
Small comforts
I didn’t want a remake. I wanted the 2004 game to run anywhere, a little more comfortably. The port has three switchable enhancements and a big Original button that turns them all off.
Sharp rendering. The game draws at your display’s resolution instead of 640×480. Everything lands on the same spot, only finer. The toon outlines are one-pixel Direct3D lines, which would thin to a hairline on a retina display. So they’re drawn as quads one original pixel wide, and they keep their weight.

Fullscreen. The game’s own Fullscreen option works again. Leaving fullscreen through the browser turns the option off, so the Options menu stays accurate.
Mouse trap. The game has its own cursor, so the real one gets locked to the page. Browsers swallow the Escape key that releases the lock, and Escape is the game’s pause key. So when the lock is released and the page didn’t ask for it, the page passes Escape to the game.
The site greets you with the game’s own loading screen, extracted from the archive and squeezed into a 15 KB AVIF.6 It waits for a click before starting, because browsers won’t play sound without one, and that same click used to skip the intro.
6 AVIF’s default chroma subsampling smeared the thin orange “Play in Our Neighborhood” lettering, so it’s encoded at full 4:4:4 chroma for 500 more bytes.

Some things didn’t make it.
- Higher-resolution assets. I searched the Android, iPhone, iPad and Wii builds for better ones. There aren’t any: the Windows art is the master, and every port is smaller or recompressed.
- The Wii voice lines. They’re full-band 44.1 kHz recordings, where the PC has 11 kHz. But they’re a different take, by a different actor, and it’s a worse performance. Dano stays.
- A longer view down the track. Drawing three times further ahead works, and both oracles still match with it on. But objects still spawn 38 units ahead, where the game spawns them, and the extra distance made the pop-in obvious. It’s parked on a branch.
Notes on tools
This project is 4,961 commits over 100 days of work since March 8.
- Matching. Codex and Claude Code worked side by side, often in parallel. Codex took batches of functions while hands-on matching went on elsewhere, and findings fed both ways. Early on Codex ran as an automated loop with evidence gates; later it was consults with gpt-6-astra on the hardest residuals.
- The port and the oracles. Built in Claude Code over the last two days, while I ran capture sessions on the Windows machine and reported crashes back.
The interesting part isn’t speed, though. My first port was fast too, and wrong in 25 documented ways. What changed is that every claim now has a check behind it: a byte diff, a replay, a session, a frame. The machine doesn’t get tired of being told it’s wrong, and the oracles never run out of patience.
Play it
- Play at snail.banteg.xyz. There’s a gear in the corner for the enhancements, and Original if you want it exactly as it was.
- Source at github.com/banteg/snail.
- Progress on decomp.dev.
- Help. If you like puzzles, there’s a list of matching challenges. Partial improvements count.
The game also has three cheat codes you type during play: NEWTON, AUTUMN and SHEEP. AUTUMN stops you from falling. NEWTON quietly disables high-score recording. As far as the recovered code can tell, SHEEP does nothing at all. If you remember what it did, I’d love to know.
And Richard, if you ever read this: thank you for Turbo. If that code is still on a disk somewhere, I’d love to diff it.
p.s. the cover isn’t a screenshot of the original. It’s the port, replaying a session I captured on Windows, at the same tick.