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.
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.
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.
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 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.
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.