UltimateZoneBuilder/Source/Core/IO/WAD.cs
2024-02-10 18:44:52 +01:00

636 lines
18 KiB
C#
Executable file

#region ================== Copyright (c) 2007 Pascal vd Heiden
/*
* Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
* This program is released under GNU General Public License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/
#endregion
#region ================== Namespaces
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Security.Cryptography;
#endregion
namespace CodeImp.DoomBuilder.IO
{
public class WAD : IDisposable
{
#region ================== Constants
// WAD types
private const string TYPE_IWAD = "IWAD";
private const string TYPE_PWAD = "PWAD";
// Encoder
public static readonly Encoding ENCODING = Encoding.ASCII;
//mxd. Official IWAD SHA1 hashes
// Source of hashes: https://github.com/Doom-Utils/iwad-patches
private static readonly HashSet<string> IWAD_HASHES = new HashSet<string>
{
// Doom 1
"df0040ccb29cc1622e74ceb3b7793a2304cca2c8", // Doom 1.1
"b5f86a559642a2b3bdfb8a75e91c8da97f057fe6", // Doom 1.2
"2e89b86859acd9fc1e552f587b710751efcffa8e", // Doom 1.666
"2c8212631b37f21ad06d18b5638c733a75e179ff", // Doom 1.8
"7742089b4468a736cadb659a7deca3320fe6dcbd", // Doom 1.9
"e5ec79505530e151ff0e6f517f3ce1fd65969c46", // Doom Bfg
"a89b39d91122882214c3088b8cd6b308713bd7c2", // Doom Ppc
"117015379c529573510be08cf59810aa10bb934e", // Doom Psn
"f770111ca9eb6d49aead51fcbd398719b462e64b", // Doom Unity 1.0
"08ab2507e1d525c4c06b6df4f6d5862568a6b009", // Doom Unity 1.1
"2a8a1ce0f29497a2781b2902c76115fd60d8bbf8", // Doom Unity 1.3
"9b07b02ab3c275a6a7570c3f73cc20d63a0e3833", // Doom Ud
"37de4510216eb3ce9a835dd939109443375d10c5", // Doom Xbla
"1d1d4f69fe14fa255228d8243470678b1b4efdc5", // Doom Xbox
// Doom 2
"a4ce5128d57cb129fdd1441c12b58245be55c8ce", // Doom2 1.666g
"6d559b7ceece4f5ad457415049711992370d520a", // Doom2 1.666
"70192b8d5aba65c7e633a7c7bcfe7e3e90640c97", // Doom2 1.7a
"78009057420b792eacff482021db6fe13b370dcc", // Doom2 1.7
"79c283b18e61b9a989cfd3e0f19a42ea98fda551", // Doom2 1.8
"d510c877031bbd5f3d198581a2c8651e09b9861f", // Doom2 1.8f
"7ec7652fcfce8ddc6e801839291f0e28ef1d5ae7", // Doom2 1.9
"a59548125f59f6aa1a41c22f615557d3dd2e85a9", // Doom2 Bfg
"f1b6ba94352d53f646b67c01d2da88c5c40e3179", // Doom2 Psn Eur
"ca8db908a7c9fbac764f34c148f0bcc78d18553e", // Doom2 Psn Usa
"9b39107b5bcfd1f989bcfe46f68dbc1f49222922", // Doom2 Unity 1.0
"b723882122e90b61a1d92a11dcfcf9cbf95a407e", // Doom2 Unity 1.1
"9574851209c9dfbede56db0dee0660ecd51e6150", // Doom2 Unity 1.3
"55e445badd63d8841ebea887910c26c62c7f525e", // Doom2 Xbla
"1c91d86cd8a2f3817227986503a6672a5e1613f0", // Doom2 Xbox
"b7ba1c68631023ea1aab1d7b9f7f6e9afc508f39", // Doom2 Xbox360bfg
"2cda310805397ae44059bbcaed3cd602f4864a82", // Doom2 Zodiac
// Plutonia
"90361e2a538d2388506657252ae41aceeb1ba360", // Plutonia 1.9
"f131cbe1946d7fddb3caec4aa258c83399c21e60", // Plutonia Anthology
"85c3517434135a5886111b324955f9288c01046c", // Plutonia Psn Eur
"327f8c41ebd4138354e9fca63cebbbd1b9489749", // Plutonia Psn Usa
"54e27b5791fbc5677bf7e83c1de3a92ea3ef935b", // Plutonia Unity 1.0
"20fd23ee410c466b263a741bbd53bbef573ab47d", // Plutonia Unity 1.3
// TNT
"9fbc66aedef7fe3bae0986cdb9323d2b8db4c9d3", // Tnt 1.9
"4a65c8b960225505187c36040b41a40b152f8f3e", // Tnt Anthology
"5066833da047117241cdda05a708b009eb266c91", // Tnt Psn Eur
"139e26d801a64b404b8d898defca10227a61867b", // Tnt Psn Usa
"503271390606ebded04a2cfaa1a4e249c0313a9d", // Tnt Unity 1.0
"ca0f0495a6c2813b49620202774c56560d6d7621", // Tnt Unity 1.3
// Heretic
"b5a6cc79cde48d97905b44282e82c4c966a23a87", // Heretic 1.0
"a54c5d30629976a649119c5ce8babae2ddfb1a60", // Heretic 1.2
"f489d479371df32f6d280a0cb23b59a35ba2b833", // Heretic 1.3
// Hexen
"ae797f5fdce845be24a7a24dd5bfc3e762a17bbe", // Hexen Beta
"ac129c4331bf26f0f080c4a56aaa40d64969c98a", // Hexen 1.0
"4b53832f0733c1e29e5f1de2428e5475e891af29", // Hexen 1.1
"4343fbe5aef905ef6d077a1517a50c919e5cc906", // Hexen Mac
// Strife
"eb0f3e157b35c34d5a598701f775e789ec85b4ae", // Strife1 1.1
"64c13b951a845ca7f8081f68138a6181557458d1", // Strife1 1.2
};
#endregion
#region ================== Structs (mxd)
private struct LumpCopyData
{
public byte[] Data;
public byte[] FixedName;
public int Index;
}
#endregion
#region ================== Variables
// File objects
private string filename;
private FileStream file;
private BinaryReader reader;
private BinaryWriter writer;
// Header
private int numlumps;
private int lumpsoffset;
private bool isiwad; //mxd
private bool isofficialiwad; //mxd
// Lumps
private List<Lump> lumps;
// Status
private bool isreadonly;
private bool isdisposed;
#endregion
#region ================== Properties
public string Filename { get { return filename; } }
public Encoding Encoding { get { return ENCODING; } }
public bool IsReadOnly { get { return isreadonly; } }
public bool IsDisposed { get { return isdisposed; } }
public bool IsIWAD { get { return isiwad; } set { isiwad = value; } } //mxd
public bool IsOfficialIWAD { get { return isofficialiwad; } } //mxd
public List<Lump> Lumps { get { return lumps; } }
#endregion
#region ================== Constructor / Disposer
// Constructor to open or create a WAD file
public WAD(string pathfilename)
{
// Initialize
this.isreadonly = false;
this.Open(pathfilename);
}
// Constructor to open or create a WAD file
public WAD(string pathfilename, bool openreadonly)
{
// Initialize
this.isreadonly = openreadonly;
this.Open(pathfilename);
}
// Destructor
/*~WAD()
{
// Make sure everything is disposed
this.Dispose();
}*/
// Disposer
public void Dispose()
{
// Not already disposed?
if(!isdisposed)
{
// Only possible when not read-only
if(!isreadonly)
{
// Flush writing changes
if(writer != null) writer.Flush();
if(file != null) file.Flush();
}
// Clean up
if(lumps != null) foreach(Lump l in lumps) l.Dispose();
if(writer != null) writer.Close();
if(reader != null) reader.Close();
if(file != null) file.Dispose();
// Done
isdisposed = true;
GC.SuppressFinalize(this); //mxd
}
}
#endregion
#region ================== IO
// Open a WAD file
private void Open(string pathfilename)
{
FileAccess access;
FileShare share;
// Keep filename
filename = pathfilename;
//mxd
CheckHash();
// Determine if opening for read only
if(isreadonly)
{
// Read only
access = FileAccess.Read;
share = FileShare.ReadWrite;
}
else
{
// Private access
access = FileAccess.ReadWrite;
share = FileShare.Read;
}
// Open the file stream
file = File.Open(pathfilename, FileMode.OpenOrCreate, access, share);
// Create file handling tools
reader = new BinaryReader(file, ENCODING);
if(!isreadonly) writer = new BinaryWriter(file, ENCODING);
// Is the WAD file zero length?
if(file.Length < 4)
{
// Create the headers in file
CreateHeaders();
}
else
{
// Read information from file
ReadHeaders();
}
}
// This creates new file headers
private void CreateHeaders()
{
// Default settings
isiwad = false; //mxd
isofficialiwad = false; //mxd
lumpsoffset = 12;
// New lumps array
lumps = new List<Lump>(numlumps);
// Write the headers
if(!isreadonly) WriteHeaders();
}
// This reads the WAD header and lumps table
private void ReadHeaders()
{
// Make sure the write is finished writing
if(!isreadonly) writer.Flush();
// Seek to beginning
file.Seek(0, SeekOrigin.Begin);
// Read WAD type
isiwad = (ENCODING.GetString(reader.ReadBytes(4)) == TYPE_IWAD); //mxd
// Number of lumps
numlumps = reader.ReadInt32();
if(numlumps < 0) throw new IOException("Invalid number of lumps in wad file.");
// Lumps table offset
lumpsoffset = reader.ReadInt32();
if(lumpsoffset < 0) throw new IOException("Invalid lumps offset in wad file.");
// Seek to the lumps table
file.Seek(lumpsoffset, SeekOrigin.Begin);
// Dispose old lumps and create new list
if(lumps != null) foreach(Lump l in lumps) l.Dispose();
lumps = new List<Lump>(numlumps);
// Go for all lumps
for(int i = 0; i < numlumps; i++)
{
// Read lump information
int offset = reader.ReadInt32();
int length = reader.ReadInt32();
byte[] fixedname = reader.ReadBytes(8);
// Create the lump
lumps.Add(new Lump(file, this, fixedname, offset, length));
}
}
// This writes the WAD header and lumps table
public void WriteHeaders()
{
// [ZZ] don't allow any edit actions on readonly archive
if (isreadonly) return;
// Seek to beginning
file.Seek(0, SeekOrigin.Begin);
// Write WAD type
writer.Write(ENCODING.GetBytes(isiwad ? TYPE_IWAD : TYPE_PWAD));
// Number of lumps
writer.Write(numlumps);
// Lumps table offset
writer.Write(lumpsoffset);
// Seek to the lumps table
file.Seek(lumpsoffset, SeekOrigin.Begin);
// Go for all lumps
for(int i = 0; i < lumps.Count; i++)
{
// Write lump information
writer.Write(lumps[i].Offset);
writer.Write(lumps[i].Length);
writer.Write(lumps[i].FixedName);
}
}
//mxd
private void CheckHash()
{
// Open the file stream
FileStream fs = File.Open(filename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite);
// Empty file can't be official iwad
if(fs.Length > 4)
{
BinaryReader r = new BinaryReader(fs, ENCODING);
// Read WAD type
if(ENCODING.GetString(r.ReadBytes(4)) == TYPE_IWAD)
{
// Rewind
r.BaseStream.Position = 0;
//isofficialiwad = IWAD_HASHES.Contains(MD5Hash.Get(r.BaseStream));
isofficialiwad = IWAD_HASHES.Contains(Hasher<SHA1>.Get(r.BaseStream));
if (!isreadonly && isofficialiwad) isreadonly = true;
}
// Close the reader
r.Close();
}
else
{
// Close the file
fs.Dispose();
}
}
//mxd. This rebuilds the WAD file, removing all "dead" entries
// Tech info: WAD.Remove() doesn't remove lump data, so MapManager.TemporaryMapFile slowly gets bigger
// with every map save/test, which leads to lumpsoffset overflowing when TemporaryMapFile size reaches
// int.MaxValue bytes in size (that's ~2Gb).
internal void Compress()
{
// No can't do...
if(isreadonly) return;
// Gather existing data
int totaldatalength = 0;
List<LumpCopyData> copydata = new List<LumpCopyData>(lumps.Count);
for(int i = 0; i < lumps.Count; i++)
{
// Copy lump...
LumpCopyData lcd = new LumpCopyData();
Lump l = lumps[i];
lcd.FixedName = l.FixedName;
lcd.Index = i;
lcd.Data = l.Stream.ReadAllBytes();
// Store data
copydata.Add(lcd);
// Track total length
totaldatalength += l.Length;
// Dispose lump
l.Dispose();
}
// Compression required?
if(totaldatalength >= lumpsoffset + 12) return;
// Set new file length
file.SetLength(totaldatalength + lumps.Count * 16);
// Reset lumpsoffset
lumpsoffset = 12;
// Reset lumps collection
lumps = new List<Lump>(copydata.Count);
numlumps = copydata.Count;
// Recreate all lumps
foreach(LumpCopyData lcd in copydata)
{
Lump l = new Lump(file, this, lcd.FixedName, lumpsoffset, lcd.Data.Length);
l.Stream.Write(lcd.Data, 0, lcd.Data.Length);
l.Stream.Seek(0, SeekOrigin.Begin);
lumps.Insert(lcd.Index, l);
lumpsoffset += lcd.Data.Length;
}
// Write new headers
WriteHeaders();
}
// This flushes writing changes
/*public void Flush()
{
// Only possible when not read-only
if(!isreadonly)
{
// Flush writing changes
if(writer != null) writer.Flush();
if(file != null) file.Flush();
}
}*/
#endregion
#region ================== Lumps
// This creates a new lump in the WAD file
public Lump Insert(string name, int position, int datalength, bool writeheaders = true)
{
// [ZZ] don't allow any edit actions on readonly archive
if (isreadonly) return null;
// We will be adding a lump
numlumps++;
// Extend the file
file.SetLength(file.Length + datalength + 16);
// Create the lump
Lump lump = new Lump(file, this, Lump.MakeFixedName(name, ENCODING), lumpsoffset, datalength);
lumps.Insert(position, lump);
// Advance lumps table offset
lumpsoffset += datalength;
// Write the new headers
if(writeheaders) WriteHeaders();
// Return the new lump
return lump;
}
// This removes a lump from the WAD file by index
public void RemoveAt(int index, bool writeheaders = true)
{
// [ZZ] don't allow any edit actions on readonly archive
if (isreadonly) return;
// Remove from list
Lump l = lumps[index];
lumps.RemoveAt(index);
l.Dispose();
numlumps--;
// Write the new headers
if (writeheaders) WriteHeaders();
}
// This removes a lump from the WAD file
public void Remove(Lump lump)
{
// [ZZ] don't allow any edit actions on readonly archive
if (isreadonly) return;
// Remove from list
lumps.Remove(lump);
lump.Dispose();
numlumps--;
// Write the new headers
WriteHeaders();
}
// This finds a lump by name, returns null when not found
public Lump FindLump(string name)
{
int index = FindLumpIndex(name);
return (index == -1 ? null : lumps[index]);
}
// This finds a lump by name, returns null when not found
public Lump FindLump(string name, int start)
{
int index = FindLumpIndex(name, start);
return (index == -1 ? null : lumps[index]);
}
// This finds a lump by name, returns null when not found
public Lump FindLump(string name, int start, int end)
{
int index = FindLumpIndex(name, start, end);
return (index == -1 ? null : lumps[index]);
}
// This finds a lump by name, returns -1 when not found
public int FindLumpIndex(string name)
{
// Do search
return FindLumpIndex(name, 0, lumps.Count - 1);
}
// This finds a lump by name, returns -1 when not found
public int FindLumpIndex(string name, int start)
{
// Do search
return FindLumpIndex(name, start, lumps.Count - 1);
}
// This finds a lump by name, returns -1 when not found
public int FindLumpIndex(string name, int start, int end)
{
if(name.Length > 8 || lumps.Count == 0 || start > lumps.Count - 1) return -1; //mxd. Can't be here. Go away!
long longname = Lump.MakeLongName(name);
// Fix start/end when they exceed safe bounds
start = Math.Max(start, 0);
end = General.Clamp(end, 0, lumps.Count - 1);
// Loop through the lumps
for(int i = start; i < end + 1; i++)
{
// Check if the lump name matches
if(lumps[i].LongName == longname)
{
// Found the lump!
return i;
}
}
// Nothing found
return -1;
}
//mxd. Same as above, but searches in reversed order
// This finds a lump by name, returns null when not found
public Lump FindLastLump(string name)
{
int index = FindLastLumpIndex(name);
return (index == -1 ? null : lumps[index]);
}
// This finds a lump by name, returns null when not found
public Lump FindLastLump(string name, int start)
{
int index = FindLastLumpIndex(name, start);
return (index == -1 ? null : lumps[index]);
}
// This finds a lump by name, returns null when not found
public Lump FindLastLump(string name, int start, int end)
{
int index = FindLastLumpIndex(name, start, end);
return (index == -1 ? null : lumps[index]);
}
// This finds a lump by name, returns -1 when not found
public int FindLastLumpIndex(string name)
{
// Do search
return FindLastLumpIndex(name, 0, lumps.Count - 1);
}
// This finds a lump by name, returns -1 when not found
public int FindLastLumpIndex(string name, int start)
{
// Do search
return FindLastLumpIndex(name, start, lumps.Count - 1);
}
// This finds a lump by name, returns -1 when not found
public int FindLastLumpIndex(string name, int start, int end)
{
if(name.Length > 8 || lumps.Count == 0 || start > lumps.Count - 1) return -1; //mxd. Can't be here. Go away!
long longname = Lump.MakeLongName(name);
// Fix start/end when they exceed safe bounds
start = Math.Max(start, 0);
end = General.Clamp(end, 0, lumps.Count - 1);
// Loop through the lumps in backwards order
for(int i = end; i > start - 1; i--)
{
// Check if the lump name matches
if(lumps[i].LongName == longname)
{
// Found the lump!
return i;
}
}
// Nothing found
return -1;
}
#endregion
}
}