Skip to content

Commit

Permalink
Work-in-progress 2022.9 support
Browse files Browse the repository at this point in the history
Saving external textures currently does not work
  • Loading branch information
colinator27 committed Nov 21, 2022
1 parent 5053582 commit 68057e1
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 36 deletions.
215 changes: 192 additions & 23 deletions UndertaleModLib/Models/UndertaleEmbeddedTexture.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -30,20 +31,57 @@ public class UndertaleEmbeddedTexture : UndertaleNamedResource, IDisposable

/// <summary>
/// The amount of generated mipmap levels. <br/>
/// GameMaker Studio: 2 only.
/// GameMaker Studio 2 only.
/// </summary>
public uint GeneratedMips { get; set; }

/// <summary>
/// TODO: something. <br/>
/// GameMaker: Studio 2 only.
/// Size of the texture block in bytes. Only appears in later 2022 versions of GameMaker.
/// </summary>
public uint TextureBlockSize { get; set; }

/// <summary>
/// The texture data in the embedded image.
/// </summary>
public TexData TextureData { get; set; } = new TexData();
public TexData TextureData { get { return _textureData ?? (_textureData = LoadExternalTexture()); } set { _textureData = value; } }
private TexData _textureData = new TexData();


/// <summary>
/// Helper variable for whether or not this texture is to be stored externally or not.
/// </summary>
public bool TextureExternal { get; set; } = false;


/// <summary>
/// Helper variable for whether or not an external texture was loaded yet.
/// </summary>
public bool TextureExternallyLoaded { get; set; } = false;

/// <summary>
/// Width of the texture. 2022.9+ only.
/// </summary>
public int TextureWidth { get; set; }

/// <summary>
/// Height of the texture. 2022.9+ only.
/// </summary>
public int TextureHeight { get; set; }

/// <summary>
/// Index of the texture in the texture group. 2022.9+ only.
/// </summary>
public int IndexInGroup { get; set; }

/// <summary>
/// Helper reference to texture group info, if found in the data file.
/// </summary>
public UndertaleTextureGroupInfo TextureInfo { get; set; }

/// <summary>
/// Helper for 2022.9+ support. Stores copy of the path to the data file.
/// </summary>
private string _2022_9_GameDirectory { get; set; }

/// <inheritdoc />
public void Serialize(UndertaleWriter writer)
Expand All @@ -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);
}

/// <inheritdoc />
Expand All @@ -64,33 +111,116 @@ public void Unserialize(UndertaleReader reader)
GeneratedMips = reader.ReadUInt32();
if (reader.undertaleData.GM2022_3)
TextureBlockSize = reader.ReadUInt32();
TextureData = reader.ReadUndertaleObjectPointer<TexData>();
if (reader.undertaleData.GM2022_9)
{
TextureWidth = reader.ReadInt32();
TextureHeight = reader.ReadInt32();
IndexInGroup = reader.ReadInt32();
_2022_9_GameDirectory = reader.Directory;
}
_textureData = reader.ReadUndertaleObjectPointer<TexData>();
TextureExternal = (_textureData == null);
}

/// <summary>
/// TODO!
/// Serializes the in-file texture blob for this texture.
/// </summary>
/// <param name="writer">Where to serialize to.</param>
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);
}

/// <summary>
/// TODO!
/// Deserializes the in-file texture blob for this texture.
/// </summary>
/// <param name="reader">Where to deserialize from.</param>
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);
}

/// <summary>
/// Assigns texture group info to every embedded texture in the supplied data file.
/// </summary>
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;
}

/// <summary>
/// Attempts to load the corresponding external texture. Should only happen in 2022.9 and above.
/// </summary>
/// <returns></returns>
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;
}

/// <inheritdoc />
Expand All @@ -108,8 +238,10 @@ public void Dispose()
{
GC.SuppressFinalize(this);

TextureData.Dispose();
_textureData.Dispose();
_textureData = null;
Name = null;
TextureInfo = null;
}

/// <summary>
Expand Down Expand Up @@ -164,15 +296,36 @@ public int Height
}
}

/// <summary>
/// Whether this texture uses QOI format.
/// </summary>
public bool FormatQOI { get; set; } = false;

/// <summary>
/// Whether this texture uses BZ2 format. (Always used in combination with QOI.)
/// </summary>
public bool FormatBZ2 { get; set; } = false;

/// <inheritdoc />
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

/// <summary>
/// Header used for PNG files.
/// </summary>
public static readonly byte[] PNGHeader = { 137, 80, 78, 71, 13, 10, 26, 10 };

/// <summary>
/// Header used for GameMaker QOI + BZ2 files.
/// </summary>
public static readonly byte[] QOIAndBZip2Header = { 50, 122, 111, 113 };

/// <summary>
/// Header used for GameMaker QOI files.
/// </summary>
public static readonly byte[] QOIHeader = { 102, 105, 111, 113 };

/// <summary>
Expand All @@ -193,30 +346,38 @@ public static void ClearSharedStream()
/// <inheritdoc />
public void Serialize(UndertaleWriter writer)
{
if (writer.undertaleData.UseQoiFormat)
Serialize(writer, writer.undertaleData.GM2022_3, writer.undertaleData.GM2022_5);
}

/// <summary>
/// Serializes the texture to any type of writer (can be any destination file).
/// </summary>
public void Serialize(FileBinaryWriter writer, bool gm2022_3, bool gm2022_5)
{
if (FormatQOI)
{
if (writer.undertaleData.UseBZipFormat)
if (FormatBZ2)
{
writer.Write(QOIAndBZip2Header);

// Encode the PNG data back to QOI+BZip2
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
Expand All @@ -225,6 +386,14 @@ public void Serialize(UndertaleWriter writer)

/// <inheritdoc />
public void Unserialize(UndertaleReader reader)
{
Unserialize(reader, reader.undertaleData.GM2022_5);
}

/// <summary>
/// Unserializes the texture from any type of reader (can be from any source).
/// </summary>
public void Unserialize(FileBinaryReader reader, bool is_2022_5)
{
sharedStream ??= new();

Expand All @@ -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)
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 68057e1

Please sign in to comment.