← The MUD Exhibit

How it works

Each game here is the original server: the actual C or C++ written in the 1990s, compiled to WebAssembly with Emscripten and run inside your browser tab. There is no game server in the middle. The whole MUD, its world and its login boot locally, one private copy per visitor. Every source change is wrapped in #ifdef __EMSCRIPTEN__, so the same tree still compiles and runs as the original native, multi-player server.

One connection instead of a network

A MUD server is a network program: it opens a TCP socket, listens, accepts connections and multiplexes them with select(). A browser tab can do none of that, so the port deletes the network rather than emulating it. init_socket() returns a dummy descriptor, and the select() loop is replaced by a game loop that creates a single player connection wired straight to the terminal.

Keys you type are pushed into an input ring buffer through one exported function, mud_feed_input(). The server reads them as if from a socket, where an empty read means "nothing typed yet", not end-of-file. Everything the server writes back to that connection is captured and printed to the screen.

Keeping the tab responsive

The classic server loop runs forever: a tick of commands and heartbeats, then a sleep until the next pulse. On a browser's single thread, actually sleeping would freeze the page. Emscripten's ASYNCIFY rewrites the code so the between-tick emscripten_sleep() unwinds back to the browser's event loop and later resumes where it stopped, so the original while loop keeps its shape and the tab stays live. The heaviest worlds, the MOOs, whose save rewrites the entire database, run in a Web Worker so a long checkpoint never stalls the page.

Teaching 1990s C to type-check

This is the hard part. WebAssembly type-checks every function call. Old C leans on implicit int foo() declarations and the language's tolerance for calling a function with the wrong number of arguments, and wasm rejects both. The build looks clean, then dies with RuntimeError: unreachable the first time a mismatched call runs (in DikuMUD, 76 functions, during world boot).

Two fixes cover it: a generated header of correct prototypes is force-included into every file so no call site has to guess, and EMULATE_FUNCTION_POINTER_CASTS handles tables invoked through a different signature, like four-argument spell handlers called through a six-argument pointer. Forcing the prototypes even surfaced a real bug that had sat latent for decades: a hit() call missing its third argument. Smaller frictions round it out. wasm has no COMMON section, so header-defined globals had to move; time_t is 64-bit while long is 32; crypt() needs a shim that keeps its salt-prefix trick so old passwords still verify; and the deadlock-watchdog signal handlers become no-ops.

A baked world with a writable layer on top

A MUD is also a pile of files: areas, help, socials, shops, and the player saves it writes back. Emscripten gives each module a virtual filesystem. The stock world is baked into the wasm at build time with --embed-file, a read-only image mounted exactly where the server expects to chdir() into it.

Player files can't live there, because a fresh module starts empty on every reload. So the mutable paths are redirected under /persist/<id> and mounted on IDBFS, a filesystem backed by the browser's IndexedDB. On the first run the front-end copies the embedded seed into that mount, and the copy and its load from IndexedDB have to finish before the server builds its player index, so boot waits on them; afterwards the mount is flushed back periodically and when you leave the tab. The effect is an overlay: a read-only world from the download, your writable character on top, kept on your device and never sent anywhere.

Each game resisted differently

None of this is one generic wrapper. Every server had its own fights. AFKMud paints its overland map from a PNG that native code reads with libgd, which isn't available in the browser build, so that exhibit carries a small zlib-based PNG decoder instead; it also resolves its command and spell tables through dlsym, which needs Emscripten's MAIN_MODULE mode.

Curious how the codebases relate? See the MUD family tree. Each game's source link on the home page points to the exact engine this port was built from.