diff --git a/BurnOutSharp.Builders/WAD.cs b/BurnOutSharp.Builders/WAD.cs index 1a8234e0..7ee2fb5b 100644 --- a/BurnOutSharp.Builders/WAD.cs +++ b/BurnOutSharp.Builders/WAD.cs @@ -160,7 +160,7 @@ namespace BurnOutSharp.Builders lump.Padding0 = data.ReadByteValue(); lump.Padding1 = data.ReadByteValue(); byte[] name = data.ReadBytes(16); - lump.Name = Encoding.ASCII.GetString(name); + lump.Name = Encoding.ASCII.GetString(name).TrimEnd('\0'); return lump; } diff --git a/BurnOutSharp.Wrappers/WAD.cs b/BurnOutSharp.Wrappers/WAD.cs new file mode 100644 index 00000000..8126853b --- /dev/null +++ b/BurnOutSharp.Wrappers/WAD.cs @@ -0,0 +1,285 @@ +using System; +using System.IO; + +namespace BurnOutSharp.Wrappers +{ + public class WAD : WrapperBase + { + #region Pass-Through Properties + + #region Header + + /// + public string Signature => _file.Header.Signature; + + /// + public uint LumpCount => _file.Header.LumpCount; + + /// + public uint LumpOffset => _file.Header.LumpOffset; + + #endregion + + #region Lumps + + /// + public Models.WAD.Lump[] Lumps => _file.Lumps; + + #endregion + + #region Lump Infos + + /// + public Models.WAD.LumpInfo[] LumpInfos => _file.LumpInfos; + + #endregion + + #endregion + + #region Extension Properties + + // TODO: Figure out what extension oroperties are needed + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the WAD + /// + private Models.WAD.File _file; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private WAD() { } + + /// + /// Create a WAD from a byte array and offset + /// + /// Byte array representing the WAD + /// Offset within the array to parse + /// A WAD wrapper on success, null on failure + public static WAD Create(byte[] data, int offset) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (offset < 0 || offset >= data.Length) + return null; + + // Create a memory stream and use that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return Create(dataStream); + } + + /// + /// Create a WAD from a Stream + /// + /// Stream representing the WAD + /// An WAD wrapper on success, null on failure + public static WAD Create(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + var file = Builders.WAD.ParseFile(data); + if (file == null) + return null; + + var wrapper = new WAD + { + _file = file, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Printing + + /// + public override void Print() + { + Console.WriteLine("WAD Information:"); + Console.WriteLine("-------------------------"); + Console.WriteLine(); + + PrintHeader(); + PrintLumps(); + PrintLumpInfos(); + } + + /// + /// Print header information + /// + private void PrintHeader() + { + Console.WriteLine(" Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Signature: {Signature}"); + Console.WriteLine($" Lump count: {LumpCount}"); + Console.WriteLine($" Lump offset: {LumpOffset}"); + Console.WriteLine(); + } + + /// + /// Print lumps information + /// + private void PrintLumps() + { + Console.WriteLine(" Lumps Information:"); + Console.WriteLine(" -------------------------"); + if (Lumps == null || Lumps.Length == 0) + { + Console.WriteLine(" No lumps"); + } + else + { + for (int i = 0; i < Lumps.Length; i++) + { + var lump = Lumps[i]; + Console.WriteLine($" Lump {i}"); + Console.WriteLine($" Offset: {lump.Offset}"); + Console.WriteLine($" Disk length: {lump.DiskLength}"); + Console.WriteLine($" Length: {lump.Length}"); + Console.WriteLine($" Type: {lump.Type}"); + Console.WriteLine($" Compression: {lump.Compression}"); + Console.WriteLine($" Padding 0: {lump.Padding0}"); + Console.WriteLine($" Padding 1: {lump.Padding1}"); + Console.WriteLine($" Name: {lump.Name}"); + } + } + Console.WriteLine(); + } + + /// + /// Print lump infos information + /// + private void PrintLumpInfos() + { + Console.WriteLine(" Lump Infos Information:"); + Console.WriteLine(" -------------------------"); + if (LumpInfos == null || LumpInfos.Length == 0) + { + Console.WriteLine(" No lump infos"); + } + else + { + for (int i = 0; i < LumpInfos.Length; i++) + { + var lumpInfo = LumpInfos[i]; + Console.WriteLine($" Lump Info {i}"); + if (lumpInfo == null) + { + Console.WriteLine(" Lump is compressed"); + } + else + { + Console.WriteLine($" Name: {lumpInfo.Name ?? "[NULL]"}"); + Console.WriteLine($" Width: {lumpInfo.Width}"); + Console.WriteLine($" Height: {lumpInfo.Height}"); + Console.WriteLine($" Pixel offset: {lumpInfo.PixelOffset}"); + // TODO: Print unknown data? + // TODO: Print pixel data? + Console.WriteLine($" Palette size: {lumpInfo.PaletteSize}"); + // TODO: Print palette data? + } + } + } + Console.WriteLine(); + } + + #endregion + + #region Extraction + + /// + /// Extract all lumps from the WAD to an output directory + /// + /// Output directory to write to + /// True if all lumps extracted, false otherwise + public bool ExtractAllLumps(string outputDirectory) + { + // If we have no lumps + if (Lumps == null || Lumps.Length == 0) + return false; + + // Loop through and extract all lumps to the output + bool allExtracted = true; + for (int i = 0; i < Lumps.Length; i++) + { + allExtracted &= ExtractLump(i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a lump from the WAD to an output directory by index + /// + /// Lump index to extract + /// Output directory to write to + /// True if the lump extracted, false otherwise + public bool ExtractLump(int index, string outputDirectory) + { + // If we have no lumps + if (Lumps == null || Lumps.Length == 0) + return false; + + // If the lumps index is invalid + if (index < 0 || index >= Lumps.Length) + return false; + + // Get the lump + var lump = Lumps[index]; + if (lump == null) + return false; + + // Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture) + byte[] data = ReadFromDataSource((int)lump.Offset, (int)lump.Length); + if (data == null) + return false; + + // Create the filename + string filename = $"{lump.Name}.lmp"; + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename); + + // Ensure the output directory is created + Directory.CreateDirectory(Path.GetDirectoryName(filename)); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return true; + } + + #endregion + } +} \ No newline at end of file diff --git a/BurnOutSharp/FileType/WAD.cs b/BurnOutSharp/FileType/WAD.cs new file mode 100644 index 00000000..82ae0da6 --- /dev/null +++ b/BurnOutSharp/FileType/WAD.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using BurnOutSharp.Interfaces; +using static BurnOutSharp.Utilities.Dictionary; + +namespace BurnOutSharp.FileType +{ + /// + /// Half-Life Texture Package File + /// + public class WAD : IScannable + { + /// + public ConcurrentDictionary> Scan(Scanner scanner, string file) + { + if (!File.Exists(file)) + return null; + + using (var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + return Scan(scanner, fs, file); + } + } + + /// + public ConcurrentDictionary> Scan(Scanner scanner, Stream stream, string file) + { + // If the WAD file itself fails + try + { + string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempPath); + + // Create the wrapper + Wrappers.WAD wad = Wrappers.WAD.Create(stream); + if (wad == null) + return null; + + // Loop through and extract all files + wad.ExtractAllLumps(tempPath); + + // Collect and format all found protections + var protections = scanner.GetProtections(tempPath); + + // If temp directory cleanup fails + try + { + Directory.Delete(tempPath, true); + } + catch (Exception ex) + { + if (scanner.IncludeDebug) Console.WriteLine(ex); + } + + // Remove temporary path references + StripFromKeys(protections, tempPath); + + return protections; + } + catch (Exception ex) + { + if (scanner.IncludeDebug) Console.WriteLine(ex); + } + + return null; + } + } +} diff --git a/BurnOutSharp/Tools/Utilities.cs b/BurnOutSharp/Tools/Utilities.cs index 22b013cc..81b093a0 100644 --- a/BurnOutSharp/Tools/Utilities.cs +++ b/BurnOutSharp/Tools/Utilities.cs @@ -639,7 +639,7 @@ namespace BurnOutSharp.Tools case SupportedFileType.Textfile: return new FileType.Textfile(); case SupportedFileType.VBSP: return new FileType.VBSP(); case SupportedFileType.VPK: return new FileType.VPK(); - case SupportedFileType.WAD: return new FileType.Valve(); + case SupportedFileType.WAD: return new FileType.WAD(); case SupportedFileType.XZ: return new FileType.XZ(); case SupportedFileType.XZP: return new FileType.Valve(); default: return null; diff --git a/Test/Program.cs b/Test/Program.cs index f3440305..91db446f 100644 --- a/Test/Program.cs +++ b/Test/Program.cs @@ -457,6 +457,25 @@ namespace Test vpk.Print(); } + // WAD + else if (IsWAD(magic)) + { + // Build the archive information + Console.WriteLine("Creating WAD deserializer"); + Console.WriteLine(); + + var wad = WAD.Create(stream); + if (wad == null) + { + Console.WriteLine("Something went wrong parsing WAD"); + Console.WriteLine(); + return; + } + + // Print the WAD info to screen + wad.Print(); + } + // Everything else else { @@ -612,6 +631,17 @@ namespace Test return magic[0] == 0x34 && magic[1] == 0x12 && magic[2] == 0xaa && magic[3] == 0x55; } + /// + /// Determine if the magic bytes indicate a WAD + /// + private static bool IsWAD(byte[] magic) + { + if (magic == null || magic.Length < 4) + return false; + + return magic[0] == 'W' && magic[1] == 'A' && magic[2] == 'D' && magic[3] == '3'; + } + #endregion } }