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/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 new file mode 100644 index 0000000..87cbe2c --- /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/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/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/AnimDataControl.cs b/UoFiddler.Controls/UserControls/AnimDataControl.cs index 3fa2799..d5175ae 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) @@ -316,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.Designer.cs b/UoFiddler.Controls/UserControls/AnimationListControl.Designer.cs index 7723218..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(); @@ -86,6 +87,11 @@ private void InitializeComponent() animationEditToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); GraphicLabel = 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(); + HueLabel = new System.Windows.Forms.ToolStripStatusLabel(); ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); splitContainer1.Panel1.SuspendLayout(); @@ -232,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); // @@ -273,7 +279,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 }); extractAnimationToolStripMenuItem.Name = "extractAnimationToolStripMenuItem"; extractAnimationToolStripMenuItem.Size = new System.Drawing.Size(173, 22); extractAnimationToolStripMenuItem.Text = "Export Animation.."; @@ -320,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; @@ -558,6 +573,25 @@ private void InitializeComponent() HueLabel.Text = "Hue:"; 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"; + // + // 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); @@ -641,5 +675,9 @@ 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; + private System.Windows.Forms.ToolStripMenuItem bulkUnpackFramesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem importAnimationToolStripMenuItem; } } diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs index 672ec2b..5f2fa53 100644 --- a/UoFiddler.Controls/UserControls/AnimationListControl.cs +++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs @@ -16,6 +16,8 @@ using System.Drawing.Imaging; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Windows.Forms; using System.Xml; using Ultima; @@ -34,8 +36,13 @@ 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; + bulkUnpackFramesToolStripMenuItem.Click += OnBulkUnpackFramesClick; } + public string[][] GetActionNames { get; } = { // Monster new[] @@ -945,57 +952,730 @@ 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) { - return (int)tx.Tag == -1 ? -1 : 1; + MessageBox.Show("No graphic selected.", "Pack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; } - if (tx.Parent.Parent != null) + + // show pack options dialog + using var optionsForm = new PackOptionsForm(); + if (optionsForm.ShowDialog() != DialogResult.OK) { - return (int)tx.Tag - (int)ty.Tag; + return; } - return string.CompareOrdinal(tx.Text, ty.Text); + var selectedDirections = optionsForm.SelectedDirections; // list + 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()) + { + 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; + + if (exportAll) + { + int exportedCount = 0; + TreeNode? bodyNode = null; + + // Find the body node based on current selection + if (TreeViewMobs.SelectedNode != null) + { + 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 + { + bodyNode = TreeViewMobs.SelectedNode.Parent; + } + } + + if (bodyNode != null) + { + foreach (TreeNode node in bodyNode.Nodes) + { + 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); + } + + 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) + { + 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; + } + } } - } - public class GraphicSorter : IComparer - { - public int Compare(object x, object 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(); + + 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) + { + int localHue = 0; + var frames = Animations.GetAnimation(body, action, dir, ref localHue, false, false); + if (frames == null || frames.Length == 0) + { + continue; + } + + if (oneRowPerDirection) + { + if (currentX > spacing) + { + currentY += rowHeight + spacing; + currentX = spacing; + 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 + spacing; + currentX = spacing; + rowHeight = 0; + } + + 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 } + }; + + packedFrames.Add(entry); + + // store image copy + images.Add(new Bitmap(anim.Bitmap)); + + canvasWidth = Math.Max(canvasWidth, currentX + w + spacing); // Include right margin + currentX += w + spacing; + canvasHeight = Math.Max(canvasHeight, currentY + rowHeight + spacing); // Include bottom margin + } + } + + 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++) + { + 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_{body}_{action}"; + 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); + + // 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) + { + string txtFile = Path.Combine(outDir, baseName + "_rows.txt"); + File.WriteAllText(txtFile, rowMapping.ToString()); + result.Add(txtFile); + } + + return result; + } + } + + private void OnUnpackFramesClick(object? sender, EventArgs e) { - TreeNode tx = x as TreeNode; - TreeNode ty = y as TreeNode; - if (tx.Parent == null) + if (_currentSelect == 0) { - return (int)tx.Tag == -1 ? -1 : 1; + MessageBox.Show("No graphic selected.", "Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; } - if (tx.Parent.Parent != null) + using (var ofd = new OpenFileDialog()) { - return (int)tx.Tag - (int)ty.Tag; + 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 + { + 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); + } } + } - int[] ix = (int[])tx.Tag; - int[] iy = (int[])ty.Tag; + private void OnBulkUnpackFramesClick(object? sender, EventArgs e) + { + if (_currentSelect == 0) + { + MessageBox.Show("No graphic selected.", "Bulk Unpack Frames", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } - if (ix[0] == iy[0]) + using (var dlg = new FolderBrowserDialog()) { - return 0; + dlg.Description = "Select folder containing exported animations (JSON + PNG)"; + dlg.ShowNewFolderButton = false; + + if (dlg.ShowDialog() != DialogResult.OK) + { + return; + } + + 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; + } + + int importedCount = 0; + int errorCount = 0; + + 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); + + if (globalChoice == DialogResult.Cancel) + return; + + bool overwriteAll = (globalChoice == DialogResult.Yes); + + foreach (string jsonFile in jsonFiles) + { + // Expected format: anim_{body}_{action}.json + string fileName = Path.GetFileNameWithoutExtension(jsonFile); + string[] parts = fileName.Split('_'); + + 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) + { + try + { + UnpackAnimation(jsonFile, body, action, false, overwriteAll); + importedCount++; + } + catch + { + 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); } + } - if (ix[0] < iy[0]) + 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) { - return -1; + throw new Exception("Invalid JSON file."); } - return 1; + 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()); + + 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; + + 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; + if (dir > 4) + { + continue; + } + 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(); + } + } + } + + Options.ChangedUltimaClass["Animations"] = true; + if (body == _currentSelect) + { + CurrentSelect = CurrentSelect; // this calls SetPicture() and repopulates frames + } + } + } + + // --- 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 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 AlphaSorter : IComparer + { + public int Compare(object x, object y) + { + 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); + } + } + + public class GraphicSorter : IComparer + { + public int Compare(object x, object y) + { + 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; + } } } } 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 new file mode 100644 index 0000000..c74d1cd --- /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 - By Moshu"; + 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; + } + } + } +}