House of Maldorne

We play, host, build, develop and recover old and new retro online games, from muds to roguelikes.

Modernizing the MudOS Driver: From Sarge to Bookworm

MudOS

Heads up: this is a technical post about programming and, frankly, software archaeology — compilers, linkers, decades-old C, that sort of thing. If you usually come here for game design, mud lore or play notes, you can safely skip this one. We will be back to the regular topics in the next post.

Some of the muds we run at maldorne are still based on MudOS, a driver whose codebase was last seriously touched in the early 2000s. Until this week, the way we built that driver was to compile it inside a Debian Sarge (3.1, released in 2005) container, with gcc 3.3.5, on a 32-bit i386 base image. It worked, the binary was rock solid, but the surrounding userland was 20 years old: ancient glibc, no security patches, no current toolchain.

So we sat down to see if we could compile MudOS on a current Linux. Spoiler: we could. This post is a tour of what it took.

The starting point

Each version branch in the maldorne/mudos repository has its own Dockerfile. They all looked roughly like this:

FROM debian/eol:sarge-slim

COPY container/sources.list /etc/apt/sources.list
RUN apt-get -o Acquire::Check-Valid-Until=false update
RUN apt-get update && apt-get -f dist-upgrade
RUN apt-get install -f -y --force-yes git gcc bison make libc6-dev

RUN groupadd -g 4200 mud
RUN useradd -u 4201 -g 4200 -ms /bin/bash mud
USER mud

WORKDIR /opt/mud
COPY --chown=mud:mud driver /opt/mud/driver/
WORKDIR /opt/mud/driver
RUN ./build.MudOS && make && make install

debian/eol:sarge-slim is i386 only, so any image FROM it inherits the same architecture. The build.MudOS shell script auto-detects the compiler and CFLAGS, picks gcc, and builds the driver binary plus an addr_server. With Sarge it Just Works because that is the world the code was written in. With anything modern, things start breaking.

Attempt 1: just swap the base image

The first try was the obvious one:

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y \
build-essential bison make gcc libc6-dev \
git openssh-client ca-certificates

# ... same as before

The first errors were noise: gcc 12 is much stricter than gcc 3.3 and complains about a lot of K&R style C that was perfectly normal in the 90s — implicit function declarations, missing return types, mismatched integer-to-pointer conversions, and tentative definitions. Most of those are warnings by default in gcc 12, but a few are errors, and they all come from a single common source: code that does not have prototypes for functions that return pointers.

The fix for that part was a set of compatibility flags applied to CFLAGS:

-fgnu89-inline
-fcommon
-Wno-implicit-function-declaration
-Wno-implicit-int
-Wno-return-type
-Wno-int-conversion
-Wno-error=implicit-function-declaration
-Wno-error=implicit-int
-Wno-error=int-conversion

What each one does:

  • -fgnu89-inline restores the pre-C99 GNU semantics of the inline keyword. The MudOS source uses an INLINE macro that expands to inline, and it relies on the old GNU rule that a plain inline function is also emitted as an external symbol. C99 changed that: a plain inline is just a hint, and the symbol only exists if there is a separate non-inline definition somewhere. Without this flag, functions like whashstr() in hash.c are inlined where used and then disappear from the object file, and the final link fails with undefined reference to whashstr.
  • -fcommon restores the pre-gcc 10 behavior for tentative definitions (the implicit extern int foo; thing). New gcc defaults to -fno-common and considers duplicate tentative definitions an error. The MudOS code has plenty of those.
  • The -Wno-* and -Wno-error=* family simply tells gcc not to upgrade legacy K&R warnings into errors. It does not silence the warnings themselves; we still see them during the build, but they no longer abort the process.

build.MudOS reassigns CFLAGS internally and ignores any ENV CFLAGS we set, so we patch the script with a sed step in the Dockerfile to inject the flags right before its own detection runs.

After that first round, the driver compiled, the binary built, and the container started. We even saw Accepting connections on port 5000.

Attempt 2: the segfault

Then we tried to log in. The driver crashed with a segfault inside /secure/master::connect(), just before that, there was a suspicious warning during the preload phase:

Illegal object to load: return value of master::creator_file() was not a string.

That warning was the smoking gun. creator_file() is supposed to return a string. The C side of the driver was reading the LPC return value and seeing… something else. A truncated pointer.

