In a prior post, I described id's WAD format used by
classic games such as DOOM and how to read them. This post
covers how to write them. As with my first post, this only
covers the original WAD format, not the enhanced ones which
followed.
The Format
A brief recap on the format. There is a 12 byte header which
details the wad type, the number of lumps of data it contains,
and an offset where the directory index is located.
Range
Description
0 - 3
Either the string IWAD or PWAD
4 - 7
The number of entries in the directory
8 - 11
The location of the directory
The directory index is comprised of (16 * number of lumps) bytes
which describe the lumps. Each 16 byte header details the size,
the position in the data and the lump name.
The nature of the WAD file means that in theory that you should
be able to make changes to it without having to rewrite the
entire file. For example, adding a new lump of data could simply
be added to the end of the existing data, overwriting the existing
directory index, and then a new index appended. Replacing a lump
with data the same size or small could overwrite the existing
lump, and adjusting the directory index entry. Even when
removing a lump you could opt to leave the data behind (or zero it out!) and
simply remove the meta data from the directory index.
However, the simplest means (albeit most inefficient) is to
rewrite the whole WAD. At this point I am simply exploring the
format (and its subsequent iterations) so therefore I'm not
going to go out of my way to complicate things and so this
demonstration program will recreate the WAD each time it is
changed.
Creating a WAD From Scratch
The previous post introduced the WadReader class, a way of
quickly enumerating a WAD file. Here, I introduce the
WadOutputStream class as a counterpart. This class can be used
to easy create a WAD file by calling PutNextLump with the name
of the lump to add, then write the lump data via the usual
Stream methods. Once done, flushing or closing the stream will
automatically write the directory entry. I borrowed this pattern
from DotNetZip as I find it a convenient way of creating
Zip files. In fact, I'll likely refactor WadReader at some
point to act more like ZipInputStream as well.
When creating an instance of WadOutputStream, it will immediately
write a placeholder header to the stream. This will have the correct WAD type, but the count and directory position will all be defaults until filled in at the end.
As with the previous article, error handling, parameter
validation and non-essential code has been elided from these
snippets.
Although the WadOutputStream inherits from Stream, you can't
just randomly write to it. Prior to writing a lump, you need to
call the PutNextLump method. This method both finalises the
previous lump, if applicable, and prepares the new lump. This is
required so that the class can keep track of the lumps in order
to write the directory entry at the end.
Finally, when we're done writing, we need to finish off the WAD
by writing the directory index and updating the header. We'll do
this by overriding both Flush and Dispose.
When preparing for writing the directory, we again check to see
if there are any lumps and finalise the last one as we did when
adding a new lump.
Now we can finalise the file header. We do this by creating a
new 8 byte array and place the lump count in the first four
bytes, the current stream position into the latter four,
representing where the directory index will be written. We then
write these 8 bytes at the start of the stream, overwriting the
zero block written earlier.
To write an integer into the byte array I'm using a custom
PutInt32Le method. While the BitConverter class has a
GetBytes method, firstly this will result in repeated
allocations from byte array creation, and secondly I'd then have
to copy the contents in the destination array. Finally,
BitConverter will return the results based on the endian-ness
of the system and we need to ensure that little-endian is
used.
Once the WAD header is updated we enumerate each of our lumps
and build the 16 byte directory header containing the lump
offset, size and the padded name and then write those at the end
of the file.
Rewriting an existing WAD
This example, taken from the WadFile class, enumerates all
existing lumps and then writes them into a new stream. Although
not demonstrated here, it assumes the GetInputStream for a
given WadLump will return either the original data for
existing lumps, the modified data for existing lumps that have
been altered, or the data for new lumps.
As it can't write to the source stream whilst also reading from
it, it does all this to a temporary stream, and then, when done,
copies the contents of the temporary stream over the original
stream.
This isn't exactly the most efficient approach, but does avoid
all of the complexity of determining which parts of the file to
update, which parts to clear, keeping a list of changed items
for batch saving, etc. This is most likely something I will
investigate further in a future topic.
To Pad, Or Not To Pad
After I discovered that the data in the DOOM picture lumps were
padded, I was curious if the WAD file itself was. I copied
the hex viewer project from that solution and used it to
highlight the different ranges in a WAD. To my surprise, it
seemed that in even though picture lumps already had their own
padding, the lumps themselves were also padded to always have an
even number of bytes. Interestingly, sometimes if a lump started
on an even number two padding bytes were still included. I
suppose there is a reason but I didn't dig further info it and
so didn't build padding support into the writer classes.
Also possibly worthy of note, I checked the DARKWAR.WAD file and
this didn't use padding at all.
Does it work?
In a word, yes. I tested dumping DOOM.WAD into separate data
files using the waddemo program, then repacking them into a
brand new WAD. I then ran DOOM using the new wad and played
through the first level. Everything seemed to be running fine.
Getting the source code
As noted in the first article in this series, there isn't a
single download available per post as I've done a
larger-than-usual demonstration solution. The full project is
available from our GitHub page.
Like what you're reading? Perhaps you like to buy us a coffee?
The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game - but until that time comes, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.
In a prior post, I described id's WAD format used by
classic games such as DOOM and how to read them. This post
covers how to write them. As with my first post, this only
covers the original WAD format, not the enhanced ones which
followed.
In my previous post, I described id's WAD format used by
classic games such as DOOM and how to read them. While
researching the format though, I wasn't 100% sure that I was
extracting lumps properly - the only readable file I'd
discovered was `DMXGUS` in `DOOM1.WAD`, and also `LICENSE` in
`DARKWAR.WAD`... hardly conclusive.
Armed with the specification from the DOOM FAQ I decided
to take a brief segue into decoding the pictures to verify the
lumps I was extracting were valid.
WAD "Where's All the Data" files used by DOOM and various other
games are simple containers, similar to zip and other archive
formats, without additional complexity (such as compression) and
data-centric rather than file. This article describes how to
read the WAD files used by DOOM, DOOM II, Rise of the Triad and
similar games of that area.
The article covers reading of a WAD and extracting its contents