Skip to content

Source Ports Guides

Working with ZMAPINFO

ZMAPINFO is the ZDoom extended map-info lump. It describes map titles, music, sky textures, episode structure, cluster intermissions, and a default map baseline.

from wadlib import WadFile

with WadFile("mod.wad") as wad:
    zi = wad.zmapinfo   # ZMapInfoLump | None
    if zi is None:
        print("No ZMAPINFO lump found")
    else:
        # Iterate all map entries
        for entry in zi.maps:
            print(f"{entry.map_name}: {entry.title!r}")
            print(f"  music={entry.music}  sky={entry.sky1}")
            print(f"  next={entry.next}  secret={entry.secretnext}")
            if entry.par:
                print(f"  par={entry.par}s")
            if entry.cluster:
                print(f"  cluster={entry.cluster}")
            # Unrecognised keys land in entry.props dict
            for key, val in entry.props.items():
                print(f"  {key}={val!r}")

        # Look up a specific map
        e1m1 = zi.get_map("E1M1")
        if e1m1:
            print(e1m1.resolved_title())   # resolves LANGUAGE lookup if needed

        # Default map settings (applied to all maps unless overridden)
        dm = zi.defaultmap
        if dm:
            print(f"default music: {dm.music}")

        # Episode definitions
        for ep in zi.episodes:
            print(f"Episode: {ep.name!r}  starts={ep.map}  pic={ep.pic_name}")

        # Cluster intermission text
        for cluster in zi.clusters:
            print(f"Cluster {cluster.cluster_num}  music={cluster.music}")
            print(f"  exit: {cluster.exittext[:40]!r}...")
            print(f"  enter: {cluster.entertext[:40]!r}...")

# Serialize entries back to ZMAPINFO text
from wadlib.lumps.zmapinfo import serialize_zmapinfo
text = serialize_zmapinfo(zi.maps)
Property Type Description
zi.maps list[ZMapInfoEntry] All map blocks in declaration order
zi.episodes list[ZMapInfoEpisode] Episode definitions
zi.clusters list[ZMapInfoCluster] Cluster (intermission) definitions
zi.defaultmap ZMapInfoEntry \| None Baseline settings inherited by all maps
zi.get_map(name) ZMapInfoEntry \| None Look up by map name (case-insensitive)

Reading DEHACKED Patches

WadFile.dehacked exposes an embedded DEHACKED lump. Use DehackedFile to load a standalone .deh file. Both return a DehackedLump whose .parsed property gives the fully structured DehackedPatch.

from wadlib import WadFile

with WadFile("DOOM2.WAD") as wad:
    deh = wad.dehacked   # DehackedLump | None
    if deh:
        patch = deh.parsed

        print(f"Doom version: {patch.doom_version}")
        print(f"Patch format: {patch.patch_format}")

        # Custom thing types — things with an ID # = N line giving a DoomEdNum
        for type_id, thing in patch.things.items():
            print(f"  EdNum {type_id}: {thing.name!r}  hp={thing.hit_points}")

        # All thing-stat patches (keyed by internal Doom thing number)
        for idx, thing in patch.all_things.items():
            if thing.hit_points is not None:
                print(f"  Thing {idx}: hp={thing.hit_points}  speed={thing.speed}")

        # Frame / state patches
        for idx, frame in patch.frames.items():
            print(f"  Frame {idx}: sprite={frame.sprite_number}  dur={frame.duration}")

        # Weapon patches
        for idx, weapon in patch.weapons.items():
            print(f"  Weapon {idx}: ammo/shot={weapon.ammo_per_shot}")

        # Ammo patches
        for idx, ammo in patch.ammo.items():
            print(f"  Ammo {idx}: max={ammo.max_ammo}  per={ammo.per_ammo}")

        # Text string replacements
        for text in patch.texts:
            print(f"  {text.original!r} -> {text.replacement!r}")

        # BEX extended string replacements (keyed by logical name)
        for key, val in patch.bex_strings.items():
            print(f"  [BEX] {key} = {val!r}")

        # BEX cheat codes ([CHEATS] section)
        for name, code in patch.cheats.items():
            print(f"  cheat {name!r} = {code!r}")

        # PAR times (keyed by "E1M1" or "MAP01")
        for map_name, secs in patch.par_times.items():
            print(f"  PAR {map_name}: {secs}s")

        # Pointer blocks (frame_index -> codepointer_frame_index)
        for frame_idx, codep in patch.pointers.items():
            print(f"  Pointer frame {frame_idx}: codep={codep}")

