Quick start

Set up your first local world.

Version: 0.1.63 | Versioning policy

Build status C++20 g++ compiler Linux platform MySQL and MariaDB Optional Redis integration GnuTLS RFC 6455 WebSocket support clang-format code style Last commit Open issues

DurisMUD - a dragon circles a citadel between moonlit ruins and a volcanic fortress

DurisMUD is a long-running dark-fantasy MUD built around a global race war between good and evil. Its text world combines full player-versus-player conflict with exploration, quests, ships, crafting, guilds, and powerful artifacts.

This repository contains the game server, world data, area-building toolchain, database schema, regression tests, and operational scripts.

Architecture

flowchart LR
    Player["MUD client"]

    subgraph Server["DurisMUD server process"]
        Network["Telnet / TLS / WebSocket"]
        Loop["Single event loop<br/>commands, combat, world ticks"]
        Snapshots["Revisioned snapshots<br/>player and world"]
        Commands["Critical commands<br/>economy, ownership, outcomes"]
        Legacy["Bounded compatibility queues<br/>item, scalar, large payload"]

        Network <--> Loop
        Loop -->|immutable jobs| Snapshots
        Loop -->|operation IDs| Commands
        Loop -->|remaining events| Legacy
    end

    Content["World + runtime data<br/>areas/ and lib/"]
    Content -->|boot and reset data| Loop
    Loop -->|boot, bounded reads, legacy routes| Database[("MySQL / MariaDB<br/>durable authority")]
    Snapshots -->|revision guarded| Database
    Commands -->|inbox, ledger, outbox| Database
    Legacy -->|deduplicated events| Database
    Snapshots -.-|optional immutable world recovery| Redis[("Redis cache / recovery")]
    Player <-->|game protocol| Network

    classDef focal fill:#f4ecd9,stroke:#9e3b25,color:#2e2418,stroke-width:2px;
    class Loop focal;
Diagram from the source document.

The C-style sources under src/ are compiled as C++20. Network I/O and mutable game state remain on one select()-driven pulse loop. Immutable revisioned snapshots and non-coalescing operation-ID commands cross typed worker boundaries; the older item, scalar, and large-payload queues retain only bounded compatibility roles. MySQL or MariaDB is the durable authority for snapshots, ledgers, current rows, inbox/results, outbox state, migration history, and lifecycle evidence. Redis is optional and limited to reconstructible caches plus validated world-recovery generations. See the full architecture guide and database guide.

Quick start

The maintained setup path is Debian/Ubuntu, matching the CI workflow and the repository's build-dependency manifest.

Docker alternative

Docker Compose provides a complete local deployment without installing the compiler or MariaDB on the host. It builds Duris, creates a private MariaDB service, loads the fresh schema, applies immutable migrations, imports the tracked help content, generates a persistent self-signed local TLS certificate, and starts the full world:

bash
./scripts/init_docker_env.sh
docker compose --env-file .env.docker up --build --detach --wait
curl http://127.0.0.1:4050/health
nc 127.0.0.1 4000

The generated .env.docker is ignored, mode 0600, and contains random database credentials. The database, filesystem-backed player state, recovery journals, backups, certificate, and logs live in named Docker volumes and survive ordinary container rebuilds. This stack is a local/development alternative to the native setup below; it is not the production deployment model. See the Docker deployment guide for lifecycle, configuration, upgrades, logs, and data-reset commands.

After setup, one command builds every maintained target and runs the complete non-database regression gate:

bash
make test-all

Run the isolated Docker/MySQL migration and schema suites separately with make test-db; Docker is an optional prerequisite for that database gate.

1. Install dependencies

bash
sudo apt update
sudo apt install equivs
make build-deps-package
sudo apt install ./bin/packages/duris-build-deps_1.0_all.deb

