commit 08aec8f1f736c0f880bbfa2695d756e8e427050d Author: bird_egop Date: Thu Nov 14 12:04:05 2024 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d031f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ + +.idea/ \ No newline at end of file diff --git a/ParkanPlayground.sln b/ParkanPlayground.sln new file mode 100644 index 0000000..6b21db4 --- /dev/null +++ b/ParkanPlayground.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParkanPlayground", "ParkanPlayground\ParkanPlayground.csproj", "{7DB19000-6F41-4BAE-A904-D34EFCA065E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TextureDecoder", "TextureDecoder\TextureDecoder.csproj", "{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Release|Any CPU.Build.0 = Release|Any CPU + {15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ParkanPlayground/ListMetadataItem.cs b/ParkanPlayground/ListMetadataItem.cs new file mode 100644 index 0000000..4f87002 --- /dev/null +++ b/ParkanPlayground/ListMetadataItem.cs @@ -0,0 +1,3 @@ +namespace ParkanPlayground; + +public record ListMetadataItem(string ItemType, int ItemLength, string FileName, int OffsetInFile); \ No newline at end of file diff --git a/ParkanPlayground/ParkanPlayground.csproj b/ParkanPlayground/ParkanPlayground.csproj new file mode 100644 index 0000000..2f4fc77 --- /dev/null +++ b/ParkanPlayground/ParkanPlayground.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs new file mode 100644 index 0000000..61bfb86 --- /dev/null +++ b/ParkanPlayground/Program.cs @@ -0,0 +1,105 @@ +using System.Buffers.Binary; +using System.Text;using ParkanPlayground; + +var libFile = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\ui\\ui_back.lib"; + +using FileStream nResFs = new FileStream(libFile, FileMode.Open); + +Span buffer = stackalloc byte[4]; + +nResFs.ReadExactly(buffer); + +var nResHeader = BinaryPrimitives.ReadInt32LittleEndian(buffer); + +var str = Encoding.ASCII.GetString(buffer); + +Console.WriteLine($"NRES Header: {nResHeader:X} - {str}"); + +// ---- +nResFs.ReadExactly(buffer); + +var version = BinaryPrimitives.ReadInt32LittleEndian(buffer); + +Console.WriteLine($"VERSION: {version:X}"); + +// ---- +nResFs.ReadExactly(buffer); + +var elementCount = BinaryPrimitives.ReadInt32LittleEndian(buffer); + +Console.WriteLine($"ElementCount: {elementCount}"); + +// ---- +nResFs.ReadExactly(buffer); + +var totalLength = BinaryPrimitives.ReadInt32LittleEndian(buffer); + +Console.WriteLine($"TOTAL_LENGTH: {totalLength}"); + +// ---- + +nResFs.Seek(-elementCount * 64, SeekOrigin.End); + +_ = 5; + +var elements = new List(elementCount); + +Span metaDataBuffer = stackalloc byte[64]; +for (int i = 0; i < elementCount; i++) +{ + nResFs.ReadExactly(metaDataBuffer); + + var itemType = Encoding.ASCII.GetString(metaDataBuffer.Slice(0, 8)); + + var itemLength = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer.Slice(12, 4)); + + var fileNameBlock = metaDataBuffer.Slice(20, 20); + var len = fileNameBlock.IndexOf((byte)'\0'); + if (len == -1) len = 20; // whole 20 bytes is a filename + var fileName = Encoding.ASCII.GetString(fileNameBlock.Slice(0, len)); + + var offsetInFile = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer.Slice(56, 4)); + + var lastMagicNumber = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer.Slice(60, 4)); + + Console.WriteLine( + $"File {i+1}: \n" + + $"\tType: {itemType}\n" + + $"\tItemLength: {itemLength}\n" + + $"\tFileName: {fileName}\n" + + $"\tOffsetInFile: {offsetInFile}\n" + + $"\tLastMagicNumber: {lastMagicNumber}" + ); + + elements.Add(new ListMetadataItem(itemType, itemLength, fileName, offsetInFile)); + + metaDataBuffer.Clear(); +} + +var libFileName = new FileInfo(libFile).Name; + +if (Directory.Exists(libFileName)) +{ + Directory.Delete(libFileName, true); +} + +var dir = Directory.CreateDirectory(libFileName); + +byte[] copyBuffer = new byte[8192]; + +foreach (var element in elements) +{ + nResFs.Seek(element.OffsetInFile, SeekOrigin.Begin); + using var destFs = new FileStream(Path.Combine(libFileName, element.FileName), FileMode.CreateNew); + + var totalCopiedBytes = 0; + while (totalCopiedBytes < element.ItemLength) + { + var needReadBytes = Math.Min(element.ItemLength - totalCopiedBytes, copyBuffer.Length); + var readBytes = nResFs.Read(copyBuffer, 0, needReadBytes); + + destFs.Write(copyBuffer, 0, readBytes); + + totalCopiedBytes += readBytes; + } +} \ No newline at end of file diff --git a/TextureDecoder/Extensions.cs b/TextureDecoder/Extensions.cs new file mode 100644 index 0000000..e422529 --- /dev/null +++ b/TextureDecoder/Extensions.cs @@ -0,0 +1,16 @@ +namespace TextureDecoder; + +public static class Extensions +{ + public static int AsStride(this int format) + { + return format switch + { + 8888 => 32, + 4444 => 16, + 565 => 16, + 888 => 32, + 0 => 32 + }; + } +} \ No newline at end of file diff --git a/TextureDecoder/Program.cs b/TextureDecoder/Program.cs new file mode 100644 index 0000000..5b8c0f3 --- /dev/null +++ b/TextureDecoder/Program.cs @@ -0,0 +1,34 @@ +using System.Buffers.Binary; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using TextureDecoder; + +var folder = "C:\\Projects\\CSharp\\ParkanPlayground\\ParkanPlayground\\bin\\Debug\\net8.0\\ui.lib"; + +var files = Directory.EnumerateFiles(folder); + +List textureFiles = []; + +foreach (var file in files) +{ + try + { + var fs = new FileStream(file, FileMode.Open); + + var textureFile = TextureFile.ReadFromStream(fs, file); + + textureFiles.Add(textureFile); + + Console.WriteLine($"Successfully read: {file}"); + } + catch + { + Console.WriteLine($"Failed read: {file}"); + } +} + +foreach (var textureFile in textureFiles) +{ + await textureFile.WriteToFolder("unpacked"); + Console.WriteLine($"Unpacked {Path.GetFileName(textureFile.FileName)} into folder"); +} \ No newline at end of file diff --git a/TextureDecoder/TextureDecoder.csproj b/TextureDecoder/TextureDecoder.csproj new file mode 100644 index 0000000..1e43a53 --- /dev/null +++ b/TextureDecoder/TextureDecoder.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/TextureDecoder/TextureFile.cs b/TextureDecoder/TextureFile.cs new file mode 100644 index 0000000..00be828 --- /dev/null +++ b/TextureDecoder/TextureFile.cs @@ -0,0 +1,433 @@ +using System.Buffers.Binary; +using System.Text; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace TextureDecoder; + +/// +/// Заголовок TEXM файла (может быть .0 файл) +/// +/// Строка TEXM +/// Ширина или высота (не имеет значения, т.к. текстуры всегда квадратные) +/// Ширина или высота (не имеет значения, т.к. текстуры всегда квадратные) +/// Кол-во мипмапов (уменьшенные копии текстуры) +/// Сколько БИТ занимает 1 пиксель +/// Неизвестно +/// Неизвестно +/// Формат пикселя(4444, 8888, 888) +public record TexmHeader( + string TexmAscii, + int Width, + int Height, + int MipmapCount, + int Stride, + int Magic1, + int Magic2, + int Format +); + +/// +/// В конце файла есть секция Page, она содержит информацию об атласе самого большого мипмапа +/// +/// Заголовок секции +/// Кол-во элементов +/// Элементы +public record PageHeader(string Page, int Count, List Items); + +public record PageItem(short X, short Width, short Y, short Height); + +public class TextureFile +{ + /// + /// Исходное имя файла текстуры TEXM + /// + public string FileName { get; set; } + + /// + /// Заголовок файла, всегда 32 байта + /// + public TexmHeader Header { get; set; } + + /// + /// Если в одной текстуре есть несколько MipMap уровней, тут будет несколько отдельных текстур + /// + public List MipmapBytes { get; set; } + + /// + /// Если текстура - это атлас, то здесь будет информация о координатах в атласе + /// + public PageHeader? Pages { get; set; } + + /// + /// В некоторых случаях, текстура может быть закодирована как lookup таблица на 1024 байта (256 цветов), + /// тогда сначала идёт 1024 байта lookup таблицы, а далее сами мипмапы, по 1 байту (каждый байт - индекс в lookup таблице) + /// + public bool IsIndexed { get; set; } + + /// + /// Lookup таблица цветов (каждый цвет закодирован как 4 байта (ARGB)) + /// + public byte[] LookupColors { get; set; } + + + private TextureFile() + { + } + + public static TextureFile ReadFromStream(Stream stream, string file) + { + Span headerBytes = stackalloc byte[32]; + stream.ReadExactly(headerBytes); + + var texmHeader = headerBytes[0..4]; + + var widthBytes = headerBytes[4..8]; + var heightBytes = headerBytes[8..12]; + var mipmapCountBytes = headerBytes[12..16]; + var strideBytes = headerBytes[16..20]; + var magic1Bytes = headerBytes[20..24]; + var magic2Bytes = headerBytes[24..28]; + var formatBytes = headerBytes[28..32]; + + var texmAscii = Encoding.ASCII.GetString(texmHeader); + var width = BinaryPrimitives.ReadInt32LittleEndian(widthBytes); + var height = BinaryPrimitives.ReadInt32LittleEndian(heightBytes); + var mipmapCount = BinaryPrimitives.ReadInt32LittleEndian(mipmapCountBytes); + var stride = BinaryPrimitives.ReadInt32LittleEndian(strideBytes); + var magic1 = BinaryPrimitives.ReadInt32LittleEndian(magic1Bytes); + var magic2 = BinaryPrimitives.ReadInt32LittleEndian(magic2Bytes); + var format = BinaryPrimitives.ReadInt32LittleEndian(formatBytes); + + var textureFile = new TextureFile() + { + FileName = file + }; + + var header = new TexmHeader( + texmAscii, + width, + height, + mipmapCount, + stride, + magic1, + magic2, + format + ); + + textureFile.Header = header; + + if (format == 0) + { + // если формат 0, то текстура использует lookup таблицу в первых 1024 байтах (256 разных цветов в формате ARGB 888) + + var lookupColors = new byte[1024]; + stream.ReadExactly(lookupColors, 0, lookupColors.Length); + + textureFile.LookupColors = lookupColors; + + var mipmapBytesList = ReadMipmapsAsIndexes( + stream, + mipmapCount, + width, + height + ); + + textureFile.MipmapBytes = mipmapBytesList; + textureFile.IsIndexed = true; + } + else + { + var mipmapBytesList = ReadMipmaps( + stream, + format.AsStride(), + mipmapCount, + width, + height + ); + + textureFile.MipmapBytes = mipmapBytesList; + } + + if (stream.Position < stream.Length) + { + // has PAGE data + var pageHeader = ReadPage(stream); + + textureFile.Pages = pageHeader; + } + + return textureFile; + } + + private static PageHeader ReadPage(Stream stream) + { + Span pageBytes = stackalloc byte[4]; + + stream.ReadExactly(pageBytes); + + var pageHeaderAscii = Encoding.ASCII.GetString(pageBytes); + + stream.ReadExactly(pageBytes); + + var pageCount = BinaryPrimitives.ReadInt32LittleEndian(pageBytes); + + List pageItems = []; + + Span itemBytes = stackalloc byte[2]; + for (int i = 0; i < pageCount; i++) + { + stream.ReadExactly(itemBytes); + var x = BinaryPrimitives.ReadInt16LittleEndian(itemBytes); + stream.ReadExactly(itemBytes); + var pageWidth = BinaryPrimitives.ReadInt16LittleEndian(itemBytes); + stream.ReadExactly(itemBytes); + var y = BinaryPrimitives.ReadInt16LittleEndian(itemBytes); + stream.ReadExactly(itemBytes); + var pageHeight = BinaryPrimitives.ReadInt16LittleEndian(itemBytes); + + pageItems.Add( + new PageItem( + x, + pageWidth, + y, + pageHeight + ) + ); + } + + var pageHeader = new PageHeader(pageHeaderAscii, pageCount, pageItems); + return pageHeader; + } + + private static List ReadMipmaps(Stream stream, int stride, int mipmapCount, int topWidth, int topHeight) + { + if (stride == 0) + { + stride = 16; + } + + List mipmapByteLengths = []; + + for (int i = 0; i < mipmapCount; i++) + { + var mipWidth = topWidth / (int) Math.Pow(2, i); + var mipHeight = topHeight / (int) Math.Pow(2, i); + + var imageByteLength = mipWidth * mipHeight * (stride / 8); + mipmapByteLengths.Add(imageByteLength); + } + + List mipmapBytesList = []; + + foreach (var mipmapByteLength in mipmapByteLengths) + { + var mipmapBuffer = new byte[mipmapByteLength]; + + stream.ReadExactly(mipmapBuffer, 0, mipmapByteLength); + + mipmapBytesList.Add(mipmapBuffer); + } + + return mipmapBytesList; + } + + private static List ReadMipmapsAsIndexes(Stream stream, int mipmapCount, int topWidth, int topHeight) + { + List mipmapByteLengths = []; + + for (int i = 0; i < mipmapCount; i++) + { + var mipWidth = topWidth / (int) Math.Pow(2, i); + var mipHeight = topHeight / (int) Math.Pow(2, i); + + var imageByteLength = mipWidth * mipHeight; + mipmapByteLengths.Add(imageByteLength); + } + + List mipmapBytesList = []; + + foreach (var mipmapByteLength in mipmapByteLengths) + { + var mipmapBuffer = new byte[mipmapByteLength]; + + stream.ReadExactly(mipmapBuffer, 0, mipmapByteLength); + + mipmapBytesList.Add(mipmapBuffer); + } + + return mipmapBytesList; + } + + public async Task WriteToFolder(string folder) + { + if (Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + + var outputDir = Path.Combine(folder, Path.GetFileName(FileName)); + Directory.CreateDirectory(outputDir); + + if (IsIndexed) + { + for (var i = 0; i < Header.MipmapCount; i++) + { + var mipWidth = Header.Width / (int) Math.Pow(2, i); + var mipHeight = Header.Height / (int) Math.Pow(2, i); + var reinterpretedPixels = ReinterpretIndexedMipmap(MipmapBytes[i], LookupColors); + + var image = Image.LoadPixelData(reinterpretedPixels, mipWidth, mipHeight); + + image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{mipWidth}x{mipHeight}_indexed.png"); + } + + return; + } + + for (var i = 0; i < Header.MipmapCount; i++) + { + var mipWidth = Header.Width / (int) Math.Pow(2, i); + var mipHeight = Header.Height / (int) Math.Pow(2, i); + + var reinterpretedPixels = ReinterpretMipmapBytesAsRgba32( + MipmapBytes[i], + mipWidth, + mipHeight, + Header.Format + ); + + var image = Image.LoadPixelData(reinterpretedPixels, mipWidth, mipHeight); + + image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{mipWidth}x{mipHeight}.png"); + } + } + + private byte[] ReinterpretIndexedMipmap(byte[] bytes, byte[] lookupColors) + { + var span = bytes.AsSpan(); + + var result = new byte[bytes.Length * 4]; + for (var i = 0; i < span.Length; i++) + { + var index = span[i]; + + var a = lookupColors[index * 4 + 0]; + var r = lookupColors[index * 4 + 1]; + var g = lookupColors[index * 4 + 2]; + var b = lookupColors[index * 4 + 3]; + + result[i * 4 + 0] = r; + result[i * 4 + 1] = g; + result[i * 4 + 2] = b; + result[i * 4 + 3] = a; + } + + return result; + } + + private byte[] ReinterpretMipmapBytesAsRgba32(byte[] bytes, int mipWidth, int mipHeight, int format) + { + var result = format switch + { + 8888 => ReinterpretAs8888(bytes, mipWidth, mipHeight), + 888 => ReinterpretAs888(bytes, mipWidth, mipHeight), + 4444 => ReinterpretAs4444(bytes, mipWidth, mipHeight), + 565 => ReinterpretAs565(bytes, mipWidth, mipHeight), + _ => throw new InvalidOperationException($"Invalid format {format}") + }; + + return result; + } + + private byte[] ReinterpretAs565(byte[] bytes, int mipWidth, int mipHeight) + { + var span = bytes.AsSpan(); + + var result = new byte[bytes.Length * 2]; + for (var i = 0; i < span.Length; i += 2) + { + var rawPixel = span.Slice(i, 2); + + var r = (byte)(((rawPixel[0] >> 3) & 0b11111) / 31 * 255); + var g = (byte)(((rawPixel[0] & 0b111) << 3) | ((rawPixel[1] >> 5) & 0b111) / 63 * 255); + var b = (byte)((rawPixel[1] & 0b11111) / 31 * 255); + + result[i / 2 * 4 + 0] = r; + result[i / 2 * 4 + 1] = g; + result[i / 2 * 4 + 2] = b; + result[i / 2 * 4 + 3] = 255; + } + + return result; + } + + private byte[] ReinterpretAs4444(byte[] bytes, int mipWidth, int mipHeight) + { + var span = bytes.AsSpan(); + + var result = new byte[bytes.Length * 2]; + for (var i = 0; i < span.Length; i += 2) + { + var rawPixel = span.Slice(i, 2); + + var a = (byte)((float)((rawPixel[0] >> 4) & 0b1111) / 15 * 255); + var r = (byte)((float)((rawPixel[0] >> 0) & 0b1111) / 15 * 255); + var g = (byte)((float)((rawPixel[1] >> 4) & 0b1111) / 15 * 255); + var b = (byte)((float)((rawPixel[1] >> 0) & 0b1111) / 15 * 255); + + result[i / 2 * 4 + 0] = r; + result[i / 2 * 4 + 1] = g; + result[i / 2 * 4 + 2] = b; + result[i / 2 * 4 + 3] = a; + } + + return result; + } + + private byte[] ReinterpretAs888(byte[] bytes, int mipWidth, int mipHeight) + { + var span = bytes.AsSpan(); + + var result = new byte[bytes.Length * 2]; + for (var i = 0; i < span.Length; i += 4) + { + var rawPixel = span.Slice(i, 4); + + var x = rawPixel[0]; + var y = rawPixel[1]; + var z = rawPixel[2]; + var w = rawPixel[3]; + + result[i + 0] = y; + result[i + 1] = z; + result[i + 2] = w; + result[i + 3] = 255; + } + + return result; + } + + private byte[] ReinterpretAs8888(byte[] bytes, int mipWidth, int mipHeight) + { + var span = bytes.AsSpan(); + + var result = new byte[bytes.Length]; + for (var i = 0; i < span.Length; i += 4) + { + var rawPixel = span.Slice(i, 4); + + var a = rawPixel[0]; + var r = rawPixel[1]; + var g = rawPixel[2]; + var b = rawPixel[3]; + + result[i + 0] = r; + result[i + 1] = g; + result[i + 2] = b; + result[i + 3] = a; + } + + return result; + } +} \ No newline at end of file