Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CLI Usage

The cwad binary ships with the crustywad-cli crate and provides quick WAD inspection from the command line.

Installation

Build and install from the workspace:

cargo install --path crates/crustywad-cli

Or run directly without installing:

cargo run -p crustywad-cli -- <subcommand> [options] <file.wad>

Synopsis

cwad [OPTIONS] <COMMAND>

Subcommands

info

Print a WAD summary: the kind (Iwad or Pwad), total lump count, data size, detected maps, an audio-lump tally, and — when the WAD positively identifies as a specific game family — a game: line.

$ cwad info doom.wad
kind:      Iwad
lumps:     1264
data size: 4194304 bytes
maps:      E1M1, E1M2
audio:     midi: 3, digital: 12

A game: line appears only when the WAD positively identifies as a specific game family (currently Strife, via its dialogue lumps — see Game identification); a Doom WAD prints none.

list

Print the full lump directory. Each line contains the zero-based index, the file offset (filepos), the byte size, and the lump name.

$ cwad list doom.wad
0000       12     1160 PLAYPAL
0001     1172     4096 COLORMAP
0002     5268        0 ENDOOM
...

Column order: index filepos size name.

validate

Check whether a WAD file parses without errors and exits with the appropriate code (see Exit codes).

$ cwad validate doom.wad
ok: doom.wad

On a corrupt file:

$ cwad validate broken.wad
error: broken.wad: invalid WAD magic

The error message goes to stderr in human format; the exit code is 2.

Deep validation

--deep goes beyond the header and directory: after the container parses, every map in the WAD is assembled — all four formats, including Doom 64 nested-WAD maps — with per-map errors and warnings reported. Validation continues past a failing map so one corrupt map cannot mask another.

$ cwad validate --deep doom.wad
ok: doom.wad (36 map(s) validated)

On a WAD whose E1M1 has a corrupt lump:

$ cwad validate --deep broken.wad
error: map E1M1: failed to decode LINEDEFS records: record stream ended mid-record at byte offset 0
error: broken.wad: 1 of 2 map(s) failed validation

Per-map diagnostics go to stderr; the exit code is 1 if any map fails — ADR-0008’s “validation errors found” code, distinct from 2 (the container itself is unreadable or malformed). The strictness flag applies: under --lenient, recoverable per-map issues become warnings on stderr and the exit code stays 0. In JSON format, --deep emits one newline-delimited record per map ({"map":"E1M1","ok":true,"warnings":0} or {"map":"E1M1","ok":false,"error":"..."}) followed by the usual summary object; in CSV it emits a map,ok,error table instead of the shallow ok/true pair.

merge

Combine multiple WAD files into one, writing lumps in the order the input files are given.

$ cwad merge base.wad patch.wad --output combined.wad

Use --kind to set the output WAD kind (iwad or pwad; default pwad). Lump-name or size validation failures during the write exit 3.

diff

Compare two WAD files lump by lump: same lump names, same count of each name, and same data for each occurrence. Directory order of distinct lump names does not matter; for a name that appears more than once, the sequence of occurrences is compared in directory order. Exits 0 if identical, 1 if any differences are found, or 2 on I/O or parse error.

$ cwad diff doom.wad doom-modified.wad
Only in doom.wad:  DEMO1
Changed:           E1M1

extract

Extract lumps from a WAD file into a directory (which must already exist). Extracts every lump by default, or only the occurrences of one lump name via --lump/-l. Each lump is written as <SANITIZED_NAME>.bin; when two or more lumps sanitize to the same filename, later ones get a _1, _2, … suffix.

$ cwad extract doom.wad --output ./out
PLAYPAL.bin
COLORMAP.bin
...

build

Build a new WAD file from NAME=FILE lump specifications, added to the output in the order listed.

$ cwad build --output custom.wad E1M1=e1m1.lmp PLAYPAL=playpal.lmp
wrote custom.wad: kind=Pwad lumps: 2

Use --kind iwad to build an IWAD instead of the default PWAD. Lump-name or size validation failures exit 3.

Building nodes: build --nodes

Pass --nodes to rebuild, after packing, every Doom-format map group in the output with engine-playable node lumps via the add_doom_map_with_nodes one-shot — the BSP tree (SEGS/SSECTORS/NODES), the collision BLOCKMAP, and the all-clear REJECT — every Hexen-format map group via an in-place node-lump splice (below), and every UDMF-format map group with a built ZNODES stream (a GL dialect by default; see --node-format below), replacing any existing ZNODES (or inserted right after TEXTMAP if the group has none) with the rest of the group’s lumps carried through unchanged:

$ cwad build --nodes -o playable.wad MAP01=map01.lmp THINGS=things.lmp ...
wrote playable.wad: kind=Pwad lumps: 11

All of a rebuilt Doom group’s node lumps — SEGS/SSECTORS/NODES, the REJECT visibility table, and the BLOCKMAP — are overwritten with the newly built ones, whether they were packed as empty placeholders or already held data. The map’s packed VERTEXES lump can also grow: the BSP pass appends any split vertices it creates to it.

A Hexen group is patched in place instead of reassembled: THINGS, LINEDEFS, SIDEDEFS, SECTORS, and BEHAVIOR carry through byte-verbatim, while SEGS/SSECTORS/NODES are rebuilt for whichever --node-format is in effect — Hexen accepts every format, including the classic default, using the same carrier conventions as a Doom group. REJECT and BLOCKMAP are always rebuilt, so a hand-tuned REJECT is replaced with the engine-safe all-zeros table. The group is re-emitted in the canonical THINGSBEHAVIOR order, since vanilla-class engines index a map’s lumps by offset from the marker; a corrupt node lump among the group’s own five (SEGS/SSECTORS/NODES/REJECT/BLOCKMAP) is repaired rather than fatal, but a separate in-WAD GL_<mapname> sidecar is not — a corrupt sidecar still strict-fails assembly (--lenient recovers) and a stale one passes through verbatim beside the rebuilt lumps. A map using polyobjects prints a warning that the rebuilt nodes may split a polyobject’s subsector; it fires on both the vanilla Hexen (3000–3002) and ZDoom Doom-in-Hexen (9300–9303) editor numbers and is advisory, since 3001/3002 are also the Doom Imp/Demon (polyobject-aware splitting is tracked in #389). See Building nodes for the full splice details.

Doom 64 (#353) map groups remain the only ones not yet supported by --nodes; they are passed through unchanged with a note on stderr. Non-map lumps always pass through unchanged; if none of a Doom, Hexen, or UDMF map group is found, --nodes is a no-op and prints a note.

build --nodes takes the same --node-format <FORMAT> flag as convert --nodesclassic (default), the non-GL extended pair (xnod/znod), the four GL dialects (xgln/xgl2/xgl3/gl), and their z* zlib twins. A UDMF map group’s ZNODES stream accepts any of them: classic auto-selects gl (noted on stderr once per group); an explicit xnod/znod builds a non-GL extended stream instead. The classic BSP pass behind them is integer-precision, so a fractional-coordinate UDMF map is rejected in strict mode (naming the offending coordinate, with a --lenient hint) and rounded to the nearest whole unit with a warning in lenient mode — the rounding applies to the node stream only, the TEXTMAP keeps the fractional originals; a map that needs exact fractional geometry preserved should use a GL dialect instead:

$ cwad build --nodes --node-format gl -o playable.wad MAP01=map01.lmp THINGS=things.lmp ...
wrote playable.wad: kind=Pwad lumps: 11

See Choosing the on-disk node format for the full value table — it applies identically to build --nodes and convert --nodes. The global --lenient flag applies to the node build too — a strict-mode build failure exits 3, and, when the error is one lenient mode can recover, hints to re-run with --lenient. See Building nodes for the full picture.

convert

Convert every map in a WAD between the classic Doom binary format and UDMF, replacing each map’s lump run in place; non-map lumps, and maps already in the target format, pass through unchanged in directory order.

$ cwad convert doom.wad -o udmf.wad --to udmf
wrote udmf.wad: converted 1 map to udmf

--to is required and takes doom or udmf. Use --map NAME to convert only the named map (e.g. --map MAP01) and pass every other map through unchanged; omit it to convert every map in the WAD. A --map NAME that matches no map in the WAD is an error (exit 3), not a no-op. Use --kind to set the output WAD kind (iwad or pwad; default pwad).

Building nodes: --nodes

By default, --to doom emits empty SEGS/SSECTORS/NODES/REJECT/BLOCKMAP lumps and always prints a NodesNotBuilt warning to stderr — playable on the ZDoom family (which rebuilds nodes at load) but not on vanilla ports. Pass --nodes to build those lumps for real, so the output is engine-playable everywhere with no external nodebuilder pass:

$ cwad convert udmf.wad -o doom.wad --to doom --nodes --lenient
wrote doom.wad: converted 1 map to doom

--nodes builds the classic 16-bit node lumps via the nodebuild pipeline (add_doom_map_with_nodes): the BSP tree, the collision BLOCKMAP, and the all-clear REJECT. The NodesNotBuilt warning is then gone (the nodes exist). The global --lenient flag applies to the build too — it is often needed for real maps, whose geometry can contain the engine-tolerated mixed-sector fan that strict mode rejects (see Building nodes).

--nodes combined with --to udmf instead builds a ZNODES stream for each converted map — UDMF has no binary node lumps, so ZNODES is the only place the dialect selected by --node-format has to go. The default classic auto-selects gl and prints a note; any explicit value (GL or non-GL) needs no note:

$ cwad convert doom.wad -o out.wad --to udmf --nodes
note: --to udmf --nodes builds GL nodes (gl auto-format) into ZNODES for each converted map
wrote out.wad: converted 1 map to udmf

A source map already in UDMF is not converted — but --to udmf --nodes retrofits its ZNODES stream in place rather than passing the group through untouched: the group’s TEXTMAP bytes are re-emitted verbatim, any port lump in the group (DIALOGUE, BEHAVIOR) is preserved untouched, and a stale or corrupt existing ZNODES is replaced (or inserted right after TEXTMAP if the group has none). A per-group note reports the retrofit (is already UDMF; rebuilt ZNODES in place (map not converted)), and it is not counted in converted N maps — this is a patch, not a conversion. A map excluded from the run by --map passes through unchanged with no retrofit. cwad build --nodes remains the spec-based alternative: it rebuilds ZNODES in place directly from NAME=FILE lump specs, without needing a whole WAD as input.

--node-format <FORMAT> selects the on-disk form of the nodes --nodes builds; default classic. It has no effect without --nodes; a non-classic value passed without --nodes prints a note on stderr and is ignored. Besides classic, the values are the non-GL extended pair — xnod (uncompressed XNOD stream in NODES) and znod (its zlib-compressed twin) — and four GL dialects — xgln, xgl2, xgl3, and gl (auto-selects the minimal sufficient dialect) — each carried in SSECTORS instead of NODES (SEGS/NODES left empty). Every GL value also has a z* zlib-compressed twin (zgln/zgl2/zgl3/zgl). All z* values, GL and non-GL alike, require cwad built with the extended-nodes-zlib feature (on by default) — without it, a z* value that actually takes effect (i.e. --nodes is in play) exits 3 with a clear error rather than a clap parse failure; without --nodes the flag is noted and ignored as described above.

--to udmf --nodes accepts any --node-format value. The ZNODES container can carry either a GL stream or the non-GL extended pair (XNOD/ZNOD) — engines accept both. The default classic auto-selects gl; an explicit xnod/znod builds that non-GL stream instead. The classic BSP pass behind them narrows coordinates through the shared integer write path, so a fractional-coordinate UDMF map exits 3 in strict mode, naming the offending coordinate and hinting at --lenient (lenient rounds for the node stream only — the TEXTMAP keeps the fractional originals):

$ cwad convert fractional.wad -o out.wad --to udmf --nodes --node-format xnod
error: failed to build nodes for map MAP01: fractional x 0.5 in vertex #0 cannot be stored as an i16
note: re-run with --lenient to build anyway

--lenient instead rounds the fractional coordinate to the nearest whole map unit and reports a warning; a map that needs the fractional geometry preserved exactly should use a GL dialect (gl, or one of xgln/xgl2/ xgl3), which has no such precision ceiling.

See Choosing the on-disk node format for the full value table.

Strict mode refuses data loss. Converting a typical ZDoom-namespace UDMF map (linedef args, thing height/id/special, …) to doom exits 3, naming the offending field on stderr:

$ cwad convert udmf.wad -o doom.wad --to doom
error: cannot convert map MAP01 to doom: thing #0 has a height value, which the Doom format cannot represent
note: re-run with --lenient to accept the data loss

This is intended, not a bug: --to doom succeeding is the answer to “does this map fit in the Doom format?” Pass the global --lenient flag to accept the loss and convert anyway; each dropped or rounded field is then reported as a warning on stderr instead. See Converting maps for the full loss policy.

A converted map keeps only the lumps its target format defines. A converted group is rebuilt from the assembled map: the marker plus TEXTMAP and ENDMAP (--to udmf), or the marker plus the classic THINGS/LINEDEFS/SIDEDEFS/VERTEXES/SECTORS run and the empty node lumps (--to doom). Any other lump that lived inside the map group — BEHAVIOR (compiled ACS), SCRIPTS, ZNODES, DIALOGUE, GL node lumps — is dropped. It is not passed through: compiled ACS is bound to the source map’s specials and node lumps describe the source geometry, so carrying either into a converted map would produce something that looks intact and is subtly broken. Dropping it is data loss, and is treated like any other:

$ cwad convert hexen.wad -o udmf.wad --to udmf
error: cannot convert map MAP01 to udmf: it contains lump(s) that cannot be carried into the converted map: BEHAVIOR
note: re-run with --lenient to convert anyway and drop them

With --lenient the conversion proceeds and each dropped lump is named in a warning on stderr. A map already in the target format is not converted, so nothing in its group is dropped.

Exits 0 on success, 2 on I/O or parse error, 3 if a map cannot be assembled, cannot be converted without loss in strict mode, or if --map NAME matches no map in the WAD.

Global options

FlagShortDescription
--lenientUse lenient parsing instead of strict when reading a WAD; attempts best-effort recovery for non-fatal issues and emits warnings to stderr. For build, also uses lenient instead of strict validation when writing
--format <FORMAT>-FOutput format: human (default), json, or csv
--help-hPrint help and exit 0
--version-VPrint version and exit 0

Lenient mode

In lenient mode cwad attempts best-effort recovery and prints warnings to stderr for any non-fatal issues encountered.

cwad --lenient info damaged.wad

Example output when the WAD magic is unrecognized:

kind:  Unknown([88, 87, 65, 68])
lumps: 3
warning: unrecognized WAD magic `XWAD`

Output formats

All subcommands accept the --format / -F flag, but merge does not currently produce any structured stdout output — it only writes the merged file, and warnings/errors still go to stderr regardless of format.

human (default)

Human-readable text written to stdout. Warnings and errors go to stderr.

json

Newline-delimited JSON (one object per record). Useful for scripting and piping into tools like jq.

cwad -F json info doom.wad
{"kind":"Iwad","lumps":1264}
cwad -F json list doom.wad
{"index":0,"filepos":12,"size":1160,"name":"PLAYPAL"}
{"index":1,"filepos":1172,"size":4096,"name":"COLORMAP"}
cwad -F json validate doom.wad
{"ok":true}

On parse failure the validate subcommand writes {"ok":false,"error":"..."} to stdout and exits 2.

csv

RFC 4180 CSV with a header row. Field values that contain commas, quotes, or newlines are wrapped in double-quotes with internal quotes doubled.

cwad -F csv info doom.wad
kind,lumps,data_size,maps,game
Iwad,1264,4194304,E1M1 E1M2,

The trailing game field is empty unless the WAD positively identifies (e.g. strife for a Strife WAD).

cwad -F csv list doom.wad
index,filepos,size,name
0,12,1160,PLAYPAL
1,1172,4096,COLORMAP
cwad -F csv validate doom.wad
ok
true

Exit codes

CodeMeaning
0Success
1Negative result — the two WADs differ (diff), or validate --deep found map validation errors
2I/O error or parse error (malformed WAD, missing file, etc.); for extract, also a nonexistent --output directory or a --lump name not found
3Usage error (unknown subcommand, invalid flag value, missing required argument, or a lump-name/size validation failure when writing for build, merge, or convert — note a non-ASCII lump name decodes under a lenient read but is rejected on write in both strictness modes); for convert, also a map that fails to assemble, a map that cannot be converted without loss in strict mode (including a group lump such as BEHAVIOR that the target format cannot carry), or a --map NAME that matches no map in the WAD; for build --nodes, also a Doom map group that fails to assemble or a node build that fails in strict mode

Man page

A man page (cwad.1) is generated into $OUT_DIR/man/ at build time via clap_mangen. To install it system-wide after building the crate, copy the generated file to the appropriate man directory, for example:

install -m 644 \
  "$(cargo build -p crustywad-cli --message-format=json \
      | jq -r 'select(.reason=="build-script-executed") | .out_dir')/man/cwad.1" \
  /usr/local/share/man/man1/cwad.1
mandb

Shell completions

Completion scripts for bash, zsh, and fish are generated into $OUT_DIR/completions/ at build time via clap_complete. Source the appropriate script for your shell to enable tab completion for cwad subcommands and flags.