equivs is only the bootstrap tool used to build the metapackage. The manifest then installs Git, Python, dos2unix, the compiler, GNU Make, GDB, Valgrind, clang-format, MariaDB-compatible client and server packages, and the XML, compression, TLS, JSON, Redis, BSD, libcurl (SMTP), and MySQL/MariaDB development libraries required by the repository. The compiler supplies the ASan/UBSan runtimes used by the sanitizer build. Redis itself is optional, and so is the SMTP relay: account password recovery by email stays off unless MAIL_ENABLED=TRUE is set in .env.

On another Linux distribution, use packaging/duris-build-deps.equivs as the authoritative dependency list.

2. Configure the server

bash
cp .env.example .env
chmod 600 .env

Edit .env and set the explicit environment role, listener address, database host/port, user, password, name, and exact DB_ALLOWED_TARGETS entry. There are no database credential defaults. The server rejects a .env that is not an owner-controlled regular file with mode 0600 or stricter. It loads this file at boot, and the migration scripts use the same values. .env is ignored by Git and must never be committed. See the configuration reference for precedence, Redis recovery, proxy handling, and diagnostic switches.

Set REDIS=TRUE with REDIS_HOST, REDIS_PORT, and an explicit REDIS_DB to enable caches and recovery integration. Local development may use shared REDIS_USERNAME/REDIS_PASSWORD ACL authentication. Production requires distinct world, presence, cache, maintenance, and, when enabled, donation ACL identities. Verified REDIS_TLS=TRUE transport applies to every runtime connection; non-loopback production endpoints require TLS. REDIS_WORLD_STATE=TRUE additionally enables immutable world recovery. Player saves do not depend on Redis. If a DurisWeb backend will authenticate through WebSocket or GMCP, give it a private DURISWEB_SECRET and follow the challenge-response contract in the DurisWeb API reference. Production browser traffic must terminate TLS at a local reverse proxy. The remaining switches in .env.example are documented inline and are intended primarily for local gameplay testing.

The local-development example enables external donation notices with REDIS_DONATION_SUBSCRIBER=TRUE. Replace its local-only placeholder with an independent HMAC key before connecting a publisher; see the donation event envelope.

3. Create a development database

The following uses duris_dev as an example. Set that database name, the new user password, and DB_ALLOWED_TARGETS=127.0.0.1/duris_dev explicitly in .env.

sql
CREATE DATABASE IF NOT EXISTS duris_dev
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'duris'@'127.0.0.1'
  IDENTIFIED BY 'CHOOSE_A_PASSWORD';
ALTER USER 'duris'@'127.0.0.1'
  IDENTIFIED BY 'CHOOSE_A_PASSWORD';
GRANT ALL PRIVILEGES ON duris_dev.* TO 'duris'@'127.0.0.1';

Load the authoritative fresh-database baseline:

bash
set -a
source .env
set +a
MYSQL_PWD="$DB_PASSWD" mysql \
  --host="$DB_HOST" \
  --port="${DB_PORT:-3306}" \
  --user="$DB_USER" \
  "$DB_NAME" < migrations/bootstrap_multithread_safe.sql

# Record the exact sealed baseline, then apply immutable post-baseline steps.
python3 scripts/migration_runner.py adopt --kind fresh_bootstrap
python3 scripts/migration_runner.py run

# Confirm the exact runtime contract before first boot.
./migrations/verify_runtime_compatibility.sh

# Epic-zone gameplay data is separate from the sealed schema contract.
# Before first boot, inspect and apply the source-derived seed as documented in
# docs/operations/EPIC_ZONE_SEED.md (it handles the initially empty zones table).
python3 scripts/epic_zone_seed.py check

# Seed the tracked help and login content. Inspect the counts first, then
# confirm the live import when prompted.
./scripts/import_help_to_prod.sh --local --dry-run
./scripts/import_help_to_prod.sh --local

[!IMPORTANT] bootstrap_multithread_safe.sql is for an empty database. To upgrade an existing populated database, back it up, restore a disposable development clone, and use a separate owner-readable configuration file that targets only that clone. Never test schema changes on a live database.