# Load a standalone .deh file (same API as DehackedLump)
from wadlib.lumps.dehacked import DehackedFile

deh_file = DehackedFile("my_mod.deh")
patch = deh_file.parsed
for type_id, thing in patch.things.items():
    print(f"Custom thing {type_id}: {thing.name!r}")

DehackedPatch field summary:

Field Type Contents
doom_version int \| None Engine version from the patch header
patch_format int \| None Patch format version
things dict[int, DehackedThing] Custom things only (those with a DoomEdNum)
all_things dict[int, DehackedThing] Every patched thing, keyed by internal index
frames dict[int, DehackedFrame] Frame/state patches
weapons dict[int, DehackedWeapon] Weapon patches
ammo dict[int, DehackedAmmo] Ammo capacity patches
sounds dict[int, DehackedSound] Sound remap patches
misc dict[int, DehackedMisc] Misc settings (initial health, etc.)
texts list[DehackedText] Text string replacements
par_times dict[str, int] PAR times in seconds per map
bex_strings dict[str, str] BEX extended string replacements
bex_codeptr dict[int, str] BEX codepointer names
pointers dict[int, int] Pointer block (frame → codepointer frame)
cheats dict[str, str] BEX [CHEATS] section

Working with ANIMDEFS

ANIMDEFS defines flat and texture animation sequences used by Hexen and ZDoom-based source ports. wad.animdefs returns an AnimDefsLump.

from wadlib import WadFile

with WadFile("mod.wad") as wad:
    ad = wad.animdefs   # AnimDefsLump | None
    if ad:
        print(f"{len(ad.animations)} total animation sequences")
        print(f"  {len(ad.flats)} flat animations")
        print(f"  {len(ad.textures)} texture animations")

        for anim in ad.animations:
            timing = "random" if anim.is_random else "fixed"
            print(f"  [{anim.kind}] {anim.name}: {len(anim.frames)} frames ({timing})")

            for frame in anim.frames:
                if frame.min_tics == frame.max_tics:
                    print(f"    pic {frame.pic}: {frame.min_tics} tics")
                else:
                    print(f"    pic {frame.pic}: {frame.min_tics}{frame.max_tics} tics")

        # resolve_frames() maps Hexen-style numeric pic indices to lump names.
        # Supply the full ordered flat or texture name list as the reference.
        flat_names = list(wad.flats.keys())

        for anim in ad.flats:
            names = anim.resolve_frames(flat_names)
            if names:
                print(f"  {anim.name}: {' -> '.join(names)}")
            # Returns None if the base name is absent or any index is out of bounds

        # For texture animations, use the TEXTURE1/TEXTURE2 name order
        tex_names = [t.name for t in (wad.texture1 or {}).textures] if wad.texture1 else []
        for anim in ad.textures:
            names = anim.resolve_frames(tex_names)
            if names:
                print(f"  texture {anim.name}: {names}")
Property Type Description
ad.animations list[AnimDef] All animation sequences (flats + textures)
ad.flats list[AnimDef] Flat animations only
ad.textures list[AnimDef] Texture animations only
anim.kind "flat" \| "texture" Resource kind
anim.name str Base lump/texture name
anim.frames list[AnimFrame] Frame sequence
anim.is_random bool True if any frame has variable timing
anim.resolve_frames(names) list[str] \| None Resolve pic indices to lump names
frame.pic int 1-based index from the base name
frame.min_tics int Minimum display duration in game tics
frame.max_tics int Maximum duration (equals min_tics for fixed timing)

Reading DECORATE Actors

DECORATE lumps define custom actors for ZDoom-based mods. WadFile.decorate returns a PWAD-aware DecorateLump (or None if the lump is absent).

from wadlib import WadFile

with WadFile.open("DOOM2.WAD", "mod.wad") as wad:
    dec = wad.decorate
    if dec:
        for actor in dec.actors:
            print(f"{actor.name} (parent={actor.parent}, "
                  f"ednum={actor.doomednum}, "
                  f"radius={actor.radius}, height={actor.height})")

