Requires Java 1.8 or better.
 

Latest Release:

 

Introduction

Doom Struct is a Java library that does a bunch of Doom Engine game data reading, manipulating, and writing.

Why?

There are several libraries out there for reading data from Doom in many different programming languages, but there isn't a decent one for Java. And by decent, I mean:

  • Useful
  • Documented
  • Performant
  • Efficient

The goal with this library is to get end-users up-and-running quickly with little boilerplate code, and to ensure clarity within documentation and with written code, and future-proofing to a reasonable extent.

What kind of features does it have?

  • Reads/edits WAD files.
  • Reads PK3 files (zips).
  • Full UDMF parsing/writing support.
  • Reads/edits all Doom data structures in Doom, Hexen/ZDoom, or Strife formats. This includes textures, patches, lines, vertices, things, sectors, nodes, palettes, colormaps, text, PNG data, music data, sound data, flats, blockmaps, reject, and even ENDOOM-type VGA lumps.
  • Supports PNGs with offset data (grAb).
  • Reads/edits Boom-engine data lumps like ANIMATED and SWITCHES.
  • Contains a utility class for converting Doom graphics to standard Java graphics structures, and vice-versa.
  • Contains a utility class for converting Doom DMX sound data to other formats via the Java SPI, and vice-versa.
  • Contains a utility class for managing texture sets without needing to care too much about Doom's nightmarish texture data setup.

EXAMPLES

Open a WAD file (and close it).

WadFile wad = new WadFile("doom2.wad");
wad.close();

Open DOOM2.WAD and read MAP01 into a Doom Map.

WadFile wad = new WadFile("doom2.wad");
DoomMap map = MapUtils.createDoomMap(wad, "map01");
wad.close();

Open DOOM2.WAD and fetch all sectors in MAP29 with the BLOOD1 floor texture.

WadFile wad = new WadFile("doom2.wad");
Set<DoomSector> set = wad.getDataAsList("sectors", "map29", DoomSector.class, DoomSector.LENGTH).stream()
    .filter((sector) -> sector.getFloorTexture().equals("BLOOD1"))
    .collect(Collectors.toSet());
wad.close();

Open DOOM.WAD and fetch all things in E1M1 that appear in multiplayer.

WadFile wad = new WadFile("doom.wad");
Set<DoomThing> set = wad.getDataAsList("things", "e1m1", DoomThing.class, DoomThing.LENGTH).stream()
    .filter((thing) -> thing.isFlagSet(DoomThingFlags.NOT_SINGLEPLAYER))
    .collect(Collectors.toSet());
wad.close();

Open HEXEN.WAD and fetch all things in MAP01 that have a special.

WadFile wad = new WadFile("hexen.wad");
Set<HexenThing> set = wad.getDataAsList("things", "map01", HexenThing.class, HexenThing.LENGTH).stream()
    .filter((thing) -> thing.getSpecial() > 0)
    .collect(Collectors.toSet());
wad.close();

Open DOOM.WAD and fetch all maps that have less than 1000 linedefs.

final WadFile wad = new WadFile("doom.wad");
Set<String> set = Arrays.asList(MapUtils.getAllMapHeaders(wad)).stream()
    .filter((header) -> wad.getEntry("linedefs", header).getSize() / DoomLinedef.LENGTH < 1000)
    .collect(Collectors.toSet());
wad.close();

Open SQUARE1.PK3, fetch maps/E1A1.WAD and read it into a UDMF Map.

DoomPK3 pk3 = new DoomPK3("square1.pk3");
WadBuffer wad = pk3.getDataAsWadBuffer("maps/e1a1.wad");
UDMFMap map = wad.getTextDataAs("textmap", Charset.forName("UTF-8"), UDMFMap.class);
pk3.close();

Open DOOM2.WAD, read DEMO2 and figure out how many tics that player 1 was pushing "fire".