For a cloned legacy database, copy the local configuration into a temporary mode-0600 file, change DB_NAME and DB_ALLOWED_TARGETS to the clone, and run the guarded legacy upgrade through MIGRATION_ENV_FILE:

bash
migration_env=$(mktemp)
chmod 600 "$migration_env"
cp .env "$migration_env"
${EDITOR:-vi} "$migration_env"
MIGRATION_ENV_FILE="$migration_env" ./migrations/run_migration.sh
rm -f "$migration_env"
unset migration_env

To replace an allow-listed local/development database directly from a private MySQL dump, use the guarded importer. It refuses active database connections, creates an owner-only backup before mutation, translates MySQL 8's 0900 collation and stale view definers for MariaDB, runs both migration layers, verifies the runtime contract, and restores the backup automatically if any stage fails:

bash
chmod 600 /path/to/legacy.sql
python3 scripts/import_legacy_dump.py \
  --env-file .env \
  --replace \
  /path/to/legacy.sql

The canonical game schema is still checked exactly. Non-runtime tables from a combined game/website dump may coexist and are preserved, but they are not treated as part of the game server's schema contract. When a launcher-era server_reboots table contains values the canonical lifecycle schema cannot represent, its complete raw rows remain in legacy_import_server_reboots before the runtime projection is normalized. Likewise, when required uniqueness cleanup collapses duplicate legacy item metadata, the complete source rows remain in corresponding legacy_import_player_* archive tables.

For the complete source-to-destination map, safety model, audited reference-import results, and recovery runbook, see Legacy Dump Import Guide.

The legacy runner is re-runnable, records verified baseline adoption, and checks runtime schema compatibility before it succeeds. If any step fails, treat the clone as partially migrated and restore it from the known backup before retrying.

4. Build and start

bash
make
./scripts/start_mud.sh --dev

The root build places every compiled artifact below bin/: the staged server is bin/server/dms_new, the editor is bin/areas/editor/de, and the area-generation tools are under bin/areas/tools/. The startup supervisor promotes the server to bin/server/dms, rotates prior executables under bin/server/history/, regenerates combined areas/world.* files, applies pending immutable migrations for an allow-listed local database, verifies the exact runtime schema on every restart, and only then starts it. Production startup performs the verification read-only; use the migration runbook to upgrade production. Without a configured user service it runs in the background and writes console output to logs/duris-console.log. The --dev quick-start listener is port 4000 and cannot select the production runtime role.

Production deployments use the checked-in systemd service rather than the local user service. Its installer requires an explicit production configuration check, enables boot startup, and supervises every exit with an unlimited restart policy. See Production systemd service for installation and cutover instructions.

For a foreground development session on port 4000, use this instead of start_mud.sh:

bash
./scripts/cycle_mud.sh --dev

For a fast development boot using only the tracked world data in areas_mini/, run:

bash
./scripts/cycle_mud.sh --minimal

--minimal implies development mode and port 4000. It validates the minimal dataset, skips full areas/world.* generation and full-world runtime systems, but keeps the player load/save and critical-command pipelines available so a configured test character can log in, play, and disconnect cleanly. Use ./scripts/start_mud.sh --minimal for the corresponding background launcher.

Troubleshooting

If the server stops during boot, inspect logs/log/status and logs/duris-console.log. The most common checks are:

  • MySQL initialization failed: confirm .env values, that the selected database exists, and that the account can connect on DB_HOST:DB_PORT. The server logs the effective database target during boot and aborts when the required schema is missing.
  • Redis connection failed: Redis is optional; set REDIS=FALSE or remove the setting to run without Redis caches and world recovery. Player checkpoints remain available through their local coordinator and journal. If Redis is required, check REDIS_HOST, REDIS_PORT, and that the service is reachable.
  • Missing world files or tools: run make build-area-tools followed by make world, then restart. Combined areas/world.* files are generated outputs and should not be edited by hand.
  • Port already in use: choose a different development port, or stop the existing local instance. Keep development on a non-7777 port.

