Last active
May 21, 2022 19:42
-
-
Save JohnnyonFlame/9ed4032eb7226e60c37b255d70993cde to your computer and use it in GitHub Desktop.
Texture Externalizer and Compressor for UndertaleModTool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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