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)
{