A map of the server sources. Hundreds of .c files under src/ are compiled
individually as C++20 and linked into bin/server/dms_new. The src/ root
contains only the Makefile and repository metadata; implementation and headers
live in subsystem directories, and includes use paths qualified from src/
(for example, #include "core/structs.h"). File boundaries retain the DikuMUD
conventions: act*.c files group player commands, do_<name> functions are
commands, and specs.*.c files hold special procedures.
Related: ARCHITECTURE.md, DATABASE.md.
Source layout
| Directories | Responsibility |
|---|---|
account/, player/ |
Account/login/creation flows and player load/save/materialization. |
classes/, magic/, combat/ |
Class and skill rules, spells, affects, and combat. |
cmd/ |
Command registration plus player, object, social, and staff handlers. |
core/ |
Shared structures, constants, prototypes, utilities, configuration, and legacy pfile support. |
economy/, item/ |
Shops, auctions, balances, crafting, equipment, lockers, and durable item movement. |
flatfile/, no_mysql/ |
Complete flat-file persistence backend and client-free build stubs. |
guild/, kingdom/ |
Guild/social ownership and the current map-territory kingdom feature. |
mob/, specs/ |
Mobile behavior, studio procs, and area/special procedures. |
net/ |
Telnet, TLS, WebSocket, GMCP, MCCP, prompts, and descriptor I/O. |
persistence/, sql/, redis/ |
Typed durability coordinators, MariaDB repositories, and Redis integrations. |
ships/ |
Naval simulation, dock economy, and player/NPC ship control. |
telemetry/ |
Optional gameplay telemetry: sessions, activity, progression, encounters, and the bounded SQL writer (contract). |
world/ |
Boot/loading, world lifecycle, movement, maps, events, quests, and global updates. |
Core engine
| Files | Role |
|---|---|
src/net/comm.c |
main(), game_loop() — select() loop, socket I/O, pulse dispatch, signal handling. |
src/world/db.c |
Boot: loads world files and zone resets; core allocation helpers. |
src/cmd/interp.c |
Command table (CMD_* rows) and command dispatch/aliasing. |
src/world/handler.c |
Object/character lifecycle and generic character maintenance events. |
src/core/structs.h |
Central data structures (char_data, obj_data, descriptor_data, ...). |
src/core/prototypes.h |
Cross-subsystem function declarations. Prefer a subsystem header for new APIs. |
src/core/utility.c |
String/number helpers and logging (logit, wizlog). |
src/core/constant.c, src/core/utils.h |
Constants, tables, and accessor macros used throughout the tree. |
Events, time, and triggers
| Files | Role |
|---|---|
src/world/events.c, src/world/new_events.c |
Timed callback wheel executed inside the game loop. |
src/world/timers.c |
Player-facing timers such as affect durations. |
src/mob/studioproc.c, src/mob/studioproc.h |
Studio-proc trigger engine for areas/world.trg; see STUDIOPROC.md. |
src/mob/studioproclib.c, src/mob/studioproclib.h |
Built-in proc function library callable from triggers. |
src/persistence/latency_trace.c |
Per-callback latency telemetry (NEVENT BUDGET WINDOW). |
Persistence
| Files | Role |
|---|---|
src/sql/sql.c, src/sql/sql.h |
Main MariaDB connection, target selection, boot schema checks, and retained synchronous queries. |
src/sql/sql_pool.c |
Bounded connection pool used by typed persistence workers. |
src/sql/sql_player.c |
Character row mapping. |
src/player/player_load_pipeline.c, src/player/player_save_pipeline.c, src/player/player_save_worker.c |
Bounded typed player load, checkpoint capture, and the one persistence writer. |
src/persistence/critical_command_coordinator.c |
Non-coalescing critical gameplay operations and entity fencing. |
src/core/files.c |
Legacy binary playerfile I/O and pfile utilities. |
src/redis/redis.c, src/redis/wizredis.c |
Redis caches, floor deltas, immutable world recovery, and staff diagnostics. |
See DATABASE.md for behavior and migrations.
Networking
| Files | Role |
|---|---|
src/account/nanny.c |
Connection state machine: login, account/character selection, creation, and hints. |
src/net/ssl.c |
TLS telnet listener (duris.crt/duris.key). |
src/net/websocket.c, src/net/websocket.h |
RFC 6455 WebSocket server and HTTP health endpoint. |
src/core/json_utils.c |
JSON encode/decode helpers used by network protocols. |
src/net/gmcp.c |
GMCP negotiation and outbound game-data packages. |
src/net/mccp.c |
MCCP (MUD Client Compression Protocol). |
src/persistence/copyover.c |
Hot reboot: survives exec() via copyover.dat and restores connections and combat. |
src/net/editor.c |
In-game line editor for mail and boards. |
src/cmd/mail.c |
Internal mail store and command handling. |
Gameplay systems
Representative entry points by feature:
- Combat and effects:
src/combat/fight.c,src/combat/mobcombat.c,src/magic/affects.c, and the other modules undercombat/. - Magic, classes, and progression:
src/magic/,src/classes/,src/world/epic.c, andsrc/world/achievements.c; see CLASSES.md. - Objects and economy:
src/cmd/actobj.c,src/item/, andsrc/economy/. Batch transfer syntax and atomicity are documented in BATCH_ITEM_COMMANDS.md. - Random equipment:
src/item/random_equipment.cgenerates items,random_equipment_tables.cholds their tables, andrandom_equipment_drop.chandles kill drops. Callers useitem/random_equipment.h;randobj.cis the staff command. See RANDOM_EQUIPMENT.md. - World and movement:
src/cmd/actmove.c,src/world/map.c,src/world/weather.c, and the rest ofsrc/world/. - Guilds and social systems:
src/guild/plus social handlers insrc/cmd/andsrc/world/. - Quests:
src/world/quest.c,src/cmd/nq.c,src/world/world_quest.c, andsrc/mob/encounters.c. - Immortal and building commands: primarily
src/cmd/actwiz.c,src/cmd/wikihelp.c,src/world/properties.c, andsrc/cmd/testcmd.c. - Chaos mode:
src/combat/chaos.c,src/combat/chaos_config.c, and the pre-entry grant flow insrc/account/nanny.c; see CHAOS_MODE.md.
Frag-based level caps
A qualifying kill's base frag share is 1.00 frag divided by the killer's side in the
room, including ungrouped mortals and excluding staff. The default
unlock.minimum.share.points=20 permits at most five allies. Maintenance compares
the stored combat_outcome.killer_allies against the current setting; individual
level and race modifiers still affect rewards, but do not affect eligibility.
Migration 0040_combat_outcome_allies adds that count before the updated server boots.
Old outcomes and replayed version-1 combat commands have count 0 (unknown) and cannot
unlock a level. New commands use payload version 2; existing rewards and history are
preserved. Follow the normal migration/backup procedure when deploying.
Ships subsystem
src/ships/ is the self-contained naval simulation. src/ships/ship_base.c owns the
core lifecycle, src/ships/ship_cargo.c cargo, src/ships/ship_combat.c naval combat,
src/ships/ship_control.c commands and movement, and src/ships/ship_shop.c dock construction and
trade. src/ships/ship_auto.c is the player autopilot; it is not generated and is
separate from the NPC combat brain in src/ships/ship_npc_ai.c. src/ships/ship_identity.c
provides process-local generation-checked references, src/ships/ship_variables.c owns
the append-only static identifier tables, and src/ships/ship_utils.c contains shared
map/contact helpers. The external API is src/ships/ships.h. Ships persist to the
ships, ship_armor, ship_crew and ship_slots tables (or the flat-file
ship_catalog). SHIPS.md is the full engineering reference, and
SHIP_GAMEPLAY.md covers commands, catalogues and the cargo economy.
Kingdom subsystem and retired siege identifiers
The current src/kingdom/ feature is a guild map-territory system, not the
retired siege/town-defense implementation. It is runtime-gated by
kingdom.enabled in lib/kingdom.cfg, represents an 80-square ordered realm
with one highest_claim integer, and persists it in kingdom_realms. Only
src/kingdom/kingdom.h is public outside the subsystem. Player rules are in
lib/information/helpkingdoms.
Several old siege identifiers are permanent compatibility reservations and must not be reused:
| Surface | Reservation |
|---|---|
| Command table | Slots 827 and 828 remain _retired_827 and _retired_828, named CMD_RETIRED_827 and CMD_RETIRED_828. |
| Persisted player flags | act2 bit 4 remains PLR2_RETIRED_KINGDOMVIEW. |
| World data | Area zone 4010 and the former 401000-series siege room/mobile range remain retired. |
| Object prototypes | VNUMs 160, 161, 178, 179, and 461 through 464 remain retired. |
| Schema | The five retired table tombstones are listed in DATABASE.md. |
The old SIEGE_ENABLED compile-time surface and its runtime code are gone.
These reservations protect persisted flags, command numbering, backups, and
area identities; they do not gate or describe the new kingdom module.
Configuration and data
- Compile-time:
src/core/config.h(ports, pulses, paths),src/sql/sql.h(SQL interface), Makefile defines. Credentials and runtime connection targets come from the configured environment; see CONFIGURATION.md. - Runtime data:
lib/—duris.properties, per-feature*.cfg(crafting.cfg,mining.cfg,hardcore.cfg,frag_cap.cfg,account_rewards.cfg,creation_availability.cfg,random_equipment.cfg,kingdom.cfg), greetings/MOTD/news/help text underlib/information/, misc runtime files underlib/misc/, boards underlib/boards/. - World data:
areas/world.*combined files generated fromareas/{wld,mob,obj,zon,qst,shp}/by themake_*tools (see BUILDING.md). Optional per-zone room-position exports live underareas/map/; the server build does not consume them. Independently,areas/dump_map_image.rbrenders a world file to a PNG overview for offline inspection; its arguments are<file.wld> <width> <height> <tile-pixels>.
src/world/map.h classifies the surface window as VNUMs 500000 through
659999, the main Underdark window as 700000 through 859999, and the Alatorin
Underdark window as 120000 through 123833. These are coordinate windows, not a
promise that every VNUM is loaded; callers must still resolve through
real_room() and handle NOWHERE.
Dispatch signatures
Most of the tree is reached through fixed-shape function pointers held in tables. These signatures are load-bearing: a parameter may be unused by a given implementation, but the slot cannot be removed without breaking every table it is registered in.
| Signature | Held in |
|---|---|
(int, P_char, char *, int, P_char, P_obj) |
skills[].spell_pointer — spell dispatch |
(P_char, char *, int) |
command handlers (src/cmd/interp.c CMD_* table, ACMD()) |
(P_char, P_char, int, char *) |
mobile and room special procedures |
(P_char, P_char, P_obj, void *) |
event callbacks |
(P_obj, P_char, int, char *) |
object special procedures |
(void *, int, char *, int, int) |
src/cmd/actset.c ac_* setters, in setBitTable::sb_func |
(descriptor_data *, cJSON *) |
WebSocket command handlers (src/net/ws_handlers.c) |
When adding a handler to one of these families, register it in the owning table in the same change. Compilation alone does not prove a handler is dispatched; verify its real command or event entry point.
C++ conventions the warning profile enforces
The build is -Werror with no -Wno-* exceptions (see
BUILDING.md). Four conventions follow from that:
-
Unused parameters in dispatch signatures are written with the name commented out —
P_obj /*obj*/— which keeps the documentation while satisfying-Wunused-parameter. Parameter names are not part of a function's type, so this can never change table compatibility. Use[[maybe_unused]]instead when the parameter's only use sits inside an#ifthat is inactive in this build; unnaming it would break that configuration silently.src/cmd/interp.h'sACMD(c)andsrc/combat/dam_mods.h'sMAKE_DAM_MOD_PRED()expand into bodies that variously do and do not read a slot, so their slots carry[[maybe_unused]]for the same reason. -
String literals into dispatch-pinned callees. Command handlers, spell functions and special procedures take a writable
char *because their type is pinned, and several tokenise the argument in place (half_chop(argument, arg, argument)). Passing a literal is a potential write to read-only memory, sosrc/core/utils.hprovideswritable_arg, a stack copy sized from the literal by deduction:do_say(ch, writable_arg("Fill me with your strength!"), CMD_SAY);Do not add a
const_castor C-style cast instead. The one deliberate exception in the tree is insidestr_free, which takesconst char *because an owning pointer to string data is normally spelled that way; the cast lives in that one function rather than at every call site. -
Immutable message and lookup tables are
const char *.damage_messagesin particular is const-only, and its helpers take the caller's real buffer size —tests/async/test_message_buffer_bounds.pypins both. It also carries default member initializers, which makes it non-trivial: value-initialize it withmsg = {}, nevermemset(&msg, 0, sizeof(msg)). -
Set-but-unused values are evidence, not noise. Never delete an assignment whose right-hand side calls anything but a known-pure helper.
generic_find()is the canonical trap: it returns a bitmask most callers ignore while depending entirely on the character/object it writes through its out-parameters.
Indexing invariants
Every one of these was a live crash. The guards are in place; keep them when touching the surrounding code.
| Invariant | Why |
|---|---|
real_room() may return NOWHERE (-1). Resolve once, check, then index world[]. |
The random-labyrinth vnum ranges (700000+, 800000+, 900000+) are largely absent from the loaded world, so reset_lab() performed thousands of world[-1] accesses and walked the garbage pointers in world[-1].people / .contents. Same hazard in create_lab, connect_lab, connect_other. |
obj_index[obj->R_num] requires obj->R_num >= 0. |
Dynamic and uninstantiated objects have R_num = -1. The OBJ_VNUM() and GET_OBJ_PROC() macros in src/core/utils.h now return -1/NULL for a negative R_num, and free_obj, do_wear, do_grab, do_remove, do_search guard it directly. |
There are exactly five bitvector banks, indices 0..4 (bitvector … bitvector5) in obj_data and affected_type. |
affect_modify read bitv[5], running off the array into the adjacent affected[0] and pointer fields. |
Race indices into stat_factor[] / combat_by_race[] ([LAST_RACE + 1]) need BOUNDED(0, race, LAST_RACE). |
Equipment affected[].modifier values are attacker-controlled data, not a validated race. Guarded in calculate_hitpoints2, apply_affs, affect_total, do_score. |
wear() must not fall back to a weapon slot for a non-weapon ITEM_HOLD item. |
Equipping a non-weapon into WIELD/WIELD3/WIELD4 breaks combat-round and damage invariants. HOLD now rejects when occupied. |
Object special procedures do not fire for items inside a container. They fire
from special() (src/cmd/interp.c), which walks ch->carrying before the typed
command runs — so the proc for an item pulled out of a portable hole fires on the
player's next command, whatever that command is. tests/async/test_wear_all_regression.py
and test_relic_lab_reset_bounds.py cover these paths.
Standalone tools
make -C src pfilebuilds the offline pfile scanner atbin/tools/pfile; it is a distinct target and is not covered by the default server build.migrations/tools/— offline conversion/migration binaries (pfile_converter, account/guild/locker/ship migrators). Not part of the server build; build the primarymigrate_pfilestool withmake -C migrations/tools, or use theaffectsandpfile_convertertargets for those dedicated binaries.areas/src/— area compiler tools that turn per-area source dirs into combinedworld.*files.
Bartender world-quest catalog
src/world/world_quest_policy.c builds the global catalog once at boot through
calc_zone_mob_level(), after world indexes, special procedures, and map setup.
Both persistence backends use loaded content; zones.quest_zone is not an
approval list. Repeated quest requests reuse cached mobile profiles, reward
pools, and per-level scores rather than scanning all prototypes or querying SQL
for zone eligibility. Temporary probes restore live entity counts.
Policy excludes sentinel/non-normal zones, explicit zones 0/292/536, towns and
hometowns, and zones without eligible local rewards. The truncated average
prototype level must lie strictly inside (L - 7, L + 5). Mapless zones require
level 41 or higher. Source-eligible rewards pass the floor 2 * itemvalue >= L;
reward counts and means are calculated after that filter. Items that any
hand-built quest pays out or asks players to hand in (the item goals of
quest_index) are never rewards, flagged ITEM2_QUESTITEM or not. Each zone
also withholds its most valuable world.quest.reward.top.withheld.percent
(default 20) percent of source-eligible items, rounded up and including value
ties. A zone whose items are all worth the same has no top tier, so nothing is
withheld there; a zone's only item is. Per-level pools and scores are built from
what remains, and the boot log reports both withheld counts. The setting is read
when the catalog is built, so a change applies at the next boot.
For eligible zones, the relative selection weight is
exp(-abs(average_level - L) / 6) * (average_ivalue / L) * eligible_item_count.
Item count is deliberately linear; zero-reward zones have zero weight. See
world_quest_policy_math.h for boundaries and constants.
Targets use the inclusive level band [L - 4, L + 5]. Kill targets need at
least two existing instances; ask targets need exactly one, speech capability,
and no player-specific aggression. Below level 31, invisible/concealed/hidden
ask targets are excluded. Dynamic aggression probes and completion-history
checks each have a request-wide budget of 32. Previously completed targets are
excluded from subsequent retries; a history-read error stops assignment.
createQuest() distinguishes no eligible zone from no valid target. Bartender
payments use the currency transaction API: the debit changes the wallet in memory
and invokes its completion before the API returns. A failed later selection
refunds the fee in memory; player saves persist the wallet. The deferred
quest continuation stores the bartender's stable vnum rather than a live NPC pointer,
so disconnect/reconnect cannot resume against stale room state. Completion
history still uses each backend's world-quest adapter. Reward selection uses
the accepted quest level's cached pool, with the existing logged random-
equipment fallback if unavailable.
tests/async/run_world_quest_dual_backend.py exercises real quest grant and
persistence in disposable MariaDB and flat-file instances; focused
test_world_quest_* tests cover policy and failure boundaries.
Quests cannot be shared unless world.quest.share.max (default 0, at most 4)
allows it. Every completed quest pays one item reward to the player who
completed it; kill quests pay at completion instead of rolling rewards onto
corpses. The fee is world.quest.cost.per.level copper per level (default 20).
Shared NPC area-target pruning
The shared area-selection helper in src/core/utility.c protects an autonomous
NPC caster's eligible melee opponent as well as its explicit spell target from
random player-target pruning. It caps the skip count at the remaining
unprotected players. This policy applies to shared NPC area spells, including
Death Field; it does not bypass altitude, safe-room, alive-target or damage
checks. Charmed NPCs and bodies controlled by switched immortals retain the
player-controlled pruning policy.
test_death_field_runtime.py runs the production casting/selection/damage
chain with controlled world and defense fixtures. Its group-target cases
verify the pruning policy; they do not reproduce every reported encounter.
Epic point and epic skill levels
Epic points have no level requirement: every award path, the epic stones, and epic
potions work at any level. epic.skills.minLevel (default 30) is the lowest level at
which an epic teacher will teach.
Server difficulty dials
src/world/difficulty.c reads eighteen server-wide dials from the [difficulty]
section of lib/duris.properties. Each runs from 1 to 10. Setting 5 is always an exact
multiplier of 1.0 and every hook skips its arithmetic at 1.0;
difficulty.curve.NN maps the other settings to multipliers. Dials on
something players want (experience earned, loot drops and quality, mob gold, player
regeneration, epic points, artefact feeding) use the reciprocal, so a higher setting is
always harder. The dials stack on top of per-zone difficulty, and the mob dials apply
only to NPCs that are not a player's pet or morph.
The difficulty command lists the dials for gods; Forgers can set, preset and
save. It routes through properties set, which re-applies every property at once but
changes memory only until difficulty save. Mob hitpoints and mob gold apply to mobs
loaded afterwards, zone repop at each zone's next reset, and the rest immediately.
The dials multiply with zone difficulty: with the shipped zone factors, a
difficulty-10 zone under dial 10 gives mobs 9x hitpoints (2.0 x 4.5), 6x melee
damage (2.0 x 3.0) and 3x spell damage (2.0 x 1.5); no zone ships above 9. The
legacy 20-platinum coin bonus in read_mobile() is decided on the file's value,
so the mob gold dial scales the payout linearly.
Hooks: mob hitpoints in apply_zone_modifier(); melee after damage_mod in hit();
spell damage after the modifier profile in spell_damage(), outside its 2.0 cap; hit
chance in chance_to_hit(); breath through breath_damage_mod(); saving throws in
NewSaves(); memorisation in get_circle_memtime(); mob coins in read_mobile(); the
experience table in update_exp_table(); earned experience (not PvP) and the death loss
in gain_exp(); PC corpse decay through difficulty_pc_corpse_decay_minutes(); player
regeneration in hit_regen(), mana_regen() and move_regen(); loot in
check_random_drop() and create_random_eq_new() (random equipment);
zone lifespan in reset_zone(); epic points (not PvP) in prepare_epic_award(); artefact
feeding in
artifact_feed_seconds(); bartender quests in the bartender fee, both backends'
sql_world_quest_can_do_another() (never fewer than one a day) and the kill count in
createQuest().
PSP regeneration
mana_regen(ch, display_only) supplies PSP admission, each recovery callback and staff
stat. Rates are points per game tick (300 pulses, nominally 75 seconds). Dead, dying,
incapacitated, fighting and sun-vulnerable characters in the existing suppressing light
conditions have zero recovery, including with meditation or equipment bonuses. Twilight
rooms and Globe of Darkness retain their existing sunlight exemptions.
Eligible characters above maximum decay at min(-1, (maximum - current) / 5). Otherwise
the base is the integer conversion of (POW + INT) / 2.5, or / 0.2 while meditating,
for both players and NPCs. Feeblemind quarters that base; sleeping doubles it and resting
adds half. Posture does not change PSP. The signed flat mana_reg modifier is added once,
then the recovery dial scales positive player gains only. There is no final square and
no extra meditation-skill bonus. At POW=INT=100, neutral normal/resting/sleeping rates
are 80/120/160; normal meditation is 1,000. Combat/sun suppression, status bonuses,
feeblemind, equipment and difficulty now affect the callback as well as the displayed
rate.
At full PSP, display reports potential positive recovery but admission returns zero.
A negative net rate can drain from full and stops at zero. Positive increments cap at
maximum; negative increments preserve gradual over-cap decay and clamp at zero. The
existing fractional accumulator, elapsed-pulse catch-up, duplicate suppression and NPC
cadence remain. Blocked callbacks terminate and discard fractional credit; a later start
begins with fresh elapsed accounting. Existing casting, meditation, rest, affect changes
and periodic player affect updates supply restarts. Combat exit relies on the next
eligible player affect tick, nominally within 75 seconds. That restart covers every
USES_MANA character, including racial PSP users without a psionic class, and lets
StartRegen() check the current rate at full or above-maximum PSP too. Negative
equipment recovery therefore resumes after suppression even from full PSP.
Movement retains its existing rates and cadence. Positive increments cap at maximum; vitality granted above maximum by restore spirit or Reaper drain decays gradually instead of losing all surplus at the first decrement. Delayed decay that crosses the maximum stops there, while negative equipment recovery below maximum still drains normally. A real rest command can start surplus decay after combat.
Racial pulse
Each race's action rates live in lib/duris.properties. spellcast.pulse.racial.<Race>
multiplies every spell's cast time; damage.pulse.racial.<Race> is the base melee round in
beats, to which damage.pulse.class.all and the class's damage.pulse.class.<Class> are
added in affects.c. Lower is faster in both. src/world/racial_pulse.c adds the pulse
command: gods list the rates with pulse or pulse list all; Forgers use
pulse adjust <cast|melee> <race> <value>, where the value is absolute and must have exactly
three decimals, and pulse save. Like the difficulty dials it routes through
properties set, so a change applies at once (melee by re-totalling every character of that
race) and stays in memory until saved.
Player-obtainable gear can carry negative APPLY_COMBAT_PULSE and APPLY_SPELL_PULSE
applies, as does the shaman spirit totem (spell pulse -1). Mob-only objects (no-show or
untakeable procs) keep theirs, as do spells and skills such as berserk and the reaver buffs.
Saved gear retains its saved applies; current prototypes determine newly created gear.