Skip to content

Maps Guides

Working with UDMF Maps

Parse, build, and serialize UDMF (Universal Doom Map Format) text-based maps used by ZDoom and other modern source ports.

from wadlib.lumps.udmf import (
    parse_udmf, serialize_udmf, UdmfMap,
    UdmfThing, UdmfVertex, UdmfLinedef, UdmfSidedef, UdmfSector,
)

# Parse an existing UDMF TEXTMAP
textmap_source = """
namespace = "zdoom";
thing { x = 64.0; y = -128.0; angle = 90; type = 1; }
vertex { x = 0.0; y = 0.0; }
vertex { x = 256.0; y = 0.0; }
linedef { v1 = 0; v2 = 1; sidefront = 0; }
sidedef { sector = 0; texturemiddle = "BRICK1"; }
sector { heightfloor = 0; heightceiling = 128;
         texturefloor = "FLAT1"; textureceiling = "CEIL3_5"; }
"""
udmf = parse_udmf(textmap_source)
print(f"{udmf.namespace}: {len(udmf.things)} things, {len(udmf.vertices)} verts")

# Build a UdmfMap from scratch
m = UdmfMap(namespace="zdoom")
m.things.append(UdmfThing(x=0.0, y=0.0, angle=90, type=1))
m.vertices.extend([
    UdmfVertex(x=0.0, y=0.0), UdmfVertex(x=512.0, y=0.0),
    UdmfVertex(x=512.0, y=512.0), UdmfVertex(x=0.0, y=512.0),
])
m.linedefs.extend([
    UdmfLinedef(v1=0, v2=1, sidefront=0),
    UdmfLinedef(v1=1, v2=2, sidefront=0),
    UdmfLinedef(v1=2, v2=3, sidefront=0),
    UdmfLinedef(v1=3, v2=0, sidefront=0),
])
m.sidedefs.append(UdmfSidedef(sector=0, texturemiddle="STARTAN2"))
m.sectors.append(UdmfSector(
    heightfloor=0, heightceiling=128,
    texturefloor="FLAT1", textureceiling="CEIL3_5", lightlevel=192,
))

# Serialize and write to a WAD
textmap_output = serialize_udmf(m)

from wadlib import WadWriter
from wadlib.enums import WadType

writer = WadWriter(WadType.PWAD)
writer.add_lump("MAP01", b"")
writer.add_lump("TEXTMAP", textmap_output.encode("utf-8"))
writer.add_lump("ENDMAP", b"")
writer.save("udmf_map.wad")

Rendering Maps

MapRenderer produces a PIL Image from any parsed map entry. RenderOptions controls scale, visibility, and output format.

from wadlib import WadFile
from wadlib.renderer import MapRenderer, RenderOptions

with WadFile("DOOM2.WAD") as wad:
    m = wad.maps[0]  # MAP01

    # Minimal render: dark background, thing markers, no floors
    r = MapRenderer(m, wad=wad)
    r.render()
    r.save("map01.png")

    # Full-featured render
    opts = RenderOptions(
        show_floors=True,     # fill subsectors with the sector's floor flat
        show_sprites=True,    # draw actual WAD sprites at thing positions
        alpha=True,           # RGBA output; void is transparent instead of dark
        multiplayer=False,    # exclude MP-only things (default)
        scale=0.0,            # 0 = auto-fit to 4096 px on the longer axis
        thing_scale=1.0,      # multiply the base thing-marker radius
        palette_index=0,      # PLAYPAL index (0 = standard game palette)
    )
    r = MapRenderer(m, wad=wad, options=opts)
    img = r.render()     # returns PIL Image; also stored in r.image
    r.save("map01_full.png")
    r.show()             # display in the system image viewer

Thing markers use category colours:

Category Colour Shape
Player Cyan Direction arrow
Monster Red Direction arrow
Weapon Yellow Dot
Ammo Orange Dot
Health Green Dot
Armor Blue Dot
Key Magenta Dot
Powerup White Dot
Decoration Dark grey Dot
Unknown Very dark grey Dot

Linedefs use automap-style colours:

Linedef type Colour
Solid (impassable) wall White
Passable floor/step change Yellow
Passable ceiling-only change Light grey
Secret sector boundary Magenta
Door / trigger special Cyan
Plain two-sided Grey

Game Type Detection

Automatically identify which game a WAD targets and look up thing type names and categories.

from wadlib import WadFile
from wadlib.types import detect_game, get_category, get_name, ThingCategory

with WadFile("HERETIC.WAD") as wad:
    game = detect_game(wad)
    print(f"Detected: {game.value}")             # "heretic"

    name = get_name(1, game)                     # "Player 1 Start"
    cat = get_category(3004, game)               # ThingCategory.MONSTER

    # Categorise all things in the first map
    for thing in wad.maps[0].things:
        print(f"  {thing.type}: {get_name(thing.type, game)} "
              f"[{get_category(thing.type, game).name}]")

# Works with DEHACKED custom types too
with WadFile.open("DOOM2.WAD", "rekkr.wad") as wad:
    game = detect_game(wad)
    deh = wad.dehacked.custom_things if wad.dehacked else None

    for thing in wad.maps[0].things:
        print(f"  {thing.type}: {get_name(thing.type, game, deh=deh)} "
              f"({get_category(thing.type, game, deh=deh).name})")

Decoding Boom Generalized Linedefs

Boom-compatible WADs use special_type values >= 0x2F80 to encode floor/ceiling/door/lift effects in bitfields rather than a flat lookup table.

from wadlib import WadFile

with WadFile("boom_mod.wad") as wad:
    for m in wad.maps:
        for line in m.lines:
            gen = line.generalized       # GeneralizedLinedef | None
            if gen:
                print(f"linedef {line.special_type:#06x}: "
                      f"{gen.category.name} / {gen.trigger.name} "
                      f"/ speed={gen.speed.name}")

# Decode manually
from wadlib.lumps.boom import decode_generalized, DOOM_SECTOR_SPECIALS

gen = decode_generalized(0x6003)
print(gen.category)  # GeneralizedCategory.FLOOR
print(gen.trigger)   # GeneralizedTrigger.SR
print(gen.speed)     # GeneralizedSpeed.SLOW

# Human-readable sector special
with WadFile("DOOM2.WAD") as wad:
    for sector in wad.maps[0].sectors:
        if sector.special:
            print(f"sector {sector.special}: {sector.special_name}")