When you compile K&R C code without prototypes on a 64-bit machine, every function whose declaration the compiler has not seen is assumed to return int. On 32-bit Linux (Sarge), that is harmless because int and void * are both 32 bits wide. On 64-bit Linux (modern Bookworm amd64), int is still 32 bits, but void * is 64 bits, so any function that actually returns a pointer gets its return value silently truncated to 32 bits and the high 32 bits are lost. The result is a corrupt pointer that the next caller dereferences and dies.

This is exactly the kind of bug the warnings we silenced (-Wimplicit-function-declaration, -Wint-conversion) were trying to tell us about. Patching every offending file in MudOS would have taken hours and would have meant carrying a divergent fork forever. The much cheaper fix is to not have a 64-bit pointer in the first place.

Attempt 3: a 32-bit binary on a 64-bit base

Debian 12 has perfectly good support for compiling and running 32-bit binaries on a 64-bit host via gcc-multilib and the i386 multiarch packages. That gives us the best of both worlds:

  • The container runs on a modern 64-bit Linux with current glibc, openssl, git, openssh, security patches, the works.
  • The MudOS driver itself is built as an i386 ELF binary, so int and void * are again the same width and the K&R code behaves exactly like it did on Sarge.

The relevant changes in the Dockerfile:

FROM --platform=linux/amd64 debian:bookworm-slim

RUN dpkg --add-architecture i386 \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential bison make gcc gcc-multilib libc6-dev libc6-dev-i386 \
libcrypt-dev:i386 \
git openssh-client ca-certificates

And -m32 is added to the CFLAGS patch mentioned above. The --platform=linux/amd64 is there so the same Dockerfile builds the same image regardless of where you build it: an Apple Silicon laptop, a Windows ARM machine, an x86 Linux server, or GitHub Actions. Inside that amd64 image, the i386 multiarch toolchain produces the 32-bit driver.

With these changes, the build is clean, the resulting driver binary is a tiny ~720 KB ELF32 executable, and it works. We tested it end-to-end against a real mudlib and everything behaves as it always did.

The result

Every supported MudOS branch in maldorne/mudosv21.7, v21.7b21_fr, v22.2b13 and v22.2b14 — has been migrated to the new build:

Before After
Debian Sarge 3.1 (2005), i386 Debian 12 Bookworm, amd64 base, i386 binary
gcc 3.3.5 gcc 12 with K&R compat flags
Pre-historic glibc, no security updates Modern glibc 2.36, regular Debian security updates
Tied to a 32-bit-only base image Builds on any host that runs Docker, regardless of architecture

The published images on the GitHub Container Registry keep the same tags (ghcr.io/maldorne/mudos:<version>), so anything that pulled them before keeps working without changes.

Why bother

It would have been easier to leave Sarge alone. The driver binary it produces is bit-for-bit reproducible, and it has worked for years. But the world around it kept moving:

  • We could not run the driver image anywhere that did not support 32-bit emulation (which is most of modern arm64 hardware).
  • We could not pull or rebuild the surrounding userland for security fixes — Sarge has been EOL since 2010.
  • And, most prosaically, every time we touched the build we had to remember that we were inside a 20-year-old time capsule.

Now the MudOS images are just normal Debian 12 containers that happen to ship a 32-bit binary inside. They get rebuilt on push, they get security updates, and they can be inspected with modern tools. The driver itself is unchanged — same source, same behavior, same protocol — it just lives in a friendlier house.

Source code, Dockerfiles and the full set of compat flags are in the maldorne/mudos repository, one branch per supported version.

#

Added functionp support for when DGD is compiled with the -DCLOSURES flag. Closures in DGD allow passing functions as values, similar to lambdas in other languages. Our functionp tries to emulate the MudOS functionp efun behavior.

Small change, but just in case you want to experiment with it, you have it available now in the Hexagon repository.

Hexagon

From Rooms to Locations: An ECS Approach

One of the biggest architectural changes in our MUD engine Hexagon is replacing the traditional room system with a new location system built around an Entity Component System (ECS) pattern.

The Problem with Rooms

The classic MUD room is a monolithic object. A room that contains a shop has to inherit from a shop class. A room with a pub inherits from a pub class. If you want a room that is both a shop and a pub, you need multiple inheritance, which in LPC gets messy fast. And then you want to add a sign, or make it dark at night, or have an NPC attendant, and you end up with inheritance chains five levels deep where half the code is workarounds for the other half.

