Building & testing

Compile, test, and contribute with confidence.

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

bash
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:

bash
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:

  1. Never reintroduce a blanket -Wno-* flag, a warning pragma, or a file-wide suppression. Resolve the diagnostic instead. tests/async/test_compiler_warning_profile.py fails if one appears.
  2. 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.
  3. Do not use const_cast or a C-style cast to satisfy -Wwrite-strings. Make the callee const char * when it does not write through the pointer; give it a mutable buffer when it does (see writable_arg in CODEBASE.md).
  4. 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:

bash
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:

  1. Build the compilers: make build-area-tools (produces make_mob, make_obj, make_qst, make_shp, make_trg, make_wld, and make_zon under bin/areas/tools/).
  2. Generate: make world (runs areas/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:

sh
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 src must 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):

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