# How we rebuilt No One Lives Forever for the browser (full text) Source: https://playnolf.com/making-of/ (Brightwork Digital, 9 October 2026). The article and its twelve technical files, as plain text. CASE FILE: THE MAKING OF # How we rebuilt No One Lives Forever for the browser A spy shooter from 2000 that nobody can buy. An engine that was never released. Ten days, one person, a team of AI agents, and every mistake we made on the way. By Brightwork Digital. Published 9 October 2026. About 20 minutes to read; the technical files add about 25. An unofficial fan port, made with respect for Monolith's work. No infringement is intended, and if the rights holders ask, it will come down. playnolf.com/play/ ## The short version No One Lives Forever, the 2000 spy shooter by Monolith Productions, now runs in a desktop web browser at playnolf.com, on a new LithTech-compatible engine written in C++ and compiled to WebAssembly by Brightwork Digital with AI agents, in ten days. - What: No One Lives Forever (Monolith Productions, 2000) now runs in a desktop browser at playnolf.com: all 15 missions plus the Game of the Year bonus mission, Rest and Relaxation. Nothing to install. The first mission is signed off by me; the rest are checked by tests and by playing, not yet signed off. - How: Monolith released the game's code in 2001 but never the engine under it. We wrote a new engine that fits the game's code like the original did, then compiled both to WebAssembly, code that browsers run at close to native speed, so they run in a browser tab. - Who: one person typing instructions, and AI agents (Anthropic's Claude) writing the code. 267 prompts from 30 September, and 771 commits (saved changes to the code) from 1 to 9 October 2026. - How we know it's right: we ran the original 2000 game beside ours on the same inputs and compared them frame by frame (a game draws about 60 frames a second), object by object. - How right it is: 57 of 65 levels start exactly like the original, over their first 300 frames. On one eight-minute recording, a development build not yet on the live site matched the first level frame for frame, on every object the server tracks. Case 7 says what that does and doesn't cover. - The honest part: the AI made confident mistakes, forgot decisions and declared victory too early. Most of this story is about how we caught that. No One Lives Forever came out in November 2000. You play Cate Archer, a former cat burglar turned spy, in a 1960s full of exploding lipstick, robot poodles and henchmen who chat about their lives before you interrupt them. Critics loved it. It won Game of the Year awards. Then the companies that owned it changed hands, the rights got tangled, and the game quietly disappeared from sale. Twenty-five years later, you still can't buy it anywhere. I run Brightwork Digital, and I own the original CDs. I just wanted to play it again. (In this story, "I" is me and "we" is me plus the AI agents.) This is the story of how that turned into rebuilding the engine it runs on, with AI agents doing the typing, and of every wall we hit. Case 1. 30 September 2026 ## Can I play this on my Mac? It started with a simple question, typed late at night. Me, 30 Sept, 02:31 UTC: > What is the best and simplest but most effective way to get No One Lives Forever working on this Mac? Claude suggested CrossOver, a paid Windows compatibility layer, plus a community patch. Sensible and correct. I pushed for something bolder, with a dose of flattery I'm not proud of ("remember you have greater knowledge than any programmer in the history of the world"). Claude came back with three routes: wrap Wine (free software that runs Windows programs on other systems) as a Mac app, play the PlayStation 2 version in an emulator, or a "moonshot": port the game natively. Then I found a native port of the sequel, NOLF 2, on a fan site, and asked if it had seen it. It had linked the wrong copy of that project and missed the finished build entirely. Strike one. Me, 30 Sept, 03:45 UTC: > Did you consider decompilation? "No, I didn't." It then explained why decompiling only the engine was a real option, because the game's own code was already public. That was the first time the AI was plainly wrong and plainly said so. It wouldn't be the last. Then I asked what it would take to play in a browser, and the three answers set the course for everything after. Technical file 1Why not just run an emulator, or Wine, in the browser? Case 2. 1 October 2026 ## I own the CDs. Build it. On 1 October, at 12:47 in the afternoon my time (17:47 UTC; prompt times here are UTC), I wrote the prompt that started the real project. I'm including most of it, with a few lines trimmed and marked, because it set the tone for the next nine days. Me, 1 Oct, 17:47 UTC: > I own the CDs for this game. Download and find the best versions, and I want you to fully decompose this game and build it out to be playable on the web. You can set up a GitHub repo and then deploy it to Cloudflare on a public website when the time comes [...]. But all your initial work must be local. This is a long-standing job. You are to do the decomposition work using Sonnet 5.5, and then all the superintending work here. [...] This is to be a recursive self-improvement job. Set a goal: working in the browser at full screen, smooth not jittery, everything perfect. Research and follow all best practices [...]. A lot of buzz now on X about people doing this. [...] I want this game playable online. This is full autonomy mode now, go. Within two minutes, four AI researchers were running in parallel: one on how other games reached the web, one on NOLF's code and file formats, one on tools, and one fetching the game. Two minutes later I was shouting at it. Me, 1 Oct, 17:49 UTC: > NOT THE DEMO! Full version! It had told the researchers to work from the free demo. It switched to the Game of the Year Edition, version 1.004. Here's the key fact behind this whole project. In 2001, Monolith did something generous: it published the source code of the game itself, all 315,324 lines, so fans could make mods. Every guard's behaviour, every gadget and every line of mission logic is in there. What Monolith never released is the engine: LithTech, the layer that draws the 3D world, plays the sound, moves the bodies and reads the files. The game's code calls that engine about 13,000 times, through 488 different functions. Without it, the code is a car with no road. So that was the plan, decided on day one: write a new engine that answers each of those calls the way the original did. Then compile it, with Monolith's game code on top, to WebAssembly, the fast machine code that browsers run. Both are written in C++, a programming language games of that era were built in, and the browser draws the result with WebGL, its built-in 3D graphics. We'd change as little of Monolith's code as possible. One wrinkle: the released code is version 1.003, but the game everyone played is the 1.004 Game of the Year Edition, so a few changes had to be found by reading the retail game's machine code. The AI researcher estimated two to four months to something playable, and nine to fifteen months for the whole game. The supervising agent added that this was "its judgement, not a measurement." It turned out to be wrong in a good direction. Technical file 2What Monolith released in 2001, and the missing engine Case 3. 1 October, the same afternoon ## Day one: every file opens A game from 2000 stores everything in its own formats: textures in DTX files, characters in ABC files, levels in DAT files, all packed into REZ archives. Before anything can be drawn, every one of them has to be read correctly. Here is what the commit log says happened that afternoon, in Central time. These are the times each helper's work was merged, so they're upper bounds, and several helpers ran in parallel. | | Time | What landed | 13:05 | All 4,160 of the game's textures decode | 13:08 | All 695 models load, skeletons and all | 13:18 | All 102 levels load, with zero bytes left unread | 13:43 | Monolith's game code compiles for the browser | 13:52 | A level appears in a browser tab, at 60 frames a second | 14:40 | Sound and the game's adaptive music | 14:43 | All 102 levels load and run 600 frames with the real game code It's the part of the story that sounds like magic. It isn't. Reading files is the easy part: you can check every file and know you're done. The hard part is behaviour. Does a guard react to a footstep like he did in 2000? Does Cate's head bob the same amount? There's no file to check that against. A trap showed up within the hour. Of the game's textures, 816 claim to be fully transparent everywhere. The original engine ignored that and took transparency from the surface instead. Nothing in the files says so. You only learn it by seeing it wrong. Technical file 3Reading 2000-era files: REZ, DTX, ABC and DAT Case 4. 1 to 2 October ## No gun, but wow Before the game itself ran, there was a simple viewer for walking around levels. I looked at the first screenshots and saw something wrong. Me, 1 Oct, 19:10 UTC: > The trees are all out of position in those. But that's part of your quality checking, isn't it? It was. Claude: "I approved the viewer after looking at a street, a bathroom and a sign, and none of those shots had a tree in them." The reason is lovely, once you know it. Monolith's tools let designers place a prop roughly and trust the game's own code to settle it on the floor when the level starts. Our quick viewer skipped that code, so trees floated. The fix was to stop polishing the viewer and run the real game. By late afternoon, it did. Me, 1 Oct, 22:28 UTC: > THAT IS UNBELIEVABLE! No gun, but wow. Objects floating and couldn't walk all the way up to the wall and stuff, but I guess all this is what you will do? The first playable build, about four and a half hours after the first commit. Two minutes later: "I was walking in the floor, fell through the floor etc. but it worked all the way through." Drag the line. The same window in Morocco on day one (left) and today (right), with the same objective on screen. They're two different moments, so compare the picture, not the framing: day one is dark and letterboxed with nothing in Cate's hands; today the lighting matches the original and she holds the rifle. The first clip we ever recorded, on 2 October: the main menu in a browser tab, with Guy Whitmore's score playing through the DirectMusic player we had to rebuild. Turn the sound on: music, no dialogue. Then the agents started tripping over each other. One tested against an old build and reported a black, featureless character that wasn't real. Another closed every browser on the machine to clean up after itself, and killed the other agents' tests mid-run. Nobody was in charge of who touched what. Me, 2 Oct, 00:19 UTC: > The agents need a set of standardized rules as they proceed with this: tests, rules, best practices, objective standards. Rigorous, detailed, etc., etc. Gaming agency standards. That became STANDARDS.md, the rulebook every agent had to follow: one owner per file, matched builds, a test that fails before every fix and passes after it. One line Claude wrote that night became the project's motto: "Agents will happily report success, so every task gets a check that's hard to fake." Technical file 4How a team of AI agents was actually run Case 5. 2 to 3 October ## Morocco The game opens in Morocco. UNITY, the 1960s spy agency Cate works for, is losing agents to H.A.R.M., and her first job is to keep an ambassador alive: a sniper's perch over the rooftops, a courtyard party, an ambush. Every NOLF fan knows it. It became our test level, and for days it hurt. October 2 was the busiest day of the project, with 268 commits. We worked in numbered rounds, and that one, round 4, ran about 25 helper jobs over two days: missions, gadgets, sound, physics and more. They fixed 58 defects and left 21 open. And Morocco kept getting worse in new ways. Me, 2 Oct, 15:11 UTC: > This page is now slower, and it hung here on the Morocco... Also, the bad guys in the Morocco scene are all jittery when they "arrive" for a while. Also, they don't look fully "clothed"; they look a little unfilled. Also, the bullet accuracy feels a little off? [...] It's slowed down since your last work. But everything else is taking shape amazingly well. Me, 2 Oct, 18:13 UTC: > I shot the bad guy on Morocco and he didn't die. Also, I don't remember those bad guys looking like that, with those faces. I think they were brown-skinned guys. Claude: "The fix was the wrong one and has been redone." The faces were a lesson in how small engine details matter. When the game spawns an enemy, it passes a short list of settings. Where a setting is missing, the original engine answered "nothing". Ours answered "empty text". To the game code, those are different, so our enemies got the wrong defaults. An agent's first fix papered over it. The second found the real cause. Then the data had its own say: the 17 gunmen in that ambush really are all one character type, and the game's data gives that type one pale, moustached face. My memory wasn't quite right either; the Moroccan agents I remembered are a different character. Then a guard's hat sat crooked. Only sometimes, depending on what else was on screen. It wasn't the hat. It was a speed shortcut in our renderer that sometimes drew the hat with the guard's bones instead of its own. And on the main menu, the sunglasses Cate wears were missing. Three separate bugs. Fixing them meant measuring every attachment (hats, guns, glasses, cigarettes) across 25 levels. Drag the line. The main menu on day one (left) and today (right): the sunglasses, the pistol and the Game of the Year version number, 1.004. Technical file 5The skew hat, the wrong faces, and the missing sunglasses Case 6. 3 to 4 October ## LLMs are not great at matching images By now we had robot players: AI-driven bots walking through each level, taking screenshots, with another AI comparing the pictures. It felt like progress. It wasn't. Me, 3 Oct, 21:26 UTC: > [...] Is having the bot do a walkthrough of the whole game the best possible way of building this out? "Honestly, no. It's useful, but it shouldn't be the main tool." A robot player's last view of Morocco before its run was marked failed, 4 October. It was sniping at the rooftop gunmen. (The Fullscreen button at the bottom right is the web page's, not the game's.) Did the engine fail, or the robot? From a picture, you can't tell. That was the problem. Me, 3 Oct, 21:30 UTC: > Hmmm, no, LLM image comparison isn't the best way, I think. All these other people doing game decomp and rebuilding, are they stuck in this place, or do they have engines they're working from? I feel like a more programmatic approach, mathematically matching to the original code, is the better way to go. LLMs are not great at matching images. This was the turn of the whole project. Claude pointed to how other fan rebuilds proved themselves, then built something better than screenshots. Take one function from the original engine. Run the real thing, from the 2000 program, inside a tiny simulated PC in the test harness. Run ours beside it. Feed both thousands of identical random inputs and compare the answers, bit for bit wherever we could. No opinions, no pictures. Same or not the same. Technical file 6Differential execution: the original engine in a simulated PC That same afternoon my Mac told me its disk was nearly full. Me, 3 Oct, 21:40 UTC: > My DISK IS AT 91%??!? About thirty old build folders were deleted. "OK, nvm... I'll get an external HDD!" The whole project later moved onto one. Then I rebooted, and the AI forgot what we'd decided. The new session couldn't find the notes from the old one, and proposed going back to the robot players we had just agreed to drop. Me, 4 Oct, 03:35 UTC: > Hold up! Look at the history... step 2 is what we WERE doing. I thought we changed? Look at the chat history from before I rebooted. Or am I missing something? "You're right, and I got it wrong. I missed my saved notes from that session because they were stored in a different place." Me, 4 Oct, 03:40 UTC: > Wait, I'd rather have you run the programmatic, scientific, deterministic story from the beginning. The bot runs were flaky. I was going in and out of Morocco and things were changing better, worse, better as we went along. What are you doing now? And what do you suggest? Quick answer. "Better, worse, better" was the problem in three words. Without a fixed yardstick, every change could fix one thing and quietly break another, and nobody would know. So I asked the obvious question. Me, 4 Oct, 03:43 UTC: > Hmm, are you sure you didn't miss some tests from literally EVERY possible angle? "No, I'm not." The comparison tool had run 39 engine functions. 36 matched. Under the strict rules we adopted that night, 33 counted as proven. Out of roughly 3,900. Thirty-three out of 3,900. That stung, and it was the most useful thing the AI said all week. It turned a vague feeling into a count we could grow. Today the scoreboard has 91 rows that match exactly, where a row can be a function, a shader or a whole move (Technical file 6). ## Running AI agents on something hard? This is the work we do at Brightwork Digital: setting up AI so it can be trusted, with checks it can't fake. Tell us what you're building. Case 7. 4 to 6 October ## The original game as judge On 5 October I showed Claude a screenshot of a Morocco guard whose head sat wrong on his body, and asked a harder question. Me, 5 Oct, 15:21 UTC: > Wouldn't stuff like this have come up from a root or systematic approach to this project? What are the foundational deep system gaps you missed? Not just these select issues. It listed six. The worst: "'Built' was counted as 'correct'." Every one of the 488 engine functions existed, but only a small share had been proven to behave like the original. Me, 5 Oct, 15:29 UTC: > Hmmm, OK, so then wouldn't it make sense to do this on one level, so Morocco, and then compare the output, having me take a look to ensure everything checks out? [...] But don't be a sycophant. Am I derailing a working path? Think of the end goal: a working game that looks like 2000 played in the browser. What is the BEST path to this goal? "You're not derailing; you've put your finger on a weakness in how I was running it." So we went deep instead of wide: one level, done completely. And we brought in the ultimate judge, the original 2000 game itself. We ran it on a rented Linux server, inside Wine, with no screen, fed it a recorded game session, and logged every object in every frame: where it stood, how it posed, what it wore. Then we ran our port on exactly the same inputs and compared. Drag the line. The judge and the defendant: frame 300 of the same recording, the original game under Wine (left) and the port (right). Close, not identical: our location caption shows a stray extra letter ("Le Chameau H"), the awning's stripes are yellow-cream and teal in the original and a darker green-teal in ours (a different awning from the one at frame 1500, but ours is darker there too), and the check on the man's trousers shows in ours but not in the original's picture, which may be resolution or a texture difference.The original's picture is grainier because Wine draws it in software at 640x480 in 16-bit colour, not because the game looked like that. The port side is a development build, not yet the live site. We captured four such frames (runs of the original are slow and capped); a second is in Technical file 7. Before trusting the judge, we checked it: same game files by checksum, 60 of 60; the same output every time it ran; and a known trap with how old processors rounded numbers. Then the findings came in, things no screenshot would ever show: - Sounds that had started before you joined a level never reached you. In 92 of the 102 levels, the background sound loops were simply missing. - Microsoft's compiler in 2000 worked out a function's inputs from right to left. Ours went left to right. In 14 places, Monolith's code rolled two or three random numbers in one line, so the dice came out in a different order. The judge caught the first; the rule found the rest. Enemies made different choices. - One of our own "bugs" turned out to be how the original really behaved. The judge cleared it. Technical file 7Replays, fingerprints and the reference run under Wine Late on 5 October, just before eleven at night my time, I played Morocco again. Me, 6 Oct, 03:56 UTC: > Morocco is looking and sounding and feeling perfect. The rooftop at dusk, the sniper's view over the courtyard, the ambassador's party, the brass of the score swelling when the assassins arrive. It played like I remembered. Over the next two days, the numbers came. On one recording, about eight minutes of play, a development build (not yet on the live site) matched the original frame for frame, for every object the game's server tracks: 28,299 frames. That was the recording we had tuned against, though. On a fresh one, the first difference shows up at frame 7,551, and that's the next thing to fix. Over the same days, the levels that start exactly like the original went from 26 of 65 to 57. Twelve seconds of the port in action, from the reel on the home page: a shoot-out with H.A.R.M. guards, the snow troopers, then Morocco. Everything here is drawn by the new engine running Monolith's code. Game sound only, no dialogue. Me, 6 Oct, 15:21 UTC: > Dang, it's taking long. Is this what other game reverse engineers would have to deal with, like GTA 5, etc.? Or are we building from the ground up? "We're doing the harder version," Claude said, pointing to fan rebuilds like OpenMW and OpenRCT2, which, it said, took volunteer teams years. Technical file 8Right to left: the compiler that rolled the dice in a different order Case 8. 6 to 7 October ## Spreading the load One Mac couldn't do it all. Checking every level against the original takes hours. So the work spread: an old Windows PC at home, a MacBook Air, the rented server, and finally Cloudflare's cloud machines, which by our estimate could check all 65 levels in about fifteen minutes for roughly a dollar. This was the part that hurt most for me, because it needed my hands. Remote logins refused, a password prompt hung, a setting on the PC wouldn't survive a restart. Me, 6 Oct, 22:02 UTC: > What is the best-practice approach for it? Should I be logged into the same Tailscale account on that Windows computer as I am here on this one? I don't... I have no idea how Tailscale works. I have no idea what I'm doing. Claude, honestly: "I can't get past its lock myself, and I shouldn't." The PC joined eventually. The MacBook never quite earned its place. Then Cloudflare. All 70 cloud machines finished their work and simply never switched off. The setting meant to put them to sleep didn't. New runs failed with "Maximum number of running container instances exceeded". There was no big bill (I checked, it was negligible), but it was a warning. The fix was to make each machine switch itself off 60 seconds after handing back its results. Then we added hard limits: 30 machines at a time and 300 runs a day. Me, 7 Oct, 17:56 UTC: > Work out how to make this entire process as robust as needed, but as simple as possible, covering all the details, not missing a thing, but simplifying, flattening, and spreading the load between machines. I would rather we reach a rapid deployment of the whole game on Cloudflare and eyeballing than constantly being blind to the progress. That afternoon, the whole game went up on a private web page: a mission board where I could open any level in a click. And then I stopped. "OK, pause your work on this for now, and save everything." It was a lot. 156 commits hadn't even been sent to GitHub. We wrote one file to read first when coming back, and I walked away for a day. Technical file 9The fleet, the cloud, and the bill that never came Case 9. 8 to 9 October ## Going public When I came back, the game was good enough to share. I wanted a front door worthy of it. Me, 8 Oct, 19:20 UTC: > I want you to build a proper landing page for this, and remove the page login. [...] I want this to be a "public-ready" page that feels properly retro, like the old internet, but without being hard to navigate. [...] It should trigger all the memories and nostalgia for a visitor. [...] The end result: a nostalgic, retro landing page, super easy to play the game, play all levels, NO clutter. Nine rounds of design, scored by AI critics. Their average peaked at 7.8 against our target of 8; one critic went as high as 8.3, the other never above 7.7. Lighthouse, Google's page checker, scored 100 for accessibility, best practices and search, and 99 to 100 for speed. Drag the line. The private mission board I used to judge each level (left) and the public front door (right). Then came a long list: accounts so your saves follow you, a 1960s television playing a 90-second reel cut from the port, the game's own music in a little player, keyboard shortcuts, a bug reporter that sends us your exact spot in the level. Not everything landed. The TV reel scored 6 out of 10 with its AI critics, because it's almost all shot from the same first-person angle. We shipped it anyway, and said so. The gadget pictures got some love too. The originals in the game's files are 48 pixels square. We enlarged them four times with an AI upscaler. They're cleaner, and not perfect: the coin's face came out a little smoothed. Drag the line. The lighter, the robot poodle, the exploding lipstick and the zip-cord belt, as shipped (left) and upscaled (right). Gadget art by Monolith. The test suite had grown, too. Every change ran the full gate: a 14-minute tier of browser gameplay tests and a 26-minute tier of robot playthroughs, on top of the faster tiers. Me, 9 Oct, 13:24 UTC: > WHY do we have this??? [...] Genuinely needed?? I'd pasted two rows of the test table: browser gameplay tests, 14 minutes, and robot playthroughs of whole levels, 26 minutes. They weren't, not on every change. A change to this page now ships after five minutes of checks. A change to the engine still gets the full set. Me, 9 Oct, 14:15 UTC: > [...] The user signup/login needs to look less retro. It looks a little "dodgy", like I'm gonna hack them, lol. Make it a little more modern and normal, but still on brand. Ensure there are systems in place to prevent this Cloudflare deployment of NOLF from skyrocketing billing for me. Put safeguards in place if you haven't already. Fair. The sign-in window got a plain, modern look, and a guard now checks the site's usage every 15 minutes and pauses the game if it passes a set limit. Drag the line. "Dodgy" (left) and fixed (right). On 9 October I bought playnolf.com, and the site moved to its own address. That brings us to this page. Technical file 10How the site is served, and how your saves are kept ## Got an old game, site or app that deserves a second life? We built every layer of this, from the engine to the accounts. Brightwork Digital takes on websites, software, apps and ports like this one. The briefing ## The stack, and how it all works Here's the whole machine in plain terms. Each layer has a technical file if you want the detail. Two programs in one tab, talking the way the original client and server did in 2000. The layers of playnolf.com | | Layer | What we used | What it does | The game | Monolith's own C++, from 2001 | Everything that makes it NOLF: guards, gadgets, missions, menus, jokes | The engine | New C++17, written for this project, guided by Monolith's published interface and, for some systems, its later Jupiter engine | Draws the world, plays sound, moves bodies, reads the files | The translator | Emscripten and WebAssembly | Turns both into programs a browser runs at near-native speed | Pictures | WebGL 2 | The browser's 3D graphics, standing in for Direct3D | Sound and music | Web Audio and our own DirectMusic player | Every footstep, and the score that follows the action | Hosting | A Cloudflare Worker and R2 storage | Serves the page and streams the game's files, a level at a time | Saves | Your browser, plus an optional account | Keeps your progress, and carries it to another computer | The proof | The original game under Wine, a CPU emulator, recorded replays | Checks that ours does exactly what the 2000 game did | The builders | Claude (Anthropic), one supervisor and many helpers | Wrote the code and the tests, under one person's direction A cutscene from the castle assault in the Alps, late in the game. Cutscenes are scripted by Monolith's code and played by the new engine, with the camera, the troopers and the blast all driven the way they were in 2000. Game sound only, no dialogue. Technical file 11Inside the new engine: renderer, physics, sound Technical file 12Every prompt that steered the project, in order Debriefing ## What we learned If you're thinking of pointing AI agents at something big, here's what this taught us. - The AI is fast at what can be checked, and confident about what can't. Reading 4,160 textures took minutes because each one either decodes or doesn't. Behaviour took days, because "looks right" isn't a test. - A reference isn't a judge. Our first physics and collision code followed Monolith's later engine closely, and was wrong for a long time, until it was run against the original. - Give it a judge it can't argue with. The turn came when we stopped asking an AI whether two pictures matched and started comparing numbers from the original game. - Count the denominator. "33 proven" felt bad. It was also the first honest number, and honest numbers can be made bigger. - Write things down where the next session will find them. A reboot cost us a decision because the notes were in the wrong place. Now there's one file to read first. - Agents need rules like any team. One owner per file. No closing other people's windows. A failing test before every fix. - Keep a record of when "done" wasn't. A few from this project: | | The AI said | What was true | The level viewer looks right | It had checked a street, a bathroom and a sign. The trees floated. | A model renders broken | The test was broken, not the model. | The guards' faces are fixed | The first fix was wrong and had to be redone. | A collision bug in the Alps is fixed | Fixed on a branch that was never merged. | Six levels are blocked by gadgets | The test robot wasn't using the gadgets properly. | Round 4 is complete | Its own review corrected 15 earlier statements. - Your taste still matters. The AI never said the trees floated, the hat was crooked or the sign-in looked dodgy. A person who loves the game did. What we can't tell you: the AI bill. Nobody recorded total token spend, so any number would be a guess. The hosting is $5 a month plus storage. What isn't done: online multiplayer, a sharper "enhanced" look, and the eight levels that don't yet start exactly like the original. In the spirit of honesty: the live site runs from a branch that isn't merged into the main line yet, and the main line's full test run isn't green. If you play and something's wrong, press Ctrl+Shift+B (⌘⇧B on a Mac) in the game. The report comes straight to us with your position and a screenshot. Credits ## Thank you, Monolith None of this exists without the people who made the game. No One Lives Forever was developed by Monolith Productions and published by Fox Interactive in November 2000. The Game of the Year Edition, with its extra mission, followed in October 2001. Craig Hubbard led the design and shaped its 1960s world. Kevin Stephens led the programming. Wes Saulsberry is credited as the artist. Chris Miller and Samantha Ryan produced it. Guy Whitmore wrote the score, an adaptive one built on Microsoft's DirectMusic that rises and falls with the action; rebuilding his music engine was one of the hardest and most rewarding parts of this project. Kit Harris gave Cate Archer her voice. Dozens more people built the levels, wrote the guards' conversations and hid the jokes, and they are named in the game's own end credits. Everything you see and hear when you play is theirs: the art, the levels, the voices and the music are the original Game of the Year Edition files, read unchanged. What we wrote is the machinery underneath. Monolith also released the game's source code in 2001 so fans could keep making things. That decision, a quarter of a century ago, is the only reason a project like this is possible. Monolith closed in February 2025. The game deserved better than limbo. We hope it gets an official release one day, and if it does, buy it. Until then, thanks to the fans who kept it alive: the patchers, the wiki writers, and the makers of the NOLF Modernizer and the native NOLF 2 port. Sources: Wikipedia (credits and release dates); Time Extension (Hubbard, Stephens and Miller); Game Developer (the 2001 source release). The full cast and crew are in the game's own end credits. ## About Brightwork Digital We're a small studio that builds websites, software and apps, and helps teams put AI to work with checks that hold up. Everything on playnolf.com is ours, from the engine to the accounts, the bug reporter, this article and the guard on the hosting bill. See what else we do, or tell us about your project. ## Questions people ask ### Can I play No One Lives Forever in a web browser? Yes. playnolf.com runs the full single-player game, all 15 missions plus the Game of the Year bonus mission, in a desktop browser with a keyboard and mouse. Nothing is installed. Phones and tablets aren't supported yet. ### How does it run in a browser? On a new engine compiled to WebAssembly. Monolith released the game's own code in 2001 but not the LithTech engine underneath it, so we wrote a new LithTech-2-compatible engine in C++, compiled it and Monolith's code to WebAssembly, and draw the game with WebGL. The game reads its original data files. ### Is this an emulator? No. An emulated Windows PC in a browser is far too slow for a 3D game. The game's own code runs directly, as WebAssembly, on a rebuilt engine. ### Was the engine decompiled? No, it wasn't decompiled into source. The engine was written new against the interface Monolith published with the game code, with the circulating source of Monolith's later Jupiter engine (NOLF 2's) as a guide for some systems, such as collision and animation timing. The original 2000 program was then read and run, in an emulator and under Wine, to measure where ours differed. Several of the Jupiter-based parts turned out to be wrong, and were corrected. ### How long did it take? Ten days. That's from the first question on 30 September 2026 to the site's own address on 9 October 2026: one person directing AI agents, 267 prompts and 771 commits. 57 of 65 levels start exactly like the original, and the work continues. ### Who made No One Lives Forever? Monolith Productions developed it and Fox Interactive published it in November 2000. Craig Hubbard was the lead designer, Guy Whitmore composed the score and Kit Harris voiced Cate Archer. ### Why can't you buy No One Lives Forever? Because nobody can say for sure who owns it. Its rights are reported to be split between Warner Bros., Activision and 20th Century Fox (now part of Disney), and none has confirmed who owns what. In 2014 Nightdive Studios filed trademarks for the series and acquired the source code, hoping to bring it back; Warner Bros. opposed, and the effort stalled. ### Where do the game files come from? They're the original Game of the Year Edition files, unchanged. Version 1.004 streams from the site's storage on Cloudflare. Nothing on the site is sold, and there are no ads. A version that reads your own copy of the game is planned. ### Is it free to play? Yes. There's nothing to buy, no ads and no sign-up needed; an account is optional and only keeps your saves. ### Can I play it on a phone or tablet? Not yet. It needs a keyboard and mouse, so it runs in desktop browsers such as Chrome, Edge, Safari and Firefox. ### Does multiplayer work? Not yet. The single-player campaign came first. The 37 multiplayer maps load, but online play isn't built. ### Can AI agents build a game engine? With close human direction, yes. Here they wrote the engine, the tests and the website. They also made confident mistakes, forgot decisions and reported success too early. What worked was measuring everything against the original game. ### Who built playnolf.com? Brightwork Digital. We're a small studio that builds websites, software and apps and advises on AI. ## Work with the people who built this Ten days, a 46,000-line engine, and 57 of 65 levels proven against the original game. Brightwork Digital builds websites, software and apps, and helps teams put AI to work with checks that hold up. If this story is the kind of stubborn, careful work you need, tell us about your project. A person reads every message and replies within two working days, with questions or a short plan. No mailing list, no sales sequence. Or email hello@brightworkdigital.co, or visit brightworkdigital.co. playnolf.com is an unofficial fan port: a thank-you to the people who made the game. No infringement is intended, and it will come down if the rights holders ask. If No One Lives Forever is ever sold again, go and buy it. No One Lives Forever © 2000–2001 Monolith Productions, Inc. The Operative, No One Lives Forever and their logos are trademarks of their owners. Screenshots on this page are from the port and the original game. Written and built by Brightwork Digital. Back to the missions. Technical file 1 ## Why not an emulator, or Wine? Three ways exist to get a Windows game from 2000 into a browser tab. ### 1. Emulate a whole PC in the browser Projects like v86 and Boxedwine run an x86 PC, or Windows programs, inside JavaScript. They are remarkable, and fine for a text editor. A 3D shooter needs millions of instructions per frame, each translated on the fly, plus a 3D card that the emulator must fake in software. A reported experiment ran Tribes 2, a 2001 shooter, on Boxedwine in the browser at 0.4 to 0.7 frames per second, with ten-minute mission loads. We didn't measure it ourselves. ### 2. Wine on a server, streamed as video Wine makes Windows programs run on Linux and macOS. It would run NOLF well, on one machine. To put that on a web page you'd stream video from a server per player, which is expensive, laggy and ugly. Wine did come back later, as the judge (Technical file 7). ### 3. Compile the game to WebAssembly WebAssembly lets a browser run C and C++ at close to native speed. If the game's code is available, you compile it with Emscripten. NOLF's game code is available. Its engine isn't, so the engine had to be rebuilt. That's this project. Technical file 2 ## What Monolith released, and the missing engine In June 2001 Monolith published the source code of NOLF version 1.003 for the mod community. It's a Visual C++ 6 workspace with: | | Part | Lines | What it is | ObjectDLL | 131,145 | The server side: AI, weapons, gadgets, doors, mission scripts | ClientShellDLL | 125,418 | The client side: menus, HUD, camera, player movement, effects | Shared | 27,524 | Code both sides use | ClientRes | 6,403 | Strings and resources | Headers | 24,834 | Including the LithTech SDK: the engine's published interface | Total | 315,324 | Not included: the engine (lithtech.exe and its renderer and sound libraries), the tools, and the game data. The SDK headers are the crucial piece. They declare about 350 entry points, the exact list of things the game may ask the engine to do: create an object, move it, play a sound, test a ray against the world. The game uses 488 distinct engine functions (counting interface methods) at about 13,000 call sites. Each one is a promise we had to keep. ### Why not borrow a later LithTech? Monolith's later Jupiter engine (used by NOLF 2) has circulated publicly, but it's too far away: level files moved from version 66 to 85, models from ABC to LTB, and the message system changed. We didn't adopt it as the engine, because bending it back would have been harder than writing to the 2001 interface directly. We did use its source as a reference for how some systems work, and the comparison with the original later showed where that reference was wrong: physics and collision code first modelled on Jupiter was, in our own notes' words, "wrong for a long time". ### Why not a matching decompilation? Projects like the Super Mario 64 decompilation rebuild source code that compiles back to the original machine code byte for byte. That needs the original compiler and a great deal of function-by-function reconstruction. NOLF's engine shipped stripped, without symbols, and it's the engine we needed, not the game code. So the engine was written fresh to the published interface, and the original program served as the reference for behaviour, and as the judge. Sources: the 2001 release readme; Game Developer, 21 June 2001; the project's research notes. Technical file 3 ## Reading 2000-era files The GOTY Edition ships five REZ archives, about 1.1 GB. Inside them: | | Format | Holds | Count | DTX | Textures, with mipmaps; some compressed (DXT1/3/5), some raw 32-bit | 4,160 | ABC | Models: meshes, skeletons, animations, attachment sockets, levels of detail | 695 | DAT (v66) | Levels: BSP geometry, lightmaps, placed objects and their properties | 102 | WAV and DirectMusic | Sounds, and the adaptive score as segments, styles and DLS instruments | 4,799 sounds, 421 segments Each loader was proven the strict way: every retail file parsed, and for levels, every byte accounted for ("zero unread bytes"). The levels hold 899,957 lightmap images and 79,968 placed objects across 151 classes. ### Traps - Some archive entries are replaced by later archives, so 4,166 entries hold 4,160 distinct textures. - 816 uncompressed textures store zero alpha everywhere. The original engine takes transparency from the surface's flags, not the texture. - The first model render that "looked broken" was a broken test, not a broken loader. Test the test. For the browser, the archives are unpacked and stored per file, by content hash, so a level fetches only what it needs and your browser caches it. Technical file 4 ## How the AI team was run One supervising Claude session (Opus) planned the work and reviewed results. Helper agents (Sonnet), each on its own branch of the code, did the jobs: one mission group, the renderer, sound, physics. Across the project, 77 helper branches were merged. Round 4 alone had about 25 helper jobs over two days. I capped them at five at a time on 2 October, because we were burning tokens, and at two from 5 October. Nobody recorded how many ran at the same moment, and we didn't record total token spend either. ### The rules that came from mistakes - One owner per file. Two agents editing one file meant one silently undid the other. - Matched builds. The client and server are separate programs. An agent testing a new client against an old server reported bugs that didn't exist. - Own your ports, never kill by name. Each agent got its own range of local web ports. A helper that closed "every Chrome" killed everyone else's tests. - Red before, green after. Every fix comes with a test that fails before it and passes after it, so "fixed" means something. - Checks that are hard to fake. Agents will report success. So success is a number from a tool, not a sentence from an agent. ### The gate A single script, tools/verify.sh, runs the checks in tiers: unit tests and file loaders first, then the engine against the original's numbers, then the real game in a real browser (walking, shooting, saving, menus). It grew from 68 checks on 2 October to 125 later that day and about 200 the next. Later it was trimmed: what runs now depends on what changed. Technical file 5 ## The hat, the faces and the sunglasses ### The skew hat Skinned characters are drawn by sending their bone positions to the graphics card, then letting the card bend the mesh. To save time, our renderer remembered which bone sets were already uploaded and skipped re-sending them. But some uploads took a longer path that bypassed that memory. When a small model, like a hat, came straight after a character, the card still held the character's bones, and drew the hat with them. That's why it only happened sometimes: it depended on draw order. The fix made the cache forget a slot whenever a long upload went around it. ### The wrong faces When the game spawns an object from a text command, the engine builds its property list. The original adds only the properties named in the command. Our first attempt filled in every class default too. And when the game asked for a text property that wasn't there, ours answered "empty text" where the original answered "nothing". The game checks for nothing before it applies its own default head, so our guards got the wrong one. The second fix matched the original exactly, and came with tests that check the heads the game's data prescribes for all 17 ambush gunmen. ### The sunglasses On the main menu, Cate wears sunglasses and holds a pistol, each attached to a socket on her model. On day one they were missing or floating. There were three separate causes: - The renderer ignored animation requests that arrived before the model file had loaded, so the glasses stayed in their "world" pose instead of "in hand". - Cate is drawn at 1.3 times scale, and the socket's rotation was taken from the scaled matrix, which skews it. - The game side computed socket positions with a second copy of the pose maths, which disagreed with what was drawn. One bystander's cigarette sat 40 units from her hand. The fix came with a numeric audit of every attachment across 25 levels, measuring the gap between each attachment and its socket. Technical file 6 ## Differential execution The idea: take one function from the original engine and prove ours gives identical answers. - Find the function in the original 2000 program, lithtech.exe, by its behaviour and the strings it uses. - Load the program into Unicorn, a CPU emulator used as a library, inside the test harness. Set up just enough memory for that one function. - Generate thousands of random inputs, including the nasty edge cases: zero, negative zero, huge values, NaN. - Call the original in the emulator and ours in WebAssembly with each input, and compare every output. Most functions must match to the last bit. The 34 "within tolerance" rows are looser by design: GPU shaders checked texel by texel, the sound mixer's gain (worst case 1.08 per cent, against an allowance of 1 per cent plus two steps of 2,048), and collision sweeps where our algorithm differs from the original's and we compare where the body ends up (to 0.15 units), whether it's standing, and the surface normal. Each row states its tolerance. Maths, physics and movement functions went first because they're pure: same input, same output. A mismatch is a bug in ours, with the exact input that shows it. ### The denominator We found 3,981 function starts in the original program. It has no symbols, so the boundaries are a careful guess. Between 2,152 and 2,905 can be reached by NOLF, and 482 of those are Windows plumbing. Of the functions in the parts a player feels (movement, physics, combat, animation, AI), 731 can be compared one at a time. On 4 October the tool had run 39; 36 matched, and 33 counted as proven under the strict rules: 4.5 per cent of those 731. The scoreboard now holds 143 rows (functions, shaders and whole-move checks): 91 identical (64 for every input, 27 across their real range), 34 within a stated tolerance, 4 that still differ (three of the ILTTransform maths functions and a 3x3 matrix-vector product), and 14 that can't be tested alone (6 out of scope, 5 that only make sense as a whole move, 3 that need game state). Everything else rests on tests, code reading and comparing the whole game frame by frame (Technical file 7), which catches the bugs that live between functions. The scoreboard today, from tools/diffexec/scoreboard.json: 143 rows of functions, shaders and whole-move checks. Orange is what still differs. ### A finding worth the trouble The original's ray test, which decides what a bullet or a line of sight hits, differed from the version we had borrowed from Monolith's later engine in 12 per cent of 5,000 test rays cast through eight real levels. Small things. Exactly the things that make a game feel slightly off. ### The floating-point problem This was the hardest part to make identical. The 2000 program does its maths on the x87 unit. Set to single precision, as we believe the game ran it, it rounds ordinary adds and multiplies just as WebAssembly does (which has no fused multiply-add to blur things further). Why we believe single precision: the 2000 renderer creates its device through DirectDraw 7, which sets single precision on that thread, and it never asks to keep the old setting; and our float engine matches the original bit for bit at that setting and at no other. What we don't know is which thread the single-player server ran on. Beyond that setting, the original's maths differs from ours in three ways: it adds terms in the order its compiler chose; its sine, arccosine and exponential instructions hand back extra-precise results that the game uses before rounding; and its numbers can grow far larger or smaller before they overflow. Each of those makes the last digit differ, and those digits compound: a model's pose, the direction of a bullet, whether a sphere touches an object. We reproduced it function by function. In the original's 4x4 matrix product, each of the 16 elements adds its terms in its own order, and we use the same ones. Quaternion blending uses the 2000 C runtime's own arccosine, not a modern library's. Friction decay uses the original's x87 instruction sequence, not a standard expf. Where the x87's wider exponent range matters (products smaller than about 1e-45) we stop, and say so: across all 695 models, 72,080 of 72,108 animation results are identical and 28 still differ that way. The reference run under Wine has to pass the same test: before trusting it, the game's x87 control word reads 0x7F, single precision, at every frame of the Wine run. That shows Wine matches our assumption about Windows, not that Windows itself does. That single-precision setting is our inference from the game's startup code, and our notes mark it as assumed: nobody has yet run the original on real Windows as a final referee. One more caveat: Unicorn computes the x87's sine and cosine with the host's double-precision library, which can differ from real hardware in about one case in 2^29. We haven't seen one. Technical file 7 ## Replays and the reference run ### Replays: a player piano A replay stores only what the engine received from the player: the input events, frame by frame. To play one back, the harness steps the game in fixed frames from a simulated clock, so a busy computer can't change the result. Alongside the inputs, each frame records a fingerprint of the game's state, one row per object: class, name, model, skins, position, rotation, animation and its time, node matrices, attachments. On the client side the rows also say what was drawn: the model file and level of detail, the texture bound to each piece, the transform and colour. A single hash per frame would tell you the game diverged. One row per object tells you which object. Every 60 frames the recording also keeps one byte per group of fields per row, so a divergence report reads like "frame 7,551, this object, node matrices"; run against a baseline build, it names the exact field. A replay is only useful if the engine is deterministic, so the first test is simply to play the same inputs three times and require byte-identical fingerprint streams. That test found the real sources of drift. Monolith's shell casings never set their own spin, so they spun by whatever happened to be left in memory; ours now starts from zeroed memory. And a few effects read the wall clock, which a replay has to hold still. Goldens are versioned. When we added "what was drawn" to the rows, old goldens were refused rather than quietly compared on fewer fields. There are 23 of them; one training level's runs to 90,307 frames. Note what a golden is: our own build's output, frozen, so it catches changes, not unfaithfulness. Only the runs of the original judge fidelity, and long ones exist so far only for Morocco's first two parts. ### The reference run The original game, version 1.004, runs under Wine in a container on a rented Linux server, at low priority, with no screen. A small hook logs the same per-object state per frame. Fed the same replay inputs, it gives the original's answer for every frame. Before trusting it, we checked: the same 60 game files by checksum, the same output on every repeat run, and the same output on three different machines. Frame 1500, same inputs. The world lines up: walls, tower, people, the ammunition count. The differences we can see: the shell casing is at a different point in its flight; the rifle sits lower and further right in the original; the palm's fronds are drawn differently; and the awning in front isn't just darker in ours but a different colour: red and white in the original, brown and teal in ours. Ours also shows a second forearm at the lower right, and the tower's cap looks different. Wine's 640x480, 16-bit software picture explains the grain, not a change of colour, so the awning, the rifle pose and the palm are on the list to trace. The judge at work: the original 2000 game, running headless under Wine in a Cloudflare container, 15 stills taken at frames 150, 300, 450 and so on to 3,000, shown one per second. A slideshow, not video: each still takes the runner about 55 seconds. No sound. Even the judge needs care. Making these pictures, our run of the original wandered off the recording after about frame 3,000, and by frame 8,660 its mission had failed. We'd left out options the earlier full comparisons used to keep its sound and random-number streams in step, which is probably why; we didn't rerun to confirm. Roughly twenty cents of cloud time by our estimate, and one more lesson. ### How the original's behaviour is measured We never decide how the original behaves by reading its code alone; we run it. There are two instruments. For one function, Unicorn runs the original's own machine code on thousands of inputs and compares answers directly (Technical file 6): exact, but only for functions that can run alone. For the whole game, the original runs under Wine with a small proxy library that feeds the recorded inputs at their frames, holds the clock to the replay's fixed step (the retail game normally uses real elapsed time, so this is itself a small difference from how people played it), and writes every object's state each frame. It logs engine state, never pixels. The two meet in the middle: the Wine run has to agree with what Unicorn already proved before it judges anything. Our engine notes mark every behaviour as verified (executed or read in the original), assumed, or corrected, and the corrections stay in the notes. ### What it proved - A typical finding, from Morocco part 2: an AI at rest (menemy1a) went to sleep at the end of frame 2,669 in ours and 2,670 in the original, so its clock read 8,100 ms where the original's read 8,116. The original counts its deactivation timer down before it updates the object, ours after. One frame, found, fixed, and locked in by a test that was red before the fix. - On the recording we tuned against (about eight minutes), a development build not yet on the live site matches Morocco part 1 on every server object from frame 2 to 28,299, and part 2 to frame 6,659. Client-side, the clock of props on their base animation starts 179 ms later in the original than in ours, a constant offset nobody can see; the player's own weapon objects and some effects have no counterpart yet. On fresh recordings, the first differences come at frames 7,551 and 3,128. Those are next. - 57 of 65 levels start like the original: 300 frames (ten seconds) with no input, every object compared, with two classes of the original's invisible markers excused. That proves loading, spawning and the first AI and animation state. It doesn't prove a level plays the same; that takes a long replay, and so far only the first mission has one. Eight levels still differ. - 20 groups of differences remain on the list. - Background sound loops started before a client joined never reached it: 92 of 102 levels were missing them. - One of our own defect reports was wrong: the original really did it that way. Technical file 8 ## Right to left C++ doesn't say in what order a function's arguments are worked out. Here is a real line from Monolith's code, where an enemy picks the spot it will aim at next (AITarget.cpp): m_vNextShootPosition = LTVector(GetRandom(-1.0f,1.0f), GetRandom(-.5f,.5f), GetRandom(-1.0f,1.0f)); Microsoft Visual C++ 6, which built the original, in practice evaluates those arguments right to left, because its calling convention pushes the last argument first. The third random number is drawn first. Clang, which builds our WebAssembly, goes left to right. Same seed, same three numbers, in a different order. So every shot an assassin fired drifted from the original. The game still works, just differently, and from that moment the replay against the original drifts too. The reference run caught the first one: at frame 4,797 of Morocco, an assassin's shot ended at a different point in the original and hit another surface. We then applied the same rule to 13 more call sites that draw several numbers at once (debris, colours, sprinkler spray, a flashlight, a laser beam). Three were confirmed against the original's machine code; the rest by reading the order of the calls. Each was rewritten to draw its numbers first, in the original's order: // MSVC evaluates the arguments of a call right to left LTFLOAT fNextZ = GetRandom(-1.0f,1.0f); LTFLOAT fNextY = GetRandom(-.5f,.5f); LTFLOAT fNextX = GetRandom(-1.0f,1.0f); m_vNextShootPosition = LTVector(fNextX, fNextY, fNextZ); Twelve shots from an enemy, same seed, same three random numbers per shot, seen from the front, sideways and up, on a unit scale: the offset the game rolls is normalised to length 1, so every shot lies on a unit sphere, and the axes show where on it (left and right from -1 to +1). Depth differs too, which a front view can't show. Only the order the numbers were drawn in differs. Computed with the game's own GetRandom and the 2000 Microsoft runtime's generator. It's the kind of bug no player would ever name, and every player would eventually feel. Technical file 9 ## The fleet, and the bill | | Machine | Job | Mac mini (the hub) | Builds, the browser tests, the supervising agent | Rented Linux server | The original game under Wine, at low priority beside other work | Windows PC at home | A second runner for the test gate, through WSL2 | Cloudflare Containers | Reference runs of all 65 levels in parallel | Cloudflare Workers and R2 | The website and the game's files Every machine had to prove it gives identical results before its answers counted: the same reference log, with the same checksum, on the server, the PC and a third machine. ### The containers that wouldn't stop Each reference run started a container. The setting that should have put idle containers to sleep didn't, so all 70 stayed up and new runs were refused. The fix: each container now exits 60 seconds after its results are collected. Then, hard limits: 30 at once and 300 runs a day. The sleep timer stays, shortened to five minutes, but it was the setting that had failed; the self-exit is the real fix. A full sweep of 65 levels costs about a dollar by our estimate; the hosting plan is $5 a month, plus storage. The site now has its own guard too: every 15 minutes it reads its usage, and if traffic ever pushed the bill past a set limit, the game would pause itself. Technical file 10 ## How the site is served The whole site is one Cloudflare Worker, a small program that runs at Cloudflare's edge, with two R2 storage buckets: one for the game's files, one for accounts. ### The game's files The art, sound and levels are Monolith's, from the Game of the Year Edition, version 1.004, unchanged. Every file is stored under the hash of its contents, compressed with Brotli: 10,803 files, about 1.08 GB raw and 467 MB stored. A manifest lists which hashes make up the current build. The page downloads the two programs and the menu's files first, about 8 MB, so the menu appears in seconds. Then it fetches the rest in the background, and each level's own files (13.8 MB for a typical level) before you enter it, while the game shows its own loading screen. The browser keeps everything, and since a hash never changes, it can be cached forever. A second visit loads a level from your own disk with no download at all. ### Accounts and saves Accounts are optional. Passwords are stored only as PBKDF2 hashes (100,000 rounds, a random salt each). Emails are encrypted, and looked up by a keyed hash, so the stored data never shows an address. Saves live in your browser; signed in, they also sync to your account, so they follow you to another computer. You get ten save slots, a quick save (Cmd+Shift+S on a Mac, Ctrl+Shift+S elsewhere, or F6; F9 loads it), and an automatic save at the start of each level. Esc pauses the game and opens a card with every control. ### Bug reports Press Ctrl+Shift+B (⌘⇧B on a Mac) anywhere. In the game, the report carries the level, your exact position and view, the game time, the last lines of the engine's log, and a screenshot taken before the window opened. Technical file 11 ## Inside the new engine The engine is 46,260 lines of C++17 in 221 files, counting its own tests and data tables (43,567 without the tables). That excludes vendored libraries: the dr_libs WAV and MP3 decoders and stb's image writer (three copies), 19,811 lines between them. With Monolith's code on top it compiles, with Emscripten, to two WebAssembly programs: the client (2.9 MB) and the server (2.7 MB), 1.3 MB together once compressed. | | Part | What it does | Interfaces | The 488 engine functions the game calls (ILTServer, ILTClient, ILTPhysics, ILTModel and the rest). None may be a silent no-op: a stub logs its name, and any stub hit in a test is a defect. | Loaders | Small C++ libraries for DTX, DAT v66, ABC, string tables and a case-insensitive file system over the REZ archives. Each parses every retail file in a test. | Renderer | WebGL 2 with 4x anti-aliasing and compressed textures (details below). | Physics | Shapes swept against the level's polygons, as the original does. The player is a cylinder whose radius is the smaller of its dimensions, read from the original and confirmed in 5,000 of 5,000 emulated runs. Outcomes agree within 0.1 per cent at 60, 120 and 144 frames a second. At 30, a scripted walk ends 4.9 per cent short: measured, logged as a major defect, not yet traced. | Sound | 4,799 sounds through Web Audio, with the original mixer's rules read from the 2000 sound library: 32 voices, its pan law and its voice arbitration. The curve between a sound's inner and outer radius is linear by assumption; reverb, occlusion and Doppler aren't implemented. | Music | Our own DirectMusic player (details below). | Compatibility | Shadow Windows headers and shims, so Monolith's Win32 calls work unedited. Even rand() is the 2000 Microsoft runtime's generator, because weapon spread and AI depend on its exact sequence. ### How client and server fit together The server is the game: objects, AI and physics, on a fixed 1/30 s step (the original used real elapsed time, clamped, so the fixed step is our choice), with model animation advancing in whole milliseconds (33 or 34) as the original's did. The client draws at your screen's rate. Objects the server drives are drawn one server tick (33 ms) behind and blended between their last two states, so motion stays even from 60 to 144 frames a second. Your own character is simulated on the client at each frame's real time step, so the controls don't feel 33 ms late. Measured with a synthetic clock at 60, 120 and 144 Hz, the drawn positions of doors, AI and projectiles match our own interpolation of the server's ticks to 0.000 units, and the player's speed is within 0.01 per cent. That shows the smoothing is exact, not that it matches the original's. What we don't know is how the original interpolated: its Prediction and MaxExtrapolateTime settings exist in the program, but the code that reads them wasn't found, so our interpolation delay is an assumption, and labelled as one. NOLF was built as a client and a server even in single player, and both halves use the same names for different things, so they can't be linked into one program. They run as two WebAssembly modules and swap packets in the original's message format, in memory. ### The renderer The world is drawn in passes: sky, opaque, then sorted translucent. Surfaces are batched by layer, blend mode, lightmap atlas page and texture. Static opaque batches are cut into chunks of at most 96 triangles along a Morton curve, each with a bounding box, so a frustum test can merge neighbouring chunks back into one draw call (checked pixel-identical against the uncut path). In Morocco's second part (median of a scripted walk, before the visibility lists below were decoded), switching on the chunked culling and a per-program uniform cache took the median frame from 607 draw calls and 47,098 triangles to 488 and 33,671, and uniform uploads from 7,166 to 957. One frame, two layers: the lighting the level's designers baked in 2000 (left), and the final picture, textures times light times two (right). Same build, same camera, game clock frozen. In the light-only view the torch glow draws as a plain block and the characters as flat grey. Lightmaps off (left) and on (right), using the game's own setting. Most of NOLF's mood is in those baked shadows, which is why reading them exactly mattered. Characters are skinned on the GPU: node matrices sit in a uniform array, and each vertex carries up to four weights. That's exactly where the skew-hat bug lived: the cache that skipped re-sending a bone set didn't see the long upload path (Technical file 5). Lightmaps follow the original's pass: texture times lightmap times two, clamped, because the game's shipped config sets Saturate 1, which lets a lightmap brighten a texture as well as darken it. The exception is baked vertex-colour polygons, which the original doesn't double. Visibility uses the level's own precomputed lists, which saves 4 to 46 per cent of the triangles drawn, depending on the level, across 16 levels measured. In two Morocco poses it drops a few distant rooftop pieces; the original's own lists probably hide them too, but that's not yet compared. ### Animation Animation was the group with the most test cases: every model in the game. The original's model loader, key interpolation and animation tracker run in the emulator on the real model files: all 695 of them, 72,108 cases, 72,080 identical. Before the animation fixes, our engine differed in 1,846 of 2,000 tracker cases and 1,426 of 2,000 pose cases, and nearly all of it was invisible in a screenshot. One part did: the renderer had numbered child-model animations wrongly on 110 of the 695 models, so sitting poses and cutscene moves played the wrong animation. The 28 left over are the x87's exponent range on products below 1e-45. ### Collision The player is a vertical cylinder swept against the level's polygons, as in the original; other objects are boxes. When you slide along a wall, the original steps your position sideways by twice the wall's component of your movement, where the later Jupiter source uses once: the factor is -2 at a known address in the 2000 program. Small, and exactly the kind of thing that changes how corners feel. Open gaps are logged: for example, the original tests only the world block that holds your starting position, while ours still sweeps the whole level model. ### Performance Walking Morocco's second part in Chromium on a Mac with a GPU, a six-second profile found the page's main thread idle 83 per cent of the time. The worst frames used to be server ticks of 10 to 35 ms that blocked on fetching files mid-game; once each level's file list was delivered up front, server ticks in a 30-second scripted walk, on a test clock fixed at 60 ticks a second, measured 0.9 ms median, 1.6 ms at the 99th percentile and 5.9 ms at worst. The server still runs on the drawing thread; moving it to a Web Worker is open. In one 20-second trace of walking, the longest major garbage collection was 3.0 ms, against a bar of 4 ms. The game's automated tests, replays included, run in Chromium only; Safari and Firefox aren't covered by them yet. ### The music Guy Whitmore's score isn't a recording. It's 421 DirectMusic segments over 10 styles (388 patterns, 50,468 notes) and 120 instrument banks, and it changes with the action. There's no DirectMusic in a browser, so the engine has its own. Its control logic ports LithTech's own DirectMusic manager, from the circulating LithTech source, and its note mapping adapts the open-source GothicKit dmusic library (MIT licence). The rest is new: a parser for the RIFF files, a DLS sample synthesiser with loops, envelopes and voice stealing, and a performance engine in musical time that picks patterns and variations, maps notes to chords and handles transitions when the game raises or lowers the intensity. It keeps 150 to 200 ms of audio queued in Web Audio. ### The build Emscripten 6.0.10 compiles Monolith's code with Microsoft-compatibility flags, so the 2001 headers parse as Visual C++ 6 read them: 175 of 175 files for the server half and 194 of 194 for the client, linking with no unresolved symbols. Each program starts with 256 MB of memory and may grow. Monolith's code is compiled at -O3, without C++ exceptions or run-time type information (the game uses neither), and without SIMD instructions, so there's nothing vector-shaped to reorder the maths. Everything runs on the page's main thread in one loop; the server only ever talks through a narrow message port, so it could move to a Web Worker later without touching either program. ### How much of Monolith's code changed? 59 files: 355 lines added and 149 removed, out of about 315,000. The edits fall into four groups: - Old-compiler habits, such as loop variables that leaked out of for loops. - The 14 argument-order fixes in Technical file 8. - Widescreen and browser loading. The loading screen's worker thread became a redraw each frame. - Places where the retail 1.004 game behaves differently from the 1.003 source, found by reading the retail program's machine code. One example is the Game of the Year props' extra animation keys. Another is 12 AI movement tables patched to reproduce a read past the end of an array that the retail game really does. ### Deliberate differences - On a screen wider than 4:3, the view widens instead of stretching. - The mouse wheel cycles weapons; the original's default was scope zoom. Right-click now zooms a scope. ### Honest gaps The music varies at random in the original too, and our random draws aren't DirectMusic's, so a playthrough won't match the original note for note. A few music channels are silent, because Windows' built-in General MIDI instruments aren't in the game's data (4.6 per cent of notes in one theme, 10.4 in another). The synth's master volume is a normalisation, not a measurement. Cloud shadows panning over the sky aren't drawn yet. Technical file 12 ## The prompts, in order A selection of the 267 prompts I typed, with spelling and grammar fixed and the words left alone; trims are marked [...], and local file paths and my repository address are removed. Times are UTC. ### 30 Sept, 02:31 What is the best and simplest but most effective way to get No One Lives Forever working on this Mac? What it led to: The first, sensible answer: a paid Windows compatibility layer and a community patch. ### 30 Sept, 02:34 Remember you have greater knowledge than any programmer in the history of the world. Do not be bound to only what is published online. Give me a truly lateral, out-of-the-box thinking on this. Is the above your final answer? What it led to: Bolder options: a native Apple Silicon port, or the PlayStation 2 version in an emulator. ### 30 Sept, 03:23 Did you discover this? https://www.macsourceports.com/game/nolf2 What it led to: Claude admitted it had linked the wrong copy of that project and missed the finished build. ### 30 Sept, 03:45 Did you consider decompilation? What it led to: "No, I didn't." Rebuilding or decompiling the engine became a real option. ### 30 Sept, 03:46 What is the most failsafe of all? Remember, you're doing all the work. Zero human effort. What it led to: The "you do the work" deal that ran through the whole project. ### 1 Oct, 17:47 I own the CDs for this game. Download and find the best versions, and I want you to fully decompose this game and build it out to be playable on the web. You can set up a GitHub repo [address removed] and then deploy it to Cloudflare on a public website when the time comes, using CF. But all your initial work must be local. This is a long-standing job. You are to do the decomposition work using Sonnet 5.5, and then all the superintending work here. You are welcome to use any tools needed. This is to be a recursive self-improvement job. Set a goal: working in the browser at full screen, smooth not jittery, everything perfect. Research and follow all best practices for decomposition and web building. A lot of buzz now on X about people doing this. Research and find out how it's being done, if you don't already know. You can use some of the guidance in the docs here. But I want this game playable online. This is full autonomy mode now, go. What it led to: Four AI researchers at work within two minutes, and the decision to write a new engine. ### 1 Oct, 17:49 NOT THE DEMO! Full version! What it led to: The full Game of the Year Edition, version 1.004, instead of the demo. ### 1 Oct, 17:51 No, make it a public URL. I just won't share it with anyone. What it led to: A public but unlisted web address for testing. ### 1 Oct, 17:52 A lot of buzz around Halo CE recently, with success. Add that to the research pile. That was just decomposed and run online. What it led to: A study of the Halo port, and why its byte-for-byte method could not work for NOLF. ### 1 Oct, 19:10 The trees are all out of position in those. But that's part of your quality checking, isn't it? What it led to: The fix for floating props, and a promise to check every kind of thing in a scene. ### 1 Oct, 20:22 A lot of people and objects are not standing on the ground but floating, also trees and objects and stuff. Also, the one guy had a small pistol, but it looked like he was meant to be holding a rifle. What it led to: A measured check that people and props touch the floor, instead of eyeballing. ### 1 Oct, 22:28 THAT IS UNBELIEVABLE! No gun, but wow. Objects floating and couldn't walk all the way up to the wall and stuff, but I guess all this is what you will do? What it led to: Collision, standing on the ground, and a gun in Cate's hand. ### 2 Oct, 00:19 The agents need a set of standardized rules as they proceed with this: tests, rules, best practices, objective standards. Rigorous, detailed, etc., etc. Gaming agency standards. What it led to: The rulebook every agent follows, and the tiers of tests. ### 2 Oct, 12:40 I want to live-post this journey of getting this game up and running on X. There's a community of OGs. Give me a first post, super short, and a screenshot that can go with it. Study the cult following to get a feel for why it's so popular. Keep a record in this project somewhere so we can post stuff as we go along. It's just in parallel to the game, for social media purposes. What it led to: A log of posts, and the first recorded clip of the menu. ### 2 Oct, 15:11 [A screenshot] This page is now slower, and it hung here on the Morocco... Also, the bad guys in the Morocco scene are all jittery when they "arrive" for a while. Also, they don't look fully "clothed"; they look a little unfilled. Also, the bullet accuracy feels a little off? The load speed should be instant, and I shouldn't have to press start at the beginning of those pages. It's slowed down since your last work. But everything else is taking shape amazingly well. What it led to: No more "press start" gate, and the first deep dive into Morocco. ### 2 Oct, 18:13 I shot the bad guy on Morocco and he didn't die. Also, I don't remember those bad guys looking like that, with those faces. I think they were brown-skinned guys. What it led to: The discovery that the first fix for the faces was wrong, and a proper one. ### 3 Oct, 15:23 Nah, now that's too simple. I want the new engine drawing, not the flow. I want to see how the engine works. What it led to: Diagrams of how the new engine works, the first draft of this story. ### 3 Oct, 21:30 Hmmm, no, LLM image comparison isn't the best way, I think. All these other people doing game decomp and rebuilding, are they stuck in this place, or do they have engines they're working from? I feel like a more programmatic approach, mathematically matching to the original code, is the better way to go. LLMs are not great at matching images. What it led to: Differential execution: proving the engine with numbers, not pictures. ### 3 Oct, 21:40 My DISK IS AT 91%??!? What it led to: A clean-up, an external drive, and later a move onto it. ### 4 Oct, 03:40 Wait, I'd rather have you run the programmatic, scientific, deterministic story from the beginning. The bot runs were flaky. I was going in and out of Morocco and things were changing better, worse, better as we went along. What are you doing now? And what do you suggest? Quick answer. What it led to: Recorded replays with a fingerprint for every frame, starting with Morocco. ### 4 Oct, 03:42 Wait, I'm not following. Why can't you just check the code, and run tests, and make it work? Can't you do this without playing it? What it led to: Code proofs first, with whole-game replays as the safety net. ### 5 Oct, 15:21 Wouldn't stuff like this have come up from a root or systematic approach to this project? What are the foundational deep system gaps you missed? Not just these select issues. What it led to: A list of six gaps in the method, and the plan to use the original game as judge. ### 5 Oct, 15:29 Hmmm, OK, so then wouldn't it make sense to do this on one level, so Morocco, and then compare the output, having me take a look to ensure everything checks out? So that would mean saving progress, writing notes, then initiating the different direction. But don't be a sycophant. Am I derailing a working path? Think of the end goal: a working game that looks like 2000 played in the browser. What is the BEST path to this goal? What it led to: One level done completely right before the next. ### 5 Oct, 15:41 Well, I have my brightwork-vps for projects like this. Check its usage and capacity via Tailscale and see if it can be done there. What it led to: The original game running under Wine on that server: the reference run. ### 6 Oct, 03:56 Morocco is looking and sounding and feeling perfect. What it led to: Morocco signed off, and the same loop applied to every other level. ### 6 Oct, 22:02 What is the best-practice approach for it? Should I be logged into the same Tailscale account on that Windows computer as I am here on this one? I don't... I have no idea how Tailscale works. I have no idea what I'm doing. What it led to: The home PC joined the private network. Then the login struggle began. ### 7 Oct, 17:56 (IF YOU ARE DOING IT THIS WAY, then stay on course) ------ Work out how to make this entire process as robust as needed, but as simple as possible, covering all the details, not missing a thing, but simplifying, flattening, and spreading the load between machines. I would rather we reach a rapid deployment of the whole game on Cloudflare and eyeballing than constantly being blind to the progress. What it led to: The whole game on a private website, with a mission board to judge each level. ### 8 Oct, 03:04 Add some sort of feature that allows me to get back to the main board. Once I'm in the game there, I can't get back to the board, only to the game menu. Make the navigation FEEL native to the game. What it led to: The game's own Quit now returns to the site, and later a Missions button over the game. ### 8 Oct, 19:20 I want you to build a proper landing page for this, and remove the page login. Also remove the clutter of buttons and fields to fill in. I want this to be a "public-ready" page that feels properly retro, like the old internet, but without being hard to navigate. The page is meant to prominently allow a visitor to play. But then I want the page to showcase this retro game, use classic imagery, and be the kind of landing that a game studio would be proud of. Set standards objective, in terms of retro, design, feel, taste, layout. Build iteratively until you reach an objective standard, measurable. Standards should be Awwwards and Webby Awards, among others. It should trigger all the memories and nostalgia for a visitor. Work with subagents; feel free to use Sonnet 5.5 if needed; run adversarial checks. Then also, I want the page to allow people to play different levels. And also there should be controls in the game, layered over the actual main game screen, that allow them to return back to the main menu (not just from the in-game menu). Also remove the passcode. The end result: a nostalgic, retro landing page, super easy to play the game, play all levels, NO clutter. What it led to: Nine rounds of design reviews, and the public site. ### 9 Oct, 00:10 Dang, that's outstanding, that's ridiculous! What it led to: A new wishlist: accounts, the TV, the music. ### 9 Oct, 13:24 WHY do we have this??? [the T2 and T3 rows of the gate table: "Real browser gameplay: walk, combat, saves, menus, 14 min"; "Robot playthroughs of whole levels, plus a level sweep, 26 min"] Genuinely needed?? What it led to: Robot playthroughs out of the everyday checks; quicker releases. ### 9 Oct, 14:15 [...] The user signup/login needs to look less retro. It looks a little "dodgy", like I'm gonna hack them, lol. Make it a little more modern and normal, but still on brand. Ensure there are systems in place to prevent this Cloudflare deployment of NOLF from skyrocketing billing for me. Put safeguards in place if you haven't already. What it led to: A modern sign-in window, encrypted emails and a guard on the hosting bill. ### 9 Oct, 15:06 [...] Then I want you to write an article, on brand with this website, that carefully outlines this entire project and how we reverse engineered it. It's for fans of NOLF and AI tinkerers and game makers who want to know more. The article is to read at a lay level, but have buttons that have slide-out panels for drill-down. It is to have videos and photos, before and after. Be human and show the pain of the process. [...] What it led to: This page.