Skip to content

Instantly share code, notes, and snippets.

@JohnnyonFlame
Last active May 21, 2022 19:42
Show Gist options
  • Save JohnnyonFlame/9ed4032eb7226e60c37b255d70993cde to your computer and use it in GitHub Desktop.
Save JohnnyonFlame/9ed4032eb7226e60c37b255d70993cde to your computer and use it in GitHub Desktop.
Texture Externalizer and Compressor for UndertaleModTool
// Texture Externalizer and Compressor by JohnnyonFlame
// Licensed under GPLv3 for UndertaleModTool
// Adapted from ExportAllEmbeddedTextures.csx
// By externalizing textures from the data file, we can lower the amount of data
// the runner is forced to preload on boot, and also gain the ability to load arbitrary
// data formats, such as loading PVR or DXTx compressed data, this allows for massive
// memory usage gains on embedded platforms such as the PSVita, and also significantly
// lower the Memory Bandwidth when heavy textures are in usage.
using System.Text;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Drawing;
using System.Drawing.Imaging;
using System.Diagnostics;
EnsureDataLoaded();
// Current external id
int currentId = 0;
// Constant for the external folder name
const string extFolderName = "ExternalizedTextures";
// The folder data.win is located in.
string extFolder = Path.Combine(GetFolder(FilePath), extFolderName);
// Compression parameters
int compressFailCount = 0;
bool compressTextures = false;
string pvrTexToolFormat = "PVRTCII_4BPP,UB,lRGB";
string pvrTexToolCliPath = "";
// Check if folder already exists, and if so, if we're deleting it or aborting
if (!CanOverwrite())
return;
// Create folder
MakeFolder(extFolder);
// Check if user wants to compress textures
compressTextures = ScriptQuestion("Do you want to compress externalized texture data?\nWarning: this process is slow and CPU taxing!");
if (compressTextures)
{
pvrTexToolCliPath = PromptLoadFile("exe", "PVRTexToolCLI.exe|PVRTexToolCLI.exe|All files|*");
if (pvrTexToolCliPath == null)
{
ScriptMessage("PVRTexToolCLI binary not provided, aborting.");
return;
}
}
SetProgressBar("Externalizing spine textures...", "Externalized spines", 0, Data.Sprites.Count);
StartProgressBarUpdater();
await Task.Run(() =>
{
for (int i = 0; i < Data.Sprites.Count; i++)
{
var sprite = Data.Sprites[i];
// Only process spine textures...
if (sprite.SSpriteType != UndertaleSprite.SpriteType.Spine)
{
IncrementProgress();
continue;
}
for (int j = 0; j < sprite.SpineTextures.Count; j++)
{
var spineTex = sprite.SpineTextures[j];
string pngFilePath = Path.Combine(extFolder, currentId + ".png");
string pvrFilePath = Path.Combine(extFolder, currentId + ".pvr");
try
{
File.WriteAllBytes(pngFilePath, spineTex.PNGBlob);
}
catch (Exception ex)
{
ScriptMessage("Failed to export file: " + ex.Message);
}
spineTex.PNGBlob = GetPlaceholderTexturePNGBytes(currentId);
CompressIfNeeded(pngFilePath, pvrFilePath);
currentId++;
}
IncrementProgress();
}
});
SetProgressBar("Externalizing embedded textures...", "Externalized textures", 0, Data.EmbeddedTextures.Count);
await Task.Run(() =>
{
for (int i = 0; i < Data.EmbeddedTextures.Count; i++)
{
string pngFilePath = Path.Combine(extFolder, currentId + ".png");
string pvrFilePath = Path.Combine(extFolder, currentId + ".pvr");
try
{
File.WriteAllBytes(pngFilePath, Data.EmbeddedTextures[i].TextureData.TextureBlob);
}
catch (Exception ex)
{
ScriptMessage($"Failed to export file: {ex.Message}");
}
Data.EmbeddedTextures[i].TextureData.TextureBlob = GetPlaceholderTexturePNGBytes(currentId);
CompressIfNeeded(pngFilePath, pvrFilePath);
currentId++;
IncrementProgress();
}
});
await StopProgressBarUpdater();
HideProgressBar();
// Only PNG placeholders supported, forcefully disable Qoi
Data.UseQoiFormat = false;
Data.UseBZipFormat = false;
if (compressFailCount == 0)
{
ScriptMessage("Externalization Completed.\n\nLocation: " + extFolder);
}
else
{
ScriptMessage($"Externalization Completed.\n{compressFailCount} files were left uncompressed.\n\nLocation: " + extFolder);
}
// Helper functions below //
// Converts from RGBA (little endian) to ARGB (big endian)
int FromRGBALE(UInt32 col)
{
// 0xAABBGGRR -> 0xAARRGGBB
return (int) // ABGR -> ARGB
((((col >> 24) & 0xFF) << 24) | // A___ -> A___
(((col >> 16) & 0xFF) ) | // _B__ -> ___B
(((col >> 8) & 0xFF) << 8) | // __G_ -> __G_
(((col ) & 0xFF) << 16)); // ___R -> _R__
}
string GetFolder(string path)
{
return Path.GetDirectoryName(path) + Path.DirectorySeparatorChar;
}
void MakeFolder(String folder)
{
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
}
// Tries to delete the texturesFolder if it doesn't exist. Returns false if the user does not want the folder deleted.
bool CanOverwrite()
{
// Overwrite Folder Check One
if (!Directory.Exists(extFolder))
return true;
bool overwriteCheckOne = ScriptQuestion("An 'ExternalizedTextures' folder already exists.\nWould you like to remove it?\n\nNote: If an error window stating that 'the directory is not empty' appears, please try again or delete the folder manually.\n");
if (!overwriteCheckOne)
{
ScriptError("An 'ExternalizedTextures' folder already exists. Please remove it.", "Export already exists.");
return false;
}
Directory.Delete(extFolder, true);
return true;
}
// Create placeholder ID texture, little endian
byte[] GetPlaceholderTexturePNGBytes(int id)
{
Bitmap bmp = new Bitmap(2, 1, PixelFormat.Format32bppArgb);
// Encode our placeholder texture
// Magic number (DE AD BE FF)
// ID (ii ii ii FF => <i>.png or <i>.pvr)
bmp.SetPixel(0, 0, Color.FromArgb(FromRGBALE(0xFFBEADDE)));
bmp.SetPixel(1, 0, Color.FromArgb(FromRGBALE(0xFF000000 + (UInt32)id)));
using (MemoryStream stream = new MemoryStream())
{
bmp.Save(stream, ImageFormat.Png);
return(stream.ToArray());
}
}
void CompressIfNeeded(string pngFilePath, string pvrFilePath)
{
if (!compressTextures)
return;
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = pvrTexToolCliPath;
startInfo.Arguments = $"-ics lRGB -f {pvrTexToolFormat} -i \"{pngFilePath}\" -o \"{pvrFilePath}\"";
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
using (Process process = Process.Start(startInfo))
{
process.WaitForExit();
// If compression succeeded, we don't need the png anymore, otherwise we want to keep them.
// Different compression schemes can fail for different reasons, if that ever occurs, we're just
// keeping original pngs as-is.
if (process.ExitCode == 0)
{
File.Delete(pngFilePath);
}
else
{
compressFailCount++;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment