L2 Architecture
Tech stack
C99, a little C++ for platform libraries, and an embedded Lua 5.5 interpreter.
- C99 for the game (
include/cstd.h,README): one portable code base. 4-space indentation, 78 columns (DEVEL/code_style.txt). - C++ only where a platform library needs it, for example
sys/share/cppregex.cppandwin/Qt/. - Lua, vendored in
nhlua/lua/: levels, dungeon layout, themed rooms and quest text are data scripts, replacing the old yacc/lex compilers (README). - Make with a "hints" system for Unix (
sys/unix/); GNUmakefile, nmake and Visual Studio files on Windows (sys/windows/). - Optional libraries: curses, X11, Qt for windows; FMOD and platform sound libraries (
sound/).
Project structure
Shared code is in src/ and include/; platform code is in sys/ and win/.
nethack/
src/ game code shared by all platforms (about 130 C files)
include/ headers: hack.h is the common one, extern.h the prototypes
dat/ data and Lua scripts: levels, dungeon.lua, quest.lua, help text
util/ build-time tools: makedefs, dlb_main, recover, sfctool, tile tools
sys/ operating-system layers: unix, windows, msdos, amiga, vms, libnh, share
win/ window ports: tty, curses, Qt, X11, win32, chain, shim
sound/ sound libraries
nhlua/ the vendored Lua source and its update script
doc/ Guidebook, man pages, fixes lists, window.txt, sound.txt, lua.adoc
DEVEL/ git setup and developer notes for contributors
test/ Lua test scripts run inside the game in wizard mode
outdated/ retired ports kept for reference
submodules/ PDCurses sources (git submodules, empty in a plain snapshot)
Subsystems
The game has these major parts; the L3 module of the same name describes each.
- Game loop and startup:
src/allmain.c;main()lives in the platform layer (sys/unix/unixmain.c). - Global state:
src/decl.candinclude/decl.h. - Commands and options:
src/cmd.c,src/options.c,src/cfgfiles.c. - Hero and actions: movement, combat, items in use,
src/hack.c,src/uhitm.c,src/worn.c,src/invent.c. - Monsters:
src/mon.c,src/monmove.c,src/mhitu.c,src/makemon.c,include/monsters.h, pets insrc/dog*.c. - Objects:
src/objects.c,include/objects.h,src/mkobj.c,src/objnam.c. - Level generation and dungeon:
src/mklev.c,src/sp_lev.c,src/dungeon.c. - Lua integration:
src/nhlua.c,src/nhlobj.c,src/nhlsel.c. - Display and vision:
src/display.c,src/vision.c,src/light.c,src/glyphs.c. - Window ports:
src/windows.c,include/winprocs.h,win/. - Save and restore:
src/save.c,src/restore.c,src/sfstruct.c,src/sfbase.c,src/bones.c. - Platform layers and build:
sys/,util/,dat/Makefilegeneration. - Sound:
src/sounds.c,include/sndprocs.h,sound/.
sys/<os>/ main() ---> allmain.c: early_init, newgame or dorecover, moveloop
|
+-----------------------+------------------------+
| | |
cmd.c commands monsters (mon.c) level (mklev.c, sp_lev.c)
hack.c hero moves monmove.c, mhitu.c dungeon.c, Lua scripts (dat/)
| | |
+-----------+-----------+------------------------+
|
display.c, vision.c (glyphs for what the hero sees)
|
windowprocs table ---> win/tty | curses | Qt | X11 | win32 ...
(win/chain wraps it; win/shim for libnh)
Data flow
Keys become commands, commands change state, and state flows back out as glyphs and messages.
rhack()insrc/cmd.ctakes the next command from the command queue, or else fromparse(), which reads a key throughreadcharand the window port.rhack()finds the key in a runtime binding list (gc.Cmd.cmdbinds), which maps it to a row of the command table, and calls the handler.- A handler that uses time returns
ECMD_TIME, which makesrhack()setsvc.context.move. On the next pass,moveloop_core()insrc/allmain.cfirst pays for that action: monsters move and timers run (src/timeout.c). Then it reads the next command. - Changes call
newsym()(src/display.c);vision_recalc()(src/vision.c) decides what the hero can see. - Glyphs and messages (
pline,src/pline.c) go throughwindowprocsto the active window port.
One call of moveloop_core() is one pass, not one turn: the time of the previous action, then one input. A turn ends only when the hero and every monster are out of movement points. See the L3 module "Game loop and startup".
Scenario
One example: the hero moves one square east in the tty interface.
unixmain.chas already calledmoveloop(), which callsmoveloop_core()repeatedly.rhack()mapslto the move handler;domove()insrc/hack.cchecks for monsters and terrain.- The hero's position changes,
vision_recalc()runs,newsym()marks changed squares. flush_screen()makes the tty port print the changed squares; monsters then move.
Data model
A few big structures: the hero, the current level, monster and object chains, the dungeon table.
struct you(u,include/you.h): the hero. The hero is also astruct monst,gy.youmonst, which shared monster code receives.- The map
levl[x][y](include/rm.h) and the current level recordsvl.level(include/decl.h): an 80 by 21 grid (COLNO,ROWNOininclude/global.h) with object lists per square. struct monst(include/monst.h) andstruct obj(include/obj.h): linked lists. A monster is on the level, migrating or travelling with the hero; an object is in one of ten places named byobj->where. Per-type data ispermonst(include/permonst.h, data ininclude/monsters.h) andobjclass(include/objclass.h, data ininclude/objects.h). Both tables are runtime copies;objects[]is rewritten per game and saved.svd.dungeonsand the level ledger (include/dungeon.h,src/dungeon.c): which dungeons exist and how deep.- Globals:
g*structures hold engine state,sv*structures state that belongs to a saved game (include/decl.h). The prefix saves nothing: every saved field is written by hand. The herou,flagsandiflagsare plain globals outside both.
Recurring patterns
Four habits explain why a small change often has side effects.
- One header, several expansions (X-macros):
include/objects.h,include/monsters.h,include/optlist.h,include/defsym.h,include/seffects.handinclude/artilist.hare each included several times with different macros. Entry order is the numbering. - Tables mirrored by hand:
struct window_procsandstruct chain_procs,soundlib_choices[]andenum soundlib_ids,defsymsandaltlabels[], the save and restore routines. The compiler does not catch a mismatch. - Orders written in comments:
newgame()start-up order, the glyph enum, skill numbers, dragons,makelevel()steps. Nothing enforces them. - State passed in globals: hero direction (
u.dx), attack position (gb.bhitpos), the pending action (ga.afternmv), the speaker (gv.voice).
Each L3 module lists the traps that apply to it.
State and persistence
A save holds the sv* globals, the hero, the dungeons and every visited level.
- The game always writes the
historicalformat, a raw dump of C structs.exportasciiexists only in the offline utilitysfctool. - While the game runs, each level that was left is written to a level file (
create_levelfileinsrc/files.c); saving gathers them (src/save.c) and restoring splits them out again (src/restore.c). Saving frees the running game, and a save is deleted when restored (except in wizard and explore mode). - Not saved: window state and most
g*globals, though a fewg*members are saved by their own routines (inventory, migrating monsters). The only Lua data saved is thenh_lua_variablestable. - Time values are saved relative to the current turn. A change to a saved structure needs
EDITLEVELininclude/patchlevel.hraised.
Configuration layers
Behaviour is set at build time, by a system file, and by the player's file.
- Build time:
include/config.h,include/unixconf.hand the like, overridden bysys/unix/hints/variables such asWANT_WIN_CURSES=1. - System: the
sysconffile (shared settings, wizard-mode user list, limits), read bysrc/sys.candsrc/cfgfiles.c. - Player:
.nethackrcor equivalent, read insrc/cfgfiles.c, which handsOPTIONS=lines tosrc/options.c; the in-gameOcommand uses the same parser. - Environment variables and command-line flags such as
-D,-X,-u(doc/nethack.txt).NETHACKOPTIONSmay hold options or name a file. - Order of precedence, last wins: built-in default, sysconf, RC file,
NETHACKOPTIONS, command line, then play (enum option_phases,include/global.h).
Extension points
New window systems, sound libraries and Lua scripts plug in without touching the core.
- Window port: fill a
struct window_procs(include/winprocs.h), described indoc/window.txt. Two other tables must mirror it by hand. - Sound library: fill a sound-procs table (
include/sndprocs.h), described indoc/sound.txt, which is partly out of date. Adding one touches about five places. - Lua: a script API under the
nhanddestables (doc/lua.adoc). - Regular expressions: one of three interchangeable files in
sys/share/(DEVEL/code_features.txt).
Trust and safety boundaries
Lua scripts run in a sandbox; save and bones files are trusted input, with some checks.
- The sandbox (
NHL_SANDBOXininclude/config.h,nhl_sandbox_infoininclude/global.h) removes risky Lua libraries (noio,os,package,debug,load) and limits memory and instruction counts. Running out of instructions always panics. - A script error is fatal by policy: nearly every call into Lua panics on failure. Treat a script as part of the program.
- Wizard mode (
-D) is gated by theWIZARDSlist insysconfwhere SYSCF is enabled. - A save file carries Lua source text that is run on restore (inside the sandbox). Restore checks the version, feature bits, structure sizes and the owner (
CHECK_SAVE_UID, default on); struct contents are not range-checked, and a truncated save inside a level is only partly guarded.
Outside interfaces
The game has a command line, config files, data files and an optional library build.
- CLI flags in
doc/nethack.txt; utilitiesrecover(rebuild a crashed game, needsINSURANCEininclude/config.h) andsfctool(convert the save file format), both inutil/. - File formats: save, bones, level files, record and logfile, the
dlbdata library (util/dlb_main.c). - Library build:
sys/libnh/produceslibnethack.aor a WebAssembly module for other programs.