Occasionally, I like to fire up gzDoom and play through some of the old Doom and Doom II Games and megawads. I use a Random Level generator, Obhack, which I also hacked further to increase enemy and ammo. However, one alteration I like to make is to have higher Ammunition limits. As it happens, the way I had it set up, this information was in a DEHacked patch file within the WAD. As a result, to make changes, I had to use a tool called “Doom Wad Editor”.
Doom WAD Editor, or DWE for short, is about the most up to date tool I could find, and it is rather messy internally. It performs a lot of up-front processing to load the file and show previews and it doesn’t support a lot of modern capabilities. I recently came to a realization that the WAD Format is not some major secret- I could create my own tool.
So far, I’ve been able to construct the Format handler that is able to open and save the internal LUMP files. I’ll likely expand things to also use the KGROUP format (which is used by sole Build Engine games like Duke Nukem 3D) and create a Modern Application for current Windows versions for modifying those older file formats.
The WAD File Format
The WAD (For “Where’s All the Data?”) Format is a format used for Doom and Doom II as well as games using the same engine as well as modern source ports for those games to store game data; this includes maps, levels, textures, sprites, sounds, etc.
The Format itself is rather straightforward. As with most files, we have a Header. At the very start of the file, we find the characters IWAD or PWAD. These characters determine the “type” of the WAD file; a PWAD is a “Patch” Wad, which means it patches another WAD file’s data by replacing it’s contents. For example, a mod that changes all the sounds to be silly animal noises would be a PWAD which uses the same names for different data. an IWAD can be thought of as an “Initial” WAD. These are the “core” WAD files that are needed to play the games in question. The Header data is followed by a signed 32-bit integer indicating the number of Lumps in the file. (A Lump being effectively a piece of data). After that, is another 32-bit integer which is a file offset, from the beginning of the file, where the Lump Directory begins. The Lump Directory is a sequence of Lump Positions in the file, their size, and their 8-character name.
This is all, so far, relatively straightforward. So let’s get to it!. Now, this is just a code example of the basic implementation- my plan going forward with this tool is to flesh it out into a WPF Application that provides full editing and manipulation capabilities to WAD files. There is still an active Doom community creating Megawads and it may prove useful to somebody, and it’s unique enough that creating such an application should be interesting. I’ve been able to load and then resave the standard DOOM.WAD and have the newly saved version function correctly, so it would seem I did something correctly so far:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
public class WADFile { public struct LumpDirectoryHeader { public Int32 LumpPos; public Int32 LumpSize; public char[] LumpName; public override String ToString() { return new String(LumpName) + " : Pos:" + LumpPos + ", Size:" + LumpSize; } } private char[] Header; private Int32 LumpCount; private Int32 DirPointer; private List<WADLump> _Lumps = null; private String sLoadedFile = null; public IList<WADLump> Lumps { get { return new List<WADLump>(_Lumps); } } public WADFile(String sWADFile) { sLoadedFile = sWADFile; using (BinaryReader br = new BinaryReader(new FileStream(sWADFile, FileMode.Open))) { Header = br.ReadChars(4); LumpCount = br.ReadInt32(); DirPointer = br.ReadInt32(); br.BaseStream.Seek(DirPointer, SeekOrigin.Begin); LumpDirectoryHeader[] LumpHeaders = ReadLumpDirectory(LumpCount, br); _Lumps = InitLumps(LumpHeaders).ToList(); foreach (var iterate in _Lumps) { iterate.LumpData = ReadData(iterate.Header, br.BaseStream); } } } public void Save(String Target) { using (FileStream fs = new FileStream(Target, FileMode.Create)) { Save(fs); } } //Save needs to write the headers- The Lump Directory header written at this point a placeholder. //It then writes out each lump. During this process, the header information is updated with the current seek position within the file before it is saved. //once all the lumps are written, write the Lump Directory- (Position, Size, Name). (The position having been updated during the previous step). //It would then seek back to the position at in the file header, and write the starting location of the lump directory. //And the save is completed. public void Save(Stream Target) { LumpCount = _Lumps.Count; using (BinaryWriter bw = new BinaryWriter(Target)) { long LumpDirectorySavePos = 0; long LumpDirectoryPos = 0; //write the header. bw.Write(Header); bw.Write(LumpCount); //store this location. LumpDirectorySavePos = bw.BaseStream.Position; //and write a placeholder. bw.Write((int) 0); foreach (var lump in _Lumps) { //update Lump Position based on the seek information. LumpDirectoryHeader ldh = new LumpDirectoryHeader() {LumpName = lump.Header.LumpName, LumpPos = (int) bw.BaseStream.Position, LumpSize = lump.Header.LumpSize}; lump.Header = ldh; //write the bytes from this lump. bw.Write(lump.LumpData); } //all lumps are written. Write out the Lump Directory itself. LumpDirectoryPos = bw.BaseStream.Position; foreach (var lump in _Lumps) { bw.Write(lump.Header.LumpPos); bw.Write(lump.Header.LumpSize); bw.Write(lump.Header.LumpName); } bw.Seek((int) LumpDirectorySavePos, SeekOrigin.Begin); bw.Write((int) LumpDirectoryPos); } } public WADFile() { _Lumps = new List<WADLump>(); } private IEnumerable<WADLump> InitLumps(IEnumerable<LumpDirectoryHeader> Headers) { foreach (var iterate in Headers) { WADLump wl = new WADLump(iterate, null, (y) => ReadData(y)); yield return wl; } } private byte[] ReadData(LumpDirectoryHeader ldh) { if (File.Exists(sLoadedFile)) { using (FileStream fs = new FileStream(sLoadedFile, FileMode.Open)) { return ReadData(ldh, fs); } } return null; } private byte[] ReadData(LumpDirectoryHeader ldh, Stream usestream) { BinaryReader br = new BinaryReader(usestream); br.BaseStream.Seek(ldh.LumpPos, SeekOrigin.Begin); Byte[] resultdata = br.ReadBytes(ldh.LumpSize); return resultdata; return null; } private LumpDirectoryHeader[] ReadLumpDirectory(int LumpCount, BinaryReader br) { LumpDirectoryHeader[] BuildHeader = new LumpDirectoryHeader[LumpCount]; //read LumpCount LumpDirectory Headers. for (int i = 0; i < LumpCount; i++) { BuildHeader[i].LumpPos = br.ReadInt32(); BuildHeader[i].LumpSize = br.ReadInt32(); //now read 8 chars. char[] readchars; readchars = br.ReadChars(8); BuildHeader[i].LumpName = readchars; } return BuildHeader; } } public class WADLump { //represents a single WAD Lump. /*0x00 4 filepos An integer holding a pointer to the start of the lump's data in the file. 0x04 4 size An integer representing the size of the lump in bytes. 0x08 8 name An ASCII string defining the lump's name. Only the characters A-Z (uppercase), 0-9, and [ ] - _ should be used in lump names (an exception has to be made for some of the Arch-Vile sprites, which use "\"). When a string is less than 8 bytes long, it should be null-padded to the eighth byte. */ public WADFile.LumpDirectoryHeader Header { get; set; } private Func<WADFile.LumpDirectoryHeader, byte[]> ReadDataCallback = null; private byte[] _LumpData = null; public byte[] LumpData { get { return _LumpData ?? (_LumpData = ReadDataCallback(Header)); } set { _LumpData = value; } } public WADLump(WADFile.LumpDirectoryHeader pHeader, byte[] pLumpData = null, Func<WADFile.LumpDirectoryHeader, byte[]> pReadDataCallback = null) { Header = pHeader; _LumpData = pLumpData; ReadDataCallback = pReadDataCallback; } public override String ToString() { return "LUMP:" + Header.ToString() + " LumpData:" + LumpData.Length + " Bytes."; } } |
Have something to say about this post? Comment!