commit 0f86b0434b1ca9ddb1fed5f17f99b9fb333da81a Author: Yukai Li Date: Tue Feb 16 01:04:29 2021 -0700 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ce6fdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,340 @@ +## 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 +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +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/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.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 +*- Backup*.rdl + +# 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 personal settings +.cr/personal + +# 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/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4072538 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libktxsharp"] + path = libktxsharp + url = https://github.com/mcraiha/libktxsharp.git diff --git a/LibDgf.Graphics/Aqualead/Image/Conversion/IAlImageConverter.cs b/LibDgf.Graphics/Aqualead/Image/Conversion/IAlImageConverter.cs new file mode 100644 index 0000000..480628e --- /dev/null +++ b/LibDgf.Graphics/Aqualead/Image/Conversion/IAlImageConverter.cs @@ -0,0 +1,17 @@ +using LibDgf.Aqualead.Image; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead.Image.Conversion +{ + public interface IAlImageConverter + { + string FileExtension { get; } + bool HasAlternativeFile(AlImage image); + bool CanConvert(string pixelFormat); + void ConvertFromAl(AlImage image, Stream destStream); + void ConvertFromAlAlt(AlImage image, Stream destStream); + } +} diff --git a/LibDgf.Graphics/Aqualead/Image/Conversion/KtxConverter.cs b/LibDgf.Graphics/Aqualead/Image/Conversion/KtxConverter.cs new file mode 100644 index 0000000..775dde2 --- /dev/null +++ b/LibDgf.Graphics/Aqualead/Image/Conversion/KtxConverter.cs @@ -0,0 +1,70 @@ +using LibDgf.Aqualead.Image; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using KtxSharp; + +namespace LibDgf.Aqualead.Image.Conversion +{ + public class KtxConverter : IAlImageConverter + { + public string FileExtension => ".ktx"; + + public void ConvertFromAl(AlImage image, Stream destStream) + { + List mips; + if (image.PixelFormat == "ETC1") + { + mips = image.Mipmaps; + } + else if (image.PixelFormat == "EC1A") + { + // Give first half of the mips + mips = new List(); + foreach (var mip in image.Mipmaps) + { + byte[] newMip = new byte[mip.Length / 2]; + Buffer.BlockCopy(mip, 0, newMip, 0, newMip.Length); + mips.Add(newMip); + } + } + else + { + throw new ArgumentException("Pixel format not supported.", nameof(image)); + } + var ktx = KtxCreator.Create(GlDataType.Compressed, GlPixelFormat.GL_RGB, GlInternalFormat.GL_ETC1_RGB8_OES, + image.Width, image.Height, mips, new Dictionary()); + KtxWriter.WriteTo(ktx, destStream); + } + + public void ConvertFromAlAlt(AlImage image, Stream destStream) + { + if (image.PixelFormat != "EC1A") + throw new ArgumentException("Pixel format does not have alternate representation.", nameof(image)); + + // Give second half of the mips + var mips = new List(); + foreach (var mip in image.Mipmaps) + { + byte[] newMip = new byte[mip.Length / 2]; + Buffer.BlockCopy(mip, newMip.Length, newMip, 0, newMip.Length); + mips.Add(newMip); + } + + var ktx = KtxCreator.Create(GlDataType.Compressed, GlPixelFormat.GL_RGB, GlInternalFormat.GL_ETC1_RGB8_OES, + image.Width, image.Height, mips, new Dictionary()); + KtxWriter.WriteTo(ktx, destStream); + } + + public bool CanConvert(string pixelFormat) + { + return pixelFormat == "EC1A" || pixelFormat == "ETC1"; + } + + public bool HasAlternativeFile(AlImage image) + { + return image.PixelFormat == "EC1A"; + } + } +} diff --git a/LibDgf.Graphics/Aqualead/Image/Conversion/PkmConverter.cs b/LibDgf.Graphics/Aqualead/Image/Conversion/PkmConverter.cs new file mode 100644 index 0000000..6380c23 --- /dev/null +++ b/LibDgf.Graphics/Aqualead/Image/Conversion/PkmConverter.cs @@ -0,0 +1,78 @@ +using LibDgf.Aqualead.Image; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead.Image.Conversion +{ + public class PkmConverter : IAlImageConverter + { + public string FileExtension => ".pkm"; + + public void ConvertFromAl(AlImage image, Stream destStream) + { + List mips; + if (image.PixelFormat == "ETC1") + { + mips = image.Mipmaps; + } + else if (image.PixelFormat == "EC1A") + { + // Give first half of the mips + mips = new List(); + byte[] newMip = new byte[image.Mipmaps[0].Length / 2]; + Buffer.BlockCopy(image.Mipmaps[0], 0, newMip, 0, newMip.Length); + mips.Add(newMip); + } + else + { + throw new ArgumentException("Pixel format not supported.", nameof(image)); + } + + WritePkm(destStream, mips[0], image.Width, image.Height); + } + + public void ConvertFromAlAlt(AlImage image, Stream destStream) + { + if (image.PixelFormat != "EC1A") + throw new ArgumentException("Pixel format does not have alternate representation.", nameof(image)); + + // Give second half of the mips + byte[] newMip = new byte[image.Mipmaps[0].Length / 2]; + Buffer.BlockCopy(image.Mipmaps[0], newMip.Length, newMip, 0, newMip.Length); + + WritePkm(destStream, newMip, image.Width, image.Height); + } + + public bool CanConvert(string pixelFormat) + { + return pixelFormat == "EC1A" || pixelFormat == "ETC1"; + } + + public bool HasAlternativeFile(AlImage image) + { + return image.PixelFormat == "EC1A"; + } + + const ushort ETC1_RGB_NO_MIPMAPS = 0; + + static void WritePkm(Stream stream, byte[] data, uint width, uint height) + { + BinaryWriter bw = new BinaryWriter(stream); + bw.Write("PKM 10".ToCharArray()); + WriteBeUInt16(bw, ETC1_RGB_NO_MIPMAPS); + WriteBeUInt16(bw, (ushort)((width + 3) & ~3)); + WriteBeUInt16(bw, (ushort)((height + 3) & ~3)); + WriteBeUInt16(bw, (ushort)width); + WriteBeUInt16(bw, (ushort)height); + bw.Write(data); + } + + static void WriteBeUInt16(BinaryWriter bw, ushort value) + { + bw.Write((byte)(value >> 8)); + bw.Write((byte)value); + } + } +} diff --git a/LibDgf.Graphics/Aqualead/Image/Conversion/PngConverter.cs b/LibDgf.Graphics/Aqualead/Image/Conversion/PngConverter.cs new file mode 100644 index 0000000..421755e --- /dev/null +++ b/LibDgf.Graphics/Aqualead/Image/Conversion/PngConverter.cs @@ -0,0 +1,62 @@ +using LibDgf.Aqualead.Image; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead.Image.Conversion +{ + public class PngConverter : IAlImageConverter + { + public string FileExtension => ".png"; + + public void ConvertFromAl(AlImage image, Stream destStream) + { + // Only grab the first mip + var pixels = image.Mipmaps[0]; + using (MemoryStream ms = new MemoryStream(pixels)) + { + BinaryReader br = new BinaryReader(ms); + using (var img = ConvertBgra32(br, (int)image.Width, (int)image.Height)) + { + img.SaveAsPng(destStream); + } + } + } + + public bool CanConvert(string pixelFormat) + { + return pixelFormat == "BGRA"; + } + + public static Image ConvertBgra32(BinaryReader br, int width, int height) + { + Image img = new Image(width, height); + for (int y = 0; y < height; ++y) + { + var row = img.GetPixelRowSpan(y); + for (int x = 0; x < width; ++x) + { + byte b = br.ReadByte(); + byte g = br.ReadByte(); + byte r = br.ReadByte(); + byte a = br.ReadByte(); + row[x] = new Bgra32(r, g, b, a); + } + } + return img; + } + + public void ConvertFromAlAlt(AlImage image, Stream destStream) + { + throw new NotSupportedException(); + } + + public bool HasAlternativeFile(AlImage image) + { + return false; + } + } +} diff --git a/LibDgf.Graphics/LibDgf.Graphics.csproj b/LibDgf.Graphics/LibDgf.Graphics.csproj new file mode 100644 index 0000000..e99bee4 --- /dev/null +++ b/LibDgf.Graphics/LibDgf.Graphics.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/LibDgf.Graphics/TxmConversion.cs b/LibDgf.Graphics/TxmConversion.cs new file mode 100644 index 0000000..71ecf8c --- /dev/null +++ b/LibDgf.Graphics/TxmConversion.cs @@ -0,0 +1,310 @@ +using LibDgf.Txm; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Graphics +{ + public static class TxmConversion + { + public static void ConvertImageToTxm(string inPath, Stream outStream, byte level = 1, short bufferBase = 0, short paletteBufferBase = 0) + { + using (var image = Image.Load(inPath)) + { + // Gather all colors to see if it would fit in PSMT8 + TxmPixelFormat pixelFormat = TxmPixelFormat.None; + HashSet colorSet = new HashSet(); + List palette = null; + for (int y = 0; y < image.Height; ++y) + { + var row = image.GetPixelRowSpan(y); + for (int x = 0; x < image.Width; ++x) + { + colorSet.Add(row[x]); + if (colorSet.Count > 256) + { + pixelFormat = TxmPixelFormat.PSMCT32; + y = image.Height; + break; + } + } + } + + short paletteWidth = 0; + short paletteHeight = 0; + if (pixelFormat == TxmPixelFormat.None) + { + // Palette check passed, assign palettized pixel format + if (colorSet.Count > 16) + { + pixelFormat = TxmPixelFormat.PSMT8; + paletteWidth = 16; + paletteHeight = 16; + } + else + { + pixelFormat = TxmPixelFormat.PSMT4; + paletteWidth = 8; + paletteHeight = 2; + } + palette = new List(colorSet); + } + + // Write header + BinaryWriter bw = new BinaryWriter(outStream); + TxmHeader txmHeader = new TxmHeader + { + ImageSourcePixelFormat = pixelFormat, + ImageVideoPixelFormat = pixelFormat, + ImageWidth = (short)image.Width, + ImageHeight = (short)image.Height, + ImageBufferBase = bufferBase, + ClutPixelFormat = palette != null ? TxmPixelFormat.PSMCT32 : TxmPixelFormat.None, + Misc = (byte)(level & 0x0f), + ClutWidth = paletteWidth, + ClutHeight = paletteHeight, + ClutBufferBase = paletteBufferBase + }; + txmHeader.Write(bw); + + // Write palette + int palettePixelsWritten = 0; + if (pixelFormat == TxmPixelFormat.PSMT4) + { + foreach (var color in palette) + { + bw.Write(color.R); + bw.Write(color.G); + bw.Write(color.B); + bw.Write((byte)((color.A + 1) >> 1)); + ++palettePixelsWritten; + } + } + else if (pixelFormat == TxmPixelFormat.PSMT8) + { + int baseOffset = 0; + Rgba32 black = new Rgba32(); + + int[] order = new int[] + { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + }; + + while (palettePixelsWritten < palette.Count) + { + foreach (var offset in order) + { + var palOffset = baseOffset + offset; + var color = palOffset < palette.Count ? palette[palOffset] : black; + bw.Write(color.R); + bw.Write(color.G); + bw.Write(color.B); + bw.Write((byte)((color.A + 1) >> 1)); + ++palettePixelsWritten; + } + + baseOffset += order.Length; + } + } + + // Pad out rest of palette + int targetOffset = 16 + (txmHeader.GetClutByteSize() + 15) / 16 * 16; + while (outStream.Position < targetOffset) + { + bw.Write((byte)0); + } + + // Write main image data + byte pal4BppBuffer = 0; + bool odd = false; + for (int y = 0; y < image.Height; ++y) + { + var row = image.GetPixelRowSpan(y); + for (int x = 0; x < image.Width; ++x) + { + var pixel = row[x]; + if (pixelFormat == TxmPixelFormat.PSMCT32) + { + bw.Write(pixel.R); + bw.Write(pixel.G); + bw.Write(pixel.B); + bw.Write((byte)((pixel.A + 1) >> 1)); + } + else + { + var palIndex = palette.IndexOf(pixel); + if (pixelFormat == TxmPixelFormat.PSMT4) + { + pal4BppBuffer <<= 4; + pal4BppBuffer |= (byte)(palIndex & 0x0f); + odd = !odd; + if (!odd) + { + bw.Write(pal4BppBuffer); + pal4BppBuffer = 0; + } + } + else + { + bw.Write((byte)palIndex); + } + } + } + } + } + } + + public static void ConvertTxmToPng(Stream stream, string outPath) + { + BinaryReader br = new BinaryReader(stream); + TxmHeader imageHeader = new TxmHeader(); + imageHeader.Read(br); + + Console.WriteLine(imageHeader); + if (imageHeader.Misc != 1) + Console.WriteLine("Different level!"); + + Image image; + if (imageHeader.ImageSourcePixelFormat == TxmPixelFormat.PSMT8 || imageHeader.ImageSourcePixelFormat == TxmPixelFormat.PSMT4) + { + Rgba32[] palette = null; + if (imageHeader.ClutPixelFormat == TxmPixelFormat.PSMCT32) + { + stream.Seek(16, SeekOrigin.Begin); + palette = GetRgba32Palette(br, imageHeader.ClutWidth, imageHeader.ClutHeight); + //fs.Seek(16, SeekOrigin.Begin); + //using (var palImage = ConvertTxmRgba32(br, imageHeader.ClutWidth, imageHeader.ClutHeight)) + //{ + // palImage.SaveAsPng(Path.ChangeExtension(outPath, ".pal.png")); + //} + } + else + { + throw new NotSupportedException("Unsupported pixel format from second texture"); + } + + stream.Seek(16 + (imageHeader.GetClutByteSize() + 15) / 16 * 16, SeekOrigin.Begin); + if (imageHeader.ImageSourcePixelFormat == TxmPixelFormat.PSMT8) + { + image = ConvertTxmIndexed8bpp(br, imageHeader.ImageWidth, imageHeader.ImageHeight, palette); + } + else + { + image = ConvertTxmIndexed4bpp(br, imageHeader.ImageWidth, imageHeader.ImageHeight, palette); + } + } + else if (imageHeader.ImageSourcePixelFormat == TxmPixelFormat.PSMCT32) + { + stream.Seek(16, SeekOrigin.Begin); + image = ConvertTxmRgba32(br, imageHeader.ImageWidth, imageHeader.ImageHeight); + } + else + { + throw new NotSupportedException("Unsupported pixel format"); + } + + image.SaveAsPng(outPath); + image.Dispose(); + } + + public static Image ConvertTxmIndexed8bpp(BinaryReader br, int width, int height, Rgba32[] palette) + { + Image img = new Image(width, height); + for (int y = 0; y < height; ++y) + { + var row = img.GetPixelRowSpan(y); + for (int x = 0; x < width; ++x) + { + byte index = br.ReadByte(); + row[x] = palette[index]; + } + } + return img; + } + + public static Image ConvertTxmRgba32(BinaryReader br, int width, int height) + { + Image img = new Image(width, height); + for (int y = 0; y < height; ++y) + { + var row = img.GetPixelRowSpan(y); + for (int x = 0; x < width; ++x) + { + byte r = br.ReadByte(); + byte g = br.ReadByte(); + byte b = br.ReadByte(); + int a = br.ReadByte() * 2; + if (a > 255) a = 255; + row[x] = new Rgba32(r, g, b, (byte)a); + } + } + return img; + } + + + public static Image ConvertTxmIndexed4bpp(BinaryReader br, int width, int height, Rgba32[] palette) + { + Image img = new Image(width, height); + bool odd = false; + byte index = 0; + for (int y = 0; y < height; ++y) + { + var row = img.GetPixelRowSpan(y); + for (int x = 0; x < width; ++x) + { + if (!odd) + { + index = br.ReadByte(); + } + row[x] = palette[index & 0xf]; + index >>= 4; + odd = !odd; + } + } + return img; + } + + static Rgba32[] GetRgba32Palette(BinaryReader br, int width, int height) + { + int count = width * height; + Rgba32[] colors = new Rgba32[count]; + for (int i = 0; i < count; ++i) + { + byte r = br.ReadByte(); + byte g = br.ReadByte(); + byte b = br.ReadByte(); + int a = br.ReadByte() * 2; + if (a > 255) a = 255; + colors[i] = new Rgba32(r, g, b, (byte)a); + } + + if (width == 8 && height == 2) return colors; + + // Reorder by column, left to right and up to down + Rgba32[] reorderedColors = new Rgba32[count]; + int j = 0; + for (int y = 0; y < height; y += 2) + { + for (int x = 0; x < width; x += 8) + { + for (int iy = 0; iy < 2; ++iy) + { + int offset = (y + iy) * width + x; + for (int ix = 0; ix < 8; ++ix) + { + reorderedColors[j++] = colors[offset + ix]; + } + } + } + } + + return reorderedColors; + } + } +} diff --git a/LibDgf.sln b/LibDgf.sln new file mode 100644 index 0000000..c6ec9b9 --- /dev/null +++ b/LibDgf.sln @@ -0,0 +1,42 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30611.23 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibDgf", "LibDgf\LibDgf.csproj", "{583FF596-94B9-4CFA-A95D-E635262EB199}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibDgf.Graphics", "LibDgf.Graphics\LibDgf.Graphics.csproj", "{080BA349-D429-4670-B655-95D7D63F52D3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KtxSharp", "libktxsharp\lib\KtxSharp.csproj", "{15D9B1D3-3603-4114-B4BE-CC228DF6DAEE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C45B6BEE-C63C-43FC-9ECA-BA287EADAD93}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {583FF596-94B9-4CFA-A95D-E635262EB199}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {583FF596-94B9-4CFA-A95D-E635262EB199}.Debug|Any CPU.Build.0 = Debug|Any CPU + {583FF596-94B9-4CFA-A95D-E635262EB199}.Release|Any CPU.ActiveCfg = Release|Any CPU + {583FF596-94B9-4CFA-A95D-E635262EB199}.Release|Any CPU.Build.0 = Release|Any CPU + {080BA349-D429-4670-B655-95D7D63F52D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {080BA349-D429-4670-B655-95D7D63F52D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {080BA349-D429-4670-B655-95D7D63F52D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {080BA349-D429-4670-B655-95D7D63F52D3}.Release|Any CPU.Build.0 = Release|Any CPU + {15D9B1D3-3603-4114-B4BE-CC228DF6DAEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15D9B1D3-3603-4114-B4BE-CC228DF6DAEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15D9B1D3-3603-4114-B4BE-CC228DF6DAEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15D9B1D3-3603-4114-B4BE-CC228DF6DAEE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9A9D70A4-E7C0-4EA3-A231-4126E3B28CE4} + EndGlobalSection +EndGlobal diff --git a/LibDgf/Aqualead/AlLzDecoder.cs b/LibDgf/Aqualead/AlLzDecoder.cs new file mode 100644 index 0000000..fdd47be --- /dev/null +++ b/LibDgf/Aqualead/AlLzDecoder.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead +{ + public class AlLzDecoder + { + uint bitBuffer; + int numBitsRemaining; + BinaryReader br; + + public byte[] Decode(Stream inStream) + { + br = new BinaryReader(inStream); + numBitsRemaining = 0; + + if (new string(br.ReadChars(4)) != "ALLZ") + throw new InvalidDataException("Not an Aqualead LZ compressed file."); + byte version = br.ReadByte(); + if (version > 1) + throw new InvalidDataException("Version too new."); + + byte minLookbackLengthBits = br.ReadByte(); + byte minLookbackPosBits = br.ReadByte(); + byte minLiteralLengthBits = br.ReadByte(); + uint decompressedLength = br.ReadUInt32(); + + byte[] output = new byte[decompressedLength]; + int outPos = 0; + + if (version == 0) ReadBits(1); // Consume literal bit if v0 + bool hasLiteral = true; + + while (true) + { + if (hasLiteral) + { + if (outPos >= decompressedLength) break; + int literalLength = (int)ReadEncodedNum(minLiteralLengthBits) + 1; + if (inStream.Read(output, outPos, literalLength) != literalLength) + throw new IOException("Could not read all bytes requested."); + outPos += literalLength; + //Console.WriteLine("Did literal"); + } + + if (outPos >= decompressedLength) break; + int lookbackPos = (int)ReadEncodedNum(minLookbackPosBits) + 1; + int lookbackLength = (int)ReadEncodedNum(minLookbackLengthBits) + 3; + + for (int i = 0; i < lookbackLength; ++i) + { + output[outPos] = output[outPos - lookbackPos]; + ++outPos; + } + //Console.WriteLine("Did lookback"); + + if (outPos >= decompressedLength) break; + hasLiteral = ReadBits(1) == 0; + //Console.WriteLine("Has literal: " + hasLiteral); + } + + br = null; + return output; + } + + uint ReadEncodedNum(int minNumBits) + { + int numExtBits = CountBits(); + uint num = ReadBits(minNumBits); + if (numExtBits > 0) + { + // Length encoding + // e e e e 0 | xxxx | yy + // Number of es indicate exponent and number of bits for mantissa + // Subsequently, ((2 ^ count(e)) - 1 + xxxx) | yy + num |= (uint)(ReadBits(numExtBits) + (1 << numExtBits) - 1) << minNumBits; + } + return num; + } + + void EnsureBits(int numBits) + { + while (numBitsRemaining < numBits) + { + if (numBitsRemaining + 8 > 32) throw new ArgumentOutOfRangeException(nameof(numBits)); + bitBuffer |= (uint)br.ReadByte() << numBitsRemaining; + numBitsRemaining += 8; + } + } + + uint ReadBits(int numBits) + { + EnsureBits(numBits); + uint output = (uint)(bitBuffer & ((1 << numBits) - 1)); + bitBuffer >>= numBits; + numBitsRemaining -= numBits; + return output; + } + + int CountBits() + { + int i = 0; + while (ReadBits(1) != 0) ++i; + return i; + } + + void WriteBits() + { + + } + } +} diff --git a/LibDgf/Aqualead/Archive/AlAarArchiveFlags.cs b/LibDgf/Aqualead/Archive/AlAarArchiveFlags.cs new file mode 100644 index 0000000..cd8b8ff --- /dev/null +++ b/LibDgf/Aqualead/Archive/AlAarArchiveFlags.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Aqualead.Archive +{ + [Flags] + public enum AlAarArchiveFlags : byte + { + IsValid = 1 << 0, + IsSorted = 1 << 5, + IsUseNameHash = 1 << 6 + } +} diff --git a/LibDgf/Aqualead/Archive/AlAarEntryFlags.cs b/LibDgf/Aqualead/Archive/AlAarEntryFlags.cs new file mode 100644 index 0000000..4dafafc --- /dev/null +++ b/LibDgf/Aqualead/Archive/AlAarEntryFlags.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Aqualead.Archive +{ + [Flags] + public enum AlAarEntryFlags : byte + { + IsResident = 1 << 0, + IsPrepare = 1 << 1, + Unknown2 = 1 << 6, + IsUseName = 1 << 7 + } +} diff --git a/LibDgf/Aqualead/Archive/AlAarEntryV2.cs b/LibDgf/Aqualead/Archive/AlAarEntryV2.cs new file mode 100644 index 0000000..2ff14bd --- /dev/null +++ b/LibDgf/Aqualead/Archive/AlAarEntryV2.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead.Archive +{ + public class AlAarEntryV2 + { + uint rest; + + public uint Id { get; set; } + public uint Offset { get; set; } + public uint Length { get; set; } + public uint Range + { + get + { + return rest & 0xffffff; + } + set + { + if (value > 0xffffff) throw new ArgumentOutOfRangeException(nameof(value)); + rest = value | (rest & 0xff000000); + } + } + public AlAarEntryFlags Flags + { + get + { + return (AlAarEntryFlags)((rest >> 24) & 0xff); + } + set + { + if ((uint)value > 0xff) throw new ArgumentOutOfRangeException(nameof(value)); + rest = ((uint)value << 24) | (rest & 0xffffff); + } + } + public string Name { get; set; } + + public void Read(BinaryReader br) + { + Id = br.ReadUInt32(); + Offset = br.ReadUInt32(); + Length = br.ReadUInt32(); + rest = br.ReadUInt32(); + } + } +} diff --git a/LibDgf/Aqualead/Archive/AlAarHeaderV2.cs b/LibDgf/Aqualead/Archive/AlAarHeaderV2.cs new file mode 100644 index 0000000..e45082a --- /dev/null +++ b/LibDgf/Aqualead/Archive/AlAarHeaderV2.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead.Archive +{ + public class AlAarHeaderV2 + { + // Magic and version omitted, handled in archive class + public AlAarArchiveFlags Flags { get; set; } + public ushort Count { get; set; } + public uint LowId { get; set; } + public uint HighId { get; set; } + + public void Read(BinaryReader br) + { + Flags = (AlAarArchiveFlags)br.ReadByte(); + Count = br.ReadUInt16(); + LowId = br.ReadUInt32(); + HighId = br.ReadUInt32(); + } + } +} diff --git a/LibDgf/Aqualead/Archive/AlArchiveV2.cs b/LibDgf/Aqualead/Archive/AlArchiveV2.cs new file mode 100644 index 0000000..4b7e86b --- /dev/null +++ b/LibDgf/Aqualead/Archive/AlArchiveV2.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Text; +using GMWare.IO; + +namespace LibDgf.Aqualead.Archive +{ + public class AlArchiveV2 : IDisposable + { + AlAarHeaderV2 header; + List entries; + Stream stream; + private bool disposedValue; + + public AlArchiveV2(Stream stream) + { + this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + Load(); + } + + public IList Entries + { + get + { + CheckDisposed(); + return new ReadOnlyCollection(entries); + } + } + + void Load() + { + BinaryReader br = new BinaryReader(stream); + if (new string(br.ReadChars(4)) != "ALAR") + throw new InvalidDataException("Not an AquaLead archive."); + if (br.ReadByte() != 2) + throw new NotSupportedException("Not version 2 archive."); + + header = new AlAarHeaderV2(); + header.Read(br); + + entries = new List(); + for (int i = 0; i < header.Count; ++i) + { + var entry = new AlAarEntryV2(); + entry.Read(br); + entries.Add(entry); + } + + foreach (var entry in entries) + { + if ((entry.Flags & AlAarEntryFlags.IsUseName) != 0) + { + stream.Seek(entry.Offset - 0x22, SeekOrigin.Begin); + entry.Name = StringReadingHelper.ReadNullTerminatedStringFromFixedSizeBlock(br, 0x20, Encoding.UTF8); + } + + if ((entry.Flags & ~AlAarEntryFlags.IsUseName) != 0) + { + Console.WriteLine($"Entry {entry.Name} has other flags set: {entry.Flags}"); + //Debugger.Break(); + } + } + } + + public Stream GetFile(AlAarEntryV2 entry) + { + if (!entries.Contains(entry)) + throw new ArgumentException("Entry not from this archive.", nameof(entry)); + if (stream == null) throw new InvalidOperationException("Archive is not opened from existing."); + CheckDisposed(); + + stream.Seek(entry.Offset, SeekOrigin.Begin); + MemoryStream ms = new MemoryStream(); + StreamUtils.StreamCopy(stream, ms, entry.Length); + ms.Seek(0, SeekOrigin.Begin); + return Utils.CheckDecompress(ms); + } + + void CheckDisposed() + { + if (disposedValue) throw new ObjectDisposedException(GetType().FullName); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + stream.Close(); + } + + entries = null; + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/LibDgf/Aqualead/Image/AlImage.cs b/LibDgf/Aqualead/Image/AlImage.cs new file mode 100644 index 0000000..5feacdc --- /dev/null +++ b/LibDgf/Aqualead/Image/AlImage.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead.Image +{ + public class AlImage + { + string pixelFormat; + + public AlImageMipmapType MipmapType { get; set; } + public AlImageFlags Flags { get; set; } + public string PixelFormat + { + get + { + return pixelFormat; + } + set + { + if (value == null) throw new ArgumentNullException(nameof(value)); + if (value.Length > 4) throw new ArgumentException("String length too long.", nameof(value)); + pixelFormat = value; + } + } + public uint Width { get; set; } + public uint Height { get; set; } + public byte[] PlatformWork { get; set; } + public uint[] PaletteColors { get; set; } + public List Mipmaps { get; set; } + + public void Read(BinaryReader br) + { + var baseOffset = br.BaseStream.Position; + if (new string(br.ReadChars(4)) != "ALIG") + throw new InvalidDataException("Not an AquaLead image."); + MipmapType = (AlImageMipmapType)br.ReadByte(); + Flags = (AlImageFlags)br.ReadByte(); + int numPaletteColors = br.ReadUInt16(); + if ((Flags & AlImageFlags.PaletteAdd64K) != 0) numPaletteColors += 0x10000; + if ((Flags & AlImageFlags.PaletteAdd128K) != 0) numPaletteColors += 0x20000; + PixelFormat = new string(br.ReadChars(4)); + br.ReadUInt32(); + if (MipmapType != AlImageMipmapType.Platform) + { + Width = br.ReadUInt32(); + Height = br.ReadUInt32(); + } + else + { + Width = br.ReadUInt16(); + Height = br.ReadUInt16(); + } + + int numMipmaps = MipmapType == AlImageMipmapType.NoMipmap ? 1 : br.ReadUInt16(); + ushort paletteOffset = br.ReadUInt16(); + ushort platformWorkLength = MipmapType == AlImageMipmapType.Platform ? br.ReadUInt16() : (ushort)0; + + uint[] mipmapOffsets = new uint[numMipmaps + 1]; + if (MipmapType == AlImageMipmapType.NoMipmap) + { + mipmapOffsets[0] = br.ReadUInt16(); + } + else + { + for (int i = 0; i < numMipmaps; ++i) + { + mipmapOffsets[i] = br.ReadUInt32(); + } + } + mipmapOffsets[numMipmaps] = (uint)(br.BaseStream.Length - baseOffset); + + PaletteColors = new uint[numPaletteColors]; + br.BaseStream.Seek(baseOffset + paletteOffset, SeekOrigin.Begin); + for (int i = 0; i < PaletteColors.Length; ++i) + { + PaletteColors[i] = br.ReadUInt32(); + } + + if (MipmapType == AlImageMipmapType.Platform) + { + br.BaseStream.Seek(baseOffset + 0x20, SeekOrigin.Begin); + PlatformWork = br.ReadBytes(platformWorkLength); + } + + Mipmaps = new List(); + for (int i = 0; i < numMipmaps; ++i) + { + br.BaseStream.Seek(baseOffset + mipmapOffsets[i], SeekOrigin.Begin); + Mipmaps.Add(br.ReadBytes((int)(mipmapOffsets[i + 1] - mipmapOffsets[i]))); + } + } + } +} diff --git a/LibDgf/Aqualead/Image/AlImageFlags.cs b/LibDgf/Aqualead/Image/AlImageFlags.cs new file mode 100644 index 0000000..25ad41e --- /dev/null +++ b/LibDgf/Aqualead/Image/AlImageFlags.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Aqualead.Image +{ + [Flags] + public enum AlImageFlags + { + IsUseAlpha = 1 << 0, + PaletteAdd64K = 1 << 4, + PaletteAdd128K = 1 << 5, + } +} diff --git a/LibDgf/Aqualead/Image/AlImageMipmapType.cs b/LibDgf/Aqualead/Image/AlImageMipmapType.cs new file mode 100644 index 0000000..1966522 --- /dev/null +++ b/LibDgf/Aqualead/Image/AlImageMipmapType.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Aqualead.Image +{ + public enum AlImageMipmapType + { + NoMipmap, + HasMipmap, + Platform + } +} diff --git a/LibDgf/Aqualead/Texture/AlPoint.cs b/LibDgf/Aqualead/Texture/AlPoint.cs new file mode 100644 index 0000000..379f66d --- /dev/null +++ b/LibDgf/Aqualead/Texture/AlPoint.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Aqualead.Texture +{ + public struct AlPoint + { + public short X; + public short Y; + } +} diff --git a/LibDgf/Aqualead/Texture/AlTexture.cs b/LibDgf/Aqualead/Texture/AlTexture.cs new file mode 100644 index 0000000..4302f10 --- /dev/null +++ b/LibDgf/Aqualead/Texture/AlTexture.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead.Texture +{ + public class AlTexture + { + public AlTextureFlags Flags { get; set; } + public List ChildTextures { get; set; } = new List(); + public short Width { get; set; } + public short Height { get; set; } + public uint IndirectReference { get; set; } + public byte[] ImageData { get; set; } + + public void Read(BinaryReader br) + { + var baseOffset = br.BaseStream.Position; + + if (new string(br.ReadChars(4)) != "ALTX") + throw new InvalidDataException("Not an Aqualead texture."); + + bool isMultitexture = br.ReadBoolean(); + Flags = (AlTextureFlags)br.ReadByte(); + ushort numTextures = br.ReadUInt16(); + uint imgOffset = br.ReadUInt32(); + ushort[] entryOffsets = new ushort[numTextures]; + for (int i = 0; i < numTextures; ++i) + { + entryOffsets[i] = br.ReadUInt16(); + } + + ChildTextures.Clear(); + uint entryOffset = 0; + for (int i = 0; i < numTextures; ++i) + { + entryOffset += entryOffsets[i]; + br.BaseStream.Seek(baseOffset + entryOffset, SeekOrigin.Begin); + AlTextureEntry entry = new AlTextureEntry(); + entry.Read(br, isMultitexture); + ChildTextures.Add(entry); + } + + if ((Flags & AlTextureFlags.IsSpecial) != 0) + { + if ((Flags & AlTextureFlags.IsIndirect) != 0) + { + IndirectReference = br.ReadUInt32(); + return; + } + else if ((Flags & AlTextureFlags.IsOverrideDimensions) != 0) + { + Width = br.ReadInt16(); + Height = br.ReadInt16(); + } + } + + br.BaseStream.Seek(baseOffset + imgOffset, SeekOrigin.Begin); + ImageData = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position)); + } + } +} diff --git a/LibDgf/Aqualead/Texture/AlTextureEntry.cs b/LibDgf/Aqualead/Texture/AlTextureEntry.cs new file mode 100644 index 0000000..a8f0be7 --- /dev/null +++ b/LibDgf/Aqualead/Texture/AlTextureEntry.cs @@ -0,0 +1,53 @@ +using GMWare.IO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Aqualead.Texture +{ + public class AlTextureEntry + { + public uint Id { get; set; } + public AlTextureEntryFlags Flags { get; set; } + public string Name { get; set; } + public List Bounds { get; set; } = new List(); + public AlPoint CenterPoint { get; set; } + + public void Read(BinaryReader br, bool isMultiTexture) + { + var baseOffset = br.BaseStream.Position; + Id = br.ReadUInt32(); + ushort mipsCount = br.ReadUInt16(); + Flags = (AlTextureEntryFlags)br.ReadByte(); + br.ReadByte(); // Alignment + if (!isMultiTexture) return; + + for (int i = 0; i < mipsCount; ++i) + { + Bounds.Add(new AlXYWH + { + X = br.ReadInt16(), + Y = br.ReadInt16(), + W = br.ReadUInt16(), + H = br.ReadUInt16() + }); + } + + if ((Flags & AlTextureEntryFlags.IsHasCenterPoint) != 0) + { + CenterPoint = new AlPoint + { + X = br.ReadInt16(), + Y = br.ReadInt16() + }; + } + + if ((Flags & AlTextureEntryFlags.IsHasName) != 0) + { + br.BaseStream.Seek(baseOffset - 0x20, SeekOrigin.Begin); + Name = StringReadingHelper.ReadNullTerminatedStringFromFixedSizeBlock(br, 0x20, Encoding.UTF8); + } + } + } +} diff --git a/LibDgf/Aqualead/Texture/AlTextureEntryFlags.cs b/LibDgf/Aqualead/Texture/AlTextureEntryFlags.cs new file mode 100644 index 0000000..77aeb14 --- /dev/null +++ b/LibDgf/Aqualead/Texture/AlTextureEntryFlags.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Aqualead.Texture +{ + [Flags] + public enum AlTextureEntryFlags : byte + { + IsHasName = 1 << 0, + IsHasCenterPoint = 1 << 1, + IsFiltered = 1 << 2 + } +} diff --git a/LibDgf/Aqualead/Texture/AlTextureFlags.cs b/LibDgf/Aqualead/Texture/AlTextureFlags.cs new file mode 100644 index 0000000..7359afb --- /dev/null +++ b/LibDgf/Aqualead/Texture/AlTextureFlags.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Aqualead.Texture +{ + [Flags] + public enum AlTextureFlags : byte + { + IsSpecial = 1 << 1, + IsIndirect = 1 << 2, + IsOverrideDimensions = 1 << 3 + } +} diff --git a/LibDgf/Aqualead/Texture/AlXYWH.cs b/LibDgf/Aqualead/Texture/AlXYWH.cs new file mode 100644 index 0000000..98e6134 --- /dev/null +++ b/LibDgf/Aqualead/Texture/AlXYWH.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Aqualead.Texture +{ + public struct AlXYWH + { + public short X; + public short Y; + public ushort W; + public ushort H; + } +} diff --git a/LibDgf/Dat/DatBuilder.cs b/LibDgf/Dat/DatBuilder.cs new file mode 100644 index 0000000..5b1e6f8 --- /dev/null +++ b/LibDgf/Dat/DatBuilder.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Dat +{ + public class DatBuilder + { + public class ReplacementEntry + { + public int Index { get; set; } + public DatReader SourceDat { get; set; } + public int SourceIndex { get; set; } + public string SourceFile { get; set; } + } + + class NewEntry + { + public DatEntry ArchEntry { get; set; } = new DatEntry(); + public DatEntry OrigEntry { get; set; } + public int OrigIndex { get; set; } + public ReplacementEntry ReplacementEntry { get; set; } + } + + DatReader sourceDat; + + public List ReplacementEntries { get; } = new List(); + + public DatBuilder(DatReader sourceDat = null) + { + this.sourceDat = sourceDat; + } + + public void Build(Stream destStream) + { + // Check there are no duplicated indexes + HashSet indexSet = new HashSet(); + foreach (var entry in ReplacementEntries) + { + if (entry.Index < 0) throw new InvalidOperationException("Entry with negative index present."); + indexSet.Add(entry.Index); + } + if (indexSet.Count != ReplacementEntries.Count) + { + throw new InvalidOperationException("Replacement entries with non-unique IDs present."); + } + + ReplacementEntries.Sort((x, y) => x.Index.CompareTo(y.Index)); + List newEntries = new List(); + + // Copy over original entries + if (sourceDat != null) + { + for (int i = 0; i < sourceDat.EntriesCount; ++i) + { + var e = sourceDat.GetEntry(i); + newEntries.Add(new NewEntry { OrigEntry = e, OrigIndex = i }); + } + } + + // Set replacement entries + foreach (var rep in ReplacementEntries) + { + if (rep.Index > newEntries.Count) + throw new InvalidOperationException("Replacement entries results in incontinuity."); + + var newEntry = new NewEntry { ReplacementEntry = rep }; + if (rep.Index == newEntries.Count) + newEntries.Add(newEntry); + else + newEntries[rep.Index] = newEntry; + } + + // Update size and position + uint dataOffset = 0; + for (int i = 0; i < newEntries.Count; ++i) + { + var newEntry = newEntries[i]; + newEntry.ArchEntry.Offset = dataOffset; + var repEntry = newEntry.ReplacementEntry; + + if (repEntry == null) + { + newEntry.ArchEntry.Length = newEntry.OrigEntry.Length; + } + else + { + if (repEntry.SourceDat != null) + { + if (repEntry.SourceFile != null) + throw new InvalidOperationException("Replacement entries with both DAT and file source specified exist."); + newEntry.ArchEntry.Length = repEntry.SourceDat.GetEntry(repEntry.SourceIndex).Length; + } + else if (repEntry.SourceFile != null) + { + newEntry.ArchEntry.Length = (uint)new FileInfo(repEntry.SourceFile).Length; + } + else + { + newEntry.ArchEntry.Length = 0; + newEntries.RemoveAt(i); + --i; + } + } + dataOffset += newEntry.ArchEntry.Length; + } + + // Write file + BinaryWriter bw = new BinaryWriter(destStream); + bw.Write("DAT\0".ToCharArray()); + bw.Write(newEntries.Count); + dataOffset = (uint)(((newEntries.Count + 1) * 8 + 15) & ~15); + foreach (var newEntry in newEntries) + { + newEntry.ArchEntry.Offset += dataOffset; + newEntry.ArchEntry.Write(bw); + } + // Do we need to 16-byte align anything after the header? + if (destStream.Position < dataOffset) + bw.Write(new byte[dataOffset - destStream.Position]); + + foreach (var newEntry in newEntries) + { + var repEntry = newEntry.ReplacementEntry; + if (repEntry == null) + { + bw.Write(sourceDat.GetData(newEntry.OrigIndex)); + } + else + { + if (repEntry.SourceDat != null) + { + bw.Write(repEntry.SourceDat.GetData(repEntry.SourceIndex)); + } + else + { + using (FileStream fs = File.OpenRead(repEntry.SourceFile)) + { + fs.CopyTo(destStream); + } + } + } + } + } + } +} diff --git a/LibDgf/Dat/DatEntry.cs b/LibDgf/Dat/DatEntry.cs new file mode 100644 index 0000000..e934f6a --- /dev/null +++ b/LibDgf/Dat/DatEntry.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Dat +{ + public class DatEntry + { + public uint Offset { get; set; } + public uint Length { get; set; } + + public void Read(BinaryReader br) + { + Offset = br.ReadUInt32(); + Length = br.ReadUInt32(); + } + + public void Write(BinaryWriter bw) + { + bw.Write(Offset); + bw.Write(Length); + } + } +} diff --git a/LibDgf/Dat/DatReader.cs b/LibDgf/Dat/DatReader.cs new file mode 100644 index 0000000..25ecf65 --- /dev/null +++ b/LibDgf/Dat/DatReader.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Dat +{ + public class DatReader : IDisposable + { + Stream stream; + BinaryReader br; + List entries = new List(); + private bool disposedValue; + + public DatReader(Stream stream) + { + this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + + br = new BinaryReader(stream); + if (new string(br.ReadChars(4)) != "DAT\0") throw new InvalidDataException("Not a DAT file."); + int numEntries = br.ReadInt32(); + for (int i = 0; i < numEntries; ++i) + { + DatEntry entry = new DatEntry(); + entry.Read(br); + entries.Add(entry); + } + } + + public int EntriesCount + { + get + { + CheckDisposed(); + return entries.Count; + } + } + + public byte[] GetData(int index) + { + CheckDisposed(); + if (index < 0) throw new ArgumentOutOfRangeException(nameof(index), "Index cannot be negative."); + if (index >= EntriesCount) throw new ArgumentOutOfRangeException(nameof(index), "Index cannot be greater than or equal to count."); + + var entry = entries[index]; + stream.Seek(entry.Offset, SeekOrigin.Begin); + return br.ReadBytes((int)entry.Length); + } + + public DatEntry GetEntry(int index) + { + CheckDisposed(); + if (index < 0) throw new ArgumentOutOfRangeException(nameof(index), "Index cannot be negative."); + if (index >= EntriesCount) throw new ArgumentOutOfRangeException(nameof(index), "Index cannot be greater than or equal to count."); + + var entry = entries[index]; + return new DatEntry + { + Offset = entry.Offset, + Length = entry.Length + }; + } + + void CheckDisposed() + { + if (disposedValue) throw new ObjectDisposedException(GetType().FullName); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + stream.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/LibDgf/GMWare.IO/StreamUtils.cs b/LibDgf/GMWare.IO/StreamUtils.cs new file mode 100644 index 0000000..346ef61 --- /dev/null +++ b/LibDgf/GMWare.IO/StreamUtils.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.IO.Compression; + +namespace GMWare.IO +{ + /// + /// Provides some commonly used methods for Stream manipulation. + /// + public static class StreamUtils + { + /// + /// Opens a DeflateStream for reading Zlib compressed data from the current position of the parameter. Closing this Stream will not close the underlying Stream. + /// + /// The Stream to create a DeflateStream from. + /// The opened DeflateStream. + public static DeflateStream OpenDeflateDecompressionStreamCheap(Stream stream) + { + return OpenDeflateDecompressionStreamCheap(stream, true); + } + + /// + /// Opens a DeflateStream for reading Zlib compressed data from the current position of the parameter. + /// + /// The Stream to create a DeflateStream from. + /// Specifies whether or not the underlying Stream will be left open when this Stream is closed. + /// The opened DeflateStream. + public static DeflateStream OpenDeflateDecompressionStreamCheap(Stream stream, bool leaveOpen) + { + if (stream == null) throw new ArgumentNullException("stream"); + stream.ReadByte(); + stream.ReadByte(); + return new DeflateStream(stream, CompressionMode.Decompress, leaveOpen); + } + + /// + /// Copies a number of bytes from one Stream to the other. The current position of each is used. + /// + /// The Stream to copy from. + /// The Stream to copy to. + /// The number of bytes to copy. + /// Whether or not all requested bytes are copied. + [Obsolete("For backward compatibility only. Please use StreamCopy().")] + public static bool StreamCopyWithLength(Stream src, Stream dest, int length) + { + return StreamCopy(src, dest, length); + } + + /// + /// Copies a number of bytes from one Stream to the other. The current position of each is used. + /// + /// The Stream to copy from. + /// The Stream to copy to. + /// The number of bytes to copy. + /// Whether or not all requested bytes are copied. + public static bool StreamCopy(Stream src, Stream dest, long length) + { + return StreamCopy(src, dest, length, null); + } + + /// + /// Copies a number of bytes from one Stream to the other. The current position of each is used. + /// + /// The Stream to copy from. + /// The Stream to copy to. + /// The number of bytes to copy. + /// A delegate to process the read buffer before it's written to the destination. + /// Whether or not all requested bytes are copied. + public static bool StreamCopy(Stream src, Stream dest, long length, StreamCopyProcessor procDelegate) + { + if (src == null) throw new ArgumentNullException("src"); + if (dest == null) throw new ArgumentNullException("dest"); + + if (length == 0) return true; + + const int BUFFER_SIZE = 4096; + + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + long left = length; + bool continueProcessing = true; + + while (continueProcessing && left / buffer.Length != 0 && (read = src.Read(buffer, 0, buffer.Length)) > 0) + { + if (procDelegate != null) + { + continueProcessing = procDelegate(buffer, read); + } + dest.Write(buffer, 0, read); + left -= read; + } + + // Should stop if zero bytes have been read from stream although some should have been read + if (length > BUFFER_SIZE && left == length) return false; + + if (src.CanSeek && src.Position == src.Length && left != 0) throw new EndOfStreamException(); + + while (continueProcessing && left > 0 && (read = src.Read(buffer, 0, (int)left)) > 0) + { + if (procDelegate != null) + { + continueProcessing = procDelegate(buffer, read); + } + dest.Write(buffer, 0, read); + left -= read; + } + + return left == 0; + } + + /// + /// Copies one Stream to the other. The current position of each is used. + /// + /// The Stream to copy from. + /// The Stream to copy to. + /// The number of bytes copied. + public static long StreamCopy(Stream src, Stream dest) + { + return StreamCopy(src, dest, null); + } + + /// + /// Copies one Stream to the other. The current position of each is used. + /// + /// The Stream to copy from. + /// The Stream to copy to. + /// A delegate for processing a read chunk before it is written. + /// The number of bytes copied. + public static long StreamCopy(Stream src, Stream dest, StreamCopyProcessor procDelegate) + { + if (src == null) throw new ArgumentNullException("src"); + if (dest == null) throw new ArgumentNullException("dest"); + + // From Stack Overflow, probably + const int BUFFER_SIZE = 4096; + + byte[] buffer = new byte[BUFFER_SIZE]; + long bytesCopied = 0; + int bytesRead; + bool continueProcessing = true; + + do + { + bytesRead = src.Read(buffer, 0, BUFFER_SIZE); + if (procDelegate != null) + { + continueProcessing = procDelegate(buffer, bytesRead); + } + dest.Write(buffer, 0, bytesRead); + bytesCopied += bytesRead; + } + while (continueProcessing && bytesRead != 0); + return bytesCopied; + } + + /// + /// Encapsulates a method for processing bytes that are being copied. + /// + /// The bytes that have been read from the source stream and will be written to the destination stream + /// The number of bytes that have been read from the source stream stored in + /// + public delegate bool StreamCopyProcessor(byte[] buffer, int bytesRead); + } +} diff --git a/LibDgf/GMWare.IO/StringReadingHelper.cs b/LibDgf/GMWare.IO/StringReadingHelper.cs new file mode 100644 index 0000000..1e38b01 --- /dev/null +++ b/LibDgf/GMWare.IO/StringReadingHelper.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace GMWare.IO +{ + /// + /// Collection of methods for reading strings using BinaryReader + /// + public static class StringReadingHelper + { + /// + /// Reads a string of a given length. + /// + /// BinaryReader to read from + /// Length of string to read + /// The string that was read. + public static string ReadLengthedString(BinaryReader reader, int length) + { + return new string(reader.ReadChars(length)); + } + + /// + /// Reads a null terminated string. + /// + /// BinaryReader to read from + /// The string that was read. + public static string ReadNullTerminatedString(BinaryReader reader) + { + StringBuilder sb = new StringBuilder(); + for (char ch = reader.ReadChar(); ch != '\0'; ch = reader.ReadChar()) + { + sb.Append(ch); + } + return sb.ToString(); + } + + /// + /// Reads a null terminated string from within a fixed size block. + /// + /// BinaryReader to read from + /// The length of the fixed size block + /// The encoding the string is in + /// The string that was read. + public static string ReadNullTerminatedStringFromFixedSizeBlock(BinaryReader reader, int blockLen, Encoding encoding) + { + byte[] data = reader.ReadBytes(blockLen); + string str = encoding.GetString(data); + int indNull = str.IndexOf('\0'); + if (indNull >= 0) + { + return str.Substring(0, indNull); + } + else + { + return str; + } + } + } +} diff --git a/LibDgf/LibDgf.csproj b/LibDgf/LibDgf.csproj new file mode 100644 index 0000000..e64087f --- /dev/null +++ b/LibDgf/LibDgf.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/LibDgf/Se/SeDatReader.cs b/LibDgf/Se/SeDatReader.cs new file mode 100644 index 0000000..7614545 --- /dev/null +++ b/LibDgf/Se/SeDatReader.cs @@ -0,0 +1,68 @@ +using LibDgf.Dat; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Se +{ + public class SeDatReader + { + DatReader dat; + List entries = new List(); + byte[] seToc; + byte[] seData; + + public SeDatReader(DatReader dat) + { + this.dat = dat ?? throw new ArgumentNullException(nameof(dat)); + + seToc = dat.GetData(0); + seData = dat.GetData(1); + + using (MemoryStream ms = new MemoryStream(seToc)) + { + BinaryReader br = new BinaryReader(ms); + while (true) + { + SeEntry entry = new SeEntry(); + entry.Read(br); + if (entry.DataOffset == -1 && entry.DataLength == -1) + break; + entries.Add(entry); + } + } + } + + public int EntriesCount + { + get + { + return entries.Count; + } + } + + public byte[] GetData(int index) + { + if (index < 0) throw new ArgumentOutOfRangeException(nameof(index), "Index cannot be negative."); + if (index >= EntriesCount) throw new ArgumentOutOfRangeException(nameof(index), "Index cannot be greater than or equal to count."); + + var entry = entries[index]; + byte[] data = new byte[entry.DataLength]; + Buffer.BlockCopy(seData, (int)entry.DataOffset, data, 0, data.Length); + return data; + } + + public string GetDataAsString(int index) + { + var data = GetData(index); + StringBuilder sb = new StringBuilder(); + foreach (byte b in data) + { + if (b == 0) break; + sb.Append((char)b); + } + return sb.ToString(); + } + } +} diff --git a/LibDgf/Se/SeEntry.cs b/LibDgf/Se/SeEntry.cs new file mode 100644 index 0000000..f24c50d --- /dev/null +++ b/LibDgf/Se/SeEntry.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Se +{ + public class SeEntry + { + public int DataOffset { get; set; } + public int DataLength { get; set; } + public uint SeLength { get; set; } + public uint Unknown { get; set; } + + public void Read(BinaryReader br) + { + DataOffset = br.ReadInt32(); + DataLength = br.ReadInt32(); + SeLength = br.ReadUInt32(); + Unknown = br.ReadUInt32(); + } + + public void Write(BinaryWriter bw) + { + bw.Write(DataOffset); + bw.Write(DataLength); + bw.Write(SeLength); + bw.Write(Unknown); + } + } +} diff --git a/LibDgf/Txm/TxmHeader.cs b/LibDgf/Txm/TxmHeader.cs new file mode 100644 index 0000000..04c574f --- /dev/null +++ b/LibDgf/Txm/TxmHeader.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Txm +{ + public class TxmHeader + { + public TxmPixelFormat ImageSourcePixelFormat { get; set; } + public TxmPixelFormat ImageVideoPixelFormat { get; set; } + public short ImageWidth { get; set; } + public short ImageHeight { get; set; } + public short ImageBufferBase { get; set; } + public TxmPixelFormat ClutPixelFormat { get; set; } + public byte Misc { get; set; } // 0x0f = level, 0x70 = count, 0x80 = fast count + public short ClutWidth { get; set; } + public short ClutHeight { get; set; } + public short ClutBufferBase { get; set; } + + public void Read(BinaryReader br) + { + ImageSourcePixelFormat = (TxmPixelFormat)br.ReadByte(); + ImageVideoPixelFormat = (TxmPixelFormat)br.ReadByte(); + ImageWidth = br.ReadInt16(); + ImageHeight = br.ReadInt16(); + ImageBufferBase = br.ReadInt16(); + ClutPixelFormat = (TxmPixelFormat)br.ReadByte(); + Misc = br.ReadByte(); + ClutWidth = br.ReadInt16(); + ClutHeight = br.ReadInt16(); + ClutBufferBase = br.ReadInt16(); + } + + public void Write(BinaryWriter bw) + { + bw.Write((byte)ImageSourcePixelFormat); + bw.Write((byte)ImageVideoPixelFormat); + bw.Write(ImageWidth); + bw.Write(ImageHeight); + bw.Write(ImageBufferBase); + bw.Write((byte)ClutPixelFormat); + bw.Write(Misc); + bw.Write(ClutWidth); + bw.Write(ClutHeight); + bw.Write(ClutBufferBase); + } + + public int GetImageByteSize() + { + return GetImageMemSize(ImageSourcePixelFormat, ImageWidth, ImageHeight); + } + + public int GetClutByteSize() + { + return GetImageMemSize(ClutPixelFormat, ClutWidth, ClutHeight); + } + + int GetImageMemSize(TxmPixelFormat format, int width, int height) + { + switch (format) + { + case TxmPixelFormat.PSMT4: + return width * height / 2; + case TxmPixelFormat.PSMT8: + return width * height; + case TxmPixelFormat.PSMCT32: + return width * height * 4; + default: + return 0; + } + } + + public override string ToString() + { + return $"{ImageSourcePixelFormat} {ImageVideoPixelFormat} {ImageWidth}x{ImageHeight} {ImageBufferBase:x4} " + + $"{ClutPixelFormat} {Misc:x2} {ClutWidth}x{ClutHeight} {ClutBufferBase:x4}"; + } + } +} diff --git a/LibDgf/Txm/TxmPixelFormat.cs b/LibDgf/Txm/TxmPixelFormat.cs new file mode 100644 index 0000000..d8fbff5 --- /dev/null +++ b/LibDgf/Txm/TxmPixelFormat.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Txm +{ + public enum TxmPixelFormat : byte + { + PSMCT32 = 0x00, + PSMCT24 = 0x01, + PSMCT16 = 0x02, + PSMCT16S = 0x0a, + PSMT8 = 0x13, + PSMT4 = 0x14, + PSMT8H = 0x1b, + PSMT4HL = 0x24, + PSMT4HH = 0x2c, + PSMZ32 = 0x30, + PSMZ24 = 0x31, + PSMZ16 = 0x32, + PSMZ16S = 0x3a, + None = 0xff + } +} diff --git a/LibDgf/Utils.cs b/LibDgf/Utils.cs new file mode 100644 index 0000000..e7b4ca1 --- /dev/null +++ b/LibDgf/Utils.cs @@ -0,0 +1,29 @@ +using LibDgf.Aqualead; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf +{ + public static class Utils + { + public static Stream CheckDecompress(Stream stream, bool keepOpen = false) + { + BinaryReader br = new BinaryReader(stream); + if (new string(br.ReadChars(4)) == "ALLZ") + { + stream.Seek(-4, SeekOrigin.Current); + var decoder = new AlLzDecoder(); + byte[] decoded = decoder.Decode(stream); + if (!keepOpen) stream.Close(); + stream = new MemoryStream(decoded); + } + else + { + stream.Seek(-4, SeekOrigin.Current); + } + return stream; + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f999c8 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +Densha de Go! Final Modding Library +=================================== + +This is WIP library for manipulating files used in Densha de Go! Final. + +Currently implemented +--------------------- + +**Final** +- DAT: file packs +- Sound effects catalog (read support only) +- TXM: texture files + +**Aqualead (Plug & Play)** +(read support only currently) +- AAR: file archive +- AIG: Native image format +- ATX: Native texture format +- ALZ: LZSS decompressor diff --git a/libktxsharp b/libktxsharp new file mode 160000 index 0000000..a6fd14e --- /dev/null +++ b/libktxsharp @@ -0,0 +1 @@ +Subproject commit a6fd14e1da06e038fd1fc7d654d72058f8d0ee73