From 8f7e17251f7785bef4d66d3e7014e17884fb2bd8 Mon Sep 17 00:00:00 2001 From: windyfairy <39211915+windyfairy@users.noreply.github.com> Date: Sat, 12 May 2018 17:00:25 +0900 Subject: [PATCH] Initial release --- .gitignore | 330 +++++++++ README | 29 + gitadora-texbintool/App.config | 6 + gitadora-texbintool/Program.cs | 693 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 36 + .../gitadora-texbintool.csproj | 58 ++ gitadora-textool.sln | 31 + gitadora-textool/App.config | 6 + gitadora-textool/DxtUtil.cs | 460 ++++++++++++ gitadora-textool/Program.cs | 530 ++++++++++++++ gitadora-textool/Properties/AssemblyInfo.cs | 36 + gitadora-textool/gitadora-textool.csproj | 55 ++ 12 files changed, 2270 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100644 gitadora-texbintool/App.config create mode 100644 gitadora-texbintool/Program.cs create mode 100644 gitadora-texbintool/Properties/AssemblyInfo.cs create mode 100644 gitadora-texbintool/gitadora-texbintool.csproj create mode 100644 gitadora-textool.sln create mode 100644 gitadora-textool/App.config create mode 100644 gitadora-textool/DxtUtil.cs create mode 100644 gitadora-textool/Program.cs create mode 100644 gitadora-textool/Properties/AssemblyInfo.cs create mode 100644 gitadora-textool/gitadora-textool.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13a9b2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,330 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ diff --git a/README b/README new file mode 100644 index 0000000..3fd8498 --- /dev/null +++ b/README @@ -0,0 +1,29 @@ +## gitadora-texbintool.exe +Convert archives of tex images. +``` +usage: gitadora-texbintool.exe [--no-rect/--nr] [--no-split/--ns] input_filename +--no-rect/--nr: Don't create a rect table (Some games like Jubeat don't use the rect table) +--no-split/--ns: Don't split images into separate images if they use the rect table +``` + +If you specify a .bin file as the `input_filename` then the tool will extract the textures into a folder with the same name as the .bin file. +If you specify a folder as the `input_filename` then the tool will create a .bin file with the same name as the folder. + +`--no-rect` is used for .bin creation. It skips writing the rect section at the end of the bin file. + +`--no-split` is used during .bin extraction. Texbins can have multiple files, and within those files have multiple rects/subimages. +This command will output the original images with a `metadata.xml` file containing the rect information. +When creating a .bin from a folder with a `metadata.xml`, the `metadata.xml` is used to create the .bin. Any files in the folder not listed in the `metadata.xml` will be ignored. +If you want to replace a specific subimage without modifying the original image file, you can modify the `ExternalFilename` part of the `metadata.xml` to point to the new image file while updating the X/Y (set to 0) and updating the W/H (set as required). + +## gitadora-textool.exe +Convert individual tex files. +``` +usage: gitadora-textool.exe input_filename +``` + +If you specify a .tex file as the `input_filename` then the tool will convert the .tex to .png. + +If you specify a non-.tex file as the `input_filename` then the tool will try to convert the image file to .tex. +The tool uses C#'s Bitmap class to load images, so any format supported normally by C# should work (PNG, JPG, BMP, etc). +PNG is the only "officially" supported format but JPG should be safe as well, and probably others too. diff --git a/gitadora-texbintool/App.config b/gitadora-texbintool/App.config new file mode 100644 index 0000000..731f6de --- /dev/null +++ b/gitadora-texbintool/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/gitadora-texbintool/Program.cs b/gitadora-texbintool/Program.cs new file mode 100644 index 0000000..5e58777 --- /dev/null +++ b/gitadora-texbintool/Program.cs @@ -0,0 +1,693 @@ +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; +using gitadora_textool; + +namespace gitadora_texbintool +{ + public class EntryInfo + { + public int Id; + public int Hash; + public int Offset; + public string Filename; + + // For data entry + public int Unk1; + public int CompSize; + public int DataOffset; + } + + public struct RectMetadata + { + public int ImageId; + public ushort X; + public ushort Y; + public ushort W; + public ushort H; + public EntryInfo Entry; + } + + public class TexInfo + { + public List RectInfo; + + public TexInfo() + { + RectInfo = new List(); + } + } + + class Program + { + static int ReadInt32(BinaryReader reader) + { + var data = reader.ReadBytes(4); + Array.Reverse(data); + return BitConverter.ToInt32(data, 0); + } + + static byte[] Decompress(BinaryReader reader) + { + var decompSize = ReadInt32(reader); + var compSize = ReadInt32(reader); + + if (compSize == 0) + { + return reader.ReadBytes(decompSize); + } + + var compData = reader.ReadBytes(compSize); + + byte[] windowData = new byte[4096]; + byte[] outputData = new byte[decompSize]; + int compOffset = 0, decompOffset = 0, window = 4078; + + uint controlByte = 0; + int bitCount = 0; + + while (true) + { + if (bitCount == 0) + { + if (compOffset >= compSize) + { + //Console.WriteLine($"compOffset >= compSize: {compOffset} >= {compSize}"); + break; + } + + controlByte = compData[compOffset++]; + bitCount = 8; + } + + if ((controlByte & 0x01) != 0) + { + if (compOffset >= compSize) + { + //Console.WriteLine($"compOffset >= compSize: {compOffset} >= {compSize}"); + break; + } + + outputData[decompOffset] = windowData[window] = compData[compOffset]; + decompOffset++; + window++; + compOffset++; + + if (decompOffset >= decompSize) + { + //Console.WriteLine($"decompOffset >= decompSize: {decompOffset} >= {decompSize}"); + break; + } + + window &= 0xfff; + } + else + { + if (decompOffset >= decompSize - 1) + { + //Console.WriteLine($"decompOffset >= decompSize - 1: {decompOffset} >= {decompSize} - 1"); + break; + } + + var slideOffset = (((compData[compOffset + 1] & 0xf0) << 4) | compData[compOffset]) & 0xfff; + var slideLength = (compData[compOffset + 1] & 0x0f) + 3; + compOffset += 2; + + if (decompOffset + slideLength > decompSize) + { + slideLength = decompSize - decompOffset; + } + + //Console.WriteLine("{0:x8} {1:x8}", slideOffset, slideLength); + + while (slideLength > 0) + { + outputData[decompOffset] = windowData[window] = windowData[slideOffset]; + decompOffset++; + window++; + slideOffset++; + + window &= 0xfff; + slideOffset &= 0xfff; + slideLength--; + } + } + + controlByte >>= 1; + bitCount--; + } + + return outputData; + } + + static int CalculateHash(string input) + { + int hash = 0; + + foreach (var c in input) + { + for (int i = 0; i <= 5; i++) + { + hash = (hash >> 31) & 0x4C11DB7 ^ ((hash << 1) | ((c >> i) & 1)); + } + } + + return hash; + } + + static void ReadDataEntrySection(BinaryReader reader, int offset, Int64 count, List entries) + { + reader.BaseStream.Seek(offset, SeekOrigin.Begin); + + for (var i = 0; i < count; i++) + { + entries[i].Unk1 = reader.ReadInt32(); + entries[i].CompSize = reader.ReadInt32(); + entries[i].DataOffset = reader.ReadInt32(); + } + } + + static List ReadNameSection(BinaryReader reader, int offset) + { + reader.BaseStream.Seek(offset, SeekOrigin.Begin); + + var nampMagic = Encoding.ASCII.GetString(reader.ReadBytes(4)); + if (nampMagic != "PMAN") + { + Console.WriteLine("Not a valid name section"); + Environment.Exit(1); + } + + var nampSectionSize = reader.ReadInt32(); + var unk1 = reader.ReadBytes(8); + var fileCount = reader.ReadInt32(); + var unk2 = reader.ReadBytes(8); + + var stringMetadata = new List(); + for (int i = 0; i < fileCount; i++) + { + int hash = reader.ReadInt32(); + int id = reader.ReadInt32(); + int strOffset = reader.ReadInt32(); + + var backupOffset = reader.BaseStream.Position; + + reader.BaseStream.Seek(offset + strOffset, SeekOrigin.Begin); + var strBytes = new List(); + while (reader.PeekChar() != 0) + { + strBytes.Add(reader.ReadByte()); + } + + var str = Encoding.ASCII.GetString(strBytes.ToArray()); + + stringMetadata.Add(new EntryInfo() { Offset = strOffset, Id = id, Hash = hash, Filename = str }); + + reader.BaseStream.Seek(backupOffset, SeekOrigin.Begin); + } + + stringMetadata.Sort((x, y) => x.Id.CompareTo(y.Id)); + + return stringMetadata; + } + + static List ReadRectEntrySection(BinaryReader reader, int offset) + { + reader.BaseStream.Seek(offset, SeekOrigin.Begin); + + var rectMagic = Encoding.ASCII.GetString(reader.ReadBytes(4)); + if (rectMagic != "TCER") + { + Console.WriteLine("Not a valid rect section"); + Environment.Exit(1); + } + + var unk1 = reader.ReadInt32(); + var unk2 = reader.ReadInt32(); + var rectSectionSize = reader.ReadInt32(); + var layerCount = reader.ReadInt32(); + var namOffset = reader.ReadInt32(); + var rectOffset = reader.ReadInt32(); + + var stringMetadata = ReadNameSection(reader, offset + namOffset); + + reader.BaseStream.Seek(offset + rectOffset, SeekOrigin.Begin); + + var rectInfoMetadata = new List(); + for (int i = 0; i < layerCount; i++) + { + var rect = new RectMetadata(); + rect.ImageId = reader.ReadInt32(); + rect.X = reader.ReadUInt16(); + rect.W = (ushort)(reader.ReadUInt16() - rect.X); + rect.Y = reader.ReadUInt16(); + rect.H = (ushort)(reader.ReadUInt16() - rect.Y); + rect.Entry = stringMetadata[i]; + rectInfoMetadata.Add(rect); + + Console.WriteLine("{0:x4}x{1:x4} {2:x4}x{3:x4}", rect.X, rect.Y, rect.W, rect.H); + } + + return rectInfoMetadata; + } + + public static string Serialize(T value, string outputFilename = null) + { + if (value == null) + { + return string.Empty; + } + try + { + var xmlserializer = new XmlSerializer(typeof(T)); + + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Indent = true; + settings.IndentChars = ("\t"); + + XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces(); + namespaces.Add(string.Empty, string.Empty); + + if (outputFilename != null) + { + using (var writer = XmlWriter.Create(outputFilename, settings)) + { + xmlserializer.Serialize(writer, value, namespaces); + } + } + + var stringWriter = new StringWriter(); + + using (var writer = XmlWriter.Create(stringWriter, settings)) + { + xmlserializer.Serialize(writer, value, namespaces); + return stringWriter.ToString(); + } + } + catch (Exception ex) + { + throw new Exception("An error occurred", ex); + } + } + + public static TexInfo Deserialize(string filename) + { + XmlSerializer serializer = new XmlSerializer(typeof(TexInfo)); + + StreamReader reader = new StreamReader(filename); + var texInfoList = (TexInfo)serializer.Deserialize(reader); + reader.Close(); + + for (var index = 0; index < texInfoList.RectInfo.Count; index++) + { + texInfoList.RectInfo[index] = new RectInfo + { + ExternalFilename = texInfoList.RectInfo[index].ExternalFilename, + Filename = texInfoList.RectInfo[index].Filename, + X = texInfoList.RectInfo[index].X, + Y = texInfoList.RectInfo[index].Y, + W = (ushort)(texInfoList.RectInfo[index].X + texInfoList.RectInfo[index].W), + H = (ushort)(texInfoList.RectInfo[index].Y + texInfoList.RectInfo[index].H), + }; + } + + return texInfoList; + } + + static void ParseTexbinFile(string filename, bool splitImages = true) + { + var outputPath = Path.Combine(Path.GetDirectoryName(filename), Path.GetFileNameWithoutExtension(filename)); + Directory.CreateDirectory(outputPath); + + using (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open))) + { + var texpMagic = Encoding.ASCII.GetString(reader.ReadBytes(4)); + + if (texpMagic != "PXET") + { + Console.WriteLine("Not a valid texbin file"); + Environment.Exit(1); + } + + var unk1 = reader.ReadInt32(); + var unk2 = reader.ReadInt32(); + var archiveSize = reader.ReadInt32(); + var unk3 = reader.ReadInt32(); + var fileCount = reader.ReadInt64(); + var dataOffset = reader.ReadInt32(); + var rectOffset = reader.ReadInt32(); + var unk4 = reader.ReadBytes(0x10); + var nameOffset = reader.ReadInt32(); + var unk5 = reader.ReadInt32(); + var dataEntryOffset = reader.ReadInt32(); + + if (fileCount == 0) + { + Console.WriteLine("This file doesn't contain any image data."); + return; + } + + var entries = ReadNameSection(reader, nameOffset); + ReadDataEntrySection(reader, dataEntryOffset, fileCount, entries); + + var texInfo = new TexInfo(); + if (rectOffset != 0) + { + var rectInfo = ReadRectEntrySection(reader, rectOffset); + foreach (var rect in rectInfo) + { + var e = new RectInfo + { + ExternalFilename = entries[rect.ImageId].Filename, + Filename = rect.Entry.Filename, + X = rect.X, + Y = rect.Y, + W = rect.W, + H = rect.H + }; + texInfo.RectInfo.Add(e); + } + + // Add code to optionally not split texture files and save a metadata file instead + if (!splitImages) + { + Serialize(texInfo, Path.Combine(outputPath, "_metadata.xml")); + } + } + + foreach (var entry in entries) + { + reader.BaseStream.Seek(entry.DataOffset, SeekOrigin.Begin); + + var data = Decompress(reader); + + if (Encoding.ASCII.GetString(data, 0, 4) == "TXDT" + || Encoding.ASCII.GetString(data, 0, 4) == "TDXT") + { + var rectInfoList = texInfo.RectInfo.Where(x => + String.CompareOrdinal(Path.GetFileNameWithoutExtension(entry.Filename), + x.ExternalFilename) == 0).ToList(); + + if (!splitImages) + { + rectInfoList.Clear(); + } + + if (rectInfoList.Count == 0) + { + var rectInfo = new RectInfo + { + ExternalFilename = entry.Filename, + Filename = entry.Filename, + X = 0, + Y = 0, + W = 0, + H = 0 + }; + rectInfoList.Add(rectInfo); + } + + foreach (var rectInfo in rectInfoList) + { + try + { + using (var stream = new MemoryStream(data)) + { + using (var dataReader = new BinaryReader(stream)) + { + byte[] extractedData; + + + if (!splitImages || rectInfoList.Count == 0 || rectInfo.W == 0 || rectInfo.H == 0) + { + extractedData = gitadora_textool.Program.ExtractImageCore(dataReader, null); + } + else + { + extractedData = gitadora_textool.Program.ExtractImageCore(dataReader, rectInfo); + } + + var ext = ".png"; + if (extractedData[0] == 'D' && extractedData[1] == 'D' && extractedData[2] == 'S' && extractedData[3] == ' ') + { + ext = ".dds"; + } + + var outputFilename = Path.Combine(outputPath, rectInfo.Filename); + outputFilename += ext; + + Console.WriteLine("Saving {0}...", outputFilename); + + File.WriteAllBytes(outputFilename, extractedData); + } + } + } + catch (Exception e) + { + Console.WriteLine("Couldn't convert image: {0}", e.Message); + File.WriteAllBytes(Path.Combine(outputPath, entry.Filename), data); + } + } + } + } + } + } + + static List CreateNameSection(List filelist_unique) + { + var nameSection = new List(); + var filenameSectionSize = 0x1c + (filelist_unique.Count * 0x0c) + filelist_unique.Select(x => x.Length + 1).Sum(); + if ((filenameSectionSize % 4) != 0) + filenameSectionSize += 4 - (filenameSectionSize % 4); + + nameSection.AddRange(new byte[] { 0x50, 0x4D, 0x41, 0x4E }); + nameSection.AddRange(BitConverter.GetBytes(filenameSectionSize)); + nameSection.AddRange(new byte[] { 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00 }); + nameSection.AddRange(BitConverter.GetBytes(filelist_unique.Count)); + + ushort a = (ushort)(1 << Convert.ToString(filelist_unique.Count >> 1, 2).Length - 1); + ushort b = (ushort)((1 << Convert.ToString(filelist_unique.Count >> 1, 2).Length) - 1); + + nameSection.AddRange(BitConverter.GetBytes(a)); + nameSection.AddRange(BitConverter.GetBytes(b)); + nameSection.AddRange(BitConverter.GetBytes(nameSection.Count + 4)); + + List hashes = filelist_unique.Select(x => (uint)CalculateHash(x)).OrderBy(x => x).ToList(); + string[] filelist_sorted = new string[filelist_unique.Count]; + + foreach (var filename in filelist_unique) + { + filelist_sorted[hashes.IndexOf((uint)CalculateHash(filename))] = filename; + } + + int nameBaseOffsetBase = nameSection.Count + (filelist_unique.Count * 0x0c); + for (int i = 0; i < filelist_sorted.Length; i++) + { + var filelist_idx = filelist_unique.IndexOf(filelist_sorted[i]); + + nameSection.AddRange(BitConverter.GetBytes(CalculateHash(filelist_sorted[i]))); + nameSection.AddRange(BitConverter.GetBytes(filelist_idx)); + + var nameBaseOffset = nameBaseOffsetBase; + for (int j = 0; j < filelist_idx; j++) + { + nameBaseOffset += filelist_unique[j].Length + 1; + } + + nameSection.AddRange(BitConverter.GetBytes(nameBaseOffset)); + } + + for (int i = 0; i < filelist_unique.Count; i++) + { + nameSection.AddRange(Encoding.ASCII.GetBytes(filelist_unique[i])); + nameSection.Add(0); + } + + while (nameSection.Count < filenameSectionSize) + { + nameSection.Add(0); + } + + return nameSection; + } + + static void CreateTexbinFile(string pathname, bool generateRectSection = true) + { + var filelist = Directory.GetFiles(pathname).Where(x => !x.ToLower().EndsWith("_metadata.xml")).ToArray(); + var filelist_unique = filelist.Select(Path.GetFileNameWithoutExtension).Distinct().Where(x => !x.ToLower().EndsWith("_metadata.xml")).ToList(); + filelist_unique = filelist_unique.Select(x => x.ToUpper()).ToList(); + + if (filelist_unique.Count != filelist.Length) + { + Console.WriteLine("Folder has more files than expected. Are there multiple files with the same name (not including extension)?"); + Environment.Exit(1); + } + + var nameSection = CreateNameSection(filelist_unique); + + var dataSection = new List(); + var fileinfoSection = new List(); + var imageRectInfo = new Dictionary>(); + for (int i = 0; i < filelist_unique.Count; i++) + { + var data = File.ReadAllBytes(filelist[i]); + + Console.WriteLine("Adding {0}...", filelist[i]); + + if (!data.Take(4).SequenceEqual(new byte[] { 0x54, 0x58, 0x44, 0x54 }) + && !data.Take(4).SequenceEqual(new byte[] { 0x54, 0x44, 0x58, 0x54 })) + { + data = gitadora_textool.Program.CreateImageCore(data, true); + } + + fileinfoSection.AddRange(BitConverter.GetBytes(0)); + fileinfoSection.AddRange(BitConverter.GetBytes(data.Length + 0x08)); + fileinfoSection.AddRange(BitConverter.GetBytes(0x40 + nameSection.Count + (filelist_unique.Count * 0x0c) + dataSection.Count)); + + dataSection.AddRange(BitConverter.GetBytes(data.Length).Reverse()); + dataSection.AddRange(BitConverter.GetBytes(0)); + dataSection.AddRange(data); + + imageRectInfo[filelist_unique[i]] = new Tuple((ushort)((data[0x11] << 8) | data[0x10]), (ushort)((data[0x13] << 8) | data[0x12])); + } + + if ((dataSection.Count % 4) != 0) + { + var padding = 4 - (dataSection.Count % 4); + while (padding > 0) + { + dataSection.Add(0); + padding--; + } + } + + var rectSection = new List(); + if (generateRectSection) + { + var rectInfo = new TexInfo(); + + if (File.Exists(Path.Combine(pathname, "_metadata.xml"))) + { + rectInfo = Deserialize(Path.Combine(pathname, "_metadata.xml")); + } + else + { + foreach (var filename in filelist_unique) + { + var data = new RectInfo + { + ExternalFilename = filename, + Filename = filename, + X = 0, + Y = 0, + W = imageRectInfo[filename].Item1, + H = imageRectInfo[filename].Item2 + }; + rectInfo.RectInfo.Add(data); + } + } + + var rectNameFilelist = rectInfo.RectInfo.Select(x => x.Filename) + .Select(Path.GetFileNameWithoutExtension).Distinct() + .Where(x => !x.ToLower().EndsWith("_metadata.xml")).ToList(); + + var rectinfoSection = new List(); + var rectNameSection = CreateNameSection(rectNameFilelist); + foreach (var data in rectInfo.RectInfo) + { + rectinfoSection.AddRange(BitConverter.GetBytes(filelist_unique.IndexOf(Path.GetFileNameWithoutExtension(data.ExternalFilename)))); + rectinfoSection.AddRange(BitConverter.GetBytes(data.X)); + rectinfoSection.AddRange(BitConverter.GetBytes(data.W)); + rectinfoSection.AddRange(BitConverter.GetBytes(data.Y)); + rectinfoSection.AddRange(BitConverter.GetBytes(data.H)); + } + + rectSection.AddRange( + new byte[] { 0x54, 0x43, 0x45, 0x52, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00 }); + rectSection.AddRange(BitConverter.GetBytes(0x1c + rectNameSection.Count + rectinfoSection.Count)); + rectSection.AddRange(BitConverter.GetBytes(rectNameFilelist.Count)); + rectSection.AddRange(BitConverter.GetBytes(0x1c)); + rectSection.AddRange(BitConverter.GetBytes(0x1c + rectNameSection.Count)); + rectSection.AddRange(rectNameSection); + rectSection.AddRange(rectinfoSection); + } + + var outputData = new List(); + outputData.AddRange(new byte[] { 0x50, 0x58, 0x45, 0x54, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00 }); + outputData.AddRange(BitConverter.GetBytes(0x40 + nameSection.Count + fileinfoSection.Count + dataSection.Count + rectSection.Count)); // Archive size + outputData.AddRange(BitConverter.GetBytes(1)); + outputData.AddRange(BitConverter.GetBytes(filelist_unique.Count)); + outputData.AddRange(BitConverter.GetBytes(0)); + outputData.AddRange(BitConverter.GetBytes(0x40 + nameSection.Count + fileinfoSection.Count)); + outputData.AddRange(BitConverter.GetBytes(rectSection.Count > 0 ? 0x40 + nameSection.Count + fileinfoSection.Count + dataSection.Count : 0)); + outputData.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); + outputData.AddRange(BitConverter.GetBytes(0x40)); // PMAN section offset + outputData.AddRange(BitConverter.GetBytes(0)); + outputData.AddRange(BitConverter.GetBytes(0x40 + nameSection.Count)); + outputData.AddRange(nameSection); + outputData.AddRange(fileinfoSection); + outputData.AddRange(dataSection); + outputData.AddRange(rectSection); + + var basePath = Path.GetFileName(pathname); + if (String.IsNullOrWhiteSpace(basePath)) + basePath = pathname.Replace(".\\", "").Replace("\\", ""); + + var outputFilename = Path.Combine(Path.GetDirectoryName(pathname), String.Format("{0}.bin", basePath)); + File.WriteAllBytes(outputFilename, outputData.ToArray()); + } + + static void Main(string[] args) + { + if (args.Length <= 0) + { + Console.WriteLine("usage: {0} [--no-rect/--nr] [--no-split/--ns] input_filename", AppDomain.CurrentDomain.FriendlyName); + Console.WriteLine("--no-rect/--nr: Don't create a rect table (Some games like Jubeat don't use the rect table)"); + Console.WriteLine("--no-split/--ns: Don't split images into separate images if they use the rect table"); + Environment.Exit(1); + } + + var splitImage = true; + var generateRectSection = true; + + var filenames = new List(); + for (var index = 0; index < args.Length; index++) + { + if (String.CompareOrdinal(args[index].ToLower(), "--no-rect") == 0 + || String.CompareOrdinal(args[index].ToLower(), "--nr") == 0) + { + generateRectSection = false; + } + else if (String.CompareOrdinal(args[index].ToLower(), "--no-split") == 0 + || String.CompareOrdinal(args[index].ToLower(), "--ns") == 0) + { + splitImage = false; + } + else + { + filenames.Add(args[index]); + } + } + + foreach (var filename in filenames) + { + if (Directory.Exists(filename)) + { + CreateTexbinFile(filename, generateRectSection); + } + else if (File.Exists(filename)) + { + ParseTexbinFile(filename, splitImage); + } + } + } + } +} diff --git a/gitadora-texbintool/Properties/AssemblyInfo.cs b/gitadora-texbintool/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..59bb22c --- /dev/null +++ b/gitadora-texbintool/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("gitadora-texbintool")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("gitadora-texbintool")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("14f21197-8ff1-4a4f-b976-aaa55378fade")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/gitadora-texbintool/gitadora-texbintool.csproj b/gitadora-texbintool/gitadora-texbintool.csproj new file mode 100644 index 0000000..b37e7ac --- /dev/null +++ b/gitadora-texbintool/gitadora-texbintool.csproj @@ -0,0 +1,58 @@ + + + + + Debug + AnyCPU + {14F21197-8FF1-4A4F-B976-AAA55378FADE} + Exe + gitadora_texbintool + gitadora-texbintool + v4.6.1 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + none + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {8c5cc09a-436c-4caa-9213-31a4f351dbdc} + gitadora-textool + + + + \ No newline at end of file diff --git a/gitadora-textool.sln b/gitadora-textool.sln new file mode 100644 index 0000000..b91decf --- /dev/null +++ b/gitadora-textool.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2010 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gitadora-textool", "gitadora-textool\gitadora-textool.csproj", "{8C5CC09A-436C-4CAA-9213-31A4F351DBDC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gitadora-texbintool", "gitadora-texbintool\gitadora-texbintool.csproj", "{14F21197-8FF1-4A4F-B976-AAA55378FADE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8C5CC09A-436C-4CAA-9213-31A4F351DBDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C5CC09A-436C-4CAA-9213-31A4F351DBDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C5CC09A-436C-4CAA-9213-31A4F351DBDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C5CC09A-436C-4CAA-9213-31A4F351DBDC}.Release|Any CPU.Build.0 = Release|Any CPU + {14F21197-8FF1-4A4F-B976-AAA55378FADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14F21197-8FF1-4A4F-B976-AAA55378FADE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14F21197-8FF1-4A4F-B976-AAA55378FADE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14F21197-8FF1-4A4F-B976-AAA55378FADE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5EDE6B8F-78F2-46C0-A47E-D3A53277210A} + EndGlobalSection +EndGlobal diff --git a/gitadora-textool/App.config b/gitadora-textool/App.config new file mode 100644 index 0000000..731f6de --- /dev/null +++ b/gitadora-textool/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/gitadora-textool/DxtUtil.cs b/gitadora-textool/DxtUtil.cs new file mode 100644 index 0000000..a1bc61b --- /dev/null +++ b/gitadora-textool/DxtUtil.cs @@ -0,0 +1,460 @@ +// #region License +// /* +// Microsoft Public License (Ms-PL) +// MonoGame - Copyright © 2009 The MonoGame Team +// +// All rights reserved. +// +// This license governs use of the accompanying software. If you use the software, you accept this license. If you do not +// accept the license, do not use the software. +// +// 1. Definitions +// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +// U.S. copyright law. +// +// A "contribution" is the original software, or any additions or changes to the software. +// A "contributor" is any person that distributes its contribution under this license. +// "Licensed patents" are a contributor's patent claims that read directly on its contribution. +// +// 2. Grant of Rights +// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. +// +// 3. Conditions and Limitations +// (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +// your patent license from such contributor to the software ends automatically. +// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +// notices that are present in the software. +// (D) If you distribute any portion of the software in source code form, you may do so only under this license by including +// a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +// code form, you may only do so under a license that complies with this license. +// (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees +// or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent +// permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular +// purpose and non-infringement. +// */ +// #endregion License +// +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Graphics +{ + internal static class DxtUtil + { + internal static byte[] DecompressDxt1(byte[] imageData, int width, int height) + { + using (MemoryStream imageStream = new MemoryStream(imageData)) + return DecompressDxt1(imageStream, width, height); + } + + internal static byte[] DecompressDxt1(Stream imageStream, int width, int height) + { + byte[] imageData = new byte[width * height * 4]; + + using (BinaryReader imageReader = new BinaryReader(imageStream)) + { + int blockCountX = (width + 3) / 4; + int blockCountY = (height + 3) / 4; + + for (int y = 0; y < blockCountY; y++) + { + for (int x = 0; x < blockCountX; x++) + { + DecompressDxt1Block(imageReader, x, y, blockCountX, width, height, imageData); + } + } + } + + return imageData; + } + + private static void DecompressDxt1Block(BinaryReader imageReader, int x, int y, int blockCountX, int width, int height, byte[] imageData) + { + ushort c0 = imageReader.ReadUInt16(); + ushort c1 = imageReader.ReadUInt16(); + + byte r0, g0, b0; + byte r1, g1, b1; + ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); + ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); + + uint lookupTable = imageReader.ReadUInt32(); + + for (int blockY = 0; blockY < 4; blockY++) + { + for (int blockX = 0; blockX < 4; blockX++) + { + byte r = 0, g = 0, b = 0, a = 255; + uint index = (lookupTable >> 2 * (4 * blockY + blockX)) & 0x03; + + if (c0 > c1) + { + switch (index) + { + case 0: + r = r0; + g = g0; + b = b0; + break; + case 1: + r = r1; + g = g1; + b = b1; + break; + case 2: + r = (byte)((2 * r0 + r1) / 3); + g = (byte)((2 * g0 + g1) / 3); + b = (byte)((2 * b0 + b1) / 3); + break; + case 3: + r = (byte)((r0 + 2 * r1) / 3); + g = (byte)((g0 + 2 * g1) / 3); + b = (byte)((b0 + 2 * b1) / 3); + break; + } + } + else + { + switch (index) + { + case 0: + r = r0; + g = g0; + b = b0; + break; + case 1: + r = r1; + g = g1; + b = b1; + break; + case 2: + r = (byte)((r0 + r1) / 2); + g = (byte)((g0 + g1) / 2); + b = (byte)((b0 + b1) / 2); + break; + case 3: + r = 0; + g = 0; + b = 0; + a = 0; + break; + } + } + + int px = (x << 2) + blockX; + int py = (y << 2) + blockY; + if ((px < width) && (py < height)) + { + int offset = ((py * width) + px) << 2; + imageData[offset] = r; + imageData[offset + 1] = g; + imageData[offset + 2] = b; + imageData[offset + 3] = a; + } + } + } + } + + internal static byte[] DecompressDxt3(byte[] imageData, int width, int height) + { + using (MemoryStream imageStream = new MemoryStream(imageData)) + return DecompressDxt3(imageStream, width, height); + } + + internal static byte[] DecompressDxt3(Stream imageStream, int width, int height) + { + byte[] imageData = new byte[width * height * 4]; + + using (BinaryReader imageReader = new BinaryReader(imageStream)) + { + int blockCountX = (width + 3) / 4; + int blockCountY = (height + 3) / 4; + + for (int y = 0; y < blockCountY; y++) + { + for (int x = 0; x < blockCountX; x++) + { + DecompressDxt3Block(imageReader, x, y, blockCountX, width, height, imageData); + } + } + } + + return imageData; + } + + private static void DecompressDxt3Block(BinaryReader imageReader, int x, int y, int blockCountX, int width, int height, byte[] imageData) + { + byte a0 = imageReader.ReadByte(); + byte a1 = imageReader.ReadByte(); + byte a2 = imageReader.ReadByte(); + byte a3 = imageReader.ReadByte(); + byte a4 = imageReader.ReadByte(); + byte a5 = imageReader.ReadByte(); + byte a6 = imageReader.ReadByte(); + byte a7 = imageReader.ReadByte(); + + ushort c0 = imageReader.ReadUInt16(); + ushort c1 = imageReader.ReadUInt16(); + + byte r0, g0, b0; + byte r1, g1, b1; + ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); + ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); + + uint lookupTable = imageReader.ReadUInt32(); + + int alphaIndex = 0; + for (int blockY = 0; blockY < 4; blockY++) + { + for (int blockX = 0; blockX < 4; blockX++) + { + byte r = 0, g = 0, b = 0, a = 0; + + uint index = (lookupTable >> 2 * (4 * blockY + blockX)) & 0x03; + + switch (alphaIndex) + { + case 0: + a = (byte)((a0 & 0x0F) | ((a0 & 0x0F) << 4)); + break; + case 1: + a = (byte)((a0 & 0xF0) | ((a0 & 0xF0) >> 4)); + break; + case 2: + a = (byte)((a1 & 0x0F) | ((a1 & 0x0F) << 4)); + break; + case 3: + a = (byte)((a1 & 0xF0) | ((a1 & 0xF0) >> 4)); + break; + case 4: + a = (byte)((a2 & 0x0F) | ((a2 & 0x0F) << 4)); + break; + case 5: + a = (byte)((a2 & 0xF0) | ((a2 & 0xF0) >> 4)); + break; + case 6: + a = (byte)((a3 & 0x0F) | ((a3 & 0x0F) << 4)); + break; + case 7: + a = (byte)((a3 & 0xF0) | ((a3 & 0xF0) >> 4)); + break; + case 8: + a = (byte)((a4 & 0x0F) | ((a4 & 0x0F) << 4)); + break; + case 9: + a = (byte)((a4 & 0xF0) | ((a4 & 0xF0) >> 4)); + break; + case 10: + a = (byte)((a5 & 0x0F) | ((a5 & 0x0F) << 4)); + break; + case 11: + a = (byte)((a5 & 0xF0) | ((a5 & 0xF0) >> 4)); + break; + case 12: + a = (byte)((a6 & 0x0F) | ((a6 & 0x0F) << 4)); + break; + case 13: + a = (byte)((a6 & 0xF0) | ((a6 & 0xF0) >> 4)); + break; + case 14: + a = (byte)((a7 & 0x0F) | ((a7 & 0x0F) << 4)); + break; + case 15: + a = (byte)((a7 & 0xF0) | ((a7 & 0xF0) >> 4)); + break; + } + ++alphaIndex; + + switch (index) + { + case 0: + r = r0; + g = g0; + b = b0; + break; + case 1: + r = r1; + g = g1; + b = b1; + break; + case 2: + r = (byte)((2 * r0 + r1) / 3); + g = (byte)((2 * g0 + g1) / 3); + b = (byte)((2 * b0 + b1) / 3); + break; + case 3: + r = (byte)((r0 + 2 * r1) / 3); + g = (byte)((g0 + 2 * g1) / 3); + b = (byte)((b0 + 2 * b1) / 3); + break; + } + + int px = (x << 2) + blockX; + int py = (y << 2) + blockY; + if ((px < width) && (py < height)) + { + int offset = ((py * width) + px) << 2; + imageData[offset] = r; + imageData[offset + 1] = g; + imageData[offset + 2] = b; + imageData[offset + 3] = a; + } + } + } + } + + internal static byte[] DecompressDxt5(byte[] imageData, int width, int height) + { + using (MemoryStream imageStream = new MemoryStream(imageData)) + return DecompressDxt5(imageStream, width, height); + } + + internal static byte[] DecompressDxt5(Stream imageStream, int width, int height) + { + byte[] imageData = new byte[width * height * 4]; + + using (BinaryReader imageReader = new BinaryReader(imageStream)) + { + int blockCountX = (width + 3) / 4; + int blockCountY = (height + 3) / 4; + + for (int y = 0; y < blockCountY; y++) + { + for (int x = 0; x < blockCountX; x++) + { + DecompressDxt5Block(imageReader, x, y, blockCountX, width, height, imageData); + } + } + } + + return imageData; + } + + private static void DecompressDxt5Block(BinaryReader imageReader, int x, int y, int blockCountX, int width, int height, byte[] imageData) + { + byte alpha0 = imageReader.ReadByte(); + byte alpha1 = imageReader.ReadByte(); + + ulong alphaMask = (ulong)imageReader.ReadByte(); + alphaMask += (ulong)imageReader.ReadByte() << 8; + alphaMask += (ulong)imageReader.ReadByte() << 16; + alphaMask += (ulong)imageReader.ReadByte() << 24; + alphaMask += (ulong)imageReader.ReadByte() << 32; + alphaMask += (ulong)imageReader.ReadByte() << 40; + + ushort c0 = imageReader.ReadUInt16(); + ushort c1 = imageReader.ReadUInt16(); + + byte r0, g0, b0; + byte r1, g1, b1; + ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); + ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); + + uint lookupTable = imageReader.ReadUInt32(); + + for (int blockY = 0; blockY < 4; blockY++) + { + for (int blockX = 0; blockX < 4; blockX++) + { + byte r = 0, g = 0, b = 0, a = 255; + uint index = (lookupTable >> 2 * (4 * blockY + blockX)) & 0x03; + + uint alphaIndex = (uint)((alphaMask >> 3 * (4 * blockY + blockX)) & 0x07); + if (alphaIndex == 0) + { + a = alpha0; + } + else if (alphaIndex == 1) + { + a = alpha1; + } + else if (alpha0 > alpha1) + { + a = (byte)(((8 - alphaIndex) * alpha0 + (alphaIndex - 1) * alpha1) / 7); + } + else if (alphaIndex == 6) + { + a = 0; + } + else if (alphaIndex == 7) + { + a = 0xff; + } + else + { + a = (byte)(((6 - alphaIndex) * alpha0 + (alphaIndex - 1) * alpha1) / 5); + } + + switch (index) + { + case 0: + r = r0; + g = g0; + b = b0; + break; + case 1: + r = r1; + g = g1; + b = b1; + break; + case 2: + r = (byte)((2 * r0 + r1) / 3); + g = (byte)((2 * g0 + g1) / 3); + b = (byte)((2 * b0 + b1) / 3); + break; + case 3: + r = (byte)((r0 + 2 * r1) / 3); + g = (byte)((g0 + 2 * g1) / 3); + b = (byte)((b0 + 2 * b1) / 3); + break; + } + + int px = (x << 2) + blockX; + int py = (y << 2) + blockY; + if ((px < width) && (py < height)) + { + int offset = ((py * width) + px) << 2; + imageData[offset] = r; + imageData[offset + 1] = g; + imageData[offset + 2] = b; + imageData[offset + 3] = a; + } + } + } + } + + public static void ConvertRgb565ToRgb888(ushort color, out byte r, out byte g, out byte b) + { + int temp; + + temp = (color >> 11) * 255 + 16; + r = (byte)((temp / 32 + temp) / 32); + temp = ((color & 0x07E0) >> 5) * 255 + 32; + g = (byte)((temp / 64 + temp) / 64); + temp = (color & 0x001F) * 255 + 16; + b = (byte)((temp / 32 + temp) / 32); + } + + public static void ConvertArgb4444ToArgb8888(ushort color, out byte a, out byte r, out byte g, out byte b) + { + int temp; + + temp = (color & 0xf000) >> 12; + a = (byte)(temp * 16 + temp); + + temp = (color & 0x0f00) >> 8; + r = (byte)(temp * 16 + temp); + + temp = (color & 0x00f0) >> 4; + g = (byte)(temp * 16 + temp); + + temp = color & 0x000f; + b = (byte)(temp * 16 + temp); + } + } +} + diff --git a/gitadora-textool/Program.cs b/gitadora-textool/Program.cs new file mode 100644 index 0000000..d8bb021 --- /dev/null +++ b/gitadora-textool/Program.cs @@ -0,0 +1,530 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.Linq.Expressions; +using System.Runtime.InteropServices; +using Microsoft.Xna.Framework.Graphics; + +namespace gitadora_textool +{ + public struct RectInfo + { + public string ExternalFilename; + public string Filename; + public ushort X; + public ushort Y; + public ushort W; + public ushort H; + } + + public class Program + { + static int ReadInt32(BinaryReader reader, bool endianness = false) + { + var data = reader.ReadBytes(4); + if (!endianness) + Array.Reverse(data); + return BitConverter.ToInt32(data, 0); + } + + static int ReadInt16(BinaryReader reader, bool endianness = false) + { + var data = reader.ReadBytes(2); + if (!endianness) + Array.Reverse(data); + return BitConverter.ToInt16(data, 0); + } + + static void WriteInt32(BinaryWriter writer, uint input, bool endianness = false) + { + var data = BitConverter.GetBytes(input); + if (!endianness) + Array.Reverse(data); + writer.Write(data); + } + + static void WriteInt16(BinaryWriter writer, ushort input, bool endianness = false) + { + var data = BitConverter.GetBytes(input); + if (!endianness) + Array.Reverse(data); + writer.Write(data); + } + + + public static byte[] ExtractImageCore(BinaryReader reader, RectInfo? rectInfoList = null) + { + + var magic = Encoding.ASCII.GetString(reader.ReadBytes(4)); + var endianCheck1 = reader.ReadInt32(); + var endianCheck2 = reader.ReadInt32(); + var requiresEndianFix = endianCheck2 == 0x00010100; + + reader.BaseStream.Seek(0x0c, SeekOrigin.Begin); + + var dataSize = ReadInt32(reader, requiresEndianFix) - 0x40; + var width = ReadInt16(reader, requiresEndianFix); + var height = ReadInt16(reader, requiresEndianFix); + + if (!requiresEndianFix) + reader.BaseStream.Seek(0x03, SeekOrigin.Current); + + + /* + GRAYSCALE_FORMAT 0x01 + GRAYSCALE_FORMAT_2 0x06 + BGR_16BIT_FORMAT 0x0C + BGRA_16BIT_FORMAT 0x0D + BGR_FORMAT 0x0E + BGRA_FORMAT 0x10 + BGR_8BIT_FORMAT 0x12 + DXT1_FORMAT 0x16 + DXT3_FORMAT 0x18 + DXT5_FORMAT 0x1A + */ + + var dataFormat = reader.ReadByte(); + + reader.BaseStream.Seek(0x40, SeekOrigin.Begin); + var bitmapData = reader.ReadBytes(dataSize); + + var pixelFormat = PixelFormat.Undefined; + if (dataFormat == 0x01) + { + // GRAYSCALE_FORMAT8 + pixelFormat = PixelFormat.Format8bppIndexed; + } + else if (dataFormat == 0x06) + { + // GRAYSCALE_FORMAT_2 + pixelFormat = PixelFormat.Format8bppIndexed; + } + else if (dataFormat == 0x0c) + { + // BGR_16BIT_FORMAT + pixelFormat = PixelFormat.Format16bppRgb565; + } + else if (dataFormat == 0x0d) + { + // BGRA_16BIT_FORMAT + byte[] newBitmapData = new byte[width * height * 4]; + for (int didx = 0, i = 0; i < height; i++) + { + for (var j = 0; j < width; j++, didx += 2) + { + ushort c = (ushort)((bitmapData[didx + 1] << 8) | bitmapData[didx]); + + DxtUtil.ConvertArgb4444ToArgb8888(c, out var a, out var r, out var g, out var b); + + newBitmapData[(j * 4) + (i * width * 4)] = a; + newBitmapData[(j * 4) + 1 + (i * width * 4)] = r; + newBitmapData[(j * 4) + 2 + (i * width * 4)] = g; + newBitmapData[(j * 4) + 3 + (i * width * 4)] = b; + } + } + + bitmapData = newBitmapData; + + pixelFormat = PixelFormat.Format32bppArgb; + } + else if (dataFormat == 0x0e) + { + // BGR_FORMAT + pixelFormat = PixelFormat.Format24bppRgb; + } + else if (dataFormat == 0x10) + { + // BGRA_FORMAT + pixelFormat = PixelFormat.Format32bppArgb; + } + else if (dataFormat == 0x12) + { + // BGR_8BIT_FORMAT + throw new Exception("Found BGR_8BIT_FORMAT"); + } + else if (dataFormat == 0x16) + { + // DXT1_FORMAT + pixelFormat = PixelFormat.Format32bppArgb; + bitmapData = DxtUtil.DecompressDxt1(bitmapData, width, height); + } + else if (dataFormat == 0x18) + { + // DXT3_FORMAT + pixelFormat = PixelFormat.Format32bppArgb; + bitmapData = DxtUtil.DecompressDxt3(bitmapData, width, height); + } + else if (dataFormat == 0x1a) + { + // DXT5_FORMAT + pixelFormat = PixelFormat.Format32bppArgb; + bitmapData = DxtUtil.DecompressDxt5(bitmapData, width, height); + } + else + { + throw new Exception("Found unknown pixel format"); + } + + for (int i = 0; i < bitmapData.Length;) + { + if (pixelFormat == PixelFormat.Format16bppArgb1555) + { + var a = bitmapData[i] & 0x0f; + var r = (bitmapData[i] >> 4) & 0x0f; + var g = bitmapData[i + 1] & 0x0f; + var b = (bitmapData[i + 1] >> 4) & 0x0f; + + bitmapData[i + 1] = (byte)((a << 4) | b); + bitmapData[i + 0] = (byte)((g << 4) | r); + + i += 2; + } + else if (pixelFormat == PixelFormat.Format16bppRgb565) + { + bitmapData[i] = (byte)((bitmapData[i] & 0xc0) | (bitmapData[i] & 0x3f) >> 1); + i += 2; + } + else if (pixelFormat == PixelFormat.Format24bppRgb) + { + var t = bitmapData[i + 2]; + bitmapData[i + 2] = bitmapData[i]; + bitmapData[i] = t; + i += 3; + } + else if (pixelFormat == PixelFormat.Format32bppArgb) + { + var t = bitmapData[i + 2]; + bitmapData[i + 2] = bitmapData[i]; + bitmapData[i] = t; + i += 4; + } + else + { + break; + } + } + + if (pixelFormat == PixelFormat.Undefined + || pixelFormat == PixelFormat.Format16bppArgb1555) + { + // Create DDS file + var output = new List(); + + output.AddRange(new byte[] + { + 0x44, 0x44, 0x53, 0x20, 0x7C, 0x00, 0x00, 0x00, 0x07, 0x10, 0x08, 0x00 + }); + + output.AddRange(BitConverter.GetBytes(height)); + output.AddRange(BitConverter.GetBytes(width)); + output.AddRange(BitConverter.GetBytes(dataSize)); + + output.AddRange(new byte[] + { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00 + }); + + if (pixelFormat == PixelFormat.Format16bppArgb1555) + { + output.AddRange(new byte[] + { + 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }); + } + else if (pixelFormat == PixelFormat.Format16bppRgb555) + { + output.AddRange(new byte[] + { + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, + 0xE0, 0x07, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }); + } + else + { + output.AddRange(new byte[] + { + 0x04, 0x00, 0x00, 0x00 + }); + + if (dataFormat == 0x16) + { + output.AddRange(Encoding.ASCII.GetBytes("DXT1")); + } + else if (dataFormat == 0x18) + { + output.AddRange(Encoding.ASCII.GetBytes("DXT3")); + } + else if (dataFormat == 0x1a) + { + output.AddRange(Encoding.ASCII.GetBytes("DXT5")); + } + + output.AddRange(new byte[] + { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }); + } + + output.AddRange(bitmapData); + + return output.ToArray(); + } + else + { + var b = new Bitmap(width, height, pixelFormat); + + if (pixelFormat == PixelFormat.Format8bppIndexed) + { + ColorPalette palette = b.Palette; + Color[] entries = palette.Entries; + for (int i = 0; i < 256; i++) + { + Color c = Color.FromArgb((byte)i, (byte)i, (byte)i); + entries[i] = c; + } + b.Palette = palette; + } + + var boundsRect = new Rectangle(0, 0, width, height); + BitmapData bmpData = b.LockBits(boundsRect, + ImageLockMode.WriteOnly, + b.PixelFormat); + + IntPtr ptr = bmpData.Scan0; + + if (pixelFormat != PixelFormat.Format24bppRgb) + { + int bytes = bmpData.Stride * b.Height; + Marshal.Copy(bitmapData, 0, ptr, bytes); + } + else + { + // Because things are stupid, we have to pad the lines for 24bit images ourself... + for (int i = 0; i < height; i++) + { + Marshal.Copy(bitmapData, i * width * 3, ptr + (bmpData.Stride * i), width * 3); + } + } + + b.UnlockBits(bmpData); + + // Split into separate smaller bitmap + if (rectInfoList != null) + { + var rect = new Rectangle(rectInfoList.Value.X, rectInfoList.Value.Y, rectInfoList.Value.W, rectInfoList.Value.H); + Bitmap subimage = new Bitmap(rect.Width, rect.Height); + + Console.WriteLine(rect); + + using (Graphics g = Graphics.FromImage(subimage)) + { + g.DrawImage(b, new Rectangle(0, 0, subimage.Width, subimage.Height), rect, GraphicsUnit.Pixel); + } + + b = subimage; + } + + ImageConverter converter = new ImageConverter(); + return (byte[])converter.ConvertTo(b, typeof(byte[])); + } + } + + static void ExtractImage(string filename) + { + using (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open))) + { + var data = ExtractImageCore(reader); + + var ext = "png"; + if (data[0] == 'D' && data[1] == 'D' && data[2] == 'S' && data[3] == ' ') + { + ext = "dds"; + } + + string outputFilename = String.Format("{0}.{1}", Path.Combine(Path.GetDirectoryName(filename), Path.GetFileNameWithoutExtension(filename)), ext); + Console.WriteLine(outputFilename); + File.WriteAllBytes(outputFilename, data); + } + } + + public static byte[] CreateImageCore(byte[] data, bool requiresEndianFix = false) + { + var image = Bitmap.FromStream(new MemoryStream(data)); + var bmpOrig = new Bitmap(image); + + var pixelFormat = image.PixelFormat; + + if (pixelFormat == PixelFormat.Format8bppIndexed) + { + pixelFormat = PixelFormat.Format32bppArgb; + } + + var bmp = new Bitmap(bmpOrig.Width, bmpOrig.Height, pixelFormat); + using (Graphics gr = Graphics.FromImage(bmp)) + { + gr.DrawImage(bmpOrig, new Rectangle(0, 0, bmp.Width, bmp.Height)); + } + + var bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, + pixelFormat); + + IntPtr ptr = bmpData.Scan0; + int bytes = Math.Abs(bmpData.Stride) * bmp.Height; + byte[] rawData = new byte[bytes]; + + Marshal.Copy(ptr, rawData, 0, bytes); + + bmp.UnlockBits(bmpData); + + var stream = new MemoryStream(); + using (BinaryWriter writer = new BinaryWriter(stream)) + { + Func writeBytes = delegate (byte[] inputData) + { + writer.Write(requiresEndianFix ? inputData.Reverse().ToArray() : inputData); + return true; + }; + + writeBytes(new byte[] { 0x54, 0x58, 0x44, 0x54 }); + if (requiresEndianFix) + { + writeBytes(new byte[] { 0x00, 0x01, 0x00, 0x00 }); + writeBytes(new byte[] { 0x00, 0x01, 0x01, 0x00 }); + } + else + { + writeBytes(new byte[] { 0x00, 0x01, 0x02, 0x00 }); + writeBytes(new byte[] { 0x00, 0x01, 0x02, 0x00 }); + } + + WriteInt32(writer, (uint)(rawData.Length + 0x40), requiresEndianFix); + WriteInt16(writer, (ushort)(bmp.Width), requiresEndianFix); + WriteInt16(writer, (ushort)(bmp.Height), requiresEndianFix); + + if (bmp.PixelFormat == PixelFormat.Format24bppRgb) + { + if (requiresEndianFix) + { + writeBytes(new byte[] { 0x11, 0x22, 0x10, 0x0E }); + } + else + { + writeBytes(new byte[] { 0x11, 0x11, 0x10, 0x0E }); + } + } + else if (bmp.PixelFormat == PixelFormat.Format32bppArgb) + { + if (requiresEndianFix) + { + writeBytes(new byte[] { 0x11, 0x22, 0x10, 0x10 }); + } + else + { + writeBytes(new byte[] { 0x11, 0x11, 0x10, 0x10 }); + } + } + else + { + Console.WriteLine("Expected 24bit or 32bit image. Don't know how to handle pixel format {0}", bmp.PixelFormat); + Environment.Exit(1); + } + + for (int i = 0; i < 0x14; i++) + { + writer.Write((byte)0x00); + } + + if (bmp.PixelFormat == PixelFormat.Format24bppRgb) + { + WriteInt32(writer, 0x01, requiresEndianFix); + } + else if (bmp.PixelFormat == PixelFormat.Format32bppArgb) + { + WriteInt32(writer, 0x03, requiresEndianFix); + } + + for (int i = 0; i < 0x10; i++) + { + writer.Write((byte)0x00); + } + + var bpp = bmp.PixelFormat == PixelFormat.Format32bppArgb ? 4 : 3; + for (int i = 0; i < rawData.Length; i += bpp) + { + var t = rawData[i]; + rawData[i] = rawData[i + 2]; + rawData[i + 2] = t; + } + + writer.Write(rawData); + } + + var outputData = stream.GetBuffer(); + Array.Resize(ref outputData, bytes + 0x40); + return outputData; + } + + static void CreateImage(string filename) + { + string outputFilename = String.Format("{0}.tex", Path.Combine(Path.GetDirectoryName(filename), Path.GetFileNameWithoutExtension(filename))); + Console.WriteLine(outputFilename); + + var rawData = File.ReadAllBytes(filename); + var data = CreateImageCore(rawData, false); + File.WriteAllBytes(outputFilename, data); + } + + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine("usage: {0} input_filename", AppDomain.CurrentDomain.FriendlyName); + return; + } + + foreach (var filename in args) + { + var isExtract = false; + using (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open))) + { + var magic = reader.ReadBytes(4); + if (magic.SequenceEqual(new byte[] { 0x54, 0x58, 0x44, 0x54 }) + || magic.SequenceEqual(new byte[] { 0x54, 0x44, 0x58, 0x54 })) + { + isExtract = true; + } + } + + try + { + if (isExtract) + { + ExtractImage(filename); + } + else + { + CreateImage(filename); + } + } + catch (Exception e) + { + Console.WriteLine("Error occurred: {0}", e.Message); + Environment.Exit(1); + } + } + } + } +} diff --git a/gitadora-textool/Properties/AssemblyInfo.cs b/gitadora-textool/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dee5efb --- /dev/null +++ b/gitadora-textool/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("gitadora-textool")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("gitadora-textool")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8c5cc09a-436c-4caa-9213-31a4f351dbdc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/gitadora-textool/gitadora-textool.csproj b/gitadora-textool/gitadora-textool.csproj new file mode 100644 index 0000000..4063a58 --- /dev/null +++ b/gitadora-textool/gitadora-textool.csproj @@ -0,0 +1,55 @@ + + + + + Debug + AnyCPU + {8C5CC09A-436C-4CAA-9213-31A4F351DBDC} + Exe + gitadora_textool + gitadora-textool + v4.6.1 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + none + true + bin\Release\ + TRACE + prompt + 4 + true + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file