diff --git a/BurnOutSharp.Builders/BSP.cs b/BurnOutSharp.Builders/BSP.cs index 8b06dd34..1b183f8f 100644 --- a/BurnOutSharp.Builders/BSP.cs +++ b/BurnOutSharp.Builders/BSP.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Linq; +using System.Text; using BurnOutSharp.Models.BSP; using BurnOutSharp.Utilities; @@ -124,6 +126,28 @@ namespace BurnOutSharp.Builders #endregion + #region Textures + + // Create the texture array + file.Textures = new Texture[textureHeader.TextureCount]; + + // Try to parse the textures + for (int i = 0; i < textureHeader.TextureCount; i++) + { + // Get the texture offset + int offset = (int)(textureHeader.Offsets[i] + file.Lumps[HL_BSP_LUMP_TEXTUREDATA].Offset); + if (offset < 0 || offset >= data.Length) + continue; + + // Seek to the texture + data.Seek(offset, SeekOrigin.Begin); + + var texture = ParseTexture(data); + file.Textures[i] = texture; + } + + #endregion + return file; } @@ -185,6 +209,65 @@ namespace BurnOutSharp.Builders return textureHeader; } + /// + /// Parse a Stream into a texture + /// + /// Stream to parse + /// Mipmap level + /// Filled texture on success, null on error + private static Texture ParseTexture(Stream data, uint mipmap = 0) + { + // TODO: Use marshalling here instead of building + Texture texture = new Texture(); + + byte[] name = data.ReadBytes(16).TakeWhile(c => c != '\0').ToArray(); + texture.Name = Encoding.ASCII.GetString(name); + texture.Width = data.ReadUInt32(); + texture.Height = data.ReadUInt32(); + texture.Offsets = new uint[4]; + for (int i = 0; i < 4; i++) + { + texture.Offsets[i] = data.ReadUInt32(); + } + + // Get the size of the pixel data + uint pixelSize = 0; + for (int i = 0; i < HL_BSP_MIPMAP_COUNT; i++) + { + if (texture.Offsets[i] != 0) + { + pixelSize += (texture.Width >> i) * (texture.Height >> i); + } + } + + // If we have no pixel data + if (pixelSize == 0) + return texture; + + texture.TextureData = data.ReadBytes((int)pixelSize); + texture.PaletteSize = data.ReadUInt16(); + texture.PaletteData = data.ReadBytes((int)(texture.PaletteSize * 3)); + + // Adjust the dimensions based on mipmap level + switch (mipmap) + { + case 1: + texture.Width /= 2; + texture.Height /= 2; + break; + case 2: + texture.Width /= 4; + texture.Height /= 4; + break; + case 3: + texture.Width /= 8; + texture.Height /= 8; + break; + } + + return texture; + } + #endregion } } diff --git a/BurnOutSharp.Models/BSP/File.cs b/BurnOutSharp.Models/BSP/File.cs index 337fa2cd..0f3b920d 100644 --- a/BurnOutSharp.Models/BSP/File.cs +++ b/BurnOutSharp.Models/BSP/File.cs @@ -20,5 +20,10 @@ namespace BurnOutSharp.Models.BSP /// Texture header data /// public TextureHeader TextureHeader { get; set; } + + /// + /// Textures + /// + public Texture[] Textures { get; set; } } } \ No newline at end of file diff --git a/BurnOutSharp.Models/BSP/Texture.cs b/BurnOutSharp.Models/BSP/Texture.cs index 974a364a..65aa7cad 100644 --- a/BurnOutSharp.Models/BSP/Texture.cs +++ b/BurnOutSharp.Models/BSP/Texture.cs @@ -22,5 +22,20 @@ namespace BurnOutSharp.Models.BSP /// Offsets /// public uint[] Offsets; + + /// + /// Texture data + /// + public byte[] TextureData; + + /// + /// Palette size + /// + public uint PaletteSize; + + /// + /// Palette data + /// + public byte[] PaletteData; } } \ No newline at end of file diff --git a/BurnOutSharp.Wrappers/BSP.cs b/BurnOutSharp.Wrappers/BSP.cs index 931613c7..94bf22d9 100644 --- a/BurnOutSharp.Wrappers/BSP.cs +++ b/BurnOutSharp.Wrappers/BSP.cs @@ -1,7 +1,6 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Linq; -using BurnOutSharp.Utilities; namespace BurnOutSharp.Wrappers { @@ -33,6 +32,13 @@ namespace BurnOutSharp.Wrappers #endregion + #region Textures + + /// + public Models.BSP.Texture[] Textures => _file.Textures; + + #endregion + #endregion #region Extension Properties @@ -116,6 +122,7 @@ namespace BurnOutSharp.Wrappers PrintHeader(); PrintLumps(); PrintTextureHeader(); + PrintTextures(); } /// @@ -176,40 +183,284 @@ namespace BurnOutSharp.Wrappers Console.WriteLine(); } + /// + /// Print textures information + /// + private void PrintTextures() + { + Console.WriteLine(" Textures Information:"); + Console.WriteLine(" -------------------------"); + if (Textures == null || Textures.Length == 0) + { + Console.WriteLine(" No textures"); + } + else + { + for (int i = 0; i < Textures.Length; i++) + { + var texture = Textures[i]; + Console.WriteLine($" Texture {i}"); + Console.WriteLine($" Name: {texture.Name}"); + Console.WriteLine($" Width: {texture.Width}"); + Console.WriteLine($" Height: {texture.Height}"); + Console.WriteLine($" Offsets: {string.Join(", ", texture.Offsets)}"); + // Skip texture data + Console.WriteLine($" Palette size: {texture.PaletteSize}"); + // Skip palette data + } + } + Console.WriteLine(); + } + #endregion #region Extraction /// - /// Extract all files from the VPK to an output directory + /// Extract all lumps from the BSP to an output directory /// /// Output directory to write to - /// True if all files extracted, false otherwise - public bool ExtractAll(string outputDirectory) + /// True if all lumps extracted, false otherwise + public bool ExtractAllLumps(string outputDirectory) { - return false; + // 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 VPK to an output directory by index + /// Extract a lump from the BSP 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) { - return false; + // 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 + byte[] data = ReadFromDataSource((int)lump.Offset, (int)lump.Length); + if (data == null) + return false; + + // Create the filename + string filename = $"lump_{index}.bin"; + switch (index) + { + case Builders.BSP.HL_BSP_LUMP_ENTITIES: + filename = "entities.ent"; + break; + case Builders.BSP.HL_BSP_LUMP_TEXTUREDATA: + filename = "texture_data.bin"; + break; + } + + // 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; } /// - /// Extract a texture from the VPK to an output directory by index + /// Extract all textures from the BSP to an output directory + /// + /// Output directory to write to + /// True if all textures extracted, false otherwise + public bool ExtractAllTextures(string outputDirectory) + { + // If we have no textures + if (Offsets == null || Offsets.Length == 0) + return false; + + // Loop through and extract all lumps to the output + bool allExtracted = true; + for (int i = 0; i < Offsets.Length; i++) + { + allExtracted &= ExtractTexture(i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a texture from the BSP to an output directory by index /// /// Lump index to extract /// Output directory to write to /// True if the texture extracted, false otherwise public bool ExtractTexture(int index, string outputDirectory) { - return false; + // If we have no textures + if (Textures == null || Textures.Length == 0) + return false; + + // If the texture index is invalid + if (index < 0 || index >= Textures.Length) + return false; + + // Get the texture + var texture = Textures[index]; + if (texture == null) + return false; + + // Read the data + byte[] data = CreateTextureData(texture); + if (data == null) + return false; + + // Create the filename + string filename = $"{texture.Name}.bmp"; + + // 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; + } + + /// + /// Create a bitmap from the texture and palette data + /// + /// Texture object to format + /// Byte array representing the texture as a bitmap + private static byte[] CreateTextureData(Models.BSP.Texture texture) + { + // If there's no texture data + if (texture.TextureData == null || texture.TextureData.Length == 0) + return null; + + // Create the bitmap file header + var fileHeader = new Models.BMP.BITMAPFILEHEADER() + { + Type = ('M' << 8) | 'B', + Size = 14 + 40 + (texture.PaletteSize * 4) + (texture.Width * texture.Height), + OffBits = 14 + 40 + (texture.PaletteSize * 4), + }; + + // Create the bitmap info header + var infoHeader = new Models.BMP.BITMAPINFOHEADER + { + Size = 40, + Width = (int)texture.Width, + Height = (int)texture.Height, + Planes = 1, + BitCount = 8, + SizeImage = 0, + ClrUsed = texture.PaletteSize, + ClrImportant = texture.PaletteSize, + }; + + // Reformat the palette data + byte[] paletteData = new byte[texture.PaletteSize * 4]; + for (uint i = 0; i < texture.PaletteSize; i++) + { + paletteData[i * 4 + 0] = texture.PaletteData[i * 3 + 2]; + paletteData[i * 4 + 1] = texture.PaletteData[i * 3 + 1]; + paletteData[i * 4 + 2] = texture.PaletteData[i * 3 + 0]; + paletteData[i * 4 + 3] = 0; + } + + // Reformat the pixel data + byte[] pixelData = new byte[texture.Width * texture.Height]; + for (uint i = 0; i < texture.Width; i++) + { + for (uint j = 0; j < texture.Height; j++) + { + pixelData[i + ((texture.Height - 1 - j) * texture.Width)] = texture.TextureData[i + j * texture.Width]; + } + } + + // Build the file data + List buffer = new List(); + + // Bitmap file header + buffer.AddRange(BitConverter.GetBytes(fileHeader.Type)); + buffer.AddRange(BitConverter.GetBytes(fileHeader.Size)); + buffer.AddRange(BitConverter.GetBytes(fileHeader.Reserved1)); + buffer.AddRange(BitConverter.GetBytes(fileHeader.Reserved2)); + buffer.AddRange(BitConverter.GetBytes(fileHeader.OffBits)); + + // Bitmap info header + buffer.AddRange(BitConverter.GetBytes(infoHeader.Size)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.Width)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.Height)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.Planes)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.BitCount)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.Compression)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.SizeImage)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.XPelsPerMeter)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.YPelsPerMeter)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.ClrUsed)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.ClrImportant)); + + // Palette data + buffer.AddRange(paletteData); + + // Pixel data + buffer.AddRange(pixelData); + + return buffer.ToArray(); } #endregion