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.
csharp
publicclass WadOutputStream : Stream
{privatereadonly List<WadLump> _lumps;privatereadonly Stream _output;privatereadonlylong _start;privatebool _writtenDirectory;public WadOutputStream(Stream output, WadType type){
_output = output;
_start = output.Position;
_lumps =new List<WadLump>();this.WriteWadHeader(type);}privatevoid WriteWadHeader(WadType type){byte[] buffer;
buffer =newbyte[WadConstants.WadHeaderLength];
buffer[0]= type == WadType.Internal ?(byte)'I':(byte)'P';
buffer[1]=(byte)'W';
buffer[2]=(byte)'A';
buffer[3]=(byte)'D';// positions 4 - 11 left at zero for now
_output.Write(buffer,0, WadConstants.WadHeaderLength);}
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.
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.
An example of padding between lumps in DOOM.WAD, running under Mono on a 64bit Raspberry Pi
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