WadFile wad = new WadFile("doom2.wad");
Demo demo = wad.getDataAs("demo2", Demo.class);
int tics = 0;
for (int i = 0; i < demo.getTicCount(); i++) {
    tics += (demo.getTic(i).getAction() & Demo.Tic.ACTION_FIRE) != 0 ? 1 : 0;
}
wad.close();

Open DOOM.WAD and get MD5 hashes of each entry.

WadFile wad = new WadFile("doom.wad");
byte[][] hashes = new byte[wad.getEntryCount()][];
int i = 0;
for (WadEntry entry : wad)
    hashes[i++] = MessageDigest.getInstance("MD5").digest(wad.getData(entry));
wad.close();

Open DOOM2.WAD, fetch all TROO* (graphic) entries and export them as PNGs.

WadFile wad = new WadFile("doom2.wad");
final Palette pal = wad.getDataAs("playpal", Palette.class);
for (WadEntry entry : wad) {
    if (entry.getName().startsWith("TROO")) {
        Picture p = wad.getDataAs(entry, Picture.class);
        ImageIO.write(GraphicUtils.createImage(p, pal), "PNG", new File(entry.getName()+".png"));
    }
}
wad.close();

Open DOOM.WAD, fetch all DS* (audio) entries, upsample them to 22kHz (cosine interpolation), and export them as WAVs.

WadFile wad = new WadFile("doom.wad");
for (WadEntry entry : wad) {
    if (entry.getName().startsWith("DS")) {
        DMXSound sound = wad.getDataAs(entry, DMXSound.class)
            .resample(InterpolationType.COSINE, DMXSound.SAMPLERATE_22KHZ);
        SoundUtils.writeSoundToFile(sound, AudioFileFormat.Type.WAVE, new File(entry.getName() + ".wav"));
    }
}
wad.close();

Open DOOM2.WAD, assemble a TextureSet, remove every texture that begins with R, and write it to a new WAD.

WadFile wad = new WadFile("doom2.wad");
TextureSet textureSet = new TextureSet(
    wad.getDataAs("pnames", PatchNames.class),
    wad.getDataAs("texture1", DoomTextureList.class)
);
wad.close();

Iterator<TextureSet.Texture> it = textureSet.iterator();
while (it.hasNext()) {
    TextureSet.Texture t = it.next();
    if (t.getName().startsWith("R")) {
        it.remove();
    }
}
PatchNames pout = new PatchNames();
DoomTextureList tout = new DoomTextureList();
textureSet.export(pout, tout);

WadFile newwad = WadFile.createWadFile("new.wad");
newwad.addData("PNAMES", pout);
newwad.addData("TEXTURE1", tout);
newwad.close();

Open DOOM.WAD, get the first map of each episode and write it to a new WAD called OUT.wad and close both (with one statement!).

WadUtils.openWadAndExtractTo("DOOM.WAD", "OUT.wad", (wad)->
    WadUtils.withEntries(MapUtils.getMapEntries(wad, "E1M1"))
        .and(MapUtils.getMapEntries(wad, "E2M1"))
        .and(MapUtils.getMapEntries(wad, "E3M1"))
        .and(MapUtils.getMapEntries(wad, "E4M1"))
    .get()
);

Open DOOM2.WAD, create a new Wad called TEXTURES.wad and copy over only a few textures and flats (and also-close them):

try (WadFile source = new WadFile("DOOM2.WAD")) {
    try (WadFile target = WadFile.createWadFile("TEXTURES.wad")) {
        try (TextureCopier copier = TextureUtils.createTextureCopier(source, target)) {
            copier.copyFlat("FLOOR7_1");
            copier.copyFlat("CEIL5_2");
            copier.copyTexture("AASHITTY"); // first texture is "null" texture
            copier.copyTexture("SUPPORT3");
            copier.copyTexture("SUPPORT2");
            copier.copyTexture("BIGDOOR1");
            copier.copyTexture("BIGDOOR2");
            copier.copyTexture("BIGDOOR3");
            copier.copyTexture("BIGDOOR4");
            copier.copyTexture("BIGDOOR5");
        }
    }
}
 

CHANGELOG