From f3bd61a5d9512451a8a19886d1bb25ee769c3472 Mon Sep 17 00:00:00 2001 From: giroletm <112472361+giroletm@users.noreply.github.com> Date: Sun, 2 Jul 2023 13:57:06 +0200 Subject: [PATCH] Legacy PVR & Sprite packing - Legacy PVR files can now be saved - Sprite packing no longer leaves a ton of empty space --- .gitignore | 397 ++++++++++++++++++++ ABStudio/FileFormats/PVR/PVRFile.cs | 162 +++++++- ABStudio/FileFormats/ZSTREAM/ZSTREAMFile.cs | 2 +- ABStudio/Forms/SpritesheetEditor.cs | 56 ++- ABStudio/Libs/StbRectPackSharp/Packer.cs | 2 +- LICENSE | 2 +- 6 files changed, 601 insertions(+), 20 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1be4e55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,397 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/ABStudio/FileFormats/PVR/PVRFile.cs b/ABStudio/FileFormats/PVR/PVRFile.cs index fcf7434..86c4b74 100644 --- a/ABStudio/FileFormats/PVR/PVRFile.cs +++ b/ABStudio/FileFormats/PVR/PVRFile.cs @@ -13,11 +13,15 @@ namespace ABStudio.FileFormats.PVR public class PVRFile { private IntPtr tex = IntPtr.Zero; + public bool isLegacy = false; public PVRFile(string filename) : this(File.ReadAllBytes(filename)) { } public PVRFile(byte[] pvrData) { + if (pvrData[0] == 0x34 && pvrData[1] == 0 && pvrData[2] == 0 && pvrData[3] == 0) + isLegacy = true; + IntPtr dataPtr = Marshal.AllocHGlobal(pvrData.Length); Marshal.Copy(pvrData, 0, dataPtr, pvrData.Length); @@ -85,12 +89,23 @@ public PVRFile(Bitmap bmp, string format="r4g4b4a4") PVRTexLib.PVRTexLibDestroyTexture(tex); } - public string GetFormat() + public ulong GetFormat() { if (ReferenceEquals(tex, null)) - return ""; + return 0xFFFFFFFF; - ulong val = PVRTexLib.PVRTexLibGetTexturePixelFormat(tex); + IntPtr header = PVRTexLib.PVRTexLibGetTextureHeader(tex); + if (ReferenceEquals(header, null)) + return 0xFFFFFFFF; + + return PVRTexLib.PVRTexLibGetTexturePixelFormat(header); + } + + public string GetFormatStr() + { + ulong val = GetFormat(); + if (val == 0xFFFFFFFF) + return ""; return FormatULongToString(val); } @@ -176,20 +191,155 @@ private IntPtr Transcode(ulong format) public void Save(string filename) { - if (!PVRTexLib.PVRTexLibSaveTextureToFile(tex, filename)) - throw new Exception("Couldn't save PVR file."); + File.WriteAllBytes(filename, Save()); } public byte[] Save() { string fn = System.IO.Path.GetTempFileName(); - Save(fn); + if (!PVRTexLib.PVRTexLibSaveTextureToFile(tex, fn)) + throw new Exception("Couldn't save PVR file."); byte[] bytes = File.ReadAllBytes(fn); File.Delete(fn); + int metaSize = bytes[0x30] | (bytes[0x31] << 8) | (bytes[0x32] << 16) | (bytes[0x33] << 24); + bytes = bytes.Take(0x34).Concat(bytes.Skip(0x34 + metaSize)).ToArray(); + bytes[0x30] = 0; + bytes[0x31] = 0; + bytes[0x32] = 0; + bytes[0x33] = 0; + + if(isLegacy) + { + byte[] newHeader = new byte[0x34]; + + newHeader[0] = 0x34; + newHeader[1] = 0; + newHeader[2] = 0; + newHeader[3] = 0; + + uint height = (uint)(bytes[0x18] | (bytes[0x19] << 8) | (bytes[0x19] << 16) | (bytes[0x19] << 24)); + newHeader[4] = bytes[0x18]; + newHeader[5] = bytes[0x19]; + newHeader[6] = bytes[0x1A]; + newHeader[7] = bytes[0x1B]; + + uint width = (uint)(bytes[0x1C] | (bytes[0x1D] << 8) | (bytes[0x1E] << 16) | (bytes[0x1F] << 24)); + newHeader[8] = bytes[0x1C]; + newHeader[9] = bytes[0x1D]; + newHeader[0xA] = bytes[0x1E]; + newHeader[0xB] = bytes[0x1F]; + + uint mipmapCount = (uint)(bytes[0x2C] | (bytes[0x2D] << 8) | (bytes[0x2E] << 16) | (bytes[0x2F] << 24)) - 1; + newHeader[0xC] = (byte)(mipmapCount & 0xFF); + newHeader[0xD] = (byte)((mipmapCount >> 8) & 0xFF); + newHeader[0xE] = (byte)((mipmapCount >> 16) & 0xFF); + newHeader[0xF] = (byte)((mipmapCount >> 24) & 0xFF); + + ulong currFormat = GetFormat(); + string currFormatStr = FormatULongToString(currFormat); + + ulong rgba4444 = FormatStringToULong("r4g4b4a4"); + ulong rgba8888 = FormatStringToULong("r8g8b8a8"); + ulong rgb565 = FormatStringToULong("r5g6b5\00"); + + if (currFormat == rgba4444) + newHeader[0x10] = 0x10; + else if (currFormat == rgba8888) + newHeader[0x10] = 0x12; + else if (currFormat == rgb565) + newHeader[0x10] = 0x13; + else + throw new Exception("PVR Legacy: unsupported format \"" + currFormatStr + "\"."); + + newHeader[0x11] = 0; + if (mipmapCount > 0) + newHeader[0x11] |= 1; + if (currFormatStr.Contains('a')) + newHeader[0x11] |= 0x80; + + newHeader[0x12] = 0; + newHeader[0x13] = 0; + + uint bpp = PVRTexLib.PVRTexLibGetFormatBitsPerPixel(currFormat); + uint surfSize = (width * height) * (bpp / 8U); + + newHeader[0x14] = (byte)(surfSize & 0xFF); + newHeader[0x15] = (byte)((surfSize >> 8) & 0xFF); + newHeader[0x16] = (byte)((surfSize >> 16) & 0xFF); + newHeader[0x17] = (byte)((surfSize >> 24) & 0xFF); + + newHeader[0x18] = (byte)(bpp & 0xFF); + newHeader[0x19] = (byte)((bpp >> 8) & 0xFF); + newHeader[0x1A] = (byte)((bpp >> 16) & 0xFF); + newHeader[0x1B] = (byte)((bpp >> 24) & 0xFF); + + uint rMask = 0; + uint gMask = 0; + uint bMask = 0; + uint aMask = 0; + + if (currFormat == rgba4444) + { + rMask = 0xF000; + gMask = 0x0F00; + bMask = 0x00F0; + aMask = 0x000F; + } + else if (currFormat == rgba8888) + { + rMask = 0xFF000000; + gMask = 0x00FF0000; + bMask = 0x0000FF00; + aMask = 0x000000FF; + } + else if (currFormat == rgb565) + { + rMask = 0xF800; + gMask = 0x07E0; + bMask = 0x001F; + aMask = 0x0000; + } + else + throw new Exception("PVR Legacy: unsupported format \"" + currFormatStr + "\"."); + + newHeader[0x1C] = (byte)(rMask & 0xFF); + newHeader[0x1D] = (byte)((rMask >> 8) & 0xFF); + newHeader[0x1E] = (byte)((rMask >> 16) & 0xFF); + newHeader[0x1F] = (byte)((rMask >> 24) & 0xFF); + + newHeader[0x20] = (byte)(gMask & 0xFF); + newHeader[0x21] = (byte)((gMask >> 8) & 0xFF); + newHeader[0x22] = (byte)((gMask >> 16) & 0xFF); + newHeader[0x23] = (byte)((gMask >> 24) & 0xFF); + + newHeader[0x24] = (byte)(bMask & 0xFF); + newHeader[0x25] = (byte)((bMask >> 8) & 0xFF); + newHeader[0x26] = (byte)((bMask >> 16) & 0xFF); + newHeader[0x27] = (byte)((bMask >> 24) & 0xFF); + + newHeader[0x28] = (byte)(aMask & 0xFF); + newHeader[0x29] = (byte)((aMask >> 8) & 0xFF); + newHeader[0x2A] = (byte)((aMask >> 16) & 0xFF); + newHeader[0x2B] = (byte)((aMask >> 24) & 0xFF); + + newHeader[0x2C] = (byte)0x50; + newHeader[0x2D] = (byte)0x56; + newHeader[0x2E] = (byte)0x52; + newHeader[0x2F] = (byte)0x21; + + newHeader[0x30] = bytes[0x24]; + newHeader[0x31] = bytes[0x25]; + newHeader[0x32] = bytes[0x26]; + newHeader[0x33] = bytes[0x27]; + + for (int i = 0; i < 0x34; i++) + bytes[i] = newHeader[i]; + } + return bytes; } diff --git a/ABStudio/FileFormats/ZSTREAM/ZSTREAMFile.cs b/ABStudio/FileFormats/ZSTREAM/ZSTREAMFile.cs index b11d686..102c5c6 100644 --- a/ABStudio/FileFormats/ZSTREAM/ZSTREAMFile.cs +++ b/ABStudio/FileFormats/ZSTREAM/ZSTREAMFile.cs @@ -392,7 +392,7 @@ public void SaveBitmap(Bitmap bmp, string path) Bitmap subbmp = bmp.Clone(new Rectangle(pInfo.x, pInfo.y, pInfo.w, pInfo.h), bmp.PixelFormat); PVRFile pvr = new PVRFile(subbmp, fmt); - byte[] asData = pvr.Save().Skip(0x44).ToArray(); + byte[] asData = pvr.Save().Skip(0x34).ToArray(); byte[] header = new byte[0x28]; subbmp.Dispose(); diff --git a/ABStudio/Forms/SpritesheetEditor.cs b/ABStudio/Forms/SpritesheetEditor.cs index 27b4a74..54d2bf5 100644 --- a/ABStudio/Forms/SpritesheetEditor.cs +++ b/ABStudio/Forms/SpritesheetEditor.cs @@ -31,6 +31,8 @@ public partial class SpritesheetEditor : Form private object SelectedObj => spritesheetPictureBox.SelectedSRect; private DATFile.SpriteData.Sprite SelectedSprite => spritesheetPictureBox.GetSRectLinkedObject(SelectedObj) as DATFile.SpriteData.Sprite; + private bool legacyPVR = false; + #region Extensions management private static readonly string[] supportedPicExt = new string[] { "pvr", "png", "jpg", "gif", "bmp", "tiff" }; @@ -180,6 +182,7 @@ private bool SaveSpritesheet() if (ext == ".pvr") { PVRFile pvr = new PVRFile(spritesheet); + pvr.isLegacy = this.legacyPVR; pvr.Save(fname); } else if(ext == ".stream") @@ -236,14 +239,25 @@ private void importSpritesFolderToolStripMenuItem_Click(object sender, EventArgs Packer.PackRectForce(ref packer, bmp.Width + 4, bmp.Height + 4, bmp); } - if (maf.ChosenAnswer == 1) - data.sprites.Clear(); + int left = int.MaxValue; + int top = int.MaxValue; + int right = int.MinValue; + int bottom = int.MinValue; Bitmap full = new Bitmap(packer.Width, packer.Height); using (Graphics g = Graphics.FromImage(full)) { foreach (PackerRectangle rect in packer.PackRectangles) { + if (rect.X < left) + left = rect.X; + if (rect.Y < top) + top = rect.Y; + if ((rect.X + rect.Width) > right) + right = rect.X + rect.Width; + if ((rect.Y + rect.Height) > bottom) + bottom = rect.Y + rect.Height; + Bitmap bmp = rect.Data as Bitmap; Rectangle mainRect = new Rectangle(rect.X + 2, rect.Y + 2, bmp.Width, bmp.Height); g.DrawImage(bmp, mainRect, new Rectangle(0, 0, bmp.Width, bmp.Height), GraphicsUnit.Pixel); @@ -253,25 +267,43 @@ private void importSpritesFolderToolStripMenuItem_Click(object sender, EventArgs g.DrawImage(bmp, new Rectangle(rect.X+2, rect.Y+1, bmp.Width, 1), new Rectangle(0, 0, bmp.Width, 1), GraphicsUnit.Pixel); g.DrawImage(bmp, new Rectangle(rect.X+2, rect.Y+bmp.Height+2, bmp.Width, 1), new Rectangle(0, bmp.Height-1, bmp.Width, 1), GraphicsUnit.Pixel); + } + } + + full = full.Clone(new Rectangle(left, top, right - left, bottom - top), full.PixelFormat); + + if (full.Width > 2048 || full.Height > 2048) + { + MessageBox.Show("Couldn't fit your sprites in a 2048x2048 spritesheet", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } - if (maf.ChosenAnswer != 2) - { - DATFile.SpriteData.Sprite sprite = new DATFile.SpriteData.Sprite(); - sprite.name = names[bmps.IndexOf(bmp)]; - sprite.rect = mainRect; - sprite.orig = new Point(sprite.rect.Width / 2, sprite.rect.Height / 2); + if (maf.ChosenAnswer == 1) + data.sprites.Clear(); + + if (maf.ChosenAnswer != 2) + { + foreach (PackerRectangle rect in packer.PackRectangles) + { + DATFile.SpriteData.Sprite sprite = new DATFile.SpriteData.Sprite(); + + Bitmap bmp = rect.Data as Bitmap; - data.sprites.Add(sprite); - } + sprite.name = names[bmps.IndexOf(rect.Data as Bitmap)]; + sprite.rect = new Rectangle(rect.X + 2 - left, rect.Y + 2 - top, bmp.Width, bmp.Height); + sprite.orig = new Point(sprite.rect.Width / 2, sprite.rect.Height / 2); + + data.sprites.Add(sprite); } } + spritesheet = full; spritesheetPictureBox.Image = spritesheet; RefreshRects(); RefreshZoom(); - filenameTextBox.Text = "-- IMPORTED SPRITE FOLDER, REPLACE THIS --"; + filenameTextBox.Text = Path.GetFileName(dialog.ResultPath) + ".png"; } } } @@ -368,6 +400,7 @@ private void LoadBitmap(string path=null) path = null; bool hasSpecifiedPath = path != null; + legacyPVR = false; if (data.filenames.Count <= 0 && !hasSpecifiedPath) { @@ -380,6 +413,7 @@ private void LoadBitmap(string path=null) if(ext == ".pvr") { PVRFile pvr = new PVRFile(fullPath); + legacyPVR = pvr.isLegacy; spritesheet = pvr.AsBitmap(); } else if(fullPath.EndsWith(".stream") || ext == ".zstream") diff --git a/ABStudio/Libs/StbRectPackSharp/Packer.cs b/ABStudio/Libs/StbRectPackSharp/Packer.cs index 01e90e7..54fd65e 100644 --- a/ABStudio/Libs/StbRectPackSharp/Packer.cs +++ b/ABStudio/Libs/StbRectPackSharp/Packer.cs @@ -122,7 +122,7 @@ public static PackerRectangle PackRectForce(ref Packer packer, int width, int he // Double the size of the packer until the new rectangle will fit while (pr == null) { - Packer newPacker = new Packer(packer.Width + 256, packer.Height + 256); + Packer newPacker = new Packer(packer.Width + 128, packer.Height + 128); // Place existing rectangles foreach (PackerRectangle existingRect in packer.PackRectangles) diff --git a/LICENSE b/LICENSE index f288702..60f217e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2023 RSM & Contributors Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.