diff --git a/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs b/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs index c03e6ae0f..b13767294 100644 --- a/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs +++ b/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs @@ -1,6 +1,7 @@ using ICSharpCode.SharpZipLib.BZip2; using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Imaging; @@ -30,20 +31,57 @@ public class UndertaleEmbeddedTexture : UndertaleNamedResource, IDisposable /// /// The amount of generated mipmap levels.
- /// GameMaker Studio: 2 only. + /// GameMaker Studio 2 only. ///
public uint GeneratedMips { get; set; } /// - /// TODO: something.
- /// GameMaker: Studio 2 only. + /// Size of the texture block in bytes. Only appears in later 2022 versions of GameMaker. ///
public uint TextureBlockSize { get; set; } /// /// The texture data in the embedded image. /// - public TexData TextureData { get; set; } = new TexData(); + public TexData TextureData { get { return _textureData ?? (_textureData = LoadExternalTexture()); } set { _textureData = value; } } + private TexData _textureData = new TexData(); + + + /// + /// Helper variable for whether or not this texture is to be stored externally or not. + /// + public bool TextureExternal { get; set; } = false; + + + /// + /// Helper variable for whether or not an external texture was loaded yet. + /// + public bool TextureExternallyLoaded { get; set; } = false; + + /// + /// Width of the texture. 2022.9+ only. + /// + public int TextureWidth { get; set; } + + /// + /// Height of the texture. 2022.9+ only. + /// + public int TextureHeight { get; set; } + + /// + /// Index of the texture in the texture group. 2022.9+ only. + /// + public int IndexInGroup { get; set; } + + /// + /// Helper reference to texture group info, if found in the data file. + /// + public UndertaleTextureGroupInfo TextureInfo { get; set; } + + /// + /// Helper for 2022.9+ support. Stores copy of the path to the data file. + /// + private string _2022_9_GameDirectory { get; set; } /// public void Serialize(UndertaleWriter writer) @@ -53,7 +91,16 @@ public void Serialize(UndertaleWriter writer) writer.Write(GeneratedMips); if (writer.undertaleData.GM2022_3) writer.Write(TextureBlockSize); - writer.WriteUndertaleObjectPointer(TextureData); + if (writer.undertaleData.GM2022_9) + { + writer.Write(TextureWidth); + writer.Write(TextureHeight); + writer.Write(IndexInGroup); + } + if (TextureExternal) + writer.Write((int)0); // Ensure null pointer is written with external texture + else + writer.WriteUndertaleObjectPointer(_textureData); } /// @@ -64,33 +111,116 @@ public void Unserialize(UndertaleReader reader) GeneratedMips = reader.ReadUInt32(); if (reader.undertaleData.GM2022_3) TextureBlockSize = reader.ReadUInt32(); - TextureData = reader.ReadUndertaleObjectPointer(); + if (reader.undertaleData.GM2022_9) + { + TextureWidth = reader.ReadInt32(); + TextureHeight = reader.ReadInt32(); + IndexInGroup = reader.ReadInt32(); + _2022_9_GameDirectory = reader.Directory; + } + _textureData = reader.ReadUndertaleObjectPointer(); + TextureExternal = (_textureData == null); } /// - /// TODO! + /// Serializes the in-file texture blob for this texture. /// /// Where to serialize to. public void SerializeBlob(UndertaleWriter writer) { + // If external, don't serialize blob + // Has sanity check for data being null as well, although the external flag should be set + if (_textureData == null || TextureExternal) + return; + // padding while (writer.Position % 0x80 != 0) writer.Write((byte)0); - writer.WriteUndertaleObject(TextureData); + writer.WriteUndertaleObject(_textureData); } /// - /// TODO! + /// Deserializes the in-file texture blob for this texture. /// /// Where to deserialize from. public void UnserializeBlob(UndertaleReader reader) { + // If external, don't deserialize blob + // Has sanity check for data being null as well, although the external flag should be set + if (_textureData == null || TextureExternal) + return; + while (reader.Position % 0x80 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); - reader.ReadUndertaleObject(TextureData); + reader.ReadUndertaleObject(_textureData); + } + + /// + /// Assigns texture group info to every embedded texture in the supplied data file. + /// + public static void FindAllTextureInfo(UndertaleData data) + { + if (data.TextureGroupInfo != null) + { + foreach (var info in data.TextureGroupInfo) + { + foreach (var tex in info.TexturePages) + tex.Resource.TextureInfo = info; + } + } + } + + private static TexData _placeholderTexture = null; + private static TexData CreatePlaceholderTexture() + { + _placeholderTexture = new(); + + // Construct new PNG file that has placeholder image + // TODO: display a helpful message instead? + Bitmap image = new Bitmap(64, 64); + Graphics g = Graphics.FromImage(image); + g.Clear(Color.Black); + g.Dispose(); + + _placeholderTexture.TextureBlob = TextureWorker.GetImageBytes(image); + return _placeholderTexture; + } + + /// + /// Attempts to load the corresponding external texture. Should only happen in 2022.9 and above. + /// + /// + public TexData LoadExternalTexture() + { + TexData texData; + + if (_2022_9_GameDirectory == null) + return _placeholderTexture ?? CreatePlaceholderTexture(); + + // Try to find file on disk + string path = Path.Combine(_2022_9_GameDirectory, TextureInfo.Directory.Content, + TextureInfo.Name.Content + "_" + IndexInGroup.ToString() + TextureInfo.Extension.Content); + if (!File.Exists(path)) + return _placeholderTexture ?? CreatePlaceholderTexture(); + + // Load file! + try + { + using FileStream fs = new(path, FileMode.Open); + using FileBinaryReader fbr = new(fs); + texData = new TexData(); + texData.Unserialize(fbr, true); + TextureExternallyLoaded = true; + } + catch (IOException) + { + return _placeholderTexture ?? CreatePlaceholderTexture(); + } + + return texData; } /// @@ -108,8 +238,10 @@ public void Dispose() { GC.SuppressFinalize(this); - TextureData.Dispose(); + _textureData.Dispose(); + _textureData = null; Name = null; + TextureInfo = null; } /// @@ -164,6 +296,16 @@ public int Height } } + /// + /// Whether this texture uses QOI format. + /// + public bool FormatQOI { get; set; } = false; + + /// + /// Whether this texture uses BZ2 format. (Always used in combination with QOI.) + /// + public bool FormatBZ2 { get; set; } = false; + /// public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string name = null) @@ -171,8 +313,19 @@ protected void OnPropertyChanged([CallerMemberName] string name = null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } + /// + /// Header used for PNG files. + /// public static readonly byte[] PNGHeader = { 137, 80, 78, 71, 13, 10, 26, 10 }; + + /// + /// Header used for GameMaker QOI + BZ2 files. + /// public static readonly byte[] QOIAndBZip2Header = { 50, 122, 111, 113 }; + + /// + /// Header used for GameMaker QOI files. + /// public static readonly byte[] QOIHeader = { 102, 105, 111, 113 }; /// @@ -193,9 +346,17 @@ public static void ClearSharedStream() /// public void Serialize(UndertaleWriter writer) { - if (writer.undertaleData.UseQoiFormat) + Serialize(writer, writer.undertaleData.GM2022_3, writer.undertaleData.GM2022_5); + } + + /// + /// Serializes the texture to any type of writer (can be any destination file). + /// + public void Serialize(FileBinaryWriter writer, bool gm2022_3, bool gm2022_5) + { + if (FormatQOI) { - if (writer.undertaleData.UseBZipFormat) + if (FormatBZ2) { writer.Write(QOIAndBZip2Header); @@ -203,20 +364,20 @@ public void Serialize(UndertaleWriter writer) using Bitmap bmp = TextureWorker.GetImageFromByteArray(TextureBlob); writer.Write((short)bmp.Width); writer.Write((short)bmp.Height); - byte[] data = QoiConverter.GetArrayFromImage(bmp, writer.undertaleData.GM2022_3 ? 0 : 4); - using MemoryStream input = new MemoryStream(data); + byte[] qoiData = QoiConverter.GetArrayFromImage(bmp, gm2022_3 ? 0 : 4); + using MemoryStream input = new MemoryStream(qoiData); if (sharedStream.Length != 0) sharedStream.Seek(0, SeekOrigin.Begin); BZip2.Compress(input, sharedStream, false, 9); - if (writer.undertaleData.GM2022_5) - writer.Write((uint)data.Length); + if (gm2022_5) + writer.Write((uint)qoiData.Length); writer.Write(sharedStream.GetBuffer().AsSpan()[..(int)sharedStream.Position]); } else { // Encode the PNG data back to QOI using Bitmap bmp = TextureWorker.GetImageFromByteArray(TextureBlob); - writer.Write(QoiConverter.GetSpanFromImage(bmp, writer.undertaleData.GM2022_3 ? 0 : 4)); + writer.Write(QoiConverter.GetSpanFromImage(bmp, gm2022_3 ? 0 : 4)); } } else @@ -225,6 +386,14 @@ public void Serialize(UndertaleWriter writer) /// public void Unserialize(UndertaleReader reader) + { + Unserialize(reader, reader.undertaleData.GM2022_5); + } + + /// + /// Unserializes the texture from any type of reader (can be from any source). + /// + public void Unserialize(FileBinaryReader reader, bool is_2022_5) { sharedStream ??= new(); @@ -237,11 +406,11 @@ public void Unserialize(UndertaleReader reader) if (header.Take(4).SequenceEqual(QOIAndBZip2Header)) { - reader.undertaleData.UseQoiFormat = true; - reader.undertaleData.UseBZipFormat = true; + FormatQOI = true; + FormatBZ2 = true; // Don't really care about the width/height, so skip them, as well as header - reader.Position += (uint)(reader.undertaleData.GM2022_5 ? 12 : 8); + reader.Position += (uint)(is_2022_5 ? 12 : 8); // Need to fully decompress and convert the QOI data to PNG for compatibility purposes (at least for now) if (sharedStream.Length != 0) @@ -258,8 +427,8 @@ public void Unserialize(UndertaleReader reader) } else if (header.Take(4).SequenceEqual(QOIHeader)) { - reader.undertaleData.UseQoiFormat = true; - reader.undertaleData.UseBZipFormat = false; + FormatQOI = true; + FormatBZ2 = false; // Need to convert the QOI data to PNG for compatibility purposes (at least for now) using Bitmap bmp = QoiConverter.GetImageFromStream(reader.Stream); diff --git a/UndertaleModLib/Models/UndertaleTextureGroupInfo.cs b/UndertaleModLib/Models/UndertaleTextureGroupInfo.cs index ca0b113b4..f2975802c 100644 --- a/UndertaleModLib/Models/UndertaleTextureGroupInfo.cs +++ b/UndertaleModLib/Models/UndertaleTextureGroupInfo.cs @@ -71,6 +71,42 @@ public class UndertaleTextureGroupInfo : UndertaleNamedResource, IDisposable /// public UndertaleSimpleResourcesList Tilesets { get; set; } + /// + /// Directory of the texture on disk. 2022.9+ only. + /// + public UndertaleString Directory { get; set; } + + /// + /// File extension of the texture on disk. 2022.9+ only. + /// + public UndertaleString Extension { get; set; } + + /// + /// The load type of the texture. 2022.9+ only. + /// + public TextureGroupLoadType LoadType { get; set; } + + /// + /// The possible load types of a texture in 2022.9 and above. Old versions default to "InFile". + /// + public enum TextureGroupLoadType + { + /// + /// The texture data is located inside this file. + /// + InFile = 0, + /// + /// The textures of the group this belongs to are located externally + /// May mean more specifically that textures for one texture group are all in one file. + /// + SeparateGroup = 1, + /// + /// The textures of the group this belongs to are located externally. + /// May mean more specifically that textures are separated into different files, within the group. + /// + SeparateTextures = 2 + } + /// /// Initializes a new instance of . /// @@ -88,6 +124,13 @@ public void Serialize(UndertaleWriter writer) { writer.WriteUndertaleString(Name); + if (writer.undertaleData.GM2022_9) + { + writer.WriteUndertaleString(Directory); + writer.WriteUndertaleString(Extension); + writer.Write((int)LoadType); + } + writer.WriteUndertaleObjectPointer(TexturePages); writer.WriteUndertaleObjectPointer(Sprites); writer.WriteUndertaleObjectPointer(SpineSprites); @@ -106,6 +149,13 @@ public void Unserialize(UndertaleReader reader) { Name = reader.ReadUndertaleString(); + if (reader.undertaleData.GM2022_9) + { + Directory = reader.ReadUndertaleString(); + Extension = reader.ReadUndertaleString(); + LoadType = (TextureGroupLoadType)reader.ReadInt32(); + } + // Read the pointers TexturePages = reader.ReadUndertaleObjectPointer>(); Sprites = reader.ReadUndertaleObjectPointer>(); diff --git a/UndertaleModLib/UndertaleChunks.cs b/UndertaleModLib/UndertaleChunks.cs index c1f763a01..5fec9573d 100644 --- a/UndertaleModLib/UndertaleChunks.cs +++ b/UndertaleModLib/UndertaleChunks.cs @@ -747,7 +747,18 @@ internal override void SerializeChunk(UndertaleWriter writer) int maxSize = List.Select(x => x.TextureData.TextureBlob?.Length ?? 0).Max(); UndertaleEmbeddedTexture.TexData.InitSharedStream(maxSize); - if (writer.undertaleData.UseQoiFormat) + bool anythingUsesQoi = false; + foreach (var tex in List) + { + if (tex.TextureExternal && !tex.TextureExternallyLoaded) + continue; // don't accidentally load everything... + if (tex.TextureData.FormatQOI) + { + anythingUsesQoi = true; + break; + } + } + if (anythingUsesQoi) { // Calculate maximum size of QOI converter buffer maxSize = List.Select(x => x.TextureData.Width * x.TextureData.Height).Max() @@ -907,6 +918,27 @@ internal override void UnserializeChunk(UndertaleReader reader) throw new InvalidOperationException(); if (reader.ReadUInt32() != 1) throw new IOException("Expected TGIN version 1"); + if (reader.undertaleData.GMS2_3) + { + // Check for 2022.8 + uint returnPosition = reader.Position; + + uint tginCount = reader.ReadUInt32(); + if (tginCount > 0) + { + uint tginPtr = reader.ReadUInt32(); + uint secondTginPtr = (tginCount >= 2) ? reader.ReadUInt32() : (returnPosition + this.Length); + reader.Position = tginPtr + 4; + + // Check to see if the pointer located at this address points within this object + // If not, then we know we're using a new format! + uint ptr = reader.ReadUInt32(); + if (ptr < tginPtr || ptr >= secondTginPtr) + reader.undertaleData.GM2022_9 = true; + } + + reader.Position = returnPosition; + } base.UnserializeChunk(reader); } } diff --git a/UndertaleModLib/UndertaleData.cs b/UndertaleModLib/UndertaleData.cs index dfa0f251b..41ad22acc 100644 --- a/UndertaleModLib/UndertaleData.cs +++ b/UndertaleModLib/UndertaleData.cs @@ -309,16 +309,6 @@ public object this[Type resourceType] /// public bool GMS2_3_2 = false; - /// - /// Whether the data file uses the QOI format for images. - /// - public bool UseQoiFormat = false; - - /// - /// Whether the data file uses BZip compression. - /// - public bool UseBZipFormat = false; - /// /// Whether the data file is from version GMS2022.1. /// @@ -344,6 +334,11 @@ public object this[Type resourceType] /// public bool GM2022_6 = false; + /// + /// Whether the data file is from version GMS2022.9. + /// + public bool GM2022_9 = false; + /// /// Some info for the editor to store data on. /// diff --git a/UndertaleModLib/UndertaleIO.cs b/UndertaleModLib/UndertaleIO.cs index dcda76810..7b50a593d 100644 --- a/UndertaleModLib/UndertaleIO.cs +++ b/UndertaleModLib/UndertaleIO.cs @@ -155,11 +155,28 @@ public class UndertaleReader : Util.FileBinaryReader private WarningHandlerDelegate WarningHandler; private MessageHandlerDelegate MessageHandler; + /// + /// The detected absolute path of the data file, if a FileStream is passed in, or null otherwise (by default). + /// Can also be manually changed. + /// + public string FilePath { get; set; } = null; + + /// + /// The detected absolute path of the directory containing the data file, if a FileStream is passed in, or null otherwise (by default). + /// Can also be manually changed. + /// + public string Directory { get; set; } = null; + public UndertaleReader(Stream input, WarningHandlerDelegate warningHandler = null, MessageHandlerDelegate messageHandler = null) : base(input) { WarningHandler = warningHandler; MessageHandler = messageHandler; + if (input is FileStream fs) + { + FilePath = fs.Name; + Directory = Path.GetDirectoryName(FilePath); + } } // TODO: This would be more useful if it reported location like the exceptions did @@ -218,6 +235,7 @@ public UndertaleData ReadUndertaleData() data.BuiltinList = new BuiltinList(data); Decompiler.AssetTypeResolver.InitializeTypes(data); + UndertaleEmbeddedTexture.FindAllTextureInfo(data); return data; } diff --git a/UndertaleModTool/MainWindow.xaml.cs b/UndertaleModTool/MainWindow.xaml.cs index cd35da3ce..c68905829 100644 --- a/UndertaleModTool/MainWindow.xaml.cs +++ b/UndertaleModTool/MainWindow.xaml.cs @@ -1143,8 +1143,7 @@ private async Task SaveFile(string filename, bool suppressDebug = false) } UndertaleEmbeddedTexture.TexData.ClearSharedStream(); - if (Data.UseQoiFormat) - QoiConverter.ClearSharedBuffer(); + QoiConverter.ClearSharedBuffer(); if (debugMode != DebugDataDialog.DebugDataMode.NoDebug) {