From 025cf96a5af7f92e285bdfa2177dbe0bda548108 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sat, 25 Oct 2025 22:47:15 -0500 Subject: [PATCH 01/12] Add pack/unpack animation frames to JSON/PNG Introduces functionality to export animation frames as packed JSON/PNG sprite sheets and import them back, including new context menu items and dialogs for options. Adds supporting classes for JSON structure in PackerClasses.cs and integrates handlers in AnimationListControl for packing and unpacking frames. --- .../UserControls/AnimDataControl.cs | 1 + .../AnimationListControl.Designer.cs | 23 +- .../UserControls/AnimationListControl.cs | 440 ++++++++++++++++-- UoFiddler/PackerClasses.cs | 72 +++ 4 files changed, 504 insertions(+), 32 deletions(-) create mode 100644 UoFiddler/PackerClasses.cs diff --git a/UoFiddler.Controls/UserControls/AnimDataControl.cs b/UoFiddler.Controls/UserControls/AnimDataControl.cs index 3fa2799..158f369 100644 --- a/UoFiddler.Controls/UserControls/AnimDataControl.cs +++ b/UoFiddler.Controls/UserControls/AnimDataControl.cs @@ -289,6 +289,7 @@ private void OnClickExport(object sender, EventArgs e) _exportForm.Show(); } + private void OnClickImport(object sender, EventArgs e) { if (_importForm?.IsDisposed == false) diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs b/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs index 7723218..9323488 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs @@ -85,7 +85,10 @@ private void InitializeComponent() tryToFindNewGraphicsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); animationEditToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); GraphicLabel = new System.Windows.Forms.ToolStripStatusLabel(); - BaseGraphicLabel = new System.Windows.Forms.ToolStripStatusLabel(); + BaseGraphicLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.packFramesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.unpackFramesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + HueLabel = new System.Windows.Forms.ToolStripStatusLabel(); ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); splitContainer1.Panel1.SuspendLayout(); @@ -273,7 +276,7 @@ private void InitializeComponent() // // extractAnimationToolStripMenuItem // - extractAnimationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { asBmpToolStripMenuItem1, asTiffToolStripMenuItem1, asJpgToolStripMenuItem1, asPngToolStripMenuItem1, asAnimatedGifToolStripMenuItem, asAnimatedGifnoLoopingToolStripMenuItem }); + extractAnimationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { asBmpToolStripMenuItem1, asTiffToolStripMenuItem1, asJpgToolStripMenuItem1, asPngToolStripMenuItem1, asAnimatedGifToolStripMenuItem, asAnimatedGifnoLoopingToolStripMenuItem, new System.Windows.Forms.ToolStripSeparator(), this.packFramesToolStripMenuItem, this.unpackFramesToolStripMenuItem }); extractAnimationToolStripMenuItem.Name = "extractAnimationToolStripMenuItem"; extractAnimationToolStripMenuItem.Size = new System.Drawing.Size(173, 22); extractAnimationToolStripMenuItem.Text = "Export Animation.."; @@ -556,7 +559,19 @@ private void InitializeComponent() HueLabel.RightToLeft = System.Windows.Forms.RightToLeft.No; HueLabel.Size = new System.Drawing.Size(32, 17); HueLabel.Text = "Hue:"; - HueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + HueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // packFramesToolStripMenuItem + // + this.packFramesToolStripMenuItem.Name = "packFramesToolStripMenuItem"; + this.packFramesToolStripMenuItem.Size = new System.Drawing.Size(221, 22); + this.packFramesToolStripMenuItem.Text = "Pack Frames to JSON/PNG"; + // + // unpackFramesToolStripMenuItem + // + this.unpackFramesToolStripMenuItem.Name = "unpackFramesToolStripMenuItem"; + this.unpackFramesToolStripMenuItem.Size = new System.Drawing.Size(221, 22); + this.unpackFramesToolStripMenuItem.Text = "Unpack Frames from JSON"; // // AnimationListControl // @@ -641,5 +656,7 @@ private void InitializeComponent() private System.Windows.Forms.CheckBox AnimateCheckBox; private System.Windows.Forms.CheckBox ShowFrameBoundsCheckBox; private System.Windows.Forms.Label directionLabel; + private System.Windows.Forms.ToolStripMenuItem packFramesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem unpackFramesToolStripMenuItem; } } diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index 672ec2b..b0dfc2c 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -18,6 +18,8 @@ using System.Linq; using System.Windows.Forms; using System.Xml; +using System.Text.Json; +using System.Text.Json.Serialization; using Ultima; using UoFiddler.Controls.Classes; using UoFiddler.Controls.Forms; @@ -34,8 +36,12 @@ public AnimationListControl() SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true); // TODO can this be moved into the control itself? listView1.Height += SystemInformation.HorizontalScrollBarHeight; + // Add handlers for new context menu items + packFramesToolStripMenuItem.Click += OnPackFramesClick; + unpackFramesToolStripMenuItem.Click += OnUnpackFramesClick; } + public string[][] GetActionNames { get; } = { // Monster new[] @@ -945,57 +951,433 @@ private void ShowFrameBoundsCheckBox_Click(object sender, EventArgs e) MainPictureBox.ShowFrameBounds = !MainPictureBox.ShowFrameBounds; ShowFrameBoundsCheckBox.Checked = MainPictureBox.ShowFrameBounds; } - } - public class AlphaSorter : IComparer - { - public int Compare(object x, object y) + private async void OnPackFramesClick(object? sender, EventArgs e) { - TreeNode tx = x as TreeNode; - TreeNode ty = y as TreeNode; - if (tx.Parent == null) // don't change Mob and Equipment + if (_currentSelect == 0) + { + MessageBox.Show("No graphic selected.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // show pack options dialog + using var optionsForm = new PackOptionsForm(); + if (optionsForm.ShowDialog() != DialogResult.OK) { - return (int)tx.Tag == -1 ? -1 : 1; + return; } - if (tx.Parent.Parent != null) + + var selectedDirections = optionsForm.SelectedDirections; // list + int maxWidth = optionsForm.MaxWidth; + + // Ask for output base name/location + using (var dlg = new FolderBrowserDialog()) + { + dlg.Description = "Select folder to save packed sprite and JSON"; + dlg.ShowNewFolderButton = true; + if (dlg.ShowDialog() != DialogResult.OK) + { + return; + } + + string outDir = dlg.SelectedPath; + + try + { + Cursor.Current = Cursors.WaitCursor; + + // Collect frames for directions 0..4 (common editable directions) + var packedFrames = new List(); + + int body = _currentSelect; + Animations.Translate(ref body); + int hue = 0; // do not preserve hue here + + var images = new List(); + + int currentX = 0, currentY = 0, rowHeight = 0, canvasWidth = 0, canvasHeight = 0; + + foreach (int dir in selectedDirections) + { + int localHue = 0; + var frames = Animations.GetAnimation(_currentSelect, _currentSelectAction, dir, ref localHue, false, false); + if (frames == null || frames.Length == 0) + { + continue; + } + + for (int fi = 0; fi < frames.Length; fi++) + { + var anim = frames[fi]; + if (anim?.Bitmap == null) + { + continue; + } + + // determine size + int w = anim.Bitmap.Width; + int h = anim.Bitmap.Height; + + if (currentX + w > maxWidth) + { + currentY += rowHeight; + currentX = 0; + rowHeight = 0; + } + + if (currentX == 0) + rowHeight = h; + + var entry = new PackedFrameEntry + { + Direction = dir, + Index = fi, + Frame = new Rect { X = currentX, Y = currentY, W = w, H = h }, + Center = new PointStruct { X = anim.Center.X, Y = anim.Center.Y } + }; + + packedFrames.Add(entry); + + // store image copy + images.Add(new Bitmap(anim.Bitmap)); + + currentX += w; + canvasWidth = Math.Max(canvasWidth, currentX); + canvasHeight = Math.Max(canvasHeight, currentY + rowHeight); + } + } + + if (images.Count == 0) + { + MessageBox.Show("No frames found to pack.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // Create sprite sheet and paste images + using (var sprite = new Bitmap(Math.Max(1, canvasWidth), Math.Max(1, canvasHeight))) + using (var g = Graphics.FromImage(sprite)) + { + g.Clear(Color.Transparent); + + for (int i = 0; i < images.Count; i++) + { + var img = images[i]; + var rect = packedFrames[i].Frame; + g.DrawImage(img, rect.X, rect.Y, rect.W, rect.H); + img.Dispose(); + } + + string baseName = $"anim_{_currentSelect}_{_currentSelectAction}"; + string imageFile = Path.Combine(outDir, baseName + ".png"); + sprite.Save(imageFile, ImageFormat.Png); + + // prepare JSON + var outObj = new PackedOutput + { + Meta = new PackedMeta { Image = Path.GetFileName(imageFile), Size = new SizeStruct { W = sprite.Width, H = sprite.Height }, Format = "RGBA8888" }, + Frames = packedFrames + }; + + string jsonFile = Path.Combine(outDir, baseName + ".json"); + var jsOptions = new JsonSerializerOptions { WriteIndented = true }; + string json = JsonSerializer.Serialize(outObj, jsOptions); + File.WriteAllText(jsonFile, json); + + MessageBox.Show($"Saved sprite: {imageFile}\nSaved JSON: {jsonFile}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + catch (Exception ex) + { + MessageBox.Show($"Failed to pack frames: {ex.Message}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + Cursor.Current = Cursors.Default; + } + } + } + + private void OnUnpackFramesClick(object? sender, EventArgs e) + { + if (_currentSelect == 0) { - return (int)tx.Tag - (int)ty.Tag; + MessageBox.Show("No graphic selected.", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; } - return string.CompareOrdinal(tx.Text, ty.Text); + using (var ofd = new OpenFileDialog()) + { + ofd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*"; + ofd.Title = "Select packing JSON file"; + if (ofd.ShowDialog() != DialogResult.OK) + { + return; + } + + string jsonFile = ofd.FileName; + try + { + string json = File.ReadAllText(jsonFile); + var doc = JsonSerializer.Deserialize(json); + if (doc == null) + { + MessageBox.Show("Invalid JSON file.", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + string spritePath = Path.Combine(Path.GetDirectoryName(jsonFile) ?? string.Empty, doc.Meta.Image); + if (!File.Exists(spritePath)) + { + MessageBox.Show($"Sprite sheet not found: {spritePath}", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + using (var sprite = new Bitmap(spritePath)) + { + // determine body/fileType for import + int bodyTrans = _currentSelect; + Animations.Translate(ref bodyTrans); + int fileType = BodyConverter.Convert(ref bodyTrans); + + // Group frames by direction + var groups = doc.Frames.GroupBy(f => f.Direction).ToDictionary(g => g.Key, g => g.OrderBy(f => f.Index).ToList()); + + foreach (var kv in groups) + { + int dir = kv.Key; + var framesList = kv.Value; + + AnimIdx animIdx = AnimationEdit.GetAnimation(fileType, bodyTrans, _currentSelectAction, dir); + if (animIdx == null) + { + MessageBox.Show($"Failed to get AnimIdx for direction {dir}", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Warning); + continue; + } + + // Ask whether to overwrite frames for this direction + var res = MessageBox.Show($"Overwrite existing frames for direction {dir}? (Yes = overwrite, No = append)", "Unpack Frames", MessageBoxButtons.YesNoCancel); + if (res == DialogResult.Cancel) + { + return; + } + + if (res == DialogResult.Yes) + { + animIdx.ClearFrames(); + } + + foreach (var frameEntry in framesList) + { + var r = frameEntry.Frame; + var crop = sprite.Clone(new Rectangle(r.X, r.Y, r.W, r.H), System.Drawing.Imaging.PixelFormat.Format32bppArgb); + // convert to 16bpp used by AnimIdx methods - AnimIdx.AddFrame expects Bitmap in proper pixel format; conversion happens in FrameEdit + Bitmap bmp16 = new Bitmap(crop); + crop.Dispose(); + + animIdx.AddFrame(bmp16, frameEntry.Center.X, frameEntry.Center.Y); + } + } + + Options.ChangedUltimaClass["Animations"] = true; + + MessageBox.Show("Import finished. Remember to save animations via AnimationEdit.Save if needed.", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + catch (Exception ex) + { + MessageBox.Show($"Failed to unpack/import frames: {ex.Message}", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } } - } - public class GraphicSorter : IComparer - { - public int Compare(object x, object y) + // Helper types for JSON + private class PackedOutput + { + [JsonPropertyName("meta")] public PackedMeta Meta { get; set; } + [JsonPropertyName("frames")] public List Frames { get; set; } + } + + private class PackedMeta + { + [JsonPropertyName("image")] public string Image { get; set; } + [JsonPropertyName("size")] public SizeStruct Size { get; set; } + [JsonPropertyName("format")] public string Format { get; set; } + } + + private class PackedFrameEntry + { + [JsonPropertyName("direction")] public int Direction { get; set; } + [JsonPropertyName("index")] public int Index { get; set; } + [JsonPropertyName("frame")] public Rect Frame { get; set; } + [JsonPropertyName("center")] public PointStruct Center { get; set; } + } + + private class Rect { - TreeNode tx = x as TreeNode; - TreeNode ty = y as TreeNode; - if (tx.Parent == null) + [JsonPropertyName("x")] public int X { get; set; } + [JsonPropertyName("y")] public int Y { get; set; } + [JsonPropertyName("w")] public int W { get; set; } + [JsonPropertyName("h")] public int H { get; set; } + } + + private class PointStruct + { + [JsonPropertyName("x")] public int X { get; set; } + [JsonPropertyName("y")] public int Y { get; set; } + } + + private class SizeStruct + { + [JsonPropertyName("w")] public int W { get; set; } + [JsonPropertyName("h")] public int H { get; set; } + } + + private class AlphaSorter : IComparer + { + public int Compare(object x, object y) { - return (int)tx.Tag == -1 ? -1 : 1; + TreeNode tx = x as TreeNode; + TreeNode ty = y as TreeNode; + if (tx.Parent == null) // don't change Mob and Equipment + { + return (int)tx.Tag == -1 ? -1 : 1; + } + if (tx.Parent.Parent != null) + { + return (int)tx.Tag - (int)ty.Tag; + } + + return string.CompareOrdinal(tx.Text, ty.Text); } + } - if (tx.Parent.Parent != null) + public class GraphicSorter : IComparer + { + public int Compare(object x, object y) { - return (int)tx.Tag - (int)ty.Tag; + TreeNode tx = x as TreeNode; + TreeNode ty = y as TreeNode; + if (tx.Parent == null) + { + return (int)tx.Tag == -1 ? -1 : 1; + } + + if (tx.Parent.Parent != null) + { + return (int)tx.Tag - (int)ty.Tag; + } + + int[] ix = (int[])tx.Tag; + int[] iy = (int[])ty.Tag; + + if (ix[0] == iy[0]) + { + return 0; + } + + if (ix[0] < iy[0]) + { + return -1; + } + + return 1; } + } + + // Pack options dialog (moved here to ensure compilation visibility) + public class PackOptionsForm : Form + { + private CheckedListBox _directionsBox; + private NumericUpDown _maxWidthUpDown; + private Button _ok; + private Button _cancel; - int[] ix = (int[])tx.Tag; - int[] iy = (int[])ty.Tag; + public List SelectedDirections { get; private set; } = new List { 0, 1, 2, 3, 4 }; + public int MaxWidth { get; private set; } = 2048; - if (ix[0] == iy[0]) + public PackOptionsForm() { - return 0; + Text = "Pack Options"; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + StartPosition = FormStartPosition.CenterParent; + // increased size to accommodate taller direction list and wider right side + ClientSize = new Size(520, 360); + Padding = new Padding(10); + + Label lbl = new Label { Text = "Directions:", Location = new Point(12, 12), AutoSize = true }; + Controls.Add(lbl); + + _directionsBox = new CheckedListBox + { + Location = new Point(12, 32), + // make dropdown / list taller + Size = new Size(160, 260), + CheckOnClick = true, + ScrollAlwaysVisible = true, + IntegralHeight = false + }; + for (int i = 0; i < 8; i++) + { + _directionsBox.Items.Add(i.ToString(), i <= 4); + } + Controls.Add(_directionsBox); + + // move the max sprite width controls further to the right + Label lbl2 = new Label { Text = "Max sprite width:", Location = new Point(190, 32), AutoSize = true }; + Controls.Add(lbl2); + + _maxWidthUpDown = new NumericUpDown + { + Location = new Point(190, 56), + Size = new Size(260, 30), + Minimum = 256, + Maximum = 8192, + Increment = 64, + Value = 2048, + ThousandsSeparator = true + }; + Controls.Add(_maxWidthUpDown); + + // Optional quick presets (moved right and made slightly taller) + var presetsLabel = new Label { Text = "Presets:", Location = new Point(190, 95), AutoSize = true }; + Controls.Add(presetsLabel); + var presetSmall = new Button { Text = "1024", Location = new Point(190, 115), Size = new Size(70, 34) }; + var presetMedium = new Button { Text = "2048", Location = new Point(266, 115), Size = new Size(70, 34) }; + var presetLarge = new Button { Text = "4096", Location = new Point(342, 115), Size = new Size(70, 34) }; + presetSmall.Click += (s, e) => _maxWidthUpDown.Value = 1024; + presetMedium.Click += (s, e) => _maxWidthUpDown.Value = 2048; + presetLarge.Click += (s, e) => _maxWidthUpDown.Value = 4096; + Controls.Add(presetSmall); + Controls.Add(presetMedium); + Controls.Add(presetLarge); + + // make OK/Cancel taller and move to the right (anchored) + _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(330, 300), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + _ok.Click += Ok_Click; + Controls.Add(_ok); + + _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(440, 300), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + Controls.Add(_cancel); + + AcceptButton = _ok; + CancelButton = _cancel; } - if (ix[0] < iy[0]) + private void Ok_Click(object sender, EventArgs e) { - return -1; - } + // Collect checked directions as integers + SelectedDirections = _directionsBox.CheckedItems.Cast().Select(o => int.Parse(o.ToString())).ToList(); + if (SelectedDirections.Count == 0) + { + MessageBox.Show("Select at least one direction.", "Pack Options", MessageBoxButtons.OK, MessageBoxIcon.Warning); + DialogResult = DialogResult.None; + return; + } - return 1; + MaxWidth = (int)_maxWidthUpDown.Value; + } } } } diff --git a/UoFiddler/PackerClasses.cs b/UoFiddler/PackerClasses.cs new file mode 100644 index 0000000..09986f5 --- /dev/null +++ b/UoFiddler/PackerClasses.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace UoFiddler.Controls.Classes +{ + /// + /// Represents the structure of the JSON file for packed animations. + /// Based on the provided Python scripts. + /// + public class AnimationPackFile + { + [JsonPropertyName("meta")] + public AnimationMetaData Meta { get; set; } + + [JsonPropertyName("frames")] + public List Frames { get; set; } + } + + public class AnimationMetaData + { + [JsonPropertyName("image")] + public string Image { get; set; } + + [JsonPropertyName("size")] + public FrameSize Size { get; set; } + + [JsonPropertyName("format")] + public string Format { get; set; } + } + + public class FrameSize + { + [JsonPropertyName("w")] + public int W { get; set; } + + [JsonPropertyName("h")] + public int H { get; set; } + } + + public class AnimationFrameData + { + [JsonPropertyName("filename")] + public string Filename { get; set; } + + [JsonPropertyName("frame")] + public FrameRect Frame { get; set; } + + [JsonPropertyName("sourceW")] + public int SourceW { get; set; } + + [JsonPropertyName("sourceH")] + public int SourceH { get; set; } + + [JsonPropertyName("format")] + public string Format { get; set; } + } + + public class FrameRect + { + [JsonPropertyName("x")] + public int X { get; set; } + + [JsonPropertyName("y")] + public int Y { get; set; } + + [JsonPropertyName("w")] + public int W { get; set; } + + [JsonPropertyName("h")] + public int H { get; set; } + } +} From 9d41dee8d536ca307ca802f8a13bf4de75dd5057 Mon Sep 17 00:00:00 2001 From: Moshu Date: Thu, 20 Nov 2025 20:39:15 -0600 Subject: [PATCH 02/12] Improve animation frame import workflow Refactored the animation frame import process to prompt the user once for overwrite/append behavior across all directions, improving usability. Added palette building and region extraction helpers for more efficient and accurate color mapping. Enhanced error handling and memory management during large imports. --- .../UserControls/AnimDataControl.cs | 2 +- .../UserControls/AnimationListControl.cs | 280 ++++++++++++++++-- 2 files changed, 259 insertions(+), 23 deletions(-) diff --git a/UoFiddler.Controls/UserControls/AnimDataControl.cs b/UoFiddler.Controls/UserControls/AnimDataControl.cs index 158f369..d5175ae 100644 --- a/UoFiddler.Controls/UserControls/AnimDataControl.cs +++ b/UoFiddler.Controls/UserControls/AnimDataControl.cs @@ -317,7 +317,7 @@ private void OnValueChangedStartDelay(object sender, EventArgs e) { if (_selAnimdataEntry == null) { - return; + return; } if (_selAnimdataEntry.FrameStart == (byte)numericUpDownStartDelay.Value) diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index b0dfc2c..35d5924 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -1142,43 +1142,56 @@ private void OnUnpackFramesClick(object? sender, EventArgs e) // Group frames by direction var groups = doc.Frames.GroupBy(f => f.Direction).ToDictionary(g => g.Key, g => g.OrderBy(f => f.Index).ToList()); + // Ask the user once how to handle existing frames + DialogResult globalChoice = MessageBox.Show( + "Overwrite existing frames for all directions?\n\nYes = overwrite\nNo = append\nCancel = abort import", + "Unpack Frames", MessageBoxButtons.YesNoCancel); + + if (globalChoice == DialogResult.Cancel) + return; + + bool overwriteAll = (globalChoice == DialogResult.Yes); + + // Build palette once + var allRects = doc.Frames.Select(f => new RectangleF(f.Frame.X, f.Frame.Y, f.Frame.W, f.Frame.H)).ToList(); + var importPalette = BuildPaletteFromFrames(sprite, allRects, alphaThreshold: 4); + foreach (var kv in groups) { int dir = kv.Key; var framesList = kv.Value; - AnimIdx animIdx = AnimationEdit.GetAnimation(fileType, bodyTrans, _currentSelectAction, dir); - if (animIdx == null) - { - MessageBox.Show($"Failed to get AnimIdx for direction {dir}", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Warning); - continue; - } + var animIdx = RequireAnimIdx(fileType, bodyTrans, _currentSelectAction, dir); - // Ask whether to overwrite frames for this direction - var res = MessageBox.Show($"Overwrite existing frames for direction {dir}? (Yes = overwrite, No = append)", "Unpack Frames", MessageBoxButtons.YesNoCancel); - if (res == DialogResult.Cancel) - { - return; - } + if (overwriteAll) animIdx.ClearFrames(); - if (res == DialogResult.Yes) - { - animIdx.ClearFrames(); - } + animIdx.ReplacePalette(importPalette); // key for proper color mapping + int imported = 0; foreach (var frameEntry in framesList) { var r = frameEntry.Frame; - var crop = sprite.Clone(new Rectangle(r.X, r.Y, r.W, r.H), System.Drawing.Imaging.PixelFormat.Format32bppArgb); - // convert to 16bpp used by AnimIdx methods - AnimIdx.AddFrame expects Bitmap in proper pixel format; conversion happens in FrameEdit - Bitmap bmp16 = new Bitmap(crop); - crop.Dispose(); - - animIdx.AddFrame(bmp16, frameEntry.Center.X, frameEntry.Center.Y); + // bounds guard to avoid GDI+ OutOfMemory on bad rects + if (r.W <= 0 || r.H <= 0 || r.X < 0 || r.Y < 0 || + r.X + r.W > sprite.Width || r.Y + r.H > sprite.Height) + continue; + + using (var bit16 = Extract1555Region(sprite, new Rectangle(r.X, r.Y, r.W, r.H), alphaThreshold: 4)) + { + animIdx.AddFrame(bit16, frameEntry.Center.X, frameEntry.Center.Y); + } + + // light GC throttle for very large imports + if ((++imported & 127) == 0) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } } } Options.ChangedUltimaClass["Animations"] = true; + CurrentSelect = CurrentSelect; // this calls SetPicture() and repopulates frames MessageBox.Show("Import finished. Remember to save animations via AnimationEdit.Save if needed.", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); } @@ -1190,7 +1203,230 @@ private void OnUnpackFramesClick(object? sender, EventArgs e) } } + // --- Reflection helpers (safe & cached) --- + private static ushort[] BuildPaletteFromFrames(Bitmap sprite, IEnumerable rects, byte alphaThreshold = 4) + { + var freq = new Dictionary(capacity: 4096); + + // Lock the whole sprite once (32bpp ARGB) + var sheetRect = new Rectangle(0, 0, sprite.Width, sprite.Height); + var sData = sprite.LockBits(sheetRect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + try + { + unsafe + { + byte* basePtr = (byte*)sData.Scan0; + int stride = sData.Stride; + + foreach (var rf in rects) + { + var r = Rectangle.Round(rf); + if (r.Width <= 0 || r.Height <= 0) continue; + if (r.X < 0 || r.Y < 0 || r.Right > sprite.Width || r.Bottom > sprite.Height) continue; + + for (int y = 0; y < r.Height; y++) + { + byte* src = basePtr + (r.Y + y) * stride + r.X * 4; + for (int x = 0; x < r.Width; x++) + { + byte b = src[0], g = src[1], r8 = src[2], a = src[3]; + src += 4; + + if (a <= alphaThreshold) continue; // transparent -> skip + + ushort col = + (ushort)(0x8000 | ((r8 >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)); // A1R5G5B5 + + if (freq.TryGetValue(col, out int n)) freq[col] = n + 1; + else freq[col] = 1; + } + } + } + } + } + finally + { + sprite.UnlockBits(sData); + } + + var palette = new ushort[256]; + int i = 0; + foreach (var kv in freq.OrderByDescending(kv => kv.Value).Take(256)) + palette[i++] = kv.Key; + + return palette; + } + + private static Bitmap Extract1555Region(Bitmap sprite, Rectangle rect, byte alphaThreshold = 4) + { + // Bounds guard + if (rect.Width <= 0 || rect.Height <= 0) throw new ArgumentException("Empty region."); + if (rect.X < 0 || rect.Y < 0 || rect.Right > sprite.Width || rect.Bottom > sprite.Height) + throw new ArgumentOutOfRangeException(nameof(rect), "Region outside sprite bounds."); + + // Dest: 16bpp A1R5G5B5 + Bitmap dst16 = new Bitmap(rect.Width, rect.Height, PixelFormat.Format16bppArgb1555); + + var sheetRect = new Rectangle(0, 0, sprite.Width, sprite.Height); + var sData = sprite.LockBits(sheetRect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + var dData = dst16.LockBits(new Rectangle(0, 0, rect.Width, rect.Height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555); + + try + { + unsafe + { + byte* sBase = (byte*)sData.Scan0; + int sStride = sData.Stride; + + byte* dBase = (byte*)dData.Scan0; + int dStride = dData.Stride; + + for (int y = 0; y < rect.Height; y++) + { + byte* sp = sBase + (rect.Y + y) * sStride + rect.X * 4; + ushort* dp = (ushort*)(dBase + y * dStride); + + for (int x = 0; x < rect.Width; x++) + { + byte b = sp[0], g = sp[1], r = sp[2], a = sp[3]; + sp += 4; + + if (a <= alphaThreshold) + dp[x] = 0; // transparent + else + dp[x] = (ushort)(0x8000 | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)); + } + } + } + } + finally + { + sprite.UnlockBits(sData); + dst16.UnlockBits(dData); + } + + return dst16; + } + + private static Bitmap ToArgb1555From32(Bitmap src32, byte alphaThreshold = 8) + { + if (src32.PixelFormat != PixelFormat.Format32bppArgb) + throw new ArgumentException("src32 must be 32bpp ARGB.", nameof(src32)); + + int w = src32.Width, h = src32.Height; + var rect = new Rectangle(0, 0, w, h); + Bitmap dst16 = new Bitmap(w, h, PixelFormat.Format16bppArgb1555); + + var sData = src32.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + var dData = dst16.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555); + + try + { + unsafe + { + byte* sp = (byte*)sData.Scan0; + byte* dp = (byte*)dData.Scan0; + int sStride = sData.Stride, dStride = dData.Stride; + + for (int y = 0; y < h; y++) + { + byte* sRow = sp + y * sStride; + ushort* dRow = (ushort*)(dp + y * dStride); + for (int x = 0; x < w; x++) + { + byte b = sRow[0], g = sRow[1], r = sRow[2], a = sRow[3]; + if (a <= alphaThreshold) + { + dRow[x] = 0; // A=0 transparent + } + else + { + ushort A = 0x8000; + ushort R = (ushort)((r >> 3) << 10); + ushort G = (ushort)((g >> 3) << 5); + ushort B = (ushort)(b >> 3); + dRow[x] = (ushort)(A | R | G | B); + } + sRow += 4; + } + } + } + } + finally + { + src32.UnlockBits(sData); + dst16.UnlockBits(dData); + } + return dst16; + } + + + // Helper types for JSON + // Replace the entire EnsureAnimIdx(..) with this: + private static AnimIdx RequireAnimIdx(int fileType, int body, int action, int dir) + { + var anim = AnimationEdit.GetAnimation(fileType, body, action, dir); + if (anim == null) + throw new InvalidOperationException( + $"Target animation missing (fileType={fileType}, body={body}, action={action}, dir={dir}). " + + "Create the action/direction in Animation Edit first, then re-run the import." + ); + return anim; + } + + private static Bitmap UnPremultiply(Bitmap src32) + + { + // expects PixelFormat.Format32bppArgb + var rect = new Rectangle(0, 0, src32.Width, src32.Height); + var data = src32.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + try + { + Bitmap dst = new Bitmap(src32.Width, src32.Height, PixelFormat.Format32bppArgb); + var ddata = dst.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + try + { + unsafe + { + byte* sp = (byte*)data.Scan0; + byte* dp = (byte*)ddata.Scan0; + int sw = data.Stride, dw = ddata.Stride; + for (int y = 0; y < src32.Height; y++) + { + byte* srow = sp + y * sw; + byte* drow = dp + y * dw; + for (int x = 0; x < src32.Width; x++) + { + byte b = srow[0], g = srow[1], r = srow[2], a = srow[3]; + if (a == 0) + { + drow[0] = drow[1] = drow[2] = 0; drow[3] = 0; + } + else if (a == 255) + { + drow[0] = b; drow[1] = g; drow[2] = r; drow[3] = 255; + } + else + { + // convert from premultiplied to straight alpha + drow[0] = (byte)Math.Min(255, (b * 255 + (a >> 1)) / a); + drow[1] = (byte)Math.Min(255, (g * 255 + (a >> 1)) / a); + drow[2] = (byte)Math.Min(255, (r * 255 + (a >> 1)) / a); + drow[3] = a; + } + srow += 4; drow += 4; + } + } + } + } + finally { dst.UnlockBits(ddata); } + return dst; + } + finally { src32.UnlockBits(data); } + } + + private class PackedOutput { [JsonPropertyName("meta")] public PackedMeta Meta { get; set; } From 44996aa156a661407b21b238162f0dd142328757 Mon Sep 17 00:00:00 2001 From: Moshu Date: Fri, 28 Nov 2025 17:22:25 -0600 Subject: [PATCH 03/12] Add option for one row per direction in frame packing Introduces a 'One row per direction' checkbox to the PackOptionsForm, allowing packed sprite sheets to organize frames with each direction on a separate row. Updates packing logic to support this option and outputs a text file with row mapping when enabled. --- .../UserControls/AnimationListControl.cs | 307 ++++++++++-------- 1 file changed, 179 insertions(+), 128 deletions(-) diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index 35d5924..c678db3 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -16,10 +16,10 @@ using System.Drawing.Imaging; using System.IO; using System.Linq; -using System.Windows.Forms; -using System.Xml; using System.Text.Json; using System.Text.Json.Serialization; +using System.Windows.Forms; +using System.Xml; using Ultima; using UoFiddler.Controls.Classes; using UoFiddler.Controls.Forms; @@ -969,133 +969,157 @@ private async void OnPackFramesClick(object? sender, EventArgs e) var selectedDirections = optionsForm.SelectedDirections; // list int maxWidth = optionsForm.MaxWidth; + bool oneRowPerDirection = optionsForm.OneRowPerDirection; + + // Ask for output base name/location + using (var dlg = new FolderBrowserDialog()) + { + dlg.Description = "Select folder to save packed sprite and JSON"; + dlg.ShowNewFolderButton = true; + if (dlg.ShowDialog() != DialogResult.OK) + { + return; + } + + string outDir = dlg.SelectedPath; + + try + { + Cursor.Current = Cursors.WaitCursor; + + // Collect frames for directions 0..4 (common editable directions) + var packedFrames = new List(); + + int body = _currentSelect; + Animations.Translate(ref body); + int hue = 0; // do not preserve hue here + + var images = new List(); + + int currentX = 0, currentY = 0, rowHeight = 0, canvasWidth = 0, canvasHeight = 0; + var rowMapping = new System.Text.StringBuilder(); + int rowIndex = 0; + + foreach (int dir in selectedDirections) + { + int localHue = 0; + var frames = Animations.GetAnimation(_currentSelect, _currentSelectAction, dir, ref localHue, false, false); + if (frames == null || frames.Length == 0) + { + continue; + } + + if (oneRowPerDirection) + { + if (currentX > 0) + { + currentY += rowHeight; + currentX = 0; + rowHeight = 0; + } + rowMapping.AppendLine($"Row {rowIndex++}: Facing {GetDirectionName(dir)}"); + } + + for (int fi = 0; fi < frames.Length; fi++) + { + var anim = frames[fi]; + if (anim?.Bitmap == null) + { + continue; + } + + // determine size + int w = anim.Bitmap.Width; + int h = anim.Bitmap.Height; + + if (!oneRowPerDirection && currentX + w > maxWidth) + { + currentY += rowHeight; + currentX = 0; + rowHeight = 0; + } + + if (currentX == 0) + rowHeight = h; + rowHeight = h; + + var entry = new PackedFrameEntry + { + Direction = dir, + Index = fi, + Frame = new Rect { X = currentX, Y = currentY, W = w, H = h }, + Center = new PointStruct { X = anim.Center.X, Y = anim.Center.Y } + }; + + packedFrames.Add(entry); - // Ask for output base name/location - using (var dlg = new FolderBrowserDialog()) - { - dlg.Description = "Select folder to save packed sprite and JSON"; - dlg.ShowNewFolderButton = true; - if (dlg.ShowDialog() != DialogResult.OK) - { - return; - } - - string outDir = dlg.SelectedPath; - - try - { - Cursor.Current = Cursors.WaitCursor; - - // Collect frames for directions 0..4 (common editable directions) - var packedFrames = new List(); - - int body = _currentSelect; - Animations.Translate(ref body); - int hue = 0; // do not preserve hue here - - var images = new List(); - - int currentX = 0, currentY = 0, rowHeight = 0, canvasWidth = 0, canvasHeight = 0; - - foreach (int dir in selectedDirections) - { - int localHue = 0; - var frames = Animations.GetAnimation(_currentSelect, _currentSelectAction, dir, ref localHue, false, false); - if (frames == null || frames.Length == 0) - { - continue; - } - - for (int fi = 0; fi < frames.Length; fi++) - { - var anim = frames[fi]; - if (anim?.Bitmap == null) - { - continue; - } - - // determine size - int w = anim.Bitmap.Width; - int h = anim.Bitmap.Height; - - if (currentX + w > maxWidth) - { - currentY += rowHeight; - currentX = 0; - rowHeight = 0; - } - - if (currentX == 0) - rowHeight = h; - - var entry = new PackedFrameEntry - { - Direction = dir, - Index = fi, - Frame = new Rect { X = currentX, Y = currentY, W = w, H = h }, - Center = new PointStruct { X = anim.Center.X, Y = anim.Center.Y } - }; - - packedFrames.Add(entry); - - // store image copy - images.Add(new Bitmap(anim.Bitmap)); - - currentX += w; - canvasWidth = Math.Max(canvasWidth, currentX); - canvasHeight = Math.Max(canvasHeight, currentY + rowHeight); - } - } - - if (images.Count == 0) - { - MessageBox.Show("No frames found to pack.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - // Create sprite sheet and paste images - using (var sprite = new Bitmap(Math.Max(1, canvasWidth), Math.Max(1, canvasHeight))) - using (var g = Graphics.FromImage(sprite)) - { - g.Clear(Color.Transparent); - - for (int i = 0; i < images.Count; i++) - { - var img = images[i]; - var rect = packedFrames[i].Frame; - g.DrawImage(img, rect.X, rect.Y, rect.W, rect.H); - img.Dispose(); - } - - string baseName = $"anim_{_currentSelect}_{_currentSelectAction}"; - string imageFile = Path.Combine(outDir, baseName + ".png"); - sprite.Save(imageFile, ImageFormat.Png); - - // prepare JSON - var outObj = new PackedOutput - { - Meta = new PackedMeta { Image = Path.GetFileName(imageFile), Size = new SizeStruct { W = sprite.Width, H = sprite.Height }, Format = "RGBA8888" }, - Frames = packedFrames - }; - - string jsonFile = Path.Combine(outDir, baseName + ".json"); - var jsOptions = new JsonSerializerOptions { WriteIndented = true }; - string json = JsonSerializer.Serialize(outObj, jsOptions); - File.WriteAllText(jsonFile, json); - - MessageBox.Show($"Saved sprite: {imageFile}\nSaved JSON: {jsonFile}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - } - catch (Exception ex) - { - MessageBox.Show($"Failed to pack frames: {ex.Message}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - finally - { - Cursor.Current = Cursors.Default; - } - } - } + // store image copy + images.Add(new Bitmap(anim.Bitmap)); + + currentX += w; + canvasWidth = Math.Max(canvasWidth, currentX); + canvasHeight = Math.Max(canvasHeight, currentY + rowHeight); + } + } + + if (images.Count == 0) + { + MessageBox.Show("No frames found to pack.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // Create sprite sheet and paste images + using (var sprite = new Bitmap(Math.Max(1, canvasWidth), Math.Max(1, canvasHeight))) + using (var g = Graphics.FromImage(sprite)) + { + g.Clear(Color.Transparent); + + for (int i = 0; i < images.Count; i++) + { + var img = images[i]; + var rect = packedFrames[i].Frame; + g.DrawImage(img, rect.X, rect.Y, rect.W, rect.H); + img.Dispose(); + } + + string baseName = $"anim_{_currentSelect}_{_currentSelectAction}"; + string imageFile = Path.Combine(outDir, baseName + ".png"); + sprite.Save(imageFile, ImageFormat.Png); + + // prepare JSON + var outObj = new PackedOutput + { + Meta = new PackedMeta { Image = Path.GetFileName(imageFile), Size = new SizeStruct { W = sprite.Width, H = sprite.Height }, Format = "RGBA8888" }, + Frames = packedFrames + }; + + string jsonFile = Path.Combine(outDir, baseName + ".json"); + var jsOptions = new JsonSerializerOptions { WriteIndented = true }; + string json = JsonSerializer.Serialize(outObj, jsOptions); + File.WriteAllText(jsonFile, json); + + if (oneRowPerDirection) + { + string txtFile = Path.Combine(outDir, baseName + "_rows.txt"); + File.WriteAllText(txtFile, rowMapping.ToString()); + MessageBox.Show($"Saved sprite: {imageFile}\nSaved JSON: {jsonFile}\nSaved Info: {txtFile}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + else + { + MessageBox.Show($"Saved sprite: {imageFile}\nSaved JSON: {jsonFile}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + } + catch (Exception ex) + { + MessageBox.Show($"Failed to pack frames: {ex.Message}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + Cursor.Current = Cursors.Default; + } + } + } private void OnUnpackFramesClick(object? sender, EventArgs e) { @@ -1448,6 +1472,22 @@ private class PackedFrameEntry [JsonPropertyName("center")] public PointStruct Center { get; set; } } + private string GetDirectionName(int dir) + { + switch (dir) + { + case 0: return "South"; + case 1: return "South West"; + case 2: return "West"; + case 3: return "North West"; + case 4: return "North"; + case 5: return "North East"; + case 6: return "East"; + case 7: return "South East"; + default: return "Unknown"; + } + } + private class Rect { [JsonPropertyName("x")] public int X { get; set; } @@ -1525,11 +1565,13 @@ public class PackOptionsForm : Form { private CheckedListBox _directionsBox; private NumericUpDown _maxWidthUpDown; + private CheckBox _oneRowPerDirectionCheckBox; private Button _ok; private Button _cancel; public List SelectedDirections { get; private set; } = new List { 0, 1, 2, 3, 4 }; public int MaxWidth { get; private set; } = 2048; + public bool OneRowPerDirection { get; private set; } public PackOptionsForm() { @@ -1589,6 +1631,14 @@ public PackOptionsForm() Controls.Add(presetMedium); Controls.Add(presetLarge); + _oneRowPerDirectionCheckBox = new CheckBox + { + Text = "One row per direction", + Location = new Point(12, 300), + AutoSize = true + }; + Controls.Add(_oneRowPerDirectionCheckBox); + // make OK/Cancel taller and move to the right (anchored) _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(330, 300), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _ok.Click += Ok_Click; @@ -1613,6 +1663,7 @@ private void Ok_Click(object sender, EventArgs e) } MaxWidth = (int)_maxWidthUpDown.Value; + OneRowPerDirection = _oneRowPerDirectionCheckBox.Checked; } } } From 29eeb25c4b2d68d665d3845462ade368d39cc8c0 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sat, 29 Nov 2025 09:57:56 -0600 Subject: [PATCH 04/12] Add frame spacing option to animation packing Introduces a 'FrameSpacing' option in the animation packing dialog, allowing users to specify spacing between frames when exporting packed animations. Updates layout logic to respect the spacing value, and adds a UI slider for user control. --- .../UserControls/AnimationListControl.cs | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index c678db3..6de22a1 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -970,6 +970,7 @@ private async void OnPackFramesClick(object? sender, EventArgs e) var selectedDirections = optionsForm.SelectedDirections; // list int maxWidth = optionsForm.MaxWidth; bool oneRowPerDirection = optionsForm.OneRowPerDirection; + int spacing = optionsForm.FrameSpacing; // Ask for output base name/location using (var dlg = new FolderBrowserDialog()) @@ -996,7 +997,7 @@ private async void OnPackFramesClick(object? sender, EventArgs e) var images = new List(); - int currentX = 0, currentY = 0, rowHeight = 0, canvasWidth = 0, canvasHeight = 0; + int currentX = spacing, currentY = spacing, rowHeight = 0, canvasWidth = 0, canvasHeight = 0; var rowMapping = new System.Text.StringBuilder(); int rowIndex = 0; @@ -1011,10 +1012,10 @@ private async void OnPackFramesClick(object? sender, EventArgs e) if (oneRowPerDirection) { - if (currentX > 0) + if (currentX > spacing) { - currentY += rowHeight; - currentX = 0; + currentY += rowHeight + spacing; + currentX = spacing; rowHeight = 0; } rowMapping.AppendLine($"Row {rowIndex++}: Facing {GetDirectionName(dir)}"); @@ -1034,14 +1035,15 @@ private async void OnPackFramesClick(object? sender, EventArgs e) if (!oneRowPerDirection && currentX + w > maxWidth) { - currentY += rowHeight; - currentX = 0; + currentY += rowHeight + spacing; + currentX = spacing; rowHeight = 0; } - if (currentX == 0) + if (currentX == spacing) rowHeight = h; - rowHeight = h; + else + rowHeight = Math.Max(rowHeight, h); var entry = new PackedFrameEntry { @@ -1056,9 +1058,9 @@ private async void OnPackFramesClick(object? sender, EventArgs e) // store image copy images.Add(new Bitmap(anim.Bitmap)); - currentX += w; + currentX += w + spacing; canvasWidth = Math.Max(canvasWidth, currentX); - canvasHeight = Math.Max(canvasHeight, currentY + rowHeight); + canvasHeight = Math.Max(canvasHeight, currentY + rowHeight + spacing); } } @@ -1566,12 +1568,15 @@ public class PackOptionsForm : Form private CheckedListBox _directionsBox; private NumericUpDown _maxWidthUpDown; private CheckBox _oneRowPerDirectionCheckBox; + private TrackBar _spacingTrackBar; + private Label _spacingLabel; private Button _ok; private Button _cancel; public List SelectedDirections { get; private set; } = new List { 0, 1, 2, 3, 4 }; public int MaxWidth { get; private set; } = 2048; public bool OneRowPerDirection { get; private set; } + public int FrameSpacing { get; private set; } = 0; public PackOptionsForm() { @@ -1581,7 +1586,7 @@ public PackOptionsForm() MinimizeBox = false; StartPosition = FormStartPosition.CenterParent; // increased size to accommodate taller direction list and wider right side - ClientSize = new Size(520, 360); + ClientSize = new Size(520, 420); Padding = new Padding(10); Label lbl = new Label { Text = "Directions:", Location = new Point(12, 12), AutoSize = true }; @@ -1631,6 +1636,25 @@ public PackOptionsForm() Controls.Add(presetMedium); Controls.Add(presetLarge); + // Spacing Slider + var spacingTitle = new Label { Text = "Spacing:", Location = new Point(190, 160), AutoSize = true }; + Controls.Add(spacingTitle); + + _spacingLabel = new Label { Text = "0", Location = new Point(460, 160), AutoSize = true }; + Controls.Add(_spacingLabel); + + _spacingTrackBar = new TrackBar + { + Location = new Point(190, 180), + Size = new Size(280, 45), + Minimum = 0, + Maximum = 20, + Value = 0, + TickFrequency = 1 + }; + _spacingTrackBar.Scroll += (s, e) => _spacingLabel.Text = _spacingTrackBar.Value.ToString(); + Controls.Add(_spacingTrackBar); + _oneRowPerDirectionCheckBox = new CheckBox { Text = "One row per direction", @@ -1640,11 +1664,11 @@ public PackOptionsForm() Controls.Add(_oneRowPerDirectionCheckBox); // make OK/Cancel taller and move to the right (anchored) - _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(330, 300), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(330, 360), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _ok.Click += Ok_Click; Controls.Add(_ok); - _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(440, 300), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(440, 360), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; Controls.Add(_cancel); AcceptButton = _ok; @@ -1664,6 +1688,7 @@ private void Ok_Click(object sender, EventArgs e) MaxWidth = (int)_maxWidthUpDown.Value; OneRowPerDirection = _oneRowPerDirectionCheckBox.Checked; + FrameSpacing = _spacingTrackBar.Value; } } } From efb8e6ecbd6df6dad96c17feed40be89e11dd630 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sat, 29 Nov 2025 10:21:57 -0600 Subject: [PATCH 05/12] Add option to export all animations in pack dialog Introduces an 'Export all animations' checkbox to the pack options form, allowing users to export all actions for the selected body. Refactors export logic to support batch exporting and updates UI layout to accommodate the new option. --- .../UserControls/AnimationListControl.cs | 280 +++++++++++------- 1 file changed, 178 insertions(+), 102 deletions(-) diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index 6de22a1..a4483d8 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -971,6 +971,7 @@ private async void OnPackFramesClick(object? sender, EventArgs e) int maxWidth = optionsForm.MaxWidth; bool oneRowPerDirection = optionsForm.OneRowPerDirection; int spacing = optionsForm.FrameSpacing; + bool exportAll = optionsForm.ExportAllAnimations; // Ask for output base name/location using (var dlg = new FolderBrowserDialog()) @@ -988,138 +989,202 @@ private async void OnPackFramesClick(object? sender, EventArgs e) { Cursor.Current = Cursors.WaitCursor; - // Collect frames for directions 0..4 (common editable directions) - var packedFrames = new List(); - - int body = _currentSelect; - Animations.Translate(ref body); - int hue = 0; // do not preserve hue here - - var images = new List(); - - int currentX = spacing, currentY = spacing, rowHeight = 0, canvasWidth = 0, canvasHeight = 0; - var rowMapping = new System.Text.StringBuilder(); - int rowIndex = 0; - - foreach (int dir in selectedDirections) + if (exportAll) { - int localHue = 0; - var frames = Animations.GetAnimation(_currentSelect, _currentSelectAction, dir, ref localHue, false, false); - if (frames == null || frames.Length == 0) - { - continue; - } + int exportedCount = 0; + TreeNode? bodyNode = null; - if (oneRowPerDirection) + // Find the body node based on current selection + if (TreeViewMobs.SelectedNode != null) { - if (currentX > spacing) + if (TreeViewMobs.SelectedNode.Tag is int[]) // It's a body node + { + bodyNode = TreeViewMobs.SelectedNode; + } + else if (TreeViewMobs.SelectedNode.Parent != null && TreeViewMobs.SelectedNode.Parent.Tag is int[]) // It's an action node { - currentY += rowHeight + spacing; - currentX = spacing; - rowHeight = 0; + bodyNode = TreeViewMobs.SelectedNode.Parent; } - rowMapping.AppendLine($"Row {rowIndex++}: Facing {GetDirectionName(dir)}"); } - for (int fi = 0; fi < frames.Length; fi++) + if (bodyNode != null) { - var anim = frames[fi]; - if (anim?.Bitmap == null) + foreach (TreeNode node in bodyNode.Nodes) { - continue; + if (node.Tag is int action) + { + var result = PackSingleAnimation(outDir, _currentSelect, action, selectedDirections, maxWidth, oneRowPerDirection, spacing); + if (result != null && result.Count > 0) + { + exportedCount++; + } + } } + } + else + { + // Fallback if tree node structure is unexpected (shouldn't happen if _currentSelect is valid) + // Try to export just the current action or maybe a small range? + // But better to warn or just do nothing if we can't find the nodes. + MessageBox.Show("Could not determine animation list from selection.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } - // determine size - int w = anim.Bitmap.Width; - int h = anim.Bitmap.Height; - - if (!oneRowPerDirection && currentX + w > maxWidth) + if (exportedCount == 0) + { + MessageBox.Show("No animations found to export.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + else + { + MessageBox.Show($"Exported {exportedCount} animations.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + else + { + var result = PackSingleAnimation(outDir, _currentSelect, _currentSelectAction, selectedDirections, maxWidth, oneRowPerDirection, spacing); + if (result != null && result.Count > 0) + { + string msg = $"Saved sprite: {result[0]}\nSaved JSON: {result[1]}"; + if (result.Count > 2) { - currentY += rowHeight + spacing; - currentX = spacing; - rowHeight = 0; + msg += $"\nSaved Info: {result[2]}"; } + MessageBox.Show(msg, "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + else + { + MessageBox.Show("No frames found to pack.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + } + catch (Exception ex) + { + MessageBox.Show($"Failed to pack frames: {ex.Message}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + Cursor.Current = Cursors.Default; + } + } + } - if (currentX == spacing) - rowHeight = h; - else - rowHeight = Math.Max(rowHeight, h); - - var entry = new PackedFrameEntry - { - Direction = dir, - Index = fi, - Frame = new Rect { X = currentX, Y = currentY, W = w, H = h }, - Center = new PointStruct { X = anim.Center.X, Y = anim.Center.Y } - }; + private List? PackSingleAnimation(string outDir, int body, int action, List selectedDirections, int maxWidth, bool oneRowPerDirection, int spacing) + { + // Collect frames for directions 0..4 (common editable directions) + var packedFrames = new List(); + var images = new List(); - packedFrames.Add(entry); + int currentX = spacing, currentY = spacing, rowHeight = 0, canvasWidth = 0, canvasHeight = 0; + var rowMapping = new System.Text.StringBuilder(); + int rowIndex = 0; - // store image copy - images.Add(new Bitmap(anim.Bitmap)); + foreach (int dir in selectedDirections) + { + int localHue = 0; + var frames = Animations.GetAnimation(body, action, dir, ref localHue, false, false); + if (frames == null || frames.Length == 0) + { + continue; + } - currentX += w + spacing; - canvasWidth = Math.Max(canvasWidth, currentX); - canvasHeight = Math.Max(canvasHeight, currentY + rowHeight + spacing); - } + if (oneRowPerDirection) + { + if (currentX > spacing) + { + currentY += rowHeight + spacing; + currentX = spacing; + rowHeight = 0; } + rowMapping.AppendLine($"Row {rowIndex++}: Facing {GetDirectionName(dir)}"); + } - if (images.Count == 0) + for (int fi = 0; fi < frames.Length; fi++) + { + var anim = frames[fi]; + if (anim?.Bitmap == null) { - MessageBox.Show("No frames found to pack.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; + continue; } - // Create sprite sheet and paste images - using (var sprite = new Bitmap(Math.Max(1, canvasWidth), Math.Max(1, canvasHeight))) - using (var g = Graphics.FromImage(sprite)) + // determine size + int w = anim.Bitmap.Width; + int h = anim.Bitmap.Height; + + if (!oneRowPerDirection && currentX + w > maxWidth) { - g.Clear(Color.Transparent); + currentY += rowHeight + spacing; + currentX = spacing; + rowHeight = 0; + } - for (int i = 0; i < images.Count; i++) - { - var img = images[i]; - var rect = packedFrames[i].Frame; - g.DrawImage(img, rect.X, rect.Y, rect.W, rect.H); - img.Dispose(); - } + if (currentX == spacing) + rowHeight = h; + else + rowHeight = Math.Max(rowHeight, h); - string baseName = $"anim_{_currentSelect}_{_currentSelectAction}"; - string imageFile = Path.Combine(outDir, baseName + ".png"); - sprite.Save(imageFile, ImageFormat.Png); + var entry = new PackedFrameEntry + { + Direction = dir, + Index = fi, + Frame = new Rect { X = currentX, Y = currentY, W = w, H = h }, + Center = new PointStruct { X = anim.Center.X, Y = anim.Center.Y } + }; - // prepare JSON - var outObj = new PackedOutput - { - Meta = new PackedMeta { Image = Path.GetFileName(imageFile), Size = new SizeStruct { W = sprite.Width, H = sprite.Height }, Format = "RGBA8888" }, - Frames = packedFrames - }; + packedFrames.Add(entry); - string jsonFile = Path.Combine(outDir, baseName + ".json"); - var jsOptions = new JsonSerializerOptions { WriteIndented = true }; - string json = JsonSerializer.Serialize(outObj, jsOptions); - File.WriteAllText(jsonFile, json); + // store image copy + images.Add(new Bitmap(anim.Bitmap)); - if (oneRowPerDirection) - { - string txtFile = Path.Combine(outDir, baseName + "_rows.txt"); - File.WriteAllText(txtFile, rowMapping.ToString()); - MessageBox.Show($"Saved sprite: {imageFile}\nSaved JSON: {jsonFile}\nSaved Info: {txtFile}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - else - { - MessageBox.Show($"Saved sprite: {imageFile}\nSaved JSON: {jsonFile}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - } + canvasWidth = Math.Max(canvasWidth, currentX + w + spacing); // Include right margin + currentX += w + spacing; + canvasHeight = Math.Max(canvasHeight, currentY + rowHeight + spacing); // Include bottom margin } - catch (Exception ex) + } + + if (images.Count == 0) + { + return null; + } + + // Create sprite sheet and paste images + using (var sprite = new Bitmap(Math.Max(1, canvasWidth), Math.Max(1, canvasHeight))) + using (var g = Graphics.FromImage(sprite)) + { + g.Clear(Color.Transparent); + + for (int i = 0; i < images.Count; i++) { - MessageBox.Show($"Failed to pack frames: {ex.Message}", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + var img = images[i]; + var rect = packedFrames[i].Frame; + g.DrawImage(img, rect.X, rect.Y, rect.W, rect.H); + img.Dispose(); } - finally + + string baseName = $"anim_{body}_{action}"; + string imageFile = Path.Combine(outDir, baseName + ".png"); + sprite.Save(imageFile, ImageFormat.Png); + + // prepare JSON + var outObj = new PackedOutput { - Cursor.Current = Cursors.Default; + Meta = new PackedMeta { Image = Path.GetFileName(imageFile), Size = new SizeStruct { W = sprite.Width, H = sprite.Height }, Format = "RGBA8888" }, + Frames = packedFrames + }; + + string jsonFile = Path.Combine(outDir, baseName + ".json"); + var jsOptions = new JsonSerializerOptions { WriteIndented = true }; + string json = JsonSerializer.Serialize(outObj, jsOptions); + File.WriteAllText(jsonFile, json); + + var result = new List { imageFile, jsonFile }; + + if (oneRowPerDirection) + { + string txtFile = Path.Combine(outDir, baseName + "_rows.txt"); + File.WriteAllText(txtFile, rowMapping.ToString()); + result.Add(txtFile); } + + return result; } } @@ -1568,6 +1633,7 @@ public class PackOptionsForm : Form private CheckedListBox _directionsBox; private NumericUpDown _maxWidthUpDown; private CheckBox _oneRowPerDirectionCheckBox; + private CheckBox _exportAllCheckBox; private TrackBar _spacingTrackBar; private Label _spacingLabel; private Button _ok; @@ -1577,6 +1643,7 @@ public class PackOptionsForm : Form public int MaxWidth { get; private set; } = 2048; public bool OneRowPerDirection { get; private set; } public int FrameSpacing { get; private set; } = 0; + public bool ExportAllAnimations { get; private set; } public PackOptionsForm() { @@ -1586,7 +1653,7 @@ public PackOptionsForm() MinimizeBox = false; StartPosition = FormStartPosition.CenterParent; // increased size to accommodate taller direction list and wider right side - ClientSize = new Size(520, 420); + ClientSize = new Size(520, 460); Padding = new Padding(10); Label lbl = new Label { Text = "Directions:", Location = new Point(12, 12), AutoSize = true }; @@ -1663,12 +1730,20 @@ public PackOptionsForm() }; Controls.Add(_oneRowPerDirectionCheckBox); + _exportAllCheckBox = new CheckBox + { + Text = "Export all animations", + Location = new Point(12, 330), + AutoSize = true + }; + Controls.Add(_exportAllCheckBox); + // make OK/Cancel taller and move to the right (anchored) - _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(330, 360), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(330, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _ok.Click += Ok_Click; Controls.Add(_ok); - _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(440, 360), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(440, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; Controls.Add(_cancel); AcceptButton = _ok; @@ -1689,6 +1764,7 @@ private void Ok_Click(object sender, EventArgs e) MaxWidth = (int)_maxWidthUpDown.Value; OneRowPerDirection = _oneRowPerDirectionCheckBox.Checked; FrameSpacing = _spacingTrackBar.Value; + ExportAllAnimations = _exportAllCheckBox.Checked; } } } From 732c5dd7a66ae8f848a39b9a91c4dc0e40f663b0 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sat, 29 Nov 2025 10:37:37 -0600 Subject: [PATCH 06/12] Add bulk unpack frames feature to AnimationListControl Introduces a 'Bulk Unpack Frames' menu item and handler to allow importing multiple animation JSON files at once. Refactors unpack logic into a reusable method and adds overwrite/append options for bulk imports, improving workflow for batch animation imports. --- .../AnimationListControl.Designer.cs | 10 +- .../UserControls/AnimationListControl.cs | 203 ++++++++++++------ 2 files changed, 152 insertions(+), 61 deletions(-) diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs b/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs index 9323488..17bb826 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs @@ -88,6 +88,7 @@ private void InitializeComponent() BaseGraphicLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.packFramesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.unpackFramesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.bulkUnpackFramesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); HueLabel = new System.Windows.Forms.ToolStripStatusLabel(); ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); @@ -276,7 +277,7 @@ private void InitializeComponent() // // extractAnimationToolStripMenuItem // - extractAnimationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { asBmpToolStripMenuItem1, asTiffToolStripMenuItem1, asJpgToolStripMenuItem1, asPngToolStripMenuItem1, asAnimatedGifToolStripMenuItem, asAnimatedGifnoLoopingToolStripMenuItem, new System.Windows.Forms.ToolStripSeparator(), this.packFramesToolStripMenuItem, this.unpackFramesToolStripMenuItem }); + extractAnimationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { asBmpToolStripMenuItem1, asTiffToolStripMenuItem1, asJpgToolStripMenuItem1, asPngToolStripMenuItem1, asAnimatedGifToolStripMenuItem, asAnimatedGifnoLoopingToolStripMenuItem, new System.Windows.Forms.ToolStripSeparator(), this.packFramesToolStripMenuItem, this.unpackFramesToolStripMenuItem, this.bulkUnpackFramesToolStripMenuItem }); extractAnimationToolStripMenuItem.Name = "extractAnimationToolStripMenuItem"; extractAnimationToolStripMenuItem.Size = new System.Drawing.Size(173, 22); extractAnimationToolStripMenuItem.Text = "Export Animation.."; @@ -573,6 +574,12 @@ private void InitializeComponent() this.unpackFramesToolStripMenuItem.Size = new System.Drawing.Size(221, 22); this.unpackFramesToolStripMenuItem.Text = "Unpack Frames from JSON"; // + // bulkUnpackFramesToolStripMenuItem + // + this.bulkUnpackFramesToolStripMenuItem.Name = "bulkUnpackFramesToolStripMenuItem"; + this.bulkUnpackFramesToolStripMenuItem.Size = new System.Drawing.Size(221, 22); + this.bulkUnpackFramesToolStripMenuItem.Text = "Bulk Unpack Frames"; + // // AnimationListControl // AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); @@ -658,5 +665,6 @@ private void InitializeComponent() private System.Windows.Forms.Label directionLabel; private System.Windows.Forms.ToolStripMenuItem packFramesToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem unpackFramesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem bulkUnpackFramesToolStripMenuItem; } } diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index a4483d8..25fac2a 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -39,6 +39,7 @@ public AnimationListControl() // Add handlers for new context menu items packFramesToolStripMenuItem.Click += OnPackFramesClick; unpackFramesToolStripMenuItem.Click += OnUnpackFramesClick; + bulkUnpackFramesToolStripMenuItem.Click += OnBulkUnpackFramesClick; } @@ -1208,88 +1209,170 @@ private void OnUnpackFramesClick(object? sender, EventArgs e) string jsonFile = ofd.FileName; try { - string json = File.ReadAllText(jsonFile); - var doc = JsonSerializer.Deserialize(json); - if (doc == null) - { - MessageBox.Show("Invalid JSON file.", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; - } - - string spritePath = Path.Combine(Path.GetDirectoryName(jsonFile) ?? string.Empty, doc.Meta.Image); - if (!File.Exists(spritePath)) - { - MessageBox.Show($"Sprite sheet not found: {spritePath}", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; - } - - using (var sprite = new Bitmap(spritePath)) - { - // determine body/fileType for import - int bodyTrans = _currentSelect; - Animations.Translate(ref bodyTrans); - int fileType = BodyConverter.Convert(ref bodyTrans); + UnpackAnimation(jsonFile, _currentSelect, _currentSelectAction, true); + MessageBox.Show("Import finished. Remember to save animations via AnimationEdit.Save if needed.", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to unpack/import frames: {ex.Message}", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } - // Group frames by direction - var groups = doc.Frames.GroupBy(f => f.Direction).ToDictionary(g => g.Key, g => g.OrderBy(f => f.Index).ToList()); + private void OnBulkUnpackFramesClick(object? sender, EventArgs e) + { + if (_currentSelect == 0) + { + MessageBox.Show("No graphic selected.", "Bulk Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } - // Ask the user once how to handle existing frames - DialogResult globalChoice = MessageBox.Show( - "Overwrite existing frames for all directions?\n\nYes = overwrite\nNo = append\nCancel = abort import", - "Unpack Frames", MessageBoxButtons.YesNoCancel); + using (var dlg = new FolderBrowserDialog()) + { + dlg.Description = "Select folder containing exported animations (JSON + PNG)"; + dlg.ShowNewFolderButton = false; - if (globalChoice == DialogResult.Cancel) - return; + if (dlg.ShowDialog() != DialogResult.OK) + { + return; + } - bool overwriteAll = (globalChoice == DialogResult.Yes); + string[] jsonFiles = Directory.GetFiles(dlg.SelectedPath, "*.json"); + if (jsonFiles.Length == 0) + { + MessageBox.Show("No JSON files found in selected directory.", "Bulk Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } - // Build palette once - var allRects = doc.Frames.Select(f => new RectangleF(f.Frame.X, f.Frame.Y, f.Frame.W, f.Frame.H)).ToList(); - var importPalette = BuildPaletteFromFrames(sprite, allRects, alphaThreshold: 4); + int importedCount = 0; + int errorCount = 0; - foreach (var kv in groups) - { - int dir = kv.Key; - var framesList = kv.Value; + Cursor.Current = Cursors.WaitCursor; + try + { + // Ask user once for overwrite preference + DialogResult globalChoice = MessageBox.Show( + "Overwrite existing frames for all imported animations?\n\nYes = overwrite\nNo = append\nCancel = abort import", + "Bulk Unpack Frames", MessageBoxButtons.YesNoCancel); - var animIdx = RequireAnimIdx(fileType, bodyTrans, _currentSelectAction, dir); + if (globalChoice == DialogResult.Cancel) + return; - if (overwriteAll) animIdx.ClearFrames(); + bool overwriteAll = (globalChoice == DialogResult.Yes); - animIdx.ReplacePalette(importPalette); // key for proper color mapping + foreach (string jsonFile in jsonFiles) + { + // Expected format: anim_{body}_{action}.json + string fileName = Path.GetFileNameWithoutExtension(jsonFile); + string[] parts = fileName.Split('_'); - int imported = 0; - foreach (var frameEntry in framesList) + if (parts.Length >= 3 && parts[0] == "anim" && int.TryParse(parts[1], out int body) && int.TryParse(parts[2], out int action)) + { + // Only import if body matches current selection (optional, but safer) + if (body == _currentSelect) { - var r = frameEntry.Frame; - // bounds guard to avoid GDI+ OutOfMemory on bad rects - if (r.W <= 0 || r.H <= 0 || r.X < 0 || r.Y < 0 || - r.X + r.W > sprite.Width || r.Y + r.H > sprite.Height) - continue; - - using (var bit16 = Extract1555Region(sprite, new Rectangle(r.X, r.Y, r.W, r.H), alphaThreshold: 4)) + try { - animIdx.AddFrame(bit16, frameEntry.Center.X, frameEntry.Center.Y); + UnpackAnimation(jsonFile, body, action, false, overwriteAll); + importedCount++; } - - // light GC throttle for very large imports - if ((++imported & 127) == 0) + catch { - GC.Collect(); - GC.WaitForPendingFinalizers(); + errorCount++; } } } + } + } + finally + { + Cursor.Current = Cursors.Default; + } + + MessageBox.Show($"Bulk import finished.\nImported: {importedCount}\nErrors: {errorCount}\n\nRemember to save animations via AnimationEdit.Save if needed.", "Bulk Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + + private void UnpackAnimation(string jsonFile, int body, int action, bool promptOverwrite, bool overwriteAll = false) + { + string json = File.ReadAllText(jsonFile); + var doc = JsonSerializer.Deserialize(json); + if (doc == null) + { + throw new Exception("Invalid JSON file."); + } + + string spritePath = Path.Combine(Path.GetDirectoryName(jsonFile) ?? string.Empty, doc.Meta.Image); + if (!File.Exists(spritePath)) + { + throw new FileNotFoundException($"Sprite sheet not found: {spritePath}"); + } + + using (var sprite = new Bitmap(spritePath)) + { + // determine body/fileType for import + int bodyTrans = body; + Animations.Translate(ref bodyTrans); + int fileType = BodyConverter.Convert(ref bodyTrans); + + // Group frames by direction + var groups = doc.Frames.GroupBy(f => f.Direction).ToDictionary(g => g.Key, g => g.OrderBy(f => f.Index).ToList()); - Options.ChangedUltimaClass["Animations"] = true; - CurrentSelect = CurrentSelect; // this calls SetPicture() and repopulates frames + if (promptOverwrite) + { + // Ask the user once how to handle existing frames + DialogResult globalChoice = MessageBox.Show( + "Overwrite existing frames for all directions?\n\nYes = overwrite\nNo = append\nCancel = abort import", + "Unpack Frames", MessageBoxButtons.YesNoCancel); + + if (globalChoice == DialogResult.Cancel) + return; - MessageBox.Show("Import finished. Remember to save animations via AnimationEdit.Save if needed.", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + overwriteAll = (globalChoice == DialogResult.Yes); + } + + // Build palette once + var allRects = doc.Frames.Select(f => new RectangleF(f.Frame.X, f.Frame.Y, f.Frame.W, f.Frame.H)).ToList(); + var importPalette = BuildPaletteFromFrames(sprite, allRects, alphaThreshold: 4); + + foreach (var kv in groups) + { + int dir = kv.Key; + var framesList = kv.Value; + + var animIdx = RequireAnimIdx(fileType, bodyTrans, action, dir); + + if (overwriteAll) animIdx.ClearFrames(); + + animIdx.ReplacePalette(importPalette); // key for proper color mapping + + int imported = 0; + foreach (var frameEntry in framesList) + { + var r = frameEntry.Frame; + // bounds guard to avoid GDI+ OutOfMemory on bad rects + if (r.W <= 0 || r.H <= 0 || r.X < 0 || r.Y < 0 || + r.X + r.W > sprite.Width || r.Y + r.H > sprite.Height) + continue; + + using (var bit16 = Extract1555Region(sprite, new Rectangle(r.X, r.Y, r.W, r.H), alphaThreshold: 4)) + { + animIdx.AddFrame(bit16, frameEntry.Center.X, frameEntry.Center.Y); + } + + // light GC throttle for very large imports + if ((++imported & 127) == 0) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } } } - catch (Exception ex) + + Options.ChangedUltimaClass["Animations"] = true; + if (body == _currentSelect) { - MessageBox.Show($"Failed to unpack/import frames: {ex.Message}", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Error); + CurrentSelect = CurrentSelect; // this calls SetPicture() and repopulates frames } } } From e3cf53eb101d237af20ce731ed0c2b551755dcf0 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sat, 29 Nov 2025 19:26:23 -0600 Subject: [PATCH 07/12] Limit animation directions to maximum of 4 Added a check to skip processing animation directions greater than 4 in AnimationListControl. This prevents handling unsupported or invalid direction values. --- UoFiddler.Controls/UserControls/AnimationListControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index 25fac2a..cbea136 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -1338,6 +1338,10 @@ private void UnpackAnimation(string jsonFile, int body, int action, bool promptO foreach (var kv in groups) { int dir = kv.Key; + if (dir > 4) + { + continue; + } var framesList = kv.Value; var animIdx = RequireAnimIdx(fileType, bodyTrans, action, dir); From 762ee6e471a6bf3b6bdadd47593d6b62b4dd73a9 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sun, 30 Nov 2025 10:27:42 -0600 Subject: [PATCH 08/12] Refactor packed animation classes to shared location Moved PackedOutput, PackedMeta, PackedFrameEntry, Rect, PointStruct, and SizeStruct classes from AnimationListControl.cs to UoFiddler.Controls/Classes for reuse and better organization. Deleted PackerClasses.cs, consolidating all packed animation-related data structures into the new shared classes. Adjusted button layout in AnimationListControl for improved UI. --- .../Classes/PackedFrameEntry.cs | 23 ++++++ UoFiddler.Controls/Classes/PackedMeta.cs | 22 ++++++ UoFiddler.Controls/Classes/PackedOutput.cs | 22 ++++++ UoFiddler.Controls/Classes/PointStruct.cs | 21 ++++++ UoFiddler.Controls/Classes/Rect.cs | 23 ++++++ UoFiddler.Controls/Classes/SizeStruct.cs | 21 ++++++ .../UserControls/AnimationListControl.cs | 46 +----------- UoFiddler/PackerClasses.cs | 72 ------------------- 8 files changed, 135 insertions(+), 115 deletions(-) create mode 100644 UoFiddler.Controls/Classes/PackedFrameEntry.cs create mode 100644 UoFiddler.Controls/Classes/PackedMeta.cs create mode 100644 UoFiddler.Controls/Classes/PackedOutput.cs create mode 100644 UoFiddler.Controls/Classes/PointStruct.cs create mode 100644 UoFiddler.Controls/Classes/Rect.cs create mode 100644 UoFiddler.Controls/Classes/SizeStruct.cs delete mode 100644 UoFiddler/PackerClasses.cs diff --git a/UoFiddler.Controls/Classes/PackedFrameEntry.cs b/UoFiddler.Controls/Classes/PackedFrameEntry.cs new file mode 100644 index 0000000..ce2bbf4 --- /dev/null +++ b/UoFiddler.Controls/Classes/PackedFrameEntry.cs @@ -0,0 +1,23 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +using System.Text.Json.Serialization; + +namespace UoFiddler.Controls.Classes +{ + public class PackedFrameEntry + { + [JsonPropertyName("direction")] public int Direction { get; set; } + [JsonPropertyName("index")] public int Index { get; set; } + [JsonPropertyName("frame")] public Rect Frame { get; set; } + [JsonPropertyName("center")] public PointStruct Center { get; set; } + } +} diff --git a/UoFiddler.Controls/Classes/PackedMeta.cs b/UoFiddler.Controls/Classes/PackedMeta.cs new file mode 100644 index 0000000..cf69f6d --- /dev/null +++ b/UoFiddler.Controls/Classes/PackedMeta.cs @@ -0,0 +1,22 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +using System.Text.Json.Serialization; + +namespace UoFiddler.Controls.Classes +{ + public class PackedMeta + { + [JsonPropertyName("image")] public string Image { get; set; } + [JsonPropertyName("size")] public SizeStruct Size { get; set; } + [JsonPropertyName("format")] public string Format { get; set; } + } +} diff --git a/UoFiddler.Controls/Classes/PackedOutput.cs b/UoFiddler.Controls/Classes/PackedOutput.cs new file mode 100644 index 0000000..8e2709f --- /dev/null +++ b/UoFiddler.Controls/Classes/PackedOutput.cs @@ -0,0 +1,22 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace UoFiddler.Controls.Classes +{ + public class PackedOutput + { + [JsonPropertyName("meta")] public PackedMeta Meta { get; set; } + [JsonPropertyName("frames")] public List Frames { get; set; } + } +} diff --git a/UoFiddler.Controls/Classes/PointStruct.cs b/UoFiddler.Controls/Classes/PointStruct.cs new file mode 100644 index 0000000..2340aa2 --- /dev/null +++ b/UoFiddler.Controls/Classes/PointStruct.cs @@ -0,0 +1,21 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +using System.Text.Json.Serialization; + +namespace UoFiddler.Controls.Classes +{ + public class PointStruct + { + [JsonPropertyName("x")] public int X { get; set; } + [JsonPropertyName("y")] public int Y { get; set; } + } +} diff --git a/UoFiddler.Controls/Classes/Rect.cs b/UoFiddler.Controls/Classes/Rect.cs new file mode 100644 index 0000000..277507c --- /dev/null +++ b/UoFiddler.Controls/Classes/Rect.cs @@ -0,0 +1,23 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +using System.Text.Json.Serialization; + +namespace UoFiddler.Controls.Classes +{ + public class Rect + { + [JsonPropertyName("x")] public int X { get; set; } + [JsonPropertyName("y")] public int Y { get; set; } + [JsonPropertyName("w")] public int W { get; set; } + [JsonPropertyName("h")] public int H { get; set; } + } +} diff --git a/UoFiddler.Controls/Classes/SizeStruct.cs b/UoFiddler.Controls/Classes/SizeStruct.cs new file mode 100644 index 0000000..de0bf0a --- /dev/null +++ b/UoFiddler.Controls/Classes/SizeStruct.cs @@ -0,0 +1,21 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +using System.Text.Json.Serialization; + +namespace UoFiddler.Controls.Classes +{ + public class SizeStruct + { + [JsonPropertyName("w")] public int W { get; set; } + [JsonPropertyName("h")] public int H { get; set; } + } +} diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index cbea136..f00d13a 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -1604,28 +1604,6 @@ private static Bitmap UnPremultiply(Bitmap src32) finally { src32.UnlockBits(data); } } - - private class PackedOutput - { - [JsonPropertyName("meta")] public PackedMeta Meta { get; set; } - [JsonPropertyName("frames")] public List Frames { get; set; } - } - - private class PackedMeta - { - [JsonPropertyName("image")] public string Image { get; set; } - [JsonPropertyName("size")] public SizeStruct Size { get; set; } - [JsonPropertyName("format")] public string Format { get; set; } - } - - private class PackedFrameEntry - { - [JsonPropertyName("direction")] public int Direction { get; set; } - [JsonPropertyName("index")] public int Index { get; set; } - [JsonPropertyName("frame")] public Rect Frame { get; set; } - [JsonPropertyName("center")] public PointStruct Center { get; set; } - } - private string GetDirectionName(int dir) { switch (dir) @@ -1642,25 +1620,7 @@ private string GetDirectionName(int dir) } } - private class Rect - { - [JsonPropertyName("x")] public int X { get; set; } - [JsonPropertyName("y")] public int Y { get; set; } - [JsonPropertyName("w")] public int W { get; set; } - [JsonPropertyName("h")] public int H { get; set; } - } - - private class PointStruct - { - [JsonPropertyName("x")] public int X { get; set; } - [JsonPropertyName("y")] public int Y { get; set; } - } - private class SizeStruct - { - [JsonPropertyName("w")] public int W { get; set; } - [JsonPropertyName("h")] public int H { get; set; } - } private class AlphaSorter : IComparer { @@ -1740,7 +1700,7 @@ public PackOptionsForm() MinimizeBox = false; StartPosition = FormStartPosition.CenterParent; // increased size to accommodate taller direction list and wider right side - ClientSize = new Size(520, 460); + ClientSize = new Size(520, 490); Padding = new Padding(10); Label lbl = new Label { Text = "Directions:", Location = new Point(12, 12), AutoSize = true }; @@ -1826,11 +1786,11 @@ public PackOptionsForm() Controls.Add(_exportAllCheckBox); // make OK/Cancel taller and move to the right (anchored) - _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(330, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(300, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _ok.Click += Ok_Click; Controls.Add(_ok); - _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(440, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(410, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; Controls.Add(_cancel); AcceptButton = _ok; diff --git a/UoFiddler/PackerClasses.cs b/UoFiddler/PackerClasses.cs deleted file mode 100644 index 09986f5..0000000 --- a/UoFiddler/PackerClasses.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace UoFiddler.Controls.Classes -{ - /// - /// Represents the structure of the JSON file for packed animations. - /// Based on the provided Python scripts. - /// - public class AnimationPackFile - { - [JsonPropertyName("meta")] - public AnimationMetaData Meta { get; set; } - - [JsonPropertyName("frames")] - public List Frames { get; set; } - } - - public class AnimationMetaData - { - [JsonPropertyName("image")] - public string Image { get; set; } - - [JsonPropertyName("size")] - public FrameSize Size { get; set; } - - [JsonPropertyName("format")] - public string Format { get; set; } - } - - public class FrameSize - { - [JsonPropertyName("w")] - public int W { get; set; } - - [JsonPropertyName("h")] - public int H { get; set; } - } - - public class AnimationFrameData - { - [JsonPropertyName("filename")] - public string Filename { get; set; } - - [JsonPropertyName("frame")] - public FrameRect Frame { get; set; } - - [JsonPropertyName("sourceW")] - public int SourceW { get; set; } - - [JsonPropertyName("sourceH")] - public int SourceH { get; set; } - - [JsonPropertyName("format")] - public string Format { get; set; } - } - - public class FrameRect - { - [JsonPropertyName("x")] - public int X { get; set; } - - [JsonPropertyName("y")] - public int Y { get; set; } - - [JsonPropertyName("w")] - public int W { get; set; } - - [JsonPropertyName("h")] - public int H { get; set; } - } -} From 8c1bd76b8dde3402d4efc7e763a22ec5b7c419f1 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sun, 30 Nov 2025 10:34:44 -0600 Subject: [PATCH 09/12] Add import animation option to context menu Introduces an 'Import Animation' menu item to the context menu in AnimationListControl, allowing users to import animations via unpacking frames. Also refactors the export animation submenu to remove unpack options, consolidating import-related actions under the new menu item. --- .../AnimationListControl.Designer.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs b/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs index 17bb826..4e06220 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs @@ -57,6 +57,7 @@ private void InitializeComponent() asPngToolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem(); extractAnimationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); asBmpToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + importAnimationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); asTiffToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); asJpgToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); asPngToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); @@ -85,7 +86,8 @@ private void InitializeComponent() tryToFindNewGraphicsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); animationEditToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); GraphicLabel = new System.Windows.Forms.ToolStripStatusLabel(); - BaseGraphicLabel = new System.Windows.Forms.ToolStripStatusLabel(); + BaseGraphicLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.packFramesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.unpackFramesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.bulkUnpackFramesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -236,7 +238,7 @@ private void InitializeComponent() // // contextMenuStrip1 // - contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { extractImageToolStripMenuItem, extractAnimationToolStripMenuItem }); + contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { extractImageToolStripMenuItem, extractAnimationToolStripMenuItem, importAnimationToolStripMenuItem }); contextMenuStrip1.Name = "contextMenuStrip1"; contextMenuStrip1.Size = new System.Drawing.Size(174, 48); // @@ -277,7 +279,7 @@ private void InitializeComponent() // // extractAnimationToolStripMenuItem // - extractAnimationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { asBmpToolStripMenuItem1, asTiffToolStripMenuItem1, asJpgToolStripMenuItem1, asPngToolStripMenuItem1, asAnimatedGifToolStripMenuItem, asAnimatedGifnoLoopingToolStripMenuItem, new System.Windows.Forms.ToolStripSeparator(), this.packFramesToolStripMenuItem, this.unpackFramesToolStripMenuItem, this.bulkUnpackFramesToolStripMenuItem }); + extractAnimationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { asBmpToolStripMenuItem1, asTiffToolStripMenuItem1, asJpgToolStripMenuItem1, asPngToolStripMenuItem1, asAnimatedGifToolStripMenuItem, asAnimatedGifnoLoopingToolStripMenuItem, new System.Windows.Forms.ToolStripSeparator(), this.packFramesToolStripMenuItem }); extractAnimationToolStripMenuItem.Name = "extractAnimationToolStripMenuItem"; extractAnimationToolStripMenuItem.Size = new System.Drawing.Size(173, 22); extractAnimationToolStripMenuItem.Text = "Export Animation.."; @@ -324,6 +326,15 @@ private void InitializeComponent() asAnimatedGifnoLoopingToolStripMenuItem.Text = "As animated Gif (no looping)"; asAnimatedGifnoLoopingToolStripMenuItem.Click += OnClickExtractAnimGifNoLooping; // + // importAnimationToolStripMenuItem + // + this.importAnimationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.unpackFramesToolStripMenuItem, + this.bulkUnpackFramesToolStripMenuItem}); + this.importAnimationToolStripMenuItem.Name = "importAnimationToolStripMenuItem"; + this.importAnimationToolStripMenuItem.Size = new System.Drawing.Size(227, 22); + this.importAnimationToolStripMenuItem.Text = "Import Animation.."; + // // listView1 // listView1.Alignment = System.Windows.Forms.ListViewAlignment.Left; @@ -560,10 +571,11 @@ private void InitializeComponent() HueLabel.RightToLeft = System.Windows.Forms.RightToLeft.No; HueLabel.Size = new System.Drawing.Size(32, 17); HueLabel.Text = "Hue:"; - HueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + HueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // packFramesToolStripMenuItem // + this.packFramesToolStripMenuItem.Name = "packFramesToolStripMenuItem"; this.packFramesToolStripMenuItem.Size = new System.Drawing.Size(221, 22); this.packFramesToolStripMenuItem.Text = "Pack Frames to JSON/PNG"; @@ -666,5 +678,6 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem packFramesToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem unpackFramesToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem bulkUnpackFramesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem importAnimationToolStripMenuItem; } } From 3c7a72e21dd4dc50d2f3def016c1da7dc6b8ea00 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sun, 30 Nov 2025 10:40:57 -0600 Subject: [PATCH 10/12] Move PackOptionsForm to separate file The PackOptionsForm class was relocated from AnimationListControl.cs to its own file, PackOptionsForm.cs, to improve code organization and maintain compilation visibility. --- .../UserControls/AnimationListControl.cs | 141 --------------- .../UserControls/PackOptionsForm.cs | 163 ++++++++++++++++++ 2 files changed, 163 insertions(+), 141 deletions(-) create mode 100644 UoFiddler.Controls/UserControls/PackOptionsForm.cs diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index f00d13a..ce7fdb7 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -1673,146 +1673,5 @@ public int Compare(object x, object y) return 1; } } - - // Pack options dialog (moved here to ensure compilation visibility) - public class PackOptionsForm : Form - { - private CheckedListBox _directionsBox; - private NumericUpDown _maxWidthUpDown; - private CheckBox _oneRowPerDirectionCheckBox; - private CheckBox _exportAllCheckBox; - private TrackBar _spacingTrackBar; - private Label _spacingLabel; - private Button _ok; - private Button _cancel; - - public List SelectedDirections { get; private set; } = new List { 0, 1, 2, 3, 4 }; - public int MaxWidth { get; private set; } = 2048; - public bool OneRowPerDirection { get; private set; } - public int FrameSpacing { get; private set; } = 0; - public bool ExportAllAnimations { get; private set; } - - public PackOptionsForm() - { - Text = "Pack Options"; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; - StartPosition = FormStartPosition.CenterParent; - // increased size to accommodate taller direction list and wider right side - ClientSize = new Size(520, 490); - Padding = new Padding(10); - - Label lbl = new Label { Text = "Directions:", Location = new Point(12, 12), AutoSize = true }; - Controls.Add(lbl); - - _directionsBox = new CheckedListBox - { - Location = new Point(12, 32), - // make dropdown / list taller - Size = new Size(160, 260), - CheckOnClick = true, - ScrollAlwaysVisible = true, - IntegralHeight = false - }; - for (int i = 0; i < 8; i++) - { - _directionsBox.Items.Add(i.ToString(), i <= 4); - } - Controls.Add(_directionsBox); - - // move the max sprite width controls further to the right - Label lbl2 = new Label { Text = "Max sprite width:", Location = new Point(190, 32), AutoSize = true }; - Controls.Add(lbl2); - - _maxWidthUpDown = new NumericUpDown - { - Location = new Point(190, 56), - Size = new Size(260, 30), - Minimum = 256, - Maximum = 8192, - Increment = 64, - Value = 2048, - ThousandsSeparator = true - }; - Controls.Add(_maxWidthUpDown); - - // Optional quick presets (moved right and made slightly taller) - var presetsLabel = new Label { Text = "Presets:", Location = new Point(190, 95), AutoSize = true }; - Controls.Add(presetsLabel); - var presetSmall = new Button { Text = "1024", Location = new Point(190, 115), Size = new Size(70, 34) }; - var presetMedium = new Button { Text = "2048", Location = new Point(266, 115), Size = new Size(70, 34) }; - var presetLarge = new Button { Text = "4096", Location = new Point(342, 115), Size = new Size(70, 34) }; - presetSmall.Click += (s, e) => _maxWidthUpDown.Value = 1024; - presetMedium.Click += (s, e) => _maxWidthUpDown.Value = 2048; - presetLarge.Click += (s, e) => _maxWidthUpDown.Value = 4096; - Controls.Add(presetSmall); - Controls.Add(presetMedium); - Controls.Add(presetLarge); - - // Spacing Slider - var spacingTitle = new Label { Text = "Spacing:", Location = new Point(190, 160), AutoSize = true }; - Controls.Add(spacingTitle); - - _spacingLabel = new Label { Text = "0", Location = new Point(460, 160), AutoSize = true }; - Controls.Add(_spacingLabel); - - _spacingTrackBar = new TrackBar - { - Location = new Point(190, 180), - Size = new Size(280, 45), - Minimum = 0, - Maximum = 20, - Value = 0, - TickFrequency = 1 - }; - _spacingTrackBar.Scroll += (s, e) => _spacingLabel.Text = _spacingTrackBar.Value.ToString(); - Controls.Add(_spacingTrackBar); - - _oneRowPerDirectionCheckBox = new CheckBox - { - Text = "One row per direction", - Location = new Point(12, 300), - AutoSize = true - }; - Controls.Add(_oneRowPerDirectionCheckBox); - - _exportAllCheckBox = new CheckBox - { - Text = "Export all animations", - Location = new Point(12, 330), - AutoSize = true - }; - Controls.Add(_exportAllCheckBox); - - // make OK/Cancel taller and move to the right (anchored) - _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(300, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; - _ok.Click += Ok_Click; - Controls.Add(_ok); - - _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(410, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; - Controls.Add(_cancel); - - AcceptButton = _ok; - CancelButton = _cancel; - } - - private void Ok_Click(object sender, EventArgs e) - { - // Collect checked directions as integers - SelectedDirections = _directionsBox.CheckedItems.Cast().Select(o => int.Parse(o.ToString())).ToList(); - if (SelectedDirections.Count == 0) - { - MessageBox.Show("Select at least one direction.", "Pack Options", MessageBoxButtons.OK, MessageBoxIcon.Warning); - DialogResult = DialogResult.None; - return; - } - - MaxWidth = (int)_maxWidthUpDown.Value; - OneRowPerDirection = _oneRowPerDirectionCheckBox.Checked; - FrameSpacing = _spacingTrackBar.Value; - ExportAllAnimations = _exportAllCheckBox.Checked; - } - } } } diff --git a/UoFiddler.Controls/UserControls/PackOptionsForm.cs b/UoFiddler.Controls/UserControls/PackOptionsForm.cs new file mode 100644 index 0000000..50d16f0 --- /dev/null +++ b/UoFiddler.Controls/UserControls/PackOptionsForm.cs @@ -0,0 +1,163 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace UoFiddler.Controls.UserControls +{ + public partial class AnimationListControl + { + // Pack options dialog (moved here to ensure compilation visibility) + public class PackOptionsForm : Form + { + private CheckedListBox _directionsBox; + private NumericUpDown _maxWidthUpDown; + private CheckBox _oneRowPerDirectionCheckBox; + private CheckBox _exportAllCheckBox; + private TrackBar _spacingTrackBar; + private Label _spacingLabel; + private Button _ok; + private Button _cancel; + + public List SelectedDirections { get; private set; } = new List { 0, 1, 2, 3, 4 }; + public int MaxWidth { get; private set; } = 2048; + public bool OneRowPerDirection { get; private set; } + public int FrameSpacing { get; private set; } = 0; + public bool ExportAllAnimations { get; private set; } + + public PackOptionsForm() + { + Text = "Pack Options"; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + StartPosition = FormStartPosition.CenterParent; + // increased size to accommodate taller direction list and wider right side + ClientSize = new Size(520, 490); + Padding = new Padding(10); + + Label lbl = new Label { Text = "Directions:", Location = new Point(12, 12), AutoSize = true }; + Controls.Add(lbl); + + _directionsBox = new CheckedListBox + { + Location = new Point(12, 32), + // make dropdown / list taller + Size = new Size(160, 260), + CheckOnClick = true, + ScrollAlwaysVisible = true, + IntegralHeight = false + }; + for (int i = 0; i < 8; i++) + { + _directionsBox.Items.Add(i.ToString(), i <= 4); + } + Controls.Add(_directionsBox); + + // move the max sprite width controls further to the right + Label lbl2 = new Label { Text = "Max sprite width:", Location = new Point(190, 32), AutoSize = true }; + Controls.Add(lbl2); + + _maxWidthUpDown = new NumericUpDown + { + Location = new Point(190, 56), + Size = new Size(260, 30), + Minimum = 256, + Maximum = 8192, + Increment = 64, + Value = 2048, + ThousandsSeparator = true + }; + Controls.Add(_maxWidthUpDown); + + // Optional quick presets (moved right and made slightly taller) + var presetsLabel = new Label { Text = "Presets:", Location = new Point(190, 95), AutoSize = true }; + Controls.Add(presetsLabel); + var presetSmall = new Button { Text = "1024", Location = new Point(190, 115), Size = new Size(70, 34) }; + var presetMedium = new Button { Text = "2048", Location = new Point(266, 115), Size = new Size(70, 34) }; + var presetLarge = new Button { Text = "4096", Location = new Point(342, 115), Size = new Size(70, 34) }; + presetSmall.Click += (s, e) => _maxWidthUpDown.Value = 1024; + presetMedium.Click += (s, e) => _maxWidthUpDown.Value = 2048; + presetLarge.Click += (s, e) => _maxWidthUpDown.Value = 4096; + Controls.Add(presetSmall); + Controls.Add(presetMedium); + Controls.Add(presetLarge); + + // Spacing Slider + var spacingTitle = new Label { Text = "Spacing:", Location = new Point(190, 160), AutoSize = true }; + Controls.Add(spacingTitle); + + _spacingLabel = new Label { Text = "0", Location = new Point(460, 160), AutoSize = true }; + Controls.Add(_spacingLabel); + + _spacingTrackBar = new TrackBar + { + Location = new Point(190, 180), + Size = new Size(280, 45), + Minimum = 0, + Maximum = 20, + Value = 0, + TickFrequency = 1 + }; + _spacingTrackBar.Scroll += (s, e) => _spacingLabel.Text = _spacingTrackBar.Value.ToString(); + Controls.Add(_spacingTrackBar); + + _oneRowPerDirectionCheckBox = new CheckBox + { + Text = "One row per direction", + Location = new Point(12, 300), + AutoSize = true + }; + Controls.Add(_oneRowPerDirectionCheckBox); + + _exportAllCheckBox = new CheckBox + { + Text = "Export all animations", + Location = new Point(12, 330), + AutoSize = true + }; + Controls.Add(_exportAllCheckBox); + + // make OK/Cancel taller and move to the right (anchored) + _ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(300, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + _ok.Click += Ok_Click; + Controls.Add(_ok); + + _cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(410, 400), Size = new Size(100, 40), Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; + Controls.Add(_cancel); + + AcceptButton = _ok; + CancelButton = _cancel; + } + + private void Ok_Click(object sender, EventArgs e) + { + // Collect checked directions as integers + SelectedDirections = _directionsBox.CheckedItems.Cast().Select(o => int.Parse(o.ToString())).ToList(); + if (SelectedDirections.Count == 0) + { + MessageBox.Show("Select at least one direction.", "Pack Options", MessageBoxButtons.OK, MessageBoxIcon.Warning); + DialogResult = DialogResult.None; + return; + } + + MaxWidth = (int)_maxWidthUpDown.Value; + OneRowPerDirection = _oneRowPerDirectionCheckBox.Checked; + FrameSpacing = _spacingTrackBar.Value; + ExportAllAnimations = _exportAllCheckBox.Checked; + } + } + } +} From 1bcadaf5e9460fd2f1ae84b0c2b875c16937671d Mon Sep 17 00:00:00 2001 From: Moshu Date: Sun, 30 Nov 2025 14:46:23 -0600 Subject: [PATCH 11/12] Add animation debug image generation Introduced AnimationDebugHelper to create a debug image with frame bounds and center crosshairs. Updated AnimationListControl to generate and save this debug image alongside the packed sprite and JSON output. --- .../Helpers/AnimationDebugHelper.cs | 57 +++++++++++++++++++ .../UserControls/AnimationListControl.cs | 6 +- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 UoFiddler.Controls/Helpers/AnimationDebugHelper.cs diff --git a/UoFiddler.Controls/Helpers/AnimationDebugHelper.cs b/UoFiddler.Controls/Helpers/AnimationDebugHelper.cs new file mode 100644 index 0000000..e35d668 --- /dev/null +++ b/UoFiddler.Controls/Helpers/AnimationDebugHelper.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using UoFiddler.Controls.Classes; + +namespace UoFiddler.Controls.Helpers +{ + public static class AnimationDebugHelper + { + public static void CreateDebugImage(string outputPath, Bitmap packedImage, List frames) + { + if (packedImage == null || frames == null) + { + return; + } + + using (Bitmap debugImage = new Bitmap(packedImage.Width, packedImage.Height, PixelFormat.Format32bppArgb)) + { + using (Graphics g = Graphics.FromImage(debugImage)) + { + // 1. Draw the packed image + g.DrawImage(packedImage, 0, 0); + + using (Pen redPen = new Pen(Color.Red, 1)) + using (Pen blackPen = new Pen(Color.Black, 1)) + { + foreach (var frame in frames) + { + if (frame.Frame == null || frame.Center == null) + { + continue; + } + + // 2. Draw Red Rectangle for frame bounds + Rectangle rect = new Rectangle(frame.Frame.X, frame.Frame.Y, frame.Frame.W, frame.Frame.H); + g.DrawRectangle(redPen, rect); + + // Calculate absolute center point (Geometric Center) + int centerX = frame.Frame.X + (frame.Frame.W / 2); + int centerY = frame.Frame.Y + (frame.Frame.H / 2); + + // 3. Draw Black Lines (Crosshair) + // Horizontal line: from left of frame to right of frame at centerY + g.DrawLine(blackPen, frame.Frame.X, centerY, frame.Frame.X + frame.Frame.W, centerY); + + // Vertical line: from top of frame to bottom of frame at centerX + g.DrawLine(blackPen, centerX, frame.Frame.Y, centerX, frame.Frame.Y + frame.Frame.H); + } + } + } + + debugImage.Save(outputPath, ImageFormat.Png); + } + } + } +} diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index ce7fdb7..5f2fa53 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -1176,7 +1176,11 @@ private async void OnPackFramesClick(object? sender, EventArgs e) string json = JsonSerializer.Serialize(outObj, jsOptions); File.WriteAllText(jsonFile, json); - var result = new List { imageFile, jsonFile }; + // Generate Debug Image + string debugImageFile = Path.Combine(outDir, $"{baseName}_guide.png"); + AnimationDebugHelper.CreateDebugImage(debugImageFile, sprite, packedFrames); + + var result = new List { imageFile, jsonFile, debugImageFile }; if (oneRowPerDirection) { From c35956e5323827130c9d5d0044772a1360847792 Mon Sep 17 00:00:00 2001 From: Moshu Date: Sun, 30 Nov 2025 16:48:25 -0600 Subject: [PATCH 12/12] Add export/import packed items feature Introduces export and import functionality for packed item ranges in ItemsControl, including new classes for packed item metadata and output, a range input form, and UI integration. Users can export selected item ranges to a sprite sheet and JSON, and import them back, facilitating easier sharing and management of item graphics. --- UoFiddler.Controls/Classes/PackedItemEntry.cs | 20 ++ .../Classes/PackedItemOutput.cs | 21 ++ UoFiddler.Controls/Classes/PackedMeta.cs | 12 +- .../Forms/ItemRangeInputForm.cs | 124 ++++++++++ .../Forms/ItemRangeInputForm.resx | 120 +++++++++ .../UserControls/ItemsControl.Designer.cs | 20 +- .../UserControls/ItemsControl.cs | 230 +++++++++++++++++- .../UserControls/PackOptionsForm.cs | 2 +- 8 files changed, 540 insertions(+), 9 deletions(-) create mode 100644 UoFiddler.Controls/Classes/PackedItemEntry.cs create mode 100644 UoFiddler.Controls/Classes/PackedItemOutput.cs create mode 100644 UoFiddler.Controls/Forms/ItemRangeInputForm.cs create mode 100644 UoFiddler.Controls/Forms/ItemRangeInputForm.resx diff --git a/UoFiddler.Controls/Classes/PackedItemEntry.cs b/UoFiddler.Controls/Classes/PackedItemEntry.cs new file mode 100644 index 0000000..4cb859d --- /dev/null +++ b/UoFiddler.Controls/Classes/PackedItemEntry.cs @@ -0,0 +1,20 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +namespace UoFiddler.Controls.Classes +{ + public class PackedItemEntry + { + public int Index { get; set; } + public Rect Frame { get; set; } + public PointStruct Center { get; set; } + } +} diff --git a/UoFiddler.Controls/Classes/PackedItemOutput.cs b/UoFiddler.Controls/Classes/PackedItemOutput.cs new file mode 100644 index 0000000..7d43c9f --- /dev/null +++ b/UoFiddler.Controls/Classes/PackedItemOutput.cs @@ -0,0 +1,21 @@ +/*************************************************************************** + * + * $Author: Turley + * + * "THE BEER-WARE LICENSE" + * As long as you retain this notice you can do whatever you want with + * this stuff. If we meet some day, and you think this stuff is worth it, + * you can buy me a beer in return. + * + ***************************************************************************/ + +using System.Collections.Generic; + +namespace UoFiddler.Controls.Classes +{ + public class PackedItemOutput + { + public PackedMeta Meta { get; set; } + public List Items { get; set; } + } +} diff --git a/UoFiddler.Controls/Classes/PackedMeta.cs b/UoFiddler.Controls/Classes/PackedMeta.cs index cf69f6d..87cbe2c 100644 --- a/UoFiddler.Controls/Classes/PackedMeta.cs +++ b/UoFiddler.Controls/Classes/PackedMeta.cs @@ -13,10 +13,10 @@ namespace UoFiddler.Controls.Classes { - public class PackedMeta - { - [JsonPropertyName("image")] public string Image { get; set; } - [JsonPropertyName("size")] public SizeStruct Size { get; set; } - [JsonPropertyName("format")] public string Format { get; set; } - } + public class PackedMeta + { + [JsonPropertyName("image")] public string Image { get; set; } + [JsonPropertyName("size")] public SizeStruct Size { get; set; } + [JsonPropertyName("format")] public string Format { get; set; } + } } diff --git a/UoFiddler.Controls/Forms/ItemRangeInputForm.cs b/UoFiddler.Controls/Forms/ItemRangeInputForm.cs new file mode 100644 index 0000000..76b09b6 --- /dev/null +++ b/UoFiddler.Controls/Forms/ItemRangeInputForm.cs @@ -0,0 +1,124 @@ +using System; +using System.Windows.Forms; + +namespace UoFiddler.Controls.Forms +{ + public partial class ItemRangeInputForm : Form + { + private TextBox _rangeTextBox; + private Button _okButton; + private Button _cancelButton; + private Label _instructionLabel; + + public int StartIndex { get; private set; } + public int EndIndex { get; private set; } + + public ItemRangeInputForm() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + _rangeTextBox = new TextBox(); + _okButton = new Button(); + _cancelButton = new Button(); + _instructionLabel = new Label(); + SuspendLayout(); + // + // _rangeTextBox + // + _rangeTextBox.Location = new System.Drawing.Point(12, 29); + _rangeTextBox.Name = "_rangeTextBox"; + _rangeTextBox.Size = new System.Drawing.Size(260, 31); + _rangeTextBox.TabIndex = 0; + // + // _okButton + // + _okButton.DialogResult = DialogResult.OK; + _okButton.Location = new System.Drawing.Point(116, 66); + _okButton.Name = "_okButton"; + _okButton.Size = new System.Drawing.Size(75, 40); + _okButton.TabIndex = 1; + _okButton.Text = "OK"; + _okButton.UseVisualStyleBackColor = true; + _okButton.Click += OkButton_Click; + // + // _cancelButton + // + _cancelButton.DialogResult = DialogResult.Cancel; + _cancelButton.Location = new System.Drawing.Point(197, 66); + _cancelButton.Name = "_cancelButton"; + _cancelButton.Size = new System.Drawing.Size(75, 40); + _cancelButton.TabIndex = 2; + _cancelButton.Text = "Cancel"; + _cancelButton.UseVisualStyleBackColor = true; + // + // _instructionLabel + // + _instructionLabel.AutoSize = true; + _instructionLabel.Location = new System.Drawing.Point(12, 9); + _instructionLabel.Name = "_instructionLabel"; + _instructionLabel.Size = new System.Drawing.Size(230, 25); + _instructionLabel.TabIndex = 3; + _instructionLabel.Text = "Enter Range (e.g., 100-200):"; + // + // ItemRangeInputForm + // + AcceptButton = _okButton; + CancelButton = _cancelButton; + ClientSize = new System.Drawing.Size(307, 117); + Controls.Add(_instructionLabel); + Controls.Add(_cancelButton); + Controls.Add(_okButton); + Controls.Add(_rangeTextBox); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + Name = "ItemRangeInputForm"; + StartPosition = FormStartPosition.CenterParent; + Text = "Export Items Range"; + ResumeLayout(false); + PerformLayout(); + + } + + private void OkButton_Click(object sender, EventArgs e) + { + string input = _rangeTextBox.Text.Trim(); + string[] parts = input.Split('-'); + + if (parts.Length != 2) + { + MessageBox.Show("Invalid format. Please use 'Start-End' (e.g., 100-200).", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + this.DialogResult = DialogResult.None; + return; + } + + if (int.TryParse(parts[0], out int start) && int.TryParse(parts[1], out int end)) + { + if (start > end) + { + MessageBox.Show("Start index cannot be greater than end index.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + this.DialogResult = DialogResult.None; + return; + } + + if (end - start + 1 > 100) + { + MessageBox.Show("Range cannot exceed 100 items.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + this.DialogResult = DialogResult.None; + return; + } + + StartIndex = start; + EndIndex = end; + } + else + { + MessageBox.Show("Invalid numbers.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + this.DialogResult = DialogResult.None; + } + } + } +} diff --git a/UoFiddler.Controls/Forms/ItemRangeInputForm.resx b/UoFiddler.Controls/Forms/ItemRangeInputForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/UoFiddler.Controls/Forms/ItemRangeInputForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/UoFiddler.Controls/UserControls/ItemsControl.Designer.cs b/UoFiddler.Controls/UserControls/ItemsControl.Designer.cs index 04605df..a5e9bb8 100644 --- a/UoFiddler.Controls/UserControls/ItemsControl.Designer.cs +++ b/UoFiddler.Controls/UserControls/ItemsControl.Designer.cs @@ -95,6 +95,8 @@ private void InitializeComponent() asTiffToolStripMenuItem = new ToolStripMenuItem(); asJpgToolStripMenuItem = new ToolStripMenuItem(); asPngToolStripMenuItem = new ToolStripMenuItem(); + exportItemsToolStripMenuItem = new ToolStripMenuItem(); + importItemsToolStripMenuItem = new ToolStripMenuItem(); colorDialog = new ColorDialog(); collapsibleSplitter1 = new CollapsibleSplitter(); ((System.ComponentModel.ISupportInitialize)splitContainer2).BeginInit(); @@ -498,7 +500,7 @@ private void InitializeComponent() // MiscToolStripDropDownButton // MiscToolStripDropDownButton.DisplayStyle = ToolStripItemDisplayStyle.Text; - MiscToolStripDropDownButton.DropDownItems.AddRange(new ToolStripItem[] { ExportAllToolStripMenuItem }); + MiscToolStripDropDownButton.DropDownItems.AddRange(new ToolStripItem[] { ExportAllToolStripMenuItem, exportItemsToolStripMenuItem, importItemsToolStripMenuItem }); MiscToolStripDropDownButton.ImageTransparentColor = System.Drawing.Color.Magenta; MiscToolStripDropDownButton.Name = "MiscToolStripDropDownButton"; MiscToolStripDropDownButton.Size = new System.Drawing.Size(45, 25); @@ -511,6 +513,20 @@ private void InitializeComponent() ExportAllToolStripMenuItem.Size = new System.Drawing.Size(129, 22); ExportAllToolStripMenuItem.Text = "Export all.."; // + // exportItemsToolStripMenuItem + // + exportItemsToolStripMenuItem.Name = "exportItemsToolStripMenuItem"; + exportItemsToolStripMenuItem.Size = new System.Drawing.Size(150, 22); + exportItemsToolStripMenuItem.Text = "Export Items.."; + exportItemsToolStripMenuItem.Click += OnExportItemsClick; + // + // importItemsToolStripMenuItem + // + importItemsToolStripMenuItem.Name = "importItemsToolStripMenuItem"; + importItemsToolStripMenuItem.Size = new System.Drawing.Size(150, 22); + importItemsToolStripMenuItem.Text = "Import Items.."; + importItemsToolStripMenuItem.Click += OnImportItemsClick; + // // asBmpToolStripMenuItem // asBmpToolStripMenuItem.Name = "asBmpToolStripMenuItem"; @@ -601,6 +617,8 @@ private void InitializeComponent() private ContextMenuStrip TileViewContextMenuStrip; private PictureBox DetailPictureBox; private ToolStripMenuItem ExportAllToolStripMenuItem; + private ToolStripMenuItem exportItemsToolStripMenuItem; + private ToolStripMenuItem importItemsToolStripMenuItem; private ToolStripMenuItem extractToolStripMenuItem; private ToolStripMenuItem findNextFreeSlotToolStripMenuItem; private ToolStripStatusLabel GraphicLabel; diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs index 7f190d5..f36e3ac 100644 --- a/UoFiddler.Controls/UserControls/ItemsControl.cs +++ b/UoFiddler.Controls/UserControls/ItemsControl.cs @@ -22,7 +22,6 @@ using UoFiddler.Controls.Forms; using UoFiddler.Controls.Helpers; using UoFiddler.Controls.UserControls.TileView; - namespace UoFiddler.Controls.UserControls { public partial class ItemsControl : UserControl @@ -825,6 +824,235 @@ private void ExportAllItemImages(ImageFormat imageFormat) } } + private void OnExportItemsClick(object sender, EventArgs e) + { + using (var form = new ItemRangeInputForm()) + { + if (form.ShowDialog() != DialogResult.OK) + { + return; + } + + int start = form.StartIndex; + int end = form.EndIndex; + + using (var dlg = new FolderBrowserDialog()) + { + dlg.Description = "Select folder to save packed items"; + dlg.ShowNewFolderButton = true; + if (dlg.ShowDialog() != DialogResult.OK) + { + return; + } + + string outDir = dlg.SelectedPath; + PackItems(start, end, outDir); + } + } + } + + private void PackItems(int start, int end, string outDir) + { + try + { + Cursor.Current = Cursors.WaitCursor; + + var packedItems = new List(); + var images = new List(); + int spacing = 2; + int currentX = spacing; + int currentY = spacing; + int rowHeight = 0; + int canvasWidth = 0; + int canvasHeight = 0; + int maxWidth = 1024; // Arbitrary max width for the sheet + + for (int i = start; i <= end; i++) + { + if (!Art.IsValidStatic(i)) + { + continue; + } + + Bitmap bmp = Art.GetStatic(i); + if (bmp == null) + { + continue; + } + + int w = bmp.Width; + int h = bmp.Height; + + if (currentX + w > maxWidth) + { + currentY += rowHeight + spacing; + currentX = spacing; + rowHeight = 0; + } + + if (currentX == spacing) + rowHeight = h; + else + rowHeight = Math.Max(rowHeight, h); + + var entry = new PackedItemEntry + { + Index = i, + Frame = new Rect { X = currentX, Y = currentY, W = w, H = h }, + Center = new PointStruct { X = w / 2, Y = h / 2 } // Default center + }; + + packedItems.Add(entry); + images.Add(new Bitmap(bmp)); + + canvasWidth = Math.Max(canvasWidth, currentX + w + spacing); + canvasHeight = Math.Max(canvasHeight, currentY + rowHeight + spacing); + currentX += w + spacing; + } + + if (images.Count == 0) + { + MessageBox.Show("No valid items found in the specified range.", "Export Items", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // Create sprite sheet + using (var sprite = new Bitmap(Math.Max(1, canvasWidth), Math.Max(1, canvasHeight))) + using (var g = Graphics.FromImage(sprite)) + { + g.Clear(Color.Transparent); + + for (int i = 0; i < images.Count; i++) + { + var img = images[i]; + var rect = packedItems[i].Frame; + g.DrawImage(img, rect.X, rect.Y, rect.W, rect.H); + img.Dispose(); + } + + string baseName = $"items_{start}-{end}"; + string imageFile = Path.Combine(outDir, baseName + ".png"); + sprite.Save(imageFile, ImageFormat.Png); + + // JSON + var outObj = new PackedItemOutput + { + Meta = new PackedMeta { Image = Path.GetFileName(imageFile), Size = new SizeStruct { W = sprite.Width, H = sprite.Height }, Format = "RGBA8888" }, + Items = packedItems + }; + + string jsonFile = Path.Combine(outDir, baseName + ".json"); + var jsOptions = new System.Text.Json.JsonSerializerOptions { WriteIndented = true }; + string json = System.Text.Json.JsonSerializer.Serialize(outObj, jsOptions); + File.WriteAllText(jsonFile, json); + + // Guide Image + string guideFile = Path.Combine(outDir, $"{baseName}_guide.png"); + CreateGuideImage(guideFile, sprite, packedItems); + + MessageBox.Show($"Exported {packedItems.Count} items to {outDir}", "Export Items", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + catch (Exception ex) + { + MessageBox.Show($"Failed to export items: {ex.Message}", "Export Items", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + Cursor.Current = Cursors.Default; + } + } + + private void CreateGuideImage(string filename, Bitmap sprite, List items) + { + using (var guide = new Bitmap(sprite)) + using (var g = Graphics.FromImage(guide)) + { + using (var pen = new Pen(Color.Red, 1)) + using (var brush = new SolidBrush(Color.FromArgb(128, 0, 0, 255))) // Semi-transparent blue + { + foreach (var item in items) + { + var r = item.Frame; + g.DrawRectangle(pen, r.X, r.Y, r.W - 1, r.H - 1); + + // Draw center cross + int cx = r.X + item.Center.X; + int cy = r.Y + item.Center.Y; + g.DrawLine(pen, cx - 3, cy, cx + 3, cy); + g.DrawLine(pen, cx, cy - 3, cx, cy + 3); + + // Draw Index + g.DrawString(item.Index.ToString(), this.Font, Brushes.Black, r.X + 1, r.Y + 1); + } + } + guide.Save(filename, ImageFormat.Png); + } + } + + private void OnImportItemsClick(object sender, EventArgs e) + { + using (var ofd = new OpenFileDialog()) + { + ofd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*"; + ofd.Title = "Select items packing JSON file"; + if (ofd.ShowDialog() != DialogResult.OK) + { + return; + } + + try + { + UnpackItems(ofd.FileName); + MessageBox.Show("Import finished.", "Import Items", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to import items: {ex.Message}", "Import Items", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void UnpackItems(string jsonFile) + { + string json = File.ReadAllText(jsonFile); + var doc = System.Text.Json.JsonSerializer.Deserialize(json); + if (doc == null) + { + throw new Exception("Invalid JSON file."); + } + + string spritePath = Path.Combine(Path.GetDirectoryName(jsonFile) ?? string.Empty, doc.Meta.Image); + if (!File.Exists(spritePath)) + { + throw new FileNotFoundException($"Sprite sheet not found: {spritePath}"); + } + + using (var sprite = new Bitmap(spritePath)) + { + foreach (var item in doc.Items) + { + var r = item.Frame; + if (r.W <= 0 || r.H <= 0) continue; + + // Extract image + // Extract image + var bmp = new Bitmap(r.W, r.H); + using (var g = Graphics.FromImage(bmp)) + { + g.Clear(Color.Transparent); + g.DrawImage(sprite, new Rectangle(0, 0, r.W, r.H), new Rectangle(r.X, r.Y, r.W, r.H), GraphicsUnit.Pixel); + } + + // Update Art + Art.ReplaceStatic(item.Index, bmp); + } + + ItemsTileView.Invalidate(); + Options.ChangedUltimaClass["Art"] = true; + } + } + private void OnClickPreLoad(object sender, EventArgs e) { if (PreLoader.IsBusy) diff --git a/UoFiddler.Controls/UserControls/PackOptionsForm.cs b/UoFiddler.Controls/UserControls/PackOptionsForm.cs index 50d16f0..c74d1cd 100644 --- a/UoFiddler.Controls/UserControls/PackOptionsForm.cs +++ b/UoFiddler.Controls/UserControls/PackOptionsForm.cs @@ -39,7 +39,7 @@ public class PackOptionsForm : Form public PackOptionsForm() { - Text = "Pack Options"; + Text = "Pack Options - By Moshu"; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;