# Parse a raw DECORATE text string directly
from wadlib.lumps.decorate import parse_decorate

text = """
Actor MyMonster : Zombieman 1234 {
    Radius 20
    Height 56
    States { Spawn: POSS A 10 Loop }
}
"""
actors = parse_decorate(text)
print(actors[0].name)          # "MyMonster"
print(actors[0].doomednum) # 1234
print(actors[0].parent)        # "Zombieman"

Working with LANGUAGE Strings

LANGUAGE lumps store localised UI strings for ZDoom mods. The lump is partitioned into locale sections; combined headers like [enu default] expand to both locales automatically.

from wadlib import WadFile

with WadFile("mod.wad") as wad:
    lang = wad.language
    if lang:
        # Look up a string in the [enu] / [default] pool
        msg = lang.lookup("PICKUPMSG", default="You got something!")
        print(msg)

        # All locales as a nested dict
        for locale, strings in lang.all_locales.items():
            print(f"[{locale}] {len(strings)} strings")

        # Per-locale access
        french = lang.strings_for("fra")
        msg_fr = lang.lookup("PICKUPMSG", locale="fra")

Strife Conversation Scripts

Strife stores NPC dialogue in binary SCRIPTxx lumps (one per map) and optionally a DIALOGUE lump. wadlib exposes these through WadFile.dialogue and WadFile.strife_scripts.

from wadlib import WadFile

with WadFile("STRIFE1.WAD") as wad:
    # Primary DIALOGUE lump (falls back to first SCRIPTxx if absent)
    conv = wad.dialogue   # ConversationLump | None

    # All conversation lumps — DIALOGUE plus every SCRIPTxx
    for lump_name, lump in wad.strife_scripts.items():
        print(f"{lump_name}: {len(lump.pages)} dialogue pages")

    if conv:
        for page in conv.pages:
            print(f"Speaker (thing type {page.speaker_id}): {page.name!r}")
            if page.voice:
                print(f"  Voice lump: {page.voice!r}")
            if page.back_pic:
                print(f"  Background: {page.back_pic!r}")
            print(f"  Text: {page.text[:80]!r}...")

            # active_choices skips the five always-present slots that are empty
            for choice in page.active_choices:
                print(f"  [{choice.text!r}] -> page {choice.next}")
                # Items required to take this choice
                for item_type, amount in zip(choice.need_items, choice.need_amounts):
                    if item_type:
                        print(f"    needs thing type {item_type} × {amount}")
                # Item given on success
                if choice.give_item:
                    print(f"    gives thing type {choice.give_item}")
                if choice.objective:
                    print(f"    sets objective page {choice.objective}")

            # What the NPC drops on death
            if page.drop_item:
                print(f"  Drops thing type {page.drop_item} on death")

            # Pre-conditions: items required before this page is shown
            for item in page.check_items:
                if item:
                    print(f"  Requires thing type {item} in inventory")

# Round-trip: re-serialize pages to binary (1 516 bytes per page)
from wadlib.lumps.strife_conversation import conversation_to_bytes
raw = conversation_to_bytes(conv.pages)
Field Type Description
page.speaker_id int Thing type of the NPC (matches thing catalog)
page.name str Display name shown in the dialogue UI
page.voice str Audio lump name to play (empty = silent)
page.back_pic str Background flat or picture lump
page.text str Main dialogue text (up to 320 characters)
page.choices tuple[…] Exactly 5 ConversationChoice slots
page.active_choices list[ConversationChoice] Non-empty choices only
page.drop_item int Thing type dropped when NPC dies (0 = none)
page.check_items tuple[int, int, int] Items required to trigger this page
page.jump_to int 1-based index of page to jump to first (0 = show directly)
choice.text str Button label
choice.text_ok str Response when requirements are met
choice.text_no str Response when requirements are not met
choice.next int Next page index (0 = end conversation, −1 = close immediately)
choice.give_item int Thing type given on success (0 = none)
choice.need_items tuple[int, int, int] Required inventory item types
choice.need_amounts tuple[int, int, int] Required quantities
choice.objective int Objective log page to set (0 = none)