Operational log locations and restart behavior are documented in the runbook.

Connect

bash
nc localhost 4000
Listener Standard start --dev start
Plain telnet 7777 4000
TLS telnet 7778 4001
WebSocket and HTTP health 4050 4050

The WebSocket port can be overridden with DURIS_WEBSOCKET_PORT. Once the game is running, scripts/healthcheck.sh verifies both process and database-pool readiness.

For local TLS, run ./scripts/generate_localhost_cert.sh. It creates an ignored, machine-local self-signed keypair under certs/; that fallback is accepted only when ENVIRONMENT=local and LISTEN_ADDRESS is exactly 127.0.0.1 or ::1. For a networked deployment, provide the ignored root files duris.crt and duris.key; every private key must be owned by the server user and mode 0600 or stricter. Startup fails if that deployment certificate boundary is not met.

Development workflow

Run the complete developer/CI gate before handing off a change:

bash
make test-all
make test-db

make test-all covers maintained builds, generated world data, Python regressions, and native tests. make test-db additionally creates disposable MySQL containers for schema contracts, immutable migration checks, and the full historical 145-step legacy upgrade, replay, fresh-bootstrap equivalence, and runtime-compatibility test. It never targets the database configured in .env.

During development, run the smallest relevant regression directly:

bash
make -C src
python3 tests/async/test_wear_all_regression.py

Tests under tests/async/ are focused Python regression or source-contract checks. The root harness generates required world data and runs these tests in bounded parallel workers. Docker/MySQL suites remain an explicit make test-db step, while externally provisioned migration checks must target a development clone. See Testing for target details and focused-test controls.

Format only touched C/C++ lines so legacy diffs stay reviewable:

bash
./scripts/format.sh
./scripts/format.sh --check

Install the repository's pre-commit hook with ./scripts/install-hooks.sh. Sanitizer and Valgrind workflows are covered in Building and Valgrind.

Repository map

Path Purpose
Makefile Root build, world-generation, and test entry points.
bin/ Ignored compiled artifacts: executables, objects, packages, tests, and runtime history.
src/ Server sources and Makefile; builds bin/server/dms_new with g++.
areas/ World sources, compilers, and generated world.* boot files.
lib/ Runtime configuration, help, boards, descriptions, and game data.
migrations/ Fresh schema, upgrade runner, and schema-contract tools.
migrations/tools/ Standalone legacy player/account conversion tools.
scripts/ Launch, backup, formatting, debugging, and maintenance helpers.
tests/async/ Focused regression, source-contract, and DB-backed tests.
docs/ Architecture, operations, database, test, and builder guides.

Runtime state belongs under Players/, Accounts/, Ships/, and logs/. Do not commit player data, credentials, logs, generated binaries, or backup archives.

Project releases use Semantic Versioning. The canonical version is stored in the root VERSION file.

Documentation

Browse the project website and documentation library for searchable guides with source links, code highlighting, and diagrams.

Guide Covers
Architecture Process model, boot gate, game loop, typed persistence, recovery.
Codebase Module-by-module map of the server sources.
Building Build flags, areas, sanitizers, verification.
Database Connections, reads, typed writes, reconciliation, schema, migrations.
Configuration Environment variables, Redis, networking, and diagnostics.
Docker deployment End-to-end local Compose setup, lifecycle, data, and troubleshooting.
Runbook Restarts, logs, backups, recovery, operations.
Testing Test layout, commands, and conventions.
Immutable migrations Baseline adoption, ordered checksums, exact resume.
Runtime compatibility Pre-mutation boot verification and lookup publication.
Data lifecycle Store inventory, pending policy, archive/export/erasure boundaries.
Critical commands Operation identity, journal, inbox/results, outbox, replay, fences.
Formatting Style, changed-line formatting, and editors.
Help system Help sources, database import, and rendering.

The complete index, including builder references and standalone diagrams, is in docs/README_docs.md.