Basic setup and first-boot instructions are in the root README.md. Runtime environment variables are documented in CONFIGURATION.md. This document covers the build system itself.
Standard build
make # server, editor, and area tools
make -C src # development server build
make -C src BUILD_PROFILE=production # production server build
cp bin/server/dms_new bin/server/dms # manually promote for a direct run
The server build has an explicit compile-time persistence dependency boundary:
make -C src PERSISTENCE_BACKEND=mariadb BUILD_PROFILE=development
make -C src PERSISTENCE_BACKEND=mariadb BUILD_PROFILE=production
make -C src PERSISTENCE_BACKEND=flatfile BUILD_PROFILE=development
mariadb is the default and includes and links the MySQL-compatible client. The
flatfile selection defines __NO_MYSQL__ and does not add the MySQL include path or
client library. Backend/profile-specific objects are kept under
bin/objects/server/<backend>/<profile>/; the maintained bin/server/dms_new path is
relinked when either selection changes, so the commands can be run sequentially without
cleaning or reusing incompatible objects. development is the default, compiles at
-Og and defines TEST_MUD; production compiles at -O2 and omits all
development-only behavior. The production launcher
checks the build stamp before promoting or running a binary. Each binary accepts only
its matching primary runtime mode. The legacy
mariadb-primary-flatfile-fallback token remains recognized but fails closed because
mixed per-operation authority transfer is not supported.
Mixed legacy modules still mention MySQL types while their durable operations are being
moved behind backend repositories. Flat builds resolve those declarations through
src/no_mysql/, a client-free compatibility surface whose connection, query, statement,
and thread initialization calls always fail. It neither connects nor persists data and
must not be treated as a backend; flat-file primary routes durable operations through
the corresponding file repositories instead of this compatibility surface.
The root Makefile is the maintained full-project entry point. make clean
removes compiled server, editor, and area-tool artifacts but preserves generated
world data, the active runtime, package artifacts, and runtime history. Every
compiled artifact belongs below bin/, whose contents are gitignored:
| Path | Contents |
|---|---|
bin/server/ |
Staged, active, and sanitizer server executables. |
bin/server/history/ |
Bounded runtime executable history. |
bin/areas/ |
Area editor and generation tools. |
bin/migrations/, bin/tools/ |
Standalone maintenance utilities. |
bin/objects/ |
Object and dependency files for every build flavor. |
bin/packages/ |
Debian package and package metadata. |
bin/tests/ |
Native test executables. |
Use make clean-all to remove every reproducible developer artifact. It
removes all generated contents below bin/, combined and scratch world
outputs, generated lookup tables, diagnostic-build output, coverage/profile
data, and Python/tool caches. It preserves .env, logs, player and account
data, and the hand-maintained areas/world.justice, world.tab, and
world.weather runtime inputs.
Compile flags
From the default build line:
| Flag | Meaning |
|---|---|
-std=c++20 |
All .c files are compiled as C++20 with g++. |
-Og / -O2 |
BUILD_PROFILE=development stays debuggable at -Og; BUILD_PROFILE=production is optimised at -O2. |
-DTEST_MUD |
Added only by BUILD_PROFILE=development; enables development-only commands and behavior. |
-D__NO_MYSQL__ |
Selected by PERSISTENCE_BACKEND=flatfile; removes the client compile/link dependency. |
Redis is optional at runtime, but Hiredis and OpenSSL remain build dependencies because one server binary supports both the enabled and disabled runtime configurations.
HARDENING_FLAGS adds -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection to both profiles. EXTRA_CFLAGS / EXTRA_LDFLAGS are
appended last and exist so a wrapper (notably scripts/build-san.sh) can add
instrumentation without discarding the warning profile or the feature defines.
Link libraries: mysqlclient, gnutls, ssl, crypto, cjson, hiredis,
bsd, curl, xml2, z, crypt, pthread. Both backends link curl
(libcurl, the libcurl4-gnutls-dev package): it carries the SMTP path for account
password recovery, which stays disabled at runtime unless MAIL_ENABLED=TRUE.
Chaos gameplay mode is selected at runtime with CHAOS_MUD=TRUE or
CHAOS_MUD=FALSE in .env. The default example disables it. The value must
use uppercase TRUE or FALSE; numeric values are not supported. See
CONFIGURATION.md for this and the other runtime switches.
Dependency install lines are in README.md. The
packaging/ directory contains the equivs manifest for the complete developer
toolset, including Git, Python, area-generation utilities, the
compiler-provided sanitizer runtimes, Valgrind, GDB, and clang-format. It
accepts an existing MariaDB or MySQL toolchain and otherwise uses the
distribution's default MySQL-compatible packages.
Warning profile
src/Makefile's WARNING_FLAGS is -Wall -Wextra -Wpedantic -Werror plus an
explicit list of correctness and hardening diagnostics. There are no
-Wno-* exceptions, and six categories that are not implied by -Wall -Wextra are named explicitly so the guarantee is deliberate rather than
incidental:
-Wwrite-strings, -Wunused-parameter, -Wunused-variable,
-Wunused-but-set-variable, -Wmissing-field-initializers,
-Wunused-function.
Those six were global exceptions until August 2026, hiding ~9,950 diagnostics.
Clearing them surfaced real defects - five MAX_STRING_LENGTH-bounded formats
writing into 256/512-byte caller buffers, a truncated player-save INSERT
handed to MySQL as malformed SQL, and a WebSocket handler that was never wired
into its dispatcher - so the rules below exist to keep the signal trustworthy:
- Never reintroduce a blanket
-Wno-*flag, a warning pragma, or a file-wide suppression. Resolve the diagnostic instead.tests/async/test_compiler_warning_profile.pyfails if one appears. - Use
[[maybe_unused]]only where a build configuration genuinely needs the declaration - typically a parameter whose only use sits inside an inactive#if. Never to hide unexplained dead state. - Do not use
const_castor a C-style cast to satisfy-Wwrite-strings. Make the calleeconst char *when it does not write through the pointer; give it a mutable buffer when it does (seewritable_argin CODEBASE.md). - Do not delete a set-but-unused calculation until its intended effect is understood - it often marks a missing check, charge, or return value.
scripts/warning-inventory.sh performs a clean build with the six categories
enabled but non-fatal and writes deduplicated counts by category and by file to
build/warning-inventory/{raw.log,dedup.txt,report.txt} (build/ is
gitignored). The report records the compiler version and the full flag set so
counts stay comparable across runs.
Note that -Wformat-truncation=2 is fatal: snprintf into a fixed buffer must
have a bound the compiler can narrow. Format each row into its own bounded
buffer rather than appending at buf + strlen(buf) with a
MAX_STRING_LENGTH bound.
The warnings that follow values (-Wnull-dereference, -Wmaybe-uninitialized,
-Wformat-truncation, -Wstringop-overflow, -Warray-bounds,
-Wstrict-overflow) see further at -O2, through inlined code, so the
production profile reports sites the development build does not. make test-all builds both: make build-production compiles the production profile
into its own object directory and bin/server/production/dms_new, leaving
bin/server/dms_new to the development build. Resolve what it reports the same
way: the first -O2 build found a staff command writing ten bytes over five
and an event whose inverted check dereferenced NULL.
When a change touches conditional code, compile-sweep the affected files under
the non-default configurations as well - REQUIRE_EMAIL_VERIFICATION,
CTF_MUD=1, and MEMCHK are the ones in use:
g++ -fsyntax-only -std=c++20 -DTEST_MUD -DCTF_MUD=1 ... src/<file>.c
Sanitizer build
scripts/build-san.sh builds an ASan/UBSan instrumented binary for hunting
memory bugs. The sanitizer runtimes arrive with the compiler declared by the
developer dependency manifest; no separate sanitizer package is needed. Use
this build for debugging sessions, not for production.
It appends its flags through EXTRA_CFLAGS/EXTRA_LDFLAGS so the full warning
profile is preserved, builds objects into bin/objects/server-san/, and leaves
its result at bin/server/dms_san - it never touches bin/server/dms. Do not
switch it back to export CFLAGS=...: a Makefile assignment overrides an
exported variable, so the flags would silently never reach the compiler.
Usage, tool selection, and the rules for when a dynamic check is required are in MEMORY_CHECKING.md.
Area file generation
The server boots from combined area files (areas/world.wld, world.mob,
world.obj, world.zon, world.qst, world.trg, plus lookup tables). These
are generated, not hand-edited:
- Build the compilers:
make build-area-tools(producesmake_mob,make_obj,make_qst,make_shp,make_trg,make_wld, andmake_zonunderbin/areas/tools/). - Generate:
make world(runsareas/m_slow, including lookup generation).
The seven independent compiler builds inherit GNU Make's jobserver, so
make -j"$(nproc)" can build them in parallel. Generation scripts stop on the
first failed command instead of leaving a partially refreshed world behind.
The root target records an ignored areas/.world.stamp and skips regeneration
when every required output exists and all area sources and tools are unchanged.
Per-area source directories (areas/wld/, areas/mob/, ...) hold editable
area data; the combined outputs land in areas/world.*. An area's studio-proc
triggers are areas/trg/<area>.trg, optional like its quests; make_trg checks
their framing and fails generation with the file and line of a malformed one
(STUDIOPROC.md).
scripts/cycle_mud.sh performs both steps automatically when the helper
binaries are missing, so first boot after a fresh clone works without manual
intervention. It generates the world before every start and refuses to boot
when generation fails, rather than start on the previous areas/world.*.
Finding unreachable code
The linker can list the functions no build reaches. Build both servers with one section per function, in object directories of their own so the normal build is not disturbed:
make -C src -j16 OBJDIR=$PWD/bin/analysis/objects SERVER_BIN_DIR=$PWD/bin/analysis \
DMS_BINARY=$PWD/bin/analysis/dms_new EXTRA_CFLAGS="-ffunction-sections -fdata-sections"
make -C src -j16 PERSISTENCE_BACKEND=flatfile OBJDIR=$PWD/bin/analysis/flat-objects \
SERVER_BIN_DIR=$PWD/bin/analysis/flat DMS_BINARY=$PWD/bin/analysis/flat/dms_new \
EXTRA_CFLAGS="-ffunction-sections -fdata-sections"
Then rerun each build's final link line from src/ without -rdynamic and with
-Wl,--gc-sections -Wl,--print-gc-sections. Each removing unused section '.text.<symbol>' line names a function that build never reaches. Demangle with c++filt,
and read st_mysql as MYSQL (the flat-file build stubs it).
A function is dead when every build that compiles it drops it. The list is a lead, not proof, so read the source first: a function reached only through a dead branch shows as live, and an inlined one shows as dead. The linker also cannot see a dead branch inside a live function.
Verifying a build
-
Recompile check:
make -C srcmust complete without errors or warnings in touched files. -
Full build and regression gate:
make test-all. -
Smoke test on a development port (uses
duris_dev, never production):./bin/server/dms 4000 & sleep 5 && nc localhost 4000 # confirm greeting, then shut down -
If you changed persistence-related code, run the relevant focused test; see TESTING.md.