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