Components Instead of Inheritance

The ECS approach flips the model: a location is a container, and features are components you attach to it. A shop is a component. A pub is a component. Having climate effects is a component. You can mix and match them freely without worrying about class hierarchies, because you compose locations by mixing components, not by stacking class hierarchies.

In practice, this means a location file could look something like this:

// A tavern that also sells equipment
add_component("shop", ([
"items": (["sword": 100, "shield": 150]),
]));

add_component("pub", ([
"menu": (["ale": 5, "mead": 7]),
]));

// And this location is outside and has weather effects
add_component("outside");

Each component manages its own state and responds to player actions independently. When a player types look, the location queries all its components for their descriptions. When a player types buy sword, the location routes the action to the component that responds to buy actions, which checks if it has that item for sale.

What Changed

The conversion touched almost every part of the codebase that dealt with rooms:

  • Actions and targets: the action system now treats components as valid targets. If a location has a shop component, buy and sell work without the location needing to know anything about commerce.
  • Builder tools: call function() @component: now you can call functions directly on components from the command line, which makes testing and debugging much easier.
  • Darkness and weather: the dark/night/outside system was refactored to work as a location property rather than a room class feature.

The old room system still works. We did not break backwards compatibility. Rooms and locations coexist, and you can convert rooms to locations incrementally.

Ventures: Managing It All

Along with locations, we built a new ventures handler. A venture is any player-facing business in the game world: a shop, a pub, etc. The handler tracks all of them across all areas, and the ventures admin command lets you list, inspect, and manage every venture in the system from one place.

Also, every behaviour that a venture can have has been refactored so we can share the code between the location system and the old room system. So if you have a shop in a room, it uses the same underlying code as a shop component in a location.

Next Steps

The plan is to gradually convert existing rooms to locations in the different games in Hexagon. We will be also adding new components and features to the location system as we go, such as support for dynamic events, more complex NPC interactions, and better integration with the quest system. The goal is to have a flexible, modular system that can support a wide variety of locations and gameplay styles without needing to rewrite code every time we want to add a new feature or mix and match different types of locations. The old room code will stay around for a while, both for compatibility and because some simple rooms do not need components at all.

The source code is available in the Hexagon repository.

Hexagon

MUD Emotes Reworked: The Souls System

The soul system in Hexagon has been completely reworked. Souls (also called emotes or social commands in other MUD codebases) are what lets players express themselves beyond game mechanics: waving, laughing, hugging, sighing, facepalming. The stuff that turns a text game into a social space.

Why This Took So Long

Hexagon is bilingual. Every soul needs to produce correct output in both English and Spanish, accounting for verb conjugation, gender, target presence, and self-targeting. “You wave at John” becomes “Saludas a John” in Spanish, but “John waves at you” becomes “John te saluda”. Multiply that by about 80 different souls and every possible combination of arguments.

The original soul data was a tangled mix of code and translated strings. Pulling them apart into clean data files (soul-data.en.h and soul-data.es.h) meant going through every single soul and testing every variation.

How It Works

Each soul has up to four different message patterns depending on context:

  1. What you see when you do it (no target)
  2. What the target sees
  3. What others in the room see

And then, maybe some souls can’t have targets at all, some other maybe only have self-targeting messages, or cannot be self-targeted.

For a soul like bow:

  • You type bow and see “You bow gracefully.”
  • Others see “Neverbot bows gracefully.”
  • You type bow john and see “You bow before John.”
  • John sees “Neverbot bows before you.”

And then you can add adjectives to the soul, like bow deeply: with this particular example, you could bow solemnly, deeply, formally, hastily, slightly, respectfully, insolently, clumsily, gracefully, dexterously, wildly or colorfully. And all of these combinations need to exist in both languages.

The Drunk Factor

On top of the souls rework, the intoxication system (drunk.c) was translated and connected to the soul output. A drunk character does not just type garbled text. Their emotes change too. Being drunk affects how souls display, adding slurring and stumbling to the output. And some chances of burping, puking, or some other fun drunk behaviour. This is the kind of detail, together with shops and pubs working as location components, that makes a MUD feel alive. Nobody needs it, but once it is there, players notice.

What’s Next

The soul system is working in both languages. The data files are clean and adding new souls is straightforward: define the four message patterns in each language file and the system picks them up.

The source code is available in the Hexagon repository.

Paper of the Week: Players Who Suit MUDs

The academic paper “Hearts, Clubs, Diamonds, Spades: Players Who Suit MUDs” was written by Richard Bartle. Published in 1996, it is a seminal work in the field of game studies and player psychology. In this paper, Bartle categorizes players of Multi-User Dungeons (MUDs) into four distinct types based on their preferred activities and motivations within the game.

Bartle's Taxonomy of Player Types
Recreation of the classic Bartle’s player types diagram from the original 1996 paper.

The Four Player Types

  1. Achievers: Players who focus on attaining in-game goals, collecting points, levels, equipment, and other measurable achievements. They want to win, or at least demonstrate mastery.
  2. Explorers: Players who enjoy discovering new areas, learning about the game mechanics, and finding out the secrets within the game world. The joy is in the discovery itself, not in what they find.
  3. Socialisers: Players who are primarily interested in interacting with other players, forming relationships, and engaging in conversations. The game is a backdrop for social interaction.
  4. Killers: Players who thrive on competition with other players, seeking to assert their dominance and affect other players’ gameplay. They need other players to exist, but not necessarily to cooperate.

The Player Dynamics

What makes Bartle’s taxonomy genuinely interesting is not just the classification itself, but the dynamics between player types. Bartle observed that increasing certain player types in a MUD directly affects the population of others. For instance, too many Killers will drive away Socialisers, which in turn reduces the supply of victims and eventually drives away the Killers themselves. A healthy MUD needs a balance of all four types.

This insight remains relevant for any online multiplayer game, even those far removed from text-based MUDs. Anyone who has seen a game community collapse under the weight of toxic PvP or watched an MMO server die after the social players left has witnessed Bartle’s dynamics in action.

Why It Matters for MUD Development

At Maldorne, we have been building multiplayer games with the Hexagon mudlib for years. Whether we are designing combat systems, crafting economies, or planning social spaces, Bartle’s framework helps us ask the right questions: which player types does this feature serve? Are we accidentally pushing a type away?

For example, the weather and climate systems in our games affect exploration and survival, giving Explorers and Achievers something to engage with. The towns, guilds, and public spaces are designed with Socialisers in mind. And combat provides the competitive edge that Killers seek.

Understanding these dynamics is also crucial when playtesting — observing which player type your testers gravitate towards tells you a lot about what your game is actually offering versus what you intended.

If you are interested in more MUD-related resources, we maintain a curated list in our awesome-muds repository.

Read the Original

Read it online, it is really worth it. At roughly 20 pages, it is an accessible and well-written read even if you have never played a MUD. The concepts apply to any multiplayer game.

Awesome-Muds

For some time now, we have been maintaining the GitHub repository awesome-muds, which contains a large list of resources, articles, technologies, etc., related to the world of MUDs. It includes some history, links to available clients for different operating systems… a little bit of everything.

Take a look, and if you think there is something that could or should be changed, improved, or added, feel free to open an issue to discuss it.

Link to the repository.

awesome-mud

New packages included with Hexagon

The Hexagon mudlib/framework has been updated with new packages, which include the following:

  • crypt: code and command to encrypt and decrypt files, based on Dave Ljung’s (Jubal) code, ported to be used with DGD and Hexagon.
  • json: code to encode and decode LPC values as json strings. Modified from an LPC snippet created for MudOS, changed to be executed with DGD.
  • uuid: easy way to create uuid strings (RFC4122), also addapted from an LPC snippet.

Take a look inside the /mudlib/packages directory in the repository to see the code, documentation and license for each package.

#

The Hexagon mudlib has reached at last its version 2.0, codenamed Castle Black. It’s a major release that includes a lot of changes and improvements, and it’s the first one that we can say is a beta version, after a lot of years in alpha. It’s a renaming of the v1.24.04 version. The next planned version will be v2.1 Daggerfall.

Release link.

#

For developers: The Docker container images we are using for the MUDs we host are not anymore in Docker Hub, now they are in the Github Container Registry, just in case anybody want to tests things in their own computers.

DGD image, MudOS Images. You can always find them in their respective repositories, in the right column, under the packages section.