From 67a4a2cf25f971fa4c7a16c88c87f159ab0400ba Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:01:26 +0200
Subject: [PATCH 01/21] Add index search to animdata. Add "Select in animdata"
to tiledata control.
---
.../UserControls/AnimDataControl.Designer.cs | 43 ++++++++++++--
.../UserControls/AnimDataControl.cs | 57 +++++++++++++++++++
.../UserControls/TileDataControl.Designer.cs | 11 +++-
.../UserControls/TileDataControl.cs | 15 +++++
4 files changed, 120 insertions(+), 6 deletions(-)
diff --git a/UoFiddler.Controls/UserControls/AnimDataControl.Designer.cs b/UoFiddler.Controls/UserControls/AnimDataControl.Designer.cs
index ec292a1..03a2302 100644
--- a/UoFiddler.Controls/UserControls/AnimDataControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/AnimDataControl.Designer.cs
@@ -41,6 +41,9 @@ private void InitializeComponent()
{
components = new System.ComponentModel.Container();
splitContainer1 = new System.Windows.Forms.SplitContainer();
+ searchToolStrip = new System.Windows.Forms.ToolStrip();
+ searchByIdToolStripLabel = new System.Windows.Forms.ToolStripLabel();
+ searchByIdToolStripTextBox = new System.Windows.Forms.ToolStripTextBox();
treeView1 = new System.Windows.Forms.TreeView();
contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(components);
addToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -80,6 +83,7 @@ private void InitializeComponent()
splitContainer1.Panel1.SuspendLayout();
splitContainer1.Panel2.SuspendLayout();
splitContainer1.SuspendLayout();
+ searchToolStrip.SuspendLayout();
contextMenuStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)splitContainer2).BeginInit();
splitContainer2.Panel1.SuspendLayout();
@@ -103,8 +107,9 @@ private void InitializeComponent()
splitContainer1.Name = "splitContainer1";
//
// splitContainer1.Panel1
- //
+ //
splitContainer1.Panel1.Controls.Add(treeView1);
+ splitContainer1.Panel1.Controls.Add(searchToolStrip);
//
// splitContainer1.Panel2
//
@@ -113,16 +118,38 @@ private void InitializeComponent()
splitContainer1.SplitterDistance = 235;
splitContainer1.SplitterWidth = 5;
splitContainer1.TabIndex = 0;
- //
+ //
+ // searchToolStrip
+ //
+ searchToolStrip.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
+ searchToolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { searchByIdToolStripLabel, searchByIdToolStripTextBox });
+ searchToolStrip.Location = new System.Drawing.Point(0, 0);
+ searchToolStrip.Name = "searchToolStrip";
+ searchToolStrip.RenderMode = System.Windows.Forms.ToolStripRenderMode.System;
+ searchToolStrip.Size = new System.Drawing.Size(235, 25);
+ searchToolStrip.TabIndex = 1;
+ //
+ // searchByIdToolStripLabel
+ //
+ searchByIdToolStripLabel.Name = "searchByIdToolStripLabel";
+ searchByIdToolStripLabel.Size = new System.Drawing.Size(39, 22);
+ searchByIdToolStripLabel.Text = "Index:";
+ //
+ // searchByIdToolStripTextBox
+ //
+ searchByIdToolStripTextBox.Name = "searchByIdToolStripTextBox";
+ searchByIdToolStripTextBox.Size = new System.Drawing.Size(100, 25);
+ searchByIdToolStripTextBox.KeyUp += SearchByIdToolStripTextBox_KeyUp;
+ //
// treeView1
- //
+ //
treeView1.ContextMenuStrip = contextMenuStrip1;
treeView1.Dock = System.Windows.Forms.DockStyle.Fill;
treeView1.HideSelection = false;
- treeView1.Location = new System.Drawing.Point(0, 0);
+ treeView1.Location = new System.Drawing.Point(0, 25);
treeView1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
treeView1.Name = "treeView1";
- treeView1.Size = new System.Drawing.Size(235, 587);
+ treeView1.Size = new System.Drawing.Size(235, 562);
treeView1.TabIndex = 0;
treeView1.AfterSelect += AfterNodeSelect;
treeView1.NodeMouseClick += OnClickNode;
@@ -491,9 +518,12 @@ private void InitializeComponent()
Size = new System.Drawing.Size(857, 587);
Load += OnLoad;
splitContainer1.Panel1.ResumeLayout(false);
+ splitContainer1.Panel1.PerformLayout();
splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
splitContainer1.ResumeLayout(false);
+ searchToolStrip.ResumeLayout(false);
+ searchToolStrip.PerformLayout();
contextMenuStrip1.ResumeLayout(false);
splitContainer2.Panel1.ResumeLayout(false);
splitContainer2.Panel2.ResumeLayout(false);
@@ -552,5 +582,8 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripMenuItem exportAsAnimatedGifToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem showFrameBoundsToolStripMenuItem;
private AnimatedPictureBox MainPictureBox;
+ private System.Windows.Forms.ToolStrip searchToolStrip;
+ private System.Windows.Forms.ToolStripLabel searchByIdToolStripLabel;
+ private System.Windows.Forms.ToolStripTextBox searchByIdToolStripTextBox;
}
}
diff --git a/UoFiddler.Controls/UserControls/AnimDataControl.cs b/UoFiddler.Controls/UserControls/AnimDataControl.cs
index 203152f..931da89 100644
--- a/UoFiddler.Controls/UserControls/AnimDataControl.cs
+++ b/UoFiddler.Controls/UserControls/AnimDataControl.cs
@@ -31,8 +31,11 @@ public AnimDataControl()
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
MainPictureBox.FrameChanged += MainPictureBox_FrameChanged;
+
+ _refMarker = this;
}
+ private static AnimDataControl _refMarker;
private static bool _loaded;
private Animdata.AnimdataEntry _selAnimdataEntry;
private int _currentSelect;
@@ -160,6 +163,7 @@ private void Reload()
MainPictureBox.Reset();
animateToolStripMenuItem.Checked = false;
showFrameBoundsToolStripMenuItem.Checked = false;
+ _loaded = false;
OnLoad(this, EventArgs.Empty);
}
}
@@ -171,6 +175,11 @@ private void OnLoad(object sender, EventArgs e)
return;
}
+ if (_loaded)
+ {
+ return;
+ }
+
Cursor.Current = Cursors.WaitCursor;
Options.LoadedUltimaClass["Animdata"] = true;
Options.LoadedUltimaClass["TileData"] = true;
@@ -690,6 +699,54 @@ private void OnClickShowFrameBounds(object sender, EventArgs e)
MainPictureBox.ShowFrameBounds = !MainPictureBox.ShowFrameBounds;
showFrameBoundsToolStripMenuItem.Checked = MainPictureBox.ShowFrameBounds;
}
+
+ private void SearchByIdToolStripTextBox_KeyUp(object sender, KeyEventArgs e)
+ {
+ if (!Utils.ConvertStringToInt(searchByIdToolStripTextBox.Text, out int indexValue, 0, Art.GetMaxItemId()))
+ {
+ return;
+ }
+
+ foreach (TreeNode node in treeView1.Nodes)
+ {
+ if ((int)node.Tag != indexValue)
+ {
+ continue;
+ }
+
+ treeView1.SelectedNode = node;
+ node.EnsureVisible();
+ return;
+ }
+ }
+
+ public static bool Select(int graphic)
+ {
+ if (_refMarker == null)
+ {
+ return false;
+ }
+
+ if (!_loaded)
+ {
+ _refMarker.OnLoad(_refMarker, EventArgs.Empty);
+ }
+
+ foreach (TreeNode node in _refMarker.treeView1.Nodes)
+ {
+ if ((int)node.Tag != graphic)
+ {
+ continue;
+ }
+
+ _refMarker.treeView1.SelectedNode = node;
+ node.EnsureVisible();
+ _refMarker.treeView1.Focus();
+ return true;
+ }
+
+ return false;
+ }
}
public class AnimdataSorter : IComparer
diff --git a/UoFiddler.Controls/UserControls/TileDataControl.Designer.cs b/UoFiddler.Controls/UserControls/TileDataControl.Designer.cs
index cd14d09..a78dfe5 100644
--- a/UoFiddler.Controls/UserControls/TileDataControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/TileDataControl.Designer.cs
@@ -52,6 +52,7 @@ private void InitializeComponent()
toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
selectInGumpsTabMaleToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
selectInGumpsTabFemaleToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ selectInAnimDataTabToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
pictureBoxItem = new System.Windows.Forms.PictureBox();
splitContainer3 = new System.Windows.Forms.SplitContainer();
nameLabel = new System.Windows.Forms.Label();
@@ -246,7 +247,7 @@ private void InitializeComponent()
//
// ItemsContextMenuStrip
//
- ItemsContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { selectInItemsToolStripMenuItem, selectRadarColorToolStripMenuItem, toolStripSeparator3, selectInGumpsTabMaleToolStripMenuItem, selectInGumpsTabFemaleToolStripMenuItem });
+ ItemsContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { selectInItemsToolStripMenuItem, selectRadarColorToolStripMenuItem, toolStripSeparator3, selectInGumpsTabMaleToolStripMenuItem, selectInGumpsTabFemaleToolStripMenuItem, selectInAnimDataTabToolStripMenuItem });
ItemsContextMenuStrip.Name = "contextMenuStrip1";
ItemsContextMenuStrip.Size = new System.Drawing.Size(201, 98);
ItemsContextMenuStrip.Opening += ItemsContextMenuStrip_Opening;
@@ -283,6 +284,13 @@ private void InitializeComponent()
selectInGumpsTabFemaleToolStripMenuItem.Size = new System.Drawing.Size(200, 22);
selectInGumpsTabFemaleToolStripMenuItem.Text = "Select in Gumps (F)";
selectInGumpsTabFemaleToolStripMenuItem.Click += SelectInGumpsTabFemaleToolStripMenuItem_Click;
+ //
+ // selectInAnimDataTabToolStripMenuItem
+ //
+ selectInAnimDataTabToolStripMenuItem.Name = "selectInAnimDataTabToolStripMenuItem";
+ selectInAnimDataTabToolStripMenuItem.Size = new System.Drawing.Size(200, 22);
+ selectInAnimDataTabToolStripMenuItem.Text = "Select in AnimData tab";
+ selectInAnimDataTabToolStripMenuItem.Click += SelectInAnimDataTabToolStripMenuItem_Click;
//
// pictureBoxItem
//
@@ -1200,6 +1208,7 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
private System.Windows.Forms.ToolStripMenuItem selectInGumpsTabMaleToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem selectInGumpsTabFemaleToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem selectInAnimDataTabToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem setTexturesToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
private System.Windows.Forms.ToolStripMenuItem setTextureOnDoubleClickToolStripMenuItem;
diff --git a/UoFiddler.Controls/UserControls/TileDataControl.cs b/UoFiddler.Controls/UserControls/TileDataControl.cs
index 37dd433..c7ace62 100644
--- a/UoFiddler.Controls/UserControls/TileDataControl.cs
+++ b/UoFiddler.Controls/UserControls/TileDataControl.cs
@@ -1731,6 +1731,7 @@ private void ItemsContextMenuStrip_Opening(object sender, System.ComponentModel.
{
selectInGumpsTabMaleToolStripMenuItem.Enabled = false;
selectInGumpsTabFemaleToolStripMenuItem.Enabled = false;
+ selectInAnimDataTabToolStripMenuItem.Enabled = false;
}
else
{
@@ -1749,7 +1750,21 @@ private void ItemsContextMenuStrip_Opening(object sender, System.ComponentModel.
selectInGumpsTabMaleToolStripMenuItem.Enabled = false;
selectInGumpsTabFemaleToolStripMenuItem.Enabled = false;
}
+
+ selectInAnimDataTabToolStripMenuItem.Enabled =
+ Animdata.GetAnimData((int)selectedItemTag) != null;
+ }
+ }
+
+ private void SelectInAnimDataTabToolStripMenuItem_Click(object sender, EventArgs e)
+ {
+ var selectedItemTag = treeViewItem.SelectedNode?.Tag;
+ if (selectedItemTag is null || (int)selectedItemTag <= 0)
+ {
+ return;
}
+
+ AnimDataControl.Select((int)selectedItemTag);
}
///
From 413d09a6dd7ae39982d1afbe4f20bd8a46693e0f Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:02:18 +0200
Subject: [PATCH 02/21] Add multi-select for compare plugins.
---
.../UserControls/TileView/TileViewControl.cs | 84 ++-
.../CompareAnimDataControl.Designer.cs | 64 ++-
.../UserControls/CompareAnimDataControl.cs | 118 +++-
.../CompareCliLocControl.Designer.cs | 1 +
.../CompareGumpControl.Designer.cs | 426 ++++++++-------
.../UserControls/CompareGumpControl.cs | 136 ++++-
.../UserControls/CompareGumpControl.resx | 60 +++
.../CompareHuesControl.Designer.cs | 16 +-
.../UserControls/CompareHuesControl.cs | 115 +++-
.../CompareItemControl.Designer.cs | 502 +++++++++---------
.../UserControls/CompareItemControl.cs | 137 ++++-
.../UserControls/CompareItemControl.resx | 60 +++
.../CompareLandControl.Designer.cs | 500 +++++++++--------
.../UserControls/CompareLandControl.cs | 135 ++++-
.../UserControls/CompareLandControl.resx | 60 +++
.../CompareRadarColControl.Designer.cs | 78 +--
.../UserControls/CompareRadarColControl.cs | 148 +++++-
.../CompareTextureControl.Designer.cs | 23 +-
.../UserControls/CompareTextureControl.cs | 135 ++++-
.../CompareTileDataControl.Designer.cs | 22 +-
.../UserControls/CompareTileDataControl.cs | 210 ++++++--
21 files changed, 2087 insertions(+), 943 deletions(-)
diff --git a/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs b/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
index 5c1cacd..bec15b1 100644
--- a/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
+++ b/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
@@ -112,6 +112,33 @@ public bool MultiSelect
}
}
+ private bool _showCheckBoxes;
+ private const int CheckBoxSize = 13;
+ private const int CheckBoxLeftInset = 5;
+
+ ///
+ /// Horizontal space (in pixels) reserved on the left of each tile for the checkbox column when
+ /// is enabled. DrawItem handlers should offset their content by
+ /// e.ContentLeft instead of hard-coding the X position so the checkbox does not overlap text/images.
+ ///
+ public const int CheckBoxColumnWidth = 22;
+
+ ///
+ /// When true, a checkbox is drawn at the right edge of every tile and clicking it toggles the tile in
+ /// without changing . Intended for multi-selection workflows.
+ ///
+ [Browsable(true)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool ShowCheckBoxes
+ {
+ get => _showCheckBoxes;
+ set
+ {
+ _showCheckBoxes = value;
+ Invalidate();
+ }
+ }
+
private int _virtualListSize;
///
@@ -340,6 +367,19 @@ public TileViewControl()
{
int idx = GetIndexAtLocation(e.Location);
+ if (_showCheckBoxes && idx >= 0 && e.Button == MouseButtons.Left && IsInCheckBoxRegion(e.Location, idx))
+ {
+ if (SelectedIndices.Contains(idx))
+ {
+ SelectedIndices.Remove(idx);
+ }
+ else
+ {
+ SelectedIndices.Add(idx);
+ }
+ return;
+ }
+
FocusIndex = idx;
if (idx != -2 && e.Button == MouseButtons.Left) // no Tile at given location
@@ -512,6 +552,13 @@ private void SelectIndex(int index)
break;
default:
+ // When the checkbox column is visible, the selection set is owned by the checkboxes;
+ // a plain click must only change focus and never wipe an in-progress multi-selection.
+ if (_showCheckBoxes)
+ {
+ break;
+ }
+
if (!SelectedIndices.Contains(index))
{
SelectedIndices.Clear();
@@ -542,6 +589,23 @@ private Point GetItemLocation(int index)
return new Point(index % _itemsPerRow * TotalTileSize.Width, index / _itemsPerRow * TotalTileSize.Height);
}
+ private Rectangle GetCheckBoxRect(int index)
+ {
+ Point itemLoc = GetItemLocation(index);
+ int left = itemLoc.X + _tileMargin.Left + (int)_tileBorder.Width + CheckBoxLeftInset;
+ int size = Math.Min(CheckBoxSize, Math.Max(8, TotalTileSize.Height - 4));
+ int top = itemLoc.Y + (TotalTileSize.Height - size) / 2;
+ return new Rectangle(left, top, size, size);
+ }
+
+ private bool IsInCheckBoxRegion(Point location, int index)
+ {
+ Rectangle cb = GetCheckBoxRect(index);
+ int virtX = location.X - AutoScrollPosition.X;
+ int virtY = location.Y - AutoScrollPosition.Y;
+ return cb.Contains(virtX, virtY);
+ }
+
///
/// Find index of Tile for given location.
///
@@ -671,7 +735,8 @@ protected override void OnPaint(PaintEventArgs e)
if (DrawItem != null)
{
- DrawItem(this, new DrawTileListItemEventArgs(e.Graphics, Font, borderRec, i, _focusIndex == i ? DrawItemState.Selected : DrawItemState.None));
+ int contentLeft = _showCheckBoxes ? CheckBoxColumnWidth : 0;
+ DrawItem(this, new DrawTileListItemEventArgs(e.Graphics, Font, borderRec, i, _focusIndex == i ? DrawItemState.Selected : DrawItemState.None, contentLeft));
}
else
{
@@ -715,6 +780,13 @@ protected override void OnPaint(PaintEventArgs e)
e.Graphics.DrawRectangle(pen, focusRec);
}
}
+
+ if (_showCheckBoxes)
+ {
+ Rectangle cb = GetCheckBoxRect(i);
+ ButtonState state = SelectedIndices.Contains(i) ? ButtonState.Checked : ButtonState.Normal;
+ ControlPaint.DrawCheckBox(e.Graphics, cb, state);
+ }
}
}
@@ -732,9 +804,17 @@ public ListViewFocusedItemSelectionChangedEventArgs(int focusedItemIndex, bool i
public class DrawTileListItemEventArgs : DrawItemEventArgs
{
+ ///
+ /// Horizontal offset inside where the handler's content
+ /// should start drawing, to avoid overlapping the checkbox column when
+ /// is enabled. Zero when no checkbox column is reserved.
+ ///
+ public int ContentLeft { get; }
+
public DrawTileListItemEventArgs(Graphics graphics, Font font, Rectangle rect, int index,
- DrawItemState state) : base(graphics, font, rect, index, state)
+ DrawItemState state, int contentLeft = 0) : base(graphics, font, rect, index, state)
{
+ ContentLeft = contentLeft;
}
}
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.Designer.cs
index 72337c2..0c4248e 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.Designer.cs
@@ -56,6 +56,7 @@ private void InitializeComponent()
buttonCopyAllDiff = new System.Windows.Forms.Button();
buttonCopySelected = new System.Windows.Forms.Button();
checkBoxShowDiff = new System.Windows.Forms.CheckBox();
+ chkMultiSelect = new System.Windows.Forms.CheckBox();
buttonLoadSecond = new System.Windows.Forms.Button();
buttonBrowse = new System.Windows.Forms.Button();
textBoxSecondFile = new System.Windows.Forms.TextBox();
@@ -90,11 +91,12 @@ private void InitializeComponent()
splitContainer1.Panel2.Controls.Add(buttonCopyAllDiff);
splitContainer1.Panel2.Controls.Add(buttonCopySelected);
splitContainer1.Panel2.Controls.Add(checkBoxShowDiff);
+ splitContainer1.Panel2.Controls.Add(chkMultiSelect);
splitContainer1.Panel2.Controls.Add(buttonLoadSecond);
splitContainer1.Panel2.Controls.Add(buttonBrowse);
splitContainer1.Panel2.Controls.Add(textBoxSecondFile);
splitContainer1.Size = new System.Drawing.Size(900, 510);
- splitContainer1.SplitterDistance = 454;
+ splitContainer1.SplitterDistance = 443;
splitContainer1.SplitterWidth = 5;
splitContainer1.TabIndex = 0;
//
@@ -112,24 +114,20 @@ private void InitializeComponent()
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 1;
tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
- tableLayoutPanel1.Size = new System.Drawing.Size(900, 454);
+ tableLayoutPanel1.Size = new System.Drawing.Size(900, 443);
tableLayoutPanel1.TabIndex = 0;
- //
+ //
// tileViewOrg
- //
+ //
tileViewOrg.Dock = System.Windows.Forms.DockStyle.Fill;
tileViewOrg.Location = new System.Drawing.Point(3, 3);
tileViewOrg.Name = "tileViewOrg";
- tileViewOrg.Size = new System.Drawing.Size(219, 448);
+ tileViewOrg.Size = new System.Drawing.Size(219, 437);
tileViewOrg.TabIndex = 0;
- tileViewOrg.TileSize = new System.Drawing.Size(219, 15);
- tileViewOrg.TileMargin = new System.Windows.Forms.Padding(0);
- tileViewOrg.TilePadding = new System.Windows.Forms.Padding(0);
- tileViewOrg.TileBorderWidth = 0f;
- tileViewOrg.TileHighLightOpacity = 0.0;
- tileViewOrg.DrawItem += new System.EventHandler(OnDrawItemOrg);
- tileViewOrg.FocusSelectionChanged += new System.EventHandler(OnFocusChangedOrg);
- tileViewOrg.SizeChanged += new System.EventHandler(OnTileViewSizeChanged);
+ tileViewOrg.TileHighLightOpacity = 0D;
+ tileViewOrg.FocusSelectionChanged += OnFocusChangedOrg;
+ tileViewOrg.DrawItem += OnDrawItemOrg;
+ tileViewOrg.SizeChanged += OnTileViewSizeChanged;
//
// panelDetail
//
@@ -139,7 +137,7 @@ private void InitializeComponent()
panelDetail.Dock = System.Windows.Forms.DockStyle.Fill;
panelDetail.Location = new System.Drawing.Point(228, 3);
panelDetail.Name = "panelDetail";
- panelDetail.Size = new System.Drawing.Size(444, 448);
+ panelDetail.Size = new System.Drawing.Size(444, 437);
panelDetail.TabIndex = 1;
//
// groupBoxLegend
@@ -411,36 +409,32 @@ private void InitializeComponent()
labelOrgFrameData.Size = new System.Drawing.Size(300, 44);
labelOrgFrameData.TabIndex = 7;
labelOrgFrameData.Text = "-";
- //
+ //
// tileViewSec
- //
+ //
tileViewSec.ContextMenuStrip = contextMenuStrip1;
tileViewSec.Dock = System.Windows.Forms.DockStyle.Fill;
tileViewSec.Location = new System.Drawing.Point(678, 3);
tileViewSec.Name = "tileViewSec";
- tileViewSec.Size = new System.Drawing.Size(219, 448);
+ tileViewSec.Size = new System.Drawing.Size(219, 437);
tileViewSec.TabIndex = 2;
- tileViewSec.TileSize = new System.Drawing.Size(219, 15);
- tileViewSec.TileMargin = new System.Windows.Forms.Padding(0);
- tileViewSec.TilePadding = new System.Windows.Forms.Padding(0);
- tileViewSec.TileBorderWidth = 0f;
- tileViewSec.TileHighLightOpacity = 0.0;
- tileViewSec.DrawItem += new System.EventHandler(OnDrawItemSec);
- tileViewSec.FocusSelectionChanged += new System.EventHandler(OnFocusChangedSec);
- tileViewSec.SizeChanged += new System.EventHandler(OnTileViewSizeChanged);
- tileViewSec.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(OnDoubleClickSec);
+ tileViewSec.TileHighLightOpacity = 0D;
+ tileViewSec.FocusSelectionChanged += OnFocusChangedSec;
+ tileViewSec.DrawItem += OnDrawItemSec;
+ tileViewSec.SizeChanged += OnTileViewSizeChanged;
+ tileViewSec.MouseDoubleClick += OnDoubleClickSec;
//
// contextMenuStrip1
//
contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { copyEntryToolStripMenuItem });
contextMenuStrip1.Name = "contextMenuStrip1";
- contextMenuStrip1.Size = new System.Drawing.Size(165, 26);
+ contextMenuStrip1.Size = new System.Drawing.Size(167, 26);
//
// copyEntryToolStripMenuItem
//
copyEntryToolStripMenuItem.Name = "copyEntryToolStripMenuItem";
- copyEntryToolStripMenuItem.Size = new System.Drawing.Size(164, 22);
- copyEntryToolStripMenuItem.Text = "Copy Entry 2 to 1";
+ copyEntryToolStripMenuItem.Size = new System.Drawing.Size(166, 22);
+ copyEntryToolStripMenuItem.Text = "Copy Entry to left";
copyEntryToolStripMenuItem.Click += OnClickCopySelected;
//
// buttonCopyAddedOnly
@@ -483,6 +477,17 @@ private void InitializeComponent()
checkBoxShowDiff.Text = "Show only Differences";
checkBoxShowDiff.UseVisualStyleBackColor = true;
checkBoxShowDiff.Click += OnChangeShowDiff;
+ //
+ // chkMultiSelect
+ //
+ chkMultiSelect.AutoSize = true;
+ chkMultiSelect.Location = new System.Drawing.Point(408, 38);
+ chkMultiSelect.Name = "chkMultiSelect";
+ chkMultiSelect.Size = new System.Drawing.Size(90, 19);
+ chkMultiSelect.TabIndex = 10;
+ chkMultiSelect.Text = "Multi-Select";
+ chkMultiSelect.UseVisualStyleBackColor = true;
+ chkMultiSelect.CheckedChanged += OnChangeMultiSelect;
//
// buttonLoadSecond
//
@@ -571,6 +576,7 @@ private void InitializeComponent()
private System.Windows.Forms.Button buttonBrowse;
private System.Windows.Forms.Button buttonLoadSecond;
private System.Windows.Forms.CheckBox checkBoxShowDiff;
+ private System.Windows.Forms.CheckBox chkMultiSelect;
private System.Windows.Forms.Button buttonCopySelected;
private System.Windows.Forms.Button buttonCopyAllDiff;
private System.Windows.Forms.Button buttonCopyAddedOnly;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
index 6b383c1..df3c4a7 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
@@ -27,10 +27,78 @@ private void OnLoad(object sender, EventArgs e)
{
legendSwatchDifferent.BackColor = Color.CornflowerBlue;
}
+ ConfigureTileView(tileViewOrg);
+ ConfigureTileView(tileViewSec);
PopulateOrgList();
+
+ tileViewSec.MultiSelect = true;
+ tileViewSec.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
+ contextMenuStrip1.Opening += (s, ev) =>
+ {
+ int count = tileViewSec.SelectedIndices.Count;
+ copyEntryToolStripMenuItem.Text = tileViewSec.ShowCheckBoxes && count > 1
+ ? $"Copy {count} Entries to left"
+ : "Copy Entry to left";
+ };
+
ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
}
+ // TileViewControl exposes TileSize/Margin/Padding/Border with DesignerSerializationVisibility.Hidden,
+ // so VS strips them when re-saving the .Designer.cs. Apply the intended values here so they survive.
+ private static void ConfigureTileView(TileViewControl tv)
+ {
+ tv.TileSize = new Size(tv.TileSize.Width, 20);
+ tv.TileMargin = new Padding(0);
+ tv.TilePadding = new Padding(0);
+ tv.TileBorderWidth = 0f;
+ }
+
+ private void OnChangeMultiSelect(object sender, EventArgs e)
+ {
+ tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ if (!chkMultiSelect.Checked)
+ {
+ tileViewSec.SelectedIndices.Clear();
+ }
+ }
+
+ private void OnSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ if (_syncingSelection)
+ {
+ return;
+ }
+
+ _syncingSelection = true;
+ try
+ {
+ tileViewOrg.SelectedIndices.Clear();
+ foreach (int idx in tileViewSec.SelectedIndices)
+ {
+ tileViewOrg.SelectedIndices.Add(idx);
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private List GetCopyTargets()
+ {
+ var sel = tileViewSec.SelectedIndices;
+ if (sel.Count > 0)
+ {
+ return sel.ToList();
+ }
+ if (tileViewSec.FocusIndex >= 0)
+ {
+ return new List { tileViewSec.FocusIndex };
+ }
+ return new List();
+ }
+
private void OnFilePathChangeEvent()
{
_compare.Clear();
@@ -69,7 +137,7 @@ private void OnDrawItemSec(object sender, TileViewControl.DrawTileListItemEventA
DrawListItem(e, _displayIndices[e.Index]);
}
- private void DrawListItem(DrawItemEventArgs e, int id)
+ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int id)
{
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
@@ -83,7 +151,7 @@ private void DrawListItem(DrawItemEventArgs e, int id)
Brush fontBrush = GetEntryBrush(id);
string text = $"0x{id:X4} ({id})";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(text, e.Font).Height) / 2f;
- e.Graphics.DrawString(text, e.Font, fontBrush, new PointF(4, y));
+ e.Graphics.DrawString(text, e.Font, fontBrush, new PointF(e.ContentLeft + 4, y));
}
private Brush GetEntryBrush(int id)
@@ -335,30 +403,62 @@ private bool Compare(int id)
private void OnDoubleClickSec(object sender, MouseEventArgs e)
{
+ if (tileViewSec.ShowCheckBoxes)
+ {
+ return;
+ }
OnClickCopySelected(sender, e);
}
private void OnClickCopySelected(object sender, EventArgs e)
{
- int focusIdx = tileViewSec.FocusIndex;
- if (focusIdx < 0)
+ var targets = GetCopyTargets();
+ if (targets.Count == 0)
{
return;
}
- int id = _displayIndices[focusIdx];
- CopyEntry(id);
+ Cursor.Current = Cursors.WaitCursor;
+ int lastId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
+ {
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
- if (checkBoxShowDiff.Checked)
+ int id = _displayIndices[focusIdx];
+ CopyEntry(id);
+ lastId = id;
+ changed = true;
+ }
+
+ if (checkBoxShowDiff.Checked && changed)
{
- _displayIndices.RemoveAt(focusIdx);
+ foreach (int idx in targets.OrderByDescending(x => x))
+ {
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
+ }
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = _displayIndices.Count;
}
+ else
+ {
+ tileViewSec.SelectedIndices.Clear();
+ }
tileViewOrg.Invalidate();
tileViewSec.Invalidate();
- UpdateDetailPanel(id);
+ if (lastId >= 0)
+ {
+ UpdateDetailPanel(lastId);
+ }
+ Cursor.Current = Cursors.Default;
}
private void OnClickCopyAllDiff(object sender, EventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.Designer.cs
index bc598bc..e4cafd6 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.Designer.cs
@@ -94,6 +94,7 @@ private void InitializeComponent()
dataGridView1.AllowUserToAddRows = false;
dataGridView1.AllowUserToDeleteRows = false;
dataGridView1.AllowUserToOrderColumns = true;
+ dataGridView1.AllowUserToResizeRows = false;
dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.Designer.cs
index a0e93b7..2fe7c7b 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.Designer.cs
@@ -39,267 +39,264 @@ protected override void Dispose(bool disposing)
///
private void InitializeComponent()
{
- this.components = new System.ComponentModel.Container();
- this.tileView1 = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
- this.tileView2 = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
- this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components);
- this.extractAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.tiffToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.bmpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.jpgToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.pngToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.copyGump2To1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
- this.pictureBox1 = new System.Windows.Forms.PictureBox();
- this.pictureBox2 = new System.Windows.Forms.PictureBox();
- this.splitContainer1 = new System.Windows.Forms.SplitContainer();
- this.checkBox1 = new System.Windows.Forms.CheckBox();
- this.button2 = new System.Windows.Forms.Button();
- this.button1 = new System.Windows.Forms.Button();
- this.textBoxSecondDir = new System.Windows.Forms.TextBox();
- this.comboBoxFileMode = new System.Windows.Forms.ComboBox();
- this.contextMenuStrip1.SuspendLayout();
- this.tableLayoutPanel1.SuspendLayout();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBox2)).BeginInit();
- ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
- this.splitContainer1.Panel1.SuspendLayout();
- this.splitContainer1.Panel2.SuspendLayout();
- this.splitContainer1.SuspendLayout();
- this.SuspendLayout();
- //
+ components = new System.ComponentModel.Container();
+ tileView1 = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
+ tileView2 = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
+ contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(components);
+ extractAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ tiffToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ bmpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ jpgToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ pngToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ copyGump2To1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
+ pictureBox1 = new System.Windows.Forms.PictureBox();
+ pictureBox2 = new System.Windows.Forms.PictureBox();
+ splitContainer1 = new System.Windows.Forms.SplitContainer();
+ checkBox1 = new System.Windows.Forms.CheckBox();
+ chkMultiSelect = new System.Windows.Forms.CheckBox();
+ button2 = new System.Windows.Forms.Button();
+ button1 = new System.Windows.Forms.Button();
+ textBoxSecondDir = new System.Windows.Forms.TextBox();
+ comboBoxFileMode = new System.Windows.Forms.ComboBox();
+ contextMenuStrip1.SuspendLayout();
+ tableLayoutPanel1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)pictureBox2).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
+ splitContainer1.Panel1.SuspendLayout();
+ splitContainer1.Panel2.SuspendLayout();
+ splitContainer1.SuspendLayout();
+ SuspendLayout();
+ //
// tileView1
- //
- this.tileView1.Dock = System.Windows.Forms.DockStyle.Left;
- this.tileView1.Location = new System.Drawing.Point(0, 0);
- this.tileView1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tileView1.Name = "tileView1";
- this.tileView1.Size = new System.Drawing.Size(174, 320);
- this.tileView1.TabIndex = 0;
- this.tileView1.TileSize = new System.Drawing.Size(174, 60);
- this.tileView1.TileMargin = new System.Windows.Forms.Padding(0);
- this.tileView1.TilePadding = new System.Windows.Forms.Padding(0);
- this.tileView1.TileBorderWidth = 0f;
- this.tileView1.TileHighLightOpacity = 0.0;
- this.tileView1.DrawItem += new System.EventHandler(this.OnDrawItem1);
- this.tileView1.FocusSelectionChanged += new System.EventHandler(this.OnFocusChanged1);
- this.tileView1.SizeChanged += new System.EventHandler(this.OnTileViewSizeChanged);
- //
+ //
+ tileView1.Dock = System.Windows.Forms.DockStyle.Left;
+ tileView1.Location = new System.Drawing.Point(0, 0);
+ tileView1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tileView1.Name = "tileView1";
+ tileView1.Size = new System.Drawing.Size(174, 309);
+ tileView1.TabIndex = 0;
+ tileView1.TileHighLightOpacity = 0D;
+ tileView1.FocusSelectionChanged += OnFocusChanged1;
+ tileView1.DrawItem += OnDrawItem1;
+ tileView1.SizeChanged += OnTileViewSizeChanged;
+ //
// tileView2
- //
- this.tileView2.ContextMenuStrip = this.contextMenuStrip1;
- this.tileView2.Dock = System.Windows.Forms.DockStyle.Right;
- this.tileView2.Location = new System.Drawing.Point(556, 0);
- this.tileView2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tileView2.Name = "tileView2";
- this.tileView2.Size = new System.Drawing.Size(174, 320);
- this.tileView2.TabIndex = 1;
- this.tileView2.TileSize = new System.Drawing.Size(174, 60);
- this.tileView2.TileMargin = new System.Windows.Forms.Padding(0);
- this.tileView2.TilePadding = new System.Windows.Forms.Padding(0);
- this.tileView2.TileBorderWidth = 0f;
- this.tileView2.TileHighLightOpacity = 0.0;
- this.tileView2.DrawItem += new System.EventHandler(this.OnDrawItem2);
- this.tileView2.FocusSelectionChanged += new System.EventHandler(this.OnFocusChanged2);
- this.tileView2.SizeChanged += new System.EventHandler(this.OnTileViewSizeChanged);
+ //
+ tileView2.ContextMenuStrip = contextMenuStrip1;
+ tileView2.Dock = System.Windows.Forms.DockStyle.Right;
+ tileView2.Location = new System.Drawing.Point(556, 0);
+ tileView2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tileView2.Name = "tileView2";
+ tileView2.Size = new System.Drawing.Size(174, 309);
+ tileView2.TabIndex = 1;
+ tileView2.TileHighLightOpacity = 0D;
+ tileView2.FocusSelectionChanged += OnFocusChanged2;
+ tileView2.DrawItem += OnDrawItem2;
+ tileView2.SizeChanged += OnTileViewSizeChanged;
//
// contextMenuStrip1
//
- this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.extractAsToolStripMenuItem,
- this.copyGump2To1ToolStripMenuItem});
- this.contextMenuStrip1.Name = "contextMenuStrip1";
- this.contextMenuStrip1.Size = new System.Drawing.Size(171, 48);
+ contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { extractAsToolStripMenuItem, copyGump2To1ToolStripMenuItem });
+ contextMenuStrip1.Name = "contextMenuStrip1";
+ contextMenuStrip1.Size = new System.Drawing.Size(173, 48);
//
// extractAsToolStripMenuItem
//
- this.extractAsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.tiffToolStripMenuItem,
- this.bmpToolStripMenuItem,
- this.jpgToolStripMenuItem,
- this.pngToolStripMenuItem});
- this.extractAsToolStripMenuItem.Name = "extractAsToolStripMenuItem";
- this.extractAsToolStripMenuItem.Size = new System.Drawing.Size(170, 22);
- this.extractAsToolStripMenuItem.Text = "Export Image..";
- //
+ extractAsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { tiffToolStripMenuItem, bmpToolStripMenuItem, jpgToolStripMenuItem, pngToolStripMenuItem });
+ extractAsToolStripMenuItem.Name = "extractAsToolStripMenuItem";
+ extractAsToolStripMenuItem.Size = new System.Drawing.Size(172, 22);
+ extractAsToolStripMenuItem.Text = "Export Image..";
+ //
// tiffToolStripMenuItem
- //
- this.tiffToolStripMenuItem.Name = "tiffToolStripMenuItem";
- this.tiffToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.tiffToolStripMenuItem.Text = "As Bmp";
- this.tiffToolStripMenuItem.Click += new System.EventHandler(this.Export_Bmp);
- //
+ //
+ tiffToolStripMenuItem.Name = "tiffToolStripMenuItem";
+ tiffToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ tiffToolStripMenuItem.Text = "As Bmp";
+ tiffToolStripMenuItem.Click += Export_Bmp;
+ //
// bmpToolStripMenuItem
- //
- this.bmpToolStripMenuItem.Name = "bmpToolStripMenuItem";
- this.bmpToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.bmpToolStripMenuItem.Text = "As Tiff";
- this.bmpToolStripMenuItem.Click += new System.EventHandler(this.Export_Tiff);
- //
+ //
+ bmpToolStripMenuItem.Name = "bmpToolStripMenuItem";
+ bmpToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ bmpToolStripMenuItem.Text = "As Tiff";
+ bmpToolStripMenuItem.Click += Export_Tiff;
+ //
// jpgToolStripMenuItem
- //
- this.jpgToolStripMenuItem.Name = "jpgToolStripMenuItem";
- this.jpgToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.jpgToolStripMenuItem.Text = "As Jpg";
- this.jpgToolStripMenuItem.Click += new System.EventHandler(this.Export_Jpg);
- //
+ //
+ jpgToolStripMenuItem.Name = "jpgToolStripMenuItem";
+ jpgToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ jpgToolStripMenuItem.Text = "As Jpg";
+ jpgToolStripMenuItem.Click += Export_Jpg;
+ //
// pngToolStripMenuItem
- //
- this.pngToolStripMenuItem.Name = "pngToolStripMenuItem";
- this.pngToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.pngToolStripMenuItem.Text = "As Png";
- this.pngToolStripMenuItem.Click += new System.EventHandler(this.Export_Png);
- //
+ //
+ pngToolStripMenuItem.Name = "pngToolStripMenuItem";
+ pngToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ pngToolStripMenuItem.Text = "As Png";
+ pngToolStripMenuItem.Click += Export_Png;
+ //
// copyGump2To1ToolStripMenuItem
//
- this.copyGump2To1ToolStripMenuItem.Name = "copyGump2To1ToolStripMenuItem";
- this.copyGump2To1ToolStripMenuItem.Size = new System.Drawing.Size(170, 22);
- this.copyGump2To1ToolStripMenuItem.Text = "Copy Gump 2 to 1";
- this.copyGump2To1ToolStripMenuItem.Click += new System.EventHandler(this.OnClickCopy);
+ copyGump2To1ToolStripMenuItem.Name = "copyGump2To1ToolStripMenuItem";
+ copyGump2To1ToolStripMenuItem.Size = new System.Drawing.Size(172, 22);
+ copyGump2To1ToolStripMenuItem.Text = "Copy Gump to left";
+ copyGump2To1ToolStripMenuItem.Click += OnClickCopy;
//
// tableLayoutPanel1
//
- this.tableLayoutPanel1.ColumnCount = 1;
- this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.tableLayoutPanel1.Controls.Add(this.pictureBox1, 0, 0);
- this.tableLayoutPanel1.Controls.Add(this.pictureBox2, 0, 1);
- this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tableLayoutPanel1.Location = new System.Drawing.Point(174, 0);
- this.tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tableLayoutPanel1.Name = "tableLayoutPanel1";
- this.tableLayoutPanel1.RowCount = 2;
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.tableLayoutPanel1.Size = new System.Drawing.Size(382, 320);
- this.tableLayoutPanel1.TabIndex = 2;
+ tableLayoutPanel1.ColumnCount = 1;
+ tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ tableLayoutPanel1.Controls.Add(pictureBox1, 0, 0);
+ tableLayoutPanel1.Controls.Add(pictureBox2, 0, 1);
+ tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
+ tableLayoutPanel1.Location = new System.Drawing.Point(174, 0);
+ tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tableLayoutPanel1.Name = "tableLayoutPanel1";
+ tableLayoutPanel1.RowCount = 2;
+ tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ tableLayoutPanel1.Size = new System.Drawing.Size(382, 309);
+ tableLayoutPanel1.TabIndex = 2;
//
// pictureBox1
//
- this.pictureBox1.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
- this.pictureBox1.Dock = System.Windows.Forms.DockStyle.Fill;
- this.pictureBox1.Location = new System.Drawing.Point(4, 3);
- this.pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.pictureBox1.Name = "pictureBox1";
- this.pictureBox1.Size = new System.Drawing.Size(374, 154);
- this.pictureBox1.TabIndex = 0;
- this.pictureBox1.TabStop = false;
+ pictureBox1.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
+ pictureBox1.Dock = System.Windows.Forms.DockStyle.Fill;
+ pictureBox1.Location = new System.Drawing.Point(4, 3);
+ pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ pictureBox1.Name = "pictureBox1";
+ pictureBox1.Size = new System.Drawing.Size(374, 148);
+ pictureBox1.TabIndex = 0;
+ pictureBox1.TabStop = false;
//
// pictureBox2
//
- this.pictureBox2.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
- this.pictureBox2.Dock = System.Windows.Forms.DockStyle.Fill;
- this.pictureBox2.Location = new System.Drawing.Point(4, 163);
- this.pictureBox2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.pictureBox2.Name = "pictureBox2";
- this.pictureBox2.Size = new System.Drawing.Size(374, 154);
- this.pictureBox2.TabIndex = 1;
- this.pictureBox2.TabStop = false;
+ pictureBox2.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
+ pictureBox2.Dock = System.Windows.Forms.DockStyle.Fill;
+ pictureBox2.Location = new System.Drawing.Point(4, 157);
+ pictureBox2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ pictureBox2.Name = "pictureBox2";
+ pictureBox2.Size = new System.Drawing.Size(374, 149);
+ pictureBox2.TabIndex = 1;
+ pictureBox2.TabStop = false;
//
// splitContainer1
//
- this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
- this.splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
- this.splitContainer1.Location = new System.Drawing.Point(0, 0);
- this.splitContainer1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.splitContainer1.Name = "splitContainer1";
- this.splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
+ splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
+ splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
+ splitContainer1.Location = new System.Drawing.Point(0, 0);
+ splitContainer1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ splitContainer1.Name = "splitContainer1";
+ splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
//
// splitContainer1.Panel1
//
- this.splitContainer1.Panel1.Controls.Add(this.tableLayoutPanel1);
- this.splitContainer1.Panel1.Controls.Add(this.tileView2);
- this.splitContainer1.Panel1.Controls.Add(this.tileView1);
+ splitContainer1.Panel1.Controls.Add(tableLayoutPanel1);
+ splitContainer1.Panel1.Controls.Add(tileView2);
+ splitContainer1.Panel1.Controls.Add(tileView1);
//
// splitContainer1.Panel2
//
- this.splitContainer1.Panel2.Controls.Add(this.checkBox1);
- this.splitContainer1.Panel2.Controls.Add(this.button2);
- this.splitContainer1.Panel2.Controls.Add(this.button1);
- this.splitContainer1.Panel2.Controls.Add(this.textBoxSecondDir);
- this.splitContainer1.Panel2.Controls.Add(this.comboBoxFileMode);
- this.splitContainer1.Size = new System.Drawing.Size(730, 378);
- this.splitContainer1.SplitterDistance = 320;
- this.splitContainer1.SplitterWidth = 5;
- this.splitContainer1.TabIndex = 3;
+ splitContainer1.Panel2.Controls.Add(checkBox1);
+ splitContainer1.Panel2.Controls.Add(chkMultiSelect);
+ splitContainer1.Panel2.Controls.Add(button2);
+ splitContainer1.Panel2.Controls.Add(button1);
+ splitContainer1.Panel2.Controls.Add(textBoxSecondDir);
+ splitContainer1.Panel2.Controls.Add(comboBoxFileMode);
+ splitContainer1.Size = new System.Drawing.Size(730, 378);
+ splitContainer1.SplitterDistance = 309;
+ splitContainer1.SplitterWidth = 5;
+ splitContainer1.TabIndex = 3;
//
// checkBox1
//
- this.checkBox1.AutoSize = true;
- this.checkBox1.Location = new System.Drawing.Point(493, 18);
- this.checkBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.checkBox1.Name = "checkBox1";
- this.checkBox1.Size = new System.Drawing.Size(143, 19);
- this.checkBox1.TabIndex = 3;
- this.checkBox1.Text = "Show only Differences";
- this.checkBox1.UseVisualStyleBackColor = true;
- this.checkBox1.Click += new System.EventHandler(this.ShowDiff_OnClick);
+ checkBox1.AutoSize = true;
+ checkBox1.Location = new System.Drawing.Point(493, 18);
+ checkBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ checkBox1.Name = "checkBox1";
+ checkBox1.Size = new System.Drawing.Size(143, 19);
+ checkBox1.TabIndex = 3;
+ checkBox1.Text = "Show only Differences";
+ checkBox1.UseVisualStyleBackColor = true;
+ checkBox1.Click += ShowDiff_OnClick;
+ //
+ // chkMultiSelect
+ //
+ chkMultiSelect.AutoSize = true;
+ chkMultiSelect.Location = new System.Drawing.Point(493, 41);
+ chkMultiSelect.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ chkMultiSelect.Name = "chkMultiSelect";
+ chkMultiSelect.Size = new System.Drawing.Size(90, 19);
+ chkMultiSelect.TabIndex = 10;
+ chkMultiSelect.Text = "Multi-Select";
+ chkMultiSelect.UseVisualStyleBackColor = true;
+ chkMultiSelect.CheckedChanged += OnChangeMultiSelect;
//
// button2
//
- this.button2.Location = new System.Drawing.Point(399, 14);
- this.button2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.button2.Name = "button2";
- this.button2.Size = new System.Drawing.Size(88, 27);
- this.button2.TabIndex = 2;
- this.button2.Text = "Load";
- this.button2.UseVisualStyleBackColor = true;
- this.button2.Click += new System.EventHandler(this.Load_Click);
+ button2.Location = new System.Drawing.Point(399, 14);
+ button2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ button2.Name = "button2";
+ button2.Size = new System.Drawing.Size(88, 27);
+ button2.TabIndex = 2;
+ button2.Text = "Load";
+ button2.UseVisualStyleBackColor = true;
+ button2.Click += Load_Click;
//
// button1
//
- this.button1.AutoSize = true;
- this.button1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
- this.button1.Location = new System.Drawing.Point(362, 14);
- this.button1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.button1.Name = "button1";
- this.button1.Size = new System.Drawing.Size(26, 25);
- this.button1.TabIndex = 1;
- this.button1.Text = "...";
- this.button1.UseVisualStyleBackColor = true;
- this.button1.Click += new System.EventHandler(this.Browse_OnClick);
+ button1.AutoSize = true;
+ button1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
+ button1.Location = new System.Drawing.Point(362, 14);
+ button1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ button1.Name = "button1";
+ button1.Size = new System.Drawing.Size(26, 25);
+ button1.TabIndex = 1;
+ button1.Text = "...";
+ button1.UseVisualStyleBackColor = true;
+ button1.Click += Browse_OnClick;
//
// textBoxSecondDir
//
- this.textBoxSecondDir.Location = new System.Drawing.Point(175, 16);
- this.textBoxSecondDir.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.textBoxSecondDir.Name = "textBoxSecondDir";
- this.textBoxSecondDir.Size = new System.Drawing.Size(179, 23);
- this.textBoxSecondDir.TabIndex = 0;
- //
+ textBoxSecondDir.Location = new System.Drawing.Point(175, 16);
+ textBoxSecondDir.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ textBoxSecondDir.Name = "textBoxSecondDir";
+ textBoxSecondDir.Size = new System.Drawing.Size(179, 23);
+ textBoxSecondDir.TabIndex = 0;
+ //
// comboBoxFileMode
- //
- this.comboBoxFileMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
- this.comboBoxFileMode.FormattingEnabled = true;
- this.comboBoxFileMode.Items.AddRange(new object[] {
- "Auto",
- "MUL",
- "UOP"});
- this.comboBoxFileMode.Location = new System.Drawing.Point(99, 16);
- this.comboBoxFileMode.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.comboBoxFileMode.Name = "comboBoxFileMode";
- this.comboBoxFileMode.Size = new System.Drawing.Size(70, 23);
- this.comboBoxFileMode.TabIndex = 4;
- //
+ //
+ comboBoxFileMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ comboBoxFileMode.FormattingEnabled = true;
+ comboBoxFileMode.Items.AddRange(new object[] { "Auto", "MUL", "UOP" });
+ comboBoxFileMode.Location = new System.Drawing.Point(99, 16);
+ comboBoxFileMode.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ comboBoxFileMode.Name = "comboBoxFileMode";
+ comboBoxFileMode.Size = new System.Drawing.Size(70, 23);
+ comboBoxFileMode.TabIndex = 4;
+ //
// CompareGumpControl
//
- this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.Controls.Add(this.splitContainer1);
- this.DoubleBuffered = true;
- this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.Name = "CompareGumpControl";
- this.Size = new System.Drawing.Size(730, 378);
- this.Load += new System.EventHandler(this.OnLoad);
- this.contextMenuStrip1.ResumeLayout(false);
- this.tableLayoutPanel1.ResumeLayout(false);
- ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBox2)).EndInit();
- this.splitContainer1.Panel1.ResumeLayout(false);
- this.splitContainer1.Panel2.ResumeLayout(false);
- this.splitContainer1.Panel2.PerformLayout();
- ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
- this.splitContainer1.ResumeLayout(false);
- this.ResumeLayout(false);
+ AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ Controls.Add(splitContainer1);
+ DoubleBuffered = true;
+ Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ Name = "CompareGumpControl";
+ Size = new System.Drawing.Size(730, 378);
+ Load += OnLoad;
+ contextMenuStrip1.ResumeLayout(false);
+ tableLayoutPanel1.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit();
+ ((System.ComponentModel.ISupportInitialize)pictureBox2).EndInit();
+ splitContainer1.Panel1.ResumeLayout(false);
+ splitContainer1.Panel2.ResumeLayout(false);
+ splitContainer1.Panel2.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
+ splitContainer1.ResumeLayout(false);
+ ResumeLayout(false);
}
@@ -309,6 +306,7 @@ private void InitializeComponent()
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.CheckBox checkBox1;
+ private System.Windows.Forms.CheckBox chkMultiSelect;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
private System.Windows.Forms.ToolStripMenuItem copyGump2To1ToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem extractAsToolStripMenuItem;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
index 0cec5b5..ea266dc 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
@@ -14,6 +14,7 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using System.Linq;
using System.Security.Cryptography;
using System.Windows.Forms;
using Ultima;
@@ -42,6 +43,9 @@ private void OnLoad(object sender, EventArgs e)
Cursor.Current = Cursors.WaitCursor;
Options.LoadedUltimaClass["Gumps"] = true;
+ ConfigureTileView(tileView1);
+ ConfigureTileView(tileView2);
+
_displayIndices.Clear();
for (int i = 0; i < 0x10000; i++)
{
@@ -63,6 +67,15 @@ private void OnLoad(object sender, EventArgs e)
if (!_loaded)
{
+ tileView2.MultiSelect = true;
+ tileView2.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
+ contextMenuStrip1.Opening += (s, ev) =>
+ {
+ int count = tileView2.SelectedIndices.Count;
+ copyGump2To1ToolStripMenuItem.Text = tileView2.ShowCheckBoxes && count > 1
+ ? $"Copy {count} Gumps to left"
+ : "Copy Gump to left";
+ };
ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
}
@@ -70,6 +83,61 @@ private void OnLoad(object sender, EventArgs e)
Cursor.Current = Cursors.Default;
}
+ // TileViewControl exposes TileSize/Margin/Padding/Border with DesignerSerializationVisibility.Hidden,
+ // so VS strips them when re-saving the .Designer.cs. Apply the intended values here so they survive.
+ private static void ConfigureTileView(TileViewControl tv)
+ {
+ tv.TileSize = new Size(tv.TileSize.Width, 60);
+ tv.TileMargin = new Padding(0);
+ tv.TilePadding = new Padding(0);
+ tv.TileBorderWidth = 0f;
+ }
+
+ private void OnChangeMultiSelect(object sender, EventArgs e)
+ {
+ tileView2.ShowCheckBoxes = chkMultiSelect.Checked;
+ if (!chkMultiSelect.Checked)
+ {
+ tileView2.SelectedIndices.Clear();
+ }
+ }
+
+ private void OnSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ if (_syncingSelection)
+ {
+ return;
+ }
+
+ _syncingSelection = true;
+ try
+ {
+ tileView1.SelectedIndices.Clear();
+ foreach (int idx in tileView2.SelectedIndices)
+ {
+ tileView1.SelectedIndices.Add(idx);
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private List GetCopyTargets()
+ {
+ var sel = tileView2.SelectedIndices;
+ if (sel.Count > 0)
+ {
+ return sel.ToList();
+ }
+ if (tileView2.FocusIndex >= 0)
+ {
+ return new List { tileView2.FocusIndex };
+ }
+ return new List();
+ }
+
private void OnFilePathChangeEvent()
{
Reload();
@@ -103,7 +171,7 @@ private void OnDrawItem2(object sender, TileViewControl.DrawTileListItemEventArg
DrawGumpItem(e, _displayIndices[e.Index], isSecondary: true);
}
- private void DrawGumpItem(DrawItemEventArgs e, int i, bool isSecondary)
+ private void DrawGumpItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
@@ -129,7 +197,7 @@ private void DrawGumpItem(DrawItemEventArgs e, int i, bool isSecondary)
int width = bmp.Width > 80 ? 80 : bmp.Width;
int height = bmp.Height > 54 ? 54 : bmp.Height;
- e.Graphics.DrawImage(bmp, new Rectangle(e.Bounds.X + 3, e.Bounds.Y + 3, width, height));
+ e.Graphics.DrawImage(bmp, new Rectangle(e.Bounds.X + e.ContentLeft + 3, e.Bounds.Y + 3, width, height));
}
else
{
@@ -143,7 +211,7 @@ private void DrawGumpItem(DrawItemEventArgs e, int i, bool isSecondary)
string label = $"0x{i:X}";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, Font).Height) / 2f;
- e.Graphics.DrawString(label, Font, fontBrush, new PointF(85, y));
+ e.Graphics.DrawString(label, Font, fontBrush, new PointF(e.ContentLeft + 85, y));
}
private void OnFocusChanged1(object sender, TileViewControl.ListViewFocusedItemSelectionChangedEventArgs e)
@@ -407,28 +475,66 @@ private void Export_Png(object sender, EventArgs e)
private void OnClickCopy(object sender, EventArgs e)
{
- int focusIdx = tileView2.FocusIndex;
- if (focusIdx < 0)
+ var targets = GetCopyTargets();
+ if (targets.Count == 0)
{
return;
}
- int i = _displayIndices[focusIdx];
- if (!SecondGump.IsValidIndex(i))
+ Cursor.Current = Cursors.WaitCursor;
+ int lastCopiedId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- return;
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
+
+ int i = _displayIndices[focusIdx];
+ if (!SecondGump.IsValidIndex(i))
+ {
+ continue;
+ }
+
+ Bitmap copy = new Bitmap(SecondGump.GetGump(i));
+ Gumps.ReplaceGump(i, copy);
+ ControlEvents.FireGumpChangeEvent(this, i);
+ _compare[i] = true;
+ lastCopiedId = i;
+ changed = true;
}
- Bitmap copy = new Bitmap(SecondGump.GetGump(i));
- Gumps.ReplaceGump(i, copy);
- Options.ChangedUltimaClass["Gumps"] = true;
- ControlEvents.FireGumpChangeEvent(this, i);
- _compare[i] = true;
+ if (changed)
+ {
+ Options.ChangedUltimaClass["Gumps"] = true;
+ }
+
+ if (checkBox1.Checked && changed)
+ {
+ foreach (int idx in targets.OrderByDescending(x => x))
+ {
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
+ }
+ tileView1.VirtualListSize = _displayIndices.Count;
+ tileView2.VirtualListSize = _displayIndices.Count;
+ }
+ else
+ {
+ tileView2.SelectedIndices.Clear();
+ }
tileView1.Invalidate();
tileView2.Invalidate();
-
- UpdatePictureBox(pictureBox1, i, isSecondary: false);
+ if (lastCopiedId >= 0)
+ {
+ UpdatePictureBox(pictureBox1, lastCopiedId, isSecondary: false);
+ }
+ Cursor.Current = Cursors.Default;
}
}
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.resx b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.resx
index 16e4841..75b37ca 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.resx
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.resx
@@ -1,4 +1,64 @@
+
+
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.Designer.cs
index 698ad27..2b61fc3 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.Designer.cs
@@ -49,6 +49,7 @@ private void InitializeComponent()
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.button2 = new System.Windows.Forms.Button();
this.button1 = new System.Windows.Forms.Button();
+ this.chkMultiSelect = new System.Windows.Forms.CheckBox();
this.textBox1 = new System.Windows.Forms.TextBox();
this.toolTip1 = new System.Windows.Forms.ToolTip(this.components);
this.tableLayoutPanel1.SuspendLayout();
@@ -103,7 +104,7 @@ private void InitializeComponent()
//
this.applyHue1ToHue2ToolStripMenuItem.Name = "applyHue1ToHue2ToolStripMenuItem";
this.applyHue1ToHue2ToolStripMenuItem.Size = new System.Drawing.Size(187, 22);
- this.applyHue1ToHue2ToolStripMenuItem.Text = "Apply Hue 2 to Hue 1";
+ this.applyHue1ToHue2ToolStripMenuItem.Text = "Apply Hue to left";
this.applyHue1ToHue2ToolStripMenuItem.Click += new System.EventHandler(this.OnClickApplyHue1to2);
//
// pictureBox2
@@ -146,6 +147,7 @@ private void InitializeComponent()
//
this.splitContainer1.Panel2.Controls.Add(this.button2);
this.splitContainer1.Panel2.Controls.Add(this.button1);
+ this.splitContainer1.Panel2.Controls.Add(this.chkMultiSelect);
this.splitContainer1.Panel2.Controls.Add(this.textBox1);
this.splitContainer1.Size = new System.Drawing.Size(733, 380);
this.splitContainer1.SplitterDistance = 320;
@@ -179,6 +181,17 @@ private void InitializeComponent()
this.button1.Text = "Load";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.OnClickLoad);
+ //
+ // chkMultiSelect
+ //
+ this.chkMultiSelect.AutoSize = true;
+ this.chkMultiSelect.Location = new System.Drawing.Point(569, 15);
+ this.chkMultiSelect.Name = "chkMultiSelect";
+ this.chkMultiSelect.Size = new System.Drawing.Size(90, 19);
+ this.chkMultiSelect.TabIndex = 10;
+ this.chkMultiSelect.Text = "Multi-Select";
+ this.chkMultiSelect.UseVisualStyleBackColor = true;
+ this.chkMultiSelect.CheckedChanged += new System.EventHandler(this.OnChangeMultiSelect);
//
// textBox1
//
@@ -220,6 +233,7 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripMenuItem applyHue1ToHue2ToolStripMenuItem;
private System.Windows.Forms.Button button1;
+ private System.Windows.Forms.CheckBox chkMultiSelect;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
private System.Windows.Forms.PictureBox pictureBox1;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
index e77dcf0..1df2c80 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
@@ -43,6 +43,8 @@ public CompareHuesControl()
private int _row;
private bool _hue2Loaded;
private readonly Dictionary _compare = new Dictionary();
+ private readonly HashSet _multiSelected = new HashSet();
+ private bool _multiSelectEnabled;
private bool _loaded;
private void OnLoad(object sender, EventArgs e)
@@ -57,6 +59,13 @@ private void OnLoad(object sender, EventArgs e)
_bmp2 = new Bitmap(pictureBox2.Width, pictureBox2.Height);
_loaded = true;
_row = pictureBox1.Height / _itemHeight;
+ contextMenuStrip1.Opening += (s, ev) =>
+ {
+ int count = _multiSelected.Count;
+ applyHue1ToHue2ToolStripMenuItem.Text = _multiSelectEnabled && count > 1
+ ? $"Apply {count} Hues to left"
+ : "Apply Hue to left";
+ };
PaintBox1();
}
@@ -81,7 +90,7 @@ private void PaintBox1()
}
Rectangle rect = new Rectangle(0, y * _itemHeight, 200, _itemHeight);
- if (index == _selected)
+ if (index == _selected || _multiSelected.Contains(index))
{
g.FillRectangle(SystemBrushes.Highlight, rect);
}
@@ -113,6 +122,9 @@ private void PaintBox1()
pictureBox1.Update();
}
+ private const int CheckBoxColumnWidth = 22;
+ private const int CheckBoxGlyphSize = 14;
+
private void PaintBox2()
{
using (Graphics g = Graphics.FromImage(_bmp2))
@@ -128,7 +140,7 @@ private void PaintBox2()
}
Rectangle rect = new Rectangle(0, y * _itemHeight, 200, _itemHeight);
- if (index == _selected)
+ if (index == _selected || _multiSelected.Contains(index))
{
g.FillRectangle(SystemBrushes.Highlight, rect);
}
@@ -141,10 +153,23 @@ private void PaintBox2()
g.FillRectangle(SystemBrushes.Window, rect);
}
+ int textStart = 3;
+ if (_multiSelectEnabled)
+ {
+ Rectangle cb = new Rectangle(
+ 4,
+ y * _itemHeight + (_itemHeight - CheckBoxGlyphSize) / 2,
+ CheckBoxGlyphSize,
+ CheckBoxGlyphSize);
+ ButtonState state = _multiSelected.Contains(index) ? ButtonState.Checked : ButtonState.Normal;
+ ControlPaint.DrawCheckBox(g, cb, state);
+ textStart = CheckBoxColumnWidth;
+ }
+
float size = (float)(pictureBox2.Width - 200) / 32;
Hue hue = SecondHue.List[index];
- Rectangle stringRect = new Rectangle(3, y * _itemHeight, pictureBox2.Width, _itemHeight);
- g.DrawString($"{hue.Index + 1,-5} {$"(0x{hue.Index + 1:X})",-7} {hue.Name}", Font, Brushes.Black, stringRect);
+ Rectangle stringRect = new Rectangle(textStart, y * _itemHeight, pictureBox2.Width, _itemHeight);
+ g.DrawString($"{hue.Index + 1,-5} {$"(0x{hue.Index + 1:X})",-7} {hue.Name}", Font, SystemBrushes.ControlText, stringRect);
for (int i = 0; i < hue.Colors.Length; i++)
{
@@ -242,6 +267,20 @@ private void OnClickLoad(object sender, EventArgs e)
PaintBox2();
}
+ private void OnChangeMultiSelect(object sender, EventArgs e)
+ {
+ _multiSelectEnabled = chkMultiSelect.Checked;
+ if (!_multiSelectEnabled)
+ {
+ _multiSelected.Clear();
+ }
+ PaintBox1();
+ if (_hue2Loaded)
+ {
+ PaintBox2();
+ }
+ }
+
private void OnMouseClick1(object sender, MouseEventArgs e)
{
pictureBox1.Focus();
@@ -252,6 +291,7 @@ private void OnMouseClick1(object sender, MouseEventArgs e)
return;
}
+ _multiSelected.Clear();
_selected = index;
PaintBox1();
if (_hue2Loaded)
@@ -270,7 +310,26 @@ private void OnMouseClick2(object sender, MouseEventArgs e)
return;
}
- _selected = index;
+ if (_multiSelectEnabled)
+ {
+ bool inCheckBox = e.X < CheckBoxColumnWidth;
+ if (inCheckBox || (Control.ModifierKeys & Keys.Control) == Keys.Control)
+ {
+ if (!_multiSelected.Remove(index))
+ {
+ _multiSelected.Add(index);
+ }
+ }
+ else
+ {
+ _selected = index;
+ }
+ }
+ else
+ {
+ _selected = index;
+ }
+
PaintBox1();
if (_hue2Loaded)
{
@@ -323,17 +382,41 @@ private void OnClickApplyHue1to2(object sender, EventArgs e)
return;
}
- Hue org = Hues.List[_selected];
- Hue sec = SecondHue.List[_selected];
- sec.Colors.CopyTo(org.Colors, 0);
- org.Name = sec.Name;
- org.TableStart = org.Colors[0];
- org.TableEnd = (ushort)(org.Colors[org.Colors.Length - 1] + 1057);
- _compare[_selected] = true;
- PaintBox1();
- PaintBox2();
- Options.ChangedUltimaClass["Hues"] = true;
- ControlEvents.FireHueChangeEvent();
+ IEnumerable targets = _multiSelected.Count > 0
+ ? (IEnumerable)_multiSelected
+ : new[] { _selected };
+
+ bool changed = false;
+ foreach (int index in targets)
+ {
+ if (index < 0 || index >= Hues.List.Length || index >= SecondHue.List.Length)
+ {
+ continue;
+ }
+
+ Hue org = Hues.List[index];
+ Hue sec = SecondHue.List[index];
+ if (org == null || sec == null)
+ {
+ continue;
+ }
+
+ sec.Colors.CopyTo(org.Colors, 0);
+ org.Name = sec.Name;
+ org.TableStart = org.Colors[0];
+ org.TableEnd = (ushort)(org.Colors[org.Colors.Length - 1] + 1057);
+ _compare[index] = true;
+ changed = true;
+ }
+
+ if (changed)
+ {
+ _multiSelected.Clear();
+ PaintBox1();
+ PaintBox2();
+ Options.ChangedUltimaClass["Hues"] = true;
+ ControlEvents.FireHueChangeEvent();
+ }
}
private void BrowseOnClick(object sender, EventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.Designer.cs
index 3f44ccd..70c0647 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.Designer.cs
@@ -39,305 +39,302 @@ protected override void Dispose(bool disposing)
///
private void InitializeComponent()
{
- this.components = new System.ComponentModel.Container();
- this.tileViewOrg = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
- this.tileViewSec = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
- this.btnCopyAllDiff = new System.Windows.Forms.Button();
- this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components);
- this.extractAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.tiffToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.bmpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.jpgToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.pngToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.copyItem2To1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.pictureBoxOrg = new System.Windows.Forms.PictureBox();
- this.pictureBoxSec = new System.Windows.Forms.PictureBox();
- this.textBoxSecondDir = new System.Windows.Forms.TextBox();
- this.button1 = new System.Windows.Forms.Button();
- this.checkBox1 = new System.Windows.Forms.CheckBox();
- this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
- this.tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel();
- this.splitContainer1 = new System.Windows.Forms.SplitContainer();
- this.button2 = new System.Windows.Forms.Button();
- this.comboBoxFileMode = new System.Windows.Forms.ComboBox();
- this.contextMenuStrip1.SuspendLayout();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBoxOrg)).BeginInit();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBoxSec)).BeginInit();
- this.tableLayoutPanel1.SuspendLayout();
- this.tableLayoutPanel2.SuspendLayout();
- ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
- this.splitContainer1.Panel1.SuspendLayout();
- this.splitContainer1.Panel2.SuspendLayout();
- this.splitContainer1.SuspendLayout();
- this.SuspendLayout();
- //
+ components = new System.ComponentModel.Container();
+ tileViewOrg = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
+ tileViewSec = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
+ contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(components);
+ extractAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ tiffToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ bmpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ jpgToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ pngToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ copyItem2To1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ btnCopyAllDiff = new System.Windows.Forms.Button();
+ pictureBoxOrg = new System.Windows.Forms.PictureBox();
+ pictureBoxSec = new System.Windows.Forms.PictureBox();
+ textBoxSecondDir = new System.Windows.Forms.TextBox();
+ button1 = new System.Windows.Forms.Button();
+ checkBox1 = new System.Windows.Forms.CheckBox();
+ chkMultiSelect = new System.Windows.Forms.CheckBox();
+ tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
+ tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel();
+ splitContainer1 = new System.Windows.Forms.SplitContainer();
+ button2 = new System.Windows.Forms.Button();
+ comboBoxFileMode = new System.Windows.Forms.ComboBox();
+ contextMenuStrip1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)pictureBoxOrg).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)pictureBoxSec).BeginInit();
+ tableLayoutPanel1.SuspendLayout();
+ tableLayoutPanel2.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
+ splitContainer1.Panel1.SuspendLayout();
+ splitContainer1.Panel2.SuspendLayout();
+ splitContainer1.SuspendLayout();
+ SuspendLayout();
+ //
// tileViewOrg
- //
- this.tileViewOrg.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tileViewOrg.Location = new System.Drawing.Point(4, 3);
- this.tileViewOrg.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tileViewOrg.Name = "tileViewOrg";
- this.tileViewOrg.Size = new System.Drawing.Size(188, 314);
- this.tileViewOrg.TabIndex = 0;
- this.tileViewOrg.TileSize = new System.Drawing.Size(188, 13);
- this.tileViewOrg.TileMargin = new System.Windows.Forms.Padding(0);
- this.tileViewOrg.TilePadding = new System.Windows.Forms.Padding(0);
- this.tileViewOrg.TileBorderWidth = 0f;
- this.tileViewOrg.TileHighLightOpacity = 0.0;
- this.tileViewOrg.DrawItem += new System.EventHandler(this.OnDrawItemOrg);
- this.tileViewOrg.FocusSelectionChanged += new System.EventHandler(this.OnFocusChangedOrg);
- this.tileViewOrg.SizeChanged += new System.EventHandler(this.OnTileViewSizeChanged);
- //
+ //
+ tileViewOrg.Dock = System.Windows.Forms.DockStyle.Fill;
+ tileViewOrg.Location = new System.Drawing.Point(4, 3);
+ tileViewOrg.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tileViewOrg.Name = "tileViewOrg";
+ tileViewOrg.Size = new System.Drawing.Size(188, 303);
+ tileViewOrg.TabIndex = 0;
+ tileViewOrg.TileHighLightOpacity = 0D;
+ tileViewOrg.FocusSelectionChanged += OnFocusChangedOrg;
+ tileViewOrg.DrawItem += OnDrawItemOrg;
+ tileViewOrg.SizeChanged += OnTileViewSizeChanged;
+ //
// tileViewSec
- //
- this.tileViewSec.ContextMenuStrip = this.contextMenuStrip1;
- this.tileViewSec.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tileViewSec.Location = new System.Drawing.Point(527, 3);
- this.tileViewSec.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tileViewSec.Name = "tileViewSec";
- this.tileViewSec.Size = new System.Drawing.Size(189, 314);
- this.tileViewSec.TabIndex = 1;
- this.tileViewSec.TileSize = new System.Drawing.Size(189, 13);
- this.tileViewSec.TileMargin = new System.Windows.Forms.Padding(0);
- this.tileViewSec.TilePadding = new System.Windows.Forms.Padding(0);
- this.tileViewSec.TileBorderWidth = 0f;
- this.tileViewSec.TileHighLightOpacity = 0.0;
- this.tileViewSec.DrawItem += new System.EventHandler(this.OnDrawItemSec);
- this.tileViewSec.FocusSelectionChanged += new System.EventHandler(this.OnFocusChangedSec);
- this.tileViewSec.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.OnDoubleClickSec);
- this.tileViewSec.SizeChanged += new System.EventHandler(this.OnTileViewSizeChanged);
+ //
+ tileViewSec.ContextMenuStrip = contextMenuStrip1;
+ tileViewSec.Dock = System.Windows.Forms.DockStyle.Fill;
+ tileViewSec.Location = new System.Drawing.Point(527, 3);
+ tileViewSec.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tileViewSec.Name = "tileViewSec";
+ tileViewSec.Size = new System.Drawing.Size(189, 303);
+ tileViewSec.TabIndex = 1;
+ tileViewSec.TileHighLightOpacity = 0D;
+ tileViewSec.FocusSelectionChanged += OnFocusChangedSec;
+ tileViewSec.DrawItem += OnDrawItemSec;
+ tileViewSec.SizeChanged += OnTileViewSizeChanged;
+ tileViewSec.MouseDoubleClick += OnDoubleClickSec;
//
// contextMenuStrip1
//
- this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.extractAsToolStripMenuItem,
- this.copyItem2To1ToolStripMenuItem});
- this.contextMenuStrip1.Name = "contextMenuStrip1";
- this.contextMenuStrip1.Size = new System.Drawing.Size(162, 48);
+ contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { extractAsToolStripMenuItem, copyItem2To1ToolStripMenuItem });
+ contextMenuStrip1.Name = "contextMenuStrip1";
+ contextMenuStrip1.Size = new System.Drawing.Size(164, 48);
//
// extractAsToolStripMenuItem
//
- this.extractAsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.tiffToolStripMenuItem,
- this.bmpToolStripMenuItem,
- this.jpgToolStripMenuItem,
- this.pngToolStripMenuItem});
- this.extractAsToolStripMenuItem.Name = "extractAsToolStripMenuItem";
- this.extractAsToolStripMenuItem.Size = new System.Drawing.Size(161, 22);
- this.extractAsToolStripMenuItem.Text = "Export Image..";
- //
+ extractAsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { tiffToolStripMenuItem, bmpToolStripMenuItem, jpgToolStripMenuItem, pngToolStripMenuItem });
+ extractAsToolStripMenuItem.Name = "extractAsToolStripMenuItem";
+ extractAsToolStripMenuItem.Size = new System.Drawing.Size(163, 22);
+ extractAsToolStripMenuItem.Text = "Export Image..";
+ //
// tiffToolStripMenuItem
- //
- this.tiffToolStripMenuItem.Name = "tiffToolStripMenuItem";
- this.tiffToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.tiffToolStripMenuItem.Text = "As Bmp";
- this.tiffToolStripMenuItem.Click += new System.EventHandler(this.ExportAsBmp);
- //
+ //
+ tiffToolStripMenuItem.Name = "tiffToolStripMenuItem";
+ tiffToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ tiffToolStripMenuItem.Text = "As Bmp";
+ tiffToolStripMenuItem.Click += ExportAsBmp;
+ //
// bmpToolStripMenuItem
- //
- this.bmpToolStripMenuItem.Name = "bmpToolStripMenuItem";
- this.bmpToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.bmpToolStripMenuItem.Text = "As Tiff";
- this.bmpToolStripMenuItem.Click += new System.EventHandler(this.ExportAsTiff);
- //
+ //
+ bmpToolStripMenuItem.Name = "bmpToolStripMenuItem";
+ bmpToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ bmpToolStripMenuItem.Text = "As Tiff";
+ bmpToolStripMenuItem.Click += ExportAsTiff;
+ //
// jpgToolStripMenuItem
- //
- this.jpgToolStripMenuItem.Name = "jpgToolStripMenuItem";
- this.jpgToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.jpgToolStripMenuItem.Text = "As Jpg";
- this.jpgToolStripMenuItem.Click += new System.EventHandler(this.ExportAsJpg);
- //
+ //
+ jpgToolStripMenuItem.Name = "jpgToolStripMenuItem";
+ jpgToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ jpgToolStripMenuItem.Text = "As Jpg";
+ jpgToolStripMenuItem.Click += ExportAsJpg;
+ //
// pngToolStripMenuItem
- //
- this.pngToolStripMenuItem.Name = "pngToolStripMenuItem";
- this.pngToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.pngToolStripMenuItem.Text = "As Png";
- this.pngToolStripMenuItem.Click += new System.EventHandler(this.ExportAsPng);
- //
+ //
+ pngToolStripMenuItem.Name = "pngToolStripMenuItem";
+ pngToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ pngToolStripMenuItem.Text = "As Png";
+ pngToolStripMenuItem.Click += ExportAsPng;
+ //
// copyItem2To1ToolStripMenuItem
//
- this.copyItem2To1ToolStripMenuItem.Name = "copyItem2To1ToolStripMenuItem";
- this.copyItem2To1ToolStripMenuItem.Size = new System.Drawing.Size(161, 22);
- this.copyItem2To1ToolStripMenuItem.Text = "Copy Item 2 to 1";
- this.copyItem2To1ToolStripMenuItem.Click += new System.EventHandler(this.OnClickCopy);
+ copyItem2To1ToolStripMenuItem.Name = "copyItem2To1ToolStripMenuItem";
+ copyItem2To1ToolStripMenuItem.Size = new System.Drawing.Size(163, 22);
+ copyItem2To1ToolStripMenuItem.Text = "Copy Item to left";
+ copyItem2To1ToolStripMenuItem.Click += OnClickCopy;
+ //
+ // btnCopyAllDiff
+ //
+ btnCopyAllDiff.AutoSize = true;
+ btnCopyAllDiff.Location = new System.Drawing.Point(598, 13);
+ btnCopyAllDiff.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ btnCopyAllDiff.Name = "btnCopyAllDiff";
+ btnCopyAllDiff.Size = new System.Drawing.Size(99, 29);
+ btnCopyAllDiff.TabIndex = 9;
+ btnCopyAllDiff.Text = "Copy All Diff";
+ btnCopyAllDiff.UseVisualStyleBackColor = true;
+ btnCopyAllDiff.Click += OnClickCopyAllDiff;
//
// pictureBoxOrg
//
- this.pictureBoxOrg.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
- this.pictureBoxOrg.Dock = System.Windows.Forms.DockStyle.Fill;
- this.pictureBoxOrg.Location = new System.Drawing.Point(5, 4);
- this.pictureBoxOrg.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.pictureBoxOrg.Name = "pictureBoxOrg";
- this.pictureBoxOrg.Size = new System.Drawing.Size(309, 149);
- this.pictureBoxOrg.TabIndex = 2;
- this.pictureBoxOrg.TabStop = false;
+ pictureBoxOrg.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
+ pictureBoxOrg.Dock = System.Windows.Forms.DockStyle.Fill;
+ pictureBoxOrg.Location = new System.Drawing.Point(5, 4);
+ pictureBoxOrg.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ pictureBoxOrg.Name = "pictureBoxOrg";
+ pictureBoxOrg.Size = new System.Drawing.Size(309, 144);
+ pictureBoxOrg.TabIndex = 2;
+ pictureBoxOrg.TabStop = false;
//
// pictureBoxSec
//
- this.pictureBoxSec.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
- this.pictureBoxSec.Dock = System.Windows.Forms.DockStyle.Fill;
- this.pictureBoxSec.Location = new System.Drawing.Point(5, 160);
- this.pictureBoxSec.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.pictureBoxSec.Name = "pictureBoxSec";
- this.pictureBoxSec.Size = new System.Drawing.Size(309, 150);
- this.pictureBoxSec.TabIndex = 3;
- this.pictureBoxSec.TabStop = false;
+ pictureBoxSec.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
+ pictureBoxSec.Dock = System.Windows.Forms.DockStyle.Fill;
+ pictureBoxSec.Location = new System.Drawing.Point(5, 155);
+ pictureBoxSec.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ pictureBoxSec.Name = "pictureBoxSec";
+ pictureBoxSec.Size = new System.Drawing.Size(309, 144);
+ pictureBoxSec.TabIndex = 3;
+ pictureBoxSec.TabStop = false;
//
// textBoxSecondDir
//
- this.textBoxSecondDir.Location = new System.Drawing.Point(126, 16);
- this.textBoxSecondDir.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.textBoxSecondDir.Name = "textBoxSecondDir";
- this.textBoxSecondDir.Size = new System.Drawing.Size(168, 23);
- this.textBoxSecondDir.TabIndex = 4;
+ textBoxSecondDir.Location = new System.Drawing.Point(126, 16);
+ textBoxSecondDir.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ textBoxSecondDir.Name = "textBoxSecondDir";
+ textBoxSecondDir.Size = new System.Drawing.Size(168, 23);
+ textBoxSecondDir.TabIndex = 4;
//
// button1
//
- this.button1.AutoSize = true;
- this.button1.Location = new System.Drawing.Point(336, 13);
- this.button1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.button1.Name = "button1";
- this.button1.Size = new System.Drawing.Size(99, 29);
- this.button1.TabIndex = 5;
- this.button1.Text = "Load Second";
- this.button1.UseVisualStyleBackColor = true;
- this.button1.Click += new System.EventHandler(this.OnClickLoadSecond);
- //
- // btnCopyAllDiff
- //
- this.btnCopyAllDiff.AutoSize = true;
- this.btnCopyAllDiff.Location = new System.Drawing.Point(598, 13);
- this.btnCopyAllDiff.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.btnCopyAllDiff.Name = "btnCopyAllDiff";
- this.btnCopyAllDiff.Size = new System.Drawing.Size(99, 29);
- this.btnCopyAllDiff.TabIndex = 9;
- this.btnCopyAllDiff.Text = "Copy All Diff";
- this.btnCopyAllDiff.UseVisualStyleBackColor = true;
- this.btnCopyAllDiff.Click += new System.EventHandler(this.OnClickCopyAllDiff);
- //
+ button1.AutoSize = true;
+ button1.Location = new System.Drawing.Point(336, 13);
+ button1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ button1.Name = "button1";
+ button1.Size = new System.Drawing.Size(99, 29);
+ button1.TabIndex = 5;
+ button1.Text = "Load Second";
+ button1.UseVisualStyleBackColor = true;
+ button1.Click += OnClickLoadSecond;
+ //
// checkBox1
- //
- this.checkBox1.AutoSize = true;
- this.checkBox1.Location = new System.Drawing.Point(443, 18);
- this.checkBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.checkBox1.Name = "checkBox1";
- this.checkBox1.Size = new System.Drawing.Size(143, 19);
- this.checkBox1.TabIndex = 6;
- this.checkBox1.Text = "Show only Differences";
- this.checkBox1.UseVisualStyleBackColor = true;
- this.checkBox1.Click += new System.EventHandler(this.OnChangeShowDiff);
+ //
+ checkBox1.AutoSize = true;
+ checkBox1.Location = new System.Drawing.Point(443, 18);
+ checkBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ checkBox1.Name = "checkBox1";
+ checkBox1.Size = new System.Drawing.Size(143, 19);
+ checkBox1.TabIndex = 6;
+ checkBox1.Text = "Show only Differences";
+ checkBox1.UseVisualStyleBackColor = true;
+ checkBox1.Click += OnChangeShowDiff;
+ //
+ // chkMultiSelect
+ //
+ chkMultiSelect.AutoSize = true;
+ chkMultiSelect.Location = new System.Drawing.Point(443, 41);
+ chkMultiSelect.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ chkMultiSelect.Name = "chkMultiSelect";
+ chkMultiSelect.Size = new System.Drawing.Size(90, 19);
+ chkMultiSelect.TabIndex = 10;
+ chkMultiSelect.Text = "Multi-Select";
+ chkMultiSelect.UseVisualStyleBackColor = true;
+ chkMultiSelect.CheckedChanged += OnChangeMultiSelect;
//
// tableLayoutPanel1
//
- this.tableLayoutPanel1.CellBorderStyle = System.Windows.Forms.TableLayoutPanelCellBorderStyle.Single;
- this.tableLayoutPanel1.ColumnCount = 1;
- this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
- this.tableLayoutPanel1.Controls.Add(this.pictureBoxSec, 0, 1);
- this.tableLayoutPanel1.Controls.Add(this.pictureBoxOrg, 0, 0);
- this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tableLayoutPanel1.Location = new System.Drawing.Point(200, 3);
- this.tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tableLayoutPanel1.Name = "tableLayoutPanel1";
- this.tableLayoutPanel1.RowCount = 2;
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.tableLayoutPanel1.Size = new System.Drawing.Size(319, 314);
- this.tableLayoutPanel1.TabIndex = 7;
+ tableLayoutPanel1.CellBorderStyle = System.Windows.Forms.TableLayoutPanelCellBorderStyle.Single;
+ tableLayoutPanel1.ColumnCount = 1;
+ tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ tableLayoutPanel1.Controls.Add(pictureBoxSec, 0, 1);
+ tableLayoutPanel1.Controls.Add(pictureBoxOrg, 0, 0);
+ tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
+ tableLayoutPanel1.Location = new System.Drawing.Point(200, 3);
+ tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tableLayoutPanel1.Name = "tableLayoutPanel1";
+ tableLayoutPanel1.RowCount = 2;
+ tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ tableLayoutPanel1.Size = new System.Drawing.Size(319, 303);
+ tableLayoutPanel1.TabIndex = 7;
//
// tableLayoutPanel2
//
- this.tableLayoutPanel2.ColumnCount = 3;
- this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 27.27273F));
- this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 45.45454F));
- this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 27.27273F));
- this.tableLayoutPanel2.Controls.Add(this.tileViewOrg, 0, 0);
- this.tableLayoutPanel2.Controls.Add(this.tileViewSec, 2, 0);
- this.tableLayoutPanel2.Controls.Add(this.tableLayoutPanel1, 1, 0);
- this.tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tableLayoutPanel2.Location = new System.Drawing.Point(0, 0);
- this.tableLayoutPanel2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tableLayoutPanel2.Name = "tableLayoutPanel2";
- this.tableLayoutPanel2.RowCount = 1;
- this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
- this.tableLayoutPanel2.Size = new System.Drawing.Size(720, 320);
- this.tableLayoutPanel2.TabIndex = 8;
+ tableLayoutPanel2.ColumnCount = 3;
+ tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 27.27273F));
+ tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 45.45454F));
+ tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 27.27273F));
+ tableLayoutPanel2.Controls.Add(tileViewOrg, 0, 0);
+ tableLayoutPanel2.Controls.Add(tileViewSec, 2, 0);
+ tableLayoutPanel2.Controls.Add(tableLayoutPanel1, 1, 0);
+ tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill;
+ tableLayoutPanel2.Location = new System.Drawing.Point(0, 0);
+ tableLayoutPanel2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tableLayoutPanel2.Name = "tableLayoutPanel2";
+ tableLayoutPanel2.RowCount = 1;
+ tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ tableLayoutPanel2.Size = new System.Drawing.Size(720, 309);
+ tableLayoutPanel2.TabIndex = 8;
//
// splitContainer1
//
- this.splitContainer1.BackColor = System.Drawing.SystemColors.Control;
- this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
- this.splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
- this.splitContainer1.IsSplitterFixed = true;
- this.splitContainer1.Location = new System.Drawing.Point(0, 0);
- this.splitContainer1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.splitContainer1.Name = "splitContainer1";
- this.splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
+ splitContainer1.BackColor = System.Drawing.SystemColors.Control;
+ splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
+ splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
+ splitContainer1.IsSplitterFixed = true;
+ splitContainer1.Location = new System.Drawing.Point(0, 0);
+ splitContainer1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ splitContainer1.Name = "splitContainer1";
+ splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
//
// splitContainer1.Panel1
//
- this.splitContainer1.Panel1.Controls.Add(this.tableLayoutPanel2);
+ splitContainer1.Panel1.Controls.Add(tableLayoutPanel2);
//
// splitContainer1.Panel2
//
- this.splitContainer1.Panel2.Controls.Add(this.btnCopyAllDiff);
- this.splitContainer1.Panel2.Controls.Add(this.button2);
- this.splitContainer1.Panel2.Controls.Add(this.textBoxSecondDir);
- this.splitContainer1.Panel2.Controls.Add(this.checkBox1);
- this.splitContainer1.Panel2.Controls.Add(this.button1);
- this.splitContainer1.Panel2.Controls.Add(this.comboBoxFileMode);
- this.splitContainer1.Size = new System.Drawing.Size(720, 383);
- this.splitContainer1.SplitterDistance = 320;
- this.splitContainer1.SplitterWidth = 5;
- this.splitContainer1.TabIndex = 9;
+ splitContainer1.Panel2.Controls.Add(btnCopyAllDiff);
+ splitContainer1.Panel2.Controls.Add(button2);
+ splitContainer1.Panel2.Controls.Add(textBoxSecondDir);
+ splitContainer1.Panel2.Controls.Add(checkBox1);
+ splitContainer1.Panel2.Controls.Add(chkMultiSelect);
+ splitContainer1.Panel2.Controls.Add(button1);
+ splitContainer1.Panel2.Controls.Add(comboBoxFileMode);
+ splitContainer1.Size = new System.Drawing.Size(720, 383);
+ splitContainer1.SplitterDistance = 309;
+ splitContainer1.SplitterWidth = 5;
+ splitContainer1.TabIndex = 9;
//
// button2
//
- this.button2.AutoSize = true;
- this.button2.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
- this.button2.Location = new System.Drawing.Point(302, 15);
- this.button2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.button2.Name = "button2";
- this.button2.Size = new System.Drawing.Size(26, 25);
- this.button2.TabIndex = 7;
- this.button2.Text = "...";
- this.button2.UseVisualStyleBackColor = true;
- this.button2.Click += new System.EventHandler(this.OnClickBrowse);
- //
+ button2.AutoSize = true;
+ button2.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
+ button2.Location = new System.Drawing.Point(302, 15);
+ button2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ button2.Name = "button2";
+ button2.Size = new System.Drawing.Size(26, 25);
+ button2.TabIndex = 7;
+ button2.Text = "...";
+ button2.UseVisualStyleBackColor = true;
+ button2.Click += OnClickBrowse;
+ //
// comboBoxFileMode
- //
- this.comboBoxFileMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
- this.comboBoxFileMode.FormattingEnabled = true;
- this.comboBoxFileMode.Items.AddRange(new object[] {
- "Auto",
- "MUL",
- "UOP"});
- this.comboBoxFileMode.Location = new System.Drawing.Point(5, 16);
- this.comboBoxFileMode.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.comboBoxFileMode.Name = "comboBoxFileMode";
- this.comboBoxFileMode.Size = new System.Drawing.Size(70, 23);
- this.comboBoxFileMode.TabIndex = 10;
- //
+ //
+ comboBoxFileMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ comboBoxFileMode.FormattingEnabled = true;
+ comboBoxFileMode.Items.AddRange(new object[] { "Auto", "MUL", "UOP" });
+ comboBoxFileMode.Location = new System.Drawing.Point(5, 16);
+ comboBoxFileMode.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ comboBoxFileMode.Name = "comboBoxFileMode";
+ comboBoxFileMode.Size = new System.Drawing.Size(70, 23);
+ comboBoxFileMode.TabIndex = 10;
+ //
// CompareItemControl
- //
- this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.Controls.Add(this.splitContainer1);
- this.DoubleBuffered = true;
- this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.Name = "CompareItemControl";
- this.Size = new System.Drawing.Size(720, 383);
- this.Load += new System.EventHandler(this.OnLoad);
- this.contextMenuStrip1.ResumeLayout(false);
- ((System.ComponentModel.ISupportInitialize)(this.pictureBoxOrg)).EndInit();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBoxSec)).EndInit();
- this.tableLayoutPanel1.ResumeLayout(false);
- this.tableLayoutPanel2.ResumeLayout(false);
- this.splitContainer1.Panel1.ResumeLayout(false);
- this.splitContainer1.Panel2.ResumeLayout(false);
- this.splitContainer1.Panel2.PerformLayout();
- ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
- this.splitContainer1.ResumeLayout(false);
- this.ResumeLayout(false);
+ //
+ AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ Controls.Add(splitContainer1);
+ DoubleBuffered = true;
+ Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ Name = "CompareItemControl";
+ Size = new System.Drawing.Size(720, 383);
+ Load += OnLoad;
+ contextMenuStrip1.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)pictureBoxOrg).EndInit();
+ ((System.ComponentModel.ISupportInitialize)pictureBoxSec).EndInit();
+ tableLayoutPanel1.ResumeLayout(false);
+ tableLayoutPanel2.ResumeLayout(false);
+ splitContainer1.Panel1.ResumeLayout(false);
+ splitContainer1.Panel2.ResumeLayout(false);
+ splitContainer1.Panel2.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
+ splitContainer1.ResumeLayout(false);
+ ResumeLayout(false);
}
@@ -348,6 +345,7 @@ private void InitializeComponent()
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.CheckBox checkBox1;
+ private System.Windows.Forms.CheckBox chkMultiSelect;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
private System.Windows.Forms.ToolStripMenuItem copyItem2To1ToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem extractAsToolStripMenuItem;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
index 216d36c..af1e328 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
@@ -14,6 +14,7 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using System.Linq;
using System.Security.Cryptography;
using System.Windows.Forms;
using Ultima;
@@ -40,6 +41,9 @@ public CompareItemControl()
private void OnLoad(object sender, EventArgs e)
{
+ ConfigureTileView(tileViewOrg);
+ ConfigureTileView(tileViewSec);
+
_displayIndices.Clear();
int count = Art.GetMaxItemId() + 1;
for (int i = 0; i < count; i++)
@@ -50,6 +54,16 @@ private void OnLoad(object sender, EventArgs e)
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = 0;
+ tileViewSec.MultiSelect = true;
+ tileViewSec.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
+ contextMenuStrip1.Opening += (s, ev) =>
+ {
+ int count = tileViewSec.SelectedIndices.Count;
+ copyItem2To1ToolStripMenuItem.Text = tileViewSec.ShowCheckBoxes && count > 1
+ ? $"Copy {count} Items to left"
+ : "Copy Item to left";
+ };
+
if (comboBoxFileMode.SelectedIndex < 0)
{
comboBoxFileMode.SelectedIndex = 0;
@@ -59,6 +73,61 @@ private void OnLoad(object sender, EventArgs e)
ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
}
+ // TileViewControl exposes TileSize/Margin/Padding/Border with DesignerSerializationVisibility.Hidden,
+ // so VS strips them when re-saving the .Designer.cs. Apply the intended values here so they survive.
+ private static void ConfigureTileView(TileViewControl tv)
+ {
+ tv.TileSize = new Size(tv.TileSize.Width, 20);
+ tv.TileMargin = new Padding(0);
+ tv.TilePadding = new Padding(0);
+ tv.TileBorderWidth = 0f;
+ }
+
+ private void OnChangeMultiSelect(object sender, EventArgs e)
+ {
+ tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ if (!chkMultiSelect.Checked)
+ {
+ tileViewSec.SelectedIndices.Clear();
+ }
+ }
+
+ private void OnSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ if (_syncingSelection)
+ {
+ return;
+ }
+
+ _syncingSelection = true;
+ try
+ {
+ tileViewOrg.SelectedIndices.Clear();
+ foreach (int idx in tileViewSec.SelectedIndices)
+ {
+ tileViewOrg.SelectedIndices.Add(idx);
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private List GetCopyTargets()
+ {
+ var sel = tileViewSec.SelectedIndices;
+ if (sel.Count > 0)
+ {
+ return sel.ToList();
+ }
+ if (tileViewSec.FocusIndex >= 0)
+ {
+ return new List { tileViewSec.FocusIndex };
+ }
+ return new List();
+ }
+
private void OnFilePathChangeEvent()
{
_compare.Clear();
@@ -98,7 +167,7 @@ private void OnDrawItemSec(object sender, TileViewControl.DrawTileListItemEventA
DrawListItem(e, _displayIndices[e.Index], isSecondary: true);
}
- private void DrawListItem(DrawItemEventArgs e, int i, bool isSecondary)
+ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
@@ -123,7 +192,7 @@ private void DrawListItem(DrawItemEventArgs e, int i, bool isSecondary)
string label = $"0x{i:X}";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, Font).Height) / 2f;
- e.Graphics.DrawString(label, Font, fontBrush, new PointF(5, y));
+ e.Graphics.DrawString(label, Font, fontBrush, new PointF(e.ContentLeft + 5, y));
}
private void OnFocusChangedOrg(object sender, TileViewControl.ListViewFocusedItemSelectionChangedEventArgs e)
@@ -377,43 +446,75 @@ private void ExportAsPng(object sender, EventArgs e)
private void OnClickCopy(object sender, EventArgs e)
{
- int focusIdx = tileViewSec.FocusIndex;
- if (focusIdx < 0)
+ var targets = GetCopyTargets();
+ if (targets.Count == 0)
{
return;
}
- int i = _displayIndices[focusIdx];
- if (!SecondArt.IsValidStatic(i))
+ Cursor.Current = Cursors.WaitCursor;
+ int maxId = Art.GetMaxItemId() + 1;
+ int lastCopiedId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- return;
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
+
+ int i = _displayIndices[focusIdx];
+ if (!SecondArt.IsValidStatic(i) || i >= maxId)
+ {
+ continue;
+ }
+
+ Bitmap copy = new Bitmap(SecondArt.GetStatic(i));
+ Art.ReplaceStatic(i, copy);
+ ControlEvents.FireItemChangeEvent(this, i);
+ _compare[i] = true;
+ lastCopiedId = i;
+ changed = true;
}
- if (i >= Art.GetMaxItemId() + 1)
+ if (changed)
{
- return;
+ Options.ChangedUltimaClass["Art"] = true;
}
- Bitmap copy = new Bitmap(SecondArt.GetStatic(i));
- Art.ReplaceStatic(i, copy);
- Options.ChangedUltimaClass["Art"] = true;
- ControlEvents.FireItemChangeEvent(this, i);
- _compare[i] = true;
-
- if (checkBox1.Checked)
+ if (checkBox1.Checked && changed)
{
- _displayIndices.RemoveAt(focusIdx);
+ foreach (int idx in targets.OrderByDescending(x => x))
+ {
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
+ }
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = _displayIndices.Count;
}
+ else
+ {
+ tileViewSec.SelectedIndices.Clear();
+ }
tileViewOrg.Invalidate();
tileViewSec.Invalidate();
- pictureBoxOrg.BackgroundImage = Art.IsValidStatic(i) ? Art.GetStatic(i) : null;
+ if (lastCopiedId >= 0)
+ {
+ pictureBoxOrg.BackgroundImage = Art.IsValidStatic(lastCopiedId) ? Art.GetStatic(lastCopiedId) : null;
+ }
+ Cursor.Current = Cursors.Default;
}
private void OnDoubleClickSec(object sender, MouseEventArgs e)
{
+ if (tileViewSec.ShowCheckBoxes)
+ {
+ return;
+ }
OnClickCopy(sender, e);
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.resx b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.resx
index 16e4841..75b37ca 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.resx
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.resx
@@ -1,4 +1,64 @@
+
+
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.Designer.cs
index 2b70467..774cf94 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.Designer.cs
@@ -39,305 +39,302 @@ protected override void Dispose(bool disposing)
///
private void InitializeComponent()
{
- this.components = new System.ComponentModel.Container();
- this.tileViewOrg = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
- this.btnCopyAllDiff = new System.Windows.Forms.Button();
- this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
- this.pictureBoxSec = new System.Windows.Forms.PictureBox();
- this.pictureBoxOrg = new System.Windows.Forms.PictureBox();
- this.textBoxSecondDir = new System.Windows.Forms.TextBox();
- this.tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel();
- this.tileViewSec = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
- this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components);
- this.exportImageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.asBmpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.asTiffToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.asJpgToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.asPngToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.copyLandTile2To1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.checkBox1 = new System.Windows.Forms.CheckBox();
- this.button1 = new System.Windows.Forms.Button();
- this.splitContainer1 = new System.Windows.Forms.SplitContainer();
- this.button2 = new System.Windows.Forms.Button();
- this.comboBoxFileMode = new System.Windows.Forms.ComboBox();
- this.tableLayoutPanel1.SuspendLayout();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBoxSec)).BeginInit();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBoxOrg)).BeginInit();
- this.tableLayoutPanel2.SuspendLayout();
- this.contextMenuStrip1.SuspendLayout();
- ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
- this.splitContainer1.Panel1.SuspendLayout();
- this.splitContainer1.Panel2.SuspendLayout();
- this.splitContainer1.SuspendLayout();
- this.SuspendLayout();
- //
+ components = new System.ComponentModel.Container();
+ tileViewOrg = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
+ btnCopyAllDiff = new System.Windows.Forms.Button();
+ tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
+ pictureBoxSec = new System.Windows.Forms.PictureBox();
+ pictureBoxOrg = new System.Windows.Forms.PictureBox();
+ textBoxSecondDir = new System.Windows.Forms.TextBox();
+ tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel();
+ tileViewSec = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
+ contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(components);
+ exportImageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ asBmpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ asTiffToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ asJpgToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ asPngToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ copyLandTile2To1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ checkBox1 = new System.Windows.Forms.CheckBox();
+ chkMultiSelect = new System.Windows.Forms.CheckBox();
+ button1 = new System.Windows.Forms.Button();
+ splitContainer1 = new System.Windows.Forms.SplitContainer();
+ button2 = new System.Windows.Forms.Button();
+ comboBoxFileMode = new System.Windows.Forms.ComboBox();
+ tableLayoutPanel1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)pictureBoxSec).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)pictureBoxOrg).BeginInit();
+ tableLayoutPanel2.SuspendLayout();
+ contextMenuStrip1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
+ splitContainer1.Panel1.SuspendLayout();
+ splitContainer1.Panel2.SuspendLayout();
+ splitContainer1.SuspendLayout();
+ SuspendLayout();
+ //
// tileViewOrg
- //
- this.tileViewOrg.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tileViewOrg.Location = new System.Drawing.Point(4, 3);
- this.tileViewOrg.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tileViewOrg.Name = "tileViewOrg";
- this.tileViewOrg.Size = new System.Drawing.Size(188, 364);
- this.tileViewOrg.TabIndex = 0;
- this.tileViewOrg.TileSize = new System.Drawing.Size(188, 13);
- this.tileViewOrg.TileMargin = new System.Windows.Forms.Padding(0);
- this.tileViewOrg.TilePadding = new System.Windows.Forms.Padding(0);
- this.tileViewOrg.TileBorderWidth = 0f;
- this.tileViewOrg.TileHighLightOpacity = 0.0;
- this.tileViewOrg.DrawItem += new System.EventHandler(this.OnDrawItemOrg);
- this.tileViewOrg.FocusSelectionChanged += new System.EventHandler(this.OnFocusChangedOrg);
- this.tileViewOrg.SizeChanged += new System.EventHandler(this.OnTileViewSizeChanged);
+ //
+ tileViewOrg.Dock = System.Windows.Forms.DockStyle.Fill;
+ tileViewOrg.Location = new System.Drawing.Point(4, 3);
+ tileViewOrg.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tileViewOrg.Name = "tileViewOrg";
+ tileViewOrg.Size = new System.Drawing.Size(188, 353);
+ tileViewOrg.TabIndex = 0;
+ tileViewOrg.TileHighLightOpacity = 0D;
+ tileViewOrg.FocusSelectionChanged += OnFocusChangedOrg;
+ tileViewOrg.DrawItem += OnDrawItemOrg;
+ tileViewOrg.SizeChanged += OnTileViewSizeChanged;
+ //
+ // btnCopyAllDiff
+ //
+ btnCopyAllDiff.AutoSize = true;
+ btnCopyAllDiff.Location = new System.Drawing.Point(594, 11);
+ btnCopyAllDiff.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ btnCopyAllDiff.Name = "btnCopyAllDiff";
+ btnCopyAllDiff.Size = new System.Drawing.Size(99, 29);
+ btnCopyAllDiff.TabIndex = 9;
+ btnCopyAllDiff.Text = "Copy All Diff";
+ btnCopyAllDiff.UseVisualStyleBackColor = true;
+ btnCopyAllDiff.Click += OnClickCopyAllDiff;
//
// tableLayoutPanel1
//
- this.tableLayoutPanel1.CellBorderStyle = System.Windows.Forms.TableLayoutPanelCellBorderStyle.Single;
- this.tableLayoutPanel1.ColumnCount = 1;
- this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
- this.tableLayoutPanel1.Controls.Add(this.pictureBoxSec, 0, 1);
- this.tableLayoutPanel1.Controls.Add(this.pictureBoxOrg, 0, 0);
- this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tableLayoutPanel1.Location = new System.Drawing.Point(200, 3);
- this.tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tableLayoutPanel1.Name = "tableLayoutPanel1";
- this.tableLayoutPanel1.RowCount = 2;
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.tableLayoutPanel1.Size = new System.Drawing.Size(318, 364);
- this.tableLayoutPanel1.TabIndex = 7;
+ tableLayoutPanel1.CellBorderStyle = System.Windows.Forms.TableLayoutPanelCellBorderStyle.Single;
+ tableLayoutPanel1.ColumnCount = 1;
+ tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ tableLayoutPanel1.Controls.Add(pictureBoxSec, 0, 1);
+ tableLayoutPanel1.Controls.Add(pictureBoxOrg, 0, 0);
+ tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
+ tableLayoutPanel1.Location = new System.Drawing.Point(200, 3);
+ tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tableLayoutPanel1.Name = "tableLayoutPanel1";
+ tableLayoutPanel1.RowCount = 2;
+ tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ tableLayoutPanel1.Size = new System.Drawing.Size(318, 353);
+ tableLayoutPanel1.TabIndex = 7;
//
// pictureBoxSec
//
- this.pictureBoxSec.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
- this.pictureBoxSec.Dock = System.Windows.Forms.DockStyle.Fill;
- this.pictureBoxSec.Location = new System.Drawing.Point(5, 185);
- this.pictureBoxSec.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.pictureBoxSec.Name = "pictureBoxSec";
- this.pictureBoxSec.Size = new System.Drawing.Size(308, 175);
- this.pictureBoxSec.TabIndex = 3;
- this.pictureBoxSec.TabStop = false;
+ pictureBoxSec.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
+ pictureBoxSec.Dock = System.Windows.Forms.DockStyle.Fill;
+ pictureBoxSec.Location = new System.Drawing.Point(5, 180);
+ pictureBoxSec.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ pictureBoxSec.Name = "pictureBoxSec";
+ pictureBoxSec.Size = new System.Drawing.Size(308, 169);
+ pictureBoxSec.TabIndex = 3;
+ pictureBoxSec.TabStop = false;
//
// pictureBoxOrg
//
- this.pictureBoxOrg.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
- this.pictureBoxOrg.Dock = System.Windows.Forms.DockStyle.Fill;
- this.pictureBoxOrg.Location = new System.Drawing.Point(5, 4);
- this.pictureBoxOrg.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.pictureBoxOrg.Name = "pictureBoxOrg";
- this.pictureBoxOrg.Size = new System.Drawing.Size(308, 174);
- this.pictureBoxOrg.TabIndex = 2;
- this.pictureBoxOrg.TabStop = false;
+ pictureBoxOrg.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
+ pictureBoxOrg.Dock = System.Windows.Forms.DockStyle.Fill;
+ pictureBoxOrg.Location = new System.Drawing.Point(5, 4);
+ pictureBoxOrg.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ pictureBoxOrg.Name = "pictureBoxOrg";
+ pictureBoxOrg.Size = new System.Drawing.Size(308, 169);
+ pictureBoxOrg.TabIndex = 2;
+ pictureBoxOrg.TabStop = false;
//
// textBoxSecondDir
//
- this.textBoxSecondDir.Location = new System.Drawing.Point(122, 13);
- this.textBoxSecondDir.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.textBoxSecondDir.Name = "textBoxSecondDir";
- this.textBoxSecondDir.Size = new System.Drawing.Size(168, 23);
- this.textBoxSecondDir.TabIndex = 4;
+ textBoxSecondDir.Location = new System.Drawing.Point(122, 13);
+ textBoxSecondDir.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ textBoxSecondDir.Name = "textBoxSecondDir";
+ textBoxSecondDir.Size = new System.Drawing.Size(168, 23);
+ textBoxSecondDir.TabIndex = 4;
//
// tableLayoutPanel2
//
- this.tableLayoutPanel2.ColumnCount = 3;
- this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 27.27273F));
- this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 45.45454F));
- this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 27.27273F));
- this.tableLayoutPanel2.Controls.Add(this.tileViewOrg, 0, 0);
- this.tableLayoutPanel2.Controls.Add(this.tileViewSec, 2, 0);
- this.tableLayoutPanel2.Controls.Add(this.tableLayoutPanel1, 1, 0);
- this.tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tableLayoutPanel2.Location = new System.Drawing.Point(0, 0);
- this.tableLayoutPanel2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tableLayoutPanel2.Name = "tableLayoutPanel2";
- this.tableLayoutPanel2.RowCount = 1;
- this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
- this.tableLayoutPanel2.Size = new System.Drawing.Size(719, 370);
- this.tableLayoutPanel2.TabIndex = 8;
- //
+ tableLayoutPanel2.ColumnCount = 3;
+ tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 27.27273F));
+ tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 45.45454F));
+ tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 27.27273F));
+ tableLayoutPanel2.Controls.Add(tileViewOrg, 0, 0);
+ tableLayoutPanel2.Controls.Add(tileViewSec, 2, 0);
+ tableLayoutPanel2.Controls.Add(tableLayoutPanel1, 1, 0);
+ tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill;
+ tableLayoutPanel2.Location = new System.Drawing.Point(0, 0);
+ tableLayoutPanel2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tableLayoutPanel2.Name = "tableLayoutPanel2";
+ tableLayoutPanel2.RowCount = 1;
+ tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ tableLayoutPanel2.Size = new System.Drawing.Size(719, 359);
+ tableLayoutPanel2.TabIndex = 8;
+ //
// tileViewSec
- //
- this.tileViewSec.ContextMenuStrip = this.contextMenuStrip1;
- this.tileViewSec.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tileViewSec.Location = new System.Drawing.Point(526, 3);
- this.tileViewSec.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.tileViewSec.Name = "tileViewSec";
- this.tileViewSec.Size = new System.Drawing.Size(189, 364);
- this.tileViewSec.TabIndex = 1;
- this.tileViewSec.TileSize = new System.Drawing.Size(189, 13);
- this.tileViewSec.TileMargin = new System.Windows.Forms.Padding(0);
- this.tileViewSec.TilePadding = new System.Windows.Forms.Padding(0);
- this.tileViewSec.TileBorderWidth = 0f;
- this.tileViewSec.TileHighLightOpacity = 0.0;
- this.tileViewSec.DrawItem += new System.EventHandler(this.OnDrawItemSec);
- this.tileViewSec.FocusSelectionChanged += new System.EventHandler(this.OnFocusChangedSec);
- this.tileViewSec.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.OnDoubleClickSec);
- this.tileViewSec.SizeChanged += new System.EventHandler(this.OnTileViewSizeChanged);
+ //
+ tileViewSec.ContextMenuStrip = contextMenuStrip1;
+ tileViewSec.Dock = System.Windows.Forms.DockStyle.Fill;
+ tileViewSec.Location = new System.Drawing.Point(526, 3);
+ tileViewSec.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ tileViewSec.Name = "tileViewSec";
+ tileViewSec.Size = new System.Drawing.Size(189, 353);
+ tileViewSec.TabIndex = 1;
+ tileViewSec.TileHighLightOpacity = 0D;
+ tileViewSec.FocusSelectionChanged += OnFocusChangedSec;
+ tileViewSec.DrawItem += OnDrawItemSec;
+ tileViewSec.SizeChanged += OnTileViewSizeChanged;
+ tileViewSec.MouseDoubleClick += OnDoubleClickSec;
//
// contextMenuStrip1
//
- this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.exportImageToolStripMenuItem,
- this.copyLandTile2To1ToolStripMenuItem});
- this.contextMenuStrip1.Name = "contextMenuStrip1";
- this.contextMenuStrip1.Size = new System.Drawing.Size(182, 48);
+ contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { exportImageToolStripMenuItem, copyLandTile2To1ToolStripMenuItem });
+ contextMenuStrip1.Name = "contextMenuStrip1";
+ contextMenuStrip1.Size = new System.Drawing.Size(184, 48);
//
// exportImageToolStripMenuItem
//
- this.exportImageToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.asBmpToolStripMenuItem,
- this.asTiffToolStripMenuItem,
- this.asJpgToolStripMenuItem,
- this.asPngToolStripMenuItem});
- this.exportImageToolStripMenuItem.Name = "exportImageToolStripMenuItem";
- this.exportImageToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
- this.exportImageToolStripMenuItem.Text = "Export Image..";
- //
+ exportImageToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { asBmpToolStripMenuItem, asTiffToolStripMenuItem, asJpgToolStripMenuItem, asPngToolStripMenuItem });
+ exportImageToolStripMenuItem.Name = "exportImageToolStripMenuItem";
+ exportImageToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
+ exportImageToolStripMenuItem.Text = "Export Image..";
+ //
// asBmpToolStripMenuItem
- //
- this.asBmpToolStripMenuItem.Name = "asBmpToolStripMenuItem";
- this.asBmpToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.asBmpToolStripMenuItem.Text = "As Bmp";
- this.asBmpToolStripMenuItem.Click += new System.EventHandler(this.ExportAsBmp);
- //
+ //
+ asBmpToolStripMenuItem.Name = "asBmpToolStripMenuItem";
+ asBmpToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ asBmpToolStripMenuItem.Text = "As Bmp";
+ asBmpToolStripMenuItem.Click += ExportAsBmp;
+ //
// asTiffToolStripMenuItem
- //
- this.asTiffToolStripMenuItem.Name = "asTiffToolStripMenuItem";
- this.asTiffToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.asTiffToolStripMenuItem.Text = "As Tiff";
- this.asTiffToolStripMenuItem.Click += new System.EventHandler(this.ExportAsTiff);
- //
+ //
+ asTiffToolStripMenuItem.Name = "asTiffToolStripMenuItem";
+ asTiffToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ asTiffToolStripMenuItem.Text = "As Tiff";
+ asTiffToolStripMenuItem.Click += ExportAsTiff;
+ //
// asJpgToolStripMenuItem
- //
- this.asJpgToolStripMenuItem.Name = "asJpgToolStripMenuItem";
- this.asJpgToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.asJpgToolStripMenuItem.Text = "As Jpg";
- this.asJpgToolStripMenuItem.Click += new System.EventHandler(this.ExportAsJpg);
- //
+ //
+ asJpgToolStripMenuItem.Name = "asJpgToolStripMenuItem";
+ asJpgToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ asJpgToolStripMenuItem.Text = "As Jpg";
+ asJpgToolStripMenuItem.Click += ExportAsJpg;
+ //
// asPngToolStripMenuItem
- //
- this.asPngToolStripMenuItem.Name = "asPngToolStripMenuItem";
- this.asPngToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
- this.asPngToolStripMenuItem.Text = "As Png";
- this.asPngToolStripMenuItem.Click += new System.EventHandler(this.ExportAsPng);
- //
+ //
+ asPngToolStripMenuItem.Name = "asPngToolStripMenuItem";
+ asPngToolStripMenuItem.Size = new System.Drawing.Size(115, 22);
+ asPngToolStripMenuItem.Text = "As Png";
+ asPngToolStripMenuItem.Click += ExportAsPng;
+ //
// copyLandTile2To1ToolStripMenuItem
//
- this.copyLandTile2To1ToolStripMenuItem.Name = "copyLandTile2To1ToolStripMenuItem";
- this.copyLandTile2To1ToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
- this.copyLandTile2To1ToolStripMenuItem.Text = "Copy LandTile 2 to 1";
- this.copyLandTile2To1ToolStripMenuItem.Click += new System.EventHandler(this.OnClickCopy);
- //
- // btnCopyAllDiff
- //
- this.btnCopyAllDiff.AutoSize = true;
- this.btnCopyAllDiff.Location = new System.Drawing.Point(594, 11);
- this.btnCopyAllDiff.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.btnCopyAllDiff.Name = "btnCopyAllDiff";
- this.btnCopyAllDiff.Size = new System.Drawing.Size(99, 29);
- this.btnCopyAllDiff.TabIndex = 9;
- this.btnCopyAllDiff.Text = "Copy All Diff";
- this.btnCopyAllDiff.UseVisualStyleBackColor = true;
- this.btnCopyAllDiff.Click += new System.EventHandler(this.OnClickCopyAllDiff);
- //
+ copyLandTile2To1ToolStripMenuItem.Name = "copyLandTile2To1ToolStripMenuItem";
+ copyLandTile2To1ToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
+ copyLandTile2To1ToolStripMenuItem.Text = "Copy LandTile to left";
+ copyLandTile2To1ToolStripMenuItem.Click += OnClickCopy;
+ //
// checkBox1
- //
- this.checkBox1.AutoSize = true;
- this.checkBox1.Location = new System.Drawing.Point(439, 15);
- this.checkBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.checkBox1.Name = "checkBox1";
- this.checkBox1.Size = new System.Drawing.Size(143, 19);
- this.checkBox1.TabIndex = 6;
- this.checkBox1.Text = "Show only Differences";
- this.checkBox1.UseVisualStyleBackColor = true;
- this.checkBox1.Click += new System.EventHandler(this.OnChangeShowDiff);
+ //
+ checkBox1.AutoSize = true;
+ checkBox1.Location = new System.Drawing.Point(439, 15);
+ checkBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ checkBox1.Name = "checkBox1";
+ checkBox1.Size = new System.Drawing.Size(143, 19);
+ checkBox1.TabIndex = 6;
+ checkBox1.Text = "Show only Differences";
+ checkBox1.UseVisualStyleBackColor = true;
+ checkBox1.Click += OnChangeShowDiff;
+ //
+ // chkMultiSelect
+ //
+ chkMultiSelect.AutoSize = true;
+ chkMultiSelect.Location = new System.Drawing.Point(439, 38);
+ chkMultiSelect.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ chkMultiSelect.Name = "chkMultiSelect";
+ chkMultiSelect.Size = new System.Drawing.Size(90, 19);
+ chkMultiSelect.TabIndex = 10;
+ chkMultiSelect.Text = "Multi-Select";
+ chkMultiSelect.UseVisualStyleBackColor = true;
+ chkMultiSelect.CheckedChanged += OnChangeMultiSelect;
//
// button1
//
- this.button1.AutoSize = true;
- this.button1.Location = new System.Drawing.Point(332, 11);
- this.button1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.button1.Name = "button1";
- this.button1.Size = new System.Drawing.Size(99, 29);
- this.button1.TabIndex = 5;
- this.button1.Text = "Load Second";
- this.button1.UseVisualStyleBackColor = true;
- this.button1.Click += new System.EventHandler(this.OnClickLoadSecond);
+ button1.AutoSize = true;
+ button1.Location = new System.Drawing.Point(332, 11);
+ button1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ button1.Name = "button1";
+ button1.Size = new System.Drawing.Size(99, 29);
+ button1.TabIndex = 5;
+ button1.Text = "Load Second";
+ button1.UseVisualStyleBackColor = true;
+ button1.Click += OnClickLoadSecond;
//
// splitContainer1
//
- this.splitContainer1.BackColor = System.Drawing.SystemColors.Control;
- this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
- this.splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
- this.splitContainer1.IsSplitterFixed = true;
- this.splitContainer1.Location = new System.Drawing.Point(0, 0);
- this.splitContainer1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.splitContainer1.Name = "splitContainer1";
- this.splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
+ splitContainer1.BackColor = System.Drawing.SystemColors.Control;
+ splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
+ splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
+ splitContainer1.IsSplitterFixed = true;
+ splitContainer1.Location = new System.Drawing.Point(0, 0);
+ splitContainer1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ splitContainer1.Name = "splitContainer1";
+ splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
//
// splitContainer1.Panel1
//
- this.splitContainer1.Panel1.Controls.Add(this.tableLayoutPanel2);
+ splitContainer1.Panel1.Controls.Add(tableLayoutPanel2);
//
// splitContainer1.Panel2
//
- this.splitContainer1.Panel2.Controls.Add(this.btnCopyAllDiff);
- this.splitContainer1.Panel2.Controls.Add(this.button2);
- this.splitContainer1.Panel2.Controls.Add(this.textBoxSecondDir);
- this.splitContainer1.Panel2.Controls.Add(this.checkBox1);
- this.splitContainer1.Panel2.Controls.Add(this.button1);
- this.splitContainer1.Panel2.Controls.Add(this.comboBoxFileMode);
- this.splitContainer1.Size = new System.Drawing.Size(719, 430);
- this.splitContainer1.SplitterDistance = 370;
- this.splitContainer1.SplitterWidth = 5;
- this.splitContainer1.TabIndex = 10;
+ splitContainer1.Panel2.Controls.Add(btnCopyAllDiff);
+ splitContainer1.Panel2.Controls.Add(button2);
+ splitContainer1.Panel2.Controls.Add(textBoxSecondDir);
+ splitContainer1.Panel2.Controls.Add(checkBox1);
+ splitContainer1.Panel2.Controls.Add(chkMultiSelect);
+ splitContainer1.Panel2.Controls.Add(button1);
+ splitContainer1.Panel2.Controls.Add(comboBoxFileMode);
+ splitContainer1.Size = new System.Drawing.Size(719, 430);
+ splitContainer1.SplitterDistance = 359;
+ splitContainer1.SplitterWidth = 5;
+ splitContainer1.TabIndex = 10;
//
// button2
//
- this.button2.AutoSize = true;
- this.button2.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
- this.button2.Location = new System.Drawing.Point(298, 13);
- this.button2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.button2.Name = "button2";
- this.button2.Size = new System.Drawing.Size(26, 25);
- this.button2.TabIndex = 7;
- this.button2.Text = "...";
- this.button2.UseVisualStyleBackColor = true;
- this.button2.Click += new System.EventHandler(this.BrowseOnClick);
- //
+ button2.AutoSize = true;
+ button2.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
+ button2.Location = new System.Drawing.Point(298, 13);
+ button2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ button2.Name = "button2";
+ button2.Size = new System.Drawing.Size(26, 25);
+ button2.TabIndex = 7;
+ button2.Text = "...";
+ button2.UseVisualStyleBackColor = true;
+ button2.Click += BrowseOnClick;
+ //
// comboBoxFileMode
- //
- this.comboBoxFileMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
- this.comboBoxFileMode.FormattingEnabled = true;
- this.comboBoxFileMode.Items.AddRange(new object[] {
- "Auto",
- "MUL",
- "UOP"});
- this.comboBoxFileMode.Location = new System.Drawing.Point(5, 13);
- this.comboBoxFileMode.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.comboBoxFileMode.Name = "comboBoxFileMode";
- this.comboBoxFileMode.Size = new System.Drawing.Size(70, 23);
- this.comboBoxFileMode.TabIndex = 10;
- //
+ //
+ comboBoxFileMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ comboBoxFileMode.FormattingEnabled = true;
+ comboBoxFileMode.Items.AddRange(new object[] { "Auto", "MUL", "UOP" });
+ comboBoxFileMode.Location = new System.Drawing.Point(5, 13);
+ comboBoxFileMode.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ comboBoxFileMode.Name = "comboBoxFileMode";
+ comboBoxFileMode.Size = new System.Drawing.Size(70, 23);
+ comboBoxFileMode.TabIndex = 10;
+ //
// CompareLandControl
//
- this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.Controls.Add(this.splitContainer1);
- this.DoubleBuffered = true;
- this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- this.Name = "CompareLandControl";
- this.Size = new System.Drawing.Size(719, 430);
- this.Load += new System.EventHandler(this.OnLoad);
- this.tableLayoutPanel1.ResumeLayout(false);
- ((System.ComponentModel.ISupportInitialize)(this.pictureBoxSec)).EndInit();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBoxOrg)).EndInit();
- this.tableLayoutPanel2.ResumeLayout(false);
- this.contextMenuStrip1.ResumeLayout(false);
- this.splitContainer1.Panel1.ResumeLayout(false);
- this.splitContainer1.Panel2.ResumeLayout(false);
- this.splitContainer1.Panel2.PerformLayout();
- ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
- this.splitContainer1.ResumeLayout(false);
- this.ResumeLayout(false);
+ AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ Controls.Add(splitContainer1);
+ DoubleBuffered = true;
+ Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ Name = "CompareLandControl";
+ Size = new System.Drawing.Size(719, 430);
+ Load += OnLoad;
+ tableLayoutPanel1.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)pictureBoxSec).EndInit();
+ ((System.ComponentModel.ISupportInitialize)pictureBoxOrg).EndInit();
+ tableLayoutPanel2.ResumeLayout(false);
+ contextMenuStrip1.ResumeLayout(false);
+ splitContainer1.Panel1.ResumeLayout(false);
+ splitContainer1.Panel2.ResumeLayout(false);
+ splitContainer1.Panel2.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
+ splitContainer1.ResumeLayout(false);
+ ResumeLayout(false);
}
@@ -351,6 +348,7 @@ private void InitializeComponent()
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.CheckBox checkBox1;
+ private System.Windows.Forms.CheckBox chkMultiSelect;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
private System.Windows.Forms.ToolStripMenuItem copyLandTile2To1ToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportImageToolStripMenuItem;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
index 1f6790a..4368bba 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
@@ -14,6 +14,7 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using System.Linq;
using System.Security.Cryptography;
using System.Windows.Forms;
using Ultima;
@@ -40,6 +41,9 @@ public CompareLandControl()
private void OnLoad(object sender, EventArgs e)
{
+ ConfigureTileView(tileViewOrg);
+ ConfigureTileView(tileViewSec);
+
_displayIndices.Clear();
for (int i = 0; i < 0x4000; i++)
{
@@ -49,6 +53,16 @@ private void OnLoad(object sender, EventArgs e)
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = 0;
+ tileViewSec.MultiSelect = true;
+ tileViewSec.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
+ contextMenuStrip1.Opening += (s, ev) =>
+ {
+ int count = tileViewSec.SelectedIndices.Count;
+ copyLandTile2To1ToolStripMenuItem.Text = tileViewSec.ShowCheckBoxes && count > 1
+ ? $"Copy {count} LandTiles to left"
+ : "Copy LandTile to left";
+ };
+
if (comboBoxFileMode.SelectedIndex < 0)
{
comboBoxFileMode.SelectedIndex = 0;
@@ -58,6 +72,61 @@ private void OnLoad(object sender, EventArgs e)
ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
}
+ // TileViewControl exposes TileSize/Margin/Padding/Border with DesignerSerializationVisibility.Hidden,
+ // so VS strips them when re-saving the .Designer.cs. Apply the intended values here so they survive.
+ private static void ConfigureTileView(TileViewControl tv)
+ {
+ tv.TileSize = new Size(tv.TileSize.Width, 20);
+ tv.TileMargin = new Padding(0);
+ tv.TilePadding = new Padding(0);
+ tv.TileBorderWidth = 0f;
+ }
+
+ private void OnChangeMultiSelect(object sender, EventArgs e)
+ {
+ tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ if (!chkMultiSelect.Checked)
+ {
+ tileViewSec.SelectedIndices.Clear();
+ }
+ }
+
+ private void OnSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ if (_syncingSelection)
+ {
+ return;
+ }
+
+ _syncingSelection = true;
+ try
+ {
+ tileViewOrg.SelectedIndices.Clear();
+ foreach (int idx in tileViewSec.SelectedIndices)
+ {
+ tileViewOrg.SelectedIndices.Add(idx);
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private List GetCopyTargets()
+ {
+ var sel = tileViewSec.SelectedIndices;
+ if (sel.Count > 0)
+ {
+ return sel.ToList();
+ }
+ if (tileViewSec.FocusIndex >= 0)
+ {
+ return new List { tileViewSec.FocusIndex };
+ }
+ return new List();
+ }
+
private void OnFilePathChangeEvent()
{
_compare.Clear();
@@ -97,7 +166,7 @@ private void OnDrawItemSec(object sender, TileViewControl.DrawTileListItemEventA
DrawListItem(e, _displayIndices[e.Index], isSecondary: true);
}
- private void DrawListItem(DrawItemEventArgs e, int i, bool isSecondary)
+ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
@@ -122,7 +191,7 @@ private void DrawListItem(DrawItemEventArgs e, int i, bool isSecondary)
string label = $"0x{i:X}";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, Font).Height) / 2f;
- e.Graphics.DrawString(label, Font, fontBrush, new PointF(5, y));
+ e.Graphics.DrawString(label, Font, fontBrush, new PointF(e.ContentLeft + 5, y));
}
private void OnFocusChangedOrg(object sender, TileViewControl.ListViewFocusedItemSelectionChangedEventArgs e)
@@ -355,38 +424,74 @@ private void BrowseOnClick(object sender, EventArgs e)
private void OnClickCopy(object sender, EventArgs e)
{
- int focusIdx = tileViewSec.FocusIndex;
- if (focusIdx < 0)
+ var targets = GetCopyTargets();
+ if (targets.Count == 0)
{
return;
}
- int i = _displayIndices[focusIdx];
- if (!SecondArt.IsValidLand(i))
+ Cursor.Current = Cursors.WaitCursor;
+ int lastCopiedId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- return;
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
+
+ int i = _displayIndices[focusIdx];
+ if (!SecondArt.IsValidLand(i))
+ {
+ continue;
+ }
+
+ Bitmap copy = new Bitmap(SecondArt.GetLand(i));
+ Art.ReplaceLand(i, copy);
+ ControlEvents.FireLandTileChangeEvent(this, i);
+ _compare[i] = true;
+ lastCopiedId = i;
+ changed = true;
}
- Bitmap copy = new Bitmap(SecondArt.GetLand(i));
- Art.ReplaceLand(i, copy);
- Options.ChangedUltimaClass["Art"] = true;
- ControlEvents.FireLandTileChangeEvent(this, i);
- _compare[i] = true;
+ if (changed)
+ {
+ Options.ChangedUltimaClass["Art"] = true;
+ }
- if (checkBox1.Checked)
+ if (checkBox1.Checked && changed)
{
- _displayIndices.RemoveAt(focusIdx);
+ foreach (int idx in targets.OrderByDescending(x => x))
+ {
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
+ }
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = _displayIndices.Count;
}
+ else
+ {
+ tileViewSec.SelectedIndices.Clear();
+ }
tileViewOrg.Invalidate();
tileViewSec.Invalidate();
- pictureBoxOrg.BackgroundImage = Art.IsValidLand(i) ? Art.GetLand(i) : null;
+ if (lastCopiedId >= 0)
+ {
+ pictureBoxOrg.BackgroundImage = Art.IsValidLand(lastCopiedId) ? Art.GetLand(lastCopiedId) : null;
+ }
+ Cursor.Current = Cursors.Default;
}
private void OnDoubleClickSec(object sender, MouseEventArgs e)
{
+ if (tileViewSec.ShowCheckBoxes)
+ {
+ return;
+ }
OnClickCopy(sender, e);
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.resx b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.resx
index 16e4841..75b37ca 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.resx
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.resx
@@ -1,4 +1,64 @@
+
+
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.Designer.cs
index b85b183..ad1bc44 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.Designer.cs
@@ -49,6 +49,7 @@ private void InitializeComponent()
buttonCopyAllDiff = new System.Windows.Forms.Button();
buttonCopySelected = new System.Windows.Forms.Button();
checkBoxShowDiff = new System.Windows.Forms.CheckBox();
+ chkMultiSelect = new System.Windows.Forms.CheckBox();
buttonLoadSecond = new System.Windows.Forms.Button();
buttonBrowse = new System.Windows.Forms.Button();
textBoxSecondFile = new System.Windows.Forms.TextBox();
@@ -89,11 +90,12 @@ private void InitializeComponent()
splitContainer1.Panel2.Controls.Add(buttonCopyAllDiff);
splitContainer1.Panel2.Controls.Add(buttonCopySelected);
splitContainer1.Panel2.Controls.Add(checkBoxShowDiff);
+ splitContainer1.Panel2.Controls.Add(chkMultiSelect);
splitContainer1.Panel2.Controls.Add(buttonLoadSecond);
splitContainer1.Panel2.Controls.Add(buttonBrowse);
splitContainer1.Panel2.Controls.Add(textBoxSecondFile);
splitContainer1.Size = new System.Drawing.Size(940, 510);
- splitContainer1.SplitterDistance = 449;
+ splitContainer1.SplitterDistance = 438;
splitContainer1.SplitterWidth = 5;
splitContainer1.TabIndex = 0;
//
@@ -105,7 +107,7 @@ private void InitializeComponent()
tabControl.Location = new System.Drawing.Point(0, 0);
tabControl.Name = "tabControl";
tabControl.SelectedIndex = 0;
- tabControl.Size = new System.Drawing.Size(940, 449);
+ tabControl.Size = new System.Drawing.Size(940, 438);
tabControl.TabIndex = 0;
tabControl.SelectedIndexChanged += OnTabChanged;
//
@@ -114,7 +116,7 @@ private void InitializeComponent()
tabPageLand.Controls.Add(tableLayoutLand);
tabPageLand.Location = new System.Drawing.Point(4, 24);
tabPageLand.Name = "tabPageLand";
- tabPageLand.Size = new System.Drawing.Size(932, 421);
+ tabPageLand.Size = new System.Drawing.Size(932, 410);
tabPageLand.TabIndex = 0;
tabPageLand.Text = "Land Tiles";
tabPageLand.UseVisualStyleBackColor = true;
@@ -133,29 +135,18 @@ private void InitializeComponent()
tableLayoutLand.Name = "tableLayoutLand";
tableLayoutLand.RowCount = 1;
tableLayoutLand.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
- tableLayoutLand.Size = new System.Drawing.Size(932, 421);
+ tableLayoutLand.Size = new System.Drawing.Size(932, 410);
tableLayoutLand.TabIndex = 0;
//
// tileViewOrg
//
tileViewOrg.ContextMenuStrip = contextMenuStripOrg;
tileViewOrg.Dock = System.Windows.Forms.DockStyle.Fill;
- tileViewOrg.FocusIndex = -1;
tileViewOrg.Location = new System.Drawing.Point(3, 3);
- tileViewOrg.MultiSelect = false;
tileViewOrg.Name = "tileViewOrg";
- tileViewOrg.Size = new System.Drawing.Size(248, 415);
+ tileViewOrg.Size = new System.Drawing.Size(248, 404);
tileViewOrg.TabIndex = 0;
- tileViewOrg.TileBackgroundColor = System.Drawing.SystemColors.Window;
- tileViewOrg.TileBorderColor = System.Drawing.Color.FromArgb(0, 0, 0);
- tileViewOrg.TileBorderWidth = 0F;
- tileViewOrg.TileFocusColor = System.Drawing.Color.DarkRed;
- tileViewOrg.TileHighlightColor = System.Drawing.SystemColors.Highlight;
tileViewOrg.TileHighLightOpacity = 0D;
- tileViewOrg.TileMargin = new System.Windows.Forms.Padding(0);
- tileViewOrg.TilePadding = new System.Windows.Forms.Padding(0);
- tileViewOrg.TileSize = new System.Drawing.Size(248, 15);
- tileViewOrg.VirtualListSize = 0;
tileViewOrg.FocusSelectionChanged += OnFocusChangedLandOrg;
tileViewOrg.DrawItem += OnDrawItemLandOrg;
tileViewOrg.SizeChanged += OnTileViewSizeChanged;
@@ -183,7 +174,7 @@ private void InitializeComponent()
panelDetail.Dock = System.Windows.Forms.DockStyle.Fill;
panelDetail.Location = new System.Drawing.Point(257, 3);
panelDetail.Name = "panelDetail";
- panelDetail.Size = new System.Drawing.Size(417, 415);
+ panelDetail.Size = new System.Drawing.Size(417, 404);
panelDetail.TabIndex = 1;
//
// groupBoxOrg
@@ -320,22 +311,11 @@ private void InitializeComponent()
//
tileViewSec.ContextMenuStrip = contextMenuStripSec;
tileViewSec.Dock = System.Windows.Forms.DockStyle.Fill;
- tileViewSec.FocusIndex = -1;
tileViewSec.Location = new System.Drawing.Point(680, 3);
- tileViewSec.MultiSelect = false;
tileViewSec.Name = "tileViewSec";
- tileViewSec.Size = new System.Drawing.Size(249, 415);
+ tileViewSec.Size = new System.Drawing.Size(249, 404);
tileViewSec.TabIndex = 2;
- tileViewSec.TileBackgroundColor = System.Drawing.SystemColors.Window;
- tileViewSec.TileBorderColor = System.Drawing.Color.FromArgb(0, 0, 0);
- tileViewSec.TileBorderWidth = 0F;
- tileViewSec.TileFocusColor = System.Drawing.Color.DarkRed;
- tileViewSec.TileHighlightColor = System.Drawing.SystemColors.Highlight;
tileViewSec.TileHighLightOpacity = 0D;
- tileViewSec.TileMargin = new System.Windows.Forms.Padding(0);
- tileViewSec.TilePadding = new System.Windows.Forms.Padding(0);
- tileViewSec.TileSize = new System.Drawing.Size(248, 15);
- tileViewSec.VirtualListSize = 0;
tileViewSec.FocusSelectionChanged += OnFocusChangedLandSec;
tileViewSec.DrawItem += OnDrawItemLandSec;
tileViewSec.SizeChanged += OnTileViewSizeChanged;
@@ -345,13 +325,13 @@ private void InitializeComponent()
//
contextMenuStripSec.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { copyEntry2To1ToolStripMenuItem });
contextMenuStripSec.Name = "contextMenuStripSec";
- contextMenuStripSec.Size = new System.Drawing.Size(165, 26);
+ contextMenuStripSec.Size = new System.Drawing.Size(167, 26);
//
// copyEntry2To1ToolStripMenuItem
//
copyEntry2To1ToolStripMenuItem.Name = "copyEntry2To1ToolStripMenuItem";
- copyEntry2To1ToolStripMenuItem.Size = new System.Drawing.Size(164, 22);
- copyEntry2To1ToolStripMenuItem.Text = "Copy Entry 2 to 1";
+ copyEntry2To1ToolStripMenuItem.Size = new System.Drawing.Size(166, 22);
+ copyEntry2To1ToolStripMenuItem.Text = "Copy Entry to left";
copyEntry2To1ToolStripMenuItem.Click += OnClickCopySelected;
//
// tabPageItem
@@ -384,22 +364,11 @@ private void InitializeComponent()
//
tileViewItemOrg.ContextMenuStrip = contextMenuStripOrg;
tileViewItemOrg.Dock = System.Windows.Forms.DockStyle.Fill;
- tileViewItemOrg.FocusIndex = -1;
tileViewItemOrg.Location = new System.Drawing.Point(3, 3);
- tileViewItemOrg.MultiSelect = false;
tileViewItemOrg.Name = "tileViewItemOrg";
tileViewItemOrg.Size = new System.Drawing.Size(248, 415);
tileViewItemOrg.TabIndex = 0;
- tileViewItemOrg.TileBackgroundColor = System.Drawing.SystemColors.Window;
- tileViewItemOrg.TileBorderColor = System.Drawing.Color.FromArgb(0, 0, 0);
- tileViewItemOrg.TileBorderWidth = 0F;
- tileViewItemOrg.TileFocusColor = System.Drawing.Color.DarkRed;
- tileViewItemOrg.TileHighlightColor = System.Drawing.SystemColors.Highlight;
tileViewItemOrg.TileHighLightOpacity = 0D;
- tileViewItemOrg.TileMargin = new System.Windows.Forms.Padding(0);
- tileViewItemOrg.TilePadding = new System.Windows.Forms.Padding(0);
- tileViewItemOrg.TileSize = new System.Drawing.Size(248, 15);
- tileViewItemOrg.VirtualListSize = 0;
tileViewItemOrg.FocusSelectionChanged += OnFocusChangedItemOrg;
tileViewItemOrg.DrawItem += OnDrawItemItemOrg;
tileViewItemOrg.SizeChanged += OnTileViewSizeChanged;
@@ -409,22 +378,11 @@ private void InitializeComponent()
//
tileViewItemSec.ContextMenuStrip = contextMenuStripSec;
tileViewItemSec.Dock = System.Windows.Forms.DockStyle.Fill;
- tileViewItemSec.FocusIndex = -1;
tileViewItemSec.Location = new System.Drawing.Point(680, 3);
- tileViewItemSec.MultiSelect = false;
tileViewItemSec.Name = "tileViewItemSec";
tileViewItemSec.Size = new System.Drawing.Size(249, 415);
tileViewItemSec.TabIndex = 2;
- tileViewItemSec.TileBackgroundColor = System.Drawing.SystemColors.Window;
- tileViewItemSec.TileBorderColor = System.Drawing.Color.FromArgb(0, 0, 0);
- tileViewItemSec.TileBorderWidth = 0F;
- tileViewItemSec.TileFocusColor = System.Drawing.Color.DarkRed;
- tileViewItemSec.TileHighlightColor = System.Drawing.SystemColors.Highlight;
tileViewItemSec.TileHighLightOpacity = 0D;
- tileViewItemSec.TileMargin = new System.Windows.Forms.Padding(0);
- tileViewItemSec.TilePadding = new System.Windows.Forms.Padding(0);
- tileViewItemSec.TileSize = new System.Drawing.Size(248, 15);
- tileViewItemSec.VirtualListSize = 0;
tileViewItemSec.FocusSelectionChanged += OnFocusChangedItemSec;
tileViewItemSec.DrawItem += OnDrawItemItemSec;
tileViewItemSec.SizeChanged += OnTileViewSizeChanged;
@@ -461,6 +419,17 @@ private void InitializeComponent()
checkBoxShowDiff.UseVisualStyleBackColor = true;
checkBoxShowDiff.Click += OnChangeShowDiff;
//
+ // chkMultiSelect
+ //
+ chkMultiSelect.AutoSize = true;
+ chkMultiSelect.Location = new System.Drawing.Point(408, 38);
+ chkMultiSelect.Name = "chkMultiSelect";
+ chkMultiSelect.Size = new System.Drawing.Size(90, 19);
+ chkMultiSelect.TabIndex = 10;
+ chkMultiSelect.Text = "Multi-Select";
+ chkMultiSelect.UseVisualStyleBackColor = true;
+ chkMultiSelect.CheckedChanged += OnChangeMultiSelect;
+ //
// buttonLoadSecond
//
buttonLoadSecond.AutoSize = true;
@@ -558,6 +527,7 @@ private void InitializeComponent()
private System.Windows.Forms.Button buttonBrowse;
private System.Windows.Forms.Button buttonLoadSecond;
private System.Windows.Forms.CheckBox checkBoxShowDiff;
+ private System.Windows.Forms.CheckBox chkMultiSelect;
private System.Windows.Forms.Button buttonCopySelected;
private System.Windows.Forms.Button buttonCopyAllDiff;
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
index c142bd7..61f6fed 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Drawing;
+using System.Linq;
using System.Windows.Forms;
using Ultima;
using UoFiddler.Controls.Classes;
@@ -33,10 +34,94 @@ private void OnLoad(object sender, EventArgs e)
{
legendSwatchDifferent.BackColor = Color.CornflowerBlue;
}
+ ConfigureTileView(tileViewOrg);
+ ConfigureTileView(tileViewSec);
+ ConfigureTileView(tileViewItemOrg);
+ ConfigureTileView(tileViewItemSec);
PopulateOrgOnly(isLand: true);
+
+ tileViewSec.MultiSelect = true;
+ tileViewItemSec.MultiSelect = true;
+ tileViewSec.SelectedIndices.CollectionChanged += OnLandSecSelectedIndicesChanged;
+ tileViewItemSec.SelectedIndices.CollectionChanged += OnItemSecSelectedIndicesChanged;
+ contextMenuStripSec.Opening += (s, ev) =>
+ {
+ int count = ActiveSecView.SelectedIndices.Count;
+ copyEntry2To1ToolStripMenuItem.Text = ActiveSecView.ShowCheckBoxes && count > 1
+ ? $"Copy {count} Entries to left"
+ : "Copy Entry to left";
+ };
+
ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
}
+ // TileViewControl exposes TileSize/Margin/Padding/Border with DesignerSerializationVisibility.Hidden,
+ // so VS strips them when re-saving the .Designer.cs. Apply the intended values here so they survive.
+ private static void ConfigureTileView(TileViewControl tv)
+ {
+ tv.TileSize = new Size(tv.TileSize.Width, 20);
+ tv.TileMargin = new Padding(0);
+ tv.TilePadding = new Padding(0);
+ tv.TileBorderWidth = 0f;
+ }
+
+ private void OnChangeMultiSelect(object sender, EventArgs e)
+ {
+ tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileViewItemSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ if (!chkMultiSelect.Checked)
+ {
+ tileViewSec.SelectedIndices.Clear();
+ tileViewItemSec.SelectedIndices.Clear();
+ }
+ }
+
+ private void OnLandSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ MirrorSelection(tileViewSec, tileViewOrg);
+ }
+
+ private void OnItemSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ MirrorSelection(tileViewItemSec, tileViewItemOrg);
+ }
+
+ private void MirrorSelection(TileViewControl source, TileViewControl target)
+ {
+ if (_syncingSelection)
+ {
+ return;
+ }
+
+ _syncingSelection = true;
+ try
+ {
+ target.SelectedIndices.Clear();
+ foreach (int idx in source.SelectedIndices)
+ {
+ target.SelectedIndices.Add(idx);
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private List GetCopyTargets(TileViewControl secView)
+ {
+ var sel = secView.SelectedIndices;
+ if (sel.Count > 0)
+ {
+ return sel.ToList();
+ }
+ if (secView.FocusIndex >= 0)
+ {
+ return new List { secView.FocusIndex };
+ }
+ return new List();
+ }
+
private void OnFilePathChangeEvent()
{
_compare.Clear();
@@ -148,7 +233,7 @@ private void OnDrawItemItemOrg(object sender, TileViewControl.DrawTileListItemEv
private void OnDrawItemItemSec(object sender, TileViewControl.DrawTileListItemEventArgs e)
=> DrawListItem(e, _itemDisplayIndices[e.Index]);
- private void DrawListItem(DrawItemEventArgs e, int idx)
+ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int idx)
{
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
@@ -166,7 +251,7 @@ private void DrawListItem(DrawItemEventArgs e, int idx)
string section = idx < 0x4000 ? "Land" : "Item";
string text = $"0x{idx:X4} [{section}]";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(text, e.Font).Height) / 2f;
- e.Graphics.DrawString(text, e.Font, fontBrush, new PointF(4, y));
+ e.Graphics.DrawString(text, e.Font, fontBrush, new PointF(e.ContentLeft + 4, y));
}
private void OnFocusChangedLandOrg(object sender, TileViewControl.ListViewFocusedItemSelectionChangedEventArgs e)
@@ -377,34 +462,69 @@ private bool IsDifferent(int idx)
return !same;
}
- private void OnDoubleClickSec(object sender, MouseEventArgs e) => OnClickCopySelected(sender, e);
+ private void OnDoubleClickSec(object sender, MouseEventArgs e)
+ {
+ if (ActiveSecView.ShowCheckBoxes)
+ {
+ return;
+ }
+ OnClickCopySelected(sender, e);
+ }
private void OnDoubleClickOrg(object sender, MouseEventArgs e) => OnClickCopy1To2(sender, e);
private void OnClickCopySelected(object sender, EventArgs e)
{
var secView = ActiveSecView;
- if (secView.FocusIndex < 0)
+ var orgView = ActiveOrgView;
+ var indices = ActiveIndices;
+
+ var targets = GetCopyTargets(secView);
+ if (targets.Count == 0)
{
return;
}
- int idx = ActiveIndices[secView.FocusIndex];
- CopySecToOrg(idx);
+ Cursor.Current = Cursors.WaitCursor;
+ int lastIdx = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
+ {
+ if (focusIdx < 0 || focusIdx >= indices.Count)
+ {
+ continue;
+ }
- if (checkBoxShowDiff.Checked)
+ int idx = indices[focusIdx];
+ CopySecToOrg(idx);
+ lastIdx = idx;
+ changed = true;
+ }
+
+ if (checkBoxShowDiff.Checked && changed)
{
- int displayIdx = ActiveIndices.IndexOf(idx);
- if (displayIdx >= 0)
+ foreach (int displayIdx in targets.OrderByDescending(x => x))
{
- ActiveIndices.RemoveAt(displayIdx);
- ActiveOrgView.VirtualListSize = ActiveIndices.Count;
- secView.VirtualListSize = ActiveIndices.Count;
+ if (displayIdx >= 0 && displayIdx < indices.Count)
+ {
+ indices.RemoveAt(displayIdx);
+ }
}
+ orgView.VirtualListSize = indices.Count;
+ secView.VirtualListSize = indices.Count;
+ }
+ else
+ {
+ secView.SelectedIndices.Clear();
}
- ActiveOrgView.Invalidate();
+ orgView.Invalidate();
secView.Invalidate();
- UpdateDetailPanel(idx);
+ if (lastIdx >= 0)
+ {
+ UpdateDetailPanel(lastIdx);
+ }
+ Cursor.Current = Cursors.Default;
}
private void OnClickCopy1To2(object sender, EventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.Designer.cs
index afdf996..4b3cddc 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.Designer.cs
@@ -55,6 +55,7 @@ private void InitializeComponent()
asPngToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
copyLandTile2To1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
checkBox1 = new System.Windows.Forms.CheckBox();
+ chkMultiSelect = new System.Windows.Forms.CheckBox();
button1 = new System.Windows.Forms.Button();
splitContainer1 = new System.Windows.Forms.SplitContainer();
CopyAddOnly = new System.Windows.Forms.Button();
@@ -79,7 +80,7 @@ private void InitializeComponent()
tileViewOrg.Name = "tileViewOrg";
tileViewOrg.Size = new System.Drawing.Size(189, 363);
tileViewOrg.TabIndex = 0;
- tileViewOrg.TileSize = new System.Drawing.Size(189, 13);
+ tileViewOrg.TileSize = new System.Drawing.Size(189, 20);
tileViewOrg.TileMargin = new System.Windows.Forms.Padding(0);
tileViewOrg.TilePadding = new System.Windows.Forms.Padding(0);
tileViewOrg.TileBorderWidth = 0f;
@@ -162,7 +163,7 @@ private void InitializeComponent()
tileViewSec.Name = "tileViewSec";
tileViewSec.Size = new System.Drawing.Size(190, 363);
tileViewSec.TabIndex = 1;
- tileViewSec.TileSize = new System.Drawing.Size(190, 13);
+ tileViewSec.TileSize = new System.Drawing.Size(190, 20);
tileViewSec.TileMargin = new System.Windows.Forms.Padding(0);
tileViewSec.TilePadding = new System.Windows.Forms.Padding(0);
tileViewSec.TileBorderWidth = 0f;
@@ -217,7 +218,7 @@ private void InitializeComponent()
//
copyLandTile2To1ToolStripMenuItem.Name = "copyLandTile2To1ToolStripMenuItem";
copyLandTile2To1ToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
- copyLandTile2To1ToolStripMenuItem.Text = "Copy LandTile 2 to 1";
+ copyLandTile2To1ToolStripMenuItem.Text = "Copy Texture to left";
copyLandTile2To1ToolStripMenuItem.Click += OnClickCopy;
//
// checkBox1
@@ -231,7 +232,19 @@ private void InitializeComponent()
checkBox1.Text = "Show only Differences";
checkBox1.UseVisualStyleBackColor = true;
checkBox1.Click += OnChangeShowDiff;
- //
+ //
+ // chkMultiSelect
+ //
+ chkMultiSelect.AutoSize = true;
+ chkMultiSelect.Location = new System.Drawing.Point(343, 38);
+ chkMultiSelect.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ chkMultiSelect.Name = "chkMultiSelect";
+ chkMultiSelect.Size = new System.Drawing.Size(90, 19);
+ chkMultiSelect.TabIndex = 10;
+ chkMultiSelect.Text = "Multi-Select";
+ chkMultiSelect.UseVisualStyleBackColor = true;
+ chkMultiSelect.CheckedChanged += OnChangeMultiSelect;
+ //
// button1
//
button1.AutoSize = true;
@@ -266,6 +279,7 @@ private void InitializeComponent()
splitContainer1.Panel2.Controls.Add(button2);
splitContainer1.Panel2.Controls.Add(textBoxSecondDir);
splitContainer1.Panel2.Controls.Add(checkBox1);
+ splitContainer1.Panel2.Controls.Add(chkMultiSelect);
splitContainer1.Panel2.Controls.Add(button1);
splitContainer1.Size = new System.Drawing.Size(724, 434);
splitContainer1.SplitterDistance = 369;
@@ -341,6 +355,7 @@ private void InitializeComponent()
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel2;
private UoFiddler.Controls.UserControls.TileView.TileViewControl tileViewSec;
private System.Windows.Forms.CheckBox checkBox1;
+ private System.Windows.Forms.CheckBox chkMultiSelect;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.SplitContainer splitContainer1;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
index cfb583a..c1701b4 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
@@ -14,6 +14,7 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using System.Linq;
using System.Security.Cryptography;
using System.Windows.Forms;
using Ultima;
@@ -40,6 +41,9 @@ public CompareTextureControl()
private void OnLoad(object sender, EventArgs e)
{
+ ConfigureTileView(tileViewOrg);
+ ConfigureTileView(tileViewSec);
+
_displayIndices.Clear();
for (int i = 0; i < 0x4000; i++)
{
@@ -49,9 +53,74 @@ private void OnLoad(object sender, EventArgs e)
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = 0;
+ tileViewSec.MultiSelect = true;
+ tileViewSec.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
+ contextMenuStrip1.Opening += (s, ev) =>
+ {
+ int count = tileViewSec.SelectedIndices.Count;
+ copyLandTile2To1ToolStripMenuItem.Text = tileViewSec.ShowCheckBoxes && count > 1
+ ? $"Copy {count} Textures to left"
+ : "Copy Texture to left";
+ };
+
ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
}
+ // TileViewControl exposes TileSize/Margin/Padding/Border with DesignerSerializationVisibility.Hidden,
+ // so VS strips them when re-saving the .Designer.cs. Apply the intended values here so they survive.
+ private static void ConfigureTileView(TileViewControl tv)
+ {
+ tv.TileSize = new Size(tv.TileSize.Width, 20);
+ tv.TileMargin = new Padding(0);
+ tv.TilePadding = new Padding(0);
+ tv.TileBorderWidth = 0f;
+ }
+
+ private void OnChangeMultiSelect(object sender, EventArgs e)
+ {
+ tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ if (!chkMultiSelect.Checked)
+ {
+ tileViewSec.SelectedIndices.Clear();
+ }
+ }
+
+ private void OnSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ if (_syncingSelection)
+ {
+ return;
+ }
+
+ _syncingSelection = true;
+ try
+ {
+ tileViewOrg.SelectedIndices.Clear();
+ foreach (int idx in tileViewSec.SelectedIndices)
+ {
+ tileViewOrg.SelectedIndices.Add(idx);
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private List GetCopyTargets()
+ {
+ var sel = tileViewSec.SelectedIndices;
+ if (sel.Count > 0)
+ {
+ return sel.ToList();
+ }
+ if (tileViewSec.FocusIndex >= 0)
+ {
+ return new List { tileViewSec.FocusIndex };
+ }
+ return new List();
+ }
+
private void OnFilePathChangeEvent()
{
_compare.Clear();
@@ -79,7 +148,7 @@ private void OnDrawItemSec(object sender, TileViewControl.DrawTileListItemEventA
DrawListItem(e, _displayIndices[e.Index], isSecondary: true);
}
- private void DrawListItem(DrawItemEventArgs e, int i, bool isSecondary)
+ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
@@ -104,7 +173,7 @@ private void DrawListItem(DrawItemEventArgs e, int i, bool isSecondary)
string label = $"0x{i:X}";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, Font).Height) / 2f;
- e.Graphics.DrawString(label, Font, fontBrush, new PointF(5, y));
+ e.Graphics.DrawString(label, Font, fontBrush, new PointF(e.ContentLeft + 5, y));
}
private void OnFocusChangedOrg(object sender, TileViewControl.ListViewFocusedItemSelectionChangedEventArgs e)
@@ -328,38 +397,74 @@ private void BrowseOnClick(object sender, EventArgs e)
private void OnClickCopy(object sender, EventArgs e)
{
- int focusIdx = tileViewSec.FocusIndex;
- if (focusIdx < 0)
+ var targets = GetCopyTargets();
+ if (targets.Count == 0)
{
return;
}
- int i = _displayIndices[focusIdx];
- if (!SecondTexture.IsValidTexture(i))
+ Cursor.Current = Cursors.WaitCursor;
+ int lastCopiedId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- return;
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
+
+ int i = _displayIndices[focusIdx];
+ if (!SecondTexture.IsValidTexture(i))
+ {
+ continue;
+ }
+
+ Bitmap copy = new Bitmap(SecondTexture.GetTexture(i));
+ Textures.Replace(i, copy);
+ ControlEvents.FireTextureChangeEvent(this, i);
+ _compare[i] = true;
+ lastCopiedId = i;
+ changed = true;
}
- Bitmap copy = new Bitmap(SecondTexture.GetTexture(i));
- Textures.Replace(i, copy);
- Options.ChangedUltimaClass["Texture"] = true;
- ControlEvents.FireTextureChangeEvent(this, i);
- _compare[i] = true;
+ if (changed)
+ {
+ Options.ChangedUltimaClass["Texture"] = true;
+ }
- if (checkBox1.Checked)
+ if (checkBox1.Checked && changed)
{
- _displayIndices.RemoveAt(focusIdx);
+ foreach (int idx in targets.OrderByDescending(x => x))
+ {
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
+ }
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = _displayIndices.Count;
}
+ else
+ {
+ tileViewSec.SelectedIndices.Clear();
+ }
tileViewOrg.Invalidate();
tileViewSec.Invalidate();
- pictureBoxOrg.BackgroundImage = Textures.TestTexture(i) ? Textures.GetTexture(i) : null;
+ if (lastCopiedId >= 0)
+ {
+ pictureBoxOrg.BackgroundImage = Textures.TestTexture(lastCopiedId) ? Textures.GetTexture(lastCopiedId) : null;
+ }
+ Cursor.Current = Cursors.Default;
}
private void CopyToLeft_Click(object sender, MouseEventArgs e)
{
+ if (tileViewSec.ShowCheckBoxes)
+ {
+ return;
+ }
OnClickCopy(sender, e);
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.Designer.cs
index 737fd38..734c0d6 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.Designer.cs
@@ -23,6 +23,7 @@ private void InitializeComponent()
btnBrowse = new System.Windows.Forms.Button();
btnLoad = new System.Windows.Forms.Button();
chkShowDiff = new System.Windows.Forms.CheckBox();
+ chkMultiSelect = new System.Windows.Forms.CheckBox();
btnToggleRules = new System.Windows.Forms.Button();
panelRules = new System.Windows.Forms.Panel();
gbLandFields = new System.Windows.Forms.GroupBox();
@@ -149,6 +150,7 @@ private void InitializeComponent()
panelTop.Controls.Add(btnBrowse);
panelTop.Controls.Add(btnLoad);
panelTop.Controls.Add(chkShowDiff);
+ panelTop.Controls.Add(chkMultiSelect);
panelTop.Controls.Add(btnToggleRules);
panelTop.Dock = System.Windows.Forms.DockStyle.Bottom;
panelTop.Location = new System.Drawing.Point(0, 591);
@@ -200,6 +202,17 @@ private void InitializeComponent()
chkShowDiff.TabIndex = 4;
chkShowDiff.Text = "Show Differences Only";
chkShowDiff.CheckedChanged += OnChangeShowDiff;
+ //
+ // chkMultiSelect
+ //
+ chkMultiSelect.AutoSize = true;
+ chkMultiSelect.Location = new System.Drawing.Point(737, 6);
+ chkMultiSelect.Name = "chkMultiSelect";
+ chkMultiSelect.Size = new System.Drawing.Size(90, 19);
+ chkMultiSelect.TabIndex = 6;
+ chkMultiSelect.Text = "Multi-Select";
+ chkMultiSelect.UseVisualStyleBackColor = true;
+ chkMultiSelect.CheckedChanged += OnChangeMultiSelect;
//
// btnToggleRules
//
@@ -525,7 +538,7 @@ private void InitializeComponent()
tileViewLandOrg.Name = "tileViewLandOrg";
tileViewLandOrg.Size = new System.Drawing.Size(280, 413);
tileViewLandOrg.TabIndex = 0;
- tileViewLandOrg.TileSize = new System.Drawing.Size(280, 15);
+ tileViewLandOrg.TileSize = new System.Drawing.Size(280, 20);
tileViewLandOrg.TileMargin = new System.Windows.Forms.Padding(0);
tileViewLandOrg.TilePadding = new System.Windows.Forms.Padding(0);
tileViewLandOrg.TileBorderWidth = 0f;
@@ -617,7 +630,7 @@ private void InitializeComponent()
tileViewLandSec.Name = "tileViewLandSec";
tileViewLandSec.Size = new System.Drawing.Size(57, 413);
tileViewLandSec.TabIndex = 0;
- tileViewLandSec.TileSize = new System.Drawing.Size(57, 15);
+ tileViewLandSec.TileSize = new System.Drawing.Size(57, 20);
tileViewLandSec.TileMargin = new System.Windows.Forms.Padding(0);
tileViewLandSec.TilePadding = new System.Windows.Forms.Padding(0);
tileViewLandSec.TileBorderWidth = 0f;
@@ -663,7 +676,7 @@ private void InitializeComponent()
tileViewItemOrg.Name = "tileViewItemOrg";
tileViewItemOrg.Size = new System.Drawing.Size(280, 413);
tileViewItemOrg.TabIndex = 0;
- tileViewItemOrg.TileSize = new System.Drawing.Size(280, 15);
+ tileViewItemOrg.TileSize = new System.Drawing.Size(280, 20);
tileViewItemOrg.TileMargin = new System.Windows.Forms.Padding(0);
tileViewItemOrg.TilePadding = new System.Windows.Forms.Padding(0);
tileViewItemOrg.TileBorderWidth = 0f;
@@ -766,7 +779,7 @@ private void InitializeComponent()
tileViewItemSec.Name = "tileViewItemSec";
tileViewItemSec.Size = new System.Drawing.Size(121, 413);
tileViewItemSec.TabIndex = 0;
- tileViewItemSec.TileSize = new System.Drawing.Size(121, 15);
+ tileViewItemSec.TileSize = new System.Drawing.Size(121, 20);
tileViewItemSec.TileMargin = new System.Windows.Forms.Padding(0);
tileViewItemSec.TilePadding = new System.Windows.Forms.Padding(0);
tileViewItemSec.TileBorderWidth = 0f;
@@ -1174,6 +1187,7 @@ private static void AddDetailRow(
private System.Windows.Forms.Button btnBrowse;
private System.Windows.Forms.Button btnLoad;
private System.Windows.Forms.CheckBox chkShowDiff;
+ private System.Windows.Forms.CheckBox chkMultiSelect;
private System.Windows.Forms.Button btnToggleRules;
private System.Windows.Forms.Panel panelRules;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
index b7afd9a..c57d584 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Drawing;
using System.IO;
+using System.Linq;
using System.Windows.Forms;
using Ultima;
using UoFiddler.Controls.Classes;
@@ -89,11 +90,88 @@ private static readonly (string Name, TileFlag Flag)[] MeaningfulFlags =
private void OnLoad(object sender, EventArgs e)
{
+ ConfigureTileView(tileViewLandOrg);
+ ConfigureTileView(tileViewLandSec);
+ ConfigureTileView(tileViewItemOrg);
+ ConfigureTileView(tileViewItemSec);
+
SetupDetailPanels();
PopulateItemOrg();
PopulateLandOrg();
BuildRulesPanel();
SetInnerSplitterPositions();
+
+ tileViewLandSec.MultiSelect = true;
+ tileViewItemSec.MultiSelect = true;
+ tileViewLandSec.SelectedIndices.CollectionChanged += OnLandSecSelectedIndicesChanged;
+ tileViewItemSec.SelectedIndices.CollectionChanged += OnItemSecSelectedIndicesChanged;
+ }
+
+ // TileViewControl exposes TileSize/Margin/Padding/Border with DesignerSerializationVisibility.Hidden,
+ // so VS strips them when re-saving the .Designer.cs. Apply the intended values here so they survive.
+ private static void ConfigureTileView(TileViewControl tv)
+ {
+ tv.TileSize = new Size(tv.TileSize.Width, 20);
+ tv.TileMargin = new Padding(0);
+ tv.TilePadding = new Padding(0);
+ tv.TileBorderWidth = 0f;
+ }
+
+ private void OnChangeMultiSelect(object sender, EventArgs e)
+ {
+ tileViewLandSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileViewItemSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ if (!chkMultiSelect.Checked)
+ {
+ tileViewLandSec.SelectedIndices.Clear();
+ tileViewItemSec.SelectedIndices.Clear();
+ }
+ }
+
+ private void OnLandSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ MirrorSelection(tileViewLandSec, tileViewLandOrg);
+ }
+
+ private void OnItemSecSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ MirrorSelection(tileViewItemSec, tileViewItemOrg);
+ }
+
+ private void MirrorSelection(TileViewControl source, TileViewControl target)
+ {
+ if (_syncingSelection)
+ {
+ return;
+ }
+
+ _syncingSelection = true;
+ try
+ {
+ target.SelectedIndices.Clear();
+ foreach (int idx in source.SelectedIndices)
+ {
+ target.SelectedIndices.Add(idx);
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private List GetCopyTargets(TileViewControl secView)
+ {
+ var sel = secView.SelectedIndices;
+ if (sel.Count > 0)
+ {
+ return sel.ToList();
+ }
+ if (secView.FocusIndex >= 0)
+ {
+ return new List { secView.FocusIndex };
+ }
+ return new List();
}
private void SetupDetailPanels()
@@ -347,17 +425,25 @@ private void OnClickLoad(object sender, EventArgs e)
}
string tileFile = Path.Combine(path, "tiledata.mul");
- string artFile = Path.Combine(path, "art.mul");
- string artIdx = Path.Combine(path, "artidx.mul");
-
- if (!File.Exists(tileFile) || !File.Exists(artFile) || !File.Exists(artIdx))
+ if (!File.Exists(tileFile))
{
- MessageBox.Show("Could not find tiledata.mul, art.mul and artidx.mul in the selected directory.",
+ MessageBox.Show("Could not find tiledata.mul in the selected directory.",
"Missing Files", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
- SecondArt.SetFileIndex(artIdx, artFile);
+ string mulFile = Path.Combine(path, "art.mul");
+ string idxFile = Path.Combine(path, "artidx.mul");
+ string uopFile = Path.Combine(path, "artLegacyMUL.uop");
+
+ if (!SecondLoadHelper.TryResolveArtPaths("Auto", idxFile, mulFile, uopFile,
+ out string resolvedIdx, out string resolvedMul, out string resolvedUop, out string error))
+ {
+ MessageBox.Show(error, "Missing Files", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+
+ SecondArt.SetFileIndex(resolvedIdx, resolvedMul, resolvedUop);
_secondTileData = new SecondTileData();
_secondTileData.Initialize(tileFile, SecondArt.IsUOAHS());
@@ -635,7 +721,7 @@ private void OnDrawItemLandSec(object sender, TileViewControl.DrawTileListItemEv
DrawLandItem(e, _landDisplayIndices[e.Index], isSecondary: true);
}
- private void DrawLandItem(DrawItemEventArgs e, int i, bool isSecondary)
+ private void DrawLandItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
@@ -650,7 +736,7 @@ private void DrawLandItem(DrawItemEventArgs e, int i, bool isSecondary)
string label = GetLandLabel(i, isSecondary);
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, e.Font).Height) / 2f;
- e.Graphics.DrawString(label, e.Font, brush, new PointF(4, y));
+ e.Graphics.DrawString(label, e.Font, brush, new PointF(e.ContentLeft + 4, y));
}
private Brush GetLandBrush(int i, bool isSecondary)
@@ -716,7 +802,7 @@ private void OnDrawItemItemSec(object sender, TileViewControl.DrawTileListItemEv
DrawItemEntry(e, _itemDisplayIndices[e.Index], isSecondary: true);
}
- private void DrawItemEntry(DrawItemEventArgs e, int i, bool isSecondary)
+ private void DrawItemEntry(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
@@ -731,7 +817,7 @@ private void DrawItemEntry(DrawItemEventArgs e, int i, bool isSecondary)
string label = GetItemLabel(i, isSecondary);
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, e.Font).Height) / 2f;
- e.Graphics.DrawString(label, e.Font, brush, new PointF(4, y));
+ e.Graphics.DrawString(label, e.Font, brush, new PointF(e.ContentLeft + 4, y));
}
private Brush GetItemBrush(int i)
@@ -1077,28 +1163,56 @@ private static void Highlight(bool differs, TextBox orgBox, TextBox secBox)
private void OnClickCopyLandSelected(object sender, EventArgs e)
{
- if (_secondTileData == null || tileViewLandSec.FocusIndex < 0)
+ if (_secondTileData == null)
{
return;
}
- int id = _landDisplayIndices[tileViewLandSec.FocusIndex];
- CopyLandEntry(id);
+ var targets = GetCopyTargets(tileViewLandSec);
+ if (targets.Count == 0)
+ {
+ return;
+ }
- if (chkShowDiff.Checked)
+ Cursor.Current = Cursors.WaitCursor;
+ int lastId = -1;
+
+ foreach (int focusIdx in targets)
+ {
+ if (focusIdx < 0 || focusIdx >= _landDisplayIndices.Count)
+ {
+ continue;
+ }
+
+ int id = _landDisplayIndices[focusIdx];
+ CopyLandEntry(id);
+ lastId = id;
+ }
+
+ if (chkShowDiff.Checked && lastId >= 0)
{
- int displayIdx = _landDisplayIndices.IndexOf(id);
- if (displayIdx >= 0)
+ foreach (int displayIdx in targets.OrderByDescending(x => x))
{
- _landDisplayIndices.RemoveAt(displayIdx);
- tileViewLandOrg.VirtualListSize = _landDisplayIndices.Count;
- tileViewLandSec.VirtualListSize = _landDisplayIndices.Count;
+ if (displayIdx >= 0 && displayIdx < _landDisplayIndices.Count)
+ {
+ _landDisplayIndices.RemoveAt(displayIdx);
+ }
}
+ tileViewLandOrg.VirtualListSize = _landDisplayIndices.Count;
+ tileViewLandSec.VirtualListSize = _landDisplayIndices.Count;
+ }
+ else
+ {
+ tileViewLandSec.SelectedIndices.Clear();
}
tileViewLandOrg.Invalidate();
tileViewLandSec.Invalidate();
- UpdateLandDetail(id);
+ if (lastId >= 0)
+ {
+ UpdateLandDetail(lastId);
+ }
+ Cursor.Current = Cursors.Default;
}
private void OnClickCopyLandAllDiff(object sender, EventArgs e)
@@ -1143,28 +1257,56 @@ private void CopyLandEntry(int id)
private void OnClickCopyItemSelected(object sender, EventArgs e)
{
- if (_secondTileData == null || tileViewItemSec.FocusIndex < 0)
+ if (_secondTileData == null)
{
return;
}
- int id = _itemDisplayIndices[tileViewItemSec.FocusIndex];
- CopyItemEntry(id);
+ var targets = GetCopyTargets(tileViewItemSec);
+ if (targets.Count == 0)
+ {
+ return;
+ }
- if (chkShowDiff.Checked)
+ Cursor.Current = Cursors.WaitCursor;
+ int lastId = -1;
+
+ foreach (int focusIdx in targets)
{
- int displayIdx = _itemDisplayIndices.IndexOf(id);
- if (displayIdx >= 0)
+ if (focusIdx < 0 || focusIdx >= _itemDisplayIndices.Count)
{
- _itemDisplayIndices.RemoveAt(displayIdx);
- tileViewItemOrg.VirtualListSize = _itemDisplayIndices.Count;
- tileViewItemSec.VirtualListSize = _itemDisplayIndices.Count;
+ continue;
}
+
+ int id = _itemDisplayIndices[focusIdx];
+ CopyItemEntry(id);
+ lastId = id;
+ }
+
+ if (chkShowDiff.Checked && lastId >= 0)
+ {
+ foreach (int displayIdx in targets.OrderByDescending(x => x))
+ {
+ if (displayIdx >= 0 && displayIdx < _itemDisplayIndices.Count)
+ {
+ _itemDisplayIndices.RemoveAt(displayIdx);
+ }
+ }
+ tileViewItemOrg.VirtualListSize = _itemDisplayIndices.Count;
+ tileViewItemSec.VirtualListSize = _itemDisplayIndices.Count;
+ }
+ else
+ {
+ tileViewItemSec.SelectedIndices.Clear();
}
tileViewItemOrg.Invalidate();
tileViewItemSec.Invalidate();
- UpdateItemDetail(id);
+ if (lastId >= 0)
+ {
+ UpdateItemDetail(lastId);
+ }
+ Cursor.Current = Cursors.Default;
}
private void OnClickCopyItemAllDiff(object sender, EventArgs e)
@@ -1196,11 +1338,19 @@ private void OnClickCopyItemAllDiff(object sender, EventArgs e)
private void OnDoubleClickItemSec(object sender, MouseEventArgs e)
{
+ if (tileViewItemSec.ShowCheckBoxes)
+ {
+ return;
+ }
OnClickCopyItemSelected(sender, e);
}
private void OnDoubleClickLandSec(object sender, MouseEventArgs e)
{
+ if (tileViewLandSec.ShowCheckBoxes)
+ {
+ return;
+ }
OnClickCopyLandSelected(sender, e);
}
From 458dc4425953d722943cea01aec599a2af5b81c2 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:03:01 +0200
Subject: [PATCH 03/21] Add anim6.mul to animation edit form.
---
Ultima/AnimationEdit.cs | 31 +++++++++++++++++++
.../Forms/AnimationEditForm.Designer.cs | 2 +-
UoFiddler.Controls/Forms/AnimationEditForm.cs | 16 ++++++++--
3 files changed, 46 insertions(+), 3 deletions(-)
diff --git a/Ultima/AnimationEdit.cs b/Ultima/AnimationEdit.cs
index db94d8f..fdf4bea 100644
--- a/Ultima/AnimationEdit.cs
+++ b/Ultima/AnimationEdit.cs
@@ -12,12 +12,14 @@ public sealed class AnimationEdit
private static FileIndex _fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", -1);
private static FileIndex _fileIndex4 = new FileIndex("Anim4.idx", "Anim4.mul", -1);
private static FileIndex _fileIndex5 = new FileIndex("Anim5.idx", "Anim5.mul", -1);
+ private static FileIndex _fileIndex6 = new FileIndex("Anim6.idx", "Anim6.mul", -1);
private static AnimIdx[] _animCache;
private static AnimIdx[] _animCache2;
private static AnimIdx[] _animCache3;
private static AnimIdx[] _animCache4;
private static AnimIdx[] _animCache5;
+ private static AnimIdx[] _animCache6;
static AnimationEdit()
{
@@ -50,6 +52,11 @@ private static void InitializeCache()
{
_animCache5 = new AnimIdx[_fileIndex5.IdxLength / 12];
}
+
+ if (_fileIndex6.IdxLength > 0)
+ {
+ _animCache6 = new AnimIdx[_fileIndex6.IdxLength / 12];
+ }
}
///
@@ -62,6 +69,7 @@ public static void Reload()
_fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", -1);
_fileIndex4 = new FileIndex("Anim4.idx", "Anim4.mul", -1);
_fileIndex5 = new FileIndex("Anim5.idx", "Anim5.mul", -1);
+ _fileIndex6 = new FileIndex("Anim6.idx", "Anim6.mul", -1);
InitializeCache();
}
@@ -147,6 +155,22 @@ private static void GetFileIndex(
index = 35000 + ((body - 400) * 175);
}
+ break;
+ case 6:
+ fileIndex = _fileIndex6;
+ if (body < 200)
+ {
+ index = body * 110;
+ }
+ else if (body < 400)
+ {
+ index = 22000 + ((body - 200) * 65);
+ }
+ else
+ {
+ index = 35000 + ((body - 400) * 175);
+ }
+
break;
}
@@ -176,6 +200,8 @@ private static AnimIdx[] GetCache(int fileType)
return _animCache4;
case 5:
return _animCache5;
+ case 6:
+ return _animCache6;
default:
return _animCache;
}
@@ -316,6 +342,11 @@ public static void Save(int fileType, string path)
cache = _animCache5;
fileIndex = _fileIndex5;
break;
+ case 6:
+ filename = "anim6";
+ cache = _animCache6;
+ fileIndex = _fileIndex6;
+ break;
}
string idx = Path.Combine(path, filename + ".idx");
diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs b/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs
index cfdb575..bfd0116 100644
--- a/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs
+++ b/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs
@@ -320,7 +320,7 @@ private void InitializeComponent()
//
// SelectFileToolStripComboBox
//
- SelectFileToolStripComboBox.Items.AddRange(new object[] { "Choose anim file", "anim", "anim2", "anim3", "anim4", "anim5" });
+ SelectFileToolStripComboBox.Items.AddRange(new object[] { "Choose anim file", "anim", "anim2", "anim3", "anim4", "anim5", "anim6" });
SelectFileToolStripComboBox.Name = "SelectFileToolStripComboBox";
SelectFileToolStripComboBox.Size = new System.Drawing.Size(140, 25);
SelectFileToolStripComboBox.SelectedIndexChanged += OnAnimChanged;
diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs
index f6075c1..f69840b 100644
--- a/UoFiddler.Controls/Forms/AnimationEditForm.cs
+++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs
@@ -382,12 +382,24 @@ private void DrawFrameItem(object sender, DrawListViewItemEventArgs e)
private void OnAnimChanged(object sender, EventArgs e)
{
- if (SelectFileToolStripComboBox.SelectedIndex == _fileType)
+ int selected = SelectFileToolStripComboBox.SelectedIndex;
+ if (selected == _fileType)
{
return;
}
- _fileType = SelectFileToolStripComboBox.SelectedIndex;
+ if (selected >= 1 && Files.GetFilePath($"anim{(selected == 1 ? "" : selected.ToString())}.mul") == null)
+ {
+ MessageBox.Show(
+ $"anim{(selected == 1 ? "" : selected.ToString())}.mul is not present in the client directory.",
+ "File not found",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Information);
+ SelectFileToolStripComboBox.SelectedIndex = _fileType;
+ return;
+ }
+
+ _fileType = selected;
OnLoad(this, EventArgs.Empty);
}
From 90db30dab1b7114189ed1e52a58b578c08ce35b1 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:04:16 +0200
Subject: [PATCH 04/21] Add gallery in animation edit form. Change how
animation files are loaded.
---
Ultima/Animations.cs | 103 ++---
Ultima/AnimationsUopLoader.cs | 104 +----
Ultima/BodyConverter.cs | 130 +++---
Ultima/MobTypes.cs | 190 ++++++++
.../Forms/AnimationEditForm.Designer.cs | 45 +-
UoFiddler.Controls/Forms/AnimationEditForm.cs | 419 +++++++++++++++---
.../UserControls/AnimationListControl.cs | 12 +-
7 files changed, 698 insertions(+), 305 deletions(-)
create mode 100644 Ultima/MobTypes.cs
diff --git a/Ultima/Animations.cs b/Ultima/Animations.cs
index 234068b..d2b2ab8 100644
--- a/Ultima/Animations.cs
+++ b/Ultima/Animations.cs
@@ -242,9 +242,6 @@ public static void Translate(ref int body, ref int hue)
private static void LoadTable()
{
- // TODO: check why it was fixed at max 1697. Probably old code for anim.mul?
- //int count = 400 + ((_fileIndex.Index.Length - 35000) / 175);
-
_table = new int[_maxAnimationValue + 1];
for (int i = 0; i < _table.Length; ++i)
@@ -345,77 +342,75 @@ public static int GetAnimCount(int fileType)
}
///
- /// Action count of given Body in given anim file
+ /// Action count of given Body in given anim file.
+ /// When mobtypes.txt is loaded, the count is taken from the
+ /// body's mobtype category; otherwise falls back to the historical
+ /// body-id range heuristic.
///
///
///
///
public static int GetAnimLength(int body, int fileType)
{
- int length;
- switch (fileType)
+ if (MobTypes.IsLoaded)
{
- case 1:
- default:
- if (body < 200)
- {
- length = 22; // high
- }
- else if (body < 400)
- {
- length = 13; // low
- }
- else
- {
- length = 35; // people
- }
+ return MobTypes.GetActionCount(GetBodyMobType(body, fileType));
+ }
- break;
- case 2:
- if (body < 200)
- {
- length = 22; // high
- }
- else
- {
- length = 13; // low
- }
+ return GetAnimLengthLegacy(body, fileType);
+ }
- break;
+ ///
+ /// Returns the mobtype category for a body in the given file. When
+ /// mobtypes.txt is loaded, the server body id is recovered via
+ /// for anim2..anim6 reverse
+ /// lookup; falls back to the legacy id-range heuristic if either the
+ /// reverse-mapping or the mobtypes lookup misses.
+ ///
+ public static MobType GetBodyMobType(int body, int fileType)
+ {
+ if (MobTypes.IsLoaded)
+ {
+ // For anim.mul (fileType=1) in-file id == server id.
+ // For anim2..6 reverse-lookup bodyconv.def to find the server id.
+ int serverBody = fileType == 1 ? body : BodyConverter.GetTrueBody(fileType, body);
+ if (serverBody >= 0 && MobTypes.TryGet(serverBody, out MobType mt, out _))
+ {
+ return mt;
+ }
+ }
+
+ return LegacyRangeToMobType(body, fileType);
+ }
+
+ private static MobType LegacyRangeToMobType(int body, int fileType)
+ {
+ switch (fileType)
+ {
+ case 2:
+ return body < 200 ? MobType.Monster : MobType.Animal;
case 3:
if (body < 300)
{
- length = 13;
+ return MobType.Animal;
}
- else if (body < 400)
- {
- length = 22;
- }
- else
- {
- length = 35;
- }
-
- break;
+ return body < 400 ? MobType.Monster : MobType.Human;
+ case 1:
case 4:
case 5:
case 6:
+ default:
if (body < 200)
{
- length = 22;
- }
- else if (body < 400)
- {
- length = 13;
- }
- else
- {
- length = 35;
+ return MobType.Monster;
}
-
- break;
+ return body < 400 ? MobType.Animal : MobType.Human;
}
- return length;
+ }
+
+ private static int GetAnimLengthLegacy(int body, int fileType)
+ {
+ return MobTypes.GetActionCount(LegacyRangeToMobType(body, fileType));
}
///
diff --git a/Ultima/AnimationsUopLoader.cs b/Ultima/AnimationsUopLoader.cs
index 5034d4e..98eeb1b 100644
--- a/Ultima/AnimationsUopLoader.cs
+++ b/Ultima/AnimationsUopLoader.cs
@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using Ultima.Helpers;
@@ -25,7 +24,6 @@ internal static class AnimationsUopLoader
private static FileStream[] _uopFiles = new FileStream[6];
private static readonly Dictionary _hashTable = new();
- private static readonly Dictionary _mobTypes = new();
private static readonly Dictionary _sequenceReplacements = new();
private static bool _isLoaded;
@@ -38,12 +36,6 @@ private struct UopEntry
public short CompressionFlag;
}
- internal struct MobTypeInfo
- {
- public int Type;
- public uint Flags;
- }
-
static AnimationsUopLoader()
{
Initialize();
@@ -63,7 +55,6 @@ public static void Reload()
_uopFiles = new FileStream[6];
_hashTable.Clear();
- _mobTypes.Clear();
_sequenceReplacements.Clear();
_isLoaded = false;
@@ -73,7 +64,7 @@ public static void Reload()
private static void Initialize()
{
LoadUopFiles();
- LoadMobTypes();
+ MobTypes.Reload();
LoadAnimationSequence();
_isLoaded = _uopFiles.Any(f => f != null);
}
@@ -161,83 +152,6 @@ private static void BuildHashTable(FileStream fs, int fileIdx)
while (true);
}
- private static void LoadMobTypes()
- {
- string path = Files.GetFilePath("mobtypes.txt");
- if (path == null)
- {
- return;
- }
-
- string[] typeNames =
- {
- "monster",
- "sea_monster",
- "animal",
- "human",
- "equipment"
- };
-
- try
- {
- foreach (string rawLine in File.ReadLines(path))
- {
- string line = rawLine.Trim();
- if (line.Length == 0 || line[0] == '#' || !char.IsDigit(line[0]))
- {
- continue;
- }
-
- string[] parts = line.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries);
- if (parts.Length < 3)
- {
- continue;
- }
-
- if (!int.TryParse(parts[0], out int id))
- {
- continue;
- }
-
- string typeName = parts[1].ToLowerInvariant();
-
- string flagStr = parts[2];
- int commentIdx = flagStr.IndexOf('#');
- if (commentIdx == 0)
- {
- continue;
- }
-
- if (commentIdx > 0)
- {
- flagStr = flagStr.Substring(0, commentIdx).Trim();
- }
-
- flagStr = flagStr.Replace("0x", "").Replace("0X", "");
- if (!uint.TryParse(flagStr, NumberStyles.HexNumber, null, out uint flags))
- {
- continue;
- }
-
- int typeIdx = Array.IndexOf(typeNames, typeName);
- if (typeIdx < 0)
- {
- continue;
- }
-
- _mobTypes[id] = new MobTypeInfo
- {
- Type = typeIdx,
- Flags = 0x80000000u | flags,
- };
- }
- }
- catch
- {
- // mobtypes.txt is optional; parsing failures are non-fatal
- }
- }
-
private static void LoadAnimationSequence()
{
string path = Files.GetFilePath("AnimationSequence.uop");
@@ -393,12 +307,15 @@ public static bool IsUopBody(int body)
return false;
}
- return _mobTypes.TryGetValue(body, out var info) && (info.Flags & 0x10000u) != 0;
+ // Preserved-as-was: 0x10000u UOP-marker bit. Originating from a
+ // sentinel in mobtypes.txt flags; pre-existing behavior was to
+ // gate UOP-body detection on this bit. Not in scope to revise.
+ return (MobTypes.GetFlags(body) & 0x10000u) != 0;
}
public static int GetAnimationType(int body)
{
- return _mobTypes.TryGetValue(body, out var info) ? info.Type : 0;
+ return MobTypes.TryGet(body, out MobType type, out _) ? (int)type : 0;
}
public static bool IsActionDefined(int body, int action)
@@ -434,17 +351,14 @@ public static List GetDefinedActions(int body)
public static IEnumerable GetAllUopBodyIds()
{
- return _mobTypes
- .Where(kv => (kv.Value.Flags & 0x10000u) != 0)
- .Select(kv => kv.Key)
+ return MobTypes.GetDefinedBodies()
+ .Where(id => (MobTypes.GetFlags(id) & 0x10000u) != 0)
.OrderBy(id => id);
}
public static IEnumerable GetAllMobTypeBodyIds()
{
- return _mobTypes
- .Keys
- .OrderBy(id => id);
+ return MobTypes.GetDefinedBodies().OrderBy(id => id);
}
public static string GetUopFileName(int body)
diff --git a/Ultima/BodyConverter.cs b/Ultima/BodyConverter.cs
index b6c22a7..ecacdb2 100644
--- a/Ultima/BodyConverter.cs
+++ b/Ultima/BodyConverter.cs
@@ -16,6 +16,14 @@ public static class BodyConverter
public static int[] Table4 { get; private set; }
public static int[] Table5 { get; private set; }
+ // Reverse maps: in-file body id → server body id (first match wins,
+ // matching the historical linear-scan behavior of GetTrueBody).
+ private static Dictionary _reverse1;
+ private static Dictionary _reverse2;
+ private static Dictionary _reverse3;
+ private static Dictionary _reverse4;
+ private static Dictionary _reverse5;
+
static BodyConverter()
{
Initialize();
@@ -219,6 +227,31 @@ public static void Initialize()
{
Table5[list5[i]] = list5[i + 1];
}
+
+ _reverse1 = BuildReverse(list1);
+ _reverse2 = BuildReverse(list2);
+ _reverse3 = BuildReverse(list3);
+ _reverse4 = BuildReverse(list4);
+ _reverse5 = BuildReverse(list5);
+ }
+
+ private static Dictionary BuildReverse(List pairs)
+ {
+ // pairs is [server0, inFile0, server1, inFile1, ...] from the
+ // forward-table population pass. Insert in order so first server
+ // body to claim an in-file id wins — matches the historical
+ // linear-scan-from-zero behavior of GetTrueBody.
+ var map = new Dictionary(pairs.Count / 2);
+ for (int i = 0; i < pairs.Count; i += 2)
+ {
+ int serverBody = pairs[i];
+ int inFile = pairs[i + 1];
+ if (!map.ContainsKey(inFile))
+ {
+ map[inFile] = serverBody;
+ }
+ }
+ return map;
}
///
@@ -360,85 +393,28 @@ public static int Convert(ref int body)
///
public static int GetTrueBody(int fileType, int index)
{
- switch (fileType)
+ if (index < 0)
{
- case 1:
- default:
- {
- return index;
- }
- case 2:
- {
- if (Table1 != null && index >= 0)
- {
- for (int i = 0; i < Table1.Length; ++i)
- {
- if (Table1[i] == index)
- {
- return i;
- }
- }
- }
- break;
- }
- case 3:
- {
- if (Table2 != null && index >= 0)
- {
- for (int i = 0; i < Table2.Length; ++i)
- {
- if (Table2[i] == index)
- {
- return i;
- }
- }
- }
- break;
- }
- case 4:
- {
- if (Table3 != null && index >= 0)
- {
- for (int i = 0; i < Table3.Length; ++i)
- {
- if (Table3[i] == index)
- {
- return i;
- }
- }
- }
- break;
- }
- case 5:
- {
- if (Table4 != null && index >= 0)
- {
- for (int i = 0; i < Table4.Length; ++i)
- {
- if (Table4[i] == index)
- {
- return i;
- }
- }
- }
- break;
- }
- case 6:
- {
- if (Table5 != null && index >= 0)
- {
- for (int i = 0; i < Table5.Length; ++i)
- {
- if (Table5[i] == index)
- {
- return i;
- }
- }
- }
- break;
- }
+ return -1;
}
- return -1;
+
+ Dictionary map = fileType switch
+ {
+ 1 => null, // anim.mul: server id == in-file id
+ 2 => _reverse1,
+ 3 => _reverse2,
+ 4 => _reverse3,
+ 5 => _reverse4,
+ 6 => _reverse5,
+ _ => null
+ };
+
+ if (fileType == 1)
+ {
+ return index;
+ }
+
+ return map != null && map.TryGetValue(index, out int serverBody) ? serverBody : -1;
}
}
}
\ No newline at end of file
diff --git a/Ultima/MobTypes.cs b/Ultima/MobTypes.cs
new file mode 100644
index 0000000..4b11577
--- /dev/null
+++ b/Ultima/MobTypes.cs
@@ -0,0 +1,190 @@
+/***************************************************************************
+ *
+ * $Author: UOFiddler Contributors
+ *
+ * "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.Globalization;
+using System.IO;
+
+namespace Ultima
+{
+ public enum MobType
+ {
+ Monster = 0,
+ Sea = 1,
+ Animal = 2,
+ Human = 3,
+ Equipment = 4
+ }
+
+ ///
+ /// Single source of truth for mobtypes.txt.
+ ///
+ /// The client uses this file (when present) to decide a body's category
+ /// (MONSTER / ANIMAL / SEA_MONSTER / HUMAN / EQUIPMENT) and per-body
+ /// optional-action flags. Without it, UOFiddler falls back to the
+ /// historical body-id range convention (0–199 = monster, 200–399 = animal,
+ /// 400+ = human/equipment).
+ ///
+ public static class MobTypes
+ {
+ public const int MaxBody = 2047;
+
+ // Internal mobtypes.txt layout: monster, sea_monster, animal, human, equipment.
+ // This matches UOFiddler's pre-existing AnimationListControl ordering
+ // (Monster=0, Sea=1, Animal=2, Human=3, Equipment=4).
+ private static readonly string[] _typeNames =
+ {
+ "monster",
+ "sea_monster",
+ "animal",
+ "human",
+ "equipment"
+ };
+
+ // Action counts per category. Equipment composites onto a humanoid so
+ // shares the human action set size.
+ private static readonly int[] _actionCounts = { 22, 9, 13, 35, 35 };
+
+ private static readonly Dictionary _entries = new();
+
+ private struct Entry
+ {
+ public MobType Type;
+ public uint Flags;
+ }
+
+ public static bool IsLoaded { get; private set; }
+
+ static MobTypes()
+ {
+ Reload();
+ }
+
+ public static void Reload()
+ {
+ _entries.Clear();
+ IsLoaded = false;
+
+ string path = Files.GetFilePath("mobtypes.txt");
+ if (path == null)
+ {
+ return;
+ }
+
+ try
+ {
+ foreach (string rawLine in File.ReadLines(path))
+ {
+ string line = rawLine.Trim();
+ if (line.Length == 0 || line[0] == '#' || !char.IsDigit(line[0]))
+ {
+ continue;
+ }
+
+ string[] parts = line.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length < 3)
+ {
+ continue;
+ }
+
+ if (!int.TryParse(parts[0], out int id))
+ {
+ continue;
+ }
+
+ string typeName = parts[1].ToLowerInvariant();
+
+ string flagStr = parts[2];
+ int commentIdx = flagStr.IndexOf('#');
+ if (commentIdx == 0)
+ {
+ continue;
+ }
+
+ if (commentIdx > 0)
+ {
+ flagStr = flagStr.Substring(0, commentIdx).Trim();
+ }
+
+ flagStr = flagStr.Replace("0x", "").Replace("0X", "");
+ if (!uint.TryParse(flagStr, NumberStyles.HexNumber, null, out uint flags))
+ {
+ continue;
+ }
+
+ int typeIdx = Array.IndexOf(_typeNames, typeName);
+ if (typeIdx < 0)
+ {
+ continue;
+ }
+
+ _entries[id] = new Entry { Type = (MobType)typeIdx, Flags = flags };
+ }
+
+ IsLoaded = _entries.Count > 0;
+ }
+ catch
+ {
+ // mobtypes.txt is optional; parsing failures are non-fatal.
+ _entries.Clear();
+ IsLoaded = false;
+ }
+ }
+
+ public static bool TryGet(int body, out MobType type, out uint flags)
+ {
+ if (_entries.TryGetValue(body, out Entry entry))
+ {
+ type = entry.Type;
+ flags = entry.Flags;
+ return true;
+ }
+
+ type = MobType.Monster;
+ flags = 0;
+ return false;
+ }
+
+ ///
+ /// Returns the mobtype for a body, or if
+ /// the body has no entry (per user-confirmed plan choice).
+ ///
+ public static MobType GetTypeOrDefault(int body)
+ {
+ return _entries.TryGetValue(body, out Entry entry) ? entry.Type : MobType.Monster;
+ }
+
+ public static uint GetFlags(int body)
+ {
+ return _entries.TryGetValue(body, out Entry entry) ? entry.Flags : 0u;
+ }
+
+ public static int GetActionCount(MobType type)
+ {
+ int idx = (int)type;
+ return (uint)idx < _actionCounts.Length ? _actionCounts[idx] : 22;
+ }
+
+ ///
+ /// idx records per body for a given category (5 directions × action count).
+ ///
+ public static int GetIdxStride(MobType type)
+ {
+ return GetActionCount(type) * 5;
+ }
+
+ public static IEnumerable GetDefinedBodies()
+ {
+ return _entries.Keys;
+ }
+ }
+}
diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs b/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs
index bfd0116..8c665b7 100644
--- a/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs
+++ b/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs
@@ -96,6 +96,8 @@ private void InitializeComponent()
numericUpDownGreen = new System.Windows.Forms.NumericUpDown();
LockColorControlsCheckBox = new System.Windows.Forms.CheckBox();
AnimationEditPage = new System.Windows.Forms.TabPage();
+ GalleryPage = new System.Windows.Forms.TabPage();
+ GalleryTileView = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
AnimationTableLayoutPanel = new System.Windows.Forms.TableLayoutPanel();
AnimationEditToolStrip = new System.Windows.Forms.ToolStrip();
toolStripSeparator7 = new System.Windows.Forms.ToolStripSeparator();
@@ -344,6 +346,7 @@ private void InitializeComponent()
//
AnimationTabControl.Controls.Add(FramePage);
AnimationTabControl.Controls.Add(AnimationEditPage);
+ AnimationTabControl.Controls.Add(GalleryPage);
AnimationTabControl.Dock = System.Windows.Forms.DockStyle.Fill;
AnimationTabControl.Location = new System.Drawing.Point(4, 3);
AnimationTabControl.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
@@ -858,9 +861,41 @@ private void InitializeComponent()
AnimationEditPage.TabIndex = 1;
AnimationEditPage.Text = "Preview/Edit";
AnimationEditPage.UseVisualStyleBackColor = true;
- //
+ //
+ // GalleryPage
+ //
+ GalleryPage.Controls.Add(GalleryTileView);
+ GalleryPage.Location = new System.Drawing.Point(4, 24);
+ GalleryPage.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ GalleryPage.Name = "GalleryPage";
+ GalleryPage.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ GalleryPage.Size = new System.Drawing.Size(827, 565);
+ GalleryPage.TabIndex = 2;
+ GalleryPage.Text = "Gallery";
+ GalleryPage.UseVisualStyleBackColor = true;
+ //
+ // GalleryTileView
+ //
+ GalleryTileView.Dock = System.Windows.Forms.DockStyle.Fill;
+ GalleryTileView.Location = new System.Drawing.Point(4, 3);
+ GalleryTileView.MultiSelect = false;
+ GalleryTileView.Name = "GalleryTileView";
+ GalleryTileView.Size = new System.Drawing.Size(819, 559);
+ GalleryTileView.TabIndex = 0;
+ GalleryTileView.TileBackgroundColor = System.Drawing.SystemColors.Window;
+ GalleryTileView.TileBorderColor = System.Drawing.Color.Gray;
+ GalleryTileView.TileBorderWidth = 1F;
+ GalleryTileView.TileFocusColor = System.Drawing.Color.DarkBlue;
+ GalleryTileView.TileHighlightColor = System.Drawing.SystemColors.Highlight;
+ GalleryTileView.TileMargin = new System.Windows.Forms.Padding(2, 2, 0, 0);
+ GalleryTileView.TilePadding = new System.Windows.Forms.Padding(1);
+ GalleryTileView.TileSize = new System.Drawing.Size(81, 110);
+ GalleryTileView.VirtualListSize = 0;
+ GalleryTileView.DrawItem += GalleryTileViewDrawItem;
+ GalleryTileView.MouseDoubleClick += GalleryTileViewMouseDoubleClick;
+ //
// AnimationTableLayoutPanel
- //
+ //
AnimationTableLayoutPanel.ColumnCount = 2;
AnimationTableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
AnimationTableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 198F));
@@ -1371,9 +1406,9 @@ private void InitializeComponent()
ExportAllToVDToolStripMenuItem.Size = new System.Drawing.Size(186, 22);
ExportAllToVDToolStripMenuItem.Text = "Export All Valid To VD";
ExportAllToVDToolStripMenuItem.Click += OnClickExportAllToVD;
- //
+ //
// AnimationTimer
- //
+ //
AnimationTimer.Tick += AnimationTimer_Tick;
//
// AnimationEditForm
@@ -1503,6 +1538,8 @@ private void InitializeComponent()
private System.Windows.Forms.TabControl AnimationTabControl;
private System.Windows.Forms.TabPage FramePage;
private System.Windows.Forms.TabPage AnimationEditPage;
+ private System.Windows.Forms.TabPage GalleryPage;
+ private UoFiddler.Controls.UserControls.TileView.TileViewControl GalleryTileView;
private System.Windows.Forms.ToolStripMenuItem textToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem tiffToolStripMenuItem;
private System.Windows.Forms.ToolStrip AnimationFileToolStrip;
diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs
index f69840b..d0a0e46 100644
--- a/UoFiddler.Controls/Forms/AnimationEditForm.cs
+++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs
@@ -51,8 +51,32 @@ public AnimationEditForm()
Icon = Options.GetFiddlerIcon();
SelectFileToolStripComboBox.SelectedIndex = 0;
+ AnimationListTreeView.ShowNodeToolTips = true;
FramesListView.MultiSelect = true;
+ if (Options.DarkMode)
+ {
+ // .NET 10 SystemColorMode.Dark overlays a dark theme on
+ // visual-style buttons that can swallow clicks on Buttons with
+ // BackgroundImage. Forcing FlatStyle.Flat bypasses theming.
+ PlayButton.FlatStyle = FlatStyle.Flat;
+ PlayButton.FlatAppearance.BorderSize = 1;
+ PlayButton.UseVisualStyleBackColor = false;
+ PlayButton.BackColor = Color.FromArgb(60, 60, 60);
+
+ // Brighter R/G/B label colors for dark backgrounds — the
+ // designer-time Red/Green(dark)/Navy are unreadable.
+ Color red = Color.Tomato;
+ Color green = Color.LimeGreen;
+ Color blue = Color.DodgerBlue;
+ ColorRedLabel.ForeColor = red;
+ ColorGreenLabel.ForeColor = green;
+ ColorBlueLabel.ForeColor = blue;
+ BackgroundRedLabel.ForeColor = red;
+ BackgroundGreenLabel.ForeColor = green;
+ BackgroundBlueLabel.ForeColor = blue;
+ }
+
_fileType = 0;
_currentDir = 0;
_framePoint = new Point(AnimationPictureBox.Width / 2, AnimationPictureBox.Height / 2);
@@ -60,25 +84,11 @@ public AnimationEditForm()
_loaded = false;
}
+ // Indexed by MobType enum: Monster=0, Sea=1, Animal=2, Human=3, Equipment=4.
+ // Equipment composites onto a humanoid and shares the human action set.
private readonly string[][] _animNames =
{
- new string[]
- {
- "Walk",
- "Run",
- "Idle",
- "Eat",
- "Alert",
- "Attack1",
- "Attack2",
- "GetHit",
- "Die1",
- "Idle",
- "Fidget",
- "LieDown",
- "Die2"
- }, //animal
- new string[]
+ new[] // Monster (22)
{
"Walk",
"Idle",
@@ -102,8 +112,36 @@ public AnimationEditForm()
"Fly",
"TakeOff",
"GetHitInAir"
- }, //Monster
- new string[]
+ },
+ new[] // Sea (9)
+ {
+ "Walk",
+ "Run",
+ "Idle",
+ "Idle",
+ "Fidget",
+ "Attack1",
+ "Attack2",
+ "GetHit",
+ "Die1"
+ },
+ new[] // Animal (13)
+ {
+ "Walk",
+ "Run",
+ "Idle",
+ "Eat",
+ "Alert",
+ "Attack1",
+ "Attack2",
+ "GetHit",
+ "Die1",
+ "Idle",
+ "Fidget",
+ "LieDown",
+ "Die2"
+ },
+ new[] // Human (35)
{
"Walk_01",
"WalkStaff_01",
@@ -140,13 +178,124 @@ public AnimationEditForm()
"Bow_Lesser_01",
"Salute_Armed1h_01",
"Ingest_Eat_01"
- } //human
+ },
+ null // Equipment — uses the Human action list (resolved below)
};
+ private static readonly char[] _typeTag = { 'M', 'S', 'L', 'H', 'E' };
+
+ // Color used for "invalid" (no frames) tree nodes and helpers. Bright
+ // red is hard to read on a dark background; switch to OrangeRed in
+ // dark mode (matches the convention used elsewhere in the app).
+ private static readonly Color _invalidColor = Options.DarkMode ? Color.OrangeRed : _invalidColor;
+
+ // In-file body ids shown in the gallery tab, populated alongside the tree.
+ private readonly System.Collections.Generic.List _galleryBodies = new();
+
+ private string[] ResolveActionNames(MobType mobType)
+ {
+ // Equipment composites onto a humanoid; reuse the human action list.
+ int idx = mobType == MobType.Equipment ? (int)MobType.Human : (int)mobType;
+ return _animNames[idx];
+ }
+
+ // mobtypes.txt flag bits — see docs/file-formats/mobtypes.txt.md.
+ // flags == 0 means "use the default action set for this category" —
+ // the body has a normal complete animation set, so no dimming.
+ // flags != 0 means the body explicitly opts into specific optional
+ // actions; absent bits in that case indicate the action falls back
+ // to a category default. We dim those for informational purposes.
+ private static bool MobTypeHasAction(MobType type, uint flags, int action)
+ {
+ return GetMissingActionFlag(type, flags, action) == null;
+ }
+
+ ///
+ /// Returns the missing flag's name (e.g. "walk") if the given action
+ /// is dimmed for this body, or null if the action is not gated /
+ /// the body has it dedicated.
+ ///
+ private static string GetMissingActionFlag(MobType type, uint flags, int action)
+ {
+ if (flags == 0u)
+ {
+ return null;
+ }
+
+ (uint bit, string name) = GetActionBit(type, action);
+ if (bit == 0u || (flags & bit) != 0u)
+ {
+ return null;
+ }
+
+ return name;
+ }
+
+ private static (uint bit, string name) GetActionBit(MobType type, int action)
+ {
+ switch (type)
+ {
+ case MobType.Monster:
+ return action switch
+ {
+ 0 => (0x0001u, "dedicated walk"),
+ 2 => (0x0100u, "die A"),
+ 3 => (0x0200u, "die B"),
+ 4 => (0x0020u, "attack 1"),
+ 5 => (0x0040u, "attack 2"),
+ 7 => (0x1000u, "bow attack"),
+ 8 => (0x1000u, "bow attack"),
+ 9 => (0x2000u, "throw attack"),
+ 10 => (0x0400u, "block / get-hit"),
+ 13 => (0x0080u, "cast spell"),
+ 14 => (0x0080u, "cast spell"),
+ 15 => (0x0400u, "block / get-hit"),
+ 16 => (0x0400u, "block / get-hit"),
+ _ => (0u, null)
+ };
+ case MobType.Animal:
+ return action switch
+ {
+ 0 => (0x0001u, "dedicated walk"),
+ 1 => (0x0002u, "dedicated run"),
+ 3 => (0x8000u, "eat"),
+ 5 => (0x0020u, "attack 1"),
+ 6 => (0x0040u, "attack 2"),
+ 7 => (0x0400u, "block / get-hit"),
+ 8 => (0x0100u, "die A"),
+ 12 => (0x0200u, "die B"),
+ _ => (0u, null)
+ };
+ case MobType.Sea:
+ return action switch
+ {
+ 0 => (0x0001u, "dedicated walk"),
+ 1 => (0x0002u, "dedicated run"),
+ 5 => (0x0020u, "attack 1"),
+ 6 => (0x0040u, "attack 2"),
+ 7 => (0x0400u, "block / get-hit"),
+ 8 => (0x0100u, "die A"),
+ _ => (0u, null)
+ };
+ case MobType.Human:
+ case MobType.Equipment:
+ default:
+ return (0u, null);
+ }
+ }
+
+ private static string BuildDimmedTooltip(string missingFlag, uint flags)
+ {
+ return $"Greyed out: mobtypes.txt flag for '{missingFlag}' is not set on this body "
+ + $"(flags=0x{flags:X}). The client would substitute a default action; frames "
+ + "are still editable here.";
+ }
+
private void OnLoad(object sender, EventArgs e)
{
Options.LoadedUltimaClass["AnimationEdit"] = true;
+ _galleryBodies.Clear();
AnimationListTreeView.BeginUpdate();
try
{
@@ -157,21 +306,27 @@ private void OnLoad(object sender, EventArgs e)
TreeNode[] nodes = new TreeNode[count];
for (int i = 0; i < count; ++i)
{
+ MobType mobType = Animations.GetBodyMobType(i, _fileType);
int animLength = Animations.GetAnimLength(i, _fileType);
- string type = animLength == 22 ? "H" : animLength == 13 ? "L" : "P";
+ string[] names = ResolveActionNames(mobType);
+ // mobtypes.txt is keyed by server body id; reverse-map for anim2..6.
+ int serverBody = _fileType == 1 ? i : BodyConverter.GetTrueBody(_fileType, i);
+ uint mobFlags = MobTypes.IsLoaded && serverBody >= 0 ? MobTypes.GetFlags(serverBody) : 0u;
+ char typeTag = _typeTag[(int)mobType];
TreeNode node = new TreeNode
{
Tag = i,
- Text = $"{type}: {i} ({BodyConverter.GetTrueBody(_fileType, i)})"
+ Text = $"{typeTag}: {i} ({BodyConverter.GetTrueBody(_fileType, i)})"
};
bool valid = false;
for (int j = 0; j < animLength; ++j)
{
+ string name = j < names.Length ? names[j] : $"Action{j}";
TreeNode treeNode = new TreeNode
{
Tag = j,
- Text = string.Format("{0:D2} {1}", j, _animNames[animLength == 22 ? 1 : animLength == 13 ? 0 : 2][j])
+ Text = string.Format("{0:D2} {1}", j, name)
};
if (AnimationEdit.IsActionDefined(_fileType, i, j))
@@ -180,7 +335,17 @@ private void OnLoad(object sender, EventArgs e)
}
else
{
- treeNode.ForeColor = Color.Red;
+ treeNode.ForeColor = _invalidColor;
+ }
+
+ if (MobTypes.IsLoaded && treeNode.ForeColor != _invalidColor)
+ {
+ string missing = GetMissingActionFlag(mobType, mobFlags, j);
+ if (missing != null)
+ {
+ treeNode.ForeColor = Color.Gray;
+ treeNode.ToolTipText = BuildDimmedTooltip(missing, mobFlags);
+ }
}
node.Nodes.Add(treeNode);
@@ -193,7 +358,12 @@ private void OnLoad(object sender, EventArgs e)
continue;
}
- node.ForeColor = Color.Red;
+ node.ForeColor = _invalidColor;
+ }
+
+ if (valid)
+ {
+ _galleryBodies.Add(i);
}
nodes[i] = node;
@@ -207,6 +377,9 @@ private void OnLoad(object sender, EventArgs e)
AnimationListTreeView.EndUpdate();
}
+ GalleryTileView.VirtualListSize = _galleryBodies.Count;
+ GalleryTileView.Invalidate();
+
if (AnimationListTreeView.Nodes.Count > 0)
{
AnimationListTreeView.SelectedNode = AnimationListTreeView.Nodes[0];
@@ -280,6 +453,101 @@ private unsafe void SetPaletteBox()
PalettePictureBox.Image = bmp;
}
+ private void GalleryTileViewDrawItem(object sender, UoFiddler.Controls.UserControls.TileView.TileViewControl.DrawTileListItemEventArgs e)
+ {
+ if (e.Index < 0 || e.Index >= _galleryBodies.Count)
+ {
+ return;
+ }
+
+ int body = _galleryBodies[e.Index];
+ Point itemPoint = new Point(e.Bounds.X + GalleryTileView.TilePadding.Left, e.Bounds.Y + GalleryTileView.TilePadding.Top);
+ Rectangle tileRect = new Rectangle(itemPoint, GalleryTileView.TileSize);
+ var previousClip = e.Graphics.Clip;
+ e.Graphics.Clip = new Region(tileRect);
+
+ if (!GalleryTileView.SelectedIndices.Contains(e.Index))
+ {
+ using var bgBrush = new SolidBrush(GalleryTileView.BackColor);
+ e.Graphics.FillRectangle(bgBrush, tileRect);
+ }
+
+ Bitmap bmp = TryGetFirstFrame(body);
+ if (bmp != null)
+ {
+ int maxW = tileRect.Width;
+ int maxH = tileRect.Height - 18;
+ int drawWidth = bmp.Width;
+ int drawHeight = bmp.Height;
+ if (drawWidth > maxW || drawHeight > maxH)
+ {
+ float scale = Math.Min((float)maxW / drawWidth, (float)maxH / drawHeight);
+ drawWidth = (int)(drawWidth * scale);
+ drawHeight = (int)(drawHeight * scale);
+ }
+ int drawX = tileRect.X + (tileRect.Width - drawWidth) / 2;
+ int drawY = tileRect.Y + Math.Max(0, (tileRect.Height - 18 - drawHeight) / 2);
+ e.Graphics.DrawImage(bmp, drawX, drawY, drawWidth, drawHeight);
+ }
+
+ int serverBody = _fileType == 1 ? body : BodyConverter.GetTrueBody(_fileType, body);
+ string label = serverBody >= 0 && serverBody != body ? $"{body} ({serverBody})" : body.ToString();
+ using var stringFormat = new StringFormat();
+ stringFormat.Alignment = StringAlignment.Center;
+ stringFormat.LineAlignment = StringAlignment.Far;
+ e.Graphics.DrawString(label, GalleryTileView.Font, SystemBrushes.ControlText,
+ new RectangleF(tileRect.X, tileRect.Y, tileRect.Width, tileRect.Height), stringFormat);
+
+ e.Graphics.Clip = previousClip;
+ }
+
+ private Bitmap TryGetFirstFrame(int body)
+ {
+ // Walk a few action slots — body 0 may not have action 0 defined,
+ // but a later action may exist; pick the first that returns frames.
+ int animLength = Animations.GetAnimLength(body, _fileType);
+ for (int action = 0; action < animLength; ++action)
+ {
+ if (!AnimationEdit.IsActionDefined(_fileType, body, action))
+ {
+ continue;
+ }
+
+ AnimIdx anim = AnimationEdit.GetAnimation(_fileType, body, action, 1);
+ if (anim?.Frames == null || anim.Frames.Count == 0)
+ {
+ continue;
+ }
+
+ Bitmap[] frames = anim.GetFrames();
+ if (frames != null && frames.Length > 0 && frames[0] != null)
+ {
+ return frames[0];
+ }
+ }
+ return null;
+ }
+
+ private void GalleryTileViewMouseDoubleClick(object sender, MouseEventArgs e)
+ {
+ int idx = GalleryTileView.FocusIndex;
+ if (idx < 0 || idx >= _galleryBodies.Count)
+ {
+ return;
+ }
+
+ int body = _galleryBodies[idx];
+ TreeNode target = GetNode(body);
+ if (target == null)
+ {
+ return;
+ }
+
+ AnimationTabControl.SelectedTab = AnimationEditPage;
+ AnimationListTreeView.SelectedNode = target;
+ AnimationListTreeView.Focus();
+ }
+
private void AfterSelectTreeView(object sender, TreeViewEventArgs e)
{
if (AnimationListTreeView.SelectedNode == null)
@@ -400,7 +668,16 @@ private void OnAnimChanged(object sender, EventArgs e)
}
_fileType = selected;
- OnLoad(this, EventArgs.Empty);
+ Cursor previous = Cursor.Current;
+ Cursor.Current = Cursors.WaitCursor;
+ try
+ {
+ OnLoad(this, EventArgs.Empty);
+ }
+ finally
+ {
+ Cursor.Current = previous;
+ }
}
private void OnDirectionChanged(object sender, EventArgs e)
@@ -693,10 +970,10 @@ private void OnClickRemoveAction(object sender, EventArgs e)
return;
}
- AnimationListTreeView.SelectedNode.ForeColor = Color.Red;
+ AnimationListTreeView.SelectedNode.ForeColor = _invalidColor;
for (int i = 0; i < AnimationListTreeView.SelectedNode.Nodes.Count; ++i)
{
- AnimationListTreeView.SelectedNode.Nodes[i].ForeColor = Color.Red;
+ AnimationListTreeView.SelectedNode.Nodes[i].ForeColor = _invalidColor;
for (int d = 0; d < 5; ++d)
{
AnimIdx edit = AnimationEdit.GetAnimation(_fileType, _currentBody, i, d);
@@ -727,11 +1004,11 @@ private void OnClickRemoveAction(object sender, EventArgs e)
edit?.ClearFrames();
}
- AnimationListTreeView.SelectedNode.Parent.Nodes[_currentAction].ForeColor = Color.Red;
+ AnimationListTreeView.SelectedNode.Parent.Nodes[_currentAction].ForeColor = _invalidColor;
bool valid = false;
foreach (TreeNode node in AnimationListTreeView.SelectedNode.Parent.Nodes)
{
- if (node.ForeColor == Color.Red)
+ if (node.ForeColor == _invalidColor)
{
continue;
}
@@ -748,7 +1025,7 @@ private void OnClickRemoveAction(object sender, EventArgs e)
}
else
{
- AnimationListTreeView.SelectedNode.Parent.ForeColor = Color.Red;
+ AnimationListTreeView.SelectedNode.Parent.ForeColor = _invalidColor;
}
}
@@ -924,8 +1201,8 @@ private void OnClickAdd(object sender, EventArgs e)
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -987,8 +1264,8 @@ private void AddImageAtCertainIndex(int frameCount, Bitmap[] bitBmp, Bitmap bmp,
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -1135,14 +1412,18 @@ private void OnClickImportFromVD(object sender, EventArgs e)
}
int animLength = Animations.GetAnimLength(_currentBody, _fileType);
+ // .vd file format: animType 0 = 22-action (monster), 1 = 13-action (animal),
+ // 2 = 35-action (human). Other lengths can't be represented; reject.
int currentType;
- if (animLength == 22)
- {
- currentType = 0;
- }
- else
- {
- currentType = animLength == 13 ? 1 : 2;
+ switch (animLength)
+ {
+ case 22: currentType = 0; break;
+ case 13: currentType = 1; break;
+ case 35: currentType = 2; break;
+ default:
+ MessageBox.Show($"Body action length {animLength} cannot be imported as .vd palette.",
+ "Import", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
+ return;
}
using (FileStream fs = new FileStream(dialog.FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
@@ -1176,15 +1457,15 @@ private void OnClickImportFromVD(object sender, EventArgs e)
{
if (AnimationEdit.IsActionDefined(_fileType, _currentBody, j))
{
- node.Nodes[j].ForeColor = Color.Black;
+ node.Nodes[j].ForeColor = Color.Empty;
valid = true;
}
else
{
- node.Nodes[j].ForeColor = Color.Red;
+ node.Nodes[j].ForeColor = _invalidColor;
}
}
- node.ForeColor = valid ? Color.Black : Color.Red;
+ node.ForeColor = valid ? Color.Empty : _invalidColor;
}
Options.ChangedUltimaClass["Animations"] = true;
@@ -1220,7 +1501,7 @@ private void OnClickShowOnlyValid(object sender, EventArgs e)
{
for (int i = AnimationListTreeView.Nodes.Count - 1; i >= 0; --i)
{
- if (AnimationListTreeView.Nodes[i].ForeColor == Color.Red)
+ if (AnimationListTreeView.Nodes[i].ForeColor == _invalidColor)
{
AnimationListTreeView.Nodes[i].Remove();
}
@@ -2068,8 +2349,8 @@ private void AddAnimationX1(Color customConvert, Bitmap bmp)
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -2247,7 +2528,7 @@ private void OnClickExportAllToVD(object sender, EventArgs e)
{
int index = (int)AnimationListTreeView.Nodes[i].Tag;
if (index < 0 || AnimationListTreeView.Nodes[i].Parent != null ||
- AnimationListTreeView.Nodes[i].ForeColor == Color.Red)
+ AnimationListTreeView.Nodes[i].ForeColor == _invalidColor)
{
continue;
}
@@ -2448,8 +2729,8 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -2498,8 +2779,8 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -2548,8 +2829,8 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -2598,8 +2879,8 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -2648,8 +2929,8 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -3184,8 +3465,8 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -3234,8 +3515,8 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -3284,8 +3565,8 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -3334,8 +3615,8 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
@@ -3384,8 +3665,8 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi
TreeNode node = GetNode(_currentBody);
if (node != null)
{
- node.ForeColor = Color.Black;
- node.Nodes[_currentAction].ForeColor = Color.Black;
+ node.ForeColor = Color.Empty;
+ node.Nodes[_currentAction].ForeColor = Color.Empty;
}
int i = edit.Frames.Count - 1;
diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs
index dfc439a..69adfe0 100644
--- a/UoFiddler.Controls/UserControls/AnimationListControl.cs
+++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs
@@ -526,9 +526,9 @@ private void LoadFromMobTypes()
continue;
}
- int type = Animations.GetUopAnimationType(body);
- bool isEquip = type == 4;
- int actionType = isEquip ? 3 : (type < 0 || type >= GetActionNames.Length ? 0 : type);
+ int type = (int)MobTypes.GetTypeOrDefault(body);
+ bool isEquip = type == (int)MobType.Equipment;
+ int actionType = isEquip ? (int)MobType.Human : (type < 0 || type >= GetActionNames.Length ? 0 : type);
if (!isEquip && (type < 0 || type >= GetActionNames.Length))
{
type = 0;
@@ -633,7 +633,7 @@ private void ListViewDrawItem(object sender, TileViewControl.DrawTileListItemEve
stringFormat.Alignment = StringAlignment.Center;
stringFormat.LineAlignment = StringAlignment.Far;
- e.Graphics.DrawString($"({graphic})", listView.Font, Brushes.Black,
+ e.Graphics.DrawString($"({graphic})", listView.Font, SystemBrushes.ControlText,
new RectangleF(tileRect.X, tileRect.Y, tileRect.Width, tileRect.Height), stringFormat);
e.Graphics.Clip = previousClip;
@@ -710,9 +710,9 @@ private void Frames_ListView_DrawItem(object sender, DrawListViewItemEventArgs e
}
e.Graphics.DrawImage(bmp, e.Bounds.X, e.Bounds.Y, width, height);
- TextRenderer.DrawText(e.Graphics, e.Item.Text, listView1.Font, e.Bounds, Color.Black, TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter);
+ TextRenderer.DrawText(e.Graphics, e.Item.Text, listView1.Font, e.Bounds, SystemColors.ControlText, TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter);
- using (var pen = new Pen(Color.Black))
+ using (var pen = new Pen(SystemColors.ControlText))
{
e.Graphics.DrawRectangle(pen, e.Bounds.X, e.Bounds.Y, e.Bounds.Width, e.Bounds.Height);
}
From ce23f929ae4c4eb9609de6ce9cf3e4e720a68bff Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:05:44 +0200
Subject: [PATCH 05/21] Optimize file loading and access.
---
Ultima/AnimationEdit.cs | 7 +-
Ultima/Animations.cs | 14 +-
Ultima/Art.cs | 812 ++++++++++++++++++-----------
Ultima/Caching/LruBitmapCache.cs | 259 +++++++++
Ultima/FileIndex.cs | 81 ++-
Ultima/Files.cs | 18 +
Ultima/Gumps.cs | 431 +++++++++++----
Ultima/Helpers/Extensions.cs | 56 +-
Ultima/Helpers/MythicDecompress.cs | 16 +-
Ultima/Helpers/UopUtils.cs | 47 ++
Ultima/Light.cs | 69 +--
Ultima/MultiComponentList.cs | 1 -
Ultima/Multis.cs | 6 +-
Ultima/Sound.cs | 3 -
Ultima/Textures.cs | 176 +++----
Ultima/Ultima.csproj | 3 +
16 files changed, 1405 insertions(+), 594 deletions(-)
create mode 100644 Ultima/Caching/LruBitmapCache.cs
diff --git a/Ultima/AnimationEdit.cs b/Ultima/AnimationEdit.cs
index fdf4bea..69253ba 100644
--- a/Ultima/AnimationEdit.cs
+++ b/Ultima/AnimationEdit.cs
@@ -405,7 +405,10 @@ public AnimIdx(int index, FileIndex fileIndex)
_idxExtra = extra;
- using (var bin = new BinaryReader(stream))
+ // leaveOpen: stream is owned by the shared FileIndex; disposing the
+ // BinaryReader must not close it, or the next FileIndex.Seek pays a
+ // full re-open.
+ using (var bin = new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true))
{
for (int i = 0; i < PaletteCapacity; ++i)
{
@@ -430,8 +433,6 @@ public AnimIdx(int index, FileIndex fileIndex)
Frames.Add(new FrameEdit(bin));
}
}
-
- stream.Close();
}
public AnimIdx(BinaryReader bin, int extra)
diff --git a/Ultima/Animations.cs b/Ultima/Animations.cs
index d2b2ab8..01e6669 100644
--- a/Ultima/Animations.cs
+++ b/Ultima/Animations.cs
@@ -23,6 +23,13 @@ public static class Animations
///
public static void Reload()
{
+ _fileIndex?.Dispose();
+ _fileIndex2?.Dispose();
+ _fileIndex3?.Dispose();
+ _fileIndex4?.Dispose();
+ _fileIndex5?.Dispose();
+ _fileIndex6?.Dispose();
+
_fileIndex = new FileIndex("Anim.idx", "Anim.mul", 0x40000, 6);
_fileIndex2 = new FileIndex("Anim2.idx", "Anim2.mul", 0x10000, -1);
_fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", 0x20000, -1);
@@ -154,7 +161,10 @@ public static AnimationFrame[] GetAnimation(int body, int action, int direction,
bool flip = direction > 4;
- using (var bin = new BinaryReader(stream))
+ // leaveOpen: stream is owned by the shared FileIndex; disposing the
+ // BinaryReader must not close it, or the next FileIndex.Seek pays a
+ // full re-open.
+ using (var bin = new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true))
{
var palette = new ushort[PaletteCapacity];
@@ -311,8 +321,6 @@ public static bool IsAnimDefined(int body, int action, int dir, int fileType)
bool def = !((stream == null) || (length == 0));
- stream?.Close();
-
return def;
}
diff --git a/Ultima/Art.cs b/Ultima/Art.cs
index 322dd63..8fe235f 100644
--- a/Ultima/Art.cs
+++ b/Ultima/Art.cs
@@ -1,8 +1,10 @@
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using Ultima.Caching;
using Ultima.Helpers;
namespace Ultima
@@ -11,30 +13,47 @@ public static class Art
{
private static FileIndex _fileIndex = new FileIndex(
"Artidx.mul", "Art.mul", "artLegacyMUL.uop", 0x14000, 4, ".tga", 0x13FDC, false);
- private static Bitmap[] _cache;
+ // LRU read cache replaces the old Bitmap[0x14000]. Sized via
+ // Files.CacheCapacityArt so the host can tune for low-RAM machines.
+ // User edits go in _replaced (below) — they are NOT subject to eviction.
+ private static LruBitmapCache _cache;
+ // User-edited bitmaps. Pinned (no eviction) so a Replace+Save round
+ // trip can never lose modifications regardless of LRU pressure.
+ private static readonly Dictionary _replaced = new Dictionary();
private static bool[] _removed;
private static readonly Dictionary _patched = new Dictionary();
public static bool Modified;
- private static byte[] _streamBuffer;
private static readonly byte[] _validBuffer = new byte[4];
private struct ImageData
{
- public byte[] Data;
public int Position;
public int Length;
}
- private static List _landImageData;
- private static List _staticImageData;
+ // M3.5: dedup index keyed by xxHash128 of the bitmap pixels. Replaces
+ // the previous List + SHA256-bytes-in-each-entry layout,
+ // which made CompareSaveImages an O(n²) linear scan.
+ private static Dictionary _landImageData;
+ private static Dictionary _staticImageData;
static Art()
{
- _cache = new Bitmap[0x14000];
+ _cache = new LruBitmapCache(Files.CacheCapacityArt);
_removed = new bool[0x14000];
}
+ ///
+ /// Override the LRU cap for the Art read cache. Lower values bound
+ /// the working set on memory-constrained machines at the cost of
+ /// more re-decodes during long browsing sessions.
+ ///
+ public static void SetCacheCapacity(int capacity)
+ {
+ _cache.SetCapacity(capacity);
+ }
+
///
/// Validates if a static bitmap will fit within the MUL format limits by computing
/// the exact encoded size. The format uses 16-bit lookup table offsets, limiting total
@@ -153,9 +172,12 @@ public static int GetIdxLength()
///
public static void Reload()
{
+ _fileIndex?.Dispose();
_fileIndex = new FileIndex(
"Artidx.mul", "Art.mul", "artLegacyMUL.uop", 0x14000, 4, ".tga", 0x13FDC, false);
- _cache = new Bitmap[0x14000];
+ _cache?.Clear();
+ _cache ??= new LruBitmapCache(Files.CacheCapacityArt);
+ _replaced.Clear();
_removed = new bool[0x14000];
_patched.Clear();
Modified = false;
@@ -180,7 +202,8 @@ public static void ReplaceStatic(int index, Bitmap bmp)
"Consider using a smaller image or one with more transparent pixels.");
}
- _cache[index] = bmp;
+ _replaced[index] = bmp;
+ _cache.Remove(index);
_removed[index] = false;
_patched.Remove(index);
@@ -189,14 +212,15 @@ public static void ReplaceStatic(int index, Bitmap bmp)
}
///
- /// Sets bmp of index in of Land
+ /// Sets bmp of index in of Land
///
///
///
public static void ReplaceLand(int index, Bitmap bmp)
{
index &= 0x3FFF;
- _cache[index] = bmp;
+ _replaced[index] = bmp;
+ _cache.Remove(index);
_removed[index] = false;
_patched.Remove(index);
@@ -242,7 +266,7 @@ public static bool IsValidStatic(int index)
return false;
}
- if (_cache[index] != null)
+ if (_replaced.ContainsKey(index) || _cache.TryGet(index, out _))
{
return true;
}
@@ -276,7 +300,7 @@ public static bool IsValidLand(int index)
return false;
}
- if (_cache[index] != null)
+ if (_replaced.ContainsKey(index) || _cache.TryGet(index, out _))
{
return true;
}
@@ -310,9 +334,14 @@ public static Bitmap GetLand(int index, out bool patched)
return null;
}
- if (_cache[index] != null)
+ if (_replaced.TryGetValue(index, out Bitmap replaced))
{
- return _cache[index];
+ return replaced;
+ }
+
+ if (_cache.TryGet(index, out Bitmap cached))
+ {
+ return cached;
}
Stream stream = _fileIndex.Seek(index, out int length, out int _, out patched);
@@ -326,12 +355,12 @@ public static Bitmap GetLand(int index, out bool patched)
_patched[index] = true;
}
- if (Files.CacheData)
+ Bitmap bmp = LoadLand(stream, length);
+ if (Files.CacheData && bmp != null)
{
- return _cache[index] = LoadLand(stream, length);
+ _cache.Set(index, bmp);
}
-
- return LoadLand(stream, length);
+ return bmp;
}
// ReSharper disable once UnusedMember.Global
@@ -347,7 +376,6 @@ public static byte[] GetRawLand(int index)
var buffer = new byte[length];
stream.ReadExactly(buffer, 0, length);
- stream.Close();
return buffer;
}
@@ -381,9 +409,14 @@ public static Bitmap GetStatic(int index, out bool patched, bool checkMaxId = tr
return null;
}
- if (_cache[index] != null)
+ if (_replaced.TryGetValue(index, out Bitmap replaced))
{
- return _cache[index];
+ return replaced;
+ }
+
+ if (_cache.TryGet(index, out Bitmap cached))
+ {
+ return cached;
}
Stream stream = _fileIndex.Seek(index, out int length, out int _, out patched);
@@ -397,12 +430,12 @@ public static Bitmap GetStatic(int index, out bool patched, bool checkMaxId = tr
_patched[index] = true;
}
- if (Files.CacheData)
+ Bitmap bmp = LoadStatic(stream, length);
+ if (Files.CacheData && bmp != null)
{
- return _cache[index] = LoadStatic(stream, length);
+ _cache.Set(index, bmp);
}
-
- return LoadStatic(stream, length);
+ return bmp;
}
// ReSharper disable once UnusedMember.Global
@@ -419,10 +452,226 @@ public static byte[] GetRawStatic(int index)
var buffer = new byte[length];
stream.ReadExactly(buffer, 0, length);
- stream.Close();
return buffer;
}
+ ///
+ /// Decodes a static into a caller-supplied pixel buffer (16bppArgb1555).
+ /// Lets the caller reuse one buffer across many decodes instead of
+ /// paying the per-call `new Bitmap(...)` + GDI handle + LockBits cost
+ /// that `GetStatic` does.
+ ///
+ /// `destination` must be at least *
+ /// ushorts. Dimensions are populated in
+ /// out parameters even when the buffer is too small, so callers can
+ /// resize and retry.
+ ///
+ /// Cache semantics: does not touch _cache. Every call decodes from
+ /// disk. Pair with TryGetStaticDimensions if you need to size the
+ /// buffer first.
+ ///
+ public static unsafe bool TryGetStaticPixels(int index, Span destination, out int width, out int height, out bool patched, bool checkMaxId = true)
+ {
+ width = 0;
+ height = 0;
+ patched = false;
+
+ index = GetLegalItemId(index, checkMaxId);
+ index += 0x4000;
+
+ if (_removed[index])
+ {
+ return false;
+ }
+
+ Stream stream = _fileIndex.Seek(index, out int length, out _, out patched);
+ if (stream == null)
+ {
+ return false;
+ }
+
+ if (patched)
+ {
+ _patched[index] = true;
+ }
+
+ byte[] buffer = ArrayPool.Shared.Rent(length);
+ try
+ {
+ stream.ReadExactly(buffer, 0, length);
+
+ fixed (byte* data = buffer)
+ {
+ var binData = (ushort*)data;
+ int count = 2;
+ width = binData[count++];
+ height = binData[count++];
+
+ if (width <= 0 || height <= 0)
+ {
+ return false;
+ }
+
+ if (destination.Length < width * height)
+ {
+ return false;
+ }
+
+ var lookups = new int[height];
+ int start = height + 4;
+ for (int i = 0; i < height; ++i)
+ {
+ lookups[i] = start + binData[count++];
+ }
+
+ fixed (ushort* destPtr = destination)
+ {
+ for (int y = 0; y < height; ++y)
+ {
+ count = lookups[y];
+
+ ushort* cur = destPtr + y * width;
+ ushort* lineEnd = cur + width;
+ int xOffset, xRun;
+
+ while ((xOffset = binData[count++]) + (xRun = binData[count++]) != 0)
+ {
+ if (xOffset > width)
+ {
+ break;
+ }
+
+ cur += xOffset;
+ if (xOffset + xRun > width)
+ {
+ break;
+ }
+
+ ushort* end = cur + xRun;
+ while (cur < end && cur < lineEnd)
+ {
+ *cur++ = (ushort)(binData[count++] ^ 0x8000);
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+
+ ///
+ /// Decodes a land tile into a caller-supplied 44x44 ushort buffer
+ /// (16bppArgb1555). All land tiles are 44x44 so dimensions are fixed.
+ /// `destination` must be at least 44*44 = 1936 ushorts.
+ ///
+ public static unsafe bool TryGetLandPixels(int index, Span destination, out bool patched)
+ {
+ patched = false;
+ index &= 0x3FFF;
+
+ if (_removed[index])
+ {
+ return false;
+ }
+
+ if (destination.Length < 44 * 44)
+ {
+ return false;
+ }
+
+ Stream stream = _fileIndex.Seek(index, out int length, out _, out patched);
+ if (stream == null)
+ {
+ return false;
+ }
+
+ if (patched)
+ {
+ _patched[index] = true;
+ }
+
+ byte[] buffer = ArrayPool.Shared.Rent(length);
+ try
+ {
+ stream.ReadExactly(buffer, 0, length);
+
+ destination.Slice(0, 44 * 44).Clear();
+
+ fixed (byte* binData = buffer)
+ fixed (ushort* destPtr = destination)
+ {
+ var bdata = (ushort*)binData;
+ int xOffset = 21;
+ int xRun = 2;
+ ushort* line = destPtr;
+
+ for (int y = 0; y < 22; ++y, --xOffset, xRun += 2, line += 44)
+ {
+ ushort* cur = line + xOffset;
+ ushort* end = cur + xRun;
+ while (cur < end)
+ {
+ *cur++ = (ushort)(*bdata++ | 0x8000);
+ }
+ }
+
+ xOffset = 0;
+ xRun = 44;
+ for (int y = 0; y < 22; ++y, ++xOffset, xRun -= 2, line += 44)
+ {
+ ushort* cur = line + xOffset;
+ ushort* end = cur + xRun;
+ while (cur < end)
+ {
+ *cur++ = (ushort)(*bdata++ | 0x8000);
+ }
+ }
+ }
+
+ return true;
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+
+ ///
+ /// Returns the dimensions of a static without decoding pixel data.
+ /// Land tiles are always 44x44 so no dimension query is needed for them.
+ ///
+ public static bool TryGetStaticDimensions(int index, out int width, out int height, bool checkMaxId = true)
+ {
+ width = 0;
+ height = 0;
+ index = GetLegalItemId(index, checkMaxId);
+ index += 0x4000;
+
+ if (_removed[index])
+ {
+ return false;
+ }
+
+ Stream stream = _fileIndex.Seek(index, out int length, out _, out _);
+ if (stream == null || length < 8)
+ {
+ return false;
+ }
+
+ // Header layout: 4 unknown ushorts then width, height as ushorts at offset 4 and 6.
+ Span header = stackalloc byte[8];
+ stream.ReadExactly(header);
+ width = header[4] | (header[5] << 8);
+ height = header[6] | (header[7] << 8);
+ return width > 0 && height > 0;
+ }
+
public static unsafe void Measure(Bitmap bmp, out int xMin, out int yMin, out int xMax, out int yMax)
{
xMin = yMin = 0;
@@ -498,124 +747,131 @@ public static unsafe void Measure(Bitmap bmp, out int xMin, out int yMin, out in
private static unsafe Bitmap LoadStatic(Stream stream, int length)
{
- if (_streamBuffer == null || _streamBuffer.Length < length)
- {
- _streamBuffer = new byte[length];
- }
-
- stream.ReadExactly(_streamBuffer, 0, length);
- stream.Close();
-
- Bitmap bmp;
- fixed (byte* data = _streamBuffer)
+ byte[] buffer = ArrayPool.Shared.Rent(length);
+ try
{
- var binData = (ushort*)data;
- int count = 2;
- int width = binData[count++];
- int height = binData[count++];
+ stream.ReadExactly(buffer, 0, length);
- if (width <= 0 || height <= 0)
+ fixed (byte* data = buffer)
{
- return null;
- }
-
- var lookups = new int[height];
+ var binData = (ushort*)data;
+ int count = 2;
+ int width = binData[count++];
+ int height = binData[count++];
- int start = height + 4;
-
- for (int i = 0; i < height; ++i)
- {
- lookups[i] = start + binData[count++];
- }
+ if (width <= 0 || height <= 0)
+ {
+ return null;
+ }
- bmp = new Bitmap(width, height, PixelFormat.Format16bppArgb1555);
- BitmapData bd = bmp.LockBits(
- new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
+ var lookups = new int[height];
- var line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
+ int start = height + 4;
- for (int y = 0; y < height; ++y, line += delta)
- {
- count = lookups[y];
+ for (int i = 0; i < height; ++i)
+ {
+ lookups[i] = start + binData[count++];
+ }
- ushort* cur = line;
- int xOffset, xRun;
+ Bitmap bmp = new Bitmap(width, height, PixelFormat.Format16bppArgb1555);
+ BitmapData bd = bmp.LockBits(
+ new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
- while ((xOffset = binData[count++]) + (xRun = binData[count++]) != 0)
+ try
{
- if (xOffset > delta)
- {
- break;
- }
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
- cur += xOffset;
- if (xOffset + xRun > delta)
+ for (int y = 0; y < height; ++y, line += delta)
{
- break;
- }
+ count = lookups[y];
- ushort* end = cur + xRun;
- while (cur < end)
- {
- *cur++ = (ushort)(binData[count++] ^ 0x8000);
+ ushort* cur = line;
+ int xOffset, xRun;
+
+ while ((xOffset = binData[count++]) + (xRun = binData[count++]) != 0)
+ {
+ if (xOffset > delta)
+ {
+ break;
+ }
+
+ cur += xOffset;
+ if (xOffset + xRun > delta)
+ {
+ break;
+ }
+
+ ushort* end = cur + xRun;
+ while (cur < end)
+ {
+ *cur++ = (ushort)(binData[count++] ^ 0x8000);
+ }
+ }
}
}
- }
+ finally
+ {
+ bmp.UnlockBits(bd);
+ }
- bmp.UnlockBits(bd);
+ return bmp;
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
}
-
- return bmp;
}
private static unsafe Bitmap LoadLand(Stream stream, int length)
{
var bmp = new Bitmap(44, 44, PixelFormat.Format16bppArgb1555);
BitmapData bd = bmp.LockBits(new Rectangle(0, 0, 44, 44), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
- if (_streamBuffer == null || _streamBuffer.Length < length)
- {
- _streamBuffer = new byte[length];
- }
-
- stream.ReadExactly(_streamBuffer, 0, length);
- stream.Close();
- fixed (byte* binData = _streamBuffer)
+ byte[] buffer = ArrayPool.Shared.Rent(length);
+ try
{
- var bdata = (ushort*)binData;
- int xOffset = 21;
- int xRun = 2;
-
- var line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
-
- for (int y = 0; y < 22; ++y, --xOffset, xRun += 2, line += delta)
+ stream.ReadExactly(buffer, 0, length);
+ fixed (byte* binData = buffer)
{
- ushort* cur = line + xOffset;
- ushort* end = cur + xRun;
+ var bdata = (ushort*)binData;
+ int xOffset = 21;
+ int xRun = 2;
+
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
- while (cur < end)
+ for (int y = 0; y < 22; ++y, --xOffset, xRun += 2, line += delta)
{
- *cur++ = (ushort)(*bdata++ | 0x8000);
- }
- }
+ ushort* cur = line + xOffset;
+ ushort* end = cur + xRun;
- xOffset = 0;
- xRun = 44;
+ while (cur < end)
+ {
+ *cur++ = (ushort)(*bdata++ | 0x8000);
+ }
+ }
- for (int y = 0; y < 22; ++y, ++xOffset, xRun -= 2, line += delta)
- {
- ushort* cur = line + xOffset;
- ushort* end = cur + xRun;
+ xOffset = 0;
+ xRun = 44;
- while (cur < end)
+ for (int y = 0; y < 22; ++y, ++xOffset, xRun -= 2, line += delta)
{
- *cur++ = (ushort)(*bdata++ | 0x8000);
+ ushort* cur = line + xOffset;
+ ushort* end = cur + xRun;
+
+ while (cur < end)
+ {
+ *cur++ = (ushort)(*bdata++ | 0x8000);
+ }
}
}
}
-
- bmp.UnlockBits(bd);
+ finally
+ {
+ bmp.UnlockBits(bd);
+ ArrayPool.Shared.Return(buffer);
+ }
return bmp;
}
@@ -626,8 +882,8 @@ private static unsafe Bitmap LoadLand(Stream stream, int length)
///
public static unsafe void Save(string path)
{
- _landImageData = new List();
- _staticImageData = new List();
+ _landImageData = new Dictionary();
+ _staticImageData = new Dictionary();
string idx = Path.Combine(path, "artidx.mul");
string mul = Path.Combine(path, "art.mul");
@@ -644,19 +900,11 @@ public static unsafe void Save(string path)
for (int index = 0; index < GetIdxLength(); index++)
{
Files.FireFileSaveEvent();
- if (_cache[index] == null)
- {
- if (index < 0x4000)
- {
- _cache[index] = GetLand(index);
- }
- else
- {
- _cache[index] = GetStatic(index - 0x4000, false);
- }
- }
-
- Bitmap bmp = _cache[index];
+ // GetLand / GetStatic transparently check _replaced
+ // first, then the LRU cache, then decode from disk.
+ Bitmap bmp = index < 0x4000
+ ? GetLand(index)
+ : GetStatic(index - 0x4000, false);
if (bmp == null || _removed[index])
{
binidx.Write(-1); // lookup
@@ -665,63 +913,67 @@ public static unsafe void Save(string path)
}
else if (index < 0x4000)
{
- byte[] imageData = bmp.ToArray(PixelFormat.Format16bppArgb1555).ToSha256();
- if (CompareSaveImagesLand(imageData, out ImageData resultImageData))
- {
- binidx.Write(resultImageData.Position); // lookup
- binidx.Write(resultImageData.Length);
- binidx.Write(0);
-
- continue;
- }
-
- // land
+ // Lock once: used for both hashing (dedup) and encoding.
BitmapData bd = bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly,
PixelFormat.Format16bppArgb1555);
- var line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
- binidx.Write((int)binmul.BaseStream.Position); // lookup
- var length = (int)binmul.BaseStream.Position;
- int x = 22;
- int y = 0; // TODO: y is never used?
- int lineWidth = 2;
- for (int m = 0; m < 22; ++m, ++y, line += delta, lineWidth += 2)
+ try
{
- --x;
- ushort* cur = line;
- for (int n = 0; n < lineWidth; ++n)
+ UInt128 hash = bd.Hash128();
+ if (_landImageData.TryGetValue(hash, out ImageData existing))
{
- binmul.Write((ushort)(cur[x + n] ^ 0x8000));
+ binidx.Write(existing.Position); // lookup
+ binidx.Write(existing.Length);
+ binidx.Write(0);
+ continue;
}
- }
- x = 0;
- lineWidth = 44;
- y = 22;
- line = (ushort*)bd.Scan0;
- line += delta * 22;
- for (int m = 0; m < 22; m++, y++, line += delta, ++x, lineWidth -= 2)
- {
- ushort* cur = line;
- for (int n = 0; n < lineWidth; n++)
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
+ binidx.Write((int)binmul.BaseStream.Position); // lookup
+ var length = (int)binmul.BaseStream.Position;
+ int x = 22;
+ int y = 0; // TODO: y is never used?
+ int lineWidth = 2;
+ for (int m = 0; m < 22; ++m, ++y, line += delta, lineWidth += 2)
{
- binmul.Write((ushort)(cur[x + n] ^ 0x8000));
+ --x;
+ ushort* cur = line;
+ for (int n = 0; n < lineWidth; ++n)
+ {
+ binmul.Write((ushort)(cur[x + n] ^ 0x8000));
+ }
+ }
+
+ x = 0;
+ lineWidth = 44;
+ y = 22;
+ line = (ushort*)bd.Scan0;
+ line += delta * 22;
+ for (int m = 0; m < 22; m++, y++, line += delta, ++x, lineWidth -= 2)
+ {
+ ushort* cur = line;
+ for (int n = 0; n < lineWidth; n++)
+ {
+ binmul.Write((ushort)(cur[x + n] ^ 0x8000));
+ }
}
- }
- int start = length;
- length = (int)binmul.BaseStream.Position - length;
- binidx.Write(length);
- binidx.Write(0);
- bmp.UnlockBits(bd);
+ int start = length;
+ length = (int)binmul.BaseStream.Position - length;
+ binidx.Write(length);
+ binidx.Write(0);
- _landImageData.Add(new ImageData
+ _landImageData[hash] = new ImageData
+ {
+ Position = start,
+ Length = length,
+ };
+ }
+ finally
{
- Position = start,
- Length = length,
- Data = imageData
- });
+ bmp.UnlockBits(bd);
+ }
}
else
{
@@ -738,100 +990,103 @@ public static unsafe void Save(string path)
continue;
}
- byte[] imageData = bmp.ToArray(PixelFormat.Format16bppArgb1555).ToSha256();
- if (CompareSaveImagesStatic(imageData, out ImageData resultImageData))
- {
- binidx.Write(resultImageData.Position); // lookup
- binidx.Write(resultImageData.Length);
- binidx.Write(0);
-
- continue;
- }
-
- // art
BitmapData bd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format16bppArgb1555);
- var line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
- binidx.Write((int)binmul.BaseStream.Position); // lookup
- var length = (int)binmul.BaseStream.Position;
- binmul.Write(1234); // header //TODO: check what to write to header? Maybe different value will be better?
- binmul.Write((short)bmp.Width);
- binmul.Write((short)bmp.Height);
- var lookup = (int)binmul.BaseStream.Position;
- int streamLoc = lookup + (bmp.Height * 2);
- int width = 0;
- for (int i = 0; i < bmp.Height; ++i) // fill lookup
+ try
{
- binmul.Write(width);
- }
+ UInt128 hash = bd.Hash128();
+ if (_staticImageData.TryGetValue(hash, out ImageData existing))
+ {
+ binidx.Write(existing.Position); // lookup
+ binidx.Write(existing.Length);
+ binidx.Write(0);
+ continue;
+ }
- for (int y = 0; y < bmp.Height; ++y, line += delta)
- {
- ushort* cur = line;
- width = (int)(binmul.BaseStream.Position - streamLoc) / 2;
- binmul.BaseStream.Seek(lookup + (y * 2), SeekOrigin.Begin);
- binmul.Write(width);
- binmul.BaseStream.Seek(streamLoc + (width * 2), SeekOrigin.Begin);
- int i = 0;
- int x = 0;
- while (i < bmp.Width)
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
+ binidx.Write((int)binmul.BaseStream.Position); // lookup
+ var length = (int)binmul.BaseStream.Position;
+ binmul.Write(1234); // header //TODO: check what to write to header? Maybe different value will be better?
+ binmul.Write((short)bmp.Width);
+ binmul.Write((short)bmp.Height);
+ var lookup = (int)binmul.BaseStream.Position;
+ int streamLoc = lookup + (bmp.Height * 2);
+ int width = 0;
+ for (int i = 0; i < bmp.Height; ++i) // fill lookup
+ {
+ binmul.Write(width);
+ }
+
+ for (int y = 0; y < bmp.Height; ++y, line += delta)
{
- for (i = x; i <= bmp.Width; ++i)
+ ushort* cur = line;
+ width = (int)(binmul.BaseStream.Position - streamLoc) / 2;
+ binmul.BaseStream.Seek(lookup + (y * 2), SeekOrigin.Begin);
+ binmul.Write(width);
+ binmul.BaseStream.Seek(streamLoc + (width * 2), SeekOrigin.Begin);
+ int i = 0;
+ int x = 0;
+ while (i < bmp.Width)
{
- // first pixel set
+ for (i = x; i <= bmp.Width; ++i)
+ {
+ // first pixel set
+ if (i >= bmp.Width)
+ {
+ continue;
+ }
+
+ if ((cur[i] & 0x8000) != 0)
+ {
+ break;
+ }
+ }
+
if (i >= bmp.Width)
{
continue;
}
- if ((cur[i] & 0x8000) != 0)
+ int j;
+ for (j = i + 1; j < bmp.Width; ++j)
{
- break;
+ // next non set pixel
+ if ((cur[j] & 0x8000) == 0)
+ {
+ break;
+ }
}
- }
- if (i >= bmp.Width)
- {
- continue;
- }
+ binmul.Write((short)(i - x)); // xOffset
+ binmul.Write((short)(j - i)); // run
- int j;
- for (j = i + 1; j < bmp.Width; ++j)
- {
- // next non set pixel
- if ((cur[j] & 0x8000) == 0)
+ for (int p = i; p < j; ++p)
{
- break;
+ binmul.Write((ushort)(cur[p] ^ 0x8000));
}
- }
-
- binmul.Write((short)(i - x)); // xOffset
- binmul.Write((short)(j - i)); // run
- for (int p = i; p < j; ++p)
- {
- binmul.Write((ushort)(cur[p] ^ 0x8000));
+ x = j;
}
- x = j;
+ binmul.Write((short)0); // xOffset
+ binmul.Write((short)0); // Run
}
- binmul.Write((short)0); // xOffset
- binmul.Write((short)0); // Run
- }
-
- int start = length;
- length = (int)binmul.BaseStream.Position - length;
- binidx.Write(length);
- binidx.Write(0);
- bmp.UnlockBits(bd);
+ int start = length;
+ length = (int)binmul.BaseStream.Position - length;
+ binidx.Write(length);
+ binidx.Write(0);
- _staticImageData.Add(new ImageData
+ _staticImageData[hash] = new ImageData
+ {
+ Position = start,
+ Length = length,
+ };
+ }
+ finally
{
- Position = start,
- Length = length,
- Data = imageData
- });
+ bmp.UnlockBits(bd);
+ }
}
}
@@ -841,80 +1096,5 @@ public static unsafe void Save(string path)
}
}
- private static bool CompareSaveImagesLand(IReadOnlyList newChecksum, out ImageData sum)
- {
- sum = new ImageData();
- for (int i = 0; i < _landImageData.Count; ++i)
- {
- byte[] cmp = _landImageData[i].Data;
- if (cmp == null || newChecksum == null || cmp.Length != newChecksum.Count)
- {
- return false;
- }
-
- bool valid = true;
-
- for (int j = 0; j < cmp.Length; ++j)
- {
- if (cmp[j] == newChecksum[j])
- {
- continue;
- }
-
- valid = false;
- break;
- }
-
- if (!valid)
- {
- continue;
- }
-
- sum = _landImageData[i];
-
- return true;
- }
-
- return false;
- }
-
- private static bool CompareSaveImagesStatic(byte[] imageData, out ImageData resultImageData)
- {
- resultImageData = new ImageData();
-
- for (int i = 0; i < _staticImageData.Count; ++i)
- {
- byte[] cmp = _staticImageData[i].Data;
-
- if (cmp == null || imageData == null || cmp.Length != imageData.Length)
- {
- return false;
- }
-
- bool valid = true;
-
- for (int j = 0; j < cmp.Length; ++j)
- {
- if (cmp[j] == imageData[j])
- {
- continue;
- }
-
- valid = false;
- break;
- }
-
- if (!valid)
- {
- continue;
- }
-
- resultImageData = _staticImageData[i];
-
- return true;
- }
-
- return false;
- }
}
}
\ No newline at end of file
diff --git a/Ultima/Caching/LruBitmapCache.cs b/Ultima/Caching/LruBitmapCache.cs
new file mode 100644
index 0000000..73f9508
--- /dev/null
+++ b/Ultima/Caching/LruBitmapCache.cs
@@ -0,0 +1,259 @@
+// /***************************************************************************
+// *
+// * "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;
+
+namespace Ultima.Caching
+{
+ ///
+ /// Bounded LRU cache for decoded bitmaps, replacing the unbounded
+ /// Bitmap[0x14000] / Bitmap[0xFFFF] arrays that previously
+ /// pinned every decoded item for the lifetime of the process.
+ ///
+ /// Eviction policy: when Set would push Count past
+ /// , the least-recently-used entry is removed.
+ /// By default the evicted is NOT disposed — the SDK
+ /// has no way to know whether the UI is still holding a reference to it,
+ /// and disposing an in-use GDI handle crashes the renderer. Consumers
+ /// that own the bitmap lifecycle exclusively can opt in via
+ /// . always disposes
+ /// every entry — call it only on shutdown or when the consumer guarantees
+ /// no stale references survive.
+ ///
+ /// Thread safety: every public member is guarded by a single lock. The
+ /// previous array-backed cache was lock-free and racy; the lock cost
+ /// (~tens of ns per op) is dwarfed by decode cost on a miss, and on a
+ /// hit it preserves SDK behavior that consumers already accept.
+ ///
+ public sealed class LruBitmapCache : IDisposable
+ {
+ private readonly object _lock = new object();
+ private readonly LinkedList> _list =
+ new LinkedList>();
+ private readonly Dictionary>> _map;
+
+ private int _capacity;
+ private int _evictedCount;
+ private int _disposedCount;
+ private bool _disposed;
+
+ public LruBitmapCache(int capacity)
+ {
+ if (capacity < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be non-negative.");
+ }
+ _capacity = capacity;
+ _map = new Dictionary>>(Math.Min(capacity, 4096));
+ }
+
+ ///
+ /// Maximum number of bitmaps held by the cache. Setting this lower
+ /// than the current Count evicts down to the new cap immediately.
+ ///
+ public int Capacity
+ {
+ get { lock (_lock) { return _capacity; } }
+ }
+
+ public int Count
+ {
+ get { lock (_lock) { return _map.Count; } }
+ }
+
+ ///
+ /// If true, bitmaps evicted by the LRU policy or by
+ /// are 'd before being dropped. Off
+ /// by default — see class remarks. ignores this
+ /// flag and always disposes everything it owns.
+ ///
+ public bool DisposeOnEvict { get; set; }
+
+ ///
+ /// Diagnostic counter — total bitmaps evicted by LRU policy since
+ /// the cache was constructed. Useful in tests/benchmarks to assert
+ /// that bounding is actually happening.
+ ///
+ public int EvictedCount
+ {
+ get { lock (_lock) { return _evictedCount; } }
+ }
+
+ ///
+ /// Diagnostic counter — total bitmaps that were Dispose'd via
+ /// either or /
+ /// .
+ ///
+ public int DisposedCount
+ {
+ get { lock (_lock) { return _disposedCount; } }
+ }
+
+ public bool TryGet(int key, out Bitmap value)
+ {
+ lock (_lock)
+ {
+ if (_disposed || _capacity == 0)
+ {
+ value = null;
+ return false;
+ }
+ if (_map.TryGetValue(key, out var node))
+ {
+ _list.Remove(node);
+ _list.AddFirst(node);
+ value = node.Value.Value;
+ return true;
+ }
+ value = null;
+ return false;
+ }
+ }
+
+ ///
+ /// Insert or update an entry. If the key already exists, updates the
+ /// existing entry and moves it to MRU; the displaced previous bitmap
+ /// is disposed iff is true. Capacity 0
+ /// is a no-op (the bitmap is not retained and not disposed).
+ ///
+ public void Set(int key, Bitmap value)
+ {
+ if (value == null)
+ {
+ Remove(key);
+ return;
+ }
+ lock (_lock)
+ {
+ if (_disposed || _capacity == 0)
+ {
+ return;
+ }
+
+ if (_map.TryGetValue(key, out var existing))
+ {
+ Bitmap previous = existing.Value.Value;
+ _list.Remove(existing);
+ var replacement = new LinkedListNode>(new KeyValuePair(key, value));
+ _list.AddFirst(replacement);
+ _map[key] = replacement;
+
+ if (DisposeOnEvict && !ReferenceEquals(previous, value))
+ {
+ previous?.Dispose();
+ _disposedCount++;
+ }
+ return;
+ }
+
+ var node = new LinkedListNode>(new KeyValuePair(key, value));
+ _list.AddFirst(node);
+ _map[key] = node;
+ EvictWhileOverCapacityNoLock();
+ }
+ }
+
+ public bool Remove(int key)
+ {
+ lock (_lock)
+ {
+ if (_map.TryGetValue(key, out var node))
+ {
+ Bitmap bmp = node.Value.Value;
+ _list.Remove(node);
+ _map.Remove(key);
+ if (DisposeOnEvict)
+ {
+ bmp?.Dispose();
+ _disposedCount++;
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+
+ ///
+ /// Drops every entry. Disposes the bitmaps iff
+ /// is true. Use this for soft resets
+ /// where consumers may still hold references; use
+ /// when you own the lifecycle outright.
+ ///
+ public void Clear()
+ {
+ lock (_lock)
+ {
+ if (DisposeOnEvict)
+ {
+ foreach (var kvp in _list)
+ {
+ kvp.Value?.Dispose();
+ _disposedCount++;
+ }
+ }
+ _list.Clear();
+ _map.Clear();
+ }
+ }
+
+ public void SetCapacity(int newCapacity)
+ {
+ if (newCapacity < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(newCapacity));
+ }
+ lock (_lock)
+ {
+ _capacity = newCapacity;
+ EvictWhileOverCapacityNoLock();
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_lock)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+ _disposed = true;
+ foreach (var kvp in _list)
+ {
+ kvp.Value?.Dispose();
+ _disposedCount++;
+ }
+ _list.Clear();
+ _map.Clear();
+ }
+ }
+
+ private void EvictWhileOverCapacityNoLock()
+ {
+ while (_map.Count > _capacity)
+ {
+ var lru = _list.Last;
+ if (lru == null)
+ {
+ break;
+ }
+ _list.RemoveLast();
+ _map.Remove(lru.Value.Key);
+ _evictedCount++;
+ if (DisposeOnEvict)
+ {
+ lru.Value.Value?.Dispose();
+ _disposedCount++;
+ }
+ }
+ }
+ }
+}
diff --git a/Ultima/FileIndex.cs b/Ultima/FileIndex.cs
index d0405e9..6097301 100644
--- a/Ultima/FileIndex.cs
+++ b/Ultima/FileIndex.cs
@@ -6,7 +6,7 @@
namespace Ultima
{
- public sealed class FileIndex
+ public sealed class FileIndex : IDisposable
{
public IFileAccessor FileAccessor { get; }
@@ -249,19 +249,15 @@ public Stream Seek(int index, out int length, out int extra, out bool patched)
return null;
}
- if ((FileAccessor.Stream?.CanRead != true) || (!FileAccessor.Stream.CanSeek))
- {
- FileAccessor.Stream = _mulPath == null ? null : new FileStream(_mulPath, FileMode.Open, FileAccess.Read, FileShare.Read);
- }
-
- if (FileAccessor.Stream == null)
+ FileStream stream = EnsureOpen();
+ if (stream == null)
{
length = extra = 0;
patched = false;
return null;
}
- if (FileAccessor.Stream.Length < e.Lookup)
+ if (stream.Length < e.Lookup)
{
length = extra = 0;
patched = false;
@@ -270,8 +266,8 @@ public Stream Seek(int index, out int length, out int extra, out bool patched)
patched = false;
- FileAccessor.Stream.Seek(e.Lookup, SeekOrigin.Begin);
- return FileAccessor.Stream;
+ stream.Seek(e.Lookup, SeekOrigin.Begin);
+ return stream;
}
public Stream Seek(int index, ref IEntry entry, out bool patched)
@@ -318,27 +314,63 @@ public Stream Seek(int index, ref IEntry entry, out bool patched)
return null;
}
- if ((FileAccessor.Stream?.CanRead != true) || (!FileAccessor.Stream.CanSeek))
+ FileStream stream = EnsureOpen();
+ if (stream == null)
{
- FileAccessor.Stream = _mulPath == null ? null : new FileStream(_mulPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ patched = false;
+ return null;
}
- if (FileAccessor.Stream == null)
+ if (stream.Length < e.Lookup)
{
patched = false;
return null;
}
- if (FileAccessor.Stream.Length < e.Lookup)
+ patched = false;
+
+ stream.Seek(e.Lookup, SeekOrigin.Begin);
+ return stream;
+ }
+
+ ///
+ /// Returns the cached FileAccessor.Stream, re-opening it only when
+ /// genuinely required (null or disposed). Replaces the per-call
+ /// CanRead/CanSeek probe that previously re-instantiated the
+ /// FileStream every time a downstream caller had Close()'d it.
+ ///
+ private FileStream EnsureOpen()
+ {
+ FileStream stream = FileAccessor.Stream;
+ if (stream != null && stream.CanRead && stream.CanSeek)
{
- patched = false;
+ return stream;
+ }
+
+ if (_mulPath == null)
+ {
+ FileAccessor.Stream = null;
return null;
}
- patched = false;
+ stream = new FileStream(_mulPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ FileAccessor.Stream = stream;
+ return stream;
+ }
- FileAccessor.Stream.Seek(e.Lookup, SeekOrigin.Begin);
- return FileAccessor.Stream;
+ ///
+ /// Releases the underlying .mul / .uop FileStream so the next access
+ /// re-opens fresh. Additive — existing code paths that ignore the
+ /// disposable contract keep working because EnsureOpen handles a
+ /// disposed FileAccessor.Stream gracefully.
+ ///
+ public void Dispose()
+ {
+ FileAccessor?.Stream?.Dispose();
+ if (FileAccessor != null)
+ {
+ FileAccessor.Stream = null;
+ }
}
public bool Valid(int index, out int length, out int extra, out bool patched)
@@ -389,12 +421,15 @@ public bool Valid(int index, out int length, out int extra, out bool patched)
return false;
}
- if ((FileAccessor.Stream?.CanRead != true) || (!FileAccessor.Stream.CanSeek))
+ FileStream stream = EnsureOpen();
+ if (stream == null)
{
- FileAccessor.Stream = new FileStream(_mulPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ length = extra = 0;
+ patched = false;
+ return false;
}
- if (FileAccessor.Stream.Length < e.Lookup)
+ if (stream.Length < e.Lookup)
{
length = extra = 0;
patched = false;
@@ -600,7 +635,9 @@ public UopFileAccessor(string path, string uopEntryExtension, int length, int id
var fileInfo = new FileInfo(path);
string uopPattern = fileInfo.Name.Replace(fileInfo.Extension, "").ToLowerInvariant();
- using (var br = new BinaryReader(Stream))
+ // leaveOpen: this ctor caches Stream on the instance for later
+ // FileIndex.Seek calls; disposing the BinaryReader must not close it.
+ using (var br = new BinaryReader(Stream, System.Text.Encoding.UTF8, leaveOpen: true))
{
br.BaseStream.Seek(0, SeekOrigin.Begin);
diff --git a/Ultima/Files.cs b/Ultima/Files.cs
index e83490e..016c06b 100644
--- a/Ultima/Files.cs
+++ b/Ultima/Files.cs
@@ -20,6 +20,24 @@ public static void FireFileSaveEvent()
///
public static bool CacheData { get; set; } = true;
+ ///
+ /// Initial LRU capacity for the Art read cache (statics + land
+ /// tiles share the same cache). Default 4096 — bounds the worst-case
+ /// working set to a few hundred MB of bitmaps even after a full
+ /// 0x14000-id scan, while keeping recent thumbnails warm. Reading
+ /// happens at static-ctor time so set this before first use, or call
+ /// at runtime.
+ ///
+ public static int CacheCapacityArt { get; set; } = 4096;
+
+ ///
+ /// Initial LRU capacity for the Gumps read cache. Default 2048 —
+ /// gumps are larger on average than statics, so the cap is lower
+ /// to keep total memory comparable. Adjust via
+ /// at runtime.
+ ///
+ public static int CacheCapacityGumps { get; set; } = 2048;
+
///
/// Contains the path infos
///
diff --git a/Ultima/Gumps.cs b/Ultima/Gumps.cs
index 15f77bb..49294e5 100644
--- a/Ultima/Gumps.cs
+++ b/Ultima/Gumps.cs
@@ -1,8 +1,10 @@
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using Ultima.Caching;
using Ultima.Helpers;
namespace Ultima
@@ -12,7 +14,10 @@ public sealed class Gumps
private static FileIndex _fileIndex = new FileIndex(
"Gumpidx.mul", "Gumpart.mul", "gumpartLegacyMUL.uop", 0xFFFF, 12, ".tga", -1, true);
- private static Bitmap[] _cache;
+ // LRU read cache replaces the old Bitmap[_fileIndex.IndexLength].
+ // User edits go in _replaced (below) and are never evicted.
+ private static LruBitmapCache _cache;
+ private static readonly Dictionary _replaced = new Dictionary();
private static bool[] _removed;
private static readonly Dictionary _patched = new Dictionary();
@@ -20,18 +25,25 @@ public sealed class Gumps
private static byte[] _streamBuffer;
private static byte[] _colorTable;
+ // Authoritative id range — what _cache.Length used to be before the
+ // LRU swap. Sourced from the FileIndex when available, falls back to
+ // 0xFFFF (the gump id space ceiling) when no client is configured.
+ private static int _indexLength;
+
static Gumps()
{
- if (_fileIndex != null)
- {
- _cache = new Bitmap[_fileIndex.IndexLength];
- _removed = new bool[_fileIndex.IndexLength];
- }
- else
- {
- _cache = new Bitmap[0xFFFF];
- _removed = new bool[0xFFFF];
- }
+ _cache = new LruBitmapCache(Files.CacheCapacityGumps);
+ _indexLength = _fileIndex?.IndexLength > 0 ? (int)_fileIndex.IndexLength : 0xFFFF;
+ _removed = new bool[_indexLength];
+ }
+
+ ///
+ /// Override the LRU cap for the Gumps read cache. See
+ /// for the default.
+ ///
+ public static void SetCacheCapacity(int capacity)
+ {
+ _cache.SetCapacity(capacity);
}
///
@@ -41,15 +53,22 @@ public static void Reload()
{
try
{
+ _fileIndex?.Dispose();
_fileIndex = new FileIndex("Gumpidx.mul", "Gumpart.mul", "gumpartLegacyMUL.uop", 0xFFFF, 12, ".tga", -1, true);
- _cache = new Bitmap[_fileIndex.IndexLength];
- _removed = new bool[_fileIndex.IndexLength];
+ _indexLength = _fileIndex.IndexLength > 0 ? (int)_fileIndex.IndexLength : 0xFFFF;
+ _cache?.Clear();
+ _cache ??= new LruBitmapCache(Files.CacheCapacityGumps);
+ _replaced.Clear();
+ _removed = new bool[_indexLength];
}
catch
{
_fileIndex = null;
- _cache = new Bitmap[0xFFFF];
- _removed = new bool[0xFFFF];
+ _indexLength = 0xFFFF;
+ _cache?.Clear();
+ _cache ??= new LruBitmapCache(Files.CacheCapacityGumps);
+ _replaced.Clear();
+ _removed = new bool[_indexLength];
}
//_pixelBuffer = null;
@@ -60,17 +79,18 @@ public static void Reload()
public static int GetCount()
{
- return _cache.Length;
+ return _indexLength;
}
///
- /// Replaces Gump
+ /// Replaces Gump
///
///
///
public static void ReplaceGump(int index, Bitmap bmp)
{
- _cache[index] = bmp;
+ _replaced[index] = bmp;
+ _cache.Remove(index);
_removed[index] = false;
_patched.Remove(index);
}
@@ -96,7 +116,7 @@ public static bool IsValidIndex(int index)
return false;
}
- if (index > _cache.Length - 1)
+ if (index > _indexLength - 1)
{
return false;
}
@@ -106,7 +126,7 @@ public static bool IsValidIndex(int index)
return false;
}
- if (_cache[index] != null)
+ if (_replaced.ContainsKey(index) || _cache.TryGet(index, out _))
{
return true;
}
@@ -206,7 +226,6 @@ public static byte[] GetRawGump(int index, out int width, out int height)
var buffer = new byte[length];
stream.ReadExactly(buffer, 0, length);
- stream.Close();
return buffer;
}
@@ -231,7 +250,6 @@ public static unsafe Bitmap GetGump(int index, Hue hue, bool onlyHueGrayPixels,
if (extra == -1)
{
- stream.Close();
return null;
}
@@ -240,7 +258,6 @@ public static unsafe Bitmap GetGump(int index, Hue hue, bool onlyHueGrayPixels,
if (width <= 0 || height <= 0)
{
- stream.Close();
return null;
}
@@ -372,8 +389,6 @@ public static unsafe Bitmap GetGump(int index, Hue hue, bool onlyHueGrayPixels,
}
}
- stream.Close();
-
return new Bitmap(width, height, bytesPerStride, PixelFormat.Format16bppArgb1555, (IntPtr)pPixelDataStart);
}
}
@@ -391,6 +406,188 @@ public static Bitmap GetGump(int index)
return GetGump(index, out bool _);
}
+ ///
+ /// Decodes a gump into a caller-supplied pixel buffer. Lets the caller
+ /// reuse a single shared destination across many decodes (e.g. a
+ /// listview rendering thumbnails) instead of paying the per-call
+ /// `new Bitmap(...)` + GDI handle + LockBits cost.
+ ///
+ /// `destination` must be at least *
+ /// ushorts; the buffer is filled with
+ /// Format16bppArgb1555 pixels. Returns false if the gump is missing,
+ /// removed, the entry has invalid dimensions, or the buffer is too
+ /// small. width / height are out parameters and are filled even when
+ /// the buffer is too small, so callers can resize and retry.
+ ///
+ /// Cache semantics: this method does not write to or read from
+ /// _cache — every call decodes from disk. Use GetGump when you want
+ /// the standard bitmap cache.
+ ///
+ public static unsafe bool TryGetGumpPixels(int index, Span destination, out int width, out int height, out bool patched)
+ {
+ width = 0;
+ height = 0;
+ patched = _patched.ContainsKey(index) && _patched[index];
+
+ if (index < 0 || index > _indexLength - 1)
+ {
+ return false;
+ }
+
+ if (_removed[index])
+ {
+ return false;
+ }
+
+ IEntry entry = null;
+ Stream stream = _fileIndex.Seek(index, ref entry, out patched);
+ if (stream == null || entry == null || entry.Extra1 == -1)
+ {
+ return false;
+ }
+
+ if (patched)
+ {
+ _patched[index] = true;
+ }
+
+ int length = entry.Length;
+ if (patched)
+ {
+ length = entry.Length & 0x7FFFFFFF;
+ }
+
+ byte[] rented = ArrayPool.Shared.Rent(length);
+ byte[] zlibBuf = null;
+ try
+ {
+ stream.ReadExactly(rented, 0, length);
+
+ byte[] data = rented;
+ int dataOffset = 0;
+ width = entry.Extra1;
+ height = entry.Extra2;
+
+ if (entry.Flag >= CompressionFlag.Zlib)
+ {
+ int decSize = entry.DecompressedLength;
+ if (decSize <= 8)
+ {
+ return false;
+ }
+
+ zlibBuf = ArrayPool.Shared.Rent(decSize);
+ if (!UopUtils.TryDecompressInto(rented, 0, length, zlibBuf, out int zlibLen))
+ {
+ return false;
+ }
+
+ if (entry.Flag == CompressionFlag.Mythic)
+ {
+ // Mythic still allocates the final byte[]; that's the next lever.
+ data = MythicDecompress.Decompress(zlibBuf, 0, zlibLen);
+ }
+ else
+ {
+ data = zlibBuf;
+ }
+
+ // Header: 4-byte width then 4-byte height (little-endian), pixel data at offset 8.
+ width = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
+ height = data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24);
+ dataOffset = 8;
+ entry.Extra1 = width;
+ entry.Extra2 = height;
+ }
+
+ if (width <= 0 || height <= 0 || destination.Length < width * height)
+ {
+ return false;
+ }
+
+ fixed (byte* dataPtr = data)
+ fixed (ushort* destPtr = destination)
+ {
+ byte* basePtr = dataPtr + dataOffset;
+ var lookup = (int*)basePtr;
+ var dat = (ushort*)basePtr;
+
+ for (int y = 0; y < height; ++y)
+ {
+ int count = (*lookup++ * 2);
+
+ ushort* cur = destPtr + y * width;
+ ushort* end = cur + width;
+
+ while (cur < end)
+ {
+ ushort color = dat[count++];
+ ushort* next = cur + dat[count++];
+
+ if (color == 0)
+ {
+ cur = next;
+ }
+ else
+ {
+ color ^= 0x8000;
+ while (cur < next)
+ {
+ *cur++ = color;
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+ finally
+ {
+ if (zlibBuf != null)
+ {
+ ArrayPool.Shared.Return(zlibBuf);
+ }
+ ArrayPool.Shared.Return(rented);
+ }
+ }
+
+ ///
+ /// Returns the dimensions of a gump without decoding pixel data.
+ /// Cheaper than TryGetGumpPixels when the caller only needs to size a
+ /// destination buffer. For UOP-compressed entries we still have to
+ /// decompress to recover width/height — those are paid for on the
+ /// first hit and cached via entry.Extra1/Extra2.
+ ///
+ public static bool TryGetGumpDimensions(int index, out int width, out int height)
+ {
+ width = 0;
+ height = 0;
+ if (index < 0 || index >= _indexLength || _fileIndex.FileAccessor == null)
+ {
+ return false;
+ }
+
+ IEntry entry = _fileIndex[index];
+ if (entry.Lookup < 0 || entry.Extra1 == -1)
+ {
+ return false;
+ }
+
+ // For uncompressed entries the index already knows width/height.
+ if (entry.Flag < CompressionFlag.Zlib)
+ {
+ width = entry.Extra1;
+ height = entry.Extra2;
+ return width > 0 && height > 0;
+ }
+
+ // Compressed entries need a one-shot decode to recover dims.
+ // Falls through to TryGetGumpPixels with a 0-length destination
+ // which returns false but populates width/height.
+ return TryGetGumpPixels(index, Span.Empty, out width, out height, out _);
+ }
+
///
/// Returns Bitmap of index and if verdata patched
///
@@ -401,7 +598,7 @@ public static unsafe Bitmap GetGump(int index, out bool patched)
{
patched = _patched.ContainsKey(index) && _patched[index];
- if (index > _cache.Length - 1)
+ if (index > _indexLength - 1)
{
return null;
}
@@ -411,9 +608,14 @@ public static unsafe Bitmap GetGump(int index, out bool patched)
return null;
}
- if (_cache[index] != null)
+ if (_replaced.TryGetValue(index, out Bitmap replaced))
+ {
+ return replaced;
+ }
+
+ if (_cache.TryGet(index, out Bitmap cached))
{
- return _cache[index];
+ return cached;
}
IEntry entry = null;
@@ -425,7 +627,6 @@ public static unsafe Bitmap GetGump(int index, out bool patched)
if (entry.Extra1 == -1)
{
- stream.Close();
return null;
}
@@ -440,103 +641,126 @@ public static unsafe Bitmap GetGump(int index, out bool patched)
length = entry.Length & 0x7FFFFFFF;
}
- if (_streamBuffer == null || _streamBuffer.Length < length)
+ byte[] rented = ArrayPool.Shared.Rent(length);
+ byte[] zlibBuf = null;
+ try
{
- _streamBuffer = new byte[length];
- }
+ stream.ReadExactly(rented, 0, length);
- stream.ReadExactly(_streamBuffer, 0, length);
+ byte[] data = rented;
+ int dataOffset = 0;
- uint width = (uint)entry.Extra1;
- uint height = (uint)entry.Extra2;
+ uint width = (uint)entry.Extra1;
+ uint height = (uint)entry.Extra2;
- // Compressed UOPs
- if (entry.Flag >= CompressionFlag.Zlib)
- {
- var result = UopUtils.Decompress(_streamBuffer);
- if (result.success is false)
- {
- return null;
- }
- if (entry.Flag == CompressionFlag.Mythic)
- {
- _streamBuffer = MythicDecompress.Decompress(result.data);
- }
- using (BinaryReader reader = new BinaryReader(new MemoryStream(_streamBuffer)))
+ // Compressed UOPs
+ if (entry.Flag >= CompressionFlag.Zlib)
{
- byte[] extra = reader.ReadBytes(8);
-
- width = (uint)((extra[3] << 24) | (extra[2] << 16) | (extra[1] << 8) | extra[0]);
- height = (uint)((extra[7] << 24) | (extra[6] << 16) | (extra[5] << 8) | extra[4]);
+ int decSize = entry.DecompressedLength;
+ if (decSize <= 8)
+ {
+ return null;
+ }
- // TODO: Tbh, whole code needs to be reworked with readers, as we're doing useless work here just re-reading everything but 8 first bytes
- _streamBuffer = reader.ReadBytes(_streamBuffer.Length - 8);
- }
+ zlibBuf = ArrayPool.Shared.Rent(decSize);
+ if (!UopUtils.TryDecompressInto(rented, 0, length, zlibBuf, out int zlibLen))
+ {
+ return null;
+ }
- entry.Extra1 = (int)width;
- entry.Extra2 = (int)height;
- }
+ if (entry.Flag == CompressionFlag.Mythic)
+ {
+ // Mythic still allocates the final byte[]; that's the next lever.
+ data = MythicDecompress.Decompress(zlibBuf, 0, zlibLen);
+ }
+ else
+ {
+ data = zlibBuf;
+ }
- if (width <= 0 || height <= 0)
- {
- return null;
- }
+ width = (uint)(data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24));
+ height = (uint)(data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24));
+ dataOffset = 8;
- try
- {
- var bmp = new Bitmap((int)width, (int)height, PixelFormat.Format16bppArgb1555);
- BitmapData bd = bmp.LockBits(
- new Rectangle(0, 0, (int)width, (int)height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
+ entry.Extra1 = (int)width;
+ entry.Extra2 = (int)height;
+ }
- fixed (byte* data = _streamBuffer)
+ if (width <= 0 || height <= 0)
{
- var lookup = (int*)data;
- var dat = (ushort*)data;
+ return null;
+ }
- var line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
+ try
+ {
+ var bmp = new Bitmap((int)width, (int)height, PixelFormat.Format16bppArgb1555);
+ BitmapData bd = bmp.LockBits(
+ new Rectangle(0, 0, (int)width, (int)height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
- for (int y = 0; y < (int)height; ++y, line += delta)
+ try
{
- int count = (*lookup++ * 2);
-
- ushort* cur = line;
- ushort* end = line + bd.Width;
-
- while (cur < end)
+ fixed (byte* dataPtr = data)
{
- ushort color = dat[count++];
- ushort* next = cur + dat[count++];
+ byte* basePtr = dataPtr + dataOffset;
+ var lookup = (int*)basePtr;
+ var dat = (ushort*)basePtr;
- if (color == 0)
- {
- cur = next;
- }
- else
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
+
+ for (int y = 0; y < (int)height; ++y, line += delta)
{
- color ^= 0x8000;
- while (cur < next)
+ int count = (*lookup++ * 2);
+
+ ushort* cur = line;
+ ushort* end = line + bd.Width;
+
+ while (cur < end)
{
- *cur++ = color;
+ ushort color = dat[count++];
+ ushort* next = cur + dat[count++];
+
+ if (color == 0)
+ {
+ cur = next;
+ }
+ else
+ {
+ color ^= 0x8000;
+ while (cur < next)
+ {
+ *cur++ = color;
+ }
+ }
}
}
}
}
- }
+ finally
+ {
+ bmp.UnlockBits(bd);
+ }
- bmp.UnlockBits(bd);
+ if (Files.CacheData)
+ {
+ _cache.Set(index, bmp);
+ }
- if (Files.CacheData)
+ return bmp;
+ }
+ catch (Exception)
{
- return _cache[index] = bmp;
+ // ignored
+ return null;
}
-
- return bmp;
}
- catch (Exception)
+ finally
{
- // ignored
- return null;
+ if (zlibBuf != null)
+ {
+ ArrayPool.Shared.Return(zlibBuf);
+ }
+ ArrayPool.Shared.Return(rented);
}
}
@@ -550,15 +774,12 @@ public static unsafe void Save(string path)
using (var binidx = new BinaryWriter(fsidx))
using (var binmul = new BinaryWriter(fsmul))
{
- for (int index = 0; index < _cache.Length; index++)
+ for (int index = 0; index < _indexLength; index++)
{
Files.FireFileSaveEvent();
- if (_cache[index] == null)
- {
- _cache[index] = GetGump(index);
- }
-
- Bitmap bmp = _cache[index];
+ // GetGump transparently checks _replaced first, then the
+ // LRU cache, then decodes from disk.
+ Bitmap bmp = GetGump(index);
if ((bmp == null) || (_removed[index]))
{
binidx.Write(-1); // lookup
diff --git a/Ultima/Helpers/Extensions.cs b/Ultima/Helpers/Extensions.cs
index 371c8e1..d8b7f05 100644
--- a/Ultima/Helpers/Extensions.cs
+++ b/Ultima/Helpers/Extensions.cs
@@ -1,8 +1,8 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
+using System.IO.Hashing;
using System.Runtime.InteropServices;
-using System.Security.Cryptography;
namespace Ultima.Helpers
{
@@ -30,11 +30,59 @@ public static byte[] ToArray(this Bitmap bmp, PixelFormat? format = null)
}
}
- static readonly SHA256 _sha256 = SHA256.Create();
+ ///
+ /// Hashes a bitmap's pixel data using xxHash128. Replaces the old
+ /// bmp.ToArray().ToSha256() pattern for Save-time deduplication:
+ ///
+ /// 1. Allocation-free — hashes from the locked
+ /// span directly. No byte[] copy of pixel data, no byte[32]
+ /// SHA256 output.
+ /// 2. ~10× faster — xxHash128 hits 10–30 GB/s on modern CPUs vs SHA256's
+ /// 1–3 GB/s, and we no longer pay the LockBits+memcpy that ToArray
+ /// did before hashing.
+ /// 3. Returns a 128-bit struct that's a perfect
+ /// key for O(1) dedup, replacing the previous O(n²) linear scan over
+ /// 32-byte SHA256 digests.
+ ///
+ /// This is not a cryptographic hash; do not use for security-sensitive
+ /// comparisons. For dedup on bitmaps that the caller already
+ /// produced/owns, xxHash128's collision probability is negligible
+ /// (~2^-64 per pair) — perfectly adequate.
+ ///
+ public static UInt128 Hash128(this Bitmap bmp, PixelFormat? format = null)
+ {
+ if (bmp == null)
+ {
+ throw new ArgumentNullException(nameof(bmp));
+ }
- public static byte[] ToSha256(this byte[] buffer)
+ Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
+ BitmapData data = bmp.LockBits(rect, ImageLockMode.ReadOnly, format ?? bmp.PixelFormat);
+ try
+ {
+ return Hash128(data);
+ }
+ finally
+ {
+ bmp.UnlockBits(data);
+ }
+ }
+
+ ///
+ /// xxHash128 over an already-locked . Use this
+ /// overload when the caller has already acquired a LockBits (Save
+ /// hot path locks once for read, hashes, then encodes).
+ ///
+ public static unsafe UInt128 Hash128(this BitmapData data)
{
- return _sha256.ComputeHash(buffer);
+ if (data == null)
+ {
+ throw new ArgumentNullException(nameof(data));
+ }
+
+ int size = data.Stride * data.Height;
+ var span = new ReadOnlySpan((void*)data.Scan0, size);
+ return XxHash128.HashToUInt128(span);
}
}
}
diff --git a/Ultima/Helpers/MythicDecompress.cs b/Ultima/Helpers/MythicDecompress.cs
index 1ef31de..1c209ac 100644
--- a/Ultima/Helpers/MythicDecompress.cs
+++ b/Ultima/Helpers/MythicDecompress.cs
@@ -18,15 +18,27 @@ public static byte[] Detransform(byte[] buffer)
}
public static byte[] Decompress(byte[] buffer)
+ {
+ return Decompress(buffer, 0, buffer.Length);
+ }
+
+ ///
+ /// Decompresses a slice of . Lets callers pass
+ /// pooled buffers that may be larger than the actual payload — the
+ /// original Decompress(byte[]) overload reads BaseStream.Length, which
+ /// would walk into uninitialized tail bytes when the input is pooled.
+ ///
+ public static byte[] Decompress(byte[] buffer, int offset, int length)
{
byte[] output;
- using (var reader = new BinaryReader(new MemoryStream(buffer)))
+ using (var ms = new MemoryStream(buffer, offset, length, writable: false))
+ using (var reader = new BinaryReader(ms))
{
var header = reader.ReadUInt32();
uint dataLength = header ^ 0x8E2C9A3D;
- var list = reader.ReadBytes((int)(reader.BaseStream.Length - 4));
+ var list = reader.ReadBytes(length - 4);
output = InternalDecompress(MoveToFrontCoding.Decode(list));
if (output.Length != dataLength)
diff --git a/Ultima/Helpers/UopUtils.cs b/Ultima/Helpers/UopUtils.cs
index 86f8055..746f91f 100644
--- a/Ultima/Helpers/UopUtils.cs
+++ b/Ultima/Helpers/UopUtils.cs
@@ -125,6 +125,53 @@ public static (bool success, byte[] data) Decompress(byte[] compressedData)
}
}
+ ///
+ /// Decompresses zlib UOP-entry bytes into a caller-supplied buffer
+ /// instead of allocating a fresh byte[]. Pair with ArrayPool to make
+ /// per-call allocations effectively zero on the hot decode paths.
+ ///
+ /// must be at least as large as
+ /// the entry's declared decompressed length (see Entry6D.DecompressedLength).
+ /// Returns false if decompression fails OR the destination is too
+ /// small to hold the full payload — in the latter case the caller
+ /// should retry with a larger buffer.
+ ///
+ public static bool TryDecompressInto(byte[] compressedData, int compressedOffset, int compressedLength, byte[] destinationBuffer, out int decompressedLength)
+ {
+ decompressedLength = 0;
+ if (compressedData == null || compressedLength <= 0 || destinationBuffer == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ using var compressedStream = new MemoryStream(compressedData, compressedOffset, compressedLength, writable: false);
+ using var zlibStream = new ZLibStream(compressedStream, CompressionMode.Decompress, leaveOpen: false);
+
+ int total = 0;
+ int read;
+ while (total < destinationBuffer.Length &&
+ (read = zlibStream.Read(destinationBuffer, total, destinationBuffer.Length - total)) > 0)
+ {
+ total += read;
+ }
+
+ // If the stream still has bytes after we filled the destination, the buffer was too small.
+ if (total == destinationBuffer.Length && zlibStream.ReadByte() != -1)
+ {
+ return false;
+ }
+
+ decompressedLength = total;
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
///
/// Method for compressing zlib byte arrays inside .uop
///
diff --git a/Ultima/Light.cs b/Ultima/Light.cs
index a5d136b..7137023 100644
--- a/Ultima/Light.cs
+++ b/Ultima/Light.cs
@@ -1,3 +1,4 @@
+using System.Buffers;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
@@ -9,7 +10,6 @@ public sealed class Light
private static FileIndex _fileIndex = new FileIndex("lightidx.mul", "light.mul", 100, -1);
private static Bitmap[] _cache = new Bitmap[100];
private static bool[] _removed = new bool[100];
- private static byte[] _streamBuffer;
///
/// ReReads light.mul
@@ -63,8 +63,6 @@ public static bool TestLight(int index)
return false;
}
- stream.Close();
-
int width = (extra & 0xFFFF);
int height = ((extra >> 16) & 0xFFFF);
@@ -110,8 +108,7 @@ public static byte[] GetRawLight(int index, out int width, out int height)
width = (extra & 0xFFFF);
height = ((extra >> 16) & 0xFFFF);
var buffer = new byte[length];
- _ = stream.Read(buffer, 0, length);
- stream.Close();
+ stream.ReadExactly(buffer, 0, length);
return buffer;
}
@@ -142,44 +139,52 @@ public static unsafe Bitmap GetLight(int index)
int width = (extra & 0xFFFF);
int height = ((extra >> 16) & 0xFFFF);
- if (_streamBuffer == null || _streamBuffer.Length < length)
+ byte[] buffer = ArrayPool.Shared.Rent(length);
+ try
{
- _streamBuffer = new byte[length];
- }
-
- _ = stream.Read(_streamBuffer, 0, length);
+ stream.ReadExactly(buffer, 0, length);
- var bmp = new Bitmap(width, height, PixelFormat.Format16bppArgb1555);
- BitmapData bd = bmp.LockBits(
- new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
+ var bmp = new Bitmap(width, height, PixelFormat.Format16bppArgb1555);
+ BitmapData bd = bmp.LockBits(
+ new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
- var line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
-
- fixed (byte* data = _streamBuffer)
- {
- var bindat = (sbyte*)data;
- for (int y = 0; y < height; ++y, line += delta)
+ try
{
- ushort* cur = line;
- ushort* end = cur + width;
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
- while (cur < end)
+ fixed (byte* data = buffer)
{
- sbyte value = *bindat++;
- *cur++ = (ushort)(((0x1f + value) << 10) + ((0x1F + value) << 5) + (0x1F + value));
+ var bindat = (sbyte*)data;
+ for (int y = 0; y < height; ++y, line += delta)
+ {
+ ushort* cur = line;
+ ushort* end = cur + width;
+
+ while (cur < end)
+ {
+ sbyte value = *bindat++;
+ *cur++ = (ushort)(((0x1f + value) << 10) + ((0x1F + value) << 5) + (0x1F + value));
+ }
+ }
}
}
- }
+ finally
+ {
+ bmp.UnlockBits(bd);
+ }
- bmp.UnlockBits(bd);
- stream.Close();
- if (!Files.CacheData)
+ if (!Files.CacheData)
+ {
+ return _cache[index] = bmp;
+ }
+
+ return bmp;
+ }
+ finally
{
- return _cache[index] = bmp;
+ ArrayPool.Shared.Return(buffer);
}
-
- return bmp;
}
public static unsafe void Save(string path)
diff --git a/Ultima/MultiComponentList.cs b/Ultima/MultiComponentList.cs
index f3b4138..e65975e 100644
--- a/Ultima/MultiComponentList.cs
+++ b/Ultima/MultiComponentList.cs
@@ -196,7 +196,6 @@ public MultiComponentList(BinaryReader reader, int count, bool useNewMultiFormat
}
}
ConvertList();
- reader.Close();
}
public MultiComponentList(string fileName, Multis.ImportType type)
diff --git a/Ultima/Multis.cs b/Ultima/Multis.cs
index dd0f178..5cbbcda 100644
--- a/Ultima/Multis.cs
+++ b/Ultima/Multis.cs
@@ -76,13 +76,15 @@ public static MultiComponentList Load(int index)
return MultiComponentList.Empty;
}
+ // leaveOpen: stream is owned by the shared FileIndex; the
+ // BinaryReader is throwaway and must not close it.
if (Art.IsUOAHS())
{
- return new MultiComponentList(new BinaryReader(stream), length / 16, true);
+ return new MultiComponentList(new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true), length / 16, true);
}
else
{
- return new MultiComponentList(new BinaryReader(stream), length / 12, false);
+ return new MultiComponentList(new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true), length / 12, false);
}
}
catch
diff --git a/Ultima/Sound.cs b/Ultima/Sound.cs
index a5b51c3..a831fab 100644
--- a/Ultima/Sound.cs
+++ b/Ultima/Sound.cs
@@ -132,7 +132,6 @@ public static UoSound GetSound(int soundId, out bool translated)
stream.ReadExactly(stringBuffer, 0, 32);
stream.ReadExactly(buffer, 0, length);
- stream.Close();
var resultBuffer = new byte[buffer.Length + (waveHeader.Length << 2)];
@@ -234,7 +233,6 @@ public static bool IsValidSound(int soundId, out string name, out bool translate
var stringBuffer = new byte[32];
stream.ReadExactly(stringBuffer, 0, 32);
- stream.Close();
name = Encoding.ASCII.GetString(stringBuffer); // seems that the null terminator's not being properly recognized :/
if (name.IndexOf('\0') > 0)
{
@@ -286,7 +284,6 @@ public static double GetSoundLength(int soundId)
return 0;
}
- stream.Close();
length -= 32; // mulheaderlength
len = length;
}
diff --git a/Ultima/Textures.cs b/Ultima/Textures.cs
index 5106dc3..f4e80ff 100644
--- a/Ultima/Textures.cs
+++ b/Ultima/Textures.cs
@@ -1,8 +1,10 @@
+using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
-using System.Security.Cryptography;
+using Ultima.Helpers;
namespace Ultima
{
@@ -15,7 +17,6 @@ public sealed class Textures
private struct Checksums
{
- public byte[] Checksum;
public int Position;
public int Length;
public int Extra;
@@ -130,43 +131,52 @@ public static unsafe Bitmap GetTexture(int index, out bool patched)
int size = extra == 0 ? 64 : 128;
- var bmp = new Bitmap(size, size, PixelFormat.Format16bppArgb1555);
- BitmapData bd = bmp.LockBits(new Rectangle(0, 0, size, size), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
-
- var line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
-
int max = size * size * 2;
- byte[] streamBuffer = new byte[max];
+ byte[] streamBuffer = ArrayPool.Shared.Rent(max);
+ try
+ {
+ stream.ReadExactly(streamBuffer, 0, max);
- stream.ReadExactly(streamBuffer, 0, max);
+ var bmp = new Bitmap(size, size, PixelFormat.Format16bppArgb1555);
+ BitmapData bd = bmp.LockBits(new Rectangle(0, 0, size, size), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
- fixed (byte* data = streamBuffer)
- {
- var binData = (ushort*)data;
- for (int y = 0; y < size; ++y, line += delta)
+ try
{
- ushort* cur = line;
- ushort* end = cur + size;
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
- while (cur < end)
+ fixed (byte* data = streamBuffer)
{
- *cur++ = (ushort)(*binData++ ^ 0x8000);
+ var binData = (ushort*)data;
+ for (int y = 0; y < size; ++y, line += delta)
+ {
+ ushort* cur = line;
+ ushort* end = cur + size;
+
+ while (cur < end)
+ {
+ *cur++ = (ushort)(*binData++ ^ 0x8000);
+ }
+ }
}
}
- }
-
- bmp.UnlockBits(bd);
+ finally
+ {
+ bmp.UnlockBits(bd);
+ }
- stream.Close();
+ if (!Files.CacheData)
+ {
+ return _cache[index] = bmp;
+ }
- if (!Files.CacheData)
+ return bmp;
+ }
+ finally
{
- return _cache[index] = bmp;
+ ArrayPool.Shared.Return(streamBuffer);
}
-
- return bmp;
}
public static unsafe void Save(string path)
@@ -174,7 +184,9 @@ public static unsafe void Save(string path)
string idx = Path.Combine(path, "texidx.mul");
string mul = Path.Combine(path, "texmaps.mul");
- List checksumList = new List();
+ // M3.5: xxHash128-keyed dedup index, replacing the old
+ // List+SHA256-bytes layout and its O(n²) linear scan.
+ Dictionary checksums = new Dictionary();
var memIdx = new MemoryStream();
var memMul = new MemoryStream();
@@ -198,55 +210,52 @@ public static unsafe void Save(string path)
}
else
{
- byte[] newChecksum;
- using (var sha = SHA256.Create())
- using (var ms = new MemoryStream())
- {
- bmp.Save(ms, ImageFormat.Bmp);
- newChecksum = sha.ComputeHash(ms.ToArray());
- }
-
- if (CompareSaveImages(checksumList, newChecksum, out Checksums sum))
- {
- binIdx.Write(sum.Position); // lookup
- binIdx.Write(sum.Length); // length
- binIdx.Write(sum.Extra); // extra
-
- continue;
- }
-
BitmapData bd = bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly,
PixelFormat.Format16bppArgb1555);
- var line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
+ try
+ {
+ UInt128 hash = bd.Hash128();
+ if (checksums.TryGetValue(hash, out Checksums existing))
+ {
+ binIdx.Write(existing.Position); // lookup
+ binIdx.Write(existing.Length); // length
+ binIdx.Write(existing.Extra); // extra
+ continue;
+ }
- binIdx.Write((int)binMul.BaseStream.Position); // lookup
- var length = (int)binMul.BaseStream.Position;
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
- for (int y = 0; y < bmp.Height; ++y, line += delta)
- {
- ushort* cur = line;
- for (int x = 0; x < bmp.Width; ++x)
+ binIdx.Write((int)binMul.BaseStream.Position); // lookup
+ var length = (int)binMul.BaseStream.Position;
+
+ for (int y = 0; y < bmp.Height; ++y, line += delta)
{
- binMul.Write((ushort)(cur[x] ^ 0x8000));
+ ushort* cur = line;
+ for (int x = 0; x < bmp.Width; ++x)
+ {
+ binMul.Write((ushort)(cur[x] ^ 0x8000));
+ }
}
- }
- int start = length;
- length = (int)binMul.BaseStream.Position - length;
- binIdx.Write(length);
- var extra = GetExtraFlag(length);
- binIdx.Write(extra);
- bmp.UnlockBits(bd);
+ int start = length;
+ length = (int)binMul.BaseStream.Position - length;
+ binIdx.Write(length);
+ var extra = GetExtraFlag(length);
+ binIdx.Write(extra);
- checksumList.Add(new Checksums
+ checksums[hash] = new Checksums
+ {
+ Position = start,
+ Length = length,
+ Extra = extra
+ };
+ }
+ finally
{
- Position = start,
- Length = length,
- Checksum = newChecksum,
- Extra = extra
- });
+ bmp.UnlockBits(bd);
+ }
}
}
@@ -267,40 +276,5 @@ private static int GetExtraFlag(int length)
return length == 0x8000 ? 1 : 0;
}
- private static bool CompareSaveImages(IReadOnlyList checksumList, IReadOnlyList newChecksum, out Checksums sum)
- {
- sum = new Checksums();
- for (int i = 0; i < checksumList.Count; ++i)
- {
- byte[] cmp = checksumList[i].Checksum;
- if ((cmp == null) || (newChecksum == null) || (cmp.Length != newChecksum.Count))
- {
- return false;
- }
-
- bool valid = true;
-
- for (int j = 0; j < cmp.Length; ++j)
- {
- if (cmp[j] == newChecksum[j])
- {
- continue;
- }
-
- valid = false;
- break;
- }
-
- if (!valid)
- {
- continue;
- }
-
- sum = checksumList[i];
- return true;
- }
-
- return false;
- }
}
}
\ No newline at end of file
diff --git a/Ultima/Ultima.csproj b/Ultima/Ultima.csproj
index 43e70b7..1fb0a2e 100644
--- a/Ultima/Ultima.csproj
+++ b/Ultima/Ultima.csproj
@@ -12,6 +12,9 @@
true
+
+
+
bin\$(Configuration)\
4096
From 750c6f49648eb4eb4514287d7813f3eb5a978c51 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:07:46 +0200
Subject: [PATCH 06/21] Replace treeview with other controls for faster loading
(tiledata, sounds, radarcol, multis, light, dress).
---
.../UserControls/DressControl.Designer.cs | 38 +-
.../UserControls/DressControl.cs | 275 +++---
.../UserControls/LightControl.Designer.cs | 37 +-
.../UserControls/LightControl.cs | 79 +-
.../UserControls/MultisControl.Designer.cs | 82 +-
.../UserControls/MultisControl.cs | 560 ++++++-----
.../RadarColorControl.Designer.cs | 66 +-
.../UserControls/RadarColorControl.cs | 705 +++++++++-----
.../UserControls/SoundsControl.Designer.cs | 42 +-
.../UserControls/SoundsControl.cs | 263 +++---
.../UserControls/TileDataControl.Designer.cs | 79 +-
.../UserControls/TileDataControl.cs | 870 ++++++++----------
.../UserControls/CompareRadarColControl.cs | 97 +-
13 files changed, 1788 insertions(+), 1405 deletions(-)
diff --git a/UoFiddler.Controls/UserControls/DressControl.Designer.cs b/UoFiddler.Controls/UserControls/DressControl.Designer.cs
index ea9cd0a..2579a8e 100644
--- a/UoFiddler.Controls/UserControls/DressControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/DressControl.Designer.cs
@@ -45,7 +45,8 @@ private void InitializeComponent()
ItemsSplitContainer = new System.Windows.Forms.SplitContainer();
SearchItemTextBox = new System.Windows.Forms.TextBox();
FindNextItemButton = new System.Windows.Forms.Button();
- treeViewItems = new System.Windows.Forms.TreeView();
+ listViewItems = new System.Windows.Forms.ListView();
+ listViewItemsColumn = new System.Windows.Forms.ColumnHeader();
splitContainer3 = new System.Windows.Forms.SplitContainer();
checkBoxHuman = new System.Windows.Forms.RadioButton();
checkBoxGargoyle = new System.Windows.Forms.RadioButton();
@@ -179,7 +180,7 @@ private void InitializeComponent()
//
// ItemsSplitContainer.Panel2
//
- ItemsSplitContainer.Panel2.Controls.Add(treeViewItems);
+ ItemsSplitContainer.Panel2.Controls.Add(listViewItems);
ItemsSplitContainer.Size = new System.Drawing.Size(248, 623);
ItemsSplitContainer.SplitterDistance = 40;
ItemsSplitContainer.SplitterWidth = 5;
@@ -205,16 +206,26 @@ private void InitializeComponent()
FindNextItemButton.UseVisualStyleBackColor = true;
FindNextItemButton.Click += FindNextItemButton_Click;
//
- // treeViewItems
- //
- treeViewItems.Dock = System.Windows.Forms.DockStyle.Fill;
- treeViewItems.Location = new System.Drawing.Point(0, 0);
- treeViewItems.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- treeViewItems.Name = "treeViewItems";
- treeViewItems.Size = new System.Drawing.Size(248, 578);
- treeViewItems.TabIndex = 1;
- treeViewItems.AfterSelect += AfterSelectTreeView;
- treeViewItems.DoubleClick += TreeViewItems_DoubleClick;
+ // listViewItems
+ //
+ listViewItems.Dock = System.Windows.Forms.DockStyle.Fill;
+ listViewItems.HideSelection = false;
+ listViewItems.Location = new System.Drawing.Point(0, 0);
+ listViewItems.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ listViewItems.Name = "listViewItems";
+ listViewItems.Size = new System.Drawing.Size(248, 578);
+ listViewItems.TabIndex = 1;
+ listViewItems.View = System.Windows.Forms.View.Details;
+ listViewItems.VirtualMode = true;
+ listViewItems.FullRowSelect = true;
+ listViewItems.MultiSelect = false;
+ listViewItems.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
+ listViewItemsColumn.Text = "Item";
+ listViewItemsColumn.Width = 240;
+ listViewItems.Columns.Add(listViewItemsColumn);
+ listViewItems.RetrieveVirtualItem += OnRetrieveItemVirtualItem;
+ listViewItems.SelectedIndexChanged += OnListItemSelectedIndexChanged;
+ listViewItems.DoubleClick += TreeViewItems_DoubleClick;
//
// splitContainer3
//
@@ -729,7 +740,8 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripMenuItem unDressToolStripMenuItem;
private System.Windows.Forms.SplitContainer ItemsSplitContainer;
private System.Windows.Forms.Button FindNextItemButton;
- private System.Windows.Forms.TreeView treeViewItems;
+ private System.Windows.Forms.ListView listViewItems;
+ private System.Windows.Forms.ColumnHeader listViewItemsColumn;
private System.Windows.Forms.TextBox SearchItemTextBox;
private System.Windows.Forms.ToolStripMenuItem asAnimatedGifToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem asAnimatedGifnoLoopingToolStripMenuItem;
diff --git a/UoFiddler.Controls/UserControls/DressControl.cs b/UoFiddler.Controls/UserControls/DressControl.cs
index 57921d2..0ea7978 100644
--- a/UoFiddler.Controls/UserControls/DressControl.cs
+++ b/UoFiddler.Controls/UserControls/DressControl.cs
@@ -35,7 +35,69 @@ public DressControl()
ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
_lastNodeIndex = 0;
- treeViewItems.HideSelection = false;
+ }
+
+ // Virtual ListView backing arrays. _displayedItems maps a row position
+ // to the wearable's objType. _displayedColors is parallel and holds
+ // the per-row foreground color, computed once at BuildDressList time
+ // (Color.Empty = default/inherit). Built fresh on every sort change.
+ private int[] _displayedItems = Array.Empty();
+ private Color[] _displayedColors = Array.Empty();
+
+ private int GetSelectedObjType()
+ {
+ return listViewItems.SelectedIndices.Count > 0
+ ? _displayedItems[listViewItems.SelectedIndices[0]]
+ : -1;
+ }
+
+ private static string FormatDressRow(int objType, byte quality, string name)
+ {
+ return string.Create(null, stackalloc char[80], $"0x{objType:X4} (0x{quality:X2}) {name}");
+ }
+
+ private void OnRetrieveItemVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
+ {
+ if ((uint)e.ItemIndex >= (uint)_displayedItems.Length)
+ {
+ e.Item = new ListViewItem(string.Empty);
+ return;
+ }
+
+ int objType = _displayedItems[e.ItemIndex];
+ ref readonly ItemData row = ref TileData.ItemTable[objType];
+ var lvi = new ListViewItem(FormatDressRow(objType, row.Quality, row.Name ?? string.Empty))
+ {
+ Tag = objType
+ };
+
+ Color color = _displayedColors[e.ItemIndex];
+ if (!color.IsEmpty)
+ {
+ lvi.ForeColor = color;
+ }
+
+ e.Item = lvi;
+ }
+
+ private void OnListItemSelectedIndexChanged(object sender, EventArgs e)
+ {
+ int objType = GetSelectedObjType();
+ if (objType >= 0)
+ {
+ UpdateSelection(objType);
+ }
+ }
+
+ private void SelectRow(int rowPos)
+ {
+ listViewItems.SelectedIndices.Clear();
+ if ((uint)rowPos < (uint)_displayedItems.Length)
+ {
+ listViewItems.SelectedIndices.Add(rowPos);
+ listViewItems.EnsureVisible(rowPos);
+ listViewItems.FocusedItem = listViewItems.Items[rowPos];
+ }
}
private static readonly int[] _drawOrder ={
@@ -679,9 +741,9 @@ private void AnimTick(object sender, EventArgs e)
DressPic.Invalidate();
}
- private void AfterSelectTreeView(object sender, TreeViewEventArgs e)
+ private void UpdateSelection(int objType)
{
- int ani = TileData.ItemTable[(int)e.Node.Tag].Animation;
+ int ani = TileData.ItemTable[objType].Animation;
int gump = ani + 50000;
int gumpOrig = gump;
int hue = 0;
@@ -736,13 +798,13 @@ private void AfterSelectTreeView(object sender, TreeViewEventArgs e)
TextBox.Clear();
TextBox.AppendText(
- $"Objtype: 0x{(int)e.Node.Tag:X4}\nLayer: 0x{TileData.ItemTable[(int)e.Node.Tag].Quality:X2}\n");
+ $"Objtype: 0x{objType:X4}\nLayer: 0x{TileData.ItemTable[objType].Quality:X2}\n");
TextBox.AppendText($"GumpID: 0x{gump:X4} (0x{gumpOrig:X4})\nHue: {hue + 1}\n");
- TextBox.AppendText($"Animation: 0x{ani:X4} (0x{TileData.ItemTable[(int)e.Node.Tag].Animation:X4})\n");
+ TextBox.AppendText($"Animation: 0x{ani:X4} (0x{TileData.ItemTable[objType].Animation:X4})\n");
TextBox.AppendText(
$"ValidGump: {Gumps.IsValidIndex(gump)}\nValidAnim: {Animations.IsActionDefined(ani, 0, 0)}\n");
TextBox.AppendText(
- $"ValidLayer: {Array.IndexOf(_drawOrder, TileData.ItemTable[(int)e.Node.Tag].Quality) != -1}");
+ $"ValidLayer: {Array.IndexOf(_drawOrder, TileData.ItemTable[objType].Quality) != -1}");
}
private void OnClick_Animate(object sender, EventArgs e)
@@ -801,12 +863,12 @@ private void OnClick_Dress(object sender, EventArgs e)
private void DressItem()
{
- if (treeViewItems.SelectedNode == null)
+ int objType = GetSelectedObjType();
+ if (objType < 0)
{
return;
}
- int objType = (int) treeViewItems.SelectedNode.Tag;
int layer = TileData.ItemTable[objType].Quality;
@@ -911,7 +973,10 @@ private void CheckedListBox_Change(object sender, EventArgs e)
private void OnChangeSort(object sender, EventArgs e)
{
- treeViewItems.TreeViewNodeSorter = LayerSort.Checked ? new LayerSorter() : (IComparer)new ObjTypeSorter();
+ // Rebuild from scratch — N is small (wearables only), and rebuilding
+ // is simpler than mutating the parallel _displayedItems/_displayedColors
+ // arrays in place.
+ BuildDressList();
}
private void OnClick_ChangeDisplay(object sender, EventArgs e)
@@ -936,61 +1001,96 @@ private void OnClick_ChangeDisplay(object sender, EventArgs e)
private void BuildDressList()
{
- treeViewItems.BeginUpdate();
- treeViewItems.Nodes.Clear();
+ if (TileData.ItemTable == null)
+ {
+ _displayedItems = Array.Empty();
+ _displayedColors = Array.Empty();
+ listViewItems.VirtualListSize = 0;
+ listViewItems.Invalidate();
+ return;
+ }
- if (TileData.ItemTable != null)
+ var items = new List(2048);
+ var colors = new List(2048);
+ for (int i = 0; i < TileData.ItemTable.Length; ++i)
{
- for (int i = 0; i < TileData.ItemTable.Length; ++i)
+ if (!TileData.ItemTable[i].Wearable)
{
- if (!TileData.ItemTable[i].Wearable)
- {
- continue;
- }
+ continue;
+ }
- int ani = TileData.ItemTable[i].Animation;
- if (ani == 0)
- {
- continue;
- }
+ int ani = TileData.ItemTable[i].Animation;
+ if (ani == 0)
+ {
+ continue;
+ }
- int hue = 0;
- int gump = ani + 50000;
+ int hue = 0;
+ int gump = ani + 50000;
- ConvertBody(ref ani, ref gump, ref hue);
+ ConvertBody(ref ani, ref gump, ref hue);
- if (!Gumps.IsValidIndex(gump))
- {
- ConvertGump(ref gump, ref hue);
- }
+ if (!Gumps.IsValidIndex(gump))
+ {
+ ConvertGump(ref gump, ref hue);
+ }
- bool hasAnimation = Animations.IsActionDefined(ani, 0, 0);
+ bool hasAnimation = Animations.IsActionDefined(ani, 0, 0);
+ bool hasGump = Gumps.IsValidIndex(gump);
- bool hasGump = Gumps.IsValidIndex(gump);
+ Color color = Color.Empty;
+ if (Array.IndexOf(_drawOrder, TileData.ItemTable[i].Quality) == -1)
+ {
+ color = Options.DarkMode ? Color.OrangeRed : Color.DarkRed;
+ }
+ else if (!hasAnimation)
+ {
+ color = !hasGump ? (Options.DarkMode ? Color.OrangeRed : Color.Red) : Color.Orange;
+ }
+ else if (!hasGump)
+ {
+ color = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
+ }
- TreeNode node = new TreeNode($"0x{i:X4} (0x{TileData.ItemTable[i].Quality:X2}) {TileData.ItemTable[i].Name}")
- {
- Tag = i
- };
+ items.Add(i);
+ colors.Add(color);
+ }
- if (Array.IndexOf(_drawOrder, TileData.ItemTable[i].Quality) == -1)
- {
- node.ForeColor = Options.DarkMode ? Color.OrangeRed : Color.DarkRed;
- }
- else if (!hasAnimation)
- {
- node.ForeColor = !hasGump ? Options.DarkMode ? Color.OrangeRed : Color.Red : Color.Orange;
- }
- else if (!hasGump)
+ // Default order is ascending objType (the iteration order above).
+ // Layer-sort: stable sort the parallel arrays by ItemTable[id].Quality.
+ if (LayerSort.Checked)
+ {
+ var perm = new int[items.Count];
+ for (int k = 0; k < perm.Length; ++k)
+ {
+ perm[k] = k;
+ }
+ Array.Sort(perm, (a, b) =>
+ {
+ int qa = TileData.ItemTable[items[a]].Quality;
+ int qb = TileData.ItemTable[items[b]].Quality;
+ if (qa != qb)
{
- node.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
+ return qa - qb;
}
-
- treeViewItems.Nodes.Add(node);
+ return a - b; // stable
+ });
+ _displayedItems = new int[items.Count];
+ _displayedColors = new Color[items.Count];
+ for (int k = 0; k < perm.Length; ++k)
+ {
+ _displayedItems[k] = items[perm[k]];
+ _displayedColors[k] = colors[perm[k]];
}
}
+ else
+ {
+ _displayedItems = items.ToArray();
+ _displayedColors = colors.ToArray();
+ }
- treeViewItems.EndUpdate();
+ listViewItems.VirtualListSize = _displayedItems.Length;
+ listViewItems.Invalidate();
}
public void RefreshDrawing()
@@ -1078,13 +1178,14 @@ private void OnScroll_Action(object sender, EventArgs e)
private void OnResizePictureDress(object sender, EventArgs e)
{
- if (treeViewItems.SelectedNode == null)
+ int objType = GetSelectedObjType();
+ if (objType < 0)
{
return;
}
pictureBoxDress.Image = new Bitmap(pictureBoxDress.Width, pictureBoxDress.Height);
- AfterSelectTreeView(this, new TreeViewEventArgs(treeViewItems.SelectedNode));
+ UpdateSelection(objType);
}
private void OnResizeDressPic(object sender, EventArgs e)
@@ -1628,7 +1729,10 @@ private void MountTextBoxOnKeyDown(object sender, KeyEventArgs e)
RefreshDrawing();
}
- private readonly List _searchResults = new List();
+ // Search results are *row positions* in _displayedItems, not raw objTypes,
+ // so cycling next/previous matches the user's visual order. Recomputed
+ // when the search text changes or the list is rebuilt.
+ private readonly List _searchResults = new List();
private int _lastNodeIndex;
@@ -1645,13 +1749,7 @@ private void SearchByName()
if (_lastSearchText != searchText)
{
- _searchResults.Clear();
-
- _lastSearchText = searchText;
-
- _lastNodeIndex = 0;
-
- SearchNodes(searchText, treeViewItems.Nodes[0]);
+ RebuildSearchResults(searchText);
}
if (_lastNodeIndex < 0 || _searchResults.Count == 0)
@@ -1664,23 +1762,24 @@ private void SearchByName()
_lastNodeIndex = 0;
}
- TreeNode selectedNode = _searchResults[_lastNodeIndex];
-
+ SelectRow(_searchResults[_lastNodeIndex]);
_lastNodeIndex++;
-
- treeViewItems.SelectedNode = selectedNode;
}
- private void SearchNodes(string searchText, TreeNode startNode)
+ private void RebuildSearchResults(string searchText)
{
- while (startNode != null)
+ _searchResults.Clear();
+ _lastSearchText = searchText;
+ _lastNodeIndex = 0;
+ for (int i = 0; i < _displayedItems.Length; ++i)
{
- if (startNode.Text.ContainsCaseInsensitive(searchText))
+ int objType = _displayedItems[i];
+ ref readonly ItemData row = ref TileData.ItemTable[objType];
+ string text = FormatDressRow(objType, row.Quality, row.Name ?? string.Empty);
+ if (text.ContainsCaseInsensitive(searchText))
{
- _searchResults.Add(startNode);
+ _searchResults.Add(i);
}
-
- startNode = startNode.NextNode;
}
}
@@ -1712,10 +1811,7 @@ private void SearchByNamePrevious()
if (_lastSearchText != searchText)
{
- _searchResults.Clear();
- _lastSearchText = searchText;
- _lastNodeIndex = 0;
- SearchNodes(searchText, treeViewItems.Nodes[0]);
+ RebuildSearchResults(searchText);
}
if (_searchResults.Count == 0)
@@ -1734,7 +1830,7 @@ private void SearchByNamePrevious()
_lastNodeIndex = _searchResults.Count + _lastNodeIndex;
}
- treeViewItems.SelectedNode = _searchResults[_lastNodeIndex];
+ SelectRow(_searchResults[_lastNodeIndex]);
_lastNodeIndex++;
}
@@ -1784,39 +1880,6 @@ public AnimEntry()
}
}
- public class ObjTypeSorter : IComparer
- {
- public int Compare(object x, object y)
- {
- TreeNode tx = x as TreeNode;
- TreeNode ty = y as TreeNode;
- return string.CompareOrdinal(tx?.Text, ty?.Text);
- }
- }
-
- public class LayerSorter : IComparer
- {
- public int Compare(object x, object y)
- {
- TreeNode tx = x as TreeNode;
- TreeNode ty = y as TreeNode;
-
- int layerX = TileData.ItemTable[(int)tx.Tag].Quality;
- int layerY = TileData.ItemTable[(int)ty.Tag].Quality;
-
- if (layerX == layerY)
- {
- return 0;
- }
-
- if (layerX < layerY)
- {
- return -1;
- }
-
- return 1;
- }
- }
public static class GumpTable
{
diff --git a/UoFiddler.Controls/UserControls/LightControl.Designer.cs b/UoFiddler.Controls/UserControls/LightControl.Designer.cs
index 382bf98..85c2ebf 100644
--- a/UoFiddler.Controls/UserControls/LightControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/LightControl.Designer.cs
@@ -41,7 +41,8 @@ private void InitializeComponent()
{
components = new System.ComponentModel.Container();
splitContainer = new System.Windows.Forms.SplitContainer();
- treeViewLights = new System.Windows.Forms.TreeView();
+ listViewLights = new System.Windows.Forms.ListView();
+ listViewLightsColumn = new System.Windows.Forms.ColumnHeader();
treeViewContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
exportImageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
asBmpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -79,7 +80,7 @@ private void InitializeComponent()
//
// splitContainer.Panel1
//
- splitContainer.Panel1.Controls.Add(treeViewLights);
+ splitContainer.Panel1.Controls.Add(listViewLights);
//
// splitContainer.Panel2
//
@@ -89,17 +90,24 @@ private void InitializeComponent()
splitContainer.SplitterWidth = 5;
splitContainer.TabIndex = 0;
//
- // treeViewLights
- //
- treeViewLights.ContextMenuStrip = treeViewContextMenuStrip;
- treeViewLights.Dock = System.Windows.Forms.DockStyle.Fill;
- treeViewLights.HideSelection = false;
- treeViewLights.Location = new System.Drawing.Point(0, 0);
- treeViewLights.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- treeViewLights.Name = "treeViewLights";
- treeViewLights.Size = new System.Drawing.Size(242, 380);
- treeViewLights.TabIndex = 0;
- treeViewLights.AfterSelect += AfterSelect;
+ // listViewLights
+ //
+ listViewLights.ContextMenuStrip = treeViewContextMenuStrip;
+ listViewLights.Dock = System.Windows.Forms.DockStyle.Fill;
+ listViewLights.HideSelection = false;
+ listViewLights.Location = new System.Drawing.Point(0, 0);
+ listViewLights.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ listViewLights.Name = "listViewLights";
+ listViewLights.Size = new System.Drawing.Size(242, 380);
+ listViewLights.TabIndex = 0;
+ listViewLights.View = System.Windows.Forms.View.Details;
+ listViewLights.FullRowSelect = true;
+ listViewLights.MultiSelect = false;
+ listViewLights.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
+ listViewLightsColumn.Text = "Light";
+ listViewLightsColumn.Width = 240;
+ listViewLights.Columns.Add(listViewLightsColumn);
+ listViewLights.SelectedIndexChanged += AfterSelect;
//
// treeViewContextMenuStrip
//
@@ -277,6 +285,7 @@ private void InitializeComponent()
private System.Windows.Forms.SplitContainer splitContainer;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
- private System.Windows.Forms.TreeView treeViewLights;
+ private System.Windows.Forms.ListView listViewLights;
+ private System.Windows.Forms.ColumnHeader listViewLightsColumn;
}
}
diff --git a/UoFiddler.Controls/UserControls/LightControl.cs b/UoFiddler.Controls/UserControls/LightControl.cs
index e52b000..f5fd910 100644
--- a/UoFiddler.Controls/UserControls/LightControl.cs
+++ b/UoFiddler.Controls/UserControls/LightControl.cs
@@ -59,10 +59,10 @@ private void OnLoad(object sender, EventArgs e)
Cursor.Current = Cursors.WaitCursor;
Options.LoadedUltimaClass["Light"] = true;
- treeViewLights.BeginUpdate();
+ listViewLights.BeginUpdate();
try
{
- treeViewLights.Nodes.Clear();
+ listViewLights.Items.Clear();
for (int i = 0; i < Ultima.Light.GetCount(); ++i)
{
if (!Ultima.Light.TestLight(i))
@@ -70,21 +70,18 @@ private void OnLoad(object sender, EventArgs e)
continue;
}
- var treeNode = new TreeNode(i.ToString())
- {
- Tag = i
- };
- treeViewLights.Nodes.Add(treeNode);
+ listViewLights.Items.Add(new ListViewItem(i.ToString()) { Tag = i });
}
}
finally
{
- treeViewLights.EndUpdate();
+ listViewLights.EndUpdate();
}
- if (treeViewLights.Nodes.Count > 0)
+ if (listViewLights.Items.Count > 0)
{
- treeViewLights.SelectedNode = treeViewLights.Nodes[0];
+ listViewLights.Items[0].Selected = true;
+ listViewLights.Items[0].EnsureVisible();
}
if (!_loaded)
@@ -101,16 +98,22 @@ private void OnFilePathChangeEvent()
Reload();
}
+ private int GetSelectedLightId()
+ {
+ return listViewLights.SelectedItems.Count > 0 ? (int)listViewLights.SelectedItems[0].Tag : -1;
+ }
+
private unsafe Bitmap GetImage()
{
- if (treeViewLights.SelectedNode == null)
+ int selectedId = GetSelectedLightId();
+ if (selectedId < 0)
{
return null;
}
if (!iGPreviewToolStripMenuItem.Checked)
{
- return Ultima.Light.GetLight((int)treeViewLights.SelectedNode.Tag);
+ return Ultima.Light.GetLight(selectedId);
}
var bit = new Bitmap(pictureBoxPreview.Width, pictureBoxPreview.Height);
@@ -138,7 +141,7 @@ private unsafe Bitmap GetImage()
}
}
- byte[] light = Ultima.Light.GetRawLight((int)treeViewLights.SelectedNode.Tag, out int lightWidth, out int lightHeight);
+ byte[] light = Ultima.Light.GetRawLight(selectedId, out int lightWidth, out int lightHeight);
if (light == null)
{
@@ -198,19 +201,20 @@ private unsafe Bitmap GetImage()
return bit;
}
- private void AfterSelect(object sender, TreeViewEventArgs e)
+ private void AfterSelect(object sender, EventArgs e)
{
pictureBoxPreview.Image = GetImage();
}
private void OnClickRemove(object sender, EventArgs e)
{
- if (treeViewLights.SelectedNode == null)
+ if (listViewLights.SelectedItems.Count == 0)
{
return;
}
- int i = (int)treeViewLights.SelectedNode.Tag;
+ var selected = listViewLights.SelectedItems[0];
+ int i = (int)selected.Tag;
DialogResult result = MessageBox.Show(string.Format("Are you sure to remove {0} (0x{0:X})", i), "Remove",
MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (result != DialogResult.Yes)
@@ -219,14 +223,14 @@ private void OnClickRemove(object sender, EventArgs e)
}
Ultima.Light.Remove(i);
- treeViewLights.Nodes.Remove(treeViewLights.SelectedNode);
- treeViewLights.Invalidate();
+ listViewLights.Items.Remove(selected);
+ listViewLights.Invalidate();
Options.ChangedUltimaClass["Light"] = true;
}
private void OnClickReplace(object sender, EventArgs e)
{
- if (treeViewLights.SelectedNode == null)
+ if (listViewLights.SelectedItems.Count == 0)
{
return;
}
@@ -251,12 +255,12 @@ private void OnClickReplace(object sender, EventArgs e)
bitmap = Utils.ConvertBmp(bitmap);
}
- int i = (int)treeViewLights.SelectedNode.Tag;
+ int i = (int)listViewLights.SelectedItems[0].Tag;
Ultima.Light.Replace(i, bitmap);
- treeViewLights.Invalidate();
- AfterSelect(this, null);
+ listViewLights.Invalidate();
+ AfterSelect(this, EventArgs.Empty);
Options.ChangedUltimaClass["Light"] = true;
}
@@ -306,29 +310,28 @@ private void OnKeyDownInsert(object sender, KeyEventArgs e)
var bmp = new Bitmap(dialog.FileName);
Ultima.Light.Replace(index, bmp);
- var treeNode = new TreeNode(index.ToString())
- {
- Tag = index
- };
+ var newItem = new ListViewItem(index.ToString()) { Tag = index };
bool done = false;
- foreach (TreeNode node in treeViewLights.Nodes)
+ foreach (ListViewItem item in listViewLights.Items)
{
- if ((int)node.Tag <= index)
+ if ((int)item.Tag <= index)
{
continue;
}
- treeViewLights.Nodes.Insert(node.Index, treeNode);
+ listViewLights.Items.Insert(item.Index, newItem);
done = true;
break;
}
if (!done)
{
- treeViewLights.Nodes.Add(treeNode);
+ listViewLights.Items.Add(newItem);
}
- treeViewLights.Invalidate();
- treeViewLights.SelectedNode = treeNode;
+ listViewLights.Invalidate();
+ listViewLights.SelectedItems.Clear();
+ newItem.Selected = true;
+ newItem.EnsureVisible();
Options.ChangedUltimaClass["Light"] = true;
}
}
@@ -342,13 +345,13 @@ private void OnClickSave(object sender, EventArgs e)
private void OnClickExportBmp(object sender, EventArgs e)
{
- if (treeViewLights.SelectedNode == null)
+ int i = GetSelectedLightId();
+ if (i < 0)
{
return;
}
string path = Options.OutputPath;
- int i = (int)treeViewLights.SelectedNode.Tag;
string fileName = Path.Combine(path, $"Light {Utils.FormatExportId(i)}.bmp");
Light.GetLight(i).Save(fileName, ImageFormat.Bmp);
FileSavedDialog.Show(FindForm(), fileName, "Light saved successfully.");
@@ -356,13 +359,13 @@ private void OnClickExportBmp(object sender, EventArgs e)
private void OnClickExportTiff(object sender, EventArgs e)
{
- if (treeViewLights.SelectedNode == null)
+ int i = GetSelectedLightId();
+ if (i < 0)
{
return;
}
string path = Options.OutputPath;
- int i = (int)treeViewLights.SelectedNode.Tag;
string fileName = Path.Combine(path, $"Light {Utils.FormatExportId(i)}.tiff");
Ultima.Light.GetLight(i).Save(fileName, ImageFormat.Tiff);
FileSavedDialog.Show(FindForm(), fileName, "Light saved successfully.");
@@ -370,13 +373,13 @@ private void OnClickExportTiff(object sender, EventArgs e)
private void OnClickExportJpg(object sender, EventArgs e)
{
- if (treeViewLights.SelectedNode == null)
+ int i = GetSelectedLightId();
+ if (i < 0)
{
return;
}
string path = Options.OutputPath;
- int i = (int)treeViewLights.SelectedNode.Tag;
string fileName = Path.Combine(path, $"Light {Utils.FormatExportId(i)}.jpg");
Ultima.Light.GetLight(i).Save(fileName, ImageFormat.Jpeg);
FileSavedDialog.Show(FindForm(), fileName, "Light saved successfully.");
diff --git a/UoFiddler.Controls/UserControls/MultisControl.Designer.cs b/UoFiddler.Controls/UserControls/MultisControl.Designer.cs
index 76def1e..ef5650a 100644
--- a/UoFiddler.Controls/UserControls/MultisControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/MultisControl.Designer.cs
@@ -41,7 +41,8 @@ private void InitializeComponent()
{
components = new System.ComponentModel.Container();
splitContainer2 = new System.Windows.Forms.SplitContainer();
- TreeViewMulti = new System.Windows.Forms.TreeView();
+ listViewMulti = new System.Windows.Forms.ListView();
+ listViewMultiColumn = new System.Windows.Forms.ColumnHeader();
contextMenuStrip2 = new System.Windows.Forms.ContextMenuStrip(components);
toolStripMenuItem4 = new System.Windows.Forms.ToolStripMenuItem();
importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -106,7 +107,8 @@ private void InitializeComponent()
tabPageMul = new System.Windows.Forms.TabPage();
tabPageUop = new System.Windows.Forms.TabPage();
splitContainerUop = new System.Windows.Forms.SplitContainer();
- treeViewUop = new System.Windows.Forms.TreeView();
+ listViewUop = new System.Windows.Forms.ListView();
+ listViewUopColumn = new System.Windows.Forms.ColumnHeader();
contextMenuStripUop = new System.Windows.Forms.ContextMenuStrip(components);
uopExportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
uopToUOAToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -192,7 +194,7 @@ private void InitializeComponent()
//
// splitContainer2.Panel1
//
- splitContainer2.Panel1.Controls.Add(TreeViewMulti);
+ splitContainer2.Panel1.Controls.Add(listViewMulti);
splitContainer2.Panel1.Controls.Add(toolStrip1);
//
// splitContainer2.Panel2
@@ -203,18 +205,27 @@ private void InitializeComponent()
splitContainer2.SplitterWidth = 5;
splitContainer2.TabIndex = 1;
//
- // TreeViewMulti
- //
- TreeViewMulti.ContextMenuStrip = contextMenuStrip2;
- TreeViewMulti.Dock = System.Windows.Forms.DockStyle.Fill;
- TreeViewMulti.HideSelection = false;
- TreeViewMulti.Location = new System.Drawing.Point(0, 25);
- TreeViewMulti.Margin = new System.Windows.Forms.Padding(0);
- TreeViewMulti.Name = "TreeViewMulti";
- TreeViewMulti.ShowNodeToolTips = true;
- TreeViewMulti.Size = new System.Drawing.Size(245, 389);
- TreeViewMulti.TabIndex = 0;
- TreeViewMulti.AfterSelect += AfterSelect_Multi;
+ // listViewMulti
+ //
+ listViewMulti.ContextMenuStrip = contextMenuStrip2;
+ listViewMulti.Dock = System.Windows.Forms.DockStyle.Fill;
+ listViewMulti.HideSelection = false;
+ listViewMulti.Location = new System.Drawing.Point(0, 25);
+ listViewMulti.Margin = new System.Windows.Forms.Padding(0);
+ listViewMulti.Name = "listViewMulti";
+ listViewMulti.ShowItemToolTips = true;
+ listViewMulti.Size = new System.Drawing.Size(245, 389);
+ listViewMulti.TabIndex = 0;
+ listViewMulti.View = System.Windows.Forms.View.Details;
+ listViewMulti.VirtualMode = true;
+ listViewMulti.FullRowSelect = true;
+ listViewMulti.MultiSelect = false;
+ listViewMulti.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
+ listViewMultiColumn.Text = "Multi";
+ listViewMultiColumn.Width = 240;
+ listViewMulti.Columns.Add(listViewMultiColumn);
+ listViewMulti.RetrieveVirtualItem += OnRetrieveMultiVirtualItem;
+ listViewMulti.SelectedIndexChanged += AfterSelect_Multi;
//
// contextMenuStrip2
//
@@ -769,7 +780,7 @@ private void InitializeComponent()
//
// splitContainerUop.Panel1
//
- splitContainerUop.Panel1.Controls.Add(treeViewUop);
+ splitContainerUop.Panel1.Controls.Add(listViewUop);
splitContainerUop.Panel1.Controls.Add(toolStripUop);
//
// splitContainerUop.Panel2
@@ -780,18 +791,27 @@ private void InitializeComponent()
splitContainerUop.SplitterWidth = 5;
splitContainerUop.TabIndex = 0;
//
- // treeViewUop
- //
- treeViewUop.ContextMenuStrip = contextMenuStripUop;
- treeViewUop.Dock = System.Windows.Forms.DockStyle.Fill;
- treeViewUop.HideSelection = false;
- treeViewUop.Location = new System.Drawing.Point(0, 25);
- treeViewUop.Margin = new System.Windows.Forms.Padding(0);
- treeViewUop.Name = "treeViewUop";
- treeViewUop.ShowNodeToolTips = true;
- treeViewUop.Size = new System.Drawing.Size(245, 389);
- treeViewUop.TabIndex = 0;
- treeViewUop.AfterSelect += AfterSelect_UopMulti;
+ // listViewUop
+ //
+ listViewUop.ContextMenuStrip = contextMenuStripUop;
+ listViewUop.Dock = System.Windows.Forms.DockStyle.Fill;
+ listViewUop.HideSelection = false;
+ listViewUop.Location = new System.Drawing.Point(0, 25);
+ listViewUop.Margin = new System.Windows.Forms.Padding(0);
+ listViewUop.Name = "listViewUop";
+ listViewUop.ShowItemToolTips = true;
+ listViewUop.Size = new System.Drawing.Size(245, 389);
+ listViewUop.TabIndex = 0;
+ listViewUop.View = System.Windows.Forms.View.Details;
+ listViewUop.VirtualMode = true;
+ listViewUop.FullRowSelect = true;
+ listViewUop.MultiSelect = false;
+ listViewUop.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
+ listViewUopColumn.Text = "Multi";
+ listViewUopColumn.Width = 240;
+ listViewUop.Columns.Add(listViewUopColumn);
+ listViewUop.RetrieveVirtualItem += OnRetrieveUopVirtualItem;
+ listViewUop.SelectedIndexChanged += AfterSelect_UopMulti;
//
// contextMenuStripUop
//
@@ -1148,7 +1168,8 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripMenuItem toUOAToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem toWSCFileToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem toWscToolStripMenuItem;
- private System.Windows.Forms.TreeView TreeViewMulti;
+ private System.Windows.Forms.ListView listViewMulti;
+ private System.Windows.Forms.ColumnHeader listViewMultiColumn;
private System.Windows.Forms.ColorDialog colorDialog;
private System.Windows.Forms.ToolStripMenuItem ChangeBackgroundColorToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
@@ -1168,7 +1189,8 @@ private void InitializeComponent()
private System.Windows.Forms.TabPage tabPageMul;
private System.Windows.Forms.TabPage tabPageUop;
private System.Windows.Forms.SplitContainer splitContainerUop;
- private System.Windows.Forms.TreeView treeViewUop;
+ private System.Windows.Forms.ListView listViewUop;
+ private System.Windows.Forms.ColumnHeader listViewUopColumn;
private System.Windows.Forms.ContextMenuStrip contextMenuStripUop;
private System.Windows.Forms.ToolStripMenuItem uopExportToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem uopToTextfileToolStripMenuItem;
diff --git a/UoFiddler.Controls/UserControls/MultisControl.cs b/UoFiddler.Controls/UserControls/MultisControl.cs
index b3e6241..e57760d 100644
--- a/UoFiddler.Controls/UserControls/MultisControl.cs
+++ b/UoFiddler.Controls/UserControls/MultisControl.cs
@@ -48,6 +48,121 @@ public MultisControl()
private bool _loaded;
private bool _showFreeSlots;
private readonly MultisControl _refMarker;
+
+ // Virtual ListView backing: row index → multi id. _mulIds includes both
+ // present and (when _showFreeSlots is on) empty slots; emptiness is
+ // resolved at draw time via Multis.GetComponents.
+ private int[] _mulIds = Array.Empty();
+ private int[] _uopIds = Array.Empty();
+
+ private int GetSelectedMulId()
+ {
+ return listViewMulti.SelectedIndices.Count > 0 && listViewMulti.SelectedIndices[0] < _mulIds.Length
+ ? _mulIds[listViewMulti.SelectedIndices[0]]
+ : -1;
+ }
+
+ private int GetSelectedUopId()
+ {
+ return listViewUop.SelectedIndices.Count > 0 && listViewUop.SelectedIndices[0] < _uopIds.Length
+ ? _uopIds[listViewUop.SelectedIndices[0]]
+ : -1;
+ }
+
+ private void OnRetrieveMultiVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
+ {
+ if ((uint)e.ItemIndex >= (uint)_mulIds.Length)
+ {
+ e.Item = new ListViewItem(string.Empty);
+ return;
+ }
+
+ int id = _mulIds[e.ItemIndex];
+ // Special-case the "missing UOP file" placeholder row (id == -1).
+ if (id < 0)
+ {
+ e.Item = new ListViewItem("multicollection.uop not found or path is not set.") { Tag = -1 };
+ return;
+ }
+
+ var lvi = new ListViewItem(BuildNodeLabel(id))
+ {
+ Tag = id,
+ ToolTipText = BuildToolTip(id)
+ };
+ if (_showFreeSlots && Multis.GetComponents(id) == MultiComponentList.Empty)
+ {
+ lvi.ForeColor = Color.Red;
+ }
+ e.Item = lvi;
+ }
+
+ private void OnRetrieveUopVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
+ {
+ if ((uint)e.ItemIndex >= (uint)_uopIds.Length)
+ {
+ e.Item = new ListViewItem(string.Empty);
+ return;
+ }
+
+ int id = _uopIds[e.ItemIndex];
+ if (id < 0)
+ {
+ e.Item = new ListViewItem("multicollection.uop not found or path is not set.") { Tag = -1 };
+ return;
+ }
+
+ e.Item = new ListViewItem(BuildNodeLabel(id))
+ {
+ Tag = id,
+ ToolTipText = BuildToolTip(id)
+ };
+ }
+
+ private string BuildToolTip(int id)
+ {
+ if (_xmlElementMultis == null)
+ {
+ return null;
+ }
+ string name = "";
+ foreach (XmlNode xMultiNode in _xmlElementMultis.SelectNodes("/Multis/Multi[@id='" + id + "']"))
+ {
+ name = xMultiNode.Attributes["name"].Value;
+ }
+ string tooltipText = null;
+ foreach (XmlNode xMultiNode in _xmlElementMultis.SelectNodes("/Multis/ToolTip[@id='" + id + "']"))
+ {
+ tooltipText = xMultiNode.Attributes["text"].Value;
+ }
+ if (tooltipText != null)
+ {
+ return name + "\r\n" + tooltipText;
+ }
+ return name;
+ }
+
+ private void SelectMulRow(int rowPos)
+ {
+ listViewMulti.SelectedIndices.Clear();
+ if ((uint)rowPos < (uint)_mulIds.Length)
+ {
+ listViewMulti.SelectedIndices.Add(rowPos);
+ listViewMulti.EnsureVisible(rowPos);
+ listViewMulti.FocusedItem = listViewMulti.Items[rowPos];
+ }
+ }
+
+ private void SelectUopRow(int rowPos)
+ {
+ listViewUop.SelectedIndices.Clear();
+ if ((uint)rowPos < (uint)_uopIds.Length)
+ {
+ listViewUop.SelectedIndices.Add(rowPos);
+ listViewUop.EnsureVisible(rowPos);
+ listViewUop.FocusedItem = listViewUop.Items[rowPos];
+ }
+ }
private bool _useTransparencyForPng = true;
private bool _previewFitMode = true;
private Bitmap _mulBitmap;
@@ -90,40 +205,6 @@ private string BuildNodeLabel(int i)
return $"{i,5} (0x{i:X}) {name}";
}
- private TreeNode BuildMulNode(int i, MultiComponentList multi)
- {
- TreeNode node;
- if (_xmlDocument == null)
- {
- node = new TreeNode(BuildNodeLabel(i));
- }
- else
- {
- node = new TreeNode(BuildNodeLabel(i));
- XmlNodeList xMultiNodeList = _xmlElementMultis.SelectNodes("/Multis/Multi[@id='" + i + "']");
- string name = "";
- foreach (XmlNode xMultiNode in xMultiNodeList)
- {
- name = xMultiNode.Attributes["name"].Value;
- }
-
- XmlNodeList tooltipList = _xmlElementMultis.SelectNodes("/Multis/ToolTip[@id='" + i + "']");
- foreach (XmlNode xMultiNode in tooltipList)
- {
- node.ToolTipText = name + "\r\n" + xMultiNode.Attributes["text"].Value;
- }
-
- if (tooltipList.Count == 0)
- {
- node.ToolTipText = name;
- }
- }
-
- node.Tag = multi;
- node.Name = i.ToString();
- return node;
- }
-
private void ApplyDarkModeIfNeeded()
{
if (Options.DarkMode)
@@ -163,32 +244,11 @@ private void OnLoad(object sender, EventArgs e)
Options.LoadedUltimaClass["Multis"] = true;
Options.LoadedUltimaClass["Hues"] = true;
- TreeViewMulti.BeginUpdate();
- try
- {
- TreeViewMulti.Nodes.Clear();
- var cache = new List();
- for (int i = 0; i < Multis.MaximumMultiIndex; ++i)
- {
- MultiComponentList multi = Multis.GetComponents(i);
- if (multi == MultiComponentList.Empty)
- {
- continue;
- }
-
- cache.Add(BuildMulNode(i, multi));
- }
-
- TreeViewMulti.Nodes.AddRange(cache.ToArray());
- }
- finally
- {
- TreeViewMulti.EndUpdate();
- }
+ RebuildMulIds(includeEmpty: false);
- if (TreeViewMulti.Nodes.Count > 0)
+ if (_mulIds.Length > 0)
{
- TreeViewMulti.SelectedNode = TreeViewMulti.Nodes[0];
+ SelectMulRow(0);
}
if (!_loaded)
@@ -235,46 +295,37 @@ private void OnMultiChangeEvent(object sender, int id)
return;
}
- bool done = false;
- for (int i = 0; i < TreeViewMulti.Nodes.Count; ++i)
+ int existing = Array.IndexOf(_mulIds, id);
+ if (existing >= 0)
{
- if (id == int.Parse(TreeViewMulti.Nodes[i].Name))
- {
- TreeViewMulti.Nodes[i].Tag = multi;
- TreeViewMulti.Nodes[i].ForeColor = Color.Black;
- if (i == TreeViewMulti.SelectedNode.Index)
- {
- AfterSelect_Multi(this, null);
- }
-
- done = true;
- break;
- }
-
- if (id >= int.Parse(TreeViewMulti.Nodes[i].Name))
+ // Already in the list — just repaint the row (text might depend
+ // on XML lookups that could now resolve differently).
+ listViewMulti.RedrawItems(existing, existing, false);
+ if (listViewMulti.SelectedIndices.Count > 0 && listViewMulti.SelectedIndices[0] == existing)
{
- continue;
+ AfterSelect_Multi(this, EventArgs.Empty);
}
-
- TreeNode node = new TreeNode(string.Format("{0,5} (0x{0:X})", id))
- {
- Tag = multi,
- Name = id.ToString()
- };
- TreeViewMulti.Nodes.Insert(i, node);
- done = true;
- break;
+ return;
}
- if (!done)
+ // Find insertion point to keep the list sorted by id.
+ int insertAt = _mulIds.Length;
+ for (int i = 0; i < _mulIds.Length; ++i)
{
- TreeNode node = new TreeNode(string.Format("{0,5} (0x{0:X})", id))
+ if (id < _mulIds[i])
{
- Tag = multi,
- Name = id.ToString()
- };
- TreeViewMulti.Nodes.Add(node);
+ insertAt = i;
+ break;
+ }
}
+
+ var next = new int[_mulIds.Length + 1];
+ Array.Copy(_mulIds, 0, next, 0, insertAt);
+ next[insertAt] = id;
+ Array.Copy(_mulIds, insertAt, next, insertAt + 1, _mulIds.Length - insertAt);
+ _mulIds = next;
+ listViewMulti.VirtualListSize = _mulIds.Length;
+ listViewMulti.Invalidate();
}
public void ChangeMulti(int id, MultiComponentList multi)
@@ -284,34 +335,27 @@ public void ChangeMulti(int id, MultiComponentList multi)
return;
}
- int index = _refMarker.TreeViewMulti.SelectedNode.Index;
- if (int.Parse(_refMarker.TreeViewMulti.SelectedNode.Name) != id)
+ int pos = Array.IndexOf(_refMarker._mulIds, id);
+ if (pos < 0)
{
- for (int i = 0; i < _refMarker.TreeViewMulti.Nodes.Count; ++i)
+ // Not yet in the list — insert sorted.
+ _refMarker.OnMultiChangeEvent(null, id);
+ pos = Array.IndexOf(_refMarker._mulIds, id);
+ if (pos < 0)
{
- if (int.Parse(_refMarker.TreeViewMulti.Nodes[i].Name) != id)
- {
- continue;
- }
-
- index = i;
- break;
+ return;
}
}
- _refMarker.TreeViewMulti.Nodes[index].Tag = multi;
- _refMarker.TreeViewMulti.Nodes[index].ForeColor = Color.Black;
- if (index != _refMarker.TreeViewMulti.SelectedNode.Index)
- {
- _refMarker.TreeViewMulti.SelectedNode = _refMarker.TreeViewMulti.Nodes[index];
- }
- AfterSelect_Multi(this, null);
- ControlEvents.FireMultiChangeEvent(this, index);
+ _refMarker.SelectMulRow(pos);
+ _refMarker.AfterSelect_Multi(this, EventArgs.Empty);
+ ControlEvents.FireMultiChangeEvent(this, pos);
}
- private void AfterSelect_Multi(object sender, TreeViewEventArgs e)
+ private void AfterSelect_Multi(object sender, EventArgs e)
{
- MultiComponentList multi = (MultiComponentList)TreeViewMulti.SelectedNode.Tag;
+ int id = GetSelectedMulId();
+ MultiComponentList multi = id >= 0 ? Multis.GetComponents(id) : MultiComponentList.Empty;
if (multi == MultiComponentList.Empty)
{
HeightChangeMulti.Maximum = 0;
@@ -334,11 +378,31 @@ private void RefreshMulBitmap()
{
_mulBitmap?.Dispose();
_mulBitmap = null;
- if (TreeViewMulti.SelectedNode?.Tag is MultiComponentList multi && multi != MultiComponentList.Empty)
+ int id = GetSelectedMulId();
+ if (id >= 0)
{
- int h = HeightChangeMulti.Maximum - HeightChangeMulti.Value;
- _mulBitmap = multi.GetImage(h);
+ MultiComponentList multi = Multis.GetComponents(id);
+ if (multi != MultiComponentList.Empty)
+ {
+ int h = HeightChangeMulti.Maximum - HeightChangeMulti.Value;
+ _mulBitmap = multi.GetImage(h);
+ }
+ }
+ }
+
+ private void RebuildMulIds(bool includeEmpty)
+ {
+ var ids = new List(Multis.MaximumMultiIndex);
+ for (int i = 0; i < Multis.MaximumMultiIndex; ++i)
+ {
+ if (includeEmpty || Multis.GetComponents(i) != MultiComponentList.Empty)
+ {
+ ids.Add(i);
+ }
}
+ _mulIds = ids.ToArray();
+ listViewMulti.VirtualListSize = _mulIds.Length;
+ listViewMulti.Invalidate();
}
private void UpdateMulPictureBox()
@@ -469,7 +533,7 @@ private void ExtractMultiImage(ImageFormat imageFormat, Color backgroundColor)
string fileExtension = Utils.GetFileExtensionFor(imageFormat);
string floorSuffix = HeightChangeMulti.Value > 0 ? $"_Z{HeightChangeMulti.Value:000}" : string.Empty;
- string fileName = Path.Combine(Options.OutputPath, $"Multi {Utils.FormatExportId(int.Parse(TreeViewMulti.SelectedNode.Name))}{floorSuffix}.{fileExtension}");
+ string fileName = Path.Combine(Options.OutputPath, $"Multi {Utils.FormatExportId(GetSelectedMulId())}{floorSuffix}.{fileExtension}");
SaveImage(_mulBitmap, fileName, imageFormat, backgroundColor);
FileSavedDialog.Show(FindForm(), fileName, "Multi saved successfully.");
}
@@ -490,54 +554,23 @@ private static void SaveImage(Image sourceImage, string fileName, ImageFormat im
private void OnClickFreeSlots(object sender, EventArgs e)
{
_showFreeSlots = !_showFreeSlots;
- TreeViewMulti.BeginUpdate();
- TreeViewMulti.Nodes.Clear();
-
- if (_showFreeSlots)
- {
- for (int i = 0; i < Multis.MaximumMultiIndex; ++i)
- {
- MultiComponentList multi = Multis.GetComponents(i);
- TreeNode node = BuildMulNode(i, multi);
- if (multi == MultiComponentList.Empty)
- {
- node.ForeColor = Color.Red;
- }
-
- TreeViewMulti.Nodes.Add(node);
- }
- }
- else
- {
- for (int i = 0; i < Multis.MaximumMultiIndex; ++i)
- {
- MultiComponentList multi = Multis.GetComponents(i);
- if (multi == MultiComponentList.Empty)
- {
- continue;
- }
-
- TreeViewMulti.Nodes.Add(BuildMulNode(i, multi));
- }
- }
- TreeViewMulti.EndUpdate();
+ RebuildMulIds(includeEmpty: _showFreeSlots);
}
private void OnExportTextFile(object sender, EventArgs e)
{
- if (TreeViewMulti.SelectedNode == null)
+ int id = GetSelectedMulId();
+ if (id < 0)
{
return;
}
- MultiComponentList multi = (MultiComponentList)TreeViewMulti.SelectedNode.Tag;
+ MultiComponentList multi = Multis.GetComponents(id);
if (multi == MultiComponentList.Empty)
{
return;
}
- int id = int.Parse(TreeViewMulti.SelectedNode.Name);
-
string path = Options.OutputPath;
string fileName = Path.Combine(path, $"Multi {Utils.FormatExportId(id)}.txt");
multi.ExportToTextFile(fileName);
@@ -547,19 +580,18 @@ private void OnExportTextFile(object sender, EventArgs e)
private void OnExportWscFile(object sender, EventArgs e)
{
- if (TreeViewMulti.SelectedNode == null)
+ int id = GetSelectedMulId();
+ if (id < 0)
{
return;
}
- MultiComponentList multi = (MultiComponentList)TreeViewMulti.SelectedNode.Tag;
+ MultiComponentList multi = Multis.GetComponents(id);
if (multi == MultiComponentList.Empty)
{
return;
}
- int id = int.Parse(TreeViewMulti.SelectedNode.Name);
-
string path = Options.OutputPath;
string fileName = Path.Combine(path, $"Multi {Utils.FormatExportId(id)}.wsc");
multi.ExportToWscFile(fileName);
@@ -569,19 +601,18 @@ private void OnExportWscFile(object sender, EventArgs e)
private void OnExportUOAFile(object sender, EventArgs e)
{
- if (TreeViewMulti.SelectedNode == null)
+ int id = GetSelectedMulId();
+ if (id < 0)
{
return;
}
- MultiComponentList multi = (MultiComponentList)TreeViewMulti.SelectedNode.Tag;
+ MultiComponentList multi = Multis.GetComponents(id);
if (multi == MultiComponentList.Empty)
{
return;
}
- int id = int.Parse(TreeViewMulti.SelectedNode.Name);
-
string path = Options.OutputPath;
string fileName = Path.Combine(path, $"Multi {Utils.FormatExportId(id)}.uoa");
multi.ExportToUOAFile(fileName);
@@ -599,18 +630,17 @@ private void OnClickSave(object sender, EventArgs e)
private void OnClickRemove(object sender, EventArgs e)
{
- if (TreeViewMulti.SelectedNode == null)
+ int id = GetSelectedMulId();
+ if (id < 0)
{
return;
}
- MultiComponentList multi = (MultiComponentList)TreeViewMulti.SelectedNode.Tag;
+ MultiComponentList multi = Multis.GetComponents(id);
if (multi == MultiComponentList.Empty)
{
return;
}
-
- int id = int.Parse(TreeViewMulti.SelectedNode.Name);
DialogResult result = MessageBox.Show(string.Format("Are you sure to remove {0} (0x{0:X})", id), "Remove",
MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (result != DialogResult.Yes)
@@ -619,15 +649,28 @@ private void OnClickRemove(object sender, EventArgs e)
}
Multis.Remove(id);
- TreeViewMulti.SelectedNode.Remove();
+ int pos = Array.IndexOf(_mulIds, id);
+ if (pos >= 0)
+ {
+ var next = new int[_mulIds.Length - 1];
+ Array.Copy(_mulIds, 0, next, 0, pos);
+ Array.Copy(_mulIds, pos + 1, next, pos, _mulIds.Length - pos - 1);
+ _mulIds = next;
+ listViewMulti.VirtualListSize = _mulIds.Length;
+ listViewMulti.Invalidate();
+ }
Options.ChangedUltimaClass["Multis"] = true;
ControlEvents.FireMultiChangeEvent(this, id);
}
private void OnClickImport(object sender, EventArgs e)
{
- MultiComponentList multi = (MultiComponentList)TreeViewMulti.SelectedNode.Tag;
- int id = int.Parse(TreeViewMulti.SelectedNode.Name);
+ int id = GetSelectedMulId();
+ if (id < 0)
+ {
+ return;
+ }
+ MultiComponentList multi = Multis.GetComponents(id);
if (multi != MultiComponentList.Empty)
{
DialogResult result = MessageBox.Show(string.Format("Are you sure to replace {0} (0x{0:X})", id),
@@ -678,9 +721,9 @@ private void ExportAllMultis(ImageFormat imageFormat, Color backgroundColor)
return;
}
- for (int i = 0; i < _refMarker.TreeViewMulti.Nodes.Count; i++)
+ for (int i = 0; i < _refMarker._mulIds.Length; i++)
{
- int index = int.Parse(_refMarker.TreeViewMulti.Nodes[i].Name);
+ int index = _refMarker._mulIds[i];
if (index < 0)
{
continue;
@@ -689,7 +732,7 @@ private void ExportAllMultis(ImageFormat imageFormat, Color backgroundColor)
const int maximumMultiHeight = 127;
string fileName = Path.Combine(dialog.SelectedPath, $"Multi {Utils.FormatExportId(index)}.{fileExtension}");
- using (Bitmap multiBitmap = ((MultiComponentList)_refMarker.TreeViewMulti.Nodes[i].Tag)?.GetImage(maximumMultiHeight))
+ using (Bitmap multiBitmap = Multis.GetComponents(index)?.GetImage(maximumMultiHeight))
{
if (multiBitmap != null)
{
@@ -713,15 +756,15 @@ private void OnClick_SaveAllText(object sender, EventArgs e)
return;
}
- for (int i = 0; i < _refMarker.TreeViewMulti.Nodes.Count; ++i)
+ for (int i = 0; i < _refMarker._mulIds.Length; ++i)
{
- int index = int.Parse(_refMarker.TreeViewMulti.Nodes[i].Name);
+ int index = _refMarker._mulIds[i];
if (index < 0)
{
continue;
}
- MultiComponentList multi = (MultiComponentList)_refMarker.TreeViewMulti.Nodes[i].Tag;
+ MultiComponentList multi = Multis.GetComponents(index);
if (multi == MultiComponentList.Empty)
{
continue;
@@ -746,15 +789,15 @@ private void OnClick_SaveAllUOA(object sender, EventArgs e)
return;
}
- for (int i = 0; i < _refMarker.TreeViewMulti.Nodes.Count; ++i)
+ for (int i = 0; i < _refMarker._mulIds.Length; ++i)
{
- int index = int.Parse(_refMarker.TreeViewMulti.Nodes[i].Name);
+ int index = _refMarker._mulIds[i];
if (index < 0)
{
continue;
}
- MultiComponentList multi = (MultiComponentList)_refMarker.TreeViewMulti.Nodes[i].Tag;
+ MultiComponentList multi = Multis.GetComponents(index);
if (multi == MultiComponentList.Empty)
{
continue;
@@ -779,15 +822,15 @@ private void OnClick_SaveAllWSC(object sender, EventArgs e)
return;
}
- for (int i = 0; i < _refMarker.TreeViewMulti.Nodes.Count; ++i)
+ for (int i = 0; i < _refMarker._mulIds.Length; ++i)
{
- int index = int.Parse(_refMarker.TreeViewMulti.Nodes[i].Name);
+ int index = _refMarker._mulIds[i];
if (index < 0)
{
continue;
}
- MultiComponentList multi = (MultiComponentList)_refMarker.TreeViewMulti.Nodes[i].Tag;
+ MultiComponentList multi = Multis.GetComponents(index);
if (multi == MultiComponentList.Empty)
{
continue;
@@ -812,15 +855,15 @@ private void OnClick_SaveAllCSV(object sender, EventArgs e)
return;
}
- for (int i = 0; i < _refMarker.TreeViewMulti.Nodes.Count; ++i)
+ for (int i = 0; i < _refMarker._mulIds.Length; ++i)
{
- int index = int.Parse(_refMarker.TreeViewMulti.Nodes[i].Name);
+ int index = _refMarker._mulIds[i];
if (index < 0)
{
continue;
}
- MultiComponentList multi = (MultiComponentList)_refMarker.TreeViewMulti.Nodes[i].Tag;
+ MultiComponentList multi = Multis.GetComponents(index);
if (multi == MultiComponentList.Empty)
{
continue;
@@ -845,15 +888,15 @@ private void OnClick_SaveAllUox3(object sender, EventArgs e)
return;
}
- for (int i = 0; i < _refMarker.TreeViewMulti.Nodes.Count; ++i)
+ for (int i = 0; i < _refMarker._mulIds.Length; ++i)
{
- int index = int.Parse(_refMarker.TreeViewMulti.Nodes[i].Name);
+ int index = _refMarker._mulIds[i];
if (index < 0)
{
continue;
}
- MultiComponentList multi = (MultiComponentList)_refMarker.TreeViewMulti.Nodes[i].Tag;
+ MultiComponentList multi = Multis.GetComponents(index);
if (multi == MultiComponentList.Empty)
{
continue;
@@ -869,19 +912,18 @@ private void OnClick_SaveAllUox3(object sender, EventArgs e)
private void OnExportCsvFile(object sender, EventArgs e)
{
- if (TreeViewMulti.SelectedNode == null)
+ int id = GetSelectedMulId();
+ if (id < 0)
{
return;
}
- MultiComponentList multi = (MultiComponentList)TreeViewMulti.SelectedNode.Tag;
+ MultiComponentList multi = Multis.GetComponents(id);
if (multi == MultiComponentList.Empty)
{
return;
}
- int id = int.Parse(TreeViewMulti.SelectedNode.Name);
-
string path = Options.OutputPath;
string fileName = Path.Combine(path, $"{id:D4}.csv");
multi.ExportToCsvFile(fileName);
@@ -890,19 +932,18 @@ private void OnExportCsvFile(object sender, EventArgs e)
private void OnExportUox3File(object sender, EventArgs e)
{
- if (TreeViewMulti.SelectedNode == null)
+ int id = GetSelectedMulId();
+ if (id < 0)
{
return;
}
- MultiComponentList multi = (MultiComponentList)TreeViewMulti.SelectedNode.Tag;
+ MultiComponentList multi = Multis.GetComponents(id);
if (multi == MultiComponentList.Empty)
{
return;
}
- int id = int.Parse(TreeViewMulti.SelectedNode.Name);
-
string path = Options.OutputPath;
string fileName = Path.Combine(path, $"Multi {Utils.FormatExportId(id)}.uox3");
multi.ExportToUox3File(fileName);
@@ -927,44 +968,36 @@ private void UseTransparencyForPNGToolStripMenuItem_CheckedChanged(object sender
private void LoadUopTree()
{
- treeViewUop.BeginUpdate();
- treeViewUop.Nodes.Clear();
-
if (!Multis.HasUopFile)
{
- treeViewUop.Nodes.Add(new TreeNode("multicollection.uop not found or path is not set.") { Name = "-1" });
- treeViewUop.EndUpdate();
+ _uopIds = new[] { -1 }; // placeholder row rendered as the "not found" message
+ listViewUop.VirtualListSize = 1;
+ listViewUop.Invalidate();
return;
}
- var cache = new List();
+ var ids = new List(Multis.MaximumMultiIndex);
for (int i = 0; i < Multis.MaximumMultiIndex; ++i)
{
- MultiComponentList multi = Multis.GetUopComponents(i);
- if (multi == MultiComponentList.Empty)
+ if (Multis.GetUopComponents(i) != MultiComponentList.Empty)
{
- continue;
+ ids.Add(i);
}
-
- cache.Add(new TreeNode(BuildNodeLabel(i)) { Tag = multi, Name = i.ToString() });
}
+ _uopIds = ids.ToArray();
+ listViewUop.VirtualListSize = _uopIds.Length;
+ listViewUop.Invalidate();
- treeViewUop.Nodes.AddRange(cache.ToArray());
- treeViewUop.EndUpdate();
-
- if (treeViewUop.Nodes.Count > 0)
+ if (_uopIds.Length > 0)
{
- treeViewUop.SelectedNode = treeViewUop.Nodes[0];
+ SelectUopRow(0);
}
}
- private void AfterSelect_UopMulti(object sender, TreeViewEventArgs e)
+ private void AfterSelect_UopMulti(object sender, EventArgs e)
{
- if (treeViewUop.SelectedNode?.Tag is not MultiComponentList multi)
- {
- return;
- }
-
+ int id = GetSelectedUopId();
+ MultiComponentList multi = id >= 0 ? Multis.GetUopComponents(id) : MultiComponentList.Empty;
if (multi == MultiComponentList.Empty)
{
HeightChangeUop.Maximum = 0;
@@ -987,10 +1020,15 @@ private void RefreshUopBitmap()
{
_uopBitmap?.Dispose();
_uopBitmap = null;
- if (treeViewUop.SelectedNode?.Tag is MultiComponentList multi && multi != MultiComponentList.Empty)
+ int id = GetSelectedUopId();
+ if (id >= 0)
{
- int h = HeightChangeUop.Maximum - HeightChangeUop.Value;
- _uopBitmap = multi.GetImage(h);
+ MultiComponentList multi = Multis.GetUopComponents(id);
+ if (multi != MultiComponentList.Empty)
+ {
+ int h = HeightChangeUop.Maximum - HeightChangeUop.Value;
+ _uopBitmap = multi.GetImage(h);
+ }
}
}
@@ -1287,7 +1325,7 @@ private void ExtractUopMultiImage(ImageFormat imageFormat, Color backgroundColor
string fileExtension = Utils.GetFileExtensionFor(imageFormat);
string floorSuffix = HeightChangeUop.Value > 0 ? $"_Z{HeightChangeUop.Value:000}" : string.Empty;
- int id = int.Parse(treeViewUop.SelectedNode.Name);
+ int id = GetSelectedUopId();
string fileName = Path.Combine(Options.OutputPath, $"UopMulti {Utils.FormatExportId(id)}{floorSuffix}.{fileExtension}");
SaveImage(_uopBitmap, fileName, imageFormat, backgroundColor);
FileSavedDialog.Show(FindForm(), fileName, "Multi saved successfully.");
@@ -1295,12 +1333,16 @@ private void ExtractUopMultiImage(ImageFormat imageFormat, Color backgroundColor
private void OnUopExportTextFile(object sender, EventArgs e)
{
- if (treeViewUop.SelectedNode?.Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ int id = GetSelectedUopId();
+ if (id < 0)
+ {
+ return;
+ }
+ MultiComponentList multi = Multis.GetUopComponents(id);
+ if (multi == MultiComponentList.Empty)
{
return;
}
-
- int id = int.Parse(treeViewUop.SelectedNode.Name);
string fileName = Path.Combine(Options.OutputPath, $"UopMulti {Utils.FormatExportId(id)}.txt");
multi.ExportToTextFile(fileName);
FileSavedDialog.Show(FindForm(), fileName, "Multi saved successfully.");
@@ -1308,12 +1350,16 @@ private void OnUopExportTextFile(object sender, EventArgs e)
private void OnUopExportUOAFile(object sender, EventArgs e)
{
- if (treeViewUop.SelectedNode?.Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ int id = GetSelectedUopId();
+ if (id < 0)
+ {
+ return;
+ }
+ MultiComponentList multi = Multis.GetUopComponents(id);
+ if (multi == MultiComponentList.Empty)
{
return;
}
-
- int id = int.Parse(treeViewUop.SelectedNode.Name);
string fileName = Path.Combine(Options.OutputPath, $"UopMulti {Utils.FormatExportId(id)}.uoa");
multi.ExportToUOAFile(fileName);
FileSavedDialog.Show(FindForm(), fileName, "Multi saved successfully.");
@@ -1321,12 +1367,16 @@ private void OnUopExportUOAFile(object sender, EventArgs e)
private void OnUopExportWscFile(object sender, EventArgs e)
{
- if (treeViewUop.SelectedNode?.Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ int id = GetSelectedUopId();
+ if (id < 0)
+ {
+ return;
+ }
+ MultiComponentList multi = Multis.GetUopComponents(id);
+ if (multi == MultiComponentList.Empty)
{
return;
}
-
- int id = int.Parse(treeViewUop.SelectedNode.Name);
string fileName = Path.Combine(Options.OutputPath, $"UopMulti {Utils.FormatExportId(id)}.wsc");
multi.ExportToWscFile(fileName);
FileSavedDialog.Show(FindForm(), fileName, "Multi saved successfully.");
@@ -1334,12 +1384,16 @@ private void OnUopExportWscFile(object sender, EventArgs e)
private void OnUopExportCsvFile(object sender, EventArgs e)
{
- if (treeViewUop.SelectedNode?.Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ int id = GetSelectedUopId();
+ if (id < 0)
+ {
+ return;
+ }
+ MultiComponentList multi = Multis.GetUopComponents(id);
+ if (multi == MultiComponentList.Empty)
{
return;
}
-
- int id = int.Parse(treeViewUop.SelectedNode.Name);
string fileName = Path.Combine(Options.OutputPath, $"{id:D4}_uop.csv");
multi.ExportToCsvFile(fileName);
FileSavedDialog.Show(FindForm(), fileName, "Multi saved successfully.");
@@ -1367,14 +1421,16 @@ private void ExportAllUopMultis(ImageFormat imageFormat, Color backgroundColor)
}
const int maxHeight = 127;
- for (int i = 0; i < treeViewUop.Nodes.Count; i++)
+ for (int i = 0; i < _uopIds.Length; i++)
{
- if (!int.TryParse(treeViewUop.Nodes[i].Name, out int index) || index < 0)
+ int index = _uopIds[i];
+ if (index < 0)
{
continue;
}
- if (treeViewUop.Nodes[i].Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ MultiComponentList multi = Multis.GetUopComponents(index);
+ if (multi == MultiComponentList.Empty)
{
continue;
}
@@ -1398,14 +1454,16 @@ private void OnUopClick_SaveAllText(object sender, EventArgs e)
return;
}
- for (int i = 0; i < treeViewUop.Nodes.Count; ++i)
+ for (int i = 0; i < _uopIds.Length; ++i)
{
- if (!int.TryParse(treeViewUop.Nodes[i].Name, out int index) || index < 0)
+ int index = _uopIds[i];
+ if (index < 0)
{
continue;
}
- if (treeViewUop.Nodes[i].Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ MultiComponentList multi = Multis.GetUopComponents(index);
+ if (multi == MultiComponentList.Empty)
{
continue;
}
@@ -1424,14 +1482,16 @@ private void OnUopClick_SaveAllUOA(object sender, EventArgs e)
return;
}
- for (int i = 0; i < treeViewUop.Nodes.Count; ++i)
+ for (int i = 0; i < _uopIds.Length; ++i)
{
- if (!int.TryParse(treeViewUop.Nodes[i].Name, out int index) || index < 0)
+ int index = _uopIds[i];
+ if (index < 0)
{
continue;
}
- if (treeViewUop.Nodes[i].Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ MultiComponentList multi = Multis.GetUopComponents(index);
+ if (multi == MultiComponentList.Empty)
{
continue;
}
@@ -1450,14 +1510,16 @@ private void OnUopClick_SaveAllWSC(object sender, EventArgs e)
return;
}
- for (int i = 0; i < treeViewUop.Nodes.Count; ++i)
+ for (int i = 0; i < _uopIds.Length; ++i)
{
- if (!int.TryParse(treeViewUop.Nodes[i].Name, out int index) || index < 0)
+ int index = _uopIds[i];
+ if (index < 0)
{
continue;
}
- if (treeViewUop.Nodes[i].Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ MultiComponentList multi = Multis.GetUopComponents(index);
+ if (multi == MultiComponentList.Empty)
{
continue;
}
@@ -1476,14 +1538,16 @@ private void OnUopClick_SaveAllCSV(object sender, EventArgs e)
return;
}
- for (int i = 0; i < treeViewUop.Nodes.Count; ++i)
+ for (int i = 0; i < _uopIds.Length; ++i)
{
- if (!int.TryParse(treeViewUop.Nodes[i].Name, out int index) || index < 0)
+ int index = _uopIds[i];
+ if (index < 0)
{
continue;
}
- if (treeViewUop.Nodes[i].Tag is not MultiComponentList multi || multi == MultiComponentList.Empty)
+ MultiComponentList multi = Multis.GetUopComponents(index);
+ if (multi == MultiComponentList.Empty)
{
continue;
}
@@ -1512,15 +1576,15 @@ private void OnClick_SaveAllToXML(object sender, EventArgs e)
groupWriter.WriteStartElement("Group");
groupWriter.WriteAttributeString("Name", "Exported Multis");
- for (int i = 0; i < _refMarker.TreeViewMulti.Nodes.Count; ++i)
+ for (int i = 0; i < _refMarker._mulIds.Length; ++i)
{
- int index = int.Parse(_refMarker.TreeViewMulti.Nodes[i].Name);
+ int index = _refMarker._mulIds[i];
if (index < 0)
{
continue;
}
- MultiComponentList multi = (MultiComponentList)_refMarker.TreeViewMulti.Nodes[i].Tag;
+ MultiComponentList multi = Multis.GetComponents(index);
if (multi == MultiComponentList.Empty)
{
continue;
@@ -1528,11 +1592,11 @@ private void OnClick_SaveAllToXML(object sender, EventArgs e)
groupWriter.WriteStartElement("Entry");
groupWriter.WriteAttributeString("ID", index.ToString());
- groupWriter.WriteAttributeString("Name", _refMarker.TreeViewMulti.Nodes[i].Text.Trim());
+ groupWriter.WriteAttributeString("Name", _refMarker.BuildNodeLabel(index).Trim());
writer.WriteStartElement("Entry");
writer.WriteAttributeString("ID", index.ToString());
- writer.WriteAttributeString("Name", _refMarker.TreeViewMulti.Nodes[i].Text.Trim());
+ writer.WriteAttributeString("Name", _refMarker.BuildNodeLabel(index).Trim());
for (int x = 0; x < multi.Width; x++)
{
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs b/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
index 39f3dfc..30dbe00 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
@@ -40,13 +40,13 @@ protected override void Dispose(bool disposing)
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
- treeViewItem = new System.Windows.Forms.TreeView();
+ tileViewItem = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(components);
selectInItemsTabToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
selectInTiledataTabToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
setAsRangeFromToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
setAsRangeToToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- treeViewLand = new System.Windows.Forms.TreeView();
+ tileViewLand = new UoFiddler.Controls.UserControls.TileView.TileViewControl();
contextMenuStrip2 = new System.Windows.Forms.ContextMenuStrip(components);
toolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
toolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem();
@@ -134,19 +134,20 @@ private void InitializeComponent()
((System.ComponentModel.ISupportInitialize)numericUpDownR).BeginInit();
SuspendLayout();
//
- // treeViewItem
- //
- treeViewItem.CheckBoxes = true;
- treeViewItem.ContextMenuStrip = contextMenuStrip1;
- treeViewItem.Dock = System.Windows.Forms.DockStyle.Fill;
- treeViewItem.HideSelection = false;
- treeViewItem.Location = new System.Drawing.Point(0, 0);
- treeViewItem.Margin = new System.Windows.Forms.Padding(4);
- treeViewItem.Name = "treeViewItem";
- treeViewItem.Size = new System.Drawing.Size(228, 164);
- treeViewItem.TabIndex = 0;
- treeViewItem.AfterCheck += AfterCheckTreeViewItem;
- treeViewItem.AfterSelect += AfterSelectTreeViewItem;
+ // tileViewItem
+ //
+ tileViewItem.ContextMenuStrip = contextMenuStrip1;
+ tileViewItem.Dock = System.Windows.Forms.DockStyle.Fill;
+ tileViewItem.Location = new System.Drawing.Point(0, 0);
+ tileViewItem.Margin = new System.Windows.Forms.Padding(4);
+ tileViewItem.Name = "tileViewItem";
+ tileViewItem.Size = new System.Drawing.Size(228, 164);
+ tileViewItem.TabIndex = 0;
+ tileViewItem.ShowCheckBoxes = true;
+ tileViewItem.TileHighLightOpacity = 0D;
+ tileViewItem.DrawItem += OnDrawItemRow;
+ tileViewItem.FocusSelectionChanged += OnItemFocusChanged;
+ tileViewItem.SizeChanged += OnTileViewSizeChanged;
//
// contextMenuStrip1
//
@@ -183,19 +184,20 @@ private void InitializeComponent()
setAsRangeToToolStripMenuItem.Text = "Set as Range \"to\"";
setAsRangeToToolStripMenuItem.Click += OnClickSetRangeTo;
//
- // treeViewLand
- //
- treeViewLand.CheckBoxes = true;
- treeViewLand.ContextMenuStrip = contextMenuStrip2;
- treeViewLand.Dock = System.Windows.Forms.DockStyle.Fill;
- treeViewLand.HideSelection = false;
- treeViewLand.Location = new System.Drawing.Point(0, 0);
- treeViewLand.Margin = new System.Windows.Forms.Padding(4);
- treeViewLand.Name = "treeViewLand";
- treeViewLand.Size = new System.Drawing.Size(228, 164);
- treeViewLand.TabIndex = 0;
- treeViewLand.AfterCheck += AfterCheckTreeViewLand;
- treeViewLand.AfterSelect += AfterSelectTreeViewLand;
+ // tileViewLand
+ //
+ tileViewLand.ContextMenuStrip = contextMenuStrip2;
+ tileViewLand.Dock = System.Windows.Forms.DockStyle.Fill;
+ tileViewLand.Location = new System.Drawing.Point(0, 0);
+ tileViewLand.Margin = new System.Windows.Forms.Padding(4);
+ tileViewLand.Name = "tileViewLand";
+ tileViewLand.Size = new System.Drawing.Size(228, 164);
+ tileViewLand.TabIndex = 0;
+ tileViewLand.ShowCheckBoxes = true;
+ tileViewLand.TileHighLightOpacity = 0D;
+ tileViewLand.DrawItem += OnDrawLandRow;
+ tileViewLand.FocusSelectionChanged += OnLandFocusChanged;
+ tileViewLand.SizeChanged += OnTileViewSizeChanged;
//
// contextMenuStrip2
//
@@ -368,7 +370,7 @@ private void InitializeComponent()
//
// splitContainer1.Panel2
//
- splitContainer1.Panel2.Controls.Add(treeViewItem);
+ splitContainer1.Panel2.Controls.Add(tileViewItem);
splitContainer1.Size = new System.Drawing.Size(228, 193);
splitContainer1.SplitterDistance = 25;
splitContainer1.TabIndex = 2;
@@ -456,7 +458,7 @@ private void InitializeComponent()
//
// splitContainer3.Panel2
//
- splitContainer3.Panel2.Controls.Add(treeViewLand);
+ splitContainer3.Panel2.Controls.Add(tileViewLand);
splitContainer3.Size = new System.Drawing.Size(228, 193);
splitContainer3.SplitterDistance = 25;
splitContainer3.TabIndex = 0;
@@ -845,8 +847,8 @@ private void InitializeComponent()
private System.Windows.Forms.TextBox textBoxMeanTo;
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem1;
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem2;
- private System.Windows.Forms.TreeView treeViewItem;
- private System.Windows.Forms.TreeView treeViewLand;
+ private UoFiddler.Controls.UserControls.TileView.TileViewControl tileViewItem;
+ private UoFiddler.Controls.UserControls.TileView.TileViewControl tileViewLand;
private System.Windows.Forms.Button button6;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label label2;
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.cs b/UoFiddler.Controls/UserControls/RadarColorControl.cs
index 6ce5b68..7bb9dfa 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.cs
@@ -22,6 +22,7 @@
using UoFiddler.Controls.Classes;
using UoFiddler.Controls.Forms;
using UoFiddler.Controls.Helpers;
+using UoFiddler.Controls.UserControls.TileView;
namespace UoFiddler.Controls.UserControls
{
@@ -33,9 +34,23 @@ public RadarColorControl()
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
_refMarker = this;
+
+ // TileViewControl runtime config (TileSize/Margin/Padding/Border) +
+ // checkbox-toggle sync into the canonical _selectedItems/_selectedLand
+ // HashSets.
+ ConfigureTileView(tileViewItem);
+ ConfigureTileView(tileViewLand);
+ tileViewItem.SelectedIndices.CollectionChanged += OnItemSelectedIndicesChanged;
+ tileViewLand.SelectedIndices.CollectionChanged += OnLandSelectedIndicesChanged;
}
private int _selectedIndex = -1;
+ // Tracks whether _selectedIndex refers to an item or a land tile. Set
+ // when _selectedIndex is updated; consulted by SaveColor so that
+ // committing the editor on a tab switch routes to the right table.
+ // (Otherwise SaveColor would see the *new* tab and save the previous
+ // tab's color to whatever index happens to be in _selectedIndex.)
+ private bool _selectedIsItem;
private ushort _currentColor;
private static RadarColorControl _refMarker;
private bool _updating;
@@ -43,9 +58,305 @@ public RadarColorControl()
private readonly Dictionary _originalLandColors = [];
private Timer _debounceTimer;
private const int _debounceTimeout = 500;
+ // Canonical "checked" backing store, keyed by graphic id so the
+ // selection survives filter changes. Sync'd both ways with the
+ // TileViewControl.SelectedIndices collection (which uses *row positions*).
private readonly HashSet _selectedItems = [];
private readonly HashSet _selectedLand = [];
+ // Visible row position → graphic id. Default identity; ApplyFilter narrows.
+ private int[] _itemIndices = Array.Empty();
+ private int[] _landIndices = Array.Empty();
+
+ // Re-entrancy guard: when we mutate TileViewControl.SelectedIndices
+ // programmatically (e.g. during a filter rebuild or Select All), we
+ // don't want the CollectionChanged handler to re-mutate our HashSet
+ // and double-count.
+ private bool _syncingSelection;
+
+ private static int[] BuildIdentity(int length)
+ {
+ var array = new int[length];
+ for (int i = 0; i < length; ++i)
+ {
+ array[i] = i;
+ }
+ return array;
+ }
+
+ // TileViewControl strips TileSize/Margin/Padding/BorderWidth in the
+ // Designer (DesignerSerializationVisibility.Hidden), so we apply them
+ // here. Matches the compare plugin's ConfigureTileView.
+ private static void ConfigureTileView(TileView.TileViewControl tv)
+ {
+ tv.TileSize = new Size(tv.TileSize.Width, 20);
+ tv.TileMargin = new Padding(0);
+ tv.TilePadding = new Padding(0);
+ tv.TileBorderWidth = 0f;
+ }
+
+ private void OnTileViewSizeChanged(object sender, EventArgs e)
+ {
+ var tv = (TileView.TileViewControl)sender;
+ int w = tv.DisplayRectangle.Width;
+ if (w > 0 && tv.TileSize.Width != w)
+ {
+ tv.TileSize = new Size(w, tv.TileSize.Height);
+ }
+ }
+
+ private int GetSelectedItemGraphic()
+ {
+ int focus = tileViewItem.FocusIndex;
+ return focus >= 0 && focus < _itemIndices.Length ? _itemIndices[focus] : -1;
+ }
+
+ private int GetSelectedLandGraphic()
+ {
+ int focus = tileViewLand.FocusIndex;
+ return focus >= 0 && focus < _landIndices.Length ? _landIndices[focus] : -1;
+ }
+
+ private void OnDrawItemRow(object sender, TileView.TileViewControl.DrawTileListItemEventArgs e)
+ {
+ if ((uint)e.Index >= (uint)_itemIndices.Length)
+ {
+ return;
+ }
+ int graphic = _itemIndices[e.Index];
+ ref readonly ItemData row = ref TileData.ItemTable[graphic];
+ DrawRow(e, graphic, row.Name, _originalItemColors.ContainsKey(graphic), RadarCol.GetItemColor(graphic));
+ }
+
+ private void OnDrawLandRow(object sender, TileView.TileViewControl.DrawTileListItemEventArgs e)
+ {
+ if ((uint)e.Index >= (uint)_landIndices.Length)
+ {
+ return;
+ }
+ int graphic = _landIndices[e.Index];
+ ref readonly LandData row = ref TileData.LandTable[graphic];
+ DrawRow(e, graphic, row.Name, _originalLandColors.ContainsKey(graphic), RadarCol.GetLandColor(graphic));
+ }
+
+ // Layout (left → right): [checkbox column from TileViewControl] | [color swatch] | [text]
+ private const int SwatchSize = 12;
+ private const int SwatchGap = 4;
+
+ private static void DrawRow(TileView.TileViewControl.DrawTileListItemEventArgs e, int graphic, string name, bool modified, ushort radarHue)
+ {
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+
+ // Row background.
+ if (focused)
+ {
+ e.Graphics.FillRectangle(SystemBrushes.Highlight, e.Bounds);
+ }
+ else
+ {
+ e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ }
+
+ // Color swatch — a small filled rectangle showing this row's radar color.
+ int swatchX = e.ContentLeft + 2;
+ int swatchY = e.Bounds.Y + (e.Bounds.Height - SwatchSize) / 2;
+ var swatchRect = new Rectangle(swatchX, swatchY, SwatchSize, SwatchSize);
+ Color swatchColor = HueHelpers.HueToColor(radarHue);
+ using (var swatchBrush = new SolidBrush(swatchColor))
+ {
+ e.Graphics.FillRectangle(swatchBrush, swatchRect);
+ }
+ using (var swatchBorder = new Pen(focused ? SystemColors.HighlightText : SystemColors.ControlDark))
+ {
+ e.Graphics.DrawRectangle(swatchBorder, swatchRect);
+ }
+
+ // Text starts after the swatch.
+ Color textColor;
+ if (focused)
+ {
+ textColor = SystemColors.HighlightText;
+ }
+ else if (modified)
+ {
+ textColor = Color.Blue;
+ }
+ else
+ {
+ textColor = Options.DarkMode ? Color.White : SystemColors.WindowText;
+ }
+
+ string text = $"0x{graphic:X4} ({graphic}) {name}";
+ int textX = swatchX + SwatchSize + SwatchGap;
+ float textY = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(text, e.Font).Height) / 2f;
+ using var brush = new SolidBrush(textColor);
+ e.Graphics.DrawString(text, e.Font, brush, new PointF(textX, textY));
+ }
+
+ private void OnItemFocusChanged(object sender, TileView.TileViewControl.ListViewFocusedItemSelectionChangedEventArgs e)
+ {
+ if (e.FocusedItemIndex < 0 || e.FocusedItemIndex >= _itemIndices.Length)
+ {
+ return;
+ }
+ UpdateSelectedItemPreview(_itemIndices[e.FocusedItemIndex]);
+ }
+
+ private void OnLandFocusChanged(object sender, TileView.TileViewControl.ListViewFocusedItemSelectionChangedEventArgs e)
+ {
+ if (e.FocusedItemIndex < 0 || e.FocusedItemIndex >= _landIndices.Length)
+ {
+ return;
+ }
+ UpdateSelectedLandPreview(_landIndices[e.FocusedItemIndex]);
+ }
+
+ private void OnItemSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ if (_syncingSelection || e.ItemsChanged == null)
+ {
+ return;
+ }
+
+ foreach (int row in e.ItemsChanged)
+ {
+ if ((uint)row >= (uint)_itemIndices.Length)
+ {
+ continue;
+ }
+ int graphic = _itemIndices[row];
+ if (e.Action == IndicesCollection.NotifyCollectionChangedAction.Add)
+ {
+ _selectedItems.Add(graphic);
+ }
+ else
+ {
+ _selectedItems.Remove(graphic);
+ }
+ }
+ }
+
+ private void OnLandSelectedIndicesChanged(object sender, IndicesCollection.NotifyCollectionChangedEventArgs e)
+ {
+ if (_syncingSelection || e.ItemsChanged == null)
+ {
+ return;
+ }
+
+ foreach (int row in e.ItemsChanged)
+ {
+ if ((uint)row >= (uint)_landIndices.Length)
+ {
+ continue;
+ }
+ int graphic = _landIndices[row];
+ if (e.Action == IndicesCollection.NotifyCollectionChangedAction.Add)
+ {
+ _selectedLand.Add(graphic);
+ }
+ else
+ {
+ _selectedLand.Remove(graphic);
+ }
+ }
+ }
+
+ private void RedrawItemRow(int graphic)
+ {
+ int pos = Array.IndexOf(_itemIndices, graphic);
+ if (pos >= 0)
+ {
+ tileViewItem.RedrawItem(pos);
+ }
+ }
+
+ private void RedrawLandRow(int graphic)
+ {
+ int pos = Array.IndexOf(_landIndices, graphic);
+ if (pos >= 0)
+ {
+ tileViewLand.RedrawItem(pos);
+ }
+ }
+
+ private void SelectItemRow(int rowPos)
+ {
+ if ((uint)rowPos < (uint)_itemIndices.Length)
+ {
+ tileViewItem.FocusIndex = rowPos;
+ }
+ }
+
+ private void SelectLandRow(int rowPos)
+ {
+ if ((uint)rowPos < (uint)_landIndices.Length)
+ {
+ tileViewLand.FocusIndex = rowPos;
+ }
+ }
+
+ // After _itemIndices/_landIndices change (reset or filter), the row
+ // positions in SelectedIndices are stale. Rebuild them from the canonical
+ // _selectedItems/_selectedLand HashSets. _syncingSelection prevents the
+ // CollectionChanged handler from feeding the writes back into the HashSet.
+ private void SyncItemSelectedIndicesFromHashSet()
+ {
+ _syncingSelection = true;
+ try
+ {
+ tileViewItem.SelectedIndices.Clear();
+ for (int i = 0; i < _itemIndices.Length; ++i)
+ {
+ if (_selectedItems.Contains(_itemIndices[i]))
+ {
+ tileViewItem.SelectedIndices.Add(i);
+ }
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private void SyncLandSelectedIndicesFromHashSet()
+ {
+ _syncingSelection = true;
+ try
+ {
+ tileViewLand.SelectedIndices.Clear();
+ for (int i = 0; i < _landIndices.Length; ++i)
+ {
+ if (_selectedLand.Contains(_landIndices[i]))
+ {
+ tileViewLand.SelectedIndices.Add(i);
+ }
+ }
+ }
+ finally
+ {
+ _syncingSelection = false;
+ }
+ }
+
+ private void ResetItemView()
+ {
+ int total = TileData.ItemTable != null ? Art.GetMaxItemId() : 0;
+ _itemIndices = BuildIdentity(total);
+ tileViewItem.VirtualListSize = _itemIndices.Length;
+ SyncItemSelectedIndicesFromHashSet();
+ tileViewItem.Invalidate();
+ }
+
+ private void ResetLandView()
+ {
+ int total = TileData.LandTable?.Length ?? 0;
+ _landIndices = BuildIdentity(total);
+ tileViewLand.VirtualListSize = _landIndices.Length;
+ SyncLandSelectedIndicesFromHashSet();
+ tileViewLand.Invalidate();
+ }
+
public bool IsLoaded { get; private set; }
[Browsable(false),
@@ -79,38 +390,41 @@ public static void Select(int graphic, bool land)
_refMarker.OnLoad(_refMarker, EventArgs.Empty);
}
- const int index = 0;
if (land)
{
- for (int i = index; i < _refMarker.treeViewLand.Nodes.Count; ++i)
+ int pos = Array.IndexOf(_refMarker._landIndices, graphic);
+ if (pos < 0)
{
- TreeNode node = _refMarker.treeViewLand.Nodes[i];
- if ((int)node.Tag != graphic)
- {
- continue;
- }
+ // Filter may exclude the target — reset and retry so
+ // cross-tab navigation always lands on the row.
+ _refMarker.ResetLandView();
+ pos = Array.IndexOf(_refMarker._landIndices, graphic);
+ }
- _refMarker.tabControl2.SelectTab(1);
- _refMarker.treeViewLand.SelectedNode = node;
- node.EnsureVisible();
- break;
+ if (pos < 0)
+ {
+ return;
}
+
+ _refMarker.tabControl2.SelectTab(1);
+ _refMarker.SelectLandRow(pos);
}
else
{
- for (int i = index; i < _refMarker.treeViewItem.Nodes.Count; ++i)
+ int pos = Array.IndexOf(_refMarker._itemIndices, graphic);
+ if (pos < 0)
{
- TreeNode node = _refMarker.treeViewItem.Nodes[i];
- if ((int)node.Tag != graphic)
- {
- continue;
- }
+ _refMarker.ResetItemView();
+ pos = Array.IndexOf(_refMarker._itemIndices, graphic);
+ }
- _refMarker.tabControl2.SelectTab(0);
- _refMarker.treeViewItem.SelectedNode = node;
- node.EnsureVisible();
- break;
+ if (pos < 0)
+ {
+ return;
}
+
+ _refMarker.tabControl2.SelectTab(0);
+ _refMarker.SelectItemRow(pos);
}
}
@@ -134,58 +448,21 @@ public void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
Options.LoadedUltimaClass["TileData"] = true;
Options.LoadedUltimaClass["Art"] = true;
Options.LoadedUltimaClass["RadarColor"] = true;
+
+ // Fresh data from disk — nothing is checked or dirty until the
+ // user edits it again.
_selectedItems.Clear();
_selectedLand.Clear();
_originalItemColors.Clear();
_originalLandColors.Clear();
+ _selectedIndex = -1;
+ _selectedIsItem = false;
- treeViewItem.BeginUpdate();
- try
- {
- treeViewItem.Nodes.Clear();
- if (TileData.ItemTable != null)
- {
- TreeNode[] nodes = new TreeNode[Art.GetMaxItemId()];
- for (int i = 0; i < Art.GetMaxItemId(); ++i)
- {
- nodes[i] = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", i, TileData.ItemTable[i].Name))
- {
- Tag = i
- };
- }
- treeViewItem.Nodes.AddRange(nodes);
- }
- }
- finally
- {
- treeViewItem.EndUpdate();
- }
-
- treeViewLand.BeginUpdate();
- try
- {
- treeViewLand.Nodes.Clear();
- if (TileData.LandTable != null)
- {
- TreeNode[] nodes = new TreeNode[TileData.LandTable.Length];
- for (int i = 0; i < TileData.LandTable.Length; ++i)
- {
- nodes[i] = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", i, TileData.LandTable[i].Name))
- {
- Tag = i
- };
- }
- treeViewLand.Nodes.AddRange(nodes);
- }
- }
- finally
- {
- treeViewLand.EndUpdate();
- }
+ ResetItemView();
+ ResetLandView();
if (!IsLoaded)
{
@@ -196,7 +473,6 @@ public void OnLoad(object sender, EventArgs e)
}
IsLoaded = true;
- Cursor.Current = Cursors.Default;
}
private void OnFilePathChangeEvent()
@@ -219,24 +495,27 @@ private void OnPreviewBackgroundColorChanged()
{
pictureBoxArt.BackColor = Options.PreviewBackgroundColor;
- if (_selectedIndex >= 0)
+ if (_selectedIndex < 0)
{
- if (tabControl2.SelectedIndex == 0)
- {
- AfterSelectTreeViewItem(this, new TreeViewEventArgs(treeViewItem.SelectedNode));
- }
- else
- {
- AfterSelectTreeViewLand(this, new TreeViewEventArgs(treeViewLand.SelectedNode));
- }
+ return;
+ }
+
+ if (tabControl2.SelectedIndex == 0)
+ {
+ UpdateSelectedItemPreview(_selectedIndex);
+ }
+ else
+ {
+ UpdateSelectedLandPreview(_selectedIndex);
}
}
- private void AfterSelectTreeViewItem(object sender, TreeViewEventArgs e)
+ private void UpdateSelectedItemPreview(int graphic)
{
SaveColor();
- _selectedIndex = (int)e.Node.Tag;
+ _selectedIndex = graphic;
+ _selectedIsItem = true;
if (Art.IsValidStatic(_selectedIndex))
{
@@ -261,11 +540,12 @@ private void AfterSelectTreeViewItem(object sender, TreeViewEventArgs e)
buttonRevertAll.Enabled = _originalLandColors.Count > 0 || _originalItemColors.Count > 0;
}
- private void AfterSelectTreeViewLand(object sender, TreeViewEventArgs e)
+ private void UpdateSelectedLandPreview(int graphic)
{
SaveColor();
- _selectedIndex = (int)e.Node.Tag;
+ _selectedIndex = graphic;
+ _selectedIsItem = false;
if (Art.IsValidLand(_selectedIndex))
{
@@ -309,16 +589,8 @@ private void OnClickSaveFile(object sender, EventArgs e)
_originalItemColors.Clear();
_originalLandColors.Clear();
-
- foreach (TreeNode node in treeViewItem.Nodes)
- {
- node.ForeColor = SystemColors.WindowText;
- }
-
- foreach (TreeNode node in treeViewLand.Nodes)
- {
- node.ForeColor = SystemColors.WindowText;
- }
+ tileViewItem.Invalidate();
+ tileViewLand.Invalidate();
Options.ChangedUltimaClass["RadarCol"] = false;
@@ -327,7 +599,10 @@ private void OnClickSaveFile(object sender, EventArgs e)
private void SaveColor()
{
- SaveColor(_selectedIndex, CurrentColor, tabControl2.SelectedIndex == 0);
+ // Use the tab the index originated from, NOT tabControl2.SelectedIndex.
+ // Otherwise switching tabs and clicking a row in the new tab would
+ // commit the editor's color to the previous tab's id, mis-attributed.
+ SaveColor(_selectedIndex, CurrentColor, _selectedIsItem);
}
private void SaveColor(int index, ushort color, bool isItemTile)
@@ -340,32 +615,18 @@ private void SaveColor(int index, ushort color, bool isItemTile)
if (isItemTile)
{
var datafileColor = RadarCol.GetItemColor(index);
- if (color != datafileColor)
+ if (color != datafileColor && _originalItemColors.TryAdd(index, datafileColor))
{
- if (_originalItemColors.TryAdd(index, datafileColor))
- {
- var previousNode = treeViewItem.Nodes.OfType()
- .FirstOrDefault(node => node.Tag.Equals(index));
-
- if (previousNode != null)
- previousNode.ForeColor = Color.Blue;
- }
+ RedrawItemRow(index);
}
RadarCol.SetItemColor(index, color);
}
else
{
var datafileColor = RadarCol.GetLandColor(index);
- if (color != datafileColor)
+ if (color != datafileColor && _originalLandColors.TryAdd(index, datafileColor))
{
- if (_originalLandColors.TryAdd(index, datafileColor))
- {
- var previousNode = treeViewLand.Nodes.OfType()
- .FirstOrDefault(node => node.Tag.Equals(index));
-
- if (previousNode != null)
- previousNode.ForeColor = Color.Blue;
- }
+ RedrawLandRow(index);
}
RadarCol.SetLandColor(index, color);
}
@@ -408,16 +669,8 @@ private void OnClickRevertAll(object sender, EventArgs e)
_originalItemColors.Clear();
_originalLandColors.Clear();
-
- foreach (TreeNode node in treeViewItem.Nodes)
- {
- node.ForeColor = SystemColors.WindowText;
- }
-
- foreach (TreeNode node in treeViewLand.Nodes)
- {
- node.ForeColor = SystemColors.WindowText;
- }
+ tileViewItem.Invalidate();
+ tileViewLand.Invalidate();
}
private void OnClickRevert(object sender, EventArgs e)
@@ -430,28 +683,16 @@ private void OnClickRevert(object sender, EventArgs e)
{
CurrentColor = color;
RadarCol.SetItemColor(_selectedIndex, color);
-
- var node = treeViewItem.Nodes.OfType()
- .FirstOrDefault(node => node.Tag.Equals(_selectedIndex));
-
- if (node != null)
- node.ForeColor = SystemColors.WindowText;
-
_originalItemColors.Remove(_selectedIndex);
+ RedrawItemRow(_selectedIndex);
}
}
else if (_originalLandColors.TryGetValue(_selectedIndex, out var color))
{
CurrentColor = color;
RadarCol.SetLandColor(_selectedIndex, color);
-
- var node = treeViewLand.Nodes.OfType()
- .FirstOrDefault(node => node.Tag.Equals(_selectedIndex));
-
- if (node != null)
- node.ForeColor = SystemColors.WindowText;
-
_originalLandColors.Remove(_selectedIndex);
+ RedrawLandRow(_selectedIndex);
}
}
@@ -486,22 +727,35 @@ private void OnClickSaveColor(object sender, EventArgs e)
private void OnClickSetRangeFrom(object sender, EventArgs e)
{
- var node = ((TreeView)((ContextMenuStrip)((ToolStripItem)sender).Owner).SourceControl).SelectedNode;
-
- if (node != null)
+ int graphic = GetGraphicFromContextSource(sender);
+ if (graphic >= 0)
{
- textBoxMeanFrom.Text = node.Tag.ToString();
+ textBoxMeanFrom.Text = graphic.ToString();
}
}
private void OnClickSetRangeTo(object sender, EventArgs e)
{
- var node = ((TreeView)((ContextMenuStrip)((ToolStripItem)sender).Owner).SourceControl).SelectedNode;
+ int graphic = GetGraphicFromContextSource(sender);
+ if (graphic >= 0)
+ {
+ textBoxMeanTo.Text = graphic.ToString();
+ }
+ }
- if (node != null)
+ private int GetGraphicFromContextSource(object sender)
+ {
+ var tv = ((ContextMenuStrip)((ToolStripItem)sender).Owner).SourceControl as TileView.TileViewControl;
+ if (tv == null || tv.FocusIndex < 0)
{
- textBoxMeanTo.Text = node.Tag.ToString();
+ return -1;
}
+ var indices = tv == tileViewItem ? _itemIndices : _landIndices;
+ if (tv.FocusIndex >= indices.Length)
+ {
+ return -1;
+ }
+ return indices[tv.FocusIndex];
}
private void OnChangeR(object sender, EventArgs e)
@@ -744,12 +998,12 @@ private void OnClickRangeToIndividualAverage(object sender, EventArgs e)
private void OnClickSelectItemsTab(object sender, EventArgs e)
{
- if (treeViewItem.SelectedNode == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
var found = ItemsControl.SearchGraphic(index);
if (!found)
{
@@ -759,23 +1013,23 @@ private void OnClickSelectItemsTab(object sender, EventArgs e)
private void OnClickSelectItemTiledataTab(object sender, EventArgs e)
{
- if (treeViewItem.SelectedNode == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
TileDataControl.Select(index, false);
}
private void OnClickSelectLandTilesTab(object sender, EventArgs e)
{
- if (treeViewLand.SelectedNode == null)
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
var found = LandTilesControl.SearchGraphic(index);
if (!found)
{
@@ -785,12 +1039,12 @@ private void OnClickSelectLandTilesTab(object sender, EventArgs e)
private void OnClickSelectLandTiledataTab(object sender, EventArgs e)
{
- if (treeViewLand.SelectedNode == null)
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
TileDataControl.Select(index, true);
}
@@ -810,16 +1064,18 @@ private void OnClickImport(object sender, EventArgs e)
RadarCol.ImportFromCSV(dialog.FileName);
if (tabControl2.SelectedTab == tabControl2.TabPages[0])
{
- if (treeViewItem.SelectedNode != null)
+ int graphic = GetSelectedItemGraphic();
+ if (graphic >= 0)
{
- AfterSelectTreeViewItem(this, new TreeViewEventArgs(treeViewItem.SelectedNode));
+ UpdateSelectedItemPreview(graphic);
}
}
else
{
- if (treeViewLand.SelectedNode != null)
+ int graphic = GetSelectedLandGraphic();
+ if (graphic >= 0)
{
- AfterSelectTreeViewLand(this, new TreeViewEventArgs(treeViewLand.SelectedNode));
+ UpdateSelectedLandPreview(graphic);
}
}
}
@@ -987,137 +1243,90 @@ private void OnTextChangedFilterItems(object sender, EventArgs e)
FilterChange(textFilterItems, FilterItems);
}
- private void ApplyFilter(TreeView control, string filterText)
+ private void FilterLand(string filterText)
{
- object table;
- int max;
- Dictionary originalColors;
- HashSet selected;
- Func getName;
-
- if (control == treeViewItem)
- {
- table = TileData.ItemTable;
- max = Art.GetMaxItemId();
- originalColors = _originalItemColors;
- getName = (int index) => TileData.ItemTable[index].Name;
- selected = _selectedItems;
- }
- else
- {
- table = TileData.LandTable;
- max = 0x3FFF;
- originalColors = _originalLandColors;
- getName = (int index) => TileData.LandTable[index].Name;
- selected = _selectedLand;
- }
-
- Cursor.Current = Cursors.WaitCursor;
- control.BeginUpdate();
- try
+ int max = TileData.LandTable?.Length ?? 0;
+ var matches = new List(max);
+ for (int i = 0; i < max; ++i)
{
- if (table == null)
+ string name = TileData.LandTable[i].Name;
+ if (name.ContainsCaseInsensitive(filterText))
{
- return;
+ matches.Add(i);
}
-
- control.Nodes.Clear();
-
- List nodes = [];
- for (int i = 0; i < max; ++i)
- {
- var name = getName(i);
- if (!name.ContainsCaseInsensitive(filterText))
- {
- continue;
- }
-
- var node = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", i, name))
- {
- Tag = i,
- Checked = selected.Contains(i)
- };
-
- if (originalColors.ContainsKey(i))
- {
- node.ForeColor = Color.Blue;
- }
-
- nodes.Add(node);
- }
-
- control.Nodes.AddRange(nodes.ToArray());
- }
- finally
- {
- control.EndUpdate();
- Cursor.Current = Cursors.Default;
}
- }
-
- private void FilterLand(string filterText)
- {
- ApplyFilter(treeViewLand, filterText);
+ _landIndices = matches.ToArray();
+ tileViewLand.VirtualListSize = _landIndices.Length;
+ SyncLandSelectedIndicesFromHashSet();
+ tileViewLand.Invalidate();
}
private void FilterItems(string filterText)
{
- ApplyFilter(treeViewItem, filterText);
- }
-
- private void AfterCheckTreeViewItem(object sender, TreeViewEventArgs e)
- {
- var index = (int)e.Node.Tag;
- if (e.Node.Checked)
+ int max = TileData.ItemTable != null ? Art.GetMaxItemId() : 0;
+ var matches = new List(max);
+ for (int i = 0; i < max; ++i)
{
- _selectedItems.Add(index);
- }
- else
- {
- _selectedItems.Remove(index);
+ string name = TileData.ItemTable[i].Name;
+ if (name.ContainsCaseInsensitive(filterText))
+ {
+ matches.Add(i);
+ }
}
+ _itemIndices = matches.ToArray();
+ tileViewItem.VirtualListSize = _itemIndices.Length;
+ SyncItemSelectedIndicesFromHashSet();
+ tileViewItem.Invalidate();
}
- private void AfterCheckTreeViewLand(object sender, TreeViewEventArgs e)
+ private void SetAllCheckedItems(bool isChecked)
{
- var index = (int)e.Node.Tag;
- if (e.Node.Checked)
+ if (isChecked)
{
- _selectedLand.Add(index);
+ foreach (int graphic in _itemIndices)
+ {
+ _selectedItems.Add(graphic);
+ }
}
else
{
- _selectedLand.Remove(index);
+ foreach (int graphic in _itemIndices)
+ {
+ _selectedItems.Remove(graphic);
+ }
}
+ SyncItemSelectedIndicesFromHashSet();
+ tileViewItem.Invalidate();
}
- private static void SetAllCheckedStatus(TreeView treeView, bool isChecked)
+ private void SetAllCheckedLand(bool isChecked)
{
- treeView.BeginUpdate();
- try
+ if (isChecked)
{
- Cursor.Current = Cursors.WaitCursor;
-
- foreach (TreeNode node in treeView.Nodes)
+ foreach (int graphic in _landIndices)
{
- node.Checked = isChecked;
+ _selectedLand.Add(graphic);
}
}
- finally
+ else
{
- treeView.EndUpdate();
- Cursor.Current = Cursors.Default;
+ foreach (int graphic in _landIndices)
+ {
+ _selectedLand.Remove(graphic);
+ }
}
+ SyncLandSelectedIndicesFromHashSet();
+ tileViewLand.Invalidate();
}
private void OnClickSelectAllItems(object sender, EventArgs e)
{
- SetAllCheckedStatus(treeViewItem, true);
+ SetAllCheckedItems(true);
}
private void OnClickSelectNoneItems(object sender, EventArgs e)
{
- SetAllCheckedStatus(treeViewItem, false);
+ SetAllCheckedItems(false);
}
private void OnCheckedChangeUseSelection(object sender, EventArgs e)
@@ -1140,12 +1349,12 @@ private void OnCheckedChangeUseRange(object sender, EventArgs e)
private void OnClickSelectAllLand(object sender, EventArgs e)
{
- SetAllCheckedStatus(treeViewLand, true);
+ SetAllCheckedLand(true);
}
private void OnClickSelectNoneLand(object sender, EventArgs e)
{
- SetAllCheckedStatus(treeViewLand, false);
+ SetAllCheckedLand(false);
}
}
}
diff --git a/UoFiddler.Controls/UserControls/SoundsControl.Designer.cs b/UoFiddler.Controls/UserControls/SoundsControl.Designer.cs
index dd69fdb..370fff8 100644
--- a/UoFiddler.Controls/UserControls/SoundsControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/SoundsControl.Designer.cs
@@ -40,7 +40,8 @@ protected override void Dispose(bool disposing)
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
- treeView = new System.Windows.Forms.TreeView();
+ listView = new System.Windows.Forms.ListView();
+ listViewColumn = new System.Windows.Forms.ColumnHeader();
cmStripSounds = new System.Windows.Forms.ContextMenuStrip(components);
nameSortToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
tsSeparator1 = new System.Windows.Forms.ToolStripSeparator();
@@ -108,20 +109,26 @@ private void InitializeComponent()
tableLayoutPanel6.SuspendLayout();
SuspendLayout();
//
- // treeView
- //
- treeView.ContextMenuStrip = cmStripSounds;
- treeView.Dock = System.Windows.Forms.DockStyle.Fill;
- treeView.HideSelection = false;
- treeView.Location = new System.Drawing.Point(0, 0);
- treeView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- treeView.Name = "treeView";
- treeView.Size = new System.Drawing.Size(406, 670);
- treeView.TabIndex = 0;
- treeView.BeforeSelect += BeforeSelect;
- treeView.AfterSelect += AfterSelect;
- treeView.NodeMouseDoubleClick += OnDoubleClick;
- treeView.KeyDown += TreeView_KeyDown;
+ // listView
+ //
+ listView.ContextMenuStrip = cmStripSounds;
+ listView.Dock = System.Windows.Forms.DockStyle.Fill;
+ listView.HideSelection = false;
+ listView.Location = new System.Drawing.Point(0, 0);
+ listView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ listView.Name = "listView";
+ listView.Size = new System.Drawing.Size(406, 670);
+ listView.TabIndex = 0;
+ listView.View = System.Windows.Forms.View.Details;
+ listView.FullRowSelect = true;
+ listView.MultiSelect = false;
+ listView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
+ listViewColumn.Text = "Sound";
+ listViewColumn.Width = 400;
+ listView.Columns.Add(listViewColumn);
+ listView.SelectedIndexChanged += AfterSelect;
+ listView.MouseDoubleClick += OnDoubleClick;
+ listView.KeyDown += TreeView_KeyDown;
//
// cmStripSounds
//
@@ -276,7 +283,7 @@ private void InitializeComponent()
//
// splitContainer1.Panel1
//
- splitContainer1.Panel1.Controls.Add(treeView);
+ splitContainer1.Panel1.Controls.Add(listView);
//
// splitContainer1.Panel2
//
@@ -752,7 +759,8 @@ private void InitializeComponent()
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem1;
private System.Windows.Forms.ToolStripStatusLabel toolStripStatusSpacer;
- private System.Windows.Forms.TreeView treeView;
+ private System.Windows.Forms.ListView listView;
+ private System.Windows.Forms.ColumnHeader listViewColumn;
private System.Windows.Forms.ToolStripSeparator tsSeparator1;
private System.Windows.Forms.ToolStripSeparator tsSeparator2;
private System.Windows.Forms.SplitContainer splitContainer1;
diff --git a/UoFiddler.Controls/UserControls/SoundsControl.cs b/UoFiddler.Controls/UserControls/SoundsControl.cs
index 90b1da3..d55b070 100644
--- a/UoFiddler.Controls/UserControls/SoundsControl.cs
+++ b/UoFiddler.Controls/UserControls/SoundsControl.cs
@@ -45,9 +45,12 @@ public SoundsControl()
_spTimer = new Timer();
_spTimer.Tick += OnSpTimerTick;
- treeView.LabelEdit = true;
- treeView.BeforeLabelEdit += TreeView_BeforeLabelEdit;
- treeView.AfterLabelEdit += TreeViewOnAfterLabelEdit;
+ listView.LabelEdit = true;
+ listView.BeforeLabelEdit += ListView_BeforeLabelEdit;
+ listView.AfterLabelEdit += ListViewOnAfterLabelEdit;
+ // ListView's default Sort() uses ListView.Sorting; enable
+ // ascending text sort so the existing toggle keeps working.
+ listView.Sorting = System.Windows.Forms.SortOrder.Ascending;
_soundIdOffset = GetSoundIdOffset();
}
@@ -79,58 +82,54 @@ private void OnLoad(object sender, EventArgs e)
int? oldItem = null;
- if (treeView.SelectedNode != null)
+ if (listView.SelectedItems.Count > 0)
{
- oldItem = (int)treeView.SelectedNode.Tag;
+ oldItem = (int)listView.SelectedItems[0].Tag;
}
- treeView.BeginUpdate();
+ listView.BeginUpdate();
try
{
- treeView.Nodes.Clear();
+ listView.Items.Clear();
_soundIdOffset = GetSoundIdOffset();
- var cache = new List();
+ var cache = new List();
for (int i = 0; i < _soundsLength; ++i)
{
if (Sounds.IsValidSound(i, out string name, out bool translated))
{
- TreeNode node = new TreeNode($"0x{i + _soundIdOffset:X3} {name}")
- {
- Tag = i
- };
+ var item = new ListViewItem($"0x{i + _soundIdOffset:X3} {name}") { Tag = i };
if (translated)
{
- node.ForeColor = Color.Blue;
- node.NodeFont = new Font(Font, FontStyle.Underline);
+ item.ForeColor = Color.Blue;
+ item.Font = new Font(Font, FontStyle.Underline);
}
- cache.Add(node);
+ cache.Add(item);
}
else if (showFreeSlotsToolStripMenuItem.Checked)
{
- TreeNode node = new TreeNode($"0x{i:X3} ")
+ cache.Add(new ListViewItem($"0x{i:X3} ")
{
Tag = i,
ForeColor = Color.Red
- };
-
- cache.Add(node);
+ });
}
}
- treeView.Nodes.AddRange(cache.ToArray());
+ listView.Items.AddRange(cache.ToArray());
}
finally
{
- treeView.EndUpdate();
+ listView.EndUpdate();
}
- if (treeView.Nodes.Count > 0)
+ if (listView.Items.Count > 0)
{
- treeView.SelectedNode = treeView.Nodes[0];
+ listView.Items[0].Selected = true;
+ listView.Items[0].EnsureVisible();
}
_sp = new System.Media.SoundPlayer();
@@ -184,12 +183,21 @@ private void OnFilePathChangeEvent()
private void OnClickPlay(object sender, EventArgs e)
{
- PlaySound((int)treeView.SelectedNode.Tag);
+ if (listView.SelectedItems.Count == 0)
+ {
+ return;
+ }
+ PlaySound((int)listView.SelectedItems[0].Tag);
}
- private void OnDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
+ private void OnDoubleClick(object sender, MouseEventArgs e)
{
- PlaySound((int)e.Node.Tag);
+ ListViewHitTestInfo hit = listView.HitTest(e.Location);
+ if (hit.Item == null)
+ {
+ return;
+ }
+ PlaySound((int)hit.Item.Tag);
}
private void OnClickStop(object sender, EventArgs e)
@@ -218,7 +226,7 @@ private void PlaySound(int id)
stopButton.Visible = false;
StopSoundButton.Enabled = false;
- if (treeView.SelectedNode == null)
+ if (listView.SelectedItems.Count == 0)
{
return;
}
@@ -248,17 +256,18 @@ private void PlaySound(int id)
}
}
- private void BeforeSelect(object sender, TreeViewCancelEventArgs e)
+ private void AfterSelect(object sender, EventArgs e)
{
+ // Mirror the old TreeView BeforeSelect behaviour: stop playback
+ // when the user moves to a different row.
if (_playing)
{
StopSound();
}
- }
- private void AfterSelect(object sender, EventArgs e)
- {
- if (treeView.SelectedNode == null)
+ ListViewItem selected = listView.SelectedItems.Count > 0 ? listView.SelectedItems[0] : null;
+
+ if (selected == null)
{
playSoundToolStripMenuItem.Enabled = false;
extractSoundToolStripMenuItem.Enabled = false;
@@ -267,13 +276,13 @@ private void AfterSelect(object sender, EventArgs e)
replaceToolStripMenuItem.Text = "Insert/Replace";
}
- if (treeView.SelectedNode != null)
+ if (selected != null)
{
- double length = Sounds.GetSoundLength((int)treeView.SelectedNode.Tag);
+ double length = Sounds.GetSoundLength((int)selected.Tag);
seconds.Text = length > 0 ? $"{length:f}s" : "Empty Slot";
}
- bool isValidSound = treeView.SelectedNode != null && Sounds.IsValidSound((int)treeView.SelectedNode.Tag, out _, out _);
+ bool isValidSound = selected != null && Sounds.IsValidSound((int)selected.Tag, out _, out _);
playSoundToolStripMenuItem.Enabled = isValidSound;
extractSoundToolStripMenuItem.Enabled = isValidSound;
@@ -282,12 +291,12 @@ private void AfterSelect(object sender, EventArgs e)
replaceToolStripMenuItem.Enabled = true;
replaceToolStripMenuItem.Text = isValidSound ? "Replace" : "Insert";
- SelectedSoundGroup.Visible = treeView.SelectedNode != null;
+ SelectedSoundGroup.Visible = selected != null;
- if (treeView.SelectedNode != null)
+ if (selected != null)
{
- SelectedSoundGroup.Text = $"Current Sound: {treeView.SelectedNode.Text} - Duration: {seconds.Text}";
- IdInsertTextbox.Text = $"0x{(int)treeView.SelectedNode.Tag + _soundIdOffset:X}";
+ SelectedSoundGroup.Text = $"Current Sound: {selected.Text} - Duration: {seconds.Text}";
+ IdInsertTextbox.Text = $"0x{(int)selected.Tag + _soundIdOffset:X}";
}
}
@@ -302,28 +311,28 @@ private void OnChangeSort(object sender, EventArgs e)
}
int? oldItem = null;
- if (treeView.SelectedNode != null)
+ if (listView.SelectedItems.Count > 0)
{
- oldItem = (int)treeView.SelectedNode.Tag;
+ oldItem = (int)listView.SelectedItems[0].Tag;
}
const string delimiter = " ";
- treeView.BeginUpdate();
+ listView.BeginUpdate();
- for (int i = 0; i < treeView.Nodes.Count; ++i)
+ for (int i = 0; i < listView.Items.Count; ++i)
{
- string name = treeView.Nodes[i].Text;
+ string name = listView.Items[i].Text;
int splitIndex = nameSortToolStripMenuItem.Checked
? name.IndexOf(delimiter, StringComparison.Ordinal)
: name.LastIndexOf(delimiter, StringComparison.Ordinal);
- treeView.Nodes[i].Text = $"{name.Substring(splitIndex).Trim()} {name.Substring(0, splitIndex).Trim()}";
+ listView.Items[i].Text = $"{name.Substring(splitIndex).Trim()} {name.Substring(0, splitIndex).Trim()}";
}
- treeView.Sort();
- treeView.EndUpdate();
+ listView.Sort();
+ listView.EndUpdate();
if (oldItem != null)
{
@@ -334,12 +343,13 @@ private void OnChangeSort(object sender, EventArgs e)
private void DoSearchName(string name, bool next, bool prev)
{
int index = 0;
+ int selectedIndex = listView.SelectedItems.Count > 0 ? listView.SelectedItems[0].Index : -1;
if (prev)
{
- if (treeView.SelectedNode.Index >= 0)
+ if (selectedIndex >= 0)
{
- index = treeView.SelectedNode.Index - _soundIdOffset;
+ index = selectedIndex - _soundIdOffset;
}
if (index <= 0)
@@ -349,16 +359,15 @@ private void DoSearchName(string name, bool next, bool prev)
for (int i = index - 1; i >= 0; --i)
{
- TreeNode node = treeView.Nodes[i];
- if (!node.Text.ContainsCaseInsensitive(name))
+ ListViewItem item = listView.Items[i];
+ if (!item.Text.ContainsCaseInsensitive(name))
{
continue;
}
- treeView.SelectedNode = node;
-
- node.EnsureVisible();
-
+ listView.SelectedItems.Clear();
+ item.Selected = true;
+ item.EnsureVisible();
return;
}
}
@@ -366,29 +375,28 @@ private void DoSearchName(string name, bool next, bool prev)
{
if (next)
{
- if (treeView.SelectedNode.Index >= 0)
+ if (selectedIndex >= 0)
{
- index = treeView.SelectedNode.Index + 1;
+ index = selectedIndex + 1;
}
- if (index >= treeView.Nodes.Count)
+ if (index >= listView.Items.Count)
{
index = 0;
}
}
- for (int i = index; i < treeView.Nodes.Count; ++i)
+ for (int i = index; i < listView.Items.Count; ++i)
{
- TreeNode node = treeView.Nodes[i];
- if (!node.Text.ContainsCaseInsensitive(name))
+ ListViewItem item = listView.Items[i];
+ if (!item.Text.ContainsCaseInsensitive(name))
{
continue;
}
- treeView.SelectedNode = node;
-
- node.EnsureVisible();
-
+ listView.SelectedItems.Clear();
+ item.Selected = true;
+ item.EnsureVisible();
return;
}
}
@@ -396,12 +404,12 @@ private void DoSearchName(string name, bool next, bool prev)
private void OnClickExtract(object sender, EventArgs e)
{
- if (treeView.SelectedNode == null)
+ if (listView.SelectedItems.Count == 0)
{
return;
}
- int id = (int)treeView.SelectedNode.Tag;
+ int id = (int)listView.SelectedItems[0].Tag;
Sounds.IsValidSound(id, out string name, out _);
@@ -437,14 +445,15 @@ private void OnClickSave(object sender, EventArgs e)
private void OnClickRemove(object sender, EventArgs e)
{
- if (treeView.SelectedNode == null)
+ if (listView.SelectedItems.Count == 0)
{
return;
}
- int id = (int)treeView.SelectedNode.Tag;
+ ListViewItem selected = listView.SelectedItems[0];
+ int id = (int)selected.Tag;
- DialogResult result = MessageBox.Show($"Are you sure to remove {treeView.SelectedNode.Text}?", "Remove",
+ DialogResult result = MessageBox.Show($"Are you sure to remove {selected.Text}?", "Remove",
MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (result != DialogResult.Yes)
@@ -456,12 +465,13 @@ private void OnClickRemove(object sender, EventArgs e)
if (!showFreeSlotsToolStripMenuItem.Checked)
{
- treeView.SelectedNode.Remove();
+ listView.Items.Remove(selected);
}
else
{
- treeView.SelectedNode.Text = $"0x{(int)treeView.SelectedNode.Tag + _soundIdOffset:X3}";
- treeView.SelectedNode.ForeColor = Color.Red;
+ selected.Text = $"0x{id + _soundIdOffset:X3}";
+ selected.ForeColor = Color.Red;
+ selected.Font = Font;
}
AfterSelect(this, e);
@@ -479,18 +489,18 @@ private void OnClickExportSoundListCsv(object sender, EventArgs e)
public bool SearchId(int id)
{
- for (int i = 0; i < treeView.Nodes.Count; ++i)
+ for (int i = 0; i < listView.Items.Count; ++i)
{
- TreeNode node = treeView.Nodes[i];
+ ListViewItem item = listView.Items[i];
- if ((int)node.Tag != id)
+ if ((int)item.Tag != id)
{
continue;
}
- treeView.SelectedNode = node;
- node.EnsureVisible();
-
+ listView.SelectedItems.Clear();
+ item.Selected = true;
+ item.EnsureVisible();
return true;
}
@@ -530,7 +540,12 @@ private void OnClickReplace(object sender, EventArgs e)
file = _wavChosen;
}
- int id = (int)treeView.SelectedNode.Tag;
+ if (listView.SelectedItems.Count == 0)
+ {
+ return;
+ }
+
+ int id = (int)listView.SelectedItems[0].Tag;
string name = Path.GetFileName(file);
if (!File.Exists(file))
@@ -545,7 +560,7 @@ private void OnClickReplace(object sender, EventArgs e)
if (Sounds.IsValidSound(id, out _, out _))
{
- DialogResult result = MessageBox.Show($"Are you sure to replace {treeView.SelectedNode.Text}?",
+ DialogResult result = MessageBox.Show($"Are you sure to replace {listView.SelectedItems[0].Text}?",
"Replace", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (result != DialogResult.Yes)
@@ -564,61 +579,59 @@ private void OnClickReplace(object sender, EventArgs e)
return;
}
- TreeNode node = new TreeNode($"0x{id + _soundIdOffset:X3} {name}");
+ ListViewItem item = new ListViewItem($"0x{id + _soundIdOffset:X3} {name}") { Tag = id };
if (nameSortToolStripMenuItem.Checked)
{
- node.Text = $"{name} 0x{id + _soundIdOffset:X3}";
+ item.Text = $"{name} 0x{id + _soundIdOffset:X3}";
}
- node.Tag = id;
-
bool done = false;
- for (int i = 0; i < treeView.Nodes.Count; ++i)
+ for (int i = 0; i < listView.Items.Count; ++i)
{
- if ((int)treeView.Nodes[i].Tag != id)
+ if ((int)listView.Items[i].Tag != id)
{
continue;
}
done = true;
- treeView.Nodes.RemoveAt(i);
- treeView.Nodes.Insert(i, node);
+ listView.Items.RemoveAt(i);
+ listView.Items.Insert(i, item);
break;
}
if (!done)
{
- treeView.Nodes.Add(node);
- treeView.Sort();
+ listView.Items.Add(item);
+ listView.Sort();
}
- node.EnsureVisible();
-
- treeView.SelectedNode = node;
- treeView.Invalidate();
+ listView.SelectedItems.Clear();
+ item.Selected = true;
+ item.EnsureVisible();
+ listView.Invalidate();
Options.ChangedUltimaClass["Sound"] = true;
}
private void NextFreeSlotToolStripMenuItem_Click(object sender, EventArgs e)
{
- for (int i = treeView.Nodes.IndexOf(treeView.SelectedNode) + 1; i < treeView.Nodes.Count; ++i)
+ int start = listView.SelectedItems.Count > 0 ? listView.SelectedItems[0].Index + 1 : 0;
+ for (int i = start; i < listView.Items.Count; ++i)
{
- TreeNode node = treeView.Nodes[i];
+ ListViewItem item = listView.Items[i];
- if (Sounds.IsValidSound((int)node.Tag, out _, out _))
+ if (Sounds.IsValidSound((int)item.Tag, out _, out _))
{
continue;
}
- treeView.SelectedNode = node;
-
- node.EnsureVisible();
-
+ listView.SelectedItems.Clear();
+ item.Selected = true;
+ item.EnsureVisible();
return;
}
}
@@ -634,19 +647,19 @@ private void TreeView_KeyDown(object sender, KeyEventArgs e)
}
else if (e.KeyCode == Keys.F2)
{
- if (treeView.SelectedNode == null)
+ if (listView.SelectedItems.Count == 0)
{
return;
}
- treeView.SelectedNode.BeginEdit();
+ listView.SelectedItems[0].BeginEdit();
e.Handled = true;
e.SuppressKeyPress = true;
}
else if (e.KeyCode == Keys.Enter)
{
- if (treeView.Nodes.OfType().Any(n => n.IsEditing))
+ if (_isEditingLabel)
{
return;
}
@@ -665,9 +678,13 @@ private void TreeView_KeyDown(object sender, KeyEventArgs e)
}
}
- private void TreeViewOnAfterLabelEdit(object sender, NodeLabelEditEventArgs e)
+ private bool _isEditingLabel;
+
+ private void ListViewOnAfterLabelEdit(object sender, LabelEditEventArgs e)
{
- int id = (int)e.Node.Tag;
+ _isEditingLabel = false;
+ ListViewItem item = listView.Items[e.Item];
+ int id = (int)item.Tag;
UoSound sound = Sounds.GetSound(id);
@@ -689,23 +706,37 @@ private void TreeViewOnAfterLabelEdit(object sender, NodeLabelEditEventArgs e)
Sounds.IsValidSound(id, out string name, out _);
- e.Node.Text = $"0x{id + _soundIdOffset:X3} {name}";
-
- if (nameSortToolStripMenuItem.Checked)
- {
- e.Node.Text = $"{name} 0x{id + _soundIdOffset:X3}";
- }
+ item.Text = nameSortToolStripMenuItem.Checked
+ ? $"{name} 0x{id + _soundIdOffset:X3}"
+ : $"0x{id + _soundIdOffset:X3} {name}";
+ // ListView semantics: CancelEdit=true rejects the framework's
+ // auto-apply of e.Label, since we already updated Text above.
e.CancelEdit = true;
}
- private void TreeView_BeforeLabelEdit(object sender, NodeLabelEditEventArgs e)
+ private void ListView_BeforeLabelEdit(object sender, LabelEditEventArgs e)
{
- int id = (int)e.Node.Tag;
+ ListViewItem item = listView.Items[e.Item];
+ int id = (int)item.Tag;
if (Sounds.IsValidSound(id, out string name, out bool translated) && !translated)
{
- treeView.SetEditText(name);
+ _isEditingLabel = true;
+ // Seed the in-place edit textbox with the bare name (not the
+ // formatted "0x... name" label) so renaming is ergonomic.
+ BeginInvoke(new Action(() =>
+ {
+ foreach (Control c in listView.Controls)
+ {
+ if (c is TextBox edit)
+ {
+ edit.Text = name;
+ edit.SelectAll();
+ break;
+ }
+ }
+ }));
}
else
{
diff --git a/UoFiddler.Controls/UserControls/TileDataControl.Designer.cs b/UoFiddler.Controls/UserControls/TileDataControl.Designer.cs
index a78dfe5..27f1276 100644
--- a/UoFiddler.Controls/UserControls/TileDataControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/TileDataControl.Designer.cs
@@ -45,7 +45,8 @@ private void InitializeComponent()
tabPageItems = new System.Windows.Forms.TabPage();
splitContainer1 = new System.Windows.Forms.SplitContainer();
splitContainer2 = new System.Windows.Forms.SplitContainer();
- treeViewItem = new System.Windows.Forms.TreeView();
+ listViewItem = new System.Windows.Forms.ListView();
+ listViewItemColumn = new System.Windows.Forms.ColumnHeader();
ItemsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
selectInItemsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
selectRadarColorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -95,7 +96,8 @@ private void InitializeComponent()
tabPageLand = new System.Windows.Forms.TabPage();
splitContainer5 = new System.Windows.Forms.SplitContainer();
splitContainer6 = new System.Windows.Forms.SplitContainer();
- treeViewLand = new System.Windows.Forms.TreeView();
+ listViewLand = new System.Windows.Forms.ListView();
+ listViewLandColumn = new System.Windows.Forms.ColumnHeader();
LandTilesContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
selectInLandtilesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
selToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -222,7 +224,7 @@ private void InitializeComponent()
//
// splitContainer2.Panel1
//
- splitContainer2.Panel1.Controls.Add(treeViewItem);
+ splitContainer2.Panel1.Controls.Add(listViewItem);
//
// splitContainer2.Panel2
//
@@ -232,18 +234,26 @@ private void InitializeComponent()
splitContainer2.SplitterWidth = 5;
splitContainer2.TabIndex = 0;
//
- // treeViewItem
- //
- treeViewItem.ContextMenuStrip = ItemsContextMenuStrip;
- treeViewItem.Dock = System.Windows.Forms.DockStyle.Fill;
- treeViewItem.HideSelection = false;
- treeViewItem.Location = new System.Drawing.Point(0, 0);
- treeViewItem.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- treeViewItem.Name = "treeViewItem";
- treeViewItem.Size = new System.Drawing.Size(245, 207);
- treeViewItem.TabIndex = 0;
- treeViewItem.BeforeExpand += OnItemDataNodeExpanded;
- treeViewItem.AfterSelect += AfterSelectTreeViewItem;
+ // listViewItem
+ //
+ listViewItem.ContextMenuStrip = ItemsContextMenuStrip;
+ listViewItem.Dock = System.Windows.Forms.DockStyle.Fill;
+ listViewItem.HideSelection = false;
+ listViewItem.Location = new System.Drawing.Point(0, 0);
+ listViewItem.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ listViewItem.Name = "listViewItem";
+ listViewItem.Size = new System.Drawing.Size(245, 207);
+ listViewItem.TabIndex = 0;
+ listViewItem.View = System.Windows.Forms.View.Details;
+ listViewItem.VirtualMode = true;
+ listViewItem.FullRowSelect = true;
+ listViewItem.MultiSelect = false;
+ listViewItem.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
+ listViewItemColumn.Text = "Item";
+ listViewItemColumn.Width = 240;
+ listViewItem.Columns.Add(listViewItemColumn);
+ listViewItem.RetrieveVirtualItem += OnRetrieveItemVirtualItem;
+ listViewItem.SelectedIndexChanged += OnItemSelectedIndexChanged;
//
// ItemsContextMenuStrip
//
@@ -777,7 +787,7 @@ private void InitializeComponent()
//
// splitContainer6.Panel1
//
- splitContainer6.Panel1.Controls.Add(treeViewLand);
+ splitContainer6.Panel1.Controls.Add(listViewLand);
//
// splitContainer6.Panel2
//
@@ -787,17 +797,26 @@ private void InitializeComponent()
splitContainer6.SplitterWidth = 5;
splitContainer6.TabIndex = 0;
//
- // treeViewLand
- //
- treeViewLand.ContextMenuStrip = LandTilesContextMenuStrip;
- treeViewLand.Dock = System.Windows.Forms.DockStyle.Fill;
- treeViewLand.HideSelection = false;
- treeViewLand.Location = new System.Drawing.Point(0, 0);
- treeViewLand.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- treeViewLand.Name = "treeViewLand";
- treeViewLand.Size = new System.Drawing.Size(245, 205);
- treeViewLand.TabIndex = 0;
- treeViewLand.AfterSelect += AfterSelectTreeViewLand;
+ // listViewLand
+ //
+ listViewLand.ContextMenuStrip = LandTilesContextMenuStrip;
+ listViewLand.Dock = System.Windows.Forms.DockStyle.Fill;
+ listViewLand.HideSelection = false;
+ listViewLand.Location = new System.Drawing.Point(0, 0);
+ listViewLand.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ listViewLand.Name = "listViewLand";
+ listViewLand.Size = new System.Drawing.Size(245, 205);
+ listViewLand.TabIndex = 0;
+ listViewLand.View = System.Windows.Forms.View.Details;
+ listViewLand.VirtualMode = true;
+ listViewLand.FullRowSelect = true;
+ listViewLand.MultiSelect = false;
+ listViewLand.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
+ listViewLandColumn.Text = "Land";
+ listViewLandColumn.Width = 240;
+ listViewLand.Columns.Add(listViewLandColumn);
+ listViewLand.RetrieveVirtualItem += OnRetrieveLandVirtualItem;
+ listViewLand.SelectedIndexChanged += OnLandSelectedIndexChanged;
//
// LandTilesContextMenuStrip
//
@@ -1203,8 +1222,10 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripDropDownButton toolStripDropDownButton1;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
- private System.Windows.Forms.TreeView treeViewItem;
- private System.Windows.Forms.TreeView treeViewLand;
+ private System.Windows.Forms.ListView listViewItem;
+ private System.Windows.Forms.ColumnHeader listViewItemColumn;
+ private System.Windows.Forms.ListView listViewLand;
+ private System.Windows.Forms.ColumnHeader listViewLandColumn;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
private System.Windows.Forms.ToolStripMenuItem selectInGumpsTabMaleToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem selectInGumpsTabFemaleToolStripMenuItem;
diff --git a/UoFiddler.Controls/UserControls/TileDataControl.cs b/UoFiddler.Controls/UserControls/TileDataControl.cs
index c7ace62..597b4aa 100644
--- a/UoFiddler.Controls/UserControls/TileDataControl.cs
+++ b/UoFiddler.Controls/UserControls/TileDataControl.cs
@@ -33,8 +33,6 @@ public TileDataControl()
_refMarker = this;
- treeViewItem.BeforeSelect += TreeViewItemOnBeforeSelect;
-
saveDirectlyOnChangesToolStripMenuItem.Checked = Options.TileDataDirectlySaveOnChange;
saveDirectlyOnChangesToolStripMenuItem.CheckedChanged += SaveDirectlyOnChangesToolStripMenuItemOnCheckedChanged;
@@ -96,6 +94,140 @@ private void InitItemsFlagsCheckBoxes()
private static TileDataControl _refMarker;
private bool _changingIndex;
+ // Virtual ListView backing state. _itemIndices/_landIndices map each
+ // visible row position to the real graphic id; default identity, narrowed
+ // by ApplyFilterItem/ApplyFilterLand. _modifiedItems/_modifiedLand hold
+ // graphic ids the user has edited in this session and should render in
+ // the modified color (formerly SelectedNode.ForeColor = Red).
+ private int[] _itemIndices = Array.Empty();
+ private int[] _landIndices = Array.Empty();
+ private readonly HashSet _modifiedItems = new HashSet();
+ private readonly HashSet _modifiedLand = new HashSet();
+
+ private static Color ModifiedColor => Options.DarkMode ? Color.OrangeRed : Color.Red;
+
+ private int GetSelectedItemGraphic()
+ {
+ return listViewItem.SelectedIndices.Count > 0
+ ? _itemIndices[listViewItem.SelectedIndices[0]]
+ : -1;
+ }
+
+ private int GetSelectedLandGraphic()
+ {
+ return listViewLand.SelectedIndices.Count > 0
+ ? _landIndices[listViewLand.SelectedIndices[0]]
+ : -1;
+ }
+
+ private static string FormatItemRow(int graphic, string name)
+ {
+ return string.Create(null, stackalloc char[64], $"0x{graphic:X4} ({graphic}) {name}");
+ }
+
+ private static string FormatLandRow(int graphic, string name)
+ {
+ return string.Create(null, stackalloc char[64], $"0x{graphic:X4} ({graphic}) {name}");
+ }
+
+ private void OnRetrieveItemVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
+ {
+ if ((uint)e.ItemIndex >= (uint)_itemIndices.Length)
+ {
+ e.Item = new ListViewItem(string.Empty);
+ return;
+ }
+
+ int graphic = _itemIndices[e.ItemIndex];
+ string name = TileData.ItemTable[graphic].Name ?? string.Empty;
+ var item = new ListViewItem(FormatItemRow(graphic, name)) { Tag = graphic };
+ if (_modifiedItems.Contains(graphic))
+ {
+ item.ForeColor = ModifiedColor;
+ }
+ e.Item = item;
+ }
+
+ private void OnRetrieveLandVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
+ {
+ if ((uint)e.ItemIndex >= (uint)_landIndices.Length)
+ {
+ e.Item = new ListViewItem(string.Empty);
+ return;
+ }
+
+ int graphic = _landIndices[e.ItemIndex];
+ string name = TileData.LandTable[graphic].Name ?? string.Empty;
+ var item = new ListViewItem(FormatLandRow(graphic, name)) { Tag = graphic };
+ if (_modifiedLand.Contains(graphic))
+ {
+ item.ForeColor = ModifiedColor;
+ }
+ e.Item = item;
+ }
+
+ private void RedrawItemRow(int graphic)
+ {
+ int pos = Array.IndexOf(_itemIndices, graphic);
+ if (pos >= 0)
+ {
+ listViewItem.RedrawItems(pos, pos, false);
+ }
+ }
+
+ private void RedrawLandRow(int graphic)
+ {
+ int pos = Array.IndexOf(_landIndices, graphic);
+ if (pos >= 0)
+ {
+ listViewLand.RedrawItems(pos, pos, false);
+ }
+ }
+
+ private void MarkItemModified(int graphic)
+ {
+ _modifiedItems.Add(graphic);
+ RedrawItemRow(graphic);
+ }
+
+ private void MarkLandModified(int graphic)
+ {
+ _modifiedLand.Add(graphic);
+ RedrawLandRow(graphic);
+ }
+
+ private void SelectItemRow(int rowPos)
+ {
+ listViewItem.SelectedIndices.Clear();
+ if ((uint)rowPos < (uint)_itemIndices.Length)
+ {
+ listViewItem.SelectedIndices.Add(rowPos);
+ listViewItem.EnsureVisible(rowPos);
+ listViewItem.FocusedItem = listViewItem.Items[rowPos];
+ }
+ }
+
+ private void SelectLandRow(int rowPos)
+ {
+ listViewLand.SelectedIndices.Clear();
+ if ((uint)rowPos < (uint)_landIndices.Length)
+ {
+ listViewLand.SelectedIndices.Add(rowPos);
+ listViewLand.EnsureVisible(rowPos);
+ listViewLand.FocusedItem = listViewLand.Items[rowPos];
+ }
+ }
+
+ private static int[] BuildIdentity(int length)
+ {
+ var array = new int[length];
+ for (int i = 0; i < length; ++i)
+ {
+ array[i] = i;
+ }
+ return array;
+ }
+
public bool IsLoaded { get; private set; }
private int? _reselectGraphic;
@@ -115,43 +247,60 @@ public static void Select(int graphic, bool land)
public static bool SearchGraphic(int graphic, bool land)
{
- const int index = 0;
if (land)
{
- for (int i = index; i < _refMarker.treeViewLand.Nodes.Count; ++i)
+ int pos = Array.IndexOf(_refMarker._landIndices, graphic);
+ if (pos < 0)
{
- TreeNode node = _refMarker.treeViewLand.Nodes[i];
- if (node.Tag == null || (int)node.Tag != graphic)
- {
- continue;
- }
+ // Filter may have excluded the target — reset and retry so
+ // cross-tab "Select in TileData" navigation always lands.
+ _refMarker.ResetLandView();
+ pos = Array.IndexOf(_refMarker._landIndices, graphic);
+ }
- _refMarker.tabcontrol.SelectTab(1);
- _refMarker.treeViewLand.SelectedNode = node;
- node.EnsureVisible();
- return true;
+ if (pos < 0)
+ {
+ return false;
}
+
+ _refMarker.tabcontrol.SelectTab(1);
+ _refMarker.SelectLandRow(pos);
+ return true;
}
else
{
- for (int i = index; i < _refMarker.treeViewItem.Nodes.Count; ++i)
+ int pos = Array.IndexOf(_refMarker._itemIndices, graphic);
+ if (pos < 0)
{
- for (int j = 0; j < _refMarker.treeViewItem.Nodes[i].Nodes.Count; ++j)
- {
- TreeNode node = _refMarker.treeViewItem.Nodes[i].Nodes[j];
- if (node.Tag == null || (int)node.Tag != graphic)
- {
- continue;
- }
-
- _refMarker.tabcontrol.SelectTab(0);
- _refMarker.treeViewItem.SelectedNode = node;
- node.EnsureVisible();
- return true;
- }
+ _refMarker.ResetItemView();
+ pos = Array.IndexOf(_refMarker._itemIndices, graphic);
}
+
+ if (pos < 0)
+ {
+ return false;
+ }
+
+ _refMarker.tabcontrol.SelectTab(0);
+ _refMarker.SelectItemRow(pos);
+ return true;
}
- return false;
+ }
+
+ private void ResetItemView()
+ {
+ int total = TileData.ItemTable?.Length ?? 0;
+ _itemIndices = BuildIdentity(total);
+ listViewItem.VirtualListSize = total;
+ listViewItem.Invalidate();
+ }
+
+ private void ResetLandView()
+ {
+ int total = TileData.LandTable?.Length ?? 0;
+ _landIndices = BuildIdentity(total);
+ listViewLand.VirtualListSize = total;
+ listViewLand.Invalidate();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
@@ -182,86 +331,41 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
public static bool SearchName(string name, bool next, bool land)
{
- int index = 0;
-
var searchMethod = SearchHelper.GetSearchMethod();
+ var indices = land ? _refMarker._landIndices : _refMarker._itemIndices;
+ var listView = land ? _refMarker.listViewLand : _refMarker.listViewItem;
- if (land)
+ int start = 0;
+ if (next && listView.SelectedIndices.Count > 0)
{
- if (next)
- {
- if (_refMarker.treeViewLand.SelectedNode?.Index >= 0)
- {
- index = _refMarker.treeViewLand.SelectedNode.Index + 1;
- }
-
- if (index >= _refMarker.treeViewLand.Nodes.Count)
- {
- index = 0;
- }
- }
-
- for (int i = index; i < _refMarker.treeViewLand.Nodes.Count; ++i)
+ start = listView.SelectedIndices[0] + 1;
+ if (start >= indices.Length)
{
- TreeNode node = _refMarker.treeViewLand.Nodes[i];
- if (node.Tag == null)
- {
- continue;
- }
-
- var searchResult = searchMethod(name, TileData.LandTable[(int)node.Tag].Name);
- if (!searchResult.EntryFound)
- {
- continue;
- }
-
- _refMarker.tabcontrol.SelectTab(1);
- _refMarker.treeViewLand.SelectedNode = node;
- node.EnsureVisible();
- return true;
+ start = 0;
}
}
- else
+
+ for (int i = start; i < indices.Length; ++i)
{
- int sIndex = 0;
- if (next && _refMarker.treeViewItem.SelectedNode != null)
+ int graphic = indices[i];
+ string candidate = land
+ ? TileData.LandTable[graphic].Name
+ : TileData.ItemTable[graphic].Name;
+ if (!searchMethod(name, candidate).EntryFound)
{
- if (_refMarker.treeViewItem.SelectedNode.Parent != null)
- {
- index = _refMarker.treeViewItem.SelectedNode.Parent.Index;
- sIndex = _refMarker.treeViewItem.SelectedNode.Index + 1;
- }
- else
- {
- index = _refMarker.treeViewItem.SelectedNode.Index;
- sIndex = 0;
- }
+ continue;
}
- for (int i = index; i < _refMarker.treeViewItem.Nodes.Count; ++i)
+ _refMarker.tabcontrol.SelectTab(land ? 1 : 0);
+ if (land)
{
- for (int j = sIndex; j < _refMarker.treeViewItem.Nodes[i].Nodes.Count; ++j)
- {
- TreeNode node = _refMarker.treeViewItem.Nodes[i].Nodes[j];
- if (node.Tag == null)
- {
- continue;
- }
-
- var searchResult = searchMethod(name, TileData.ItemTable[(int)node.Tag].Name);
- if (!searchResult.EntryFound)
- {
- continue;
- }
-
- _refMarker.tabcontrol.SelectTab(0);
- _refMarker.treeViewItem.SelectedNode = node;
- node.EnsureVisible();
- return true;
- }
-
- sIndex = 0;
+ _refMarker.SelectLandRow(i);
+ }
+ else
+ {
+ _refMarker.SelectItemRow(i);
}
+ return true;
}
return false;
@@ -270,84 +374,40 @@ public static bool SearchName(string name, bool next, bool land)
public static bool SearchNamePrevious(string name, bool land)
{
var searchMethod = SearchHelper.GetSearchMethod();
+ var indices = land ? _refMarker._landIndices : _refMarker._itemIndices;
+ var listView = land ? _refMarker.listViewLand : _refMarker.listViewItem;
- if (land)
+ int start = indices.Length - 1;
+ if (listView.SelectedIndices.Count > 0)
{
- int index = _refMarker.treeViewLand.Nodes.Count - 1;
- if (_refMarker.treeViewLand.SelectedNode?.Index >= 0)
+ start = listView.SelectedIndices[0] - 1;
+ if (start < 0)
{
- index = _refMarker.treeViewLand.SelectedNode.Index - 1;
- if (index < 0)
- {
- index = _refMarker.treeViewLand.Nodes.Count - 1;
- }
+ start = indices.Length - 1;
}
+ }
- for (int i = index; i >= 0; --i)
+ for (int i = start; i >= 0; --i)
+ {
+ int graphic = indices[i];
+ string candidate = land
+ ? TileData.LandTable[graphic].Name
+ : TileData.ItemTable[graphic].Name;
+ if (!searchMethod(name, candidate).EntryFound)
{
- TreeNode node = _refMarker.treeViewLand.Nodes[i];
- if (node.Tag == null)
- {
- continue;
- }
-
- var searchResult = searchMethod(name, TileData.LandTable[(int)node.Tag].Name);
- if (!searchResult.EntryFound)
- {
- continue;
- }
-
- _refMarker.tabcontrol.SelectTab(1);
- _refMarker.treeViewLand.SelectedNode = node;
- node.EnsureVisible();
- return true;
+ continue;
}
- }
- else
- {
- int parentIndex = _refMarker.treeViewItem.Nodes.Count - 1;
- int sIndex = -1;
- if (_refMarker.treeViewItem.SelectedNode != null)
+ _refMarker.tabcontrol.SelectTab(land ? 1 : 0);
+ if (land)
{
- if (_refMarker.treeViewItem.SelectedNode.Parent != null)
- {
- parentIndex = _refMarker.treeViewItem.SelectedNode.Parent.Index;
- sIndex = _refMarker.treeViewItem.SelectedNode.Index - 1;
- }
- else
- {
- parentIndex = _refMarker.treeViewItem.SelectedNode.Index;
- }
+ _refMarker.SelectLandRow(i);
}
-
- for (int i = parentIndex; i >= 0; --i)
+ else
{
- var parentNode = _refMarker.treeViewItem.Nodes[i];
- int startChild = sIndex >= 0 ? sIndex : parentNode.Nodes.Count - 1;
-
- for (int j = startChild; j >= 0; --j)
- {
- TreeNode node = parentNode.Nodes[j];
- if (node.Tag == null)
- {
- continue;
- }
-
- var searchResult = searchMethod(name, TileData.ItemTable[(int)node.Tag].Name);
- if (!searchResult.EntryFound)
- {
- continue;
- }
-
- _refMarker.tabcontrol.SelectTab(0);
- _refMarker.treeViewItem.SelectedNode = node;
- node.EnsureVisible();
- return true;
- }
-
- sIndex = -1;
+ _refMarker.SelectItemRow(i);
}
+ return true;
}
return false;
@@ -355,157 +415,109 @@ public static bool SearchNamePrevious(string name, bool land)
public void ApplyFilterItem(ItemData item)
{
- treeViewItem.BeginUpdate();
- treeViewItem.Nodes.Clear();
-
- var nodes = new List();
- var nodesSa = new List();
- var nodesHsa = new List();
-
- for (int i = 0; i < TileData.ItemTable.Length; ++i)
+ int total = TileData.ItemTable?.Length ?? 0;
+ var matches = new List(total);
+ for (int i = 0; i < total; ++i)
{
- if (!string.IsNullOrEmpty(item.Name) && TileData.ItemTable[i].Name.IndexOf(item.Name, StringComparison.OrdinalIgnoreCase) < 0)
- {
- continue;
- }
+ ref readonly ItemData row = ref TileData.ItemTable[i];
- if (item.Animation != 0 && TileData.ItemTable[i].Animation != item.Animation)
+ if (!string.IsNullOrEmpty(item.Name) && row.Name.IndexOf(item.Name, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
-
- if (item.Weight != 0 && TileData.ItemTable[i].Weight != item.Weight)
+ if (item.Animation != 0 && row.Animation != item.Animation)
{
continue;
}
-
- if (item.Quality != 0 && TileData.ItemTable[i].Quality != item.Quality)
+ if (item.Weight != 0 && row.Weight != item.Weight)
{
continue;
}
-
- if (item.Quantity != 0 && TileData.ItemTable[i].Quantity != item.Quantity)
+ if (item.Quality != 0 && row.Quality != item.Quality)
{
continue;
}
-
- if (item.Hue != 0 && TileData.ItemTable[i].Hue != item.Hue)
+ if (item.Quantity != 0 && row.Quantity != item.Quantity)
{
continue;
}
-
- if (item.StackingOffset != 0 && TileData.ItemTable[i].StackingOffset != item.StackingOffset)
+ if (item.Hue != 0 && row.Hue != item.Hue)
{
continue;
}
-
- if (item.Value != 0 && TileData.ItemTable[i].Value != item.Value)
+ if (item.StackingOffset != 0 && row.StackingOffset != item.StackingOffset)
{
continue;
}
-
- if (item.Height != 0 && TileData.ItemTable[i].Height != item.Height)
+ if (item.Value != 0 && row.Value != item.Value)
{
continue;
}
-
- if (item.MiscData != 0 && TileData.ItemTable[i].MiscData != item.MiscData)
+ if (item.Height != 0 && row.Height != item.Height)
{
continue;
}
-
- if (item.Unk2 != 0 && TileData.ItemTable[i].Unk2 != item.Unk2)
+ if (item.MiscData != 0 && row.MiscData != item.MiscData)
{
continue;
}
-
- if (item.Unk3 != 0 && TileData.ItemTable[i].Unk3 != item.Unk3)
+ if (item.Unk2 != 0 && row.Unk2 != item.Unk2)
{
continue;
}
-
- if (item.Flags != 0 && (TileData.ItemTable[i].Flags & item.Flags) == 0)
+ if (item.Unk3 != 0 && row.Unk3 != item.Unk3)
{
continue;
}
-
- TreeNode node = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", i, TileData.ItemTable[i].Name))
- {
- Tag = i
- };
-
- if (i < 0x4000)
- {
- nodes.Add(node);
- }
- else if (i < 0x8000)
+ if (item.Flags != 0 && (row.Flags & item.Flags) == 0)
{
- nodesSa.Add(node);
- }
- else
- {
- nodesHsa.Add(node);
+ continue;
}
- }
- if (nodes.Count > 0)
- {
- treeViewItem.Nodes.Add(new TreeNode("AOS - ML", nodes.ToArray()));
+ matches.Add(i);
}
- if (nodesSa.Count > 0)
- {
- treeViewItem.Nodes.Add(new TreeNode("Stygian Abyss", nodesSa.ToArray()));
- }
+ _itemIndices = matches.ToArray();
+ listViewItem.VirtualListSize = _itemIndices.Length;
+ listViewItem.Invalidate();
- if (nodesHsa.Count > 0)
+ if (_itemIndices.Length > 0)
{
- treeViewItem.Nodes.Add(new TreeNode("Adventures High Seas", nodesHsa.ToArray()));
- }
-
- treeViewItem.EndUpdate();
-
- if (treeViewItem.Nodes.Count > 0 && _refMarker.treeViewItem.Nodes[0].Nodes.Count > 0)
- {
- treeViewItem.SelectedNode = _refMarker.treeViewItem.Nodes[0].Nodes[0];
+ SelectItemRow(0);
}
}
public static void ApplyFilterLand(LandData land)
{
- _refMarker.treeViewLand.BeginUpdate();
- _refMarker.treeViewLand.Nodes.Clear();
- var nodes = new List();
- for (int i = 0; i < TileData.LandTable.Length; ++i)
+ int total = TileData.LandTable?.Length ?? 0;
+ var matches = new List(total);
+ for (int i = 0; i < total; ++i)
{
- if (!string.IsNullOrEmpty(land.Name) && TileData.ItemTable[i].Name.IndexOf(land.Name, StringComparison.OrdinalIgnoreCase) < 0)
+ ref readonly LandData row = ref TileData.LandTable[i];
+
+ if (!string.IsNullOrEmpty(land.Name) && row.Name.IndexOf(land.Name, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
-
- if (land.TextureId != 0 && TileData.LandTable[i].TextureId != land.TextureId)
+ if (land.TextureId != 0 && row.TextureId != land.TextureId)
{
continue;
}
-
- if (land.Flags != 0 && (TileData.LandTable[i].Flags & land.Flags) == 0)
+ if (land.Flags != 0 && (row.Flags & land.Flags) == 0)
{
continue;
}
- TreeNode node = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", i, TileData.LandTable[i].Name))
- {
- Tag = i
- };
- nodes.Add(node);
+ matches.Add(i);
}
- _refMarker.treeViewLand.Nodes.AddRange(nodes.ToArray());
- _refMarker.treeViewLand.EndUpdate();
+ _refMarker._landIndices = matches.ToArray();
+ _refMarker.listViewLand.VirtualListSize = _refMarker._landIndices.Length;
+ _refMarker.listViewLand.Invalidate();
- if (_refMarker.treeViewLand.Nodes.Count > 0)
+ if (_refMarker._landIndices.Length > 0)
{
- _refMarker.treeViewLand.SelectedNode = _refMarker.treeViewLand.Nodes[0];
+ _refMarker.SelectLandRow(0);
}
}
@@ -540,76 +552,18 @@ public void OnLoad(object sender, EventArgs e)
InitItemsFlagsCheckBoxes();
InitLandTilesFlagsCheckBoxes();
- Cursor.Current = Cursors.WaitCursor;
Options.LoadedUltimaClass["TileData"] = true;
Options.LoadedUltimaClass["Art"] = true;
- treeViewItem.BeginUpdate();
- treeViewItem.Nodes.Clear();
- if (TileData.ItemTable != null)
- {
- var nodes = new TreeNode[0x4000];
- for (int i = 0; i < 0x4000; ++i)
- {
- nodes[i] = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", i, TileData.ItemTable[i].Name))
- {
- Tag = i
- };
- }
- treeViewItem.Nodes.Add(new TreeNode("AOS - ML", nodes));
-
- if (TileData.ItemTable.Length > 0x4000) // SA
- {
- nodes = new TreeNode[0x4000];
- for (int i = 0; i < 0x4000; ++i)
- {
- int j = i + 0x4000;
- nodes[i] = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", j, TileData.ItemTable[j].Name))
- {
- Tag = j
- };
- }
- treeViewItem.Nodes.Add(new TreeNode("Stygian Abyss", nodes));
- }
-
- if (TileData.ItemTable.Length > 0x8000) // AHS
- {
- nodes = new TreeNode[0x8000];
- for (int i = 0; i < 0x8000; ++i)
- {
- int j = i + 0x8000;
- nodes[i] = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", j, TileData.ItemTable[j].Name))
- {
- Tag = j
- };
- }
- treeViewItem.Nodes.Add(new TreeNode("Adventures High Seas", nodes));
- }
- else
- {
- treeViewItem.ExpandAll();
- }
- }
- treeViewItem.EndUpdate();
+ // Reset modification markers on full (re)load — the data is fresh
+ // from disk, so nothing is dirty until the user edits it again.
+ _modifiedItems.Clear();
+ _modifiedLand.Clear();
- treeViewLand.BeginUpdate();
- treeViewLand.Nodes.Clear();
- if (TileData.LandTable != null)
- {
- var nodes = new TreeNode[TileData.LandTable.Length];
- for (int i = 0; i < TileData.LandTable.Length; ++i)
- {
- nodes[i] = new TreeNode(string.Format("0x{0:X4} ({0}) {1}", i, TileData.LandTable[i].Name))
- {
- Tag = i
- };
- }
- treeViewLand.Nodes.AddRange(nodes);
- }
- treeViewLand.EndUpdate();
+ ResetItemView();
+ ResetLandView();
IsLoaded = true;
- Cursor.Current = Cursors.Default;
}
private void OnFilePathChangeEvent()
@@ -633,14 +587,16 @@ private void OnPreviewBackgroundColorChanged()
pictureBoxItem.BackColor = Options.PreviewBackgroundColor;
pictureBoxLand.BackColor = Options.PreviewBackgroundColor;
- if (treeViewItem.SelectedNode != null)
+ int itemGraphic = GetSelectedItemGraphic();
+ if (itemGraphic >= 0)
{
- AfterSelectTreeViewItem(this, new TreeViewEventArgs(treeViewItem.SelectedNode));
+ UpdateSelectedItemPreview(itemGraphic);
}
- if (treeViewLand.SelectedNode != null)
+ int landGraphic = GetSelectedLandGraphic();
+ if (landGraphic >= 0)
{
- AfterSelectTreeViewLand(this, new TreeViewEventArgs(treeViewLand.SelectedNode));
+ UpdateSelectedLandPreview(landGraphic);
}
}
@@ -658,70 +614,47 @@ private void OnTileDataChangeEvent(object sender, int index)
if (index > 0x3FFF) // items
{
- if (treeViewItem.SelectedNode == null)
+ int graphic = index - 0x4000;
+ MarkItemModified(graphic);
+ if (GetSelectedItemGraphic() == graphic)
{
- return;
- }
-
- if ((int)treeViewItem.SelectedNode.Tag == index)
- {
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
- AfterSelectTreeViewItem(this, new TreeViewEventArgs(treeViewItem.SelectedNode));
- }
- else
- {
- foreach (TreeNode parentNode in treeViewItem.Nodes)
- {
- foreach (TreeNode node in parentNode.Nodes)
- {
- if ((int)node.Tag != index)
- {
- continue;
- }
-
- node.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
- break;
- }
- }
+ UpdateSelectedItemPreview(graphic);
}
}
else
{
- if (treeViewLand.SelectedNode == null)
+ MarkLandModified(index);
+ if (GetSelectedLandGraphic() == index)
{
- return;
+ UpdateSelectedLandPreview(index);
}
+ }
+ }
- if ((int)treeViewLand.SelectedNode.Tag == index)
- {
- treeViewLand.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
- AfterSelectTreeViewLand(this, new TreeViewEventArgs(treeViewLand.SelectedNode));
- }
- else
- {
- foreach (TreeNode node in treeViewLand.Nodes)
- {
- if ((int)node.Tag != index)
- {
- continue;
- }
-
- node.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
- break;
- }
- }
+ private void OnItemSelectedIndexChanged(object sender, EventArgs e)
+ {
+ int graphic = GetSelectedItemGraphic();
+ if (graphic < 0)
+ {
+ return;
}
+
+ UpdateSelectedItemPreview(graphic);
}
- private void AfterSelectTreeViewItem(object sender, TreeViewEventArgs e)
+ private void OnLandSelectedIndexChanged(object sender, EventArgs e)
{
- if (e.Node?.Tag == null)
+ int graphic = GetSelectedLandGraphic();
+ if (graphic < 0)
{
return;
}
- int index = (int)e.Node.Tag;
+ UpdateSelectedLandPreview(graphic);
+ }
+ private void UpdateSelectedItemPreview(int index)
+ {
Bitmap bit = Art.GetStatic(index);
if (bit != null)
{
@@ -764,15 +697,8 @@ private void AfterSelectTreeViewItem(object sender, TreeViewEventArgs e)
_changingIndex = false;
}
- private void AfterSelectTreeViewLand(object sender, TreeViewEventArgs e)
+ private void UpdateSelectedLandPreview(int index)
{
- if (e.Node == null)
- {
- return;
- }
-
- int index = (int)e.Node.Tag;
-
Bitmap bit = Art.GetLand(index);
if (bit != null)
{
@@ -818,12 +744,12 @@ private void OnClickSaveChanges(object sender, EventArgs e)
{
if (tabcontrol.SelectedIndex == 0) // items
{
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
string name = textBoxName.Text;
if (name.Length > 20)
@@ -832,7 +758,6 @@ private void OnClickSaveChanges(object sender, EventArgs e)
}
item.Name = name;
- treeViewItem.SelectedNode.Text = string.Format("0x{0:X4} ({0}) {1}", index, name);
if (short.TryParse(textBoxAnim.Text, out short shortRes))
{
item.Animation = shortRes;
@@ -899,7 +824,7 @@ private void OnClickSaveChanges(object sender, EventArgs e)
}
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
if (memorySaveWarningToolStripMenuItem.Checked)
@@ -912,12 +837,12 @@ private void OnClickSaveChanges(object sender, EventArgs e)
}
else // land
{
- if (treeViewLand.SelectedNode == null)
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
LandData land = TileData.LandTable[index];
string name = textBoxNameLand.Text;
if (name.Length > 20)
@@ -926,7 +851,6 @@ private void OnClickSaveChanges(object sender, EventArgs e)
}
land.Name = name;
- treeViewLand.SelectedNode.Text = $"0x{index:X4} {name}";
if (ushort.TryParse(textBoxTexID.Text, out ushort shortRes))
{
land.TextureId = shortRes;
@@ -945,7 +869,7 @@ private void OnClickSaveChanges(object sender, EventArgs e)
TileData.LandTable[index] = land;
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index);
- treeViewLand.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkLandModified(index);
if (memorySaveWarningToolStripMenuItem.Checked)
{
MessageBox.Show(
@@ -973,7 +897,8 @@ private void OnTextChangedItemAnim(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -983,11 +908,10 @@ private void OnTextChangedItemAnim(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Animation = shortRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1004,12 +928,12 @@ private void OnTextChangedItemName(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
string name = textBoxName.Text;
if (name.Length == 0)
@@ -1025,33 +949,11 @@ private void OnTextChangedItemName(object sender, EventArgs e)
item.Name = name;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
- private void TreeViewItemOnBeforeSelect(object sender, TreeViewCancelEventArgs treeViewCancelEventArgs)
- {
- if (!saveDirectlyOnChangesToolStripMenuItem.Checked)
- {
- return;
- }
-
- if (treeViewItem.SelectedNode?.Tag == null)
- {
- return;
- }
-
- int index = (int)treeViewItem.SelectedNode.Tag;
- ItemData item = TileData.ItemTable[index];
-
- string itemText = string.Format("0x{0:X4} ({0}) {1}", index, item.Name);
- if (treeViewItem.SelectedNode.Text != itemText)
- {
- treeViewItem.SelectedNode.Text = string.Format("0x{0:X4} ({0}) {1}", index, item.Name);
- }
- }
-
private void OnTextChangedItemWeight(object sender, EventArgs e)
{
if (!saveDirectlyOnChangesToolStripMenuItem.Checked)
@@ -1064,7 +966,8 @@ private void OnTextChangedItemWeight(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1074,11 +977,10 @@ private void OnTextChangedItemWeight(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Weight = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1095,7 +997,8 @@ private void OnTextChangedItemQuality(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1105,11 +1008,10 @@ private void OnTextChangedItemQuality(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Quality = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1126,7 +1028,8 @@ private void OnTextChangedItemQuantity(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1136,11 +1039,10 @@ private void OnTextChangedItemQuantity(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Quantity = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1157,7 +1059,8 @@ private void OnTextChangedItemHue(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1167,11 +1070,10 @@ private void OnTextChangedItemHue(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Hue = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1188,7 +1090,8 @@ private void OnTextChangedItemStackOff(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1198,11 +1101,10 @@ private void OnTextChangedItemStackOff(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.StackingOffset = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1219,7 +1121,8 @@ private void OnTextChangedItemValue(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1229,11 +1132,10 @@ private void OnTextChangedItemValue(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Value = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1250,7 +1152,8 @@ private void OnTextChangedItemHeight(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1260,11 +1163,10 @@ private void OnTextChangedItemHeight(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Height = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1281,7 +1183,8 @@ private void OnTextChangedItemMiscData(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1291,11 +1194,10 @@ private void OnTextChangedItemMiscData(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.MiscData = shortRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1312,7 +1214,8 @@ private void OnTextChangedItemUnk2(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1322,11 +1225,10 @@ private void OnTextChangedItemUnk2(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Unk2 = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1343,7 +1245,8 @@ private void OnTextChangedItemUnk3(object sender, EventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
@@ -1353,11 +1256,10 @@ private void OnTextChangedItemUnk3(object sender, EventArgs e)
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
item.Unk3 = byteRes;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1374,12 +1276,12 @@ private void OnTextChangedLandName(object sender, EventArgs e)
return;
}
- if (treeViewLand.SelectedNode?.Tag == null)
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
LandData land = TileData.LandTable[index];
string name = textBoxNameLand.Text;
if (name.Length == 0)
@@ -1393,9 +1295,8 @@ private void OnTextChangedLandName(object sender, EventArgs e)
}
land.Name = name;
- treeViewLand.SelectedNode.Text = string.Format("0x{0:X4} ({0}) {1}", index, name);
TileData.LandTable[index] = land;
- treeViewLand.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkLandModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index);
}
@@ -1412,7 +1313,8 @@ private void OnTextChangedLandTexID(object sender, EventArgs e)
return;
}
- if (treeViewLand.SelectedNode == null)
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
{
return;
}
@@ -1422,11 +1324,10 @@ private void OnTextChangedLandTexID(object sender, EventArgs e)
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
LandData land = TileData.LandTable[index];
land.TextureId = shortRes;
TileData.LandTable[index] = land;
- treeViewLand.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkLandModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index);
}
@@ -1448,12 +1349,12 @@ private void OnFlagItemCheckItems(object sender, ItemCheckEventArgs e)
return;
}
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
ItemData item = TileData.ItemTable[index];
Array enumValues = Enum.GetValues(typeof(TileFlag));
@@ -1468,7 +1369,7 @@ private void OnFlagItemCheckItems(object sender, ItemCheckEventArgs e)
item.Flags ^= changeFlag;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1481,7 +1382,7 @@ private void OnFlagItemCheckItems(object sender, ItemCheckEventArgs e)
item.Flags |= changeFlag;
TileData.ItemTable[index] = item;
- treeViewItem.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkItemModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index + 0x4000);
}
@@ -1504,12 +1405,12 @@ private void OnFlagItemCheckLandTiles(object sender, ItemCheckEventArgs e)
return;
}
- if (treeViewLand.SelectedNode == null)
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
LandData land = TileData.LandTable[index];
TileFlag changeFlag;
switch (e.Index)
@@ -1548,7 +1449,7 @@ private void OnFlagItemCheckLandTiles(object sender, ItemCheckEventArgs e)
land.Flags ^= changeFlag;
TileData.LandTable[index] = land;
- treeViewLand.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkLandModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index);
}
@@ -1561,7 +1462,7 @@ private void OnFlagItemCheckLandTiles(object sender, ItemCheckEventArgs e)
land.Flags |= changeFlag;
TileData.LandTable[index] = land;
- treeViewLand.SelectedNode.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
+ MarkLandModified(index);
Options.ChangedUltimaClass["TileData"] = true;
ControlEvents.FireTileDataChangeEvent(this, index);
}
@@ -1587,12 +1488,12 @@ private void OnClickExport(object sender, EventArgs e)
}
private void OnClickSelectItem(object sender, EventArgs e)
{
- if (treeViewItem.SelectedNode?.Tag == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
var found = ItemsControl.SearchGraphic(index);
if (!found)
{
@@ -1602,12 +1503,12 @@ private void OnClickSelectItem(object sender, EventArgs e)
private void OnClickSelectInLandTiles(object sender, EventArgs e)
{
- if (treeViewLand.SelectedNode == null)
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
var found = LandTilesControl.SearchGraphic(index);
if (!found)
{
@@ -1617,23 +1518,23 @@ private void OnClickSelectInLandTiles(object sender, EventArgs e)
private void OnClickSelectRadarItem(object sender, EventArgs e)
{
- if (treeViewItem.SelectedNode == null)
+ int index = GetSelectedItemGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewItem.SelectedNode.Tag;
RadarColorControl.Select(index, false);
}
private void OnClickSelectRadarLand(object sender, EventArgs e)
{
- if (treeViewLand.SelectedNode == null)
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
{
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
RadarColorControl.Select(index, true);
}
@@ -1682,15 +1583,6 @@ private void OnClickSetFilter(object sender, EventArgs e)
_filterFormForm.Show();
}
- private void OnItemDataNodeExpanded(object sender, TreeViewCancelEventArgs e)
- {
- // workaround for 65536 items microsoft bug
- if (treeViewItem.Nodes.Count == 3)
- {
- treeViewItem.CollapseAll();
- }
- }
-
private const int _maleGumpOffset = 50_000;
private const int _femaleGumpOffset = 60_000;
@@ -1704,30 +1596,30 @@ private static void SelectInGumpsTab(int tiledataIndex, bool female = false)
private void SelectInGumpsTabMaleToolStripMenuItem_Click(object sender, EventArgs e)
{
- var selectedItemTag = treeViewItem.SelectedNode?.Tag;
- if (selectedItemTag is null || (int)selectedItemTag <= 0)
+ int graphic = GetSelectedItemGraphic();
+ if (graphic <= 0)
{
return;
}
- SelectInGumpsTab((int)selectedItemTag);
+ SelectInGumpsTab(graphic);
}
private void SelectInGumpsTabFemaleToolStripMenuItem_Click(object sender, EventArgs e)
{
- var selectedItemTag = treeViewItem.SelectedNode?.Tag;
- if (selectedItemTag is null || (int)selectedItemTag <= 0)
+ int graphic = GetSelectedItemGraphic();
+ if (graphic <= 0)
{
return;
}
- SelectInGumpsTab((int)selectedItemTag, true);
+ SelectInGumpsTab(graphic, true);
}
private void ItemsContextMenuStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e)
{
- var selectedItemTag = treeViewItem.SelectedNode?.Tag;
- if (selectedItemTag is null || (int)selectedItemTag <= 0)
+ int graphic = GetSelectedItemGraphic();
+ if (graphic <= 0)
{
selectInGumpsTabMaleToolStripMenuItem.Enabled = false;
selectInGumpsTabFemaleToolStripMenuItem.Enabled = false;
@@ -1735,7 +1627,7 @@ private void ItemsContextMenuStrip_Opening(object sender, System.ComponentModel.
}
else
{
- var itemData = TileData.ItemTable[(int)selectedItemTag];
+ var itemData = TileData.ItemTable[graphic];
if (itemData.Animation > 0)
{
@@ -1752,19 +1644,19 @@ private void ItemsContextMenuStrip_Opening(object sender, System.ComponentModel.
}
selectInAnimDataTabToolStripMenuItem.Enabled =
- Animdata.GetAnimData((int)selectedItemTag) != null;
+ Animdata.GetAnimData(graphic) != null;
}
}
private void SelectInAnimDataTabToolStripMenuItem_Click(object sender, EventArgs e)
{
- var selectedItemTag = treeViewItem.SelectedNode?.Tag;
- if (selectedItemTag is null || (int)selectedItemTag <= 0)
+ int graphic = GetSelectedItemGraphic();
+ if (graphic <= 0)
{
return;
}
- AnimDataControl.Select((int)selectedItemTag);
+ AnimDataControl.Select(graphic);
}
///
@@ -1780,7 +1672,12 @@ private void TextBoxTexID_DoubleClick(object sender, EventArgs e)
return;
}
- int index = (int)treeViewLand.SelectedNode.Tag;
+ int index = GetSelectedLandGraphic();
+ if (index < 0)
+ {
+ return;
+ }
+
if (!int.TryParse(textBoxTexID.Text, out int texIdValue) || texIdValue == index)
{
return;
@@ -1818,12 +1715,7 @@ private void SetTextureMenuItem_Click(object sender, EventArgs e)
TileData.LandTable[i].TextureId = (ushort)i;
- var node = treeViewLand.Nodes.OfType().FirstOrDefault(x => x.Tag.Equals(i));
- if (node != null)
- {
- node.ForeColor = (Options.DarkMode ? Color.OrangeRed : Color.Red);
- }
-
+ MarkLandModified(i);
updated++;
Options.ChangedUltimaClass["TileData"] = true;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
index 61f6fed..1d6c8f2 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Windows.Forms;
using Ultima;
+using Ultima.Helpers;
using UoFiddler.Controls.Classes;
using UoFiddler.Controls.UserControls.TileView;
using UoFiddler.Plugin.Compare.Classes;
@@ -222,20 +223,27 @@ private void OnTileViewSizeChanged(object sender, EventArgs e)
}
private void OnDrawItemLandOrg(object sender, TileViewControl.DrawTileListItemEventArgs e)
- => DrawListItem(e, _landDisplayIndices[e.Index]);
+ => DrawListItem(e, _landDisplayIndices[e.Index], isSec: false);
private void OnDrawItemLandSec(object sender, TileViewControl.DrawTileListItemEventArgs e)
- => DrawListItem(e, _landDisplayIndices[e.Index]);
+ => DrawListItem(e, _landDisplayIndices[e.Index], isSec: true);
private void OnDrawItemItemOrg(object sender, TileViewControl.DrawTileListItemEventArgs e)
- => DrawListItem(e, _itemDisplayIndices[e.Index]);
+ => DrawListItem(e, _itemDisplayIndices[e.Index], isSec: false);
private void OnDrawItemItemSec(object sender, TileViewControl.DrawTileListItemEventArgs e)
- => DrawListItem(e, _itemDisplayIndices[e.Index]);
+ => DrawListItem(e, _itemDisplayIndices[e.Index], isSec: true);
- private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int idx)
+ // Layout (left → right): [checkbox column from TileViewControl, if any] | [color swatch] | [text].
+ // Mirrors RadarColorControl so the eye doesn't have to retrain when
+ // switching between the two tabs.
+ private const int SwatchSize = 12;
+ private const int SwatchGap = 4;
+
+ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int idx, bool isSec)
{
- if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ if (focused)
{
e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
}
@@ -244,14 +252,66 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int idx)
e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
}
+ // Color swatch.
+ ushort radarHue = GetRadarColor(idx, isSec);
+ int swatchX = e.ContentLeft + 2;
+ int swatchY = e.Bounds.Y + (e.Bounds.Height - SwatchSize) / 2;
+ var swatchRect = new Rectangle(swatchX, swatchY, SwatchSize, SwatchSize);
+ using (var swatchBrush = new SolidBrush(HueHelpers.HueToColor(radarHue)))
+ {
+ e.Graphics.FillRectangle(swatchBrush, swatchRect);
+ }
+ using (var border = new Pen(SystemColors.ControlDark))
+ {
+ e.Graphics.DrawRectangle(border, swatchRect);
+ }
+
+ // Text — display id is *within* the section (0x0000-based for both
+ // items and land), matching RadarColorControl's labelling.
+ int displayId = idx < 0x4000 ? idx : idx - 0x4000;
+ string name = GetTileName(idx);
+ string text = string.IsNullOrEmpty(name)
+ ? $"0x{displayId:X4} ({displayId})"
+ : $"0x{displayId:X4} ({displayId}) {name}";
+
Brush fontBrush = SecondRadarCol.IsLoaded && IsDifferent(idx)
? (Options.DarkMode ? Brushes.CornflowerBlue : Brushes.Blue)
: Brushes.Gray;
- string section = idx < 0x4000 ? "Land" : "Item";
- string text = $"0x{idx:X4} [{section}]";
- float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(text, e.Font).Height) / 2f;
- e.Graphics.DrawString(text, e.Font, fontBrush, new PointF(e.ContentLeft + 4, y));
+ int textX = swatchX + SwatchSize + SwatchGap;
+ float textY = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(text, e.Font).Height) / 2f;
+ e.Graphics.DrawString(text, e.Font, fontBrush, new PointF(textX, textY));
+ }
+
+ private static ushort GetRadarColor(int idx, bool isSec)
+ {
+ if (isSec)
+ {
+ return SecondRadarCol.IsLoaded ? SecondRadarCol.GetColor(idx) : (ushort)0;
+ }
+ return RadarCol.Colors != null && idx < RadarCol.Colors.Length
+ ? RadarCol.Colors[idx]
+ : (ushort)0;
+ }
+
+ private static string GetTileName(int idx)
+ {
+ if (idx < 0x4000)
+ {
+ if (TileData.LandTable != null && idx < TileData.LandTable.Length)
+ {
+ return TileData.LandTable[idx].Name;
+ }
+ }
+ else
+ {
+ int itemId = idx - 0x4000;
+ if (TileData.ItemTable != null && itemId < TileData.ItemTable.Length)
+ {
+ return TileData.ItemTable[itemId].Name;
+ }
+ }
+ return null;
}
private void OnFocusChangedLandOrg(object sender, TileViewControl.ListViewFocusedItemSelectionChangedEventArgs e)
@@ -367,12 +427,12 @@ private void UpdateDetailPanel(int idx)
ushort secColor = SecondRadarCol.IsLoaded ? SecondRadarCol.GetColor(idx) : (ushort)0;
labelOrgColorValue.Text = $"0x{orgColor:X4} ({orgColor})";
- pictureBoxOrgColor.BackColor = UshortToColor(orgColor);
+ pictureBoxOrgColor.BackColor = HueHelpers.HueToColor(orgColor);
if (SecondRadarCol.IsLoaded)
{
labelSecColorValue.Text = $"0x{secColor:X4} ({secColor})";
- pictureBoxSecColor.BackColor = UshortToColor(secColor);
+ pictureBoxSecColor.BackColor = HueHelpers.HueToColor(secColor);
}
else
{
@@ -381,19 +441,6 @@ private void UpdateDetailPanel(int idx)
}
}
- private static Color UshortToColor(ushort value)
- {
- if (value == 0)
- {
- return Color.Black;
- }
-
- int b = (value & 0x7C00) >> 10;
- int g = (value & 0x03E0) >> 5;
- int r = value & 0x001F;
- return Color.FromArgb((r << 3) | (r >> 2), (g << 3) | (g >> 2), (b << 3) | (b >> 2));
- }
-
private void OnClickBrowse(object sender, EventArgs e)
{
using (OpenFileDialog dialog = new OpenFileDialog())
From bd8eb32704e819e49ad4f5c144b93540c224c5ed Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:16:40 +0200
Subject: [PATCH 07/21] Publish artifacts after PR build.
---
.github/workflows/build-pr.yml | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
index 9a60cf5..30a0f9d 100644
--- a/.github/workflows/build-pr.yml
+++ b/.github/workflows/build-pr.yml
@@ -39,4 +39,11 @@ jobs:
- name: Restore & build the application
run: dotnet build $env:Solution_Name --configuration $env:Configuration
env:
- Configuration: ${{ matrix.configuration }}
\ No newline at end of file
+ Configuration: ${{ matrix.configuration }}
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v7.0.0
+ with:
+ name: UOFiddler-PR${{ github.event.pull_request.number }}-${{ github.sha }}
+ path: ./UoFiddler/bin/Release/
+ retention-days: 7
\ No newline at end of file
From 9db8fe0d958abad444d591d26a14afbd8baefa21 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:45:16 +0200
Subject: [PATCH 08/21] Improve "Select in..." option to properly switch to tab
when used.
---
.../Helpers/TabPageNavigator.cs | 39 +++++++++++++++++++
.../UserControls/AnimDataControl.cs | 2 +
.../UserControls/GumpControl.cs | 7 ++++
.../UserControls/ItemsControl.cs | 24 ++++++++++--
.../UserControls/LandTilesControl.cs | 24 ++++++++++--
.../UserControls/RadarColorControl.cs | 19 +++++++++
.../UserControls/TexturesControl.cs | 25 ++++++++++--
.../UserControls/TileDataControl.cs | 32 +++++++--------
8 files changed, 147 insertions(+), 25 deletions(-)
create mode 100644 UoFiddler.Controls/Helpers/TabPageNavigator.cs
diff --git a/UoFiddler.Controls/Helpers/TabPageNavigator.cs b/UoFiddler.Controls/Helpers/TabPageNavigator.cs
new file mode 100644
index 0000000..6780666
--- /dev/null
+++ b/UoFiddler.Controls/Helpers/TabPageNavigator.cs
@@ -0,0 +1,39 @@
+/***************************************************************************
+ *
+ * "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.Windows.Forms;
+
+namespace UoFiddler.Controls.Helpers
+{
+ public static class TabPageNavigator
+ {
+ ///
+ /// Walks the control's parent chain and, if it sits inside a TabControl,
+ /// activates the owning TabPage. No-op when the control is not hosted in
+ /// a TabPage (e.g., undocked into a standalone form, or used outside the
+ /// main TabPanel).
+ ///
+ public static void ActivateOwningTabPage(Control control)
+ {
+ Control current = control;
+ while (current != null)
+ {
+ if (current.Parent is TabControl outerTabControl && current is TabPage outerTabPage)
+ {
+ if (outerTabControl.SelectedTab != outerTabPage)
+ {
+ outerTabControl.SelectedTab = outerTabPage;
+ }
+ return;
+ }
+ current = current.Parent;
+ }
+ }
+ }
+}
diff --git a/UoFiddler.Controls/UserControls/AnimDataControl.cs b/UoFiddler.Controls/UserControls/AnimDataControl.cs
index 931da89..9fe59e5 100644
--- a/UoFiddler.Controls/UserControls/AnimDataControl.cs
+++ b/UoFiddler.Controls/UserControls/AnimDataControl.cs
@@ -732,6 +732,8 @@ public static bool Select(int graphic)
_refMarker.OnLoad(_refMarker, EventArgs.Empty);
}
+ TabPageNavigator.ActivateOwningTabPage(_refMarker);
+
foreach (TreeNode node in _refMarker.treeView1.Nodes)
{
if ((int)node.Tag != graphic)
diff --git a/UoFiddler.Controls/UserControls/GumpControl.cs b/UoFiddler.Controls/UserControls/GumpControl.cs
index b7aa359..8875ee3 100644
--- a/UoFiddler.Controls/UserControls/GumpControl.cs
+++ b/UoFiddler.Controls/UserControls/GumpControl.cs
@@ -853,11 +853,18 @@ private void PreLoaderCompleted(object sender, RunWorkerCompletedEventArgs e)
internal static void Select(int gumpId)
{
+ if (_refMarker == null)
+ {
+ return;
+ }
+
if (!_refMarker._loaded)
{
_refMarker.OnLoad(EventArgs.Empty);
}
+ TabPageNavigator.ActivateOwningTabPage(_refMarker);
+
Search(gumpId);
}
diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs
index 86ddbd6..dd3d3a9 100644
--- a/UoFiddler.Controls/UserControls/ItemsControl.cs
+++ b/UoFiddler.Controls/UserControls/ItemsControl.cs
@@ -105,6 +105,11 @@ public void UpdateTileView()
///
public static bool SearchGraphic(int graphic)
{
+ if (RefMarker == null)
+ {
+ return false;
+ }
+
if (!RefMarker.IsLoaded)
{
RefMarker.OnLoad(RefMarker, EventArgs.Empty);
@@ -115,9 +120,22 @@ public static bool SearchGraphic(int graphic)
return false;
}
- // we have to invalidate focus so it will scroll to item
- RefMarker.ItemsTileView.FocusIndex = -1;
- RefMarker.SelectedGraphicId = graphic;
+ TabPageNavigator.ActivateOwningTabPage(RefMarker);
+
+ if (RefMarker.IsHandleCreated)
+ {
+ RefMarker.BeginInvoke(new Action(() =>
+ {
+ // we have to invalidate focus so it will scroll to item
+ RefMarker.ItemsTileView.FocusIndex = -1;
+ RefMarker.SelectedGraphicId = graphic;
+ }));
+ }
+ else
+ {
+ RefMarker.ItemsTileView.FocusIndex = -1;
+ RefMarker.SelectedGraphicId = graphic;
+ }
return true;
}
diff --git a/UoFiddler.Controls/UserControls/LandTilesControl.cs b/UoFiddler.Controls/UserControls/LandTilesControl.cs
index fbdc694..652f4e0 100644
--- a/UoFiddler.Controls/UserControls/LandTilesControl.cs
+++ b/UoFiddler.Controls/UserControls/LandTilesControl.cs
@@ -66,6 +66,11 @@ public int SelectedGraphicId
///
public static bool SearchGraphic(int graphic)
{
+ if (_refMarker == null)
+ {
+ return false;
+ }
+
if (!_refMarker.IsLoaded)
{
_refMarker.OnLoad(_refMarker, EventArgs.Empty);
@@ -76,9 +81,22 @@ public static bool SearchGraphic(int graphic)
return false;
}
- // we have to invalidate focus so it will scroll to item
- _refMarker.LandTilesTileView.FocusIndex = -1;
- _refMarker.SelectedGraphicId = graphic;
+ TabPageNavigator.ActivateOwningTabPage(_refMarker);
+
+ if (_refMarker.IsHandleCreated)
+ {
+ _refMarker.BeginInvoke(new Action(() =>
+ {
+ // we have to invalidate focus so it will scroll to item
+ _refMarker.LandTilesTileView.FocusIndex = -1;
+ _refMarker.SelectedGraphicId = graphic;
+ }));
+ }
+ else
+ {
+ _refMarker.LandTilesTileView.FocusIndex = -1;
+ _refMarker.SelectedGraphicId = graphic;
+ }
return true;
}
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.cs b/UoFiddler.Controls/UserControls/RadarColorControl.cs
index 7bb9dfa..4431b8d 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.cs
@@ -385,11 +385,30 @@ public ushort CurrentColor
public static void Select(int graphic, bool land)
{
+ if (_refMarker == null)
+ {
+ return;
+ }
+
if (!_refMarker.IsLoaded)
{
_refMarker.OnLoad(_refMarker, EventArgs.Empty);
}
+ TabPageNavigator.ActivateOwningTabPage(_refMarker);
+
+ if (_refMarker.IsHandleCreated)
+ {
+ _refMarker.BeginInvoke(new Action(() => ApplySelect(graphic, land)));
+ }
+ else
+ {
+ ApplySelect(graphic, land);
+ }
+ }
+
+ private static void ApplySelect(int graphic, bool land)
+ {
if (land)
{
int pos = Array.IndexOf(_refMarker._landIndices, graphic);
diff --git a/UoFiddler.Controls/UserControls/TexturesControl.cs b/UoFiddler.Controls/UserControls/TexturesControl.cs
index 0e139f5..c7d8536 100644
--- a/UoFiddler.Controls/UserControls/TexturesControl.cs
+++ b/UoFiddler.Controls/UserControls/TexturesControl.cs
@@ -54,6 +54,11 @@ public int SelectedTextureId
public static bool Select(int textureId)
{
+ if (_refMarker == null)
+ {
+ return false;
+ }
+
if (!_refMarker._loaded)
{
_refMarker.OnLoad(_refMarker, EventArgs.Empty);
@@ -64,9 +69,23 @@ public static bool Select(int textureId)
return false;
}
- // Reset focus index to ensure the view scrolls to the selected texture
- _refMarker.TextureTileView.FocusIndex = -1;
- _refMarker.SelectedTextureId = textureId;
+ TabPageNavigator.ActivateOwningTabPage(_refMarker);
+
+ if (_refMarker.IsHandleCreated)
+ {
+ _refMarker.BeginInvoke(new Action(() =>
+ {
+ // Reset focus index to ensure the view scrolls to the selected texture
+ _refMarker.TextureTileView.FocusIndex = -1;
+ _refMarker.SelectedTextureId = textureId;
+ }));
+ }
+ else
+ {
+ _refMarker.TextureTileView.FocusIndex = -1;
+ _refMarker.SelectedTextureId = textureId;
+ }
+
return true;
}
diff --git a/UoFiddler.Controls/UserControls/TileDataControl.cs b/UoFiddler.Controls/UserControls/TileDataControl.cs
index 597b4aa..1142278 100644
--- a/UoFiddler.Controls/UserControls/TileDataControl.cs
+++ b/UoFiddler.Controls/UserControls/TileDataControl.cs
@@ -230,19 +230,27 @@ private static int[] BuildIdentity(int length)
public bool IsLoaded { get; private set; }
- private int? _reselectGraphic;
- private bool? _reselectGraphicLand;
-
public static void Select(int graphic, bool land)
{
- if (!_refMarker.IsLoaded)
+ if (_refMarker == null)
{
- _refMarker.OnLoad(_refMarker, EventArgs.Empty);
- _refMarker._reselectGraphic = graphic;
- _refMarker._reselectGraphicLand = land;
+ return;
}
- SearchGraphic(graphic, land);
+ // Activate the outer TileData TabPage so the virtual ListView is on
+ // a visible tab before we set selection — assigning SelectedIndices
+ // on a VirtualMode ListView whose parent TabPage hasn't been shown
+ // does not stick across the later tab activation.
+ TabPageNavigator.ActivateOwningTabPage(_refMarker);
+
+ if (_refMarker.IsHandleCreated)
+ {
+ _refMarker.BeginInvoke(new Action(() => SearchGraphic(graphic, land)));
+ }
+ else
+ {
+ SearchGraphic(graphic, land);
+ }
}
public static bool SearchGraphic(int graphic, bool land)
@@ -536,14 +544,6 @@ public void OnLoad(object sender, EventArgs e)
return;
}
- if (_reselectGraphic != null && _reselectGraphicLand != null)
- {
- SearchGraphic(_reselectGraphic.Value, _reselectGraphicLand.Value);
-
- _reselectGraphic = null;
- _reselectGraphicLand = null;
- }
-
if (IsLoaded && (!(e is MyEventArgs args) || args.Type != MyEventArgs.Types.ForceReload))
{
return;
From 421bb467f5c8728cc634e6438c07c8b0bc613a0f Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 21:59:45 +0200
Subject: [PATCH 09/21] Fix some colors when in dark mode.
---
UoFiddler.Controls/Forms/HueEditForm.cs | 15 +++++++++------
UoFiddler.Controls/UserControls/GumpControl.cs | 5 +++--
UoFiddler.Controls/UserControls/HuesControl.cs | 4 +++-
UoFiddler.Controls/UserControls/ItemsControl.cs | 5 +++--
.../UserControls/LandTilesControl.cs | 5 +++--
UoFiddler.Controls/UserControls/LightControl.cs | 15 +++++++++------
UoFiddler.Controls/UserControls/MapControl.cs | 4 +++-
.../UserControls/RadarColorControl.cs | 2 +-
.../UserControls/SkillGroupControl.cs | 2 +-
UoFiddler.Controls/UserControls/SoundsControl.cs | 6 +++---
.../UserControls/TexturesControl.cs | 5 +++--
11 files changed, 41 insertions(+), 27 deletions(-)
diff --git a/UoFiddler.Controls/Forms/HueEditForm.cs b/UoFiddler.Controls/Forms/HueEditForm.cs
index 131c015..bd60432 100644
--- a/UoFiddler.Controls/Forms/HueEditForm.cs
+++ b/UoFiddler.Controls/Forms/HueEditForm.cs
@@ -296,9 +296,10 @@ private void OnClickHueOnlyGrey(object sender, EventArgs e)
private void OnTextChangedArt(object sender, EventArgs e)
{
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
TextBoxArt.ForeColor = Utils.ConvertStringToInt(TextBoxArt.Text, out int index, 0, Art.GetMaxItemId())
- ? Art.IsValidStatic(index) ? Color.Black : Color.Red
- : Color.Red;
+ ? Art.IsValidStatic(index) ? SystemColors.ControlText : invalidColor
+ : invalidColor;
}
private void OnKeyDownArt(object sender, KeyEventArgs e)
@@ -327,9 +328,10 @@ private void OnKeyDownArt(object sender, KeyEventArgs e)
private void OnTextChangedAnim(object sender, EventArgs e)
{
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
TextBoxAnim.ForeColor = Utils.ConvertStringToInt(TextBoxAnim.Text, out int index, 1, 10000)
- ? Animations.IsActionDefined(index, 0, 0) ? Color.Black : Color.Red
- : Color.Red;
+ ? Animations.IsActionDefined(index, 0, 0) ? SystemColors.ControlText : invalidColor
+ : invalidColor;
}
private void OnKeyDownAnim(object sender, KeyEventArgs e)
@@ -367,13 +369,14 @@ private void OnKeyDownAnim(object sender, KeyEventArgs e)
private void OnTextChangedGump(object sender, EventArgs e)
{
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
if (Utils.ConvertStringToInt(TextBoxGump.Text, out int index, 0, 0xFFFE))
{
- TextBoxGump.ForeColor = Gumps.IsValidIndex(index) ? Color.Black : Color.Red;
+ TextBoxGump.ForeColor = Gumps.IsValidIndex(index) ? SystemColors.ControlText : invalidColor;
}
else
{
- TextBoxGump.ForeColor = Color.Red;
+ TextBoxGump.ForeColor = invalidColor;
}
}
diff --git a/UoFiddler.Controls/UserControls/GumpControl.cs b/UoFiddler.Controls/UserControls/GumpControl.cs
index 8875ee3..2ea9d66 100644
--- a/UoFiddler.Controls/UserControls/GumpControl.cs
+++ b/UoFiddler.Controls/UserControls/GumpControl.cs
@@ -617,13 +617,14 @@ private void OnClickFindFree(object sender, EventArgs e)
private void OnTextChanged_InsertAt(object sender, EventArgs e)
{
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
if (Utils.ConvertStringToInt(InsertText.Text, out int index, 0, Gumps.GetCount()))
{
- InsertText.ForeColor = Gumps.IsValidIndex(index) ? Color.Red : Color.Black;
+ InsertText.ForeColor = Gumps.IsValidIndex(index) ? invalidColor : SystemColors.ControlText;
}
else
{
- InsertText.ForeColor = Color.Red;
+ InsertText.ForeColor = invalidColor;
}
}
diff --git a/UoFiddler.Controls/UserControls/HuesControl.cs b/UoFiddler.Controls/UserControls/HuesControl.cs
index 8cb2f96..d29fc46 100644
--- a/UoFiddler.Controls/UserControls/HuesControl.cs
+++ b/UoFiddler.Controls/UserControls/HuesControl.cs
@@ -247,7 +247,9 @@ private void OnClickSave(object sender, EventArgs e)
private void OnTextChangedReplace(object sender, EventArgs e)
{
- ReplaceText.ForeColor = Utils.ConvertStringToInt(ReplaceText.Text, out _, 1, 3000) ? Color.Black : Color.Red;
+ ReplaceText.ForeColor = Utils.ConvertStringToInt(ReplaceText.Text, out _, 1, 3000)
+ ? SystemColors.ControlText
+ : (Options.DarkMode ? Color.OrangeRed : Color.Red);
}
private void OnKeyDownReplace(object sender, KeyEventArgs e)
diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs
index dd3d3a9..114f4d9 100644
--- a/UoFiddler.Controls/UserControls/ItemsControl.cs
+++ b/UoFiddler.Controls/UserControls/ItemsControl.cs
@@ -593,13 +593,14 @@ private void OnClickRemove(object sender, EventArgs e)
private void OnTextChangedInsert(object sender, EventArgs e)
{
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
if (Utils.ConvertStringToInt(InsertText.Text, out int index, 0, Art.GetMaxItemId()))
{
- InsertText.ForeColor = Art.IsValidStatic(index) ? Color.Red : Color.Black;
+ InsertText.ForeColor = Art.IsValidStatic(index) ? invalidColor : SystemColors.ControlText;
}
else
{
- InsertText.ForeColor = Color.Red;
+ InsertText.ForeColor = invalidColor;
}
}
diff --git a/UoFiddler.Controls/UserControls/LandTilesControl.cs b/UoFiddler.Controls/UserControls/LandTilesControl.cs
index 652f4e0..c59594a 100644
--- a/UoFiddler.Controls/UserControls/LandTilesControl.cs
+++ b/UoFiddler.Controls/UserControls/LandTilesControl.cs
@@ -422,13 +422,14 @@ private void OnClickReplace(object sender, EventArgs e)
private void OnTextChangedInsert(object sender, EventArgs e)
{
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
if (Utils.ConvertStringToInt(InsertText.Text, out int index, 0, 0x3FFF))
{
- InsertText.ForeColor = Art.IsValidLand(index) ? Color.Red : Color.Black;
+ InsertText.ForeColor = Art.IsValidLand(index) ? invalidColor : SystemColors.ControlText;
}
else
{
- InsertText.ForeColor = Color.Red;
+ InsertText.ForeColor = invalidColor;
}
}
diff --git a/UoFiddler.Controls/UserControls/LightControl.cs b/UoFiddler.Controls/UserControls/LightControl.cs
index f5fd910..6f3206a 100644
--- a/UoFiddler.Controls/UserControls/LightControl.cs
+++ b/UoFiddler.Controls/UserControls/LightControl.cs
@@ -269,13 +269,14 @@ private void OnClickReplace(object sender, EventArgs e)
private void OnTextChangedInsert(object sender, EventArgs e)
{
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
if (Utils.ConvertStringToInt(InsertText.Text, out int index, 0, 99))
{
- InsertText.ForeColor = Ultima.Light.TestLight(index) ? Color.Red : Color.Black;
+ InsertText.ForeColor = Ultima.Light.TestLight(index) ? invalidColor : SystemColors.ControlText;
}
else
{
- InsertText.ForeColor = Color.Red;
+ InsertText.ForeColor = invalidColor;
}
}
@@ -403,13 +404,14 @@ private void LandTileTextChanged(object sender, EventArgs e)
return;
}
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
if (Utils.ConvertStringToInt(LandTileText.Text, out int index, 0, 0x3FFF))
{
- LandTileText.ForeColor = !Ultima.Art.IsValidLand(index) ? Color.Red : Color.Black;
+ LandTileText.ForeColor = !Ultima.Art.IsValidLand(index) ? invalidColor : SystemColors.ControlText;
}
else
{
- LandTileText.ForeColor = Color.Red;
+ LandTileText.ForeColor = invalidColor;
}
}
@@ -442,13 +444,14 @@ private void LightTileTextChanged(object sender, EventArgs e)
return;
}
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
if (Utils.ConvertStringToInt(LightTileText.Text, out int index, 0, Ultima.Art.GetMaxItemId()))
{
- LightTileText.ForeColor = !Ultima.Art.IsValidStatic(index) ? Color.Red : Color.Black;
+ LightTileText.ForeColor = !Ultima.Art.IsValidStatic(index) ? invalidColor : SystemColors.ControlText;
}
else
{
- LightTileText.ForeColor = Color.Red;
+ LightTileText.ForeColor = invalidColor;
}
}
diff --git a/UoFiddler.Controls/UserControls/MapControl.cs b/UoFiddler.Controls/UserControls/MapControl.cs
index c770961..fa9b680 100644
--- a/UoFiddler.Controls/UserControls/MapControl.cs
+++ b/UoFiddler.Controls/UserControls/MapControl.cs
@@ -1089,7 +1089,9 @@ private void OnClickSwitchVisible(object sender, EventArgs e)
OverlayObject o = (OverlayObject)OverlayObjectTree.SelectedNode.Tag;
o.Visible = !o.Visible;
- OverlayObjectTree.SelectedNode.ForeColor = !o.Visible ? Color.Red : Color.Black;
+ OverlayObjectTree.SelectedNode.ForeColor = !o.Visible
+ ? (Options.DarkMode ? Color.OrangeRed : Color.Red)
+ : SystemColors.ControlText;
OverlayObjectTree.Invalidate();
pictureBox.Invalidate();
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.cs b/UoFiddler.Controls/UserControls/RadarColorControl.cs
index 4431b8d..0fec203 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.cs
@@ -179,7 +179,7 @@ private static void DrawRow(TileView.TileViewControl.DrawTileListItemEventArgs e
}
else if (modified)
{
- textColor = Color.Blue;
+ textColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
}
else
{
diff --git a/UoFiddler.Controls/UserControls/SkillGroupControl.cs b/UoFiddler.Controls/UserControls/SkillGroupControl.cs
index 78268b8..2986ad2 100644
--- a/UoFiddler.Controls/UserControls/SkillGroupControl.cs
+++ b/UoFiddler.Controls/UserControls/SkillGroupControl.cs
@@ -66,7 +66,7 @@ private void OnLoad(object sender, EventArgs e)
if (string.Equals("Misc", group.Name))
{
- groupNode.ForeColor = Color.Blue;
+ groupNode.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
}
for (int i = 0; i < SkillGroups.SkillList.Count; ++i)
diff --git a/UoFiddler.Controls/UserControls/SoundsControl.cs b/UoFiddler.Controls/UserControls/SoundsControl.cs
index d55b070..dc78698 100644
--- a/UoFiddler.Controls/UserControls/SoundsControl.cs
+++ b/UoFiddler.Controls/UserControls/SoundsControl.cs
@@ -103,7 +103,7 @@ private void OnLoad(object sender, EventArgs e)
if (translated)
{
- item.ForeColor = Color.Blue;
+ item.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
item.Font = new Font(Font, FontStyle.Underline);
}
@@ -114,7 +114,7 @@ private void OnLoad(object sender, EventArgs e)
cache.Add(new ListViewItem($"0x{i:X3} ")
{
Tag = i,
- ForeColor = Color.Red
+ ForeColor = Options.DarkMode ? Color.OrangeRed : Color.Red
});
}
}
@@ -470,7 +470,7 @@ private void OnClickRemove(object sender, EventArgs e)
else
{
selected.Text = $"0x{id + _soundIdOffset:X3}";
- selected.ForeColor = Color.Red;
+ selected.ForeColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
selected.Font = Font;
}
diff --git a/UoFiddler.Controls/UserControls/TexturesControl.cs b/UoFiddler.Controls/UserControls/TexturesControl.cs
index c7d8536..6f26925 100644
--- a/UoFiddler.Controls/UserControls/TexturesControl.cs
+++ b/UoFiddler.Controls/UserControls/TexturesControl.cs
@@ -319,13 +319,14 @@ private void OnClickReplace(object sender, EventArgs e)
private void OnTextChangedInsert(object sender, EventArgs e)
{
+ Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
if (Utils.ConvertStringToInt(InsertText.Text, out int index, 0, 0x3FFF))
{
- InsertText.ForeColor = Textures.TestTexture(index) ? Color.Red : Color.Black;
+ InsertText.ForeColor = Textures.TestTexture(index) ? invalidColor : SystemColors.ControlText;
}
else
{
- InsertText.ForeColor = Color.Red;
+ InsertText.ForeColor = invalidColor;
}
}
From d86a483f0db66f3d4f0f5dc1bf255e54fff6d901 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Mon, 25 May 2026 22:04:46 +0200
Subject: [PATCH 10/21] Use configured background color for preview in hue edit
form.
---
UoFiddler.Controls/Forms/HueEditForm.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/UoFiddler.Controls/Forms/HueEditForm.cs b/UoFiddler.Controls/Forms/HueEditForm.cs
index bd60432..f3aebe5 100644
--- a/UoFiddler.Controls/Forms/HueEditForm.cs
+++ b/UoFiddler.Controls/Forms/HueEditForm.cs
@@ -66,6 +66,7 @@ public HueEditForm(int index)
Selected = 0;
SecondSelected = -1;
+ pictureBoxPreview.BackColor = Options.PreviewBackgroundColor;
pictureBoxPreview.Image = new Bitmap(pictureBoxPreview.Width, pictureBoxPreview.Height);
}
@@ -416,7 +417,7 @@ private void RefreshPreview()
Hues.ApplyTo(bmp, _colors, hueOnlyGreyToolStripMenuItem.Checked);
using (Graphics g = Graphics.FromImage(pictureBoxPreview.Image))
{
- g.Clear(Color.White);
+ g.Clear(Options.PreviewBackgroundColor);
int x = (pictureBoxPreview.Image.Width / 2) - (bmp.Width / 2);
int y = (pictureBoxPreview.Image.Height / 2) - (bmp.Height / 2);
g.DrawImage(bmp, x, y);
From bb4afd10d94f294ce78f8b058dc7395fd8ddcd96 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 00:52:11 +0200
Subject: [PATCH 11/21] Improved radarcol and hue conversion.
---
Ultima/Helpers/HueHelpers.cs | 90 ++-
Ultima/Hues.cs | 58 +-
UoFiddler.Controls/Classes/Options.cs | 6 +
.../Classes/RadarColorAveraging.cs | 657 ++++++++++++++++++
.../RadarColorControl.Designer.cs | 653 ++++++++++-------
.../UserControls/RadarColorControl.cs | 546 ++++++++++++---
.../UserControls/RadarColorControl.resx | 16 +-
UoFiddler.Plugin.Compare/Classes/Utils.cs | 3 +-
8 files changed, 1577 insertions(+), 452 deletions(-)
create mode 100644 UoFiddler.Controls/Classes/RadarColorAveraging.cs
diff --git a/Ultima/Helpers/HueHelpers.cs b/Ultima/Helpers/HueHelpers.cs
index 1b04ab9..6f5b392 100644
--- a/Ultima/Helpers/HueHelpers.cs
+++ b/Ultima/Helpers/HueHelpers.cs
@@ -15,63 +15,77 @@ namespace Ultima.Helpers
{
public static class HueHelpers
{
- ///
- /// Converts RGB value to Hue color
- ///
- ///
- ///
- public static ushort ColorToHue(Color color)
+ // Canonical 8-bit RGB -> 15-bit hue. Uses bit-shift (>>3) packing with the rule:
+ // input all-zero -> 0; else any lane that collapses to 0 -> 1.
+ public static ushort ColorToHue(Color color) => ColorToHueShift(color.R, color.G, color.B);
+
+ // Canonical 15-bit hue -> 32-bit ARGB, expanding 5-bit components via
+ // (c<<3)|(c>>2) so 31 maps to 255 (not 248 as the previous *8 integer math did).
+ public static Color HueToColor(ushort hue)
{
- const double scale = 31.0 / 255;
+ return Color.FromArgb(
+ Expand5To8((hue & 0x7c00) >> 10),
+ Expand5To8((hue & 0x03e0) >> 5),
+ Expand5To8(hue & 0x001f));
+ }
- ushort origRed = color.R;
- var newRed = (ushort)(origRed * scale);
- if (newRed == 0 && origRed != 0)
- {
- newRed = 1;
- }
+ public static int HueToColorR(ushort hue) => Expand5To8((hue & 0x7c00) >> 10);
+ public static int HueToColorG(ushort hue) => Expand5To8((hue & 0x03e0) >> 5);
+ public static int HueToColorB(ushort hue) => Expand5To8(hue & 0x001f);
- ushort origGreen = color.G;
- var newGreen = (ushort)(origGreen * scale);
- if (newGreen == 0 && origGreen != 0)
+ // Canonical RGB->555 packer for this format. Packs each channel via bit-shift:
+ // result = ((r>>3) << 10) | ((g>>3) << 5) | (b>>3)
+ // Clamp rule: if r|g|b == 0 the pixel is 0 (transparent); else if a lane
+ // downscales to 0, force that lane to 1.
+ public static ushort ColorToHueShift(int r8, int g8, int b8)
+ {
+ if ((r8 | g8 | b8) == 0)
{
- newGreen = 1;
+ return 0;
}
- ushort origBlue = color.B;
- var newBlue = (ushort)(origBlue * scale);
- if (newBlue == 0 && origBlue != 0)
+ int r5 = r8 >> 3;
+ int g5 = g8 >> 3;
+ int b5 = b8 >> 3;
+ if (r5 == 0 && g5 == 0 && b5 == 0)
{
- newBlue = 1;
+ return 1;
}
- return (ushort)((newRed << 10) | (newGreen << 5) | newBlue);
+ return (ushort)((r5 << 10) | (g5 << 5) | b5);
}
- ///
- /// Converts Hue color to RGB color
- ///
- ///
- ///
- public static Color HueToColor(ushort hue)
+ // Rounding alternative: ((c*31 + 127) / 255). Same clamp rule as the shift version.
+ public static ushort ColorToHueRounded(int r8, int g8, int b8)
{
- const int scale = 255 / 31;
- return Color.FromArgb(((hue & 0x7c00) >> 10) * scale, ((hue & 0x3e0) >> 5) * scale, (hue & 0x1f) * scale);
- }
+ if ((r8 | g8 | b8) == 0)
+ {
+ return 0;
+ }
- public static int HueToColorR(ushort hue)
- {
- return ((hue & 0x7c00) >> 10) * (255 / 31);
+ int r5 = (r8 * 31 + 127) / 255;
+ int g5 = (g8 * 31 + 127) / 255;
+ int b5 = (b8 * 31 + 127) / 255;
+ if (r5 == 0 && g5 == 0 && b5 == 0)
+ {
+ return 1;
+ }
+
+ return (ushort)((r5 << 10) | (g5 << 5) | b5);
}
- public static int HueToColorG(ushort hue)
+ public static void HueExtract5(ushort hue, out int r5, out int g5, out int b5)
{
- return ((hue & 0x3e0) >> 5) * (255 / 31);
+ r5 = (hue & 0x7c00) >> 10;
+ g5 = (hue & 0x03e0) >> 5;
+ b5 = hue & 0x001f;
}
- public static int HueToColorB(ushort hue)
+ // Canonical 5-bit -> 8-bit expansion: replicate top 3 bits into the low ones.
+ // Equivalent to round(c5 * 255 / 31). 0->0, 31->255, monotonic.
+ public static int Expand5To8(int c5)
{
- return (hue & 0x1f) * (255 / 31);
+ return (c5 << 3) | (c5 >> 2);
}
}
}
\ No newline at end of file
diff --git a/Ultima/Hues.cs b/Ultima/Hues.cs
index 7c50850..9eb4838 100644
--- a/Ultima/Hues.cs
+++ b/Ultima/Hues.cs
@@ -149,54 +149,6 @@ public static Hue GetHue(int index)
return List[0];
}
- ///
- /// Converts RGB value to Hue color
- ///
- ///
- ///
- public static ushort ColorToHue(Color color)
- {
- const double scale = 31.0 / 255;
-
- ushort origRed = color.R;
- var newRed = (ushort)(origRed * scale);
- if (newRed == 0 && origRed != 0)
- {
- newRed = 1;
- }
-
- ushort origGreen = color.G;
- var newGreen = (ushort)(origGreen * scale);
- if (newGreen == 0 && origGreen != 0)
- {
- newGreen = 1;
- }
-
- ushort origBlue = color.B;
- var newBlue = (ushort)(origBlue * scale);
- if (newBlue == 0 && origBlue != 0)
- {
- newBlue = 1;
- }
-
- return (ushort)((newRed << 10) | (newGreen << 5) | newBlue);
- }
-
- public static int HueToColorR(ushort hue)
- {
- return ((hue & 0x7c00) >> 10) * (255 / 31);
- }
-
- public static int HueToColorG(ushort hue)
- {
- return ((hue & 0x3e0) >> 5) * (255 / 31);
- }
-
- public static int HueToColorB(ushort hue)
- {
- return (hue & 0x1f) * (255 / 31);
- }
-
public static unsafe void ApplyTo(Bitmap bmp, ushort[] colors, bool onlyHueGrayPixels)
{
BitmapData bd = bmp.LockBits(
@@ -334,7 +286,15 @@ public Hue(int index, HueDataMul mulStruct)
Colors = new ushort[32];
for (int i = 0; i < 32; ++i)
{
- Colors[i] = mulStruct.colors[i];
+ ushort c = mulStruct.colors[i];
+ // Clamp c == 0 or any value with the high bit set to 1. The high bit is a
+ // flag in this format, never part of a valid color value.
+ if (c == 0 || c > 0x7fff)
+ {
+ c = 1;
+ }
+
+ Colors[i] = c;
}
TableStart = mulStruct.tableStart;
diff --git a/UoFiddler.Controls/Classes/Options.cs b/UoFiddler.Controls/Classes/Options.cs
index 28d072f..1cceb13 100644
--- a/UoFiddler.Controls/Classes/Options.cs
+++ b/UoFiddler.Controls/Classes/Options.cs
@@ -32,6 +32,12 @@ public static class Options
///
public static bool ArtItemClip { get; set; } = true;
+ ///
+ /// Strategy used by the RadarColor control to derive a 16-bit color from a tile graphic.
+ /// Runtime-only (not persisted across sessions yet).
+ ///
+ public static RadarAveragingStrategy RadarColorStrategy { get; set; } = RadarAveragingStrategy.Mean5BankersRound;
+
///
/// Offsets the sound ids in Sound tab by 1 (POL specific setting)
///
diff --git a/UoFiddler.Controls/Classes/RadarColorAveraging.cs b/UoFiddler.Controls/Classes/RadarColorAveraging.cs
new file mode 100644
index 0000000..c5b549d
--- /dev/null
+++ b/UoFiddler.Controls/Classes/RadarColorAveraging.cs
@@ -0,0 +1,657 @@
+/***************************************************************************
+ *
+ * $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.Drawing.Imaging;
+using Ultima;
+using Ultima.Helpers;
+
+namespace UoFiddler.Controls.Classes
+{
+ public enum RadarAveragingStrategy
+ {
+ // Order roughly matches likelihood of matching the on-disk radarcol.mul.
+ Mean5,
+ Mean8Shift,
+ Mean5Rounded,
+ Mean5RoundedIncludeAlpha,
+ Mean5BankersRound,
+ Mean5RoundedNoOutline,
+ SnapToLandPalette,
+ SnapToItemPalette,
+ MeanRounded,
+ MeanLinear,
+ Mode16,
+ MedianPerChannel,
+ MeanNoOutline,
+ // Reproduces the historical UOFiddler behavior exactly (average in *31/255-truncated
+ // 8-bit space, then ColorToHue with the same truncation). Kept so existing users can
+ // get back the values they're used to.
+ Legacy,
+ }
+
+ public static class RadarColorAveraging
+ {
+ public static IReadOnlyList All { get; } = new[]
+ {
+ RadarAveragingStrategy.Mean5,
+ RadarAveragingStrategy.Mean8Shift,
+ RadarAveragingStrategy.Mean5Rounded,
+ RadarAveragingStrategy.Mean5RoundedIncludeAlpha,
+ RadarAveragingStrategy.Mean5BankersRound,
+ RadarAveragingStrategy.Mean5RoundedNoOutline,
+ RadarAveragingStrategy.SnapToLandPalette,
+ RadarAveragingStrategy.SnapToItemPalette,
+ RadarAveragingStrategy.MeanRounded,
+ RadarAveragingStrategy.MeanLinear,
+ RadarAveragingStrategy.Mode16,
+ RadarAveragingStrategy.MedianPerChannel,
+ RadarAveragingStrategy.MeanNoOutline,
+ RadarAveragingStrategy.Legacy,
+ };
+
+ public static string DisplayName(RadarAveragingStrategy s) => s switch
+ {
+ RadarAveragingStrategy.Mean5 => "Mean (5-bit)",
+ RadarAveragingStrategy.Mean8Shift => "Mean (8-bit, >>3 pack)",
+ RadarAveragingStrategy.Mean5Rounded => "Mean (5-bit, rounded)",
+ RadarAveragingStrategy.Mean5RoundedIncludeAlpha => "Mean (5-bit, rounded, incl. transparent)",
+ RadarAveragingStrategy.Mean5BankersRound => "Mean (5-bit, banker's round)",
+ RadarAveragingStrategy.Mean5RoundedNoOutline => "Mean (5-bit, rounded, no outline)",
+ RadarAveragingStrategy.SnapToLandPalette => "Snap to land palette",
+ RadarAveragingStrategy.SnapToItemPalette => "Snap to item palette",
+ RadarAveragingStrategy.MeanRounded => "Mean (8-bit, rounded pack)",
+ RadarAveragingStrategy.MeanLinear => "Mean (linear-light)",
+ RadarAveragingStrategy.Mode16 => "Mode (dominant pixel)",
+ RadarAveragingStrategy.MedianPerChannel => "Median per channel",
+ RadarAveragingStrategy.MeanNoOutline => "Mean (no outline)",
+ RadarAveragingStrategy.Legacy => "Legacy (UOFiddler)",
+ _ => s.ToString(),
+ };
+
+ public static ushort Compute(Bitmap image, RadarAveragingStrategy strategy)
+ {
+ if (image == null)
+ {
+ return 0;
+ }
+
+ ushort[] pixels = IncludesTransparent(strategy)
+ ? CollectAllPixels(image)
+ : CollectOpaquePixels(image, out int _, out int _);
+ if (pixels.Length == 0)
+ {
+ return 0;
+ }
+
+ return Dispatch(pixels, strategy);
+ }
+
+ private static bool IncludesTransparent(RadarAveragingStrategy strategy) =>
+ strategy == RadarAveragingStrategy.Mean5RoundedIncludeAlpha;
+
+ private static ushort Dispatch(ushort[] pixels, RadarAveragingStrategy strategy) => strategy switch
+ {
+ RadarAveragingStrategy.Mean5 => Mean5(pixels, rounded: false),
+ RadarAveragingStrategy.Mean5Rounded => Mean5(pixels, rounded: true),
+ // Same math as Mean5Rounded; the difference is at pixel collection time
+ // (zeros are included in the pooled array and dilute the average).
+ RadarAveragingStrategy.Mean5RoundedIncludeAlpha => Mean5(pixels, rounded: true),
+ RadarAveragingStrategy.Mean5BankersRound => Mean5Banker(pixels),
+ RadarAveragingStrategy.SnapToLandPalette => SnapTo(Mean5Banker(pixels), GetLandPalette()),
+ RadarAveragingStrategy.SnapToItemPalette => SnapTo(Mean5Banker(pixels), GetItemPalette()),
+ RadarAveragingStrategy.Mean5RoundedNoOutline => Mean5RoundedNoOutline(pixels),
+ RadarAveragingStrategy.Mean8Shift => Mean8(pixels, ConvertPack.Shift),
+ RadarAveragingStrategy.MeanRounded => Mean8(pixels, ConvertPack.Rounded),
+ RadarAveragingStrategy.MeanLinear => MeanLinear(pixels),
+ RadarAveragingStrategy.Mode16 => Mode16(pixels),
+ RadarAveragingStrategy.MedianPerChannel => MedianPerChannel(pixels),
+ RadarAveragingStrategy.MeanNoOutline => MeanNoOutline(pixels),
+ RadarAveragingStrategy.Legacy => Legacy(pixels),
+ _ => Mean5(pixels, rounded: true),
+ };
+
+ ///
+ /// Pools pixels across multiple bitmaps and runs the strategy once.
+ ///
+ ///
+ ///
+ ///
+ public static ushort ComputeFromMany(IEnumerable images, RadarAveragingStrategy strategy)
+ {
+ bool includeAlpha = IncludesTransparent(strategy);
+ var pooled = new List(4096);
+ foreach (Bitmap img in images)
+ {
+ if (img == null)
+ {
+ continue;
+ }
+ ushort[] px = includeAlpha
+ ? CollectAllPixels(img)
+ : CollectOpaquePixels(img, out int _, out int _);
+ if (px.Length > 0)
+ {
+ pooled.AddRange(px);
+ }
+ }
+ if (pooled.Count == 0)
+ {
+ return 0;
+ }
+ return Dispatch(pooled.ToArray(), strategy);
+ }
+
+ ///
+ /// Reads all non-zero pixels (zero is the canonical transparency marker in the
+ /// Format16bppArgb1555 bitmaps Art.GetStatic / Art.GetLand produce) and returns them as a flat ushort[].
+ /// Width/height returned for callers that care. Reads every pixel including zeros (transparent).
+ /// Strategies that want to weight transparent area into the average operate on this output.
+ ///
+ ///
+ ///
+ private static unsafe ushort[] CollectAllPixels(Bitmap image)
+ {
+ int width = image.Width;
+ int height = image.Height;
+ BitmapData bd = image.LockBits(
+ new Rectangle(0, 0, width, height),
+ ImageLockMode.ReadOnly,
+ PixelFormat.Format16bppArgb1555);
+ try
+ {
+ var result = new ushort[width * height];
+ ushort* line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
+ int idx = 0;
+ for (int y = 0; y < height; ++y, line += delta)
+ {
+ for (int x = 0; x < width; ++x)
+ {
+ result[idx++] = line[x];
+ }
+ }
+
+ return result;
+ }
+ finally
+ {
+ image.UnlockBits(bd);
+ }
+ }
+
+ private static unsafe ushort[] CollectOpaquePixels(Bitmap image, out int width, out int height)
+ {
+ width = image.Width;
+ height = image.Height;
+
+ BitmapData bd = image.LockBits(
+ new Rectangle(0, 0, width, height),
+ ImageLockMode.ReadOnly,
+ PixelFormat.Format16bppArgb1555);
+
+ try
+ {
+ var list = new List(width * height);
+ ushort* line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
+ for (int y = 0; y < height; ++y, line += delta)
+ {
+ for (int x = 0; x < width; ++x)
+ {
+ ushort p = line[x];
+
+ if (p != 0)
+ {
+ list.Add(p);
+ }
+ }
+ }
+
+ return list.ToArray();
+ }
+ finally
+ {
+ image.UnlockBits(bd);
+ }
+ }
+
+ private enum ConvertPack { Shift, Rounded }
+
+ private static ushort Mean5(ushort[] pixels, bool rounded)
+ {
+ long r = 0;
+ long g = 0;
+ long b = 0;
+
+ int n = pixels.Length;
+
+ for (int i = 0; i < n; ++i)
+ {
+ HueHelpers.HueExtract5(pixels[i], out int r5, out int g5, out int b5);
+
+ r += r5;
+ g += g5;
+ b += b5;
+ }
+
+ int half = rounded ? n / 2 : 0;
+ int rr = (int)((r + half) / n);
+ int gg = (int)((g + half) / n);
+ int bb = (int)((b + half) / n);
+
+ return Pack5WithClamp(rr, gg, bb);
+ }
+
+ ///
+ /// Round-half-to-even (banker's).
+ /// Differs from Mean5(rounded) only when the channel sum is exactly half-way between two ints.
+ ///
+ ///
+ ///
+ private static ushort Mean5Banker(ushort[] pixels)
+ {
+ long r = 0;
+ long g = 0;
+ long b = 0;
+
+ int n = pixels.Length;
+
+ for (int i = 0; i < n; ++i)
+ {
+ HueHelpers.HueExtract5(pixels[i], out int r5, out int g5, out int b5);
+
+ r += r5;
+ g += g5;
+ b += b5;
+ }
+
+ return Pack5WithClamp(DivRoundEven(r, n), DivRoundEven(g, n), DivRoundEven(b, n));
+ }
+
+ private static int DivRoundEven(long sum, int n)
+ {
+ long q = sum / n;
+ long rem = sum - q * n;
+ long twice = rem * 2;
+ if (twice < n)
+ {
+ return (int)q;
+ }
+
+ if (twice > n)
+ {
+ return (int)(q + 1);
+ }
+
+ // exact tie: round to even
+ return (q & 1) == 0 ? (int)q : (int)(q + 1);
+ }
+
+ ///
+ /// Drop near-black outline pixels (5-bit luma < 2) then apply rounded 5-bit mean.
+ /// Tests whether OSI excluded outline pixels before averaging.
+ ///
+ ///
+ ///
+ private static ushort Mean5RoundedNoOutline(ushort[] pixels)
+ {
+ long r = 0;
+ long g = 0;
+ long b = 0;
+
+ int n = 0;
+ foreach (ushort p in pixels)
+ {
+ HueHelpers.HueExtract5(p, out int r5, out int g5, out int b5);
+
+ int y5 = (r5 + g5 + b5) / 3;
+ if (y5 < 2)
+ {
+ continue;
+ }
+
+ r += r5;
+ g += g5;
+ b += b5;
+
+ ++n;
+ }
+
+ if (n == 0)
+ {
+ return Mean5(pixels, rounded: true);
+ }
+
+ int half = n / 2;
+
+ return Pack5WithClamp((int)((r + half) / n), (int)((g + half) / n), (int)((b + half) / n));
+ }
+
+ private static ushort Mean8(ushort[] pixels, ConvertPack pack)
+ {
+ long r = 0;
+ long g = 0;
+ long b = 0;
+
+ int n = pixels.Length;
+ for (int i = 0; i < n; ++i)
+ {
+ HueHelpers.HueExtract5(pixels[i], out int r5, out int g5, out int b5);
+
+ r += HueHelpers.Expand5To8(r5);
+ g += HueHelpers.Expand5To8(g5);
+ b += HueHelpers.Expand5To8(b5);
+ }
+
+ int rr = (int)((r + n / 2) / n);
+ int gg = (int)((g + n / 2) / n);
+ int bb = (int)((b + n / 2) / n);
+
+ return pack == ConvertPack.Shift
+ ? HueHelpers.ColorToHueShift(rr, gg, bb)
+ : HueHelpers.ColorToHueRounded(rr, gg, bb);
+ }
+
+ ///
+ /// sRGB -> linear via x*x (cheap, monotonic, no transcendentals); average in linear;
+ /// back via sqrt. This is a control candidate; 1997 tooling is unlikely to
+ /// be gamma-aware so we don't expect a high exact-match.
+ ///
+ ///
+ ///
+ private static ushort MeanLinear(ushort[] pixels)
+ {
+ double r = 0;
+ double g = 0;
+ double b = 0;
+
+ int n = pixels.Length;
+ for (int i = 0; i < n; ++i)
+ {
+ HueHelpers.HueExtract5(pixels[i], out int r5, out int g5, out int b5);
+
+ double rr = HueHelpers.Expand5To8(r5) / 255.0;
+ double gg = HueHelpers.Expand5To8(g5) / 255.0;
+ double bb = HueHelpers.Expand5To8(b5) / 255.0;
+
+ r += rr * rr;
+ g += gg * gg;
+ b += bb * bb;
+ }
+
+ int r8 = (int)Math.Round(Math.Sqrt(r / n) * 255.0);
+ int g8 = (int)Math.Round(Math.Sqrt(g / n) * 255.0);
+ int b8 = (int)Math.Round(Math.Sqrt(b / n) * 255.0);
+
+ return HueHelpers.ColorToHueShift(r8, g8, b8);
+ }
+
+ private static ushort Mode16(ushort[] pixels)
+ {
+ var counts = new Dictionary(pixels.Length);
+ foreach (ushort p in pixels)
+ {
+ counts.TryGetValue(p, out int c);
+ counts[p] = c + 1;
+ }
+
+ ushort best = pixels[0];
+
+ int bestCount = 0;
+ foreach (var kv in counts)
+ {
+ if (kv.Value > bestCount)
+ {
+ best = kv.Key;
+ bestCount = kv.Value;
+ }
+ }
+
+ // Strip stored alpha bit: radarcol entries never have 0x8000 set.
+ return (ushort)(best & 0x7fff);
+ }
+
+ private static ushort MedianPerChannel(ushort[] pixels)
+ {
+ int n = pixels.Length;
+
+ var rs = new int[n];
+ var gs = new int[n];
+ var bs = new int[n];
+
+ for (int i = 0; i < n; ++i)
+ {
+ HueHelpers.HueExtract5(pixels[i], out int r5, out int g5, out int b5);
+ rs[i] = r5; gs[i] = g5; bs[i] = b5;
+ }
+
+ Array.Sort(rs); Array.Sort(gs); Array.Sort(bs);
+
+ int mid = n / 2;
+
+ return Pack5WithClamp(rs[mid], gs[mid], bs[mid]);
+ }
+
+ ///
+ /// Drop near-black outline pixels (5-bit luminance < 2) before averaging.
+ /// Falls back to plain Mean5 if everything is dark.
+ ///
+ ///
+ ///
+ private static ushort MeanNoOutline(ushort[] pixels)
+ {
+ long r = 0;
+ long g = 0;
+ long b = 0;
+
+ int n = 0;
+ foreach (ushort p in pixels)
+ {
+ HueHelpers.HueExtract5(p, out int r5, out int g5, out int b5);
+
+ // 5-bit Rec.601 luma weights would be ideal but for outline rejection a
+ // simple mean of the channels in 5-bit space is adequate.
+ int y5 = (r5 + g5 + b5) / 3;
+ if (y5 < 2)
+ {
+ continue;
+ }
+
+ r += r5;
+ g += g5;
+ b += b5;
+
+ ++n;
+ }
+
+ if (n == 0)
+ {
+ return Mean5(pixels, rounded: false);
+ }
+
+ return Pack5WithClamp((int)(r / n), (int)(g / n), (int)(b / n));
+ }
+
+ // The pre-existing UOFiddler behavior, reproduced verbatim so users can opt back
+ // in if they want bit-for-bit continuity with older builds. Math is inlined here
+ // because the rest of the codebase has since migrated HueHelpers.HueToColor* /
+ // ColorToHue to the OSI-canonical convention.
+ private static ushort Legacy(ushort[] pixels)
+ {
+ const int legacyUpscale = 255 / 31; // == 8 (integer truncation, the historical bug)
+
+ long r = 0;
+ long g = 0;
+ long b = 0;
+
+ int n = pixels.Length;
+ for (int i = 0; i < n; ++i)
+ {
+ ushort p = pixels[i];
+
+ r += ((p & 0x7c00) >> 10) * legacyUpscale;
+ g += ((p & 0x03e0) >> 5) * legacyUpscale;
+ b += (p & 0x001f) * legacyUpscale;
+ }
+
+ int rr = (int)(r / n);
+ int gg = (int)(g / n);
+ int bb = (int)(b / n);
+
+ // Legacy downscale: floor(c * 31 / 255) per channel, with the 0->1 clamp.
+ const double legacyDownscale = 31.0 / 255;
+
+ int newR = (int)(rr * legacyDownscale);
+ if (newR == 0 && rr != 0)
+ {
+ newR = 1;
+ }
+
+ int newG = (int)(gg * legacyDownscale); if (newG == 0 && gg != 0)
+ {
+ newG = 1;
+ }
+
+ int newB = (int)(bb * legacyDownscale); if (newB == 0 && bb != 0)
+ {
+ newB = 1;
+ }
+
+ return (ushort)((newR << 10) | (newG << 5) | newB);
+ }
+
+ ///
+ /// Snaps a 16-bit color to the nearest entry in the supplied palette by 5-bit squared Euclidean distance.
+ /// Used for the land snap strategy: the loaded radarcol.mul has ~100 unique land colors (terrain palette),
+ /// and any computed average should round to whichever of those it lies closest to.
+ ///
+ ///
+ ///
+ ///
+ private static ushort SnapTo(ushort candidate, ushort[] palette)
+ {
+ if (palette.Length == 0)
+ {
+ return candidate;
+ }
+
+ HueHelpers.HueExtract5(candidate, out int cr, out int cg, out int cb);
+
+ ushort best = palette[0];
+
+ int bestDist = int.MaxValue;
+ for (int i = 0; i < palette.Length; ++i)
+ {
+ HueHelpers.HueExtract5(palette[i], out int pr, out int pg, out int pb);
+
+ int dr = cr - pr, dg = cg - pg, db = cb - pb;
+ int d = dr * dr + dg * dg + db * db;
+
+ if (d < bestDist)
+ {
+ bestDist = d;
+ best = palette[i];
+ }
+ }
+
+ return best;
+ }
+
+ // Cached unique palettes built from the currently-loaded radarcol.mul. Invalidated
+ // when the Colors array reference changes (load/import) — we compare references,
+ // which is cheap and matches RadarCol's reassignment pattern.
+ private static ushort[] _landPalette = Array.Empty();
+ private static ushort[] _itemPalette = Array.Empty();
+ private static ushort[] _palettesBuiltFor;
+
+ private static void RebuildPalettesIfStale()
+ {
+ ushort[] cur = RadarCol.Colors;
+ if (ReferenceEquals(cur, _palettesBuiltFor))
+ {
+ return;
+ }
+
+ _palettesBuiltFor = cur;
+
+ if (cur == null || cur.Length == 0)
+ {
+ _landPalette = Array.Empty();
+ _itemPalette = Array.Empty();
+ return;
+ }
+
+ var landSet = new HashSet();
+ var itemSet = new HashSet();
+ int landEnd = Math.Min(0x4000, cur.Length);
+
+ for (int i = 0; i < landEnd; ++i)
+ {
+ if (cur[i] != 0)
+ {
+ landSet.Add(cur[i]);
+ }
+ }
+
+ for (int i = 0x4000; i < cur.Length; ++i)
+ {
+ if (cur[i] != 0)
+ {
+ itemSet.Add(cur[i]);
+ }
+ }
+
+ _landPalette = new ushort[landSet.Count];
+ landSet.CopyTo(_landPalette);
+
+ _itemPalette = new ushort[itemSet.Count];
+ itemSet.CopyTo(_itemPalette);
+ }
+
+ private static ushort[] GetLandPalette()
+ {
+ RebuildPalettesIfStale();
+
+ return _landPalette;
+ }
+
+ private static ushort[] GetItemPalette()
+ {
+ RebuildPalettesIfStale();
+
+ return _itemPalette;
+ }
+
+ ///
+ /// Packs 5-bit components and applies the OSI clamp (all-zero -> 0, else any collapsed lane in a non-zero pixel -> 1).
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static ushort Pack5WithClamp(int r5, int g5, int b5)
+ {
+ r5 = Math.Clamp(r5, 0, 31);
+ g5 = Math.Clamp(g5, 0, 31);
+ b5 = Math.Clamp(b5, 0, 31);
+
+ if ((r5 | g5 | b5) == 0)
+ {
+ return 0;
+ }
+
+ return (ushort)((r5 << 10) | (g5 << 5) | b5);
+ }
+ }
+}
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs b/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
index 30dbe00..149870a 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
@@ -53,6 +53,8 @@ private void InitializeComponent()
setAsRangeFromToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
setAsRangeToToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
pictureBoxArt = new System.Windows.Forms.PictureBox();
+ PictureBoxContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
+ changeBackgroundColorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
pictureBoxColor = new System.Windows.Forms.PictureBox();
splitContainer5 = new System.Windows.Forms.SplitContainer();
splitContainer6 = new System.Windows.Forms.SplitContainer();
@@ -69,37 +71,45 @@ private void InitializeComponent()
textFilterLand = new System.Windows.Forms.TextBox();
buttonSelectAllLand = new System.Windows.Forms.Button();
buttonSelectNoneLand = new System.Windows.Forms.Button();
+ groupColor = new System.Windows.Forms.GroupBox();
+ groupColorValue = new System.Windows.Forms.GroupBox();
+ labelHex = new System.Windows.Forms.Label();
+ numericUpDownShortCol = new System.Windows.Forms.NumericUpDown();
+ labelRgb = new System.Windows.Forms.Label();
+ numericUpDownR = new System.Windows.Forms.NumericUpDown();
+ numericUpDownG = new System.Windows.Forms.NumericUpDown();
+ numericUpDownB = new System.Windows.Forms.NumericUpDown();
+ groupSingleTile = new System.Windows.Forms.GroupBox();
+ buttonMean = new System.Windows.Forms.Button();
+ buttonRevert = new System.Windows.Forms.Button();
+ comboMeanStrategy = new System.Windows.Forms.ComboBox();
+ buttonStrategyHelp = new System.Windows.Forms.Button();
+ buttonSaveColor = new System.Windows.Forms.Button();
+ groupBatch = new System.Windows.Forms.GroupBox();
radioUseSelection = new System.Windows.Forms.RadioButton();
radioUseRange = new System.Windows.Forms.RadioButton();
+ textBoxMeanFrom = new System.Windows.Forms.TextBox();
label4 = new System.Windows.Forms.Label();
- buttonRangeToRangeAverage = new System.Windows.Forms.Button();
+ textBoxMeanTo = new System.Windows.Forms.TextBox();
+ buttonCurrentToRangeAverage = new System.Windows.Forms.Button();
buttonRangeToIndividualAverage = new System.Windows.Forms.Button();
+ buttonRangeToRangeAverage = new System.Windows.Forms.Button();
+ groupFile = new System.Windows.Forms.GroupBox();
+ buttonSaveFile = new System.Windows.Forms.Button();
buttonRevertAll = new System.Windows.Forms.Button();
- buttonRevert = new System.Windows.Forms.Button();
- label2 = new System.Windows.Forms.Label();
+ buttonExport = new System.Windows.Forms.Button();
+ buttonImport = new System.Windows.Forms.Button();
+ buttonAverageAll = new System.Windows.Forms.Button();
label1 = new System.Windows.Forms.Label();
- progressBar2 = new System.Windows.Forms.ProgressBar();
progressBar1 = new System.Windows.Forms.ProgressBar();
- button6 = new System.Windows.Forms.Button();
- button5 = new System.Windows.Forms.Button();
- button4 = new System.Windows.Forms.Button();
- numericUpDownShortCol = new System.Windows.Forms.NumericUpDown();
- textBoxMeanFrom = new System.Windows.Forms.TextBox();
- textBoxMeanTo = new System.Windows.Forms.TextBox();
- buttonCurrentToRangeAverage = new System.Windows.Forms.Button();
- numericUpDownB = new System.Windows.Forms.NumericUpDown();
- numericUpDownG = new System.Windows.Forms.NumericUpDown();
- numericUpDownR = new System.Windows.Forms.NumericUpDown();
- button2 = new System.Windows.Forms.Button();
- button1 = new System.Windows.Forms.Button();
- buttonMean = new System.Windows.Forms.Button();
- PictureBoxContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
- changeBackgroundColorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ label2 = new System.Windows.Forms.Label();
+ progressBar2 = new System.Windows.Forms.ProgressBar();
+ toolTip = new System.Windows.Forms.ToolTip(components);
colorDialog = new System.Windows.Forms.ColorDialog();
contextMenuStrip1.SuspendLayout();
contextMenuStrip2.SuspendLayout();
- PictureBoxContextMenuStrip.SuspendLayout();
((System.ComponentModel.ISupportInitialize)pictureBoxArt).BeginInit();
+ PictureBoxContextMenuStrip.SuspendLayout();
((System.ComponentModel.ISupportInitialize)pictureBoxColor).BeginInit();
((System.ComponentModel.ISupportInitialize)splitContainer5).BeginInit();
splitContainer5.Panel1.SuspendLayout();
@@ -128,25 +138,30 @@ private void InitializeComponent()
splitContainer4.Panel1.SuspendLayout();
splitContainer4.Panel2.SuspendLayout();
splitContainer4.SuspendLayout();
+ groupColor.SuspendLayout();
+ groupColorValue.SuspendLayout();
((System.ComponentModel.ISupportInitialize)numericUpDownShortCol).BeginInit();
- ((System.ComponentModel.ISupportInitialize)numericUpDownB).BeginInit();
- ((System.ComponentModel.ISupportInitialize)numericUpDownG).BeginInit();
((System.ComponentModel.ISupportInitialize)numericUpDownR).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)numericUpDownG).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)numericUpDownB).BeginInit();
+ groupSingleTile.SuspendLayout();
+ groupBatch.SuspendLayout();
+ groupFile.SuspendLayout();
SuspendLayout();
//
// tileViewItem
- //
+ //
tileViewItem.ContextMenuStrip = contextMenuStrip1;
tileViewItem.Dock = System.Windows.Forms.DockStyle.Fill;
tileViewItem.Location = new System.Drawing.Point(0, 0);
tileViewItem.Margin = new System.Windows.Forms.Padding(4);
tileViewItem.Name = "tileViewItem";
- tileViewItem.Size = new System.Drawing.Size(228, 164);
+ tileViewItem.Size = new System.Drawing.Size(259, 289);
tileViewItem.TabIndex = 0;
tileViewItem.ShowCheckBoxes = true;
tileViewItem.TileHighLightOpacity = 0D;
- tileViewItem.DrawItem += OnDrawItemRow;
tileViewItem.FocusSelectionChanged += OnItemFocusChanged;
+ tileViewItem.DrawItem += OnDrawItemRow;
tileViewItem.SizeChanged += OnTileViewSizeChanged;
//
// contextMenuStrip1
@@ -185,7 +200,7 @@ private void InitializeComponent()
setAsRangeToToolStripMenuItem.Click += OnClickSetRangeTo;
//
// tileViewLand
- //
+ //
tileViewLand.ContextMenuStrip = contextMenuStrip2;
tileViewLand.Dock = System.Windows.Forms.DockStyle.Fill;
tileViewLand.Location = new System.Drawing.Point(0, 0);
@@ -195,8 +210,8 @@ private void InitializeComponent()
tileViewLand.TabIndex = 0;
tileViewLand.ShowCheckBoxes = true;
tileViewLand.TileHighLightOpacity = 0D;
- tileViewLand.DrawItem += OnDrawLandRow;
tileViewLand.FocusSelectionChanged += OnLandFocusChanged;
+ tileViewLand.DrawItem += OnDrawLandRow;
tileViewLand.SizeChanged += OnTileViewSizeChanged;
//
// contextMenuStrip2
@@ -235,35 +250,35 @@ private void InitializeComponent()
setAsRangeToToolStripMenuItem1.Click += OnClickSetRangeTo;
//
// pictureBoxArt
- //
+ //
pictureBoxArt.ContextMenuStrip = PictureBoxContextMenuStrip;
pictureBoxArt.Dock = System.Windows.Forms.DockStyle.Fill;
pictureBoxArt.Location = new System.Drawing.Point(0, 0);
pictureBoxArt.Margin = new System.Windows.Forms.Padding(4);
pictureBoxArt.Name = "pictureBoxArt";
- pictureBoxArt.Size = new System.Drawing.Size(244, 154);
+ pictureBoxArt.Size = new System.Drawing.Size(275, 241);
pictureBoxArt.TabIndex = 0;
pictureBoxArt.TabStop = false;
- //
+ //
// PictureBoxContextMenuStrip
- //
+ //
PictureBoxContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { changeBackgroundColorToolStripMenuItem });
PictureBoxContextMenuStrip.Name = "PictureBoxContextMenuStrip";
PictureBoxContextMenuStrip.Size = new System.Drawing.Size(213, 26);
- //
+ //
// changeBackgroundColorToolStripMenuItem
- //
+ //
changeBackgroundColorToolStripMenuItem.Name = "changeBackgroundColorToolStripMenuItem";
changeBackgroundColorToolStripMenuItem.Size = new System.Drawing.Size(212, 22);
changeBackgroundColorToolStripMenuItem.Text = "Change background color";
changeBackgroundColorToolStripMenuItem.Click += ChangeBackgroundColorToolStripMenuItem_Click;
- //
+ //
// pictureBoxColor
//
- pictureBoxColor.Location = new System.Drawing.Point(4, 4);
+ pictureBoxColor.Location = new System.Drawing.Point(10, 22);
pictureBoxColor.Margin = new System.Windows.Forms.Padding(4);
pictureBoxColor.Name = "pictureBoxColor";
- pictureBoxColor.Size = new System.Drawing.Size(161, 116);
+ pictureBoxColor.Size = new System.Drawing.Size(210, 96);
pictureBoxColor.TabIndex = 0;
pictureBoxColor.TabStop = false;
//
@@ -280,33 +295,13 @@ private void InitializeComponent()
//
// splitContainer5.Panel2
//
- splitContainer5.Panel2.Controls.Add(radioUseSelection);
- splitContainer5.Panel2.Controls.Add(radioUseRange);
- splitContainer5.Panel2.Controls.Add(label4);
- splitContainer5.Panel2.Controls.Add(buttonRangeToRangeAverage);
- splitContainer5.Panel2.Controls.Add(buttonRangeToIndividualAverage);
- splitContainer5.Panel2.Controls.Add(buttonRevertAll);
- splitContainer5.Panel2.Controls.Add(buttonRevert);
- splitContainer5.Panel2.Controls.Add(label2);
- splitContainer5.Panel2.Controls.Add(label1);
- splitContainer5.Panel2.Controls.Add(progressBar2);
- splitContainer5.Panel2.Controls.Add(progressBar1);
- splitContainer5.Panel2.Controls.Add(button6);
- splitContainer5.Panel2.Controls.Add(button5);
- splitContainer5.Panel2.Controls.Add(button4);
- splitContainer5.Panel2.Controls.Add(numericUpDownShortCol);
- splitContainer5.Panel2.Controls.Add(textBoxMeanFrom);
- splitContainer5.Panel2.Controls.Add(textBoxMeanTo);
- splitContainer5.Panel2.Controls.Add(buttonCurrentToRangeAverage);
- splitContainer5.Panel2.Controls.Add(numericUpDownB);
- splitContainer5.Panel2.Controls.Add(numericUpDownG);
- splitContainer5.Panel2.Controls.Add(numericUpDownR);
- splitContainer5.Panel2.Controls.Add(button2);
- splitContainer5.Panel2.Controls.Add(button1);
- splitContainer5.Panel2.Controls.Add(buttonMean);
- splitContainer5.Panel2.Controls.Add(pictureBoxColor);
- splitContainer5.Size = new System.Drawing.Size(744, 388);
- splitContainer5.SplitterDistance = 244;
+ splitContainer5.Panel2.Controls.Add(groupColor);
+ splitContainer5.Panel2.Controls.Add(groupColorValue);
+ splitContainer5.Panel2.Controls.Add(groupSingleTile);
+ splitContainer5.Panel2.Controls.Add(groupBatch);
+ splitContainer5.Panel2.Controls.Add(groupFile);
+ splitContainer5.Size = new System.Drawing.Size(840, 600);
+ splitContainer5.SplitterDistance = 275;
splitContainer5.TabIndex = 1;
//
// splitContainer6
@@ -324,8 +319,8 @@ private void InitializeComponent()
// splitContainer6.Panel2
//
splitContainer6.Panel2.Controls.Add(pictureBoxArt);
- splitContainer6.Size = new System.Drawing.Size(244, 388);
- splitContainer6.SplitterDistance = 229;
+ splitContainer6.Size = new System.Drawing.Size(275, 600);
+ splitContainer6.SplitterDistance = 354;
splitContainer6.SplitterWidth = 5;
splitContainer6.TabIndex = 0;
//
@@ -338,7 +333,7 @@ private void InitializeComponent()
tabControl2.Margin = new System.Windows.Forms.Padding(4);
tabControl2.Name = "tabControl2";
tabControl2.SelectedIndex = 0;
- tabControl2.Size = new System.Drawing.Size(244, 229);
+ tabControl2.Size = new System.Drawing.Size(275, 354);
tabControl2.TabIndex = 0;
//
// tabPage3
@@ -348,7 +343,7 @@ private void InitializeComponent()
tabPage3.Margin = new System.Windows.Forms.Padding(4);
tabPage3.Name = "tabPage3";
tabPage3.Padding = new System.Windows.Forms.Padding(4);
- tabPage3.Size = new System.Drawing.Size(236, 201);
+ tabPage3.Size = new System.Drawing.Size(267, 326);
tabPage3.TabIndex = 0;
tabPage3.Text = "Items";
tabPage3.UseVisualStyleBackColor = true;
@@ -371,7 +366,7 @@ private void InitializeComponent()
// splitContainer1.Panel2
//
splitContainer1.Panel2.Controls.Add(tileViewItem);
- splitContainer1.Size = new System.Drawing.Size(228, 193);
+ splitContainer1.Size = new System.Drawing.Size(259, 318);
splitContainer1.SplitterDistance = 25;
splitContainer1.TabIndex = 2;
//
@@ -394,8 +389,8 @@ private void InitializeComponent()
splitContainer2.Panel2.Controls.Add(buttonSelectNoneItems);
splitContainer2.Panel2.Controls.Add(buttonSelectAllItems);
splitContainer2.Panel2MinSize = 170;
- splitContainer2.Size = new System.Drawing.Size(228, 25);
- splitContainer2.SplitterDistance = 54;
+ splitContainer2.Size = new System.Drawing.Size(259, 25);
+ splitContainer2.SplitterDistance = 85;
splitContainer2.TabIndex = 1;
//
// textFilterItems
@@ -404,7 +399,7 @@ private void InitializeComponent()
textFilterItems.Location = new System.Drawing.Point(0, 0);
textFilterItems.Name = "textFilterItems";
textFilterItems.PlaceholderText = "Filter";
- textFilterItems.Size = new System.Drawing.Size(54, 23);
+ textFilterItems.Size = new System.Drawing.Size(85, 23);
textFilterItems.TabIndex = 1;
textFilterItems.TextChanged += OnTextChangedFilterItems;
//
@@ -516,74 +511,118 @@ private void InitializeComponent()
buttonSelectNoneLand.UseVisualStyleBackColor = true;
buttonSelectNoneLand.Click += OnClickSelectNoneLand;
//
- // radioUseSelection
+ // groupColor
+ //
+ groupColor.Controls.Add(pictureBoxColor);
+ groupColor.Location = new System.Drawing.Point(4, 4);
+ groupColor.Name = "groupColor";
+ groupColor.Size = new System.Drawing.Size(230, 124);
+ groupColor.TabIndex = 0;
+ groupColor.TabStop = false;
+ groupColor.Text = "Color preview";
+ //
+ // groupColorValue
+ //
+ groupColorValue.Controls.Add(labelHex);
+ groupColorValue.Controls.Add(numericUpDownShortCol);
+ groupColorValue.Controls.Add(labelRgb);
+ groupColorValue.Controls.Add(numericUpDownR);
+ groupColorValue.Controls.Add(numericUpDownG);
+ groupColorValue.Controls.Add(numericUpDownB);
+ groupColorValue.Location = new System.Drawing.Point(240, 4);
+ groupColorValue.Name = "groupColorValue";
+ groupColorValue.Size = new System.Drawing.Size(252, 78);
+ groupColorValue.TabIndex = 1;
+ groupColorValue.TabStop = false;
+ groupColorValue.Text = "Color value";
+ //
+ // labelHex
+ //
+ labelHex.AutoSize = true;
+ labelHex.Location = new System.Drawing.Point(10, 25);
+ labelHex.Name = "labelHex";
+ labelHex.Size = new System.Drawing.Size(31, 15);
+ labelHex.TabIndex = 0;
+ labelHex.Text = "Hex:";
//
- radioUseSelection.AutoSize = true;
- radioUseSelection.Checked = true;
- radioUseSelection.Location = new System.Drawing.Point(220, 109);
- radioUseSelection.Name = "radioUseSelection";
- radioUseSelection.Size = new System.Drawing.Size(154, 19);
- radioUseSelection.TabIndex = 7;
- radioUseSelection.TabStop = true;
- radioUseSelection.Text = "Selection / Checked tiles";
- radioUseSelection.UseVisualStyleBackColor = true;
- radioUseSelection.CheckedChanged += OnCheckedChangeUseSelection;
+ // numericUpDownShortCol
//
- // radioUseRange
+ numericUpDownShortCol.Location = new System.Drawing.Point(50, 22);
+ numericUpDownShortCol.Margin = new System.Windows.Forms.Padding(4);
+ numericUpDownShortCol.Maximum = new decimal(new int[] { 32767, 0, 0, 0 });
+ numericUpDownShortCol.Name = "numericUpDownShortCol";
+ numericUpDownShortCol.Size = new System.Drawing.Size(116, 23);
+ numericUpDownShortCol.TabIndex = 3;
+ numericUpDownShortCol.ValueChanged += OnNumericShortColChanged;
//
- radioUseRange.AutoSize = true;
- radioUseRange.Location = new System.Drawing.Point(220, 135);
- radioUseRange.Name = "radioUseRange";
- radioUseRange.Size = new System.Drawing.Size(61, 19);
- radioUseRange.TabIndex = 8;
- radioUseRange.Text = "Range:";
- radioUseRange.UseVisualStyleBackColor = true;
- radioUseRange.CheckedChanged += OnCheckedChangeUseRange;
+ // labelRgb
//
- // label4
+ labelRgb.AutoSize = true;
+ labelRgb.Location = new System.Drawing.Point(10, 52);
+ labelRgb.Name = "labelRgb";
+ labelRgb.Size = new System.Drawing.Size(32, 15);
+ labelRgb.TabIndex = 4;
+ labelRgb.Text = "RGB:";
//
- label4.AutoSize = true;
- label4.Location = new System.Drawing.Point(356, 136);
- label4.Name = "label4";
- label4.Size = new System.Drawing.Size(16, 15);
- label4.TabIndex = 27;
- label4.Text = "...";
+ // numericUpDownR
//
- // buttonRangeToRangeAverage
+ numericUpDownR.Location = new System.Drawing.Point(50, 50);
+ numericUpDownR.Margin = new System.Windows.Forms.Padding(4);
+ numericUpDownR.Maximum = new decimal(new int[] { 255, 0, 0, 0 });
+ numericUpDownR.Name = "numericUpDownR";
+ numericUpDownR.Size = new System.Drawing.Size(55, 23);
+ numericUpDownR.TabIndex = 4;
+ numericUpDownR.ValueChanged += OnChangeR;
//
- buttonRangeToRangeAverage.Location = new System.Drawing.Point(220, 233);
- buttonRangeToRangeAverage.Name = "buttonRangeToRangeAverage";
- buttonRangeToRangeAverage.Size = new System.Drawing.Size(220, 26);
- buttonRangeToRangeAverage.TabIndex = 13;
- buttonRangeToRangeAverage.Text = "Selected tiles to selection average";
- buttonRangeToRangeAverage.UseVisualStyleBackColor = true;
- buttonRangeToRangeAverage.Click += OnClickRangeToRangeAverage;
+ // numericUpDownG
//
- // buttonRangeToIndividualAverage
+ numericUpDownG.Location = new System.Drawing.Point(115, 50);
+ numericUpDownG.Margin = new System.Windows.Forms.Padding(4);
+ numericUpDownG.Maximum = new decimal(new int[] { 255, 0, 0, 0 });
+ numericUpDownG.Name = "numericUpDownG";
+ numericUpDownG.Size = new System.Drawing.Size(55, 23);
+ numericUpDownG.TabIndex = 5;
+ numericUpDownG.ValueChanged += OnChangeG;
//
- buttonRangeToIndividualAverage.Location = new System.Drawing.Point(220, 198);
- buttonRangeToIndividualAverage.Name = "buttonRangeToIndividualAverage";
- buttonRangeToIndividualAverage.Size = new System.Drawing.Size(220, 26);
- buttonRangeToIndividualAverage.TabIndex = 12;
- buttonRangeToIndividualAverage.Text = "Selected tiles to individual average";
- buttonRangeToIndividualAverage.UseVisualStyleBackColor = true;
- buttonRangeToIndividualAverage.Click += OnClickRangeToIndividualAverage;
+ // numericUpDownB
//
- // buttonRevertAll
+ numericUpDownB.Location = new System.Drawing.Point(180, 50);
+ numericUpDownB.Margin = new System.Windows.Forms.Padding(4);
+ numericUpDownB.Maximum = new decimal(new int[] { 255, 0, 0, 0 });
+ numericUpDownB.Name = "numericUpDownB";
+ numericUpDownB.Size = new System.Drawing.Size(55, 23);
+ numericUpDownB.TabIndex = 6;
+ numericUpDownB.ValueChanged += OnChangeB;
//
- buttonRevertAll.Enabled = false;
- buttonRevertAll.Location = new System.Drawing.Point(4, 294);
- buttonRevertAll.Name = "buttonRevertAll";
- buttonRevertAll.Size = new System.Drawing.Size(88, 26);
- buttonRevertAll.TabIndex = 14;
- buttonRevertAll.Text = "Revert All";
- buttonRevertAll.UseVisualStyleBackColor = true;
- buttonRevertAll.Click += OnClickRevertAll;
+ // groupSingleTile
+ //
+ groupSingleTile.Controls.Add(buttonMean);
+ groupSingleTile.Controls.Add(buttonRevert);
+ groupSingleTile.Controls.Add(comboMeanStrategy);
+ groupSingleTile.Controls.Add(buttonStrategyHelp);
+ groupSingleTile.Controls.Add(buttonSaveColor);
+ groupSingleTile.Location = new System.Drawing.Point(4, 132);
+ groupSingleTile.Name = "groupSingleTile";
+ groupSingleTile.Size = new System.Drawing.Size(230, 130);
+ groupSingleTile.TabIndex = 2;
+ groupSingleTile.TabStop = false;
+ groupSingleTile.Text = "Single tile";
+ //
+ // buttonMean
+ //
+ buttonMean.Location = new System.Drawing.Point(10, 22);
+ buttonMean.Margin = new System.Windows.Forms.Padding(4);
+ buttonMean.Name = "buttonMean";
+ buttonMean.Size = new System.Drawing.Size(88, 26);
+ buttonMean.TabIndex = 0;
+ buttonMean.Text = "Average Color";
+ buttonMean.UseVisualStyleBackColor = true;
+ buttonMean.Click += OnClickMeanColor;
//
// buttonRevert
//
buttonRevert.Enabled = false;
- buttonRevert.Location = new System.Drawing.Point(97, 128);
+ buttonRevert.Location = new System.Drawing.Point(104, 22);
buttonRevert.Name = "buttonRevert";
buttonRevert.Size = new System.Drawing.Size(88, 26);
buttonRevert.TabIndex = 1;
@@ -591,99 +630,108 @@ private void InitializeComponent()
buttonRevert.UseVisualStyleBackColor = true;
buttonRevert.Click += OnClickRevert;
//
- // label2
- //
- label2.AutoSize = true;
- label2.Location = new System.Drawing.Point(226, 364);
- label2.Name = "label2";
- label2.Size = new System.Drawing.Size(62, 15);
- label2.TabIndex = 21;
- label2.Text = "Land Tiles:";
- //
- // label1
- //
- label1.AutoSize = true;
- label1.Location = new System.Drawing.Point(255, 337);
- label1.Name = "label1";
- label1.Size = new System.Drawing.Size(39, 15);
- label1.TabIndex = 20;
- label1.Text = "Items:";
- //
- // progressBar2
+ // comboMeanStrategy
+ //
+ comboMeanStrategy.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ comboMeanStrategy.FormattingEnabled = true;
+ comboMeanStrategy.Location = new System.Drawing.Point(10, 56);
+ comboMeanStrategy.Margin = new System.Windows.Forms.Padding(4);
+ comboMeanStrategy.Name = "comboMeanStrategy";
+ comboMeanStrategy.Size = new System.Drawing.Size(182, 23);
+ comboMeanStrategy.TabIndex = 19;
+ comboMeanStrategy.TabStop = false;
+ comboMeanStrategy.SelectedIndexChanged += OnSelectedMeanStrategyChanged;
+ //
+ // buttonStrategyHelp
+ //
+ buttonStrategyHelp.Location = new System.Drawing.Point(196, 56);
+ buttonStrategyHelp.Margin = new System.Windows.Forms.Padding(0);
+ buttonStrategyHelp.Name = "buttonStrategyHelp";
+ buttonStrategyHelp.Size = new System.Drawing.Size(24, 23);
+ buttonStrategyHelp.TabIndex = 21;
+ buttonStrategyHelp.TabStop = false;
+ buttonStrategyHelp.Text = "?";
+ toolTip.SetToolTip(buttonStrategyHelp, "About these averaging strategies");
+ buttonStrategyHelp.UseVisualStyleBackColor = true;
+ buttonStrategyHelp.Click += OnClickStrategyHelp;
+ //
+ // buttonSaveColor
+ //
+ buttonSaveColor.Location = new System.Drawing.Point(10, 90);
+ buttonSaveColor.Margin = new System.Windows.Forms.Padding(4);
+ buttonSaveColor.Name = "buttonSaveColor";
+ buttonSaveColor.Size = new System.Drawing.Size(210, 26);
+ buttonSaveColor.TabIndex = 2;
+ buttonSaveColor.Text = "Save Color";
+ buttonSaveColor.UseVisualStyleBackColor = true;
+ buttonSaveColor.Click += OnClickSaveColor;
+ //
+ // groupBatch
+ //
+ groupBatch.Controls.Add(radioUseSelection);
+ groupBatch.Controls.Add(radioUseRange);
+ groupBatch.Controls.Add(textBoxMeanFrom);
+ groupBatch.Controls.Add(label4);
+ groupBatch.Controls.Add(textBoxMeanTo);
+ groupBatch.Controls.Add(buttonCurrentToRangeAverage);
+ groupBatch.Controls.Add(buttonRangeToIndividualAverage);
+ groupBatch.Controls.Add(buttonRangeToRangeAverage);
+ groupBatch.Location = new System.Drawing.Point(240, 86);
+ groupBatch.Name = "groupBatch";
+ groupBatch.Size = new System.Drawing.Size(252, 176);
+ groupBatch.TabIndex = 3;
+ groupBatch.TabStop = false;
+ groupBatch.Text = "Batch";
//
- progressBar2.Location = new System.Drawing.Point(309, 359);
- progressBar2.Name = "progressBar2";
- progressBar2.Size = new System.Drawing.Size(117, 22);
- progressBar2.TabIndex = 19;
- //
- // progressBar1
- //
- progressBar1.Location = new System.Drawing.Point(309, 331);
- progressBar1.Name = "progressBar1";
- progressBar1.Size = new System.Drawing.Size(117, 22);
- progressBar1.TabIndex = 18;
+ // radioUseSelection
//
- // button6
- //
- button6.AutoSize = true;
- button6.Location = new System.Drawing.Point(220, 294);
- button6.Margin = new System.Windows.Forms.Padding(4);
- button6.Name = "button6";
- button6.Size = new System.Drawing.Size(206, 31);
- button6.TabIndex = 18;
- button6.TabStop = false;
- button6.Text = "Average All (Items and Land Tiles)";
- button6.UseVisualStyleBackColor = true;
- button6.Click += OnClickMeanColorAll;
- //
- // button5
- //
- button5.Location = new System.Drawing.Point(97, 329);
- button5.Margin = new System.Windows.Forms.Padding(4);
- button5.Name = "button5";
- button5.Size = new System.Drawing.Size(88, 26);
- button5.TabIndex = 17;
- button5.Text = "Import..";
- button5.UseVisualStyleBackColor = true;
- button5.Click += OnClickImport;
- //
- // button4
- //
- button4.Location = new System.Drawing.Point(3, 329);
- button4.Margin = new System.Windows.Forms.Padding(4);
- button4.Name = "button4";
- button4.Size = new System.Drawing.Size(88, 26);
- button4.TabIndex = 16;
- button4.Text = "Export..";
- button4.UseVisualStyleBackColor = true;
- button4.Click += OnClickExport;
+ radioUseSelection.AutoSize = true;
+ radioUseSelection.Checked = true;
+ radioUseSelection.Location = new System.Drawing.Point(10, 22);
+ radioUseSelection.Name = "radioUseSelection";
+ radioUseSelection.Size = new System.Drawing.Size(154, 19);
+ radioUseSelection.TabIndex = 7;
+ radioUseSelection.TabStop = true;
+ radioUseSelection.Text = "Selection / Checked tiles";
+ radioUseSelection.UseVisualStyleBackColor = true;
+ radioUseSelection.CheckedChanged += OnCheckedChangeUseSelection;
//
- // numericUpDownShortCol
+ // radioUseRange
//
- numericUpDownShortCol.Location = new System.Drawing.Point(220, 11);
- numericUpDownShortCol.Margin = new System.Windows.Forms.Padding(4);
- numericUpDownShortCol.Maximum = new decimal(new int[] { 32767, 0, 0, 0 });
- numericUpDownShortCol.Name = "numericUpDownShortCol";
- numericUpDownShortCol.Size = new System.Drawing.Size(116, 23);
- numericUpDownShortCol.TabIndex = 3;
- numericUpDownShortCol.ValueChanged += OnNumericShortColChanged;
+ radioUseRange.AutoSize = true;
+ radioUseRange.Location = new System.Drawing.Point(10, 50);
+ radioUseRange.Name = "radioUseRange";
+ radioUseRange.Size = new System.Drawing.Size(61, 19);
+ radioUseRange.TabIndex = 8;
+ radioUseRange.Text = "Range:";
+ radioUseRange.UseVisualStyleBackColor = true;
+ radioUseRange.CheckedChanged += OnCheckedChangeUseRange;
//
// textBoxMeanFrom
//
textBoxMeanFrom.Enabled = false;
textBoxMeanFrom.ForeColor = System.Drawing.SystemColors.WindowText;
- textBoxMeanFrom.Location = new System.Drawing.Point(290, 133);
+ textBoxMeanFrom.Location = new System.Drawing.Point(80, 48);
textBoxMeanFrom.Margin = new System.Windows.Forms.Padding(4);
textBoxMeanFrom.Name = "textBoxMeanFrom";
textBoxMeanFrom.PlaceholderText = "from";
textBoxMeanFrom.Size = new System.Drawing.Size(60, 23);
textBoxMeanFrom.TabIndex = 9;
//
+ // label4
+ //
+ label4.AutoSize = true;
+ label4.Location = new System.Drawing.Point(145, 53);
+ label4.Name = "label4";
+ label4.Size = new System.Drawing.Size(17, 15);
+ label4.TabIndex = 27;
+ label4.Text = "→";
+ //
// textBoxMeanTo
//
textBoxMeanTo.Enabled = false;
textBoxMeanTo.ForeColor = System.Drawing.SystemColors.WindowText;
- textBoxMeanTo.Location = new System.Drawing.Point(380, 134);
+ textBoxMeanTo.Location = new System.Drawing.Point(165, 48);
textBoxMeanTo.Margin = new System.Windows.Forms.Padding(4);
textBoxMeanTo.Name = "textBoxMeanTo";
textBoxMeanTo.PlaceholderText = "to";
@@ -692,97 +740,158 @@ private void InitializeComponent()
//
// buttonCurrentToRangeAverage
//
- buttonCurrentToRangeAverage.AutoSize = true;
- buttonCurrentToRangeAverage.Location = new System.Drawing.Point(220, 163);
+ buttonCurrentToRangeAverage.Location = new System.Drawing.Point(10, 76);
buttonCurrentToRangeAverage.Margin = new System.Windows.Forms.Padding(4);
buttonCurrentToRangeAverage.Name = "buttonCurrentToRangeAverage";
- buttonCurrentToRangeAverage.Size = new System.Drawing.Size(220, 26);
+ buttonCurrentToRangeAverage.Size = new System.Drawing.Size(232, 26);
buttonCurrentToRangeAverage.TabIndex = 11;
buttonCurrentToRangeAverage.Text = "Current tile to selection average";
buttonCurrentToRangeAverage.UseVisualStyleBackColor = true;
buttonCurrentToRangeAverage.Click += OnClickCurrentToRangeAverage;
//
- // numericUpDownB
+ // buttonRangeToIndividualAverage
//
- numericUpDownB.Location = new System.Drawing.Point(344, 41);
- numericUpDownB.Margin = new System.Windows.Forms.Padding(4);
- numericUpDownB.Maximum = new decimal(new int[] { 255, 0, 0, 0 });
- numericUpDownB.Name = "numericUpDownB";
- numericUpDownB.Size = new System.Drawing.Size(55, 23);
- numericUpDownB.TabIndex = 6;
- numericUpDownB.ValueChanged += OnChangeB;
+ buttonRangeToIndividualAverage.Location = new System.Drawing.Point(10, 108);
+ buttonRangeToIndividualAverage.Name = "buttonRangeToIndividualAverage";
+ buttonRangeToIndividualAverage.Size = new System.Drawing.Size(232, 26);
+ buttonRangeToIndividualAverage.TabIndex = 12;
+ buttonRangeToIndividualAverage.Text = "Selected tiles to individual average";
+ buttonRangeToIndividualAverage.UseVisualStyleBackColor = true;
+ buttonRangeToIndividualAverage.Click += OnClickRangeToIndividualAverage;
//
- // numericUpDownG
+ // buttonRangeToRangeAverage
//
- numericUpDownG.Location = new System.Drawing.Point(283, 41);
- numericUpDownG.Margin = new System.Windows.Forms.Padding(4);
- numericUpDownG.Maximum = new decimal(new int[] { 255, 0, 0, 0 });
- numericUpDownG.Name = "numericUpDownG";
- numericUpDownG.Size = new System.Drawing.Size(55, 23);
- numericUpDownG.TabIndex = 5;
- numericUpDownG.ValueChanged += OnChangeG;
+ buttonRangeToRangeAverage.Location = new System.Drawing.Point(10, 140);
+ buttonRangeToRangeAverage.Name = "buttonRangeToRangeAverage";
+ buttonRangeToRangeAverage.Size = new System.Drawing.Size(232, 26);
+ buttonRangeToRangeAverage.TabIndex = 13;
+ buttonRangeToRangeAverage.Text = "Selected tiles to selection average";
+ buttonRangeToRangeAverage.UseVisualStyleBackColor = true;
+ buttonRangeToRangeAverage.Click += OnClickRangeToRangeAverage;
//
- // numericUpDownR
+ // groupFile
+ //
+ groupFile.Controls.Add(buttonSaveFile);
+ groupFile.Controls.Add(buttonRevertAll);
+ groupFile.Controls.Add(buttonExport);
+ groupFile.Controls.Add(buttonImport);
+ groupFile.Controls.Add(buttonAverageAll);
+ groupFile.Controls.Add(label1);
+ groupFile.Controls.Add(progressBar1);
+ groupFile.Controls.Add(label2);
+ groupFile.Controls.Add(progressBar2);
+ groupFile.Location = new System.Drawing.Point(4, 268);
+ groupFile.Name = "groupFile";
+ groupFile.Size = new System.Drawing.Size(488, 90);
+ groupFile.TabIndex = 4;
+ groupFile.TabStop = false;
+ groupFile.Text = "File";
+ //
+ // buttonSaveFile
+ //
+ buttonSaveFile.Location = new System.Drawing.Point(10, 22);
+ buttonSaveFile.Margin = new System.Windows.Forms.Padding(4);
+ buttonSaveFile.Name = "buttonSaveFile";
+ buttonSaveFile.Size = new System.Drawing.Size(100, 26);
+ buttonSaveFile.TabIndex = 15;
+ buttonSaveFile.Text = "Save File";
+ buttonSaveFile.UseVisualStyleBackColor = true;
+ buttonSaveFile.Click += OnClickSaveFile;
//
- numericUpDownR.Location = new System.Drawing.Point(220, 41);
- numericUpDownR.Margin = new System.Windows.Forms.Padding(4);
- numericUpDownR.Maximum = new decimal(new int[] { 255, 0, 0, 0 });
- numericUpDownR.Name = "numericUpDownR";
- numericUpDownR.Size = new System.Drawing.Size(55, 23);
- numericUpDownR.TabIndex = 4;
- numericUpDownR.ValueChanged += OnChangeR;
+ // buttonRevertAll
//
- // button2
+ buttonRevertAll.Enabled = false;
+ buttonRevertAll.Location = new System.Drawing.Point(117, 22);
+ buttonRevertAll.Name = "buttonRevertAll";
+ buttonRevertAll.Size = new System.Drawing.Size(100, 26);
+ buttonRevertAll.TabIndex = 14;
+ buttonRevertAll.Text = "Revert All";
+ buttonRevertAll.UseVisualStyleBackColor = true;
+ buttonRevertAll.Click += OnClickRevertAll;
//
- button2.Location = new System.Drawing.Point(97, 294);
- button2.Margin = new System.Windows.Forms.Padding(4);
- button2.Name = "button2";
- button2.Size = new System.Drawing.Size(88, 26);
- button2.TabIndex = 15;
- button2.Text = "Save File";
- button2.UseVisualStyleBackColor = true;
- button2.Click += OnClickSaveFile;
+ // buttonExport
+ //
+ buttonExport.Location = new System.Drawing.Point(224, 22);
+ buttonExport.Margin = new System.Windows.Forms.Padding(4);
+ buttonExport.Name = "buttonExport";
+ buttonExport.Size = new System.Drawing.Size(100, 26);
+ buttonExport.TabIndex = 16;
+ buttonExport.Text = "Export..";
+ buttonExport.UseVisualStyleBackColor = true;
+ buttonExport.Click += OnClickExport;
+ //
+ // buttonImport
+ //
+ buttonImport.Location = new System.Drawing.Point(332, 22);
+ buttonImport.Margin = new System.Windows.Forms.Padding(4);
+ buttonImport.Name = "buttonImport";
+ buttonImport.Size = new System.Drawing.Size(100, 26);
+ buttonImport.TabIndex = 17;
+ buttonImport.Text = "Import..";
+ buttonImport.UseVisualStyleBackColor = true;
+ buttonImport.Click += OnClickImport;
+ //
+ // buttonAverageAll
+ //
+ buttonAverageAll.Location = new System.Drawing.Point(11, 55);
+ buttonAverageAll.Margin = new System.Windows.Forms.Padding(4);
+ buttonAverageAll.Name = "buttonAverageAll";
+ buttonAverageAll.Size = new System.Drawing.Size(160, 26);
+ buttonAverageAll.TabIndex = 18;
+ buttonAverageAll.TabStop = false;
+ buttonAverageAll.Text = "Auto-fill empty entries";
+ buttonAverageAll.UseVisualStyleBackColor = true;
+ buttonAverageAll.Click += OnClickMeanColorAll;
//
- // button1
+ // label1
//
- button1.Location = new System.Drawing.Point(3, 163);
- button1.Margin = new System.Windows.Forms.Padding(4);
- button1.Name = "button1";
- button1.Size = new System.Drawing.Size(88, 26);
- button1.TabIndex = 2;
- button1.Text = "Save Color";
- button1.UseVisualStyleBackColor = true;
- button1.Click += OnClickSaveColor;
+ label1.AutoSize = true;
+ label1.Location = new System.Drawing.Point(178, 57);
+ label1.Name = "label1";
+ label1.Size = new System.Drawing.Size(39, 15);
+ label1.TabIndex = 20;
+ label1.Text = "Items:";
//
- // buttonMean
+ // progressBar1
//
- buttonMean.Location = new System.Drawing.Point(4, 128);
- buttonMean.Margin = new System.Windows.Forms.Padding(4);
- buttonMean.Name = "buttonMean";
- buttonMean.Size = new System.Drawing.Size(88, 26);
- buttonMean.TabIndex = 0;
- buttonMean.Text = "Average Color";
- buttonMean.UseVisualStyleBackColor = true;
- buttonMean.Click += OnClickMeanColor;
+ progressBar1.Location = new System.Drawing.Point(222, 55);
+ progressBar1.Name = "progressBar1";
+ progressBar1.Size = new System.Drawing.Size(125, 22);
+ progressBar1.TabIndex = 18;
//
- // RadarColorControl
+ // label2
+ //
+ label2.AutoSize = true;
+ label2.Location = new System.Drawing.Point(353, 57);
+ label2.Name = "label2";
+ label2.Size = new System.Drawing.Size(36, 15);
+ label2.TabIndex = 21;
+ label2.Text = "Land:";
+ //
+ // progressBar2
//
+ progressBar2.Location = new System.Drawing.Point(393, 55);
+ progressBar2.Name = "progressBar2";
+ progressBar2.Size = new System.Drawing.Size(85, 22);
+ progressBar2.TabIndex = 19;
+ //
+ // RadarColorControl
+ //
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
Controls.Add(splitContainer5);
DoubleBuffered = true;
Margin = new System.Windows.Forms.Padding(4);
Name = "RadarColorControl";
- Size = new System.Drawing.Size(744, 388);
+ Size = new System.Drawing.Size(840, 600);
Load += OnLoad;
contextMenuStrip1.ResumeLayout(false);
contextMenuStrip2.ResumeLayout(false);
- PictureBoxContextMenuStrip.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)pictureBoxArt).EndInit();
+ PictureBoxContextMenuStrip.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)pictureBoxColor).EndInit();
splitContainer5.Panel1.ResumeLayout(false);
splitContainer5.Panel2.ResumeLayout(false);
- splitContainer5.Panel2.PerformLayout();
((System.ComponentModel.ISupportInitialize)splitContainer5).EndInit();
splitContainer5.ResumeLayout(false);
splitContainer6.Panel1.ResumeLayout(false);
@@ -810,21 +919,32 @@ private void InitializeComponent()
splitContainer4.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer4).EndInit();
splitContainer4.ResumeLayout(false);
+ groupColor.ResumeLayout(false);
+ groupColorValue.ResumeLayout(false);
+ groupColorValue.PerformLayout();
((System.ComponentModel.ISupportInitialize)numericUpDownShortCol).EndInit();
- ((System.ComponentModel.ISupportInitialize)numericUpDownB).EndInit();
- ((System.ComponentModel.ISupportInitialize)numericUpDownG).EndInit();
((System.ComponentModel.ISupportInitialize)numericUpDownR).EndInit();
+ ((System.ComponentModel.ISupportInitialize)numericUpDownG).EndInit();
+ ((System.ComponentModel.ISupportInitialize)numericUpDownB).EndInit();
+ groupSingleTile.ResumeLayout(false);
+ groupBatch.ResumeLayout(false);
+ groupBatch.PerformLayout();
+ groupFile.ResumeLayout(false);
+ groupFile.PerformLayout();
ResumeLayout(false);
}
#endregion
- private System.Windows.Forms.Button button1;
- private System.Windows.Forms.Button button2;
+ private System.Windows.Forms.Button buttonSaveColor;
+ private System.Windows.Forms.Button buttonSaveFile;
private System.Windows.Forms.Button buttonCurrentToRangeAverage;
- private System.Windows.Forms.Button button4;
- private System.Windows.Forms.Button button5;
+ private System.Windows.Forms.Button buttonExport;
+ private System.Windows.Forms.Button buttonImport;
private System.Windows.Forms.Button buttonMean;
+ private System.Windows.Forms.ComboBox comboMeanStrategy;
+ private System.Windows.Forms.Button buttonStrategyHelp;
+ private System.Windows.Forms.ToolTip toolTip;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip2;
private System.Windows.Forms.ContextMenuStrip PictureBoxContextMenuStrip;
@@ -849,7 +969,7 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem2;
private UoFiddler.Controls.UserControls.TileView.TileViewControl tileViewItem;
private UoFiddler.Controls.UserControls.TileView.TileViewControl tileViewLand;
- private System.Windows.Forms.Button button6;
+ private System.Windows.Forms.Button buttonAverageAll;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label1;
@@ -875,5 +995,12 @@ private void InitializeComponent()
private System.Windows.Forms.Button buttonSelectAllLand;
private System.Windows.Forms.RadioButton radioUseSelection;
private System.Windows.Forms.RadioButton radioUseRange;
+ private System.Windows.Forms.GroupBox groupColor;
+ private System.Windows.Forms.GroupBox groupColorValue;
+ private System.Windows.Forms.GroupBox groupSingleTile;
+ private System.Windows.Forms.GroupBox groupBatch;
+ private System.Windows.Forms.GroupBox groupFile;
+ private System.Windows.Forms.Label labelHex;
+ private System.Windows.Forms.Label labelRgb;
}
}
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.cs b/UoFiddler.Controls/UserControls/RadarColorControl.cs
index 0fec203..83e6d2c 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.cs
@@ -42,6 +42,23 @@ public RadarColorControl()
ConfigureTileView(tileViewLand);
tileViewItem.SelectedIndices.CollectionChanged += OnItemSelectedIndicesChanged;
tileViewLand.SelectedIndices.CollectionChanged += OnLandSelectedIndicesChanged;
+
+#if DEBUG
+ // Dev-only research harness. Created programmatically (rather than via the
+ // designer) so the WinForms designer can't re-serialize it without the #if
+ // guard and break Release builds.
+ var benchmark = new Button
+ {
+ Location = new Point(4, 365),
+ Size = new Size(488, 24),
+ Margin = new Padding(4),
+ Text = "Algorithm benchmark (CSV)",
+ TabStop = false,
+ UseVisualStyleBackColor = true,
+ };
+ benchmark.Click += OnClickAlgorithmBenchmark;
+ splitContainer5.Panel2.Controls.Add(benchmark);
+#endif
}
private int _selectedIndex = -1;
@@ -93,6 +110,10 @@ private static void ConfigureTileView(TileView.TileViewControl tv)
tv.TileMargin = new Padding(0);
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
+ // Suppress the default focus-rectangle (DarkRed 1px outline). DrawRow already
+ // renders a SystemBrushes.Highlight fill for the focused row, so the extra
+ // border just adds a red line at the row edges.
+ tv.TileFocusColor = Color.Transparent;
}
private void OnTileViewSizeChanged(object sender, EventArgs e)
@@ -489,11 +510,225 @@ public void OnLoad(object sender, EventArgs e)
ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
pictureBoxArt.BackColor = Options.PreviewBackgroundColor;
+
+ PopulateMeanStrategyCombo();
}
IsLoaded = true;
}
+ private void PopulateMeanStrategyCombo()
+ {
+ if (comboMeanStrategy.Items.Count > 0)
+ {
+ return;
+ }
+
+ comboMeanStrategy.BeginUpdate();
+ foreach (RadarAveragingStrategy s in RadarColorAveraging.All)
+ {
+ comboMeanStrategy.Items.Add(new MeanStrategyItem(s));
+ }
+ // Select the persisted/runtime strategy.
+ for (int i = 0; i < comboMeanStrategy.Items.Count; ++i)
+ {
+ if (((MeanStrategyItem)comboMeanStrategy.Items[i]).Strategy == Options.RadarColorStrategy)
+ {
+ comboMeanStrategy.SelectedIndex = i;
+ break;
+ }
+ }
+ if (comboMeanStrategy.SelectedIndex < 0)
+ {
+ comboMeanStrategy.SelectedIndex = 0;
+ }
+ comboMeanStrategy.EndUpdate();
+ }
+
+ private sealed class MeanStrategyItem
+ {
+ public RadarAveragingStrategy Strategy { get; }
+ public MeanStrategyItem(RadarAveragingStrategy s) { Strategy = s; }
+ public override string ToString() => RadarColorAveraging.DisplayName(Strategy);
+ }
+
+ private RadarAveragingStrategy CurrentStrategy =>
+ comboMeanStrategy.SelectedItem is MeanStrategyItem item ? item.Strategy : Options.RadarColorStrategy;
+
+ private void OnSelectedMeanStrategyChanged(object sender, EventArgs e)
+ {
+ if (comboMeanStrategy.SelectedItem is MeanStrategyItem item)
+ {
+ Options.RadarColorStrategy = item.Strategy;
+ }
+ }
+
+ private void OnClickStrategyHelp(object sender, EventArgs e)
+ {
+ using var dlg = new StrategyHelpForm();
+ dlg.ShowDialog(FindForm());
+ }
+
+ // Modal explainer for the averaging strategies. Read-only TextBox so users can
+ // copy/paste text out of it. Content comes from a static string so it lives next
+ // to the code that defines the strategies.
+ private sealed class StrategyHelpForm : Form
+ {
+ public StrategyHelpForm()
+ {
+ Text = "Radar color — averaging strategies";
+ FormBorderStyle = FormBorderStyle.Sizable;
+ StartPosition = FormStartPosition.CenterParent;
+ MinimumSize = new Size(560, 400);
+ ClientSize = new Size(640, 540);
+ ShowInTaskbar = false;
+ MinimizeBox = false;
+ MaximizeBox = true;
+
+ var text = new TextBox
+ {
+ Multiline = true,
+ ReadOnly = true,
+ ScrollBars = ScrollBars.Vertical,
+ WordWrap = true,
+ Dock = DockStyle.Fill,
+ Font = new Font(FontFamily.GenericSansSerif, 9f),
+ Text = HelpText,
+ TabStop = false,
+ };
+ var ok = new Button
+ {
+ Text = "Close",
+ DialogResult = DialogResult.OK,
+ Dock = DockStyle.Bottom,
+ Height = 32,
+ };
+ Controls.Add(text);
+ Controls.Add(ok);
+ AcceptButton = ok;
+ CancelButton = ok;
+ // Route initial focus to the Close button so the read-only TextBox
+ // doesn't auto-select its entire content when the dialog opens.
+ ActiveControl = ok;
+ }
+
+ // Defensive: if focus ever lands on the TextBox (e.g. user clicks into it),
+ // collapse the selection to the start instead of leaving everything selected.
+ protected override void OnShown(EventArgs e)
+ {
+ base.OnShown(e);
+ foreach (Control c in Controls)
+ {
+ if (c is TextBox tb)
+ {
+ tb.SelectionStart = 0;
+ tb.SelectionLength = 0;
+ break;
+ }
+ }
+ }
+
+ private const string HelpText =
+ "Radar color averaging strategies\r\n" +
+ "================================\r\n\r\n" +
+ "Each entry in radarcol.mul is a 16-bit (RGB555) color used to render the world map. " +
+ "When you click \"Average Color\", UOFiddler computes that color from the tile's pixels. " +
+ "There are several ways to compute the average; they differ in rounding, source-pixel " +
+ "selection, and the color space used.\r\n\r\n" +
+
+ "What we learned by benchmarking\r\n" +
+ "-------------------------------\r\n" +
+ "Each strategy was scored against the values in radarcol.mul. The findings:\r\n\r\n" +
+ " - The 24->15 bit downscale that produced the file's values uses bit-shift (>>3), not " +
+ "the *31/255 rounding that older UOFiddler builds used.\r\n\r\n" +
+ " - For ITEMS (statics): values are reproducible by a per-channel arithmetic mean of " +
+ "the tile's 5-bit pixels with round-half-up. \"Mean (5-bit, banker's round)\" matches " +
+ "the file byte-for-byte on ~96.6% of 13,771 item entries; the remainder is " +
+ "sub-1-step error.\r\n\r\n" +
+ " - For LAND tiles: 4,239 active entries use only ~103 unique colors total. That is " +
+ "a hand-tuned terrain palette, not a computed result. No pixel-averaging algorithm " +
+ "matches more than ~10% of land entries. \"Snap to land palette\" picks the closest " +
+ "entry from the colors already in the file, preserving terrain coherence.\r\n\r\n" +
+
+ "Gold standard (recommended defaults)\r\n" +
+ "------------------------------------\r\n" +
+ " - Items tab -> Mean (5-bit, banker's round) [the actual default]\r\n" +
+ " - Land tab -> Snap to land palette\r\n\r\n" +
+ "Switch the dropdown manually when changing tabs; selection persists in this session.\r\n\r\n" +
+
+ "Strategies in detail\r\n" +
+ "--------------------\r\n\r\n" +
+
+ "Mean (5-bit)\r\n" +
+ " Extract 5-bit R/G/B per non-transparent pixel, sum, divide by count with " +
+ "truncation. Simple but biases dark by ~half a step per channel. ~13% match on items.\r\n\r\n" +
+
+ "Mean (5-bit, rounded)\r\n" +
+ " Same as Mean (5-bit) but uses round-half-up ((sum + n/2) / n). 96.4% match on items.\r\n\r\n" +
+
+ "Mean (5-bit, banker's round) [DEFAULT]\r\n" +
+ " Same but with round-half-to-even tie-break. The empirical winner: 96.6% match on items.\r\n\r\n" +
+
+ "Mean (5-bit, rounded, incl. transparent)\r\n" +
+ " Includes transparent pixels (value 0) in the divisor. Drags the average toward 0; " +
+ "useful only as a diagnostic.\r\n\r\n" +
+
+ "Mean (8-bit, >>3 pack)\r\n" +
+ " Expands each 5-bit pixel to 8-bit (via (c<<3)|(c>>2)), averages in 8-bit space, then " +
+ "packs back to 555 with bit-shift. Halfway result: ~42% match on items.\r\n\r\n" +
+
+ "Mean (8-bit, rounded pack)\r\n" +
+ " Same as Mean (8-bit) but packs with ((c*31+127)/255). ~69% match.\r\n\r\n" +
+
+ "Mean (5-bit, rounded, no outline)\r\n" +
+ " Drops near-black pixels (5-bit Y < 2) before averaging. Tests the common 90s sprite " +
+ "trick of excluding outlines. Worse than the rounded mean in practice (~44%).\r\n\r\n" +
+
+ "Mean (linear-light)\r\n" +
+ " Gamma-corrects sRGB to linear (x*x), averages, then back to sRGB. A control candidate; " +
+ "1997 art pipelines almost certainly weren't gamma-aware. Low match (~11%).\r\n\r\n" +
+
+ "Mode (dominant pixel)\r\n" +
+ " Returns the single most common non-transparent pixel. Useful for very uniform tiles, " +
+ "noisy elsewhere. ~5% match.\r\n\r\n" +
+
+ "Median per channel\r\n" +
+ " Independent per-channel median in 5-bit space. Robust to outliers but doesn't match " +
+ "the file's values (~15%).\r\n\r\n" +
+
+ "Mean (no outline)\r\n" +
+ " Plain 5-bit truncated mean with outline rejection. (~19%.)\r\n\r\n" +
+
+ "Snap to land palette\r\n" +
+ " Computes the banker's-rounded 5-bit mean, then snaps the result to whichever of the " +
+ "~103 land colors already in radarcol.mul is closest in 5-bit Euclidean distance. The " +
+ "right choice for new/edited land tiles when you want them to look like neighbours " +
+ "instead of producing a freeform color.\r\n\r\n" +
+
+ "Snap to item palette\r\n" +
+ " Analogous to Snap to land palette but uses the ~2,200 unique item colors. Less " +
+ "useful for items (their values are genuinely computed); kept for symmetry.\r\n\r\n" +
+
+ "Legacy (UOFiddler)\r\n" +
+ " Reproduces UOFiddler's earlier behavior bit-for-bit: average in *31/255-truncated " +
+ "8-bit space, repack with the same truncation. Matches only ~1.9% of file values. Kept " +
+ "for continuity with older versions of UOFiddler.\r\n\r\n" +
+
+ "Transparency and clamps\r\n" +
+ "-----------------------\r\n" +
+ "All strategies (except \"incl. transparent\") skip pixels equal to 0 (transparent). " +
+ "The clamp rule is applied on the output: if all components downscale to 0 but the " +
+ "input was non-zero, force the lane to 1. Pure black opaque pixels do not survive a " +
+ "24->15 bit downscale; they appear as transparent both at runtime and in our " +
+ "averaging.\r\n\r\n" +
+
+ "Tools\r\n" +
+ "-----\r\n" +
+ " - In Debug builds, the \"Algorithm benchmark (CSV)\" button on this control runs " +
+ "every strategy against the loaded radarcol.mul and writes a per-strategy report to " +
+ "Options.OutputPath\\radarcol_eval.csv.\r\n";
+ }
+
private void OnFilePathChangeEvent()
{
Reload();
@@ -597,7 +832,7 @@ private void OnClickMeanColor(object sender, EventArgs e)
return;
}
- CurrentColor = HueHelpers.ColorToHue(AverageColorFrom(image));
+ CurrentColor = RadarColorAveraging.Compute(image, CurrentStrategy);
}
private void OnClickSaveFile(object sender, EventArgs e)
@@ -854,68 +1089,23 @@ private IEnumerable GetValidSequence()
private ushort GetSequenceAverage(IEnumerable sequence)
{
- int gmeanr = 0;
- int gmeang = 0;
- int gmeanb = 0;
-
- foreach (int i in sequence)
+ // Pool pixels across all tiles in the sequence and run the chosen strategy once.
+ // The previous implementation averaged per-tile averages, which over-weights small
+ // tiles and biases the result; pooling is what you'd expect "average over a range"
+ // to mean.
+ bool isItem = tabControl2.SelectedIndex == 0;
+ IEnumerable Images()
{
- Bitmap image = tabControl2.SelectedIndex == 0 ? Art.GetStatic(i) : Art.GetLand(i);
- if (image == null)
+ foreach (int i in sequence)
{
- continue;
- }
-
- unsafe
- {
- BitmapData bd = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format16bppArgb1555);
- ushort* line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
- ushort* cur = line;
-
- int meanr = 0;
- int meang = 0;
- int meanb = 0;
-
- int count = 0;
- for (int y = 0; y < image.Height; ++y, line += delta)
+ Bitmap image = isItem ? Art.GetStatic(i) : Art.GetLand(i);
+ if (image != null)
{
- cur = line;
- for (int x = 0; x < image.Width; ++x)
- {
- if (cur[x] != 0)
- {
- meanr += HueHelpers.HueToColorR(cur[x]);
- meang += HueHelpers.HueToColorG(cur[x]);
- meanb += HueHelpers.HueToColorB(cur[x]);
- ++count;
- }
- }
+ yield return image;
}
- image.UnlockBits(bd);
-
- meanr /= count;
- meang /= count;
- meanb /= count;
-
- gmeanr += meanr;
- gmeang += meang;
- gmeanb += meanb;
}
}
-
- var diff = sequence.Count();
-
- if (diff > 0)
- {
-
- gmeanr /= diff;
- gmeang /= diff;
- gmeanb /= diff;
- }
-
- Color col = Color.FromArgb(gmeanr, gmeang, gmeanb);
- return HueHelpers.ColorToHue(col);
+ return RadarColorAveraging.ComputeFromMany(Images(), CurrentStrategy);
}
private void OnClickCurrentToRangeAverage(object sender, EventArgs e)
@@ -993,7 +1183,7 @@ private void OnClickRangeToIndividualAverage(object sender, EventArgs e)
continue;
}
- var color = HueHelpers.ColorToHue(AverageColorFrom(image));
+ var color = RadarColorAveraging.Compute(image, CurrentStrategy);
SaveColor(i, color, isItemTile);
@@ -1147,7 +1337,7 @@ private void OnClickMeanColorAll(object sender, EventArgs e)
continue;
}
- var currentColor = HueHelpers.ColorToHue(AverageColorFrom(image));
+ var currentColor = RadarColorAveraging.Compute(image, CurrentStrategy);
RadarCol.SetItemColor(i, currentColor);
Options.ChangedUltimaClass["RadarCol"] = true;
}
@@ -1176,7 +1366,7 @@ private void OnClickMeanColorAll(object sender, EventArgs e)
continue;
}
- var currentColor = HueHelpers.ColorToHue(AverageColorFrom(image));
+ var currentColor = RadarColorAveraging.Compute(image, CurrentStrategy);
RadarCol.SetLandColor(i, currentColor);
Options.ChangedUltimaClass["RadarCol"] = true;
}
@@ -1188,45 +1378,6 @@ private void OnClickMeanColorAll(object sender, EventArgs e)
progressBar2.Value = 0;
}
- private unsafe Color AverageColorFrom(Bitmap image)
- {
- BitmapData bd = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format16bppArgb1555);
- ushort* line = (ushort*)bd.Scan0;
- int delta = bd.Stride >> 1;
- ushort* cur = line;
-
- int meanR = 0;
- int meanG = 0;
- int meanB = 0;
-
- int count = 0;
- for (int y = 0; y < image.Height; ++y, line += delta)
- {
- cur = line;
- for (int x = 0; x < image.Width; ++x)
- {
- if (cur[x] == 0)
- {
- continue;
- }
-
- meanR += HueHelpers.HueToColorR(cur[x]);
- meanG += HueHelpers.HueToColorG(cur[x]);
- meanB += HueHelpers.HueToColorB(cur[x]);
- ++count;
- }
- }
- image.UnlockBits(bd);
-
- if (count > 0)
- {
- meanR /= count;
- meanG /= count;
- meanB /= count;
- }
-
- return Color.FromArgb(meanR, meanG, meanB);
- }
private void FilterChange(TextBox control, Action filterCallback)
{
@@ -1375,5 +1526,202 @@ private void OnClickSelectNoneLand(object sender, EventArgs e)
{
SetAllCheckedLand(false);
}
+
+#if DEBUG
+ private void OnClickAlgorithmBenchmark(object sender, EventArgs e)
+ {
+ // Dev-only research harness for the radarcol generation algorithm. Sweeps
+ // every tile in the currently-loaded radarcol.mul, runs each candidate
+ // averaging strategy against the tile graphic, and writes a CSV of
+ // exact-match rates plus per-channel error stats. Output goes to
+ // Options.OutputPath. Not shipped in Release.
+ string outDir = Options.OutputPath ?? Path.GetTempPath();
+ string outPath = Path.Combine(outDir, "radarcol_eval.csv");
+
+ var strategies = RadarColorAveraging.All;
+ int n = strategies.Count;
+
+ const int landCount = 0x4000;
+ int itemCount = Math.Min(Art.GetMaxItemId() + 1, 0x4000);
+
+ // Per-class stats so we can see whether land vs item behave differently.
+ var land = new BenchStats(n);
+ var item = new BenchStats(n);
+
+ // Uniqueness check: if land has very few unique values, it's almost certainly
+ // hand-tuned to a palette rather than computed from the tile art.
+ var landUnique = new HashSet();
+ var itemUnique = new HashSet();
+
+ using var progress = new ProgressBarForm("Algorithm benchmark", "Iterating tiles…");
+ progress.Show(FindForm());
+
+ for (int i = 0; i < landCount; ++i)
+ {
+ ushort target = RadarCol.GetLandColor(i);
+ if (target == 0)
+ {
+ continue;
+ }
+ landUnique.Add(target);
+ Bitmap image;
+ try { image = Art.GetLand(i); }
+ catch { continue; }
+ if (image == null)
+ {
+ continue;
+ }
+ for (int s = 0; s < n; ++s)
+ {
+ ushort got = RadarColorAveraging.Compute(image, strategies[s]);
+ land.Tally(target, got, s);
+ }
+ }
+
+ for (int i = 0; i < itemCount; ++i)
+ {
+ ushort target = RadarCol.GetItemColor(i);
+ if (target == 0)
+ {
+ continue;
+ }
+ itemUnique.Add(target);
+ if (!Art.IsValidStatic(i))
+ {
+ continue;
+ }
+ Bitmap image;
+ try { image = Art.GetStatic(i); }
+ catch { continue; }
+ if (image == null)
+ {
+ continue;
+ }
+ for (int s = 0; s < n; ++s)
+ {
+ ushort got = RadarColorAveraging.Compute(image, strategies[s]);
+ item.Tally(target, got, s);
+ }
+ }
+
+ progress.Close();
+
+ using (var sw = new StreamWriter(outPath))
+ {
+ sw.WriteLine($"# unique_land_colors={landUnique.Count} (out of {land.Counted[0]} land tiles with a non-zero entry)");
+ sw.WriteLine($"# unique_item_colors={itemUnique.Count} (out of {item.Counted[0]} item tiles with a non-zero entry)");
+ sw.WriteLine("class;strategy;tiles;exact;exact_pct;mae_r5;mae_g5;mae_b5;max_r5;max_g5;max_b5");
+ for (int s = 0; s < n; ++s)
+ {
+ land.WriteRow(sw, "land", strategies[s], s);
+ }
+ for (int s = 0; s < n; ++s)
+ {
+ item.WriteRow(sw, "item", strategies[s], s);
+ }
+ for (int s = 0; s < n; ++s)
+ {
+ BenchStats.WriteCombinedRow(sw, "total", strategies[s], s, land, item);
+ }
+ }
+
+ // Pick the best strategy by combined exact-match count.
+ int bestIdx = 0;
+ long bestExact = land.Exact[0] + item.Exact[0];
+ long bestCount = land.Counted[0] + item.Counted[0];
+ for (int s = 1; s < n; ++s)
+ {
+ long ex = land.Exact[s] + item.Exact[s];
+ if (ex > bestExact)
+ {
+ bestIdx = s;
+ bestExact = ex;
+ bestCount = land.Counted[s] + item.Counted[s];
+ }
+ }
+ string summary = $"Tiles evaluated: {bestCount} (land {land.Counted[bestIdx]} + item {item.Counted[bestIdx]})\n" +
+ $"Unique colors: land {landUnique.Count}, item {itemUnique.Count}\n\n" +
+ $"Best: {RadarColorAveraging.DisplayName(strategies[bestIdx])}\n" +
+ $" land : {land.Exact[bestIdx]}/{land.Counted[bestIdx]} " +
+ $"({(land.Counted[bestIdx] == 0 ? 0 : 100.0 * land.Exact[bestIdx] / land.Counted[bestIdx]):F2}%)\n" +
+ $" item : {item.Exact[bestIdx]}/{item.Counted[bestIdx]} " +
+ $"({(item.Counted[bestIdx] == 0 ? 0 : 100.0 * item.Exact[bestIdx] / item.Counted[bestIdx]):F2}%)\n\n" +
+ $"Full report: {outPath}";
+ MessageBox.Show(FindForm(), summary, "Algorithm benchmark");
+ }
+
+ private sealed class BenchStats
+ {
+ public readonly long[] Exact;
+ public readonly long[] Counted;
+ public readonly long[] SumAbsR;
+ public readonly long[] SumAbsG;
+ public readonly long[] SumAbsB;
+ public readonly int[] MaxAbsR;
+ public readonly int[] MaxAbsG;
+ public readonly int[] MaxAbsB;
+
+ public BenchStats(int n)
+ {
+ Exact = new long[n];
+ Counted = new long[n];
+ SumAbsR = new long[n]; SumAbsG = new long[n]; SumAbsB = new long[n];
+ MaxAbsR = new int[n]; MaxAbsG = new int[n]; MaxAbsB = new int[n];
+ }
+
+ public void Tally(ushort target, ushort got, int s)
+ {
+ Counted[s]++;
+ if (got == target) Exact[s]++;
+ HueHelpers.HueExtract5(target, out int tr, out int tg, out int tb);
+ HueHelpers.HueExtract5(got, out int gr, out int gg, out int gb);
+ int dr = Math.Abs(tr - gr), dg = Math.Abs(tg - gg), db = Math.Abs(tb - gb);
+ SumAbsR[s] += dr; SumAbsG[s] += dg; SumAbsB[s] += db;
+ if (dr > MaxAbsR[s]) MaxAbsR[s] = dr;
+ if (dg > MaxAbsG[s]) MaxAbsG[s] = dg;
+ if (db > MaxAbsB[s]) MaxAbsB[s] = db;
+ }
+
+ public void WriteRow(StreamWriter sw, string label, RadarAveragingStrategy strat, int s)
+ {
+ long c = Counted[s];
+ if (c == 0) { sw.WriteLine($"{label};{strat};0;0;0;0;0;0;0;0;0"); return; }
+ double pct = 100.0 * Exact[s] / c;
+ double mr = (double)SumAbsR[s] / c, mg = (double)SumAbsG[s] / c, mb = (double)SumAbsB[s] / c;
+ sw.WriteLine(
+ $"{label};{strat};{c};{Exact[s]};{pct:F2};{mr:F3};{mg:F3};{mb:F3};{MaxAbsR[s]};{MaxAbsG[s]};{MaxAbsB[s]}");
+ }
+
+ public static void WriteCombinedRow(StreamWriter sw, string label, RadarAveragingStrategy strat, int s, BenchStats a, BenchStats b)
+ {
+ long c = a.Counted[s] + b.Counted[s];
+ if (c == 0) { sw.WriteLine($"{label};{strat};0;0;0;0;0;0;0;0;0"); return; }
+ long ex = a.Exact[s] + b.Exact[s];
+ double pct = 100.0 * ex / c;
+ double mr = (double)(a.SumAbsR[s] + b.SumAbsR[s]) / c;
+ double mg = (double)(a.SumAbsG[s] + b.SumAbsG[s]) / c;
+ double mb = (double)(a.SumAbsB[s] + b.SumAbsB[s]) / c;
+ int xr = Math.Max(a.MaxAbsR[s], b.MaxAbsR[s]);
+ int xg = Math.Max(a.MaxAbsG[s], b.MaxAbsG[s]);
+ int xb = Math.Max(a.MaxAbsB[s], b.MaxAbsB[s]);
+ sw.WriteLine($"{label};{strat};{c};{ex};{pct:F2};{mr:F3};{mg:F3};{mb:F3};{xr};{xg};{xb}");
+ }
+ }
+
+ // Minimal modal progress indicator for the benchmark; nothing fancy.
+ private sealed class ProgressBarForm : Form
+ {
+ public ProgressBarForm(string title, string label)
+ {
+ Text = title;
+ FormBorderStyle = FormBorderStyle.FixedDialog;
+ ControlBox = false;
+ StartPosition = FormStartPosition.CenterParent;
+ Size = new Size(320, 90);
+ var lbl = new Label { Text = label, AutoSize = true, Location = new Point(12, 14) };
+ Controls.Add(lbl);
+ }
+ }
+#endif
}
}
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.resx b/UoFiddler.Controls/UserControls/RadarColorControl.resx
index 519a5dd..f7497ab 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.resx
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.resx
@@ -118,9 +118,21 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- 17, 17
+ 431, 17
- 162, 17
+ 586, 17
+
+
+ 108, 17
+
+
+ 17, 17
+
+
+ 17, 17
+
+
+ 316, 17
\ No newline at end of file
diff --git a/UoFiddler.Plugin.Compare/Classes/Utils.cs b/UoFiddler.Plugin.Compare/Classes/Utils.cs
index bf5e14a..d902db3 100644
--- a/UoFiddler.Plugin.Compare/Classes/Utils.cs
+++ b/UoFiddler.Plugin.Compare/Classes/Utils.cs
@@ -15,6 +15,7 @@
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
+using Ultima.Helpers;
namespace UoFiddler.Plugin.Compare.Classes
{
@@ -403,7 +404,7 @@ public static unsafe ushort AverageCol(Bitmap bmp, bool noneBlack = true)
ushort hue = 0x0421;
if (bmp.PixelFormat == PixelFormat.Format32bppRgb)
{
- hue = Ultima.Hues.ColorToHue(Color.FromArgb((int)r, (int)g, (int)b));
+ hue = HueHelpers.ColorToHue(Color.FromArgb((int)r, (int)g, (int)b));
}
else if (bmp.PixelFormat == PixelFormat.Format16bppArgb1555)
{
From ef69718707670349dcf96133d4447a0229e05528 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 01:34:52 +0200
Subject: [PATCH 12/21] Improve map control performance.
---
Ultima/Map.cs | 561 +++++++++++++++---
.../UserControls/MapControl.Designer.cs | 9 +-
UoFiddler.Controls/UserControls/MapControl.cs | 272 ++++++++-
3 files changed, 715 insertions(+), 127 deletions(-)
diff --git a/Ultima/Map.cs b/Ultima/Map.cs
index d170dbd..e96bd6a 100644
--- a/Ultima/Map.cs
+++ b/Ultima/Map.cs
@@ -108,21 +108,79 @@ public sealed class Map
private readonly string _path;
private static bool _useDiff;
+ private static int _altitudeIntensity = 15;
+ private static AltitudeShadingPreset _shadingPreset = AltitudeShadingPreset.Normal;
+ private static AltitudeShadingSettings _customShadingSettings = AltitudeShadingSettings.GetPreset(AltitudeShadingPreset.Soft);
+
///
/// Controls the intensity of altitude-based shading (1-20, lower = more contrast)
/// Default is 15 for subtle effect
///
- public static int AltitudeIntensity { get; set; } = 15;
+ public static int AltitudeIntensity
+ {
+ get => _altitudeIntensity;
+ set
+ {
+ if (_altitudeIntensity == value)
+ {
+ return;
+ }
+ _altitudeIntensity = value;
+ InvalidateAltitudeShadingCache();
+ }
+ }
///
/// Current altitude shading preset
///
- public static AltitudeShadingPreset ShadingPreset { get; set; } = AltitudeShadingPreset.Normal;
+ public static AltitudeShadingPreset ShadingPreset
+ {
+ get => _shadingPreset;
+ set
+ {
+ if (_shadingPreset == value)
+ {
+ return;
+ }
+ _shadingPreset = value;
+ InvalidateAltitudeShadingCache();
+ }
+ }
///
/// Custom altitude shading settings (used when ShadingPreset is Custom)
///
- public static AltitudeShadingSettings CustomShadingSettings { get; set; } = AltitudeShadingSettings.GetPreset(AltitudeShadingPreset.Soft);
+ public static AltitudeShadingSettings CustomShadingSettings
+ {
+ get => _customShadingSettings;
+ set
+ {
+ _customShadingSettings = value;
+ InvalidateAltitudeShadingCache();
+ }
+ }
+
+ ///
+ /// Clears the cached altitude-shaded color blocks across all map instances.
+ /// Call after changing any shading parameter so the next paint re-shades.
+ ///
+ public static void InvalidateAltitudeShadingCache()
+ {
+ Felucca.ClearLitCache();
+ Trammel.ClearLitCache();
+ Ilshenar.ClearLitCache();
+ Malas.ClearLitCache();
+ Tokuno.ClearLitCache();
+ TerMur.ClearLitCache();
+ }
+
+ private void ClearLitCache()
+ {
+ _litCache = null;
+ _litCacheNoStatics = null;
+ _litCacheNoPatch = null;
+ _litCacheNoStaticsNoPatch = null;
+ }
public static bool UseDiff
{
@@ -196,6 +254,33 @@ public static void Reload()
Trammel._cacheNoStaticsNoPatch =
Ilshenar._cacheNoStaticsNoPatch =
Malas._cacheNoStaticsNoPatch = Tokuno._cacheNoStaticsNoPatch = TerMur._cacheNoStaticsNoPatch = null;
+
+ foreach (Map m in new[] { Felucca, Trammel, Ilshenar, Malas, Tokuno, TerMur })
+ {
+ m.ClearMipCaches();
+ m.ClearAltitudeCaches();
+ m.ClearLitCache();
+ }
+ }
+
+ private void ClearMipCaches()
+ {
+ _cacheHalf = null;
+ _cacheHalfNoStatics = null;
+ _cacheHalfNoPatch = null;
+ _cacheHalfNoStaticsNoPatch = null;
+ _cacheQuarter = null;
+ _cacheQuarterNoStatics = null;
+ _cacheQuarterNoPatch = null;
+ _cacheQuarterNoStaticsNoPatch = null;
+ }
+
+ private void ClearAltitudeCaches()
+ {
+ _altitudeCache = null;
+ _altitudeCacheNoStatics = null;
+ _altitudeCacheNoPatch = null;
+ _altitudeCacheNoStaticsNoPatch = null;
}
public void ResetCache()
@@ -205,6 +290,10 @@ public void ResetCache()
_cacheNoStatics = null;
_cacheNoStaticsNoPatch = null;
+ ClearMipCaches();
+ ClearAltitudeCaches();
+ ClearLitCache();
+
_isCachedDefault = false;
_isCachedNoStatics = false;
_isCachedNoPatch = false;
@@ -268,6 +357,33 @@ public Bitmap GetImage(int x, int y, int width, int height, bool statics)
private ushort[][][] _cacheNoStaticsNoPatch;
private ushort[] _black;
+ // Half-resolution mipmap (4x4 px per block, 16 ushorts)
+ private ushort[][][] _cacheHalf;
+ private ushort[][][] _cacheHalfNoStatics;
+ private ushort[][][] _cacheHalfNoPatch;
+ private ushort[][][] _cacheHalfNoStaticsNoPatch;
+ private ushort[] _blackHalf;
+
+ // Quarter-resolution mipmap (2x2 px per block, 4 ushorts)
+ private ushort[][][] _cacheQuarter;
+ private ushort[][][] _cacheQuarterNoStatics;
+ private ushort[][][] _cacheQuarterNoPatch;
+ private ushort[][][] _cacheQuarterNoStaticsNoPatch;
+ private ushort[] _blackQuarter;
+
+ // Per-block altitude data (sbyte[64])
+ private sbyte[][][] _altitudeCache;
+ private sbyte[][][] _altitudeCacheNoStatics;
+ private sbyte[][][] _altitudeCacheNoPatch;
+ private sbyte[][][] _altitudeCacheNoStaticsNoPatch;
+ private sbyte[] _blackAltitude;
+
+ // Pre-shaded color blocks for NormalWithAltitude mode (ushort[64])
+ private ushort[][][] _litCache;
+ private ushort[][][] _litCacheNoStatics;
+ private ushort[][][] _litCacheNoPatch;
+ private ushort[][][] _litCacheNoStaticsNoPatch;
+
public bool IsCached(bool statics)
{
if (UseDiff)
@@ -284,39 +400,81 @@ public void PreloadRenderedBlock(int x, int y, bool statics)
if (x < 0 || y < 0 || x >= matrix.BlockWidth || y >= matrix.BlockHeight)
{
- if (_black == null)
- {
- _black = new ushort[64];
- }
-
+ _black ??= new ushort[64];
return;
}
+ ushort[][][] cache = EnsureColorCacheArray(statics);
+
+ if (cache[y] == null)
+ {
+ cache[y] = new ushort[_tiles.BlockWidth][];
+ }
+
+ if (cache[y][x] == null)
+ {
+ cache[y][x] = RenderBlock(x, y, statics, UseDiff);
+ }
+ }
+
+ private ushort[][][] EnsureColorCacheArray(bool statics)
+ {
ushort[][][] cache;
if (UseDiff)
{
- if (statics)
+ cache = statics ? _cache : _cacheNoStatics;
+ if (cache == null)
{
- _isCachedDefault = true;
+ cache = new ushort[_tiles.BlockHeight][][];
+ if (statics) _cache = cache; else _cacheNoStatics = cache;
}
- else
+ }
+ else
+ {
+ cache = statics ? _cacheNoPatch : _cacheNoStaticsNoPatch;
+ if (cache == null)
{
- _isCachedNoStatics = true;
+ cache = new ushort[_tiles.BlockHeight][][];
+ if (statics) _cacheNoPatch = cache; else _cacheNoStaticsNoPatch = cache;
}
+ }
+ return cache;
+ }
- cache = (statics ? _cache : _cacheNoStatics);
+ ///
+ /// Marks the chosen color cache as fully preloaded. Call once after a full
+ /// PreloadRenderedBlock sweep so IsCached(statics) reports true.
+ ///
+ public void MarkPreloaded(bool statics)
+ {
+ if (UseDiff)
+ {
+ if (statics) _isCachedDefault = true;
+ else _isCachedNoStatics = true;
}
else
{
- if (statics)
- {
- _isCachedNoPatch = true;
- }
- else
- {
- _isCachedNoStaticsNoPatch = true;
- }
+ if (statics) _isCachedNoPatch = true;
+ else _isCachedNoStaticsNoPatch = true;
+ }
+ }
+
+ private ushort[] GetRenderedBlock(int x, int y, bool statics)
+ {
+ TileMatrix matrix = Tiles;
+ if (x < 0 || y < 0 || x >= matrix.BlockWidth || y >= matrix.BlockHeight)
+ {
+ return _black ??= new ushort[64];
+ }
+
+ ushort[][][] cache;
+ if (UseDiff)
+ {
+ cache = (statics ? _cache : _cacheNoStatics);
+ }
+ else
+ {
cache = (statics ? _cacheNoPatch : _cacheNoStaticsNoPatch);
}
@@ -351,15 +509,64 @@ public void PreloadRenderedBlock(int x, int y, bool statics)
cache[y] = new ushort[_tiles.BlockWidth][];
}
- if (cache[y][x] == null)
+ ushort[] data = cache[y][x];
+
+ if (data == null)
{
- cache[y][x] = RenderBlock(x, y, statics, UseDiff);
+ cache[y][x] = data = RenderBlock(x, y, statics, UseDiff);
}
- _tiles.CloseStreams();
+ return data;
}
- private ushort[] GetRenderedBlock(int x, int y, bool statics)
+ private sbyte[] GetAltitudeBlockCached(int x, int y, bool statics)
+ {
+ TileMatrix matrix = Tiles;
+
+ if (x < 0 || y < 0 || x >= matrix.BlockWidth || y >= matrix.BlockHeight)
+ {
+ return _blackAltitude ??= new sbyte[64];
+ }
+
+ sbyte[][][] cache;
+ if (UseDiff)
+ {
+ cache = statics ? _altitudeCache : _altitudeCacheNoStatics;
+ }
+ else
+ {
+ cache = statics ? _altitudeCacheNoPatch : _altitudeCacheNoStaticsNoPatch;
+ }
+
+ if (cache == null)
+ {
+ cache = new sbyte[_tiles.BlockHeight][][];
+ if (UseDiff)
+ {
+ if (statics) _altitudeCache = cache; else _altitudeCacheNoStatics = cache;
+ }
+ else
+ {
+ if (statics) _altitudeCacheNoPatch = cache; else _altitudeCacheNoStaticsNoPatch = cache;
+ }
+ }
+
+ if (cache[y] == null)
+ {
+ cache[y] = new sbyte[_tiles.BlockWidth][];
+ }
+
+ sbyte[] data = cache[y][x];
+
+ if (data == null)
+ {
+ cache[y][x] = data = GetAltitudeBlock(x, y, statics);
+ }
+
+ return data;
+ }
+
+ private ushort[] GetLitBlock(int x, int y, bool statics)
{
TileMatrix matrix = Tiles;
@@ -371,36 +578,23 @@ private ushort[] GetRenderedBlock(int x, int y, bool statics)
ushort[][][] cache;
if (UseDiff)
{
- cache = (statics ? _cache : _cacheNoStatics);
+ cache = statics ? _litCache : _litCacheNoStatics;
}
else
{
- cache = (statics ? _cacheNoPatch : _cacheNoStaticsNoPatch);
+ cache = statics ? _litCacheNoPatch : _litCacheNoStaticsNoPatch;
}
if (cache == null)
{
+ cache = new ushort[_tiles.BlockHeight][][];
if (UseDiff)
{
- if (statics)
- {
- _cache = cache = new ushort[_tiles.BlockHeight][][];
- }
- else
- {
- _cacheNoStatics = cache = new ushort[_tiles.BlockHeight][][];
- }
+ if (statics) _litCache = cache; else _litCacheNoStatics = cache;
}
else
{
- if (statics)
- {
- _cacheNoPatch = cache = new ushort[_tiles.BlockHeight][][];
- }
- else
- {
- _cacheNoStaticsNoPatch = cache = new ushort[_tiles.BlockHeight][][];
- }
+ if (statics) _litCacheNoPatch = cache; else _litCacheNoStaticsNoPatch = cache;
}
}
@@ -413,7 +607,120 @@ private ushort[] GetRenderedBlock(int x, int y, bool statics)
if (data == null)
{
- cache[y][x] = data = RenderBlock(x, y, statics, UseDiff);
+ ushort[] colors = GetRenderedBlock(x, y, statics);
+ sbyte[] altitudes = GetAltitudeBlockCached(x, y, statics);
+ cache[y][x] = data = ProcessBlockWithAltitude(colors, altitudes);
+ }
+
+ return data;
+ }
+
+ private ushort[] GetRenderedBlockHalf(int x, int y, bool statics)
+ {
+ TileMatrix matrix = Tiles;
+
+ if (x < 0 || y < 0 || x >= matrix.BlockWidth || y >= matrix.BlockHeight)
+ {
+ return _blackHalf ??= new ushort[16];
+ }
+
+ ushort[][][] cache;
+ if (UseDiff)
+ {
+ cache = statics ? _cacheHalf : _cacheHalfNoStatics;
+ }
+ else
+ {
+ cache = statics ? _cacheHalfNoPatch : _cacheHalfNoStaticsNoPatch;
+ }
+
+ if (cache == null)
+ {
+ cache = new ushort[_tiles.BlockHeight][][];
+ if (UseDiff)
+ {
+ if (statics) _cacheHalf = cache; else _cacheHalfNoStatics = cache;
+ }
+ else
+ {
+ if (statics) _cacheHalfNoPatch = cache; else _cacheHalfNoStaticsNoPatch = cache;
+ }
+ }
+
+ if (cache[y] == null)
+ {
+ cache[y] = new ushort[_tiles.BlockWidth][];
+ }
+
+ ushort[] data = cache[y][x];
+
+ if (data == null)
+ {
+ ushort[] full = GetRenderedBlock(x, y, statics);
+ var half = new ushort[16];
+ // Subsample 8x8 -> 4x4 (nearest neighbour, same as GDI NearestNeighbor)
+ for (int j = 0; j < 4; ++j)
+ {
+ int srcRow = (j * 2) * 8;
+ int dstRow = j * 4;
+ half[dstRow + 0] = full[srcRow + 0];
+ half[dstRow + 1] = full[srcRow + 2];
+ half[dstRow + 2] = full[srcRow + 4];
+ half[dstRow + 3] = full[srcRow + 6];
+ }
+ cache[y][x] = data = half;
+ }
+
+ return data;
+ }
+
+ private ushort[] GetRenderedBlockQuarter(int x, int y, bool statics)
+ {
+ TileMatrix matrix = Tiles;
+
+ if (x < 0 || y < 0 || x >= matrix.BlockWidth || y >= matrix.BlockHeight)
+ {
+ return _blackQuarter ??= new ushort[4];
+ }
+
+ ushort[][][] cache;
+ if (UseDiff)
+ {
+ cache = statics ? _cacheQuarter : _cacheQuarterNoStatics;
+ }
+ else
+ {
+ cache = statics ? _cacheQuarterNoPatch : _cacheQuarterNoStaticsNoPatch;
+ }
+
+ if (cache == null)
+ {
+ cache = new ushort[_tiles.BlockHeight][][];
+ if (UseDiff)
+ {
+ if (statics) _cacheQuarter = cache; else _cacheQuarterNoStatics = cache;
+ }
+ else
+ {
+ if (statics) _cacheQuarterNoPatch = cache; else _cacheQuarterNoStaticsNoPatch = cache;
+ }
+ }
+
+ if (cache[y] == null)
+ {
+ cache[y] = new ushort[_tiles.BlockWidth][];
+ }
+
+ ushort[] data = cache[y][x];
+
+ if (data == null)
+ {
+ ushort[] full = GetRenderedBlock(x, y, statics);
+ cache[y][x] = data = new ushort[]
+ {
+ full[0], full[4],
+ full[32], full[36]
+ };
}
return data;
@@ -654,7 +961,83 @@ public unsafe void GetImage(int x, int y, int width, int height, Bitmap bmp, boo
}
bmp.UnlockBits(bd);
- _tiles.CloseStreams();
+ }
+
+ ///
+ /// Renders the requested block region into a half-resolution bitmap (4x4 px per block).
+ /// Bitmap must be Format16bppRgb555 sized (width*4, height*4).
+ ///
+ public unsafe void GetImageHalf(int x, int y, int width, int height, Bitmap bmp, bool statics)
+ {
+ BitmapData bd = bmp.LockBits(
+ new Rectangle(0, 0, width << 2, height << 2), ImageLockMode.WriteOnly, PixelFormat.Format16bppRgb555);
+ int stride = bd.Stride;
+ int blockStride = stride << 2; // 4 rows per block
+
+ var pStart = (byte*)bd.Scan0;
+
+ for (int oy = 0, by = y; oy < height; ++oy, ++by, pStart += blockStride)
+ {
+ var pRow0 = (int*)(pStart + (0 * stride));
+ var pRow1 = (int*)(pStart + (1 * stride));
+ var pRow2 = (int*)(pStart + (2 * stride));
+ var pRow3 = (int*)(pStart + (3 * stride));
+
+ for (int ox = 0, bx = x; ox < width; ++ox, ++bx)
+ {
+ ushort[] data = GetRenderedBlockHalf(bx, by, statics);
+
+ fixed (ushort* pData = data)
+ {
+ var pvData = (int*)pData;
+
+ *pRow0++ = *pvData++;
+ *pRow0++ = *pvData++;
+ *pRow1++ = *pvData++;
+ *pRow1++ = *pvData++;
+ *pRow2++ = *pvData++;
+ *pRow2++ = *pvData++;
+ *pRow3++ = *pvData++;
+ *pRow3++ = *pvData;
+ }
+ }
+ }
+
+ bmp.UnlockBits(bd);
+ }
+
+ ///
+ /// Renders the requested block region into a quarter-resolution bitmap (2x2 px per block).
+ /// Bitmap must be Format16bppRgb555 sized (width*2, height*2).
+ ///
+ public unsafe void GetImageQuarter(int x, int y, int width, int height, Bitmap bmp, bool statics)
+ {
+ BitmapData bd = bmp.LockBits(
+ new Rectangle(0, 0, width << 1, height << 1), ImageLockMode.WriteOnly, PixelFormat.Format16bppRgb555);
+ int stride = bd.Stride;
+ int blockStride = stride << 1; // 2 rows per block
+
+ var pStart = (byte*)bd.Scan0;
+
+ for (int oy = 0, by = y; oy < height; ++oy, ++by, pStart += blockStride)
+ {
+ var pRow0 = (int*)(pStart + (0 * stride));
+ var pRow1 = (int*)(pStart + (1 * stride));
+
+ for (int ox = 0, bx = x; ox < width; ++ox, ++bx)
+ {
+ ushort[] data = GetRenderedBlockQuarter(bx, by, statics);
+
+ fixed (ushort* pData = data)
+ {
+ var pvData = (int*)pData;
+ *pRow0++ = *pvData++;
+ *pRow1++ = *pvData;
+ }
+ }
+ }
+
+ bmp.UnlockBits(bd);
}
public static void DefragStatics(string path, Map map, int width, int height, bool remove)
@@ -1108,8 +1491,8 @@ public Bitmap GetImageWithAltitude(int x, int y, int width, int height, bool sta
/// Altitude rendering mode
public unsafe void GetImageWithAltitude(int x, int y, int width, int height, Bitmap bmp, bool statics, MapAltitudeMode altitudeMode)
{
- PixelFormat format = altitudeMode == MapAltitudeMode.Altitude
- ? PixelFormat.Format8bppIndexed
+ PixelFormat format = altitudeMode == MapAltitudeMode.Altitude
+ ? PixelFormat.Format8bppIndexed
: PixelFormat.Format16bppRgb555;
BitmapData bd = bmp.LockBits(
@@ -1124,36 +1507,31 @@ public unsafe void GetImageWithAltitude(int x, int y, int width, int height, Bit
// 8-bit altitude mode
for (int oy = 0, by = y; oy < height; ++oy, ++by, pStart += blockStride)
{
- var pRow0 = (byte*)(pStart + (0 * stride));
- var pRow1 = (byte*)(pStart + (1 * stride));
- var pRow2 = (byte*)(pStart + (2 * stride));
- var pRow3 = (byte*)(pStart + (3 * stride));
- var pRow4 = (byte*)(pStart + (4 * stride));
- var pRow5 = (byte*)(pStart + (5 * stride));
- var pRow6 = (byte*)(pStart + (6 * stride));
- var pRow7 = (byte*)(pStart + (7 * stride));
+ var pRow0 = pStart + (0 * stride);
+ var pRow1 = pStart + (1 * stride);
+ var pRow2 = pStart + (2 * stride);
+ var pRow3 = pStart + (3 * stride);
+ var pRow4 = pStart + (4 * stride);
+ var pRow5 = pStart + (5 * stride);
+ var pRow6 = pStart + (6 * stride);
+ var pRow7 = pStart + (7 * stride);
for (int ox = 0, bx = x; ox < width; ++ox, ++bx)
{
- sbyte[] altitudeData = GetAltitudeBlock(bx, by, statics);
+ sbyte[] altitudeData = GetAltitudeBlockCached(bx, by, statics);
- for (int i = 0; i < 64; i++)
+ fixed (sbyte* pAlt = altitudeData)
{
- byte altValue = (byte)Math.Clamp(altitudeData[i] + 128, 0, 255);
- int rowIndex = i / 8;
- int colIndex = i % 8;
-
- switch (rowIndex)
- {
- case 0: pRow0[ox * 8 + colIndex] = altValue; break;
- case 1: pRow1[ox * 8 + colIndex] = altValue; break;
- case 2: pRow2[ox * 8 + colIndex] = altValue; break;
- case 3: pRow3[ox * 8 + colIndex] = altValue; break;
- case 4: pRow4[ox * 8 + colIndex] = altValue; break;
- case 5: pRow5[ox * 8 + colIndex] = altValue; break;
- case 6: pRow6[ox * 8 + colIndex] = altValue; break;
- case 7: pRow7[ox * 8 + colIndex] = altValue; break;
- }
+ sbyte* pa = pAlt;
+
+ for (int col = 0; col < 8; ++col) *pRow0++ = (byte)(*pa++ + 128);
+ for (int col = 0; col < 8; ++col) *pRow1++ = (byte)(*pa++ + 128);
+ for (int col = 0; col < 8; ++col) *pRow2++ = (byte)(*pa++ + 128);
+ for (int col = 0; col < 8; ++col) *pRow3++ = (byte)(*pa++ + 128);
+ for (int col = 0; col < 8; ++col) *pRow4++ = (byte)(*pa++ + 128);
+ for (int col = 0; col < 8; ++col) *pRow5++ = (byte)(*pa++ + 128);
+ for (int col = 0; col < 8; ++col) *pRow6++ = (byte)(*pa++ + 128);
+ for (int col = 0; col < 8; ++col) *pRow7++ = (byte)(*pa++ + 128);
}
}
}
@@ -1161,6 +1539,8 @@ public unsafe void GetImageWithAltitude(int x, int y, int width, int height, Bit
else
{
// 16-bit color modes (Normal and NormalWithAltitude)
+ bool withAltitude = altitudeMode == MapAltitudeMode.NormalWithAltitude;
+
for (int oy = 0, by = y; oy < height; ++oy, ++by, pStart += blockStride)
{
var pRow0 = (ushort*)(pStart + (0 * stride));
@@ -1174,37 +1554,28 @@ public unsafe void GetImageWithAltitude(int x, int y, int width, int height, Bit
for (int ox = 0, bx = x; ox < width; ++ox, ++bx)
{
- ushort[] colorData = GetRenderedBlock(bx, by, statics);
+ ushort[] colorData = withAltitude
+ ? GetLitBlock(bx, by, statics)
+ : GetRenderedBlock(bx, by, statics);
- if (altitudeMode == MapAltitudeMode.NormalWithAltitude)
+ fixed (ushort* pData = colorData)
{
- sbyte[] altitudeData = GetAltitudeBlock(bx, by, statics);
- colorData = ProcessBlockWithAltitude(colorData, altitudeData);
- }
-
- for (int i = 0; i < 64; i++)
- {
- int rowIndex = i / 8;
- int colIndex = i % 8;
-
- switch (rowIndex)
- {
- case 0: pRow0[ox * 8 + colIndex] = colorData[i]; break;
- case 1: pRow1[ox * 8 + colIndex] = colorData[i]; break;
- case 2: pRow2[ox * 8 + colIndex] = colorData[i]; break;
- case 3: pRow3[ox * 8 + colIndex] = colorData[i]; break;
- case 4: pRow4[ox * 8 + colIndex] = colorData[i]; break;
- case 5: pRow5[ox * 8 + colIndex] = colorData[i]; break;
- case 6: pRow6[ox * 8 + colIndex] = colorData[i]; break;
- case 7: pRow7[ox * 8 + colIndex] = colorData[i]; break;
- }
+ ushort* pd = pData;
+
+ for (int col = 0; col < 8; ++col) *pRow0++ = *pd++;
+ for (int col = 0; col < 8; ++col) *pRow1++ = *pd++;
+ for (int col = 0; col < 8; ++col) *pRow2++ = *pd++;
+ for (int col = 0; col < 8; ++col) *pRow3++ = *pd++;
+ for (int col = 0; col < 8; ++col) *pRow4++ = *pd++;
+ for (int col = 0; col < 8; ++col) *pRow5++ = *pd++;
+ for (int col = 0; col < 8; ++col) *pRow6++ = *pd++;
+ for (int col = 0; col < 8; ++col) *pRow7++ = *pd++;
}
}
}
}
bmp.UnlockBits(bd);
- _tiles.CloseStreams();
}
///
diff --git a/UoFiddler.Controls/UserControls/MapControl.Designer.cs b/UoFiddler.Controls/UserControls/MapControl.Designer.cs
index 5940399..88379e2 100644
--- a/UoFiddler.Controls/UserControls/MapControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/MapControl.Designer.cs
@@ -30,7 +30,14 @@ protected override void Dispose(bool disposing)
}
if (disposing)
{
-
+ _zoomBufferGraphics?.Dispose();
+ _zoomBuffer?.Dispose();
+ _renderBuffer?.Dispose();
+ _dragTrailTimer?.Dispose();
+ _zoomBufferGraphics = null;
+ _zoomBuffer = null;
+ _renderBuffer = null;
+ _dragTrailTimer = null;
}
base.Dispose(disposing);
}
diff --git a/UoFiddler.Controls/UserControls/MapControl.cs b/UoFiddler.Controls/UserControls/MapControl.cs
index fa9b680..234e3eb 100644
--- a/UoFiddler.Controls/UserControls/MapControl.cs
+++ b/UoFiddler.Controls/UserControls/MapControl.cs
@@ -85,6 +85,10 @@ private void AddAltitudeIntensityMenuItems()
public static double Zoom = 1;
private Bitmap _map;
+ private Bitmap _renderBuffer;
+ private Bitmap _zoomBuffer;
+ private Graphics _zoomBufferGraphics;
+ private PixelFormat _renderBufferFormat;
private int _currentMapId;
private bool _syncWithClient;
private int _clientX;
@@ -96,6 +100,13 @@ private void AddAltitudeIntensityMenuItems()
private Point _movingPoint;
private bool _renderingZoom;
private MapAltitudeMode _altitudeMode = MapAltitudeMode.Normal;
+ private readonly System.Diagnostics.Stopwatch _dragRepaintTimer = new System.Diagnostics.Stopwatch();
+ private System.Windows.Forms.Timer _dragTrailTimer;
+ private bool _dragInvalidatePending;
+ private double _dragAccumX;
+ private double _dragAccumY;
+ private int _preloadValue;
+ private int _preloadMax;
private int HScrollBar => hScrollBar.Value;
private int VScrollBar => vScrollBar.Value;
@@ -225,15 +236,55 @@ public static int Round(int x)
return (x >> 3) << 3;
}
- private void ZoomMap(ref Bitmap bmp0)
+ private Bitmap ZoomMap(Bitmap source, double effectiveZoom)
{
- Bitmap bmp1 = new Bitmap((int)(_map.Width * Zoom), (int)(_map.Height * Zoom));
- Graphics graph = Graphics.FromImage(bmp1);
- graph.InterpolationMode = InterpolationMode.NearestNeighbor;
- graph.PixelOffsetMode = PixelOffsetMode.Half;
- graph.DrawImage(bmp0, new Rectangle(0, 0, bmp1.Width, bmp1.Height));
- graph.Dispose();
- bmp0 = bmp1;
+ int targetWidth = (int)(source.Width * effectiveZoom);
+ int targetHeight = (int)(source.Height * effectiveZoom);
+
+ if (targetWidth <= 0 || targetHeight <= 0)
+ {
+ return source;
+ }
+
+ if (_zoomBuffer == null || _zoomBuffer.Width != targetWidth || _zoomBuffer.Height != targetHeight)
+ {
+ _zoomBufferGraphics?.Dispose();
+ _zoomBuffer?.Dispose();
+ _zoomBuffer = new Bitmap(targetWidth, targetHeight, PixelFormat.Format32bppArgb);
+ _zoomBufferGraphics = Graphics.FromImage(_zoomBuffer);
+ _zoomBufferGraphics.InterpolationMode = InterpolationMode.NearestNeighbor;
+ _zoomBufferGraphics.PixelOffsetMode = PixelOffsetMode.Half;
+ }
+
+ _zoomBufferGraphics.DrawImage(source, new Rectangle(0, 0, targetWidth, targetHeight));
+ return _zoomBuffer;
+ }
+
+ private Bitmap EnsureRenderBuffer(int pixelWidth, int pixelHeight, PixelFormat format)
+ {
+ if (_renderBuffer != null
+ && _renderBuffer.Width == pixelWidth
+ && _renderBuffer.Height == pixelHeight
+ && _renderBufferFormat == format)
+ {
+ return _renderBuffer;
+ }
+
+ _renderBuffer?.Dispose();
+ _renderBuffer = new Bitmap(pixelWidth, pixelHeight, format);
+ _renderBufferFormat = format;
+
+ if (format == PixelFormat.Format8bppIndexed)
+ {
+ ColorPalette palette = _renderBuffer.Palette;
+ for (int i = 0; i < 256; i++)
+ {
+ palette.Entries[i] = Color.FromArgb(i, i, i);
+ }
+ _renderBuffer.Palette = palette;
+ }
+
+ return _renderBuffer;
}
private void SetScrollBarValues()
@@ -416,6 +467,8 @@ private void OnMouseDown(object sender, MouseEventArgs e)
_moving = true;
_movingPoint.X = e.X;
_movingPoint.Y = e.Y;
+ _dragAccumX = 0;
+ _dragAccumY = 0;
Cursor = Cursors.Hand;
}
else
@@ -436,6 +489,8 @@ private void OnMouseUp(object sender, MouseEventArgs e)
Cursor = Cursors.Default;
}
+ private const int DragRepaintIntervalMs = 16;
+
private void OnMouseMove(object sender, MouseEventArgs e)
{
int xDelta = Math.Min(CurrentMap.Width, (int)(e.X / Zoom) + Round(hScrollBar.Value));
@@ -448,15 +503,67 @@ private void OnMouseMove(object sender, MouseEventArgs e)
return;
}
- int deltaX = (int)(-1 * (e.X - _movingPoint.X) / Zoom);
- int deltaY = (int)(-1 * (e.Y - _movingPoint.Y) / Zoom);
+ // Accumulate the fractional part of the drag so high-zoom drags (where 1 mouse pixel
+ // is less than 1 tile) don't lose precision: at Zoom = 4, a 1px mouse move is 0.25
+ // tiles and int-truncates to 0 without an accumulator.
+ _dragAccumX += -(e.X - _movingPoint.X) / Zoom;
+ _dragAccumY += -(e.Y - _movingPoint.Y) / Zoom;
+
+ int deltaX = (int)_dragAccumX;
+ int deltaY = (int)_dragAccumY;
+ _dragAccumX -= deltaX;
+ _dragAccumY -= deltaY;
_movingPoint.X = e.X;
_movingPoint.Y = e.Y;
+ if (deltaX == 0 && deltaY == 0)
+ {
+ return;
+ }
+
hScrollBar.Value = Math.Max(0, Math.Min(hScrollBar.Maximum, hScrollBar.Value + deltaX));
vScrollBar.Value = Math.Max(0, Math.Min(vScrollBar.Maximum, vScrollBar.Value + deltaY));
+ RequestDragRepaint();
+ }
+
+ private void RequestDragRepaint()
+ {
+ if (!_dragRepaintTimer.IsRunning || _dragRepaintTimer.ElapsedMilliseconds >= DragRepaintIntervalMs)
+ {
+ _dragInvalidatePending = false;
+ _dragRepaintTimer.Restart();
+ pictureBox.Invalidate();
+ return;
+ }
+
+ // Coalesce into a trailing-edge repaint so the final position always lands on screen.
+ if (_dragInvalidatePending)
+ {
+ return;
+ }
+
+ _dragInvalidatePending = true;
+ if (_dragTrailTimer == null)
+ {
+ _dragTrailTimer = new System.Windows.Forms.Timer { Interval = DragRepaintIntervalMs };
+ _dragTrailTimer.Tick += OnDragTrailTick;
+ }
+ _dragTrailTimer.Stop();
+ _dragTrailTimer.Interval = Math.Max(1, DragRepaintIntervalMs - (int)_dragRepaintTimer.ElapsedMilliseconds);
+ _dragTrailTimer.Start();
+ }
+
+ private void OnDragTrailTick(object sender, EventArgs e)
+ {
+ _dragTrailTimer.Stop();
+ if (!_dragInvalidatePending)
+ {
+ return;
+ }
+ _dragInvalidatePending = false;
+ _dragRepaintTimer.Restart();
pictureBox.Invalidate();
}
@@ -661,27 +768,112 @@ private void OnPaint(object sender, PaintEventArgs e)
if (PreloadWorker.IsBusy)
{
- e.Graphics.DrawString("Preloading map. Please wait...", SystemFonts.DefaultFont, Brushes.Black, 60, 60);
+ e.Graphics.Clear(pictureBox.BackColor);
+ const int textX = 60;
+ const int textY = 60;
+ string msg = "Preloading map. Please wait...";
+ e.Graphics.DrawString(msg, SystemFonts.DefaultFont, SystemBrushes.ControlText, textX, textY);
+
+ if (_preloadMax > 0)
+ {
+ SizeF textSize = e.Graphics.MeasureString(msg, SystemFonts.DefaultFont);
+ int barX = textX;
+ int barY = textY + (int)textSize.Height + 6;
+ int barW = Math.Max(200, (int)textSize.Width);
+ const int barH = 14;
+
+ e.Graphics.FillRectangle(SystemBrushes.ControlDark, barX, barY, barW, barH);
+ int fillW = (int)((long)barW * _preloadValue / _preloadMax);
+ if (fillW > 0)
+ {
+ e.Graphics.FillRectangle(SystemBrushes.Highlight, barX, barY, fillW, barH);
+ }
+ e.Graphics.DrawRectangle(SystemPens.ControlDarkDark, barX, barY, barW, barH);
+ }
return;
}
- // Use altitude-aware rendering if mode is not Normal
- if (_altitudeMode != MapAltitudeMode.Normal)
+ bool statics = showStaticsToolStripMenuItem1.Checked;
+ int blockX = hScrollBar.Value >> 3;
+ int blockY = vScrollBar.Value >> 3;
+ // +16 (2 blocks of padding) so the sub-block scroll offset never reveals empty space
+ // along the right/bottom edge of the viewport.
+ int widthBlocks = ((int)Math.Ceiling(e.ClipRectangle.Width / Zoom) + 16) >> 3;
+ int heightBlocks = ((int)Math.Ceiling(e.ClipRectangle.Height / Zoom) + 16) >> 3;
+
+ // Mipmap selection: only kicks in for color modes (Normal / NormalWithAltitude),
+ // not for pure 8bpp Altitude grayscale.
+ bool colorMode = _altitudeMode != MapAltitudeMode.Altitude;
+ int mipShift; // bits to shift block index to pixel size (3=full,2=half,1=quarter)
+ if (colorMode && Zoom <= 0.25)
+ {
+ mipShift = 1;
+ }
+ else if (colorMode && Zoom <= 0.5)
+ {
+ mipShift = 2;
+ }
+ else
+ {
+ mipShift = 3;
+ }
+
+ // When using a mip, the rendered bitmap is already at the correct screen size for that
+ // zoom level; only the residual factor (1.0) needs to go through ZoomMap, so we skip it.
+ // For zoom != mip-native and zoom > 0.5 (i.e. zoom 1, 2, 4), we still need ZoomMap.
+ double mipScale = 1 << mipShift; // 8, 4, or 2 pixels per block at this resolution
+ double effectiveZoom = Zoom * 8.0 / mipScale;
+
+ PixelFormat targetFormat = _altitudeMode == MapAltitudeMode.Altitude
+ ? PixelFormat.Format8bppIndexed
+ : PixelFormat.Format16bppRgb555;
+
+ int bufferPixelW = widthBlocks << mipShift;
+ int bufferPixelH = heightBlocks << mipShift;
+ _map = EnsureRenderBuffer(bufferPixelW, bufferPixelH, targetFormat);
+
+ if (mipShift == 3)
+ {
+ if (_altitudeMode != MapAltitudeMode.Normal)
+ {
+ CurrentMap.GetImageWithAltitude(blockX, blockY, widthBlocks, heightBlocks, _map, statics, _altitudeMode);
+ }
+ else
+ {
+ CurrentMap.GetImage(blockX, blockY, widthBlocks, heightBlocks, _map, statics);
+ }
+ }
+ else if (mipShift == 2)
{
- _map = CurrentMap.GetImageWithAltitude(hScrollBar.Value >> 3, vScrollBar.Value >> 3,
- (int)((e.ClipRectangle.Width / Zoom) + 8) >> 3, (int)((e.ClipRectangle.Height / Zoom) + 8) >> 3,
- showStaticsToolStripMenuItem1.Checked, _altitudeMode);
+ CurrentMap.GetImageHalf(blockX, blockY, widthBlocks, heightBlocks, _map, statics);
}
else
{
- _map = CurrentMap.GetImage(hScrollBar.Value >> 3, vScrollBar.Value >> 3,
- (int)((e.ClipRectangle.Width / Zoom) + 8) >> 3, (int)((e.ClipRectangle.Height / Zoom) + 8) >> 3,
- showStaticsToolStripMenuItem1.Checked);
+ CurrentMap.GetImageQuarter(blockX, blockY, widthBlocks, heightBlocks, _map, statics);
}
MessageLabel.Text = CurrentMap.Tiles.AllFilesExist() ? "" : "One of map files is missing!";
- ZoomMap(ref _map);
- e.Graphics.DrawImageUnscaledAndClipped(_map, e.ClipRectangle);
+
+ Bitmap toDraw;
+ if (Math.Abs(effectiveZoom - 1.0) < 1e-6)
+ {
+ toDraw = _map;
+ }
+ else
+ {
+ toDraw = ZoomMap(_map, effectiveZoom);
+ }
+
+ // The render buffer starts at the block boundary (blockX * 8). Shift the draw position
+ // by the sub-block portion of the scroll so viewport pixel 0 maps to the exact tile
+ // the scrollbar points at. Without this, dragging at zoom > 1 looks "stuck" between
+ // 8-tile block boundaries.
+ int subTileX = hScrollBar.Value - (blockX << 3);
+ int subTileY = vScrollBar.Value - (blockY << 3);
+ int drawOffsetX = (int)Math.Round(subTileX * Zoom);
+ int drawOffsetY = (int)Math.Round(subTileY * Zoom);
+
+ e.Graphics.DrawImageUnscaled(toDraw, -drawOffsetX, -drawOffsetY);
if (showCenterCrossToolStripMenuItem1.Checked)
{
@@ -707,8 +899,8 @@ private void OnPaint(object sender, PaintEventArgs e)
using (Brush brush = new SolidBrush(Color.FromArgb(180, Color.Yellow)))
using (Pen pen = new Pen(brush))
{
- int x = (int)((_clientX - Round(hScrollBar.Value)) * Zoom);
- int y = (int)((_clientY - Round(vScrollBar.Value)) * Zoom);
+ int x = (int)((_clientX - hScrollBar.Value) * Zoom);
+ int y = (int)((_clientY - vScrollBar.Value) * Zoom);
e.Graphics.DrawLine(pen, x - 4, y, x + 4, y);
e.Graphics.DrawLine(pen, x, y - 4, x, y + 4);
@@ -728,7 +920,7 @@ private void OnPaint(object sender, PaintEventArgs e)
OverlayObject o = (OverlayObject)obj.Tag;
if (o.IsVisible(e.ClipRectangle, _currentMapId, HScrollBar, VScrollBar, Zoom))
{
- o.Draw(e.Graphics, Round(HScrollBar), Round(VScrollBar), Zoom, CurrentMap.Width);
+ o.Draw(e.Graphics, HScrollBar, VScrollBar, Zoom, CurrentMap.Width);
}
}
}
@@ -1008,11 +1200,13 @@ private void OnClickPreloadMap(object sender, EventArgs e)
return;
}
- ProgressBar.Minimum = 0;
- ProgressBar.Maximum = (CurrentMap.Width >> 3) * (CurrentMap.Height >> 3);
- ProgressBar.Step = 1;
- ProgressBar.Value = 0;
- ProgressBar.Visible = true;
+ _preloadValue = 0;
+ _preloadMax = (CurrentMap.Width >> 3) * (CurrentMap.Height >> 3);
+ // Progress is drawn directly in the pictureBox so it appears under the
+ // "Preloading map..." message rather than orphaned in the toolstrip.
+ PreloadMap.Visible = false;
+ ProgressBar.Visible = false;
+ pictureBox.Invalidate();
PreloadWorker.RunWorkerAsync(new object[] { CurrentMap, showStaticsToolStripMenuItem1.Checked });
}
@@ -1022,23 +1216,39 @@ private void PreLoadDoWork(object sender, DoWorkEventArgs e)
bool statics = (bool)((object[])e.Argument)[1];
int width = CurrentMap.Width >> 3;
int height = CurrentMap.Height >> 3;
+ int total = width * height;
+ int reportEvery = Math.Max(1, total / 200); // ~200 UI updates total
+ int sinceReport = 0;
+ int done = 0;
for (int x = 0; x < width; ++x)
{
for (int y = 0; y < height; ++y)
{
CurrentMap.PreloadRenderedBlock(x, y, statics);
- PreloadWorker.ReportProgress(1);
+ ++done;
+ if (++sinceReport >= reportEvery)
+ {
+ sinceReport = 0;
+ PreloadWorker.ReportProgress(done);
+ }
}
}
+
+ // Final report so the bar reaches 100%.
+ PreloadWorker.ReportProgress(done);
+ CurrentMap.MarkPreloaded(statics);
+ CurrentMap.Tiles.CloseStreams();
}
private void PreLoadProgressChanged(object sender, ProgressChangedEventArgs e)
{
- ProgressBar.PerformStep();
+ _preloadValue = e.ProgressPercentage;
+ pictureBox.Invalidate();
}
private void PreLoadCompleted(object sender, RunWorkerCompletedEventArgs e)
{
+ _preloadMax = 0;
ProgressBar.Visible = false;
PreloadMap.Visible = false;
pictureBox.Invalidate();
From 6f1cfaf3210aa3b476a8778b2eaaff9b390bb45e Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 01:43:04 +0200
Subject: [PATCH 13/21] Minor optimization to items and gumps preload.
---
UoFiddler.Controls/UserControls/GumpControl.cs | 16 +++++++++++++---
UoFiddler.Controls/UserControls/ItemsControl.cs | 14 ++++++++++++--
2 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/UoFiddler.Controls/UserControls/GumpControl.cs b/UoFiddler.Controls/UserControls/GumpControl.cs
index 2ea9d66..b5f657e 100644
--- a/UoFiddler.Controls/UserControls/GumpControl.cs
+++ b/UoFiddler.Controls/UserControls/GumpControl.cs
@@ -835,16 +835,26 @@ private void OnClickPreLoad(object sender, EventArgs e)
private void PreLoaderDoWork(object sender, DoWorkEventArgs e)
{
- for (int i = 0; i < Gumps.GetCount(); ++i)
+ int total = Gumps.GetCount();
+ int reportEvery = Math.Max(1, total / 200);
+ int sinceReport = 0;
+ int done = 0;
+ for (int i = 0; i < total; ++i)
{
Gumps.GetGump(i);
- PreLoader.ReportProgress(1);
+ ++done;
+ if (++sinceReport >= reportEvery)
+ {
+ sinceReport = 0;
+ PreLoader.ReportProgress(done);
+ }
}
+ PreLoader.ReportProgress(done);
}
private void PreLoaderProgressChanged(object sender, ProgressChangedEventArgs e)
{
- ProgressBar.PerformStep();
+ ProgressBar.Value = Math.Min(ProgressBar.Maximum, Math.Max(ProgressBar.Minimum, e.ProgressPercentage));
}
private void PreLoaderCompleted(object sender, RunWorkerCompletedEventArgs e)
diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs
index 114f4d9..a38a27e 100644
--- a/UoFiddler.Controls/UserControls/ItemsControl.cs
+++ b/UoFiddler.Controls/UserControls/ItemsControl.cs
@@ -877,16 +877,26 @@ private void OnClickPreLoad(object sender, EventArgs e)
private void PreLoaderDoWork(object sender, DoWorkEventArgs e)
{
+ int total = _itemList.Count;
+ int reportEvery = Math.Max(1, total / 200);
+ int sinceReport = 0;
+ int done = 0;
foreach (int item in _itemList)
{
Art.GetStatic(item);
- PreLoader.ReportProgress(1);
+ ++done;
+ if (++sinceReport >= reportEvery)
+ {
+ sinceReport = 0;
+ PreLoader.ReportProgress(done);
+ }
}
+ PreLoader.ReportProgress(done);
}
private void PreLoaderProgressChanged(object sender, ProgressChangedEventArgs e)
{
- ProgressBar.PerformStep();
+ ProgressBar.Value = Math.Min(ProgressBar.Maximum, Math.Max(ProgressBar.Minimum, e.ProgressPercentage));
}
private void PreLoaderCompleted(object sender, RunWorkerCompletedEventArgs e)
From 4e05e3cb40971430d110ee48a9aca27d78908056 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 18:45:56 +0200
Subject: [PATCH 14/21] More performance optimizations. Update map compare and
uop packer plugins.
---
Ultima/FileIndex.cs | 34 +-
Ultima/Files.cs | 6 +-
Ultima/Gumps.cs | 378 +++++++++++++--
Ultima/Helpers/MoveToFront.cs | 14 +-
Ultima/Helpers/MythicDecompress.cs | 194 +++++---
Ultima/Helpers/TileDataHelpers.cs | 17 +
Ultima/Hues.cs | 66 ++-
Ultima/RadarCol.cs | 9 +-
Ultima/StringList.cs | 155 ++++---
Ultima/TileData.cs | 97 ++--
Ultima/TileMatrix.cs | 85 ++--
Ultima/TileMatrixPatch.cs | 67 +--
.../UserControls/GumpControl.cs | 16 +-
.../Classes/SecondFileAccessor.cs | 7 +-
.../Classes/SecondGump.cs | 81 +++-
UoFiddler.Plugin.Compare/Classes/SecondHue.cs | 20 +-
.../Classes/SecondRadarCol.cs | 7 +-
.../Classes/SecondTileData.cs | 101 ++--
.../UserControls/CompareMapControl.cs | 432 ++++++++++++------
.../Classes/LegacyMulFileConverter.cs | 15 +-
20 files changed, 1193 insertions(+), 608 deletions(-)
diff --git a/Ultima/FileIndex.cs b/Ultima/FileIndex.cs
index 6097301..48b4234 100644
--- a/Ultima/FileIndex.cs
+++ b/Ultima/FileIndex.cs
@@ -20,6 +20,13 @@ public IEntry this[int index]
private readonly string _mulPath;
+ ///
+ /// Absolute path to the .mul or .uop file backing this index, or null
+ /// if no client file was located. Exposed so parallel preloaders can
+ /// open their own per-thread FileStreams (FileShare.Read).
+ ///
+ public string MulPath => _mulPath;
+
public FileIndex(string idxFile, string mulFile, int length, int file) : this(idxFile, mulFile, null, length,
file, ".dat", -1, false)
{
@@ -40,12 +47,12 @@ public FileIndex(string idxFile, string mulFile, string uopFile, int length, int
if (Files.MulPath.Count > 0)
{
- idxPath = Files.MulPath[idxFile.ToLower()];
- _mulPath = Files.MulPath[mulFile.ToLower()];
+ idxPath = Files.MulPath[idxFile];
+ _mulPath = Files.MulPath[mulFile];
- if (!string.IsNullOrEmpty(uopFile) && Files.MulPath.ContainsKey(uopFile.ToLower()))
+ if (!string.IsNullOrEmpty(uopFile) && Files.MulPath.ContainsKey(uopFile))
{
- uopPath = Files.MulPath[uopFile.ToLower()];
+ uopPath = Files.MulPath[uopFile];
}
if (string.IsNullOrEmpty(idxPath))
@@ -145,8 +152,8 @@ public FileIndex(string idxFile, string mulFile, int file)
if (Files.MulPath.Count > 0)
{
- idxPath = Files.MulPath[idxFile.ToLower()];
- _mulPath = Files.MulPath[mulFile.ToLower()];
+ idxPath = Files.MulPath[idxFile];
+ _mulPath = Files.MulPath[mulFile];
if (string.IsNullOrEmpty(idxPath))
{
idxPath = null;
@@ -557,11 +564,10 @@ public MulFileAccessor(string idxPath, string path, int length)
Stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var count = (int)(index.Length / 12);
IdxLength = index.Length;
- GCHandle gc = GCHandle.Alloc(Index, GCHandleType.Pinned);
- var buffer = new byte[index.Length];
- index.ReadExactly(buffer, 0, (int)index.Length);
- Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)Math.Min(IdxLength, Index.Length * 12));
- gc.Free();
+
+ int readLen = (int)Math.Min(IdxLength, (long)Index.Length * 12);
+ index.ReadExactly(MemoryMarshal.AsBytes(Index.AsSpan()).Slice(0, readLen));
+
for (int i = count; i < Index.Length; ++i)
{
Index[i].Lookup = -1;
@@ -579,11 +585,7 @@ public MulFileAccessor(string idxPath, string path)
var count = (int)(index.Length / 12);
IdxLength = index.Length;
Index = new Entry3D[count];
- GCHandle gc = GCHandle.Alloc(Index, GCHandleType.Pinned);
- var buffer = new byte[index.Length];
- index.ReadExactly(buffer, 0, (int)index.Length);
- Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)index.Length);
- gc.Free();
+ index.ReadExactly(MemoryMarshal.AsBytes(Index.AsSpan()));
}
}
diff --git a/Ultima/Files.cs b/Ultima/Files.cs
index 016c06b..89e1ee2 100644
--- a/Ultima/Files.cs
+++ b/Ultima/Files.cs
@@ -201,7 +201,7 @@ public static void ReLoadDirectory()
///
public static void LoadMulPath()
{
- MulPath = new Dictionary();
+ MulPath = new Dictionary(StringComparer.OrdinalIgnoreCase);
RootDir = Directory ?? string.Empty;
foreach (string file in _uoFiles)
@@ -276,9 +276,9 @@ public static string GetFilePath(string file)
string path = string.Empty;
- if (MulPath.ContainsKey(file.ToLower()))
+ if (MulPath.TryGetValue(file, out string mapped))
{
- path = MulPath[file.ToLower()];
+ path = mapped;
}
if (string.IsNullOrEmpty(path))
diff --git a/Ultima/Gumps.cs b/Ultima/Gumps.cs
index 49294e5..6ae188d 100644
--- a/Ultima/Gumps.cs
+++ b/Ultima/Gumps.cs
@@ -4,6 +4,8 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using Ultima.Caching;
using Ultima.Helpers;
@@ -172,37 +174,79 @@ public static byte[] GetRawGump(int index, out int width, out int height)
throw new InvalidOperationException("Verdata.mul is not supported for compressed UOP");
}
- if (_streamBuffer == null || _streamBuffer.Length < entry.Length)
- {
- _streamBuffer = new byte[entry.Length];
- }
-
- stream.ReadExactly(_streamBuffer, 0, entry.Length);
-
- var result = UopUtils.Decompress(_streamBuffer);
- if (result.success is false)
+ int compLen = entry.Length;
+ int decSize = entry.DecompressedLength;
+ if (decSize <= 8)
{
return null;
}
- if (entry.Flag == CompressionFlag.Mythic)
+ byte[] rented = ArrayPool.Shared.Rent(compLen);
+ byte[] zlibBuf = ArrayPool.Shared.Rent(decSize);
+ byte[] mythicBuf = null;
+ try
{
- _streamBuffer = MythicDecompress.Decompress(result.data);
- }
+ stream.ReadExactly(rented, 0, compLen);
- using (BinaryReader reader = new BinaryReader(new MemoryStream(_streamBuffer)))
- {
- byte[] extra = reader.ReadBytes(8);
+ if (!UopUtils.TryDecompressInto(rented, 0, compLen, zlibBuf, out int zlibLen))
+ {
+ return null;
+ }
+
+ byte[] payload;
+ int payloadLength;
+
+ if (entry.Flag == CompressionFlag.Mythic)
+ {
+ uint mythicLen = MythicDecompress.PeekDecompressedLength(zlibBuf.AsSpan(0, zlibLen));
+ if (mythicLen <= 8 || mythicLen > int.MaxValue)
+ {
+ return null;
+ }
- width = (extra[3] << 24) | (extra[2] << 16) | (extra[1] << 8) | extra[0];
- height = (extra[7] << 24) | (extra[6] << 16) | (extra[5] << 8) | extra[4];
+ mythicBuf = ArrayPool.Shared.Rent((int)mythicLen);
+ if (!MythicDecompress.TryDecompress(
+ zlibBuf.AsSpan(0, zlibLen), mythicBuf.AsSpan(0, (int)mythicLen), out _))
+ {
+ return null;
+ }
+
+ payload = mythicBuf;
+ payloadLength = (int)mythicLen;
+ }
+ else
+ {
+ payload = zlibBuf;
+ payloadLength = zlibLen;
+ }
+
+ width = (payload[3] << 24) | (payload[2] << 16) | (payload[1] << 8) | payload[0];
+ height = (payload[7] << 24) | (payload[6] << 16) | (payload[5] << 8) | payload[4];
+ entry.Extra1 = width;
+ entry.Extra2 = height;
- // TODO: Tbh, whole code needs to be reworked with readers, as we're doing useless work here just re-reading everything but 8 first bytes
- _streamBuffer = reader.ReadBytes(_streamBuffer.Length - 8);
+ if (width <= 0 || height <= 0)
+ {
+ return null;
+ }
+
+ // Returned array holds the payload without the 8-byte header.
+ int resultLen = payloadLength - 8;
+ byte[] result = new byte[resultLen];
+ Buffer.BlockCopy(payload, 8, result, 0, resultLen);
+
+ return result;
}
+ finally
+ {
+ if (mythicBuf != null)
+ {
+ ArrayPool.Shared.Return(mythicBuf);
+ }
- entry.Extra1 = width;
- entry.Extra2 = height;
+ ArrayPool.Shared.Return(zlibBuf);
+ ArrayPool.Shared.Return(rented);
+ }
}
width = entry.Extra1;
@@ -213,11 +257,6 @@ public static byte[] GetRawGump(int index, out int width, out int height)
return null;
}
- if (entry.Flag == CompressionFlag.Mythic)
- {
- return _streamBuffer;
- }
-
var length = entry.Length;
if (patched)
{
@@ -459,6 +498,7 @@ public static unsafe bool TryGetGumpPixels(int index, Span destination,
byte[] rented = ArrayPool.Shared.Rent(length);
byte[] zlibBuf = null;
+ byte[] mythicBuf = null;
try
{
stream.ReadExactly(rented, 0, length);
@@ -484,8 +524,20 @@ public static unsafe bool TryGetGumpPixels(int index, Span destination,
if (entry.Flag == CompressionFlag.Mythic)
{
- // Mythic still allocates the final byte[]; that's the next lever.
- data = MythicDecompress.Decompress(zlibBuf, 0, zlibLen);
+ uint mythicLen = MythicDecompress.PeekDecompressedLength(zlibBuf.AsSpan(0, zlibLen));
+ if (mythicLen <= 8 || mythicLen > int.MaxValue)
+ {
+ return false;
+ }
+
+ mythicBuf = ArrayPool.Shared.Rent((int)mythicLen);
+ if (!MythicDecompress.TryDecompress(
+ zlibBuf.AsSpan(0, zlibLen), mythicBuf.AsSpan(0, (int)mythicLen), out _))
+ {
+ return false;
+ }
+
+ data = mythicBuf;
}
else
{
@@ -544,6 +596,10 @@ public static unsafe bool TryGetGumpPixels(int index, Span destination,
}
finally
{
+ if (mythicBuf != null)
+ {
+ ArrayPool.Shared.Return(mythicBuf);
+ }
if (zlibBuf != null)
{
ArrayPool.Shared.Return(zlibBuf);
@@ -643,6 +699,7 @@ public static unsafe Bitmap GetGump(int index, out bool patched)
byte[] rented = ArrayPool.Shared.Rent(length);
byte[] zlibBuf = null;
+ byte[] mythicBuf = null;
try
{
stream.ReadExactly(rented, 0, length);
@@ -670,8 +727,20 @@ public static unsafe Bitmap GetGump(int index, out bool patched)
if (entry.Flag == CompressionFlag.Mythic)
{
- // Mythic still allocates the final byte[]; that's the next lever.
- data = MythicDecompress.Decompress(zlibBuf, 0, zlibLen);
+ uint mythicLen = MythicDecompress.PeekDecompressedLength(zlibBuf.AsSpan(0, zlibLen));
+ if (mythicLen <= 8 || mythicLen > int.MaxValue)
+ {
+ return null;
+ }
+
+ mythicBuf = ArrayPool.Shared.Rent((int)mythicLen);
+ if (!MythicDecompress.TryDecompress(
+ zlibBuf.AsSpan(0, zlibLen), mythicBuf.AsSpan(0, (int)mythicLen), out _))
+ {
+ return null;
+ }
+
+ data = mythicBuf;
}
else
{
@@ -756,10 +825,259 @@ public static unsafe Bitmap GetGump(int index, out bool patched)
}
finally
{
+ if (mythicBuf != null)
+ {
+ ArrayPool.Shared.Return(mythicBuf);
+ }
+
+ if (zlibBuf != null)
+ {
+ ArrayPool.Shared.Return(zlibBuf);
+ }
+
+ ArrayPool.Shared.Return(rented);
+ }
+ }
+
+ ///
+ /// Preloads all gumps in parallel, populating the LRU bitmap cache.
+ /// Each worker opens its own FileStream against the .uop / .mul so the
+ /// expensive part — zlib + Mythic decompression and RLE decode — runs
+ /// concurrently across CPU cores. Per-bitmap work is unchanged; only
+ /// the orchestration is parallel.
+ ///
+ /// Set to 0 to use ProcessorCount.
+ /// is invoked from worker threads
+ /// with the cumulative count of completed gumps; the caller is
+ /// responsible for marshalling to the UI thread if needed.
+ ///
+ public static void PreloadParallel(int parallelism, Action progressCallback)
+ {
+ if (_fileIndex?.FileAccessor == null)
+ {
+ return;
+ }
+
+ string mulPath = _fileIndex.MulPath;
+ if (string.IsNullOrEmpty(mulPath) || !File.Exists(mulPath))
+ {
+ return;
+ }
+
+ int total = _indexLength;
+ if (total <= 0)
+ {
+ return;
+ }
+
+ if (parallelism <= 0)
+ {
+ parallelism = Environment.ProcessorCount;
+ }
+
+ int done = 0;
+ int reportEvery = Math.Max(1, total / 200);
+ int nextReport = reportEvery;
+ object reportLock = new object();
+
+ var options = new ParallelOptions { MaxDegreeOfParallelism = parallelism };
+
+ Parallel.For(
+ 0, total, options,
+ localInit: () => new FileStream(mulPath, FileMode.Open, FileAccess.Read, FileShare.Read),
+ body: (index, _, stream) =>
+ {
+ DecodeAndCacheOne(index, stream);
+
+ int doneNow = Interlocked.Increment(ref done);
+ if (progressCallback != null && doneNow >= Volatile.Read(ref nextReport))
+ {
+ bool shouldReport = false;
+ lock (reportLock)
+ {
+ if (doneNow >= nextReport)
+ {
+ nextReport = doneNow + reportEvery;
+ shouldReport = true;
+ }
+ }
+
+ if (shouldReport)
+ {
+ progressCallback(doneNow);
+ }
+ }
+
+ return stream;
+ },
+ localFinally: stream => stream?.Dispose()
+ );
+
+ progressCallback?.Invoke(total);
+ }
+
+ private static unsafe void DecodeAndCacheOne(int index, FileStream stream)
+ {
+ if (_removed[index] || _replaced.ContainsKey(index))
+ {
+ return;
+ }
+
+ // Cheap precheck — if the cache already has it (e.g. from a prior
+ // single-shot GetGump), don't redecode.
+ if (_cache.TryGet(index, out _))
+ {
+ return;
+ }
+
+ IEntry entry = _fileIndex[index];
+ if (entry == null || entry.Lookup < 0 || entry.Extra1 == -1)
+ {
+ return;
+ }
+
+ int length = entry.Length & 0x7FFFFFFF;
+ if (length <= 0)
+ {
+ return;
+ }
+
+ byte[] rented = ArrayPool.Shared.Rent(length);
+ byte[] zlibBuf = null;
+ byte[] mythicBuf = null;
+ try
+ {
+ stream.Seek(entry.Lookup, SeekOrigin.Begin);
+ stream.ReadExactly(rented, 0, length);
+
+ byte[] data = rented;
+ int dataOffset = 0;
+ uint width = (uint)entry.Extra1;
+ uint height = (uint)entry.Extra2;
+
+ if (entry.Flag >= CompressionFlag.Zlib)
+ {
+ int decSize = entry.DecompressedLength;
+ if (decSize <= 8)
+ {
+ return;
+ }
+
+ zlibBuf = ArrayPool.Shared.Rent(decSize);
+ if (!UopUtils.TryDecompressInto(rented, 0, length, zlibBuf, out int zlibLen))
+ {
+ return;
+ }
+
+ if (entry.Flag == CompressionFlag.Mythic)
+ {
+ uint mythicLen = MythicDecompress.PeekDecompressedLength(zlibBuf.AsSpan(0, zlibLen));
+ if (mythicLen <= 8 || mythicLen > int.MaxValue)
+ {
+ return;
+ }
+
+ mythicBuf = ArrayPool.Shared.Rent((int)mythicLen);
+ if (!MythicDecompress.TryDecompress(
+ zlibBuf.AsSpan(0, zlibLen), mythicBuf.AsSpan(0, (int)mythicLen), out _))
+ {
+ return;
+ }
+
+ data = mythicBuf;
+ }
+ else
+ {
+ data = zlibBuf;
+ }
+
+ width = (uint)(data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24));
+ height = (uint)(data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24));
+ dataOffset = 8;
+ }
+
+ if (width == 0 || height == 0 || width > 0xFFFF || height > 0xFFFF)
+ {
+ return;
+ }
+
+ Bitmap bmp;
+ try
+ {
+ bmp = new Bitmap((int)width, (int)height, PixelFormat.Format16bppArgb1555);
+ }
+ catch
+ {
+ return;
+ }
+
+ BitmapData bd = bmp.LockBits(
+ new Rectangle(0, 0, (int)width, (int)height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555);
+ try
+ {
+ fixed (byte* dataPtr = data)
+ {
+ byte* basePtr = dataPtr + dataOffset;
+ var lookup = (int*)basePtr;
+ var dat = (ushort*)basePtr;
+
+ var line = (ushort*)bd.Scan0;
+ int delta = bd.Stride >> 1;
+
+ for (int y = 0; y < (int)height; ++y, line += delta)
+ {
+ int count = (*lookup++ * 2);
+
+ ushort* cur = line;
+ ushort* end = line + bd.Width;
+
+ while (cur < end)
+ {
+ ushort color = dat[count++];
+ ushort* next = cur + dat[count++];
+
+ if (color == 0)
+ {
+ cur = next;
+ }
+ else
+ {
+ color ^= 0x8000;
+ while (cur < next)
+ {
+ *cur++ = color;
+ }
+ }
+ }
+ }
+ }
+ }
+ finally
+ {
+ bmp.UnlockBits(bd);
+ }
+
+ if (Files.CacheData)
+ {
+ _cache.Set(index, bmp);
+ }
+ }
+ catch
+ {
+ // Skip this index; preload should not abort the whole sweep.
+ }
+ finally
+ {
+ if (mythicBuf != null)
+ {
+ ArrayPool.Shared.Return(mythicBuf);
+ }
+
if (zlibBuf != null)
{
ArrayPool.Shared.Return(zlibBuf);
}
+
ArrayPool.Shared.Return(rented);
}
}
diff --git a/Ultima/Helpers/MoveToFront.cs b/Ultima/Helpers/MoveToFront.cs
index 1842f43..9d02bc9 100644
--- a/Ultima/Helpers/MoveToFront.cs
+++ b/Ultima/Helpers/MoveToFront.cs
@@ -27,8 +27,18 @@ public static byte[] Encode(byte[] input)
// complexity : O(256*N) -> O(N)
public static byte[] Decode(byte[] input)
{
- Span symbols = stackalloc byte[256];
byte[] output = new byte[input.Length];
+ Decode(input, output);
+ return output;
+ }
+
+ ///
+ /// MTF-decodes into .
+ /// must be at least .Length long.
+ ///
+ public static void Decode(ReadOnlySpan input, Span output)
+ {
+ Span symbols = stackalloc byte[256];
for (int i = 0; i < 256; i++)
{
@@ -42,8 +52,6 @@ public static byte[] Decode(byte[] input)
MoveToFront(symbols, ind);
}
-
- return output;
}
// params : array, element to move
diff --git a/Ultima/Helpers/MythicDecompress.cs b/Ultima/Helpers/MythicDecompress.cs
index 1c209ac..d5dc586 100644
--- a/Ultima/Helpers/MythicDecompress.cs
+++ b/Ultima/Helpers/MythicDecompress.cs
@@ -1,5 +1,6 @@
using System;
-using System.Collections.Generic;
+using System.Buffers;
+using System.Buffers.Binary;
using System.IO;
using System.Runtime.InteropServices;
@@ -7,6 +8,9 @@ namespace Ultima.Helpers
{
public static class MythicDecompress
{
+ private const uint HeaderXorKey = 0x8E2C9A3D;
+ private const int FrequencyHeaderSize = 1024; // 256 ints
+
public static byte[] Transform(byte[] buffer)
{
return MoveToFrontCoding.Encode(InternalCompress(buffer));
@@ -30,60 +34,155 @@ public static byte[] Decompress(byte[] buffer)
///
public static byte[] Decompress(byte[] buffer, int offset, int length)
{
- byte[] output;
+ ReadOnlySpan source = buffer.AsSpan(offset, length);
+ uint dataLength = PeekDecompressedLength(source);
+
+ byte[] output = new byte[dataLength];
+ if (!TryDecompress(source, output, out int written) || written != (int)dataLength)
+ {
+ throw new InvalidDataException(
+ $"Decompressed length {written} does not match expected {dataLength}. File is not in compressed cliloc format.");
+ }
+
+ return output;
+ }
+
+ ///
+ /// Reads the embedded decompressed length from the 4-byte XOR-obfuscated
+ /// header at the start of a Mythic payload. Lets callers size an
+ /// rent exactly before calling
+ /// .
+ ///
+ public static uint PeekDecompressedLength(ReadOnlySpan source)
+ {
+ if (source.Length < 4)
+ {
+ return 0;
+ }
+
+ return BinaryPrimitives.ReadUInt32LittleEndian(source) ^ HeaderXorKey;
+ }
+
+ ///
+ /// Pooled-friendly decompression. Reads the header, MTF-decodes the
+ /// payload into a rented scratch span, and writes the final output
+ /// into . Returns false if the
+ /// destination is too small or the payload is malformed.
+ ///
+ public static bool TryDecompress(ReadOnlySpan source, Span destination, out int written)
+ {
+ written = 0;
- using (var ms = new MemoryStream(buffer, offset, length, writable: false))
- using (var reader = new BinaryReader(ms))
+ if (source.Length < 4)
{
- var header = reader.ReadUInt32();
- uint dataLength = header ^ 0x8E2C9A3D;
+ return false;
+ }
+
+ uint dataLength = BinaryPrimitives.ReadUInt32LittleEndian(source) ^ HeaderXorKey;
+ if (destination.Length < (int)dataLength)
+ {
+ return false;
+ }
- var list = reader.ReadBytes(length - 4);
- output = InternalDecompress(MoveToFrontCoding.Decode(list));
+ ReadOnlySpan mtfInput = source.Slice(4);
- if (output.Length != dataLength)
+ byte[] rented = ArrayPool.Shared.Rent(mtfInput.Length);
+ try
+ {
+ Span mtfBuffer = rented.AsSpan(0, mtfInput.Length);
+ MoveToFrontCoding.Decode(mtfInput, mtfBuffer);
+
+ if (!TryInternalDecompress(mtfBuffer, destination, out written))
{
- throw new InvalidDataException(
- $"Decompressed length {output.Length} does not match expected {dataLength}. File is not in compressed cliloc format.");
+ return false;
}
+
+ return written == (int)dataLength;
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(rented);
+ }
+ }
+
+ public static byte[] InternalDecompress(Span input)
+ {
+ if (input.Length < FrequencyHeaderSize)
+ {
+ throw new InvalidDataException("Mythic payload smaller than frequency header.");
+ }
+
+ Span header = stackalloc int[256];
+ input.Slice(0, FrequencyHeaderSize).CopyTo(MemoryMarshal.AsBytes(header));
+
+ int sum = 0;
+ for (int i = 0; i < 256; i++)
+ {
+ sum += header[i];
+ }
+
+ if (sum == 0)
+ {
+ return Array.Empty();
+ }
+
+ byte[] output = new byte[sum];
+ if (!TryInternalDecompress(input, output, out int written) || written != sum)
+ {
+ throw new InvalidDataException("Mythic decompression produced unexpected length.");
}
return output;
}
- public static byte[] InternalDecompress(Span input)
+ ///
+ /// Mythic stage 2: turns the MTF-decoded payload into the original
+ /// bytes, writing into . Returns false
+ /// if the destination is too small or the input is malformed.
+ ///
+ public static bool TryInternalDecompress(ReadOnlySpan input, Span destination, out int written)
{
+ written = 0;
+
try
{
+ if (input.Length < FrequencyHeaderSize)
+ {
+ return false;
+ }
+
Span symbolTable = stackalloc byte[256];
Span frequency = stackalloc byte[256];
Span partialInput = stackalloc int[256 * 3];
partialInput.Clear();
- for (var i = 0; i < 256; i++)
+ for (int i = 0; i < 256; i++)
{
symbolTable[i] = (byte)i;
}
- input.Slice(0, 1024).CopyTo(MemoryMarshal.AsBytes(partialInput));
+ input.Slice(0, FrequencyHeaderSize).CopyTo(MemoryMarshal.AsBytes(partialInput));
- var sum = 0;
- for (var i = 0; i < 256; i++)
+ int sum = 0;
+ for (int i = 0; i < 256; i++)
{
sum += partialInput[i];
}
if (sum == 0)
{
- return Array.Empty();
+ written = 0;
+ return true;
}
- var output = new byte[sum];
- var count = 0;
- var nonZeroCount = 0;
+ if (destination.Length < sum)
+ {
+ return false;
+ }
- for (var i = 0; i < 256; i++)
+ int nonZeroCount = 0;
+ for (int i = 0; i < 256; i++)
{
if (partialInput[i] != 0)
{
@@ -96,29 +195,23 @@ public static byte[] InternalDecompress(Span input)
for (int i = 0, m = 0; i < nonZeroCount; ++i)
{
var freq = frequency[i];
- symbolTable[input[m + 1024]] = freq;
+ symbolTable[input[m + FrequencyHeaderSize]] = freq;
partialInput[freq + 256] = m + 1;
- // TODO: check how safe is updating m counter inside a loop
m += partialInput[freq];
partialInput[freq + 512] = m;
}
- var val = symbolTable[0];
-
- // TODO: expression is always false?
- if (sum == 0)
- {
- return output;
- }
+ byte val = symbolTable[0];
+ int count = 0;
do
{
- ref var firstValRef = ref partialInput[val + 256];
- output[count] = val;
+ ref int firstValRef = ref partialInput[val + 256];
+ destination[count] = val;
if (firstValRef < partialInput[val + 512])
{
- var idx = input[firstValRef + 1024];
+ byte idx = input[firstValRef + FrequencyHeaderSize];
firstValRef++;
if (idx != 0)
@@ -139,15 +232,12 @@ public static byte[] InternalDecompress(Span input)
count++;
} while (count < sum);
- return output;
- }
- catch (InvalidDataException)
- {
- throw;
+ written = sum;
+ return true;
}
- catch (Exception ex)
+ catch (Exception)
{
- throw new InvalidDataException("Mythic decompression failed: " + ex.Message, ex);
+ return false;
}
}
@@ -227,36 +317,34 @@ public static byte[] InternalCompress(Span input)
for (int i = 0; i < 256; ++i)
{
- byte[] bytes = BitConverter.GetBytes(partialInput[i]);
- output[i * 4] = bytes[0];
- output[i * 4 + 1] = bytes[1];
- output[i * 4 + 2] = bytes[2];
- output[i * 4 + 3] = bytes[3];
+ BinaryPrimitives.WriteInt32LittleEndian(output.AsSpan(i * 4, 4), partialInput[i]);
}
int count = input.Length - 1;
- List addedSymbols = new List(256); // keeping track for added symbols
+ Span added = stackalloc bool[256];
+ int addedCount = 0;
do
{
var val = input[count];
- ref var firstValRef = ref partialInput[val + 512];
+ ref int firstValRef = ref partialInput[val + 512];
var outputAddress = firstValRef + 1024;
// first add, just put it in symbolTable from the left and assign 0 idx
- if (!addedSymbols.Contains(val))
+ if (!added[val])
{
- ShiftRight(symbolTable, addedSymbols.Count);
+ ShiftRight(symbolTable, addedCount);
symbolTable[0] = val;
- addedSymbols.Add(val);
+ added[val] = true;
+ addedCount++;
output[outputAddress] = 0;
}
// we're already have symbol in table, so getting it idx and putting it in output stream
- else if (firstValRef >= partialInput[val + 256])
+ else if (firstValRef >= partialInput[val + 256])
{
- var idx = GetIdx(symbolTable, val, addedSymbols.Count);
+ var idx = GetIdx(symbolTable, val, addedCount);
ShiftRight(symbolTable, idx);
symbolTable[0] = val;
output[outputAddress] = idx;
@@ -297,4 +385,4 @@ static void ShiftRight(Span input, int element)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Ultima/Helpers/TileDataHelpers.cs b/Ultima/Helpers/TileDataHelpers.cs
index 5bcfc71..c19c40e 100644
--- a/Ultima/Helpers/TileDataHelpers.cs
+++ b/Ultima/Helpers/TileDataHelpers.cs
@@ -9,6 +9,7 @@
// *
// ***************************************************************************/
+using System;
using System.Globalization;
using System.Text;
@@ -41,6 +42,22 @@ public static string ReadNameString(byte[] buffer, int len)
return Encoding.ASCII.GetString(buffer, 0, count);
}
+ ///
+ /// Reads a NUL-padded ASCII name from the start of ,
+ /// up to 20 bytes. Lets callers avoid pinning + Marshal.PtrToStructure.
+ ///
+ public static string ReadNameString(ReadOnlySpan buffer)
+ {
+ int max = Math.Min(20, buffer.Length);
+ int count = 0;
+ while (count < max && buffer[count] != 0)
+ {
+ count++;
+ }
+
+ return Encoding.ASCII.GetString(buffer.Slice(0, count));
+ }
+
public static int ConvertStringToInt(string text)
{
int result;
diff --git a/Ultima/Hues.cs b/Ultima/Hues.cs
index 9eb4838..6d0f927 100644
--- a/Ultima/Hues.cs
+++ b/Ultima/Hues.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers.Binary;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
@@ -42,33 +43,27 @@ public static void Initialize()
}
_header = new int[blockCount];
- int structSize = Marshal.SizeOf(typeof(HueDataMul));
- var buffer = new byte[blockCount * (4 + (8 * structSize))];
- GCHandle gc = GCHandle.Alloc(buffer, GCHandleType.Pinned);
- try
+
+ // Disk layout per HueDataMul: 32 ushorts (64) + 2 ushorts (4) + 20-byte name = 88 bytes.
+ // Each block = 4-byte header + 8 * 88 = 708 bytes.
+ const int hueDataSize = 88;
+ const int blockSize = 4 + 8 * hueDataSize;
+ var buffer = new byte[blockCount * blockSize];
+ fs.ReadExactly(buffer, 0, buffer.Length);
+ ReadOnlySpan bufferSpan = buffer;
+
+ int cursor = 0;
+ for (int i = 0; i < blockCount; ++i)
{
- fs.ReadExactly(buffer, 0, buffer.Length);
- long currentPos = 0;
+ _header[i] = BinaryPrimitives.ReadInt32LittleEndian(bufferSpan.Slice(cursor));
+ cursor += 4;
- for (int i = 0; i < blockCount; ++i)
+ for (int j = 0; j < 8; ++j, ++index)
{
- var ptrHeader = new IntPtr(gc.AddrOfPinnedObject() + currentPos);
- currentPos += 4;
- _header[i] = (int)Marshal.PtrToStructure(ptrHeader, typeof(int));
-
- for (int j = 0; j < 8; ++j, ++index)
- {
- var ptr = new IntPtr(gc.AddrOfPinnedObject() + currentPos);
- currentPos += structSize;
- var cur = (HueDataMul)Marshal.PtrToStructure(ptr, typeof(HueDataMul));
- List[index] = new Hue(index, cur);
- }
+ List[index] = new Hue(index, bufferSpan.Slice(cursor, hueDataSize));
+ cursor += hueDataSize;
}
}
- finally
- {
- gc.Free();
- }
}
}
@@ -304,6 +299,33 @@ public Hue(int index, HueDataMul mulStruct)
Name = Name.Replace("\n", " ");
}
+ ///
+ /// Builds a Hue directly from the on-disk byte layout: 32 ushorts of
+ /// colors, then tableStart / tableEnd ushorts, then a 20-byte ASCII
+ /// name. Lets the loader skip Marshal.PtrToStructure boxing per hue.
+ ///
+ public Hue(int index, ReadOnlySpan data)
+ {
+ Index = index;
+ Colors = new ushort[32];
+ for (int i = 0; i < 32; ++i)
+ {
+ ushort c = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(i * 2));
+ if (c == 0 || c > 0x7fff)
+ {
+ c = 1;
+ }
+
+ Colors[i] = c;
+ }
+
+ TableStart = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(64));
+ TableEnd = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(66));
+
+ Name = TileDataHelpers.ReadNameString(data.Slice(68, 20));
+ Name = Name.Replace("\n", " ");
+ }
+
///
/// Applies Hue to Bitmap
///
diff --git a/Ultima/RadarCol.cs b/Ultima/RadarCol.cs
index 42b3d82..b37377e 100644
--- a/Ultima/RadarCol.cs
+++ b/Ultima/RadarCol.cs
@@ -1,4 +1,5 @@
-using System.Globalization;
+using System;
+using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
@@ -42,11 +43,7 @@ public static void Initialize()
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
Colors = new ushort[fs.Length / 2];
- GCHandle gc = GCHandle.Alloc(Colors, GCHandleType.Pinned);
- var buffer = new byte[(int)fs.Length];
- fs.ReadExactly(buffer, 0, (int)fs.Length);
- Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)fs.Length);
- gc.Free();
+ fs.ReadExactly(MemoryMarshal.AsBytes(Colors.AsSpan()));
}
}
else
diff --git a/Ultima/StringList.cs b/Ultima/StringList.cs
index 7aaafef..276dfde 100644
--- a/Ultima/StringList.cs
+++ b/Ultima/StringList.cs
@@ -1,4 +1,6 @@
using System;
+using System.Buffers;
+using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Text;
@@ -24,7 +26,6 @@ public sealed class StringList
private Dictionary _stringTable;
private Dictionary _entryTable;
- private static byte[] _buffer = new byte[1024];
///
/// Initialize of Language
@@ -134,95 +135,107 @@ private static ParseResult TryParse(byte[] buffer, bool decompress)
EntryTable = new Dictionary(),
};
- byte[] clilocData;
+ byte[] rented = null;
try
{
- clilocData = decompress ? MythicDecompress.Decompress(buffer) : buffer;
- }
- catch (Exception ex)
- {
- result.ErrorMessage = $"decompression failed: {ex.Message}";
- return result;
- }
-
- // Header is 4 + 2 bytes.
- if (clilocData.Length < 6)
- {
- result.ErrorMessage = $"file is {clilocData.Length} bytes, smaller than the 6-byte header.";
- return result;
- }
-
- using var stream = new MemoryStream(clilocData);
- using var reader = new BinaryReader(stream);
- result.Header1 = reader.ReadInt32();
- result.Header2 = reader.ReadInt16();
-
- int lastNumber = -1;
- while (stream.Position < stream.Length)
- {
- long entryStart = stream.Position;
- long remaining = stream.Length - entryStart;
+ ReadOnlySpan data;
- // Each entry header is 4 (number) + 1 (flag) + 2 (length) = 7 bytes.
- if (remaining < 7)
+ if (decompress)
{
- result.ErrorMessage =
- $"unexpected {remaining} trailing byte(s) at offset 0x{entryStart:X} after entry #{lastNumber}; " +
- $"need 7 bytes for the next entry header.";
- return result;
- }
+ uint expectedLen = MythicDecompress.PeekDecompressedLength(buffer);
+ if (expectedLen == 0 || expectedLen > int.MaxValue)
+ {
+ result.ErrorMessage = "decompression failed: invalid header.";
+ return result;
+ }
- int number = reader.ReadInt32();
- byte flag = reader.ReadByte();
- // Writer emits ushort; reading as signed Int16 truncates strings ≥32768 bytes to a negative length.
- int length = reader.ReadUInt16();
+ rented = ArrayPool.Shared.Rent((int)expectedLen);
+ if (!MythicDecompress.TryDecompress(buffer, rented.AsSpan(0, (int)expectedLen), out int written))
+ {
+ result.ErrorMessage = "decompression failed.";
+ return result;
+ }
- long bodyRemaining = stream.Length - stream.Position;
- if (length > bodyRemaining)
- {
- result.ErrorMessage =
- $"entry #{number} at offset 0x{entryStart:X} declares length {length}, " +
- $"but only {bodyRemaining} byte(s) remain in the file " +
- $"(previous entry was #{lastNumber}, parsed {result.EntriesParsed} so far).";
- return result;
+ data = rented.AsSpan(0, written);
}
-
- if (length > _buffer.Length)
+ else
{
- _buffer = new byte[(length + 1023) & ~1023];
+ data = buffer;
}
- int read = reader.Read(_buffer, 0, length);
- if (read != length)
+ // Header is 4 + 2 bytes.
+ if (data.Length < 6)
{
- result.ErrorMessage =
- $"entry #{number} at offset 0x{entryStart:X} expected {length} body byte(s) " +
- $"but only {read} were available.";
+ result.ErrorMessage = $"file is {data.Length} bytes, smaller than the 6-byte header.";
return result;
}
- string text;
- try
+ result.Header1 = BinaryPrimitives.ReadInt32LittleEndian(data);
+ result.Header2 = BinaryPrimitives.ReadInt16LittleEndian(data.Slice(4));
+
+ int cursor = 6;
+ int lastNumber = -1;
+ while (cursor < data.Length)
{
- text = Encoding.UTF8.GetString(_buffer, 0, length);
+ int entryStart = cursor;
+ int remaining = data.Length - cursor;
+
+ // Each entry header is 4 (number) + 1 (flag) + 2 (length) = 7 bytes.
+ if (remaining < 7)
+ {
+ result.ErrorMessage =
+ $"unexpected {remaining} trailing byte(s) at offset 0x{entryStart:X} after entry #{lastNumber}; " +
+ $"need 7 bytes for the next entry header.";
+ return result;
+ }
+
+ int number = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(cursor));
+ byte flag = data[cursor + 4];
+ // Writer emits ushort; reading as signed Int16 truncates strings ≥32768 bytes to a negative length.
+ int length = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(cursor + 5));
+ cursor += 7;
+
+ int bodyRemaining = data.Length - cursor;
+ if (length > bodyRemaining)
+ {
+ result.ErrorMessage =
+ $"entry #{number} at offset 0x{entryStart:X} declares length {length}, " +
+ $"but only {bodyRemaining} byte(s) remain in the file " +
+ $"(previous entry was #{lastNumber}, parsed {result.EntriesParsed} so far).";
+ return result;
+ }
+
+ string text;
+ try
+ {
+ text = Encoding.UTF8.GetString(data.Slice(cursor, length));
+ }
+ catch (Exception ex)
+ {
+ result.ErrorMessage =
+ $"entry #{number} at offset 0x{entryStart:X} has {length} body bytes that are not valid UTF-8: {ex.Message}";
+ return result;
+ }
+ cursor += length;
+
+ var se = new StringEntry(number, text, flag);
+ result.Entries.Add(se);
+ result.StringTable[number] = text;
+ result.EntryTable[number] = se;
+ result.EntriesParsed++;
+ lastNumber = number;
}
- catch (Exception ex)
+
+ result.Success = true;
+ return result;
+ }
+ finally
+ {
+ if (rented != null)
{
- result.ErrorMessage =
- $"entry #{number} at offset 0x{entryStart:X} has {length} body bytes that are not valid UTF-8: {ex.Message}";
- return result;
+ ArrayPool.Shared.Return(rented);
}
-
- var se = new StringEntry(number, text, flag);
- result.Entries.Add(se);
- result.StringTable[number] = text;
- result.EntryTable[number] = se;
- result.EntriesParsed++;
- lastNumber = number;
}
-
- result.Success = true;
- return result;
}
///
diff --git a/Ultima/TileData.cs b/Ultima/TileData.cs
index b931396..66d7ba1 100644
--- a/Ultima/TileData.cs
+++ b/Ultima/TileData.cs
@@ -1,5 +1,7 @@
using System;
+using System.Buffers.Binary;
using System.IO;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Ultima.Helpers;
@@ -1331,74 +1333,63 @@ public static unsafe void Initialize()
LandTable = new LandData[0x4000];
var buffer = new byte[fs.Length];
- GCHandle gc = GCHandle.Alloc(buffer, GCHandleType.Pinned);
- long currentPos = 0;
- try
+ fs.ReadExactly(buffer, 0, buffer.Length);
+ int currentPos = 0;
+
+ int landStructSize = useNeWTileDataFormat ? sizeof(NewLandTileDataMul) : sizeof(OldLandTileDataMul);
+
+ for (int i = 0; i < 0x4000; i += 32)
{
- fs.ReadExactly(buffer, 0, buffer.Length);
- for (int i = 0; i < 0x4000; i += 32)
+ _landHeader[j++] = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(currentPos));
+ currentPos += 4;
+ for (int count = 0; count < 32; ++count)
{
- var ptrHeader = new IntPtr(gc.AddrOfPinnedObject() + currentPos);
- currentPos += 4;
- _landHeader[j++] = (int)Marshal.PtrToStructure(ptrHeader, typeof(int));
- for (int count = 0; count < 32; ++count)
+ if (useNeWTileDataFormat)
+ {
+ var cur = Unsafe.ReadUnaligned(ref buffer[currentPos]);
+ LandTable[i + count] = new LandData(cur);
+ }
+ else
{
- var ptr = new IntPtr(gc.AddrOfPinnedObject() + currentPos);
- if (useNeWTileDataFormat)
- {
- currentPos += sizeof(NewLandTileDataMul);
- var cur = (NewLandTileDataMul)Marshal.PtrToStructure(ptr, typeof(NewLandTileDataMul));
- LandTable[i + count] = new LandData(cur);
- }
- else
- {
- currentPos += sizeof(OldLandTileDataMul);
- var cur = (OldLandTileDataMul)Marshal.PtrToStructure(ptr, typeof(OldLandTileDataMul));
- LandTable[i + count] = new LandData(cur);
- }
+ var cur = Unsafe.ReadUnaligned(ref buffer[currentPos]);
+ LandTable[i + count] = new LandData(cur);
}
+ currentPos += landStructSize;
}
+ }
- long remaining = buffer.Length - currentPos;
+ long remaining = buffer.Length - currentPos;
- int structSize = useNeWTileDataFormat ? sizeof(NewItemTileDataMul) : sizeof(OldItemTileDataMul);
+ int structSize = useNeWTileDataFormat ? sizeof(NewItemTileDataMul) : sizeof(OldItemTileDataMul);
- _itemHeader = new int[remaining / ((structSize * 32) + 4)];
- int itemLength = _itemHeader.Length * 32;
+ _itemHeader = new int[remaining / ((structSize * 32) + 4)];
+ int itemLength = _itemHeader.Length * 32;
- ItemTable = new ItemData[itemLength];
- HeightTable = new int[itemLength];
+ ItemTable = new ItemData[itemLength];
+ HeightTable = new int[itemLength];
- j = 0;
- for (int i = 0; i < itemLength; i += 32)
+ j = 0;
+ for (int i = 0; i < itemLength; i += 32)
+ {
+ _itemHeader[j++] = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(currentPos));
+ currentPos += 4;
+ for (int count = 0; count < 32; ++count)
{
- var ptrHeader = new IntPtr(gc.AddrOfPinnedObject() + currentPos);
- currentPos += 4;
- _itemHeader[j++] = (int)Marshal.PtrToStructure(ptrHeader, typeof(int));
- for (int count = 0; count < 32; ++count)
+ if (useNeWTileDataFormat)
+ {
+ var cur = Unsafe.ReadUnaligned(ref buffer[currentPos]);
+ ItemTable[i + count] = new ItemData(cur);
+ HeightTable[i + count] = cur.height;
+ }
+ else
{
- var ptr = new IntPtr(gc.AddrOfPinnedObject() + currentPos);
- if (useNeWTileDataFormat)
- {
- currentPos += sizeof(NewItemTileDataMul);
- var cur = (NewItemTileDataMul)Marshal.PtrToStructure(ptr, typeof(NewItemTileDataMul));
- ItemTable[i + count] = new ItemData(cur);
- HeightTable[i + count] = cur.height;
- }
- else
- {
- currentPos += sizeof(OldItemTileDataMul);
- var cur = (OldItemTileDataMul)Marshal.PtrToStructure(ptr, typeof(OldItemTileDataMul));
- ItemTable[i + count] = new ItemData(cur);
- HeightTable[i + count] = cur.height;
- }
+ var cur = Unsafe.ReadUnaligned(ref buffer[currentPos]);
+ ItemTable[i + count] = new ItemData(cur);
+ HeightTable[i + count] = cur.height;
}
+ currentPos += structSize;
}
}
- finally
- {
- gc.Free();
- }
}
}
diff --git a/Ultima/TileMatrix.cs b/Ultima/TileMatrix.cs
index 67bc0fe..1395456 100644
--- a/Ultima/TileMatrix.cs
+++ b/Ultima/TileMatrix.cs
@@ -239,11 +239,9 @@ private void InitStatics()
{
_statics = new FileStream(_staticsPath, FileMode.Open, FileAccess.Read, FileShare.Read);
- GCHandle gc = GCHandle.Alloc(_staticIndex, GCHandleType.Pinned);
- var buffer = new byte[index.Length];
- index.ReadExactly(buffer, 0, (int)index.Length);
- Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)Math.Min(index.Length, BlockHeight * BlockWidth * 12));
- gc.Free();
+ int readLen = (int)Math.Min(index.Length, (long)BlockHeight * BlockWidth * 12);
+ index.ReadExactly(MemoryMarshal.AsBytes(_staticIndex.AsSpan()).Slice(0, readLen));
+
for (var i = (int)Math.Min(index.Length, BlockHeight * BlockWidth); i < BlockHeight * BlockWidth; ++i)
{
_staticIndex[i].Lookup = -1;
@@ -294,53 +292,47 @@ private unsafe HuedTile[][][] ReadStaticBlock(int x, int y)
_buffer = new byte[length];
}
- GCHandle gc = GCHandle.Alloc(_buffer, GCHandleType.Pinned);
- try
+ _statics.ReadExactly(_buffer, 0, length);
+
+ if (_lists == null)
{
- _statics.ReadExactly(_buffer, 0, length);
+ _lists = new HuedTileList[8][];
- if (_lists == null)
+ for (int i = 0; i < 8; ++i)
{
- _lists = new HuedTileList[8][];
+ _lists[i] = new HuedTileList[8];
- for (int i = 0; i < 8; ++i)
+ for (int j = 0; j < 8; ++j)
{
- _lists[i] = new HuedTileList[8];
-
- for (int j = 0; j < 8; ++j)
- {
- _lists[i][j] = new HuedTileList();
- }
+ _lists[i][j] = new HuedTileList();
}
}
+ }
- HuedTileList[][] lists = _lists;
-
- for (int i = 0; i < count; ++i)
- {
- var ptr = new IntPtr((long)gc.AddrOfPinnedObject() + (i * sizeof(StaticTile)));
- var cur = (StaticTile)Marshal.PtrToStructure(ptr, typeof(StaticTile));
- lists[cur.X & 0x7][cur.Y & 0x7].Add(Art.GetLegalItemId(cur.Id), cur.Hue, cur.Z);
- }
+ HuedTileList[][] lists = _lists;
- var tiles = new HuedTile[8][][];
+ ReadOnlySpan staticTiles = MemoryMarshal.Cast(
+ _buffer.AsSpan(0, count * sizeof(StaticTile)));
- for (int i = 0; i < 8; ++i)
- {
- tiles[i] = new HuedTile[8][];
+ for (int i = 0; i < count; ++i)
+ {
+ StaticTile cur = staticTiles[i];
+ lists[cur.X & 0x7][cur.Y & 0x7].Add(Art.GetLegalItemId(cur.Id), cur.Hue, cur.Z);
+ }
- for (int j = 0; j < 8; ++j)
- {
- tiles[i][j] = lists[i][j].ToArray();
- }
- }
+ var tiles = new HuedTile[8][][];
- return tiles;
- }
- finally
+ for (int i = 0; i < 8; ++i)
{
- gc.Free();
+ tiles[i] = new HuedTile[8][];
+
+ for (int j = 0; j < 8; ++j)
+ {
+ tiles[i][j] = lists[i][j].ToArray();
+ }
}
+
+ return tiles;
}
/*
@@ -488,22 +480,7 @@ private Tile[] ReadLandBlock(int x, int y)
_map.Seek(offset, SeekOrigin.Begin);
- GCHandle gc = GCHandle.Alloc(tiles, GCHandleType.Pinned);
- try
- {
- if (_buffer == null || _buffer.Length < 192)
- {
- _buffer = new byte[192];
- }
-
- _map.ReadExactly(_buffer, 0, 192);
-
- Marshal.Copy(_buffer, 0, gc.AddrOfPinnedObject(), 192);
- }
- finally
- {
- gc.Free();
- }
+ _map.ReadExactly(MemoryMarshal.AsBytes(tiles.AsSpan()));
return tiles;
}
diff --git a/Ultima/TileMatrixPatch.cs b/Ultima/TileMatrixPatch.cs
index f66b3dd..d63f2c2 100644
--- a/Ultima/TileMatrixPatch.cs
+++ b/Ultima/TileMatrixPatch.cs
@@ -14,7 +14,6 @@ public sealed class TileMatrixPatch
private readonly int _blockWidth;
private readonly int _blockHeight;
- private static byte[] _buffer;
private static StaticTile[] _tileBuffer = new StaticTile[128];
public bool IsLandBlockPatched(int x, int y)
@@ -185,22 +184,7 @@ private int PatchLand(TileMatrix matrix, string dataPath, string indexPath)
var tiles = new Tile[64];
- GCHandle gc = GCHandle.Alloc(tiles, GCHandleType.Pinned);
- try
- {
- if (_buffer == null || _buffer.Length < 192)
- {
- _buffer = new byte[192];
- }
-
- fsData.ReadExactly(_buffer, 0, 192);
-
- Marshal.Copy(_buffer, 0, gc.AddrOfPinnedObject(), 192);
- }
- finally
- {
- gc.Free();
- }
+ fsData.ReadExactly(MemoryMarshal.AsBytes(tiles.AsSpan()));
if (LandBlocks[x] == null)
{
@@ -269,47 +253,32 @@ private int PatchStatics(TileMatrix matrix, string dataPath, string indexPath, s
StaticTile[] staTiles = _tileBuffer;
- GCHandle gc = GCHandle.Alloc(staTiles, GCHandleType.Pinned);
- try
- {
- if (_buffer == null || _buffer.Length < length)
- {
- _buffer = new byte[length];
- }
-
- fsData.ReadExactly(_buffer, 0, length);
-
- Marshal.Copy(_buffer, 0, gc.AddrOfPinnedObject(), length);
+ fsData.ReadExactly(MemoryMarshal.AsBytes(staTiles.AsSpan(0, tileCount)));
- for (int j = 0; j < tileCount; ++j)
- {
- StaticTile cur = staTiles[j];
- lists[cur.X & 0x7][cur.Y & 0x7].Add(Art.GetLegalItemId(cur.Id), cur.Hue, cur.Z);
- }
-
- var tiles = new HuedTile[8][][];
+ for (int j = 0; j < tileCount; ++j)
+ {
+ StaticTile cur = staTiles[j];
+ lists[cur.X & 0x7][cur.Y & 0x7].Add(Art.GetLegalItemId(cur.Id), cur.Hue, cur.Z);
+ }
- for (int x = 0; x < 8; ++x)
- {
- tiles[x] = new HuedTile[8][];
+ var tiles = new HuedTile[8][][];
- for (int y = 0; y < 8; ++y)
- {
- tiles[x][y] = lists[x][y].ToArray();
- }
- }
+ for (int x = 0; x < 8; ++x)
+ {
+ tiles[x] = new HuedTile[8][];
- if (StaticBlocks[blockX] == null)
+ for (int y = 0; y < 8; ++y)
{
- StaticBlocks[blockX] = new HuedTile[matrix.BlockHeight][][][];
+ tiles[x][y] = lists[x][y].ToArray();
}
-
- StaticBlocks[blockX][blockY] = tiles;
}
- finally
+
+ if (StaticBlocks[blockX] == null)
{
- gc.Free();
+ StaticBlocks[blockX] = new HuedTile[matrix.BlockHeight][][][];
}
+
+ StaticBlocks[blockX][blockY] = tiles;
}
return count;
diff --git a/UoFiddler.Controls/UserControls/GumpControl.cs b/UoFiddler.Controls/UserControls/GumpControl.cs
index b5f657e..a0a5b81 100644
--- a/UoFiddler.Controls/UserControls/GumpControl.cs
+++ b/UoFiddler.Controls/UserControls/GumpControl.cs
@@ -835,21 +835,7 @@ private void OnClickPreLoad(object sender, EventArgs e)
private void PreLoaderDoWork(object sender, DoWorkEventArgs e)
{
- int total = Gumps.GetCount();
- int reportEvery = Math.Max(1, total / 200);
- int sinceReport = 0;
- int done = 0;
- for (int i = 0; i < total; ++i)
- {
- Gumps.GetGump(i);
- ++done;
- if (++sinceReport >= reportEvery)
- {
- sinceReport = 0;
- PreLoader.ReportProgress(done);
- }
- }
- PreLoader.ReportProgress(done);
+ Gumps.PreloadParallel(0, done => PreLoader.ReportProgress(done));
}
private void PreLoaderProgressChanged(object sender, ProgressChangedEventArgs e)
diff --git a/UoFiddler.Plugin.Compare/Classes/SecondFileAccessor.cs b/UoFiddler.Plugin.Compare/Classes/SecondFileAccessor.cs
index 8352037..1e4e445 100644
--- a/UoFiddler.Plugin.Compare/Classes/SecondFileAccessor.cs
+++ b/UoFiddler.Plugin.Compare/Classes/SecondFileAccessor.cs
@@ -121,11 +121,8 @@ public SecondMulFileAccessor(string idxPath, string mulPath, int length)
int count = (int)(idx.Length / 12);
IdxLength = idx.Length;
- GCHandle gc = GCHandle.Alloc(Index, GCHandleType.Pinned);
- byte[] buffer = new byte[idx.Length];
- idx.ReadExactly(buffer, 0, (int)idx.Length);
- Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)Math.Min(IdxLength, Index.Length * 12L));
- gc.Free();
+ int readLen = (int)Math.Min(IdxLength, (long)Index.Length * 12);
+ idx.ReadExactly(MemoryMarshal.AsBytes(Index.AsSpan()).Slice(0, readLen));
for (int i = count; i < Index.Length; ++i)
{
diff --git a/UoFiddler.Plugin.Compare/Classes/SecondGump.cs b/UoFiddler.Plugin.Compare/Classes/SecondGump.cs
index e88e395..873a868 100644
--- a/UoFiddler.Plugin.Compare/Classes/SecondGump.cs
+++ b/UoFiddler.Plugin.Compare/Classes/SecondGump.cs
@@ -9,6 +9,8 @@
*
***************************************************************************/
+using System;
+using System.Buffers;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
@@ -197,38 +199,73 @@ private static int ReadEntryPayload(Stream stream, SecondIEntry entry, out int w
if (entry.Flag >= SecondCompressionFlag.Zlib)
{
- byte[] compressed = new byte[length];
- System.Buffer.BlockCopy(_streamBuffer, 0, compressed, 0, length);
-
- var result = UopUtils.Decompress(compressed);
- if (!result.success)
+ int decSize = entry.DecompressedLength;
+ if (decSize <= 8)
{
width = height = -1;
return 0;
}
- byte[] decompressed = entry.Flag == SecondCompressionFlag.Mythic
- ? MythicDecompress.Decompress(result.data)
- : result.data;
-
- if (decompressed == null || decompressed.Length < 8)
+ byte[] zlibBuf = ArrayPool.Shared.Rent(decSize);
+ byte[] mythicBuf = null;
+ try
{
- width = height = -1;
- return 0;
- }
+ if (!UopUtils.TryDecompressInto(_streamBuffer, 0, length, zlibBuf, out int zlibLen))
+ {
+ width = height = -1;
+ return 0;
+ }
+
+ byte[] payload;
+ int payloadLength;
+
+ if (entry.Flag == SecondCompressionFlag.Mythic)
+ {
+ uint mythicLen = MythicDecompress.PeekDecompressedLength(zlibBuf.AsSpan(0, zlibLen));
+ if (mythicLen <= 8 || mythicLen > int.MaxValue)
+ {
+ width = height = -1;
+ return 0;
+ }
- width = (decompressed[3] << 24) | (decompressed[2] << 16) | (decompressed[1] << 8) | decompressed[0];
- height = (decompressed[7] << 24) | (decompressed[6] << 16) | (decompressed[5] << 8) | decompressed[4];
- entry.Extra1 = width;
- entry.Extra2 = height;
+ mythicBuf = ArrayPool.Shared.Rent((int)mythicLen);
+ if (!MythicDecompress.TryDecompress(
+ zlibBuf.AsSpan(0, zlibLen), mythicBuf.AsSpan(0, (int)mythicLen), out _))
+ {
+ width = height = -1;
+ return 0;
+ }
+
+ payload = mythicBuf;
+ payloadLength = (int)mythicLen;
+ }
+ else
+ {
+ payload = zlibBuf;
+ payloadLength = zlibLen;
+ }
- int rleLen = decompressed.Length - 8;
- if (_streamBuffer.Length < rleLen)
+ width = (payload[3] << 24) | (payload[2] << 16) | (payload[1] << 8) | payload[0];
+ height = (payload[7] << 24) | (payload[6] << 16) | (payload[5] << 8) | payload[4];
+ entry.Extra1 = width;
+ entry.Extra2 = height;
+
+ int rleLen = payloadLength - 8;
+ if (_streamBuffer.Length < rleLen)
+ {
+ _streamBuffer = new byte[rleLen];
+ }
+ System.Buffer.BlockCopy(payload, 8, _streamBuffer, 0, rleLen);
+ return rleLen;
+ }
+ finally
{
- _streamBuffer = new byte[rleLen];
+ if (mythicBuf != null)
+ {
+ ArrayPool.Shared.Return(mythicBuf);
+ }
+ ArrayPool.Shared.Return(zlibBuf);
}
- System.Buffer.BlockCopy(decompressed, 8, _streamBuffer, 0, rleLen);
- return rleLen;
}
width = entry.Extra1;
diff --git a/UoFiddler.Plugin.Compare/Classes/SecondHue.cs b/UoFiddler.Plugin.Compare/Classes/SecondHue.cs
index b0fe027..5a1ad44 100644
--- a/UoFiddler.Plugin.Compare/Classes/SecondHue.cs
+++ b/UoFiddler.Plugin.Compare/Classes/SecondHue.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
using Ultima;
namespace UoFiddler.Plugin.Compare.Classes
@@ -20,8 +21,6 @@ public static void Initialize(string path)
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
- BinaryReader bin = new BinaryReader(fs);
-
int blockCount = (int)fs.Length / 708;
if (blockCount > 375)
@@ -29,13 +28,24 @@ public static void Initialize(string path)
blockCount = 375;
}
+ // Disk layout per HueDataMul: 32 ushorts (64) + 2 ushorts (4) + 20-byte name = 88 bytes.
+ // Each block = 4-byte header + 8 * 88 = 708 bytes.
+ const int hueDataSize = 88;
+ const int blockSize = 4 + 8 * hueDataSize;
+ var buffer = new byte[blockCount * blockSize];
+ fs.ReadExactly(buffer, 0, buffer.Length);
+ ReadOnlySpan bufferSpan = buffer;
+
+ int cursor = 0;
for (int i = 0; i < blockCount; ++i)
{
- bin.ReadInt32();
+ // 4-byte header per block is unused on the Compare side.
+ cursor += 4;
for (int j = 0; j < 8; ++j, ++index)
{
- List[index] = new Hue(index, bin);
+ List[index] = new Hue(index, bufferSpan.Slice(cursor, hueDataSize));
+ cursor += hueDataSize;
}
}
}
diff --git a/UoFiddler.Plugin.Compare/Classes/SecondRadarCol.cs b/UoFiddler.Plugin.Compare/Classes/SecondRadarCol.cs
index 86eb492..46ab596 100644
--- a/UoFiddler.Plugin.Compare/Classes/SecondRadarCol.cs
+++ b/UoFiddler.Plugin.Compare/Classes/SecondRadarCol.cs
@@ -1,3 +1,4 @@
+using System;
using System.IO;
using System.Runtime.InteropServices;
@@ -30,11 +31,7 @@ public static bool Initialize(string filePath)
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
var colors = new ushort[fs.Length / 2];
- var buffer = new byte[(int)fs.Length];
- fs.ReadExactly(buffer, 0, (int)fs.Length);
- GCHandle gc = GCHandle.Alloc(colors, GCHandleType.Pinned);
- Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)fs.Length);
- gc.Free();
+ fs.ReadExactly(MemoryMarshal.AsBytes(colors.AsSpan()));
_colors = colors;
}
}
diff --git a/UoFiddler.Plugin.Compare/Classes/SecondTileData.cs b/UoFiddler.Plugin.Compare/Classes/SecondTileData.cs
index ad88f43..466df1e 100644
--- a/UoFiddler.Plugin.Compare/Classes/SecondTileData.cs
+++ b/UoFiddler.Plugin.Compare/Classes/SecondTileData.cs
@@ -1,6 +1,7 @@
using System;
+using System.Buffers.Binary;
using System.IO;
-using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
using Ultima;
namespace UoFiddler.Plugin.Compare.Classes
@@ -34,81 +35,69 @@ public unsafe void Initialize(string path, bool useNeWTileDataFormat)
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
-
var landHeader = new int[512];
int j = 0;
LandTable = new LandData[0x4000];
var buffer = new byte[fs.Length];
- GCHandle gc = GCHandle.Alloc(buffer, GCHandleType.Pinned);
- long currentPos = 0;
- try
+ fs.ReadExactly(buffer, 0, buffer.Length);
+ int currentPos = 0;
+
+ int landStructSize = useNeWTileDataFormat ? sizeof(NewLandTileDataMul) : sizeof(OldLandTileDataMul);
+
+ for (int i = 0; i < 0x4000; i += 32)
{
- fs.ReadExactly(buffer, 0, buffer.Length);
- for (int i = 0; i < 0x4000; i += 32)
+ landHeader[j++] = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(currentPos));
+ currentPos += 4;
+ for (int count = 0; count < 32; ++count)
{
- var ptrHeader = new IntPtr((long)gc.AddrOfPinnedObject() + currentPos);
- currentPos += 4;
- landHeader[j++] = (int)Marshal.PtrToStructure(ptrHeader, typeof(int));
- for (int count = 0; count < 32; ++count)
+ if (useNeWTileDataFormat)
{
- var ptr = new IntPtr((long)gc.AddrOfPinnedObject() + currentPos);
- if (useNeWTileDataFormat)
- {
- currentPos += sizeof(NewLandTileDataMul);
- var cur = (NewLandTileDataMul)Marshal.PtrToStructure(ptr, typeof(NewLandTileDataMul));
- LandTable[i + count] = new LandData(cur);
- }
- else
- {
- currentPos += sizeof(OldLandTileDataMul);
- var cur = (OldLandTileDataMul)Marshal.PtrToStructure(ptr, typeof(OldLandTileDataMul));
- LandTable[i + count] = new LandData(cur);
- }
+ var cur = Unsafe.ReadUnaligned(ref buffer[currentPos]);
+ LandTable[i + count] = new LandData(cur);
}
+ else
+ {
+ var cur = Unsafe.ReadUnaligned(ref buffer[currentPos]);
+ LandTable[i + count] = new LandData(cur);
+ }
+ currentPos += landStructSize;
}
+ }
- long remaining = buffer.Length - currentPos;
+ long remaining = buffer.Length - currentPos;
- int structSize = useNeWTileDataFormat ? sizeof(NewItemTileDataMul) : sizeof(OldItemTileDataMul);
+ int structSize = useNeWTileDataFormat ? sizeof(NewItemTileDataMul) : sizeof(OldItemTileDataMul);
- var itemHeader = new int[remaining / ((structSize * 32) + 4)];
- int itemLength = itemHeader.Length * 32;
+ var itemHeader = new int[remaining / ((structSize * 32) + 4)];
+ int itemLength = itemHeader.Length * 32;
- ItemTable = new ItemData[itemLength];
- HeightTable = new int[itemLength];
+ ItemTable = new ItemData[itemLength];
+ HeightTable = new int[itemLength];
- j = 0;
- for (int i = 0; i < itemLength; i += 32)
+ j = 0;
+ for (int i = 0; i < itemLength; i += 32)
+ {
+ itemHeader[j++] = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(currentPos));
+ currentPos += 4;
+ for (int count = 0; count < 32; ++count)
{
- var ptrHeader = new IntPtr((long)gc.AddrOfPinnedObject() + currentPos);
- currentPos += 4;
- itemHeader[j++] = (int)Marshal.PtrToStructure(ptrHeader, typeof(int));
- for (int count = 0; count < 32; ++count)
+ if (useNeWTileDataFormat)
+ {
+ var cur = Unsafe.ReadUnaligned(ref buffer[currentPos]);
+ ItemTable[i + count] = new ItemData(cur);
+ HeightTable[i + count] = cur.height;
+ }
+ else
{
- var ptr = new IntPtr((long)gc.AddrOfPinnedObject() + currentPos);
- if (useNeWTileDataFormat)
- {
- currentPos += sizeof(NewItemTileDataMul);
- var cur = (NewItemTileDataMul)Marshal.PtrToStructure(ptr, typeof(NewItemTileDataMul));
- ItemTable[i + count] = new ItemData(cur);
- HeightTable[i + count] = cur.height;
- }
- else
- {
- currentPos += sizeof(OldItemTileDataMul);
- var cur = (OldItemTileDataMul)Marshal.PtrToStructure(ptr, typeof(OldItemTileDataMul));
- ItemTable[i + count] = new ItemData(cur);
- HeightTable[i + count] = cur.height;
- }
+ var cur = Unsafe.ReadUnaligned(ref buffer[currentPos]);
+ ItemTable[i + count] = new ItemData(cur);
+ HeightTable[i + count] = cur.height;
}
+ currentPos += structSize;
}
}
- finally
- {
- gc.Free();
- }
}
}
}
-}
\ No newline at end of file
+}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
index 93e28de..8404564 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
@@ -13,7 +13,9 @@
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
using System.IO;
+using System.Numerics;
using System.Windows.Forms;
using Ultima;
using UoFiddler.Controls.Classes;
@@ -26,6 +28,7 @@ public CompareMapControl()
{
InitializeComponent();
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
+ pictureBox.MouseWheel += OnMouseWheel;
}
private bool _loaded;
@@ -36,8 +39,21 @@ public CompareMapControl()
private Map _originalMap;
private int _currentMapId;
private Bitmap _map;
+ private Bitmap _renderBuffer;
+ private Bitmap _zoomBuffer;
+ private Graphics _zoomBufferGraphics;
private static double _zoom = 1;
- private bool[][][][] _diffs;
+ // One ulong per 8x8 block: bit (xb<<3 | yb) is set when that tile differs.
+ // Flat 1D, indexed as blockX * _diffHeightBlocks + blockY.
+ private ulong[] _diffMasks;
+ private int _diffWidthBlocks;
+ private int _diffHeightBlocks;
+ private readonly System.Diagnostics.Stopwatch _dragRepaintTimer = new System.Diagnostics.Stopwatch();
+ private System.Windows.Forms.Timer _dragTrailTimer;
+ private bool _dragInvalidatePending;
+ private double _dragAccumX;
+ private double _dragAccumY;
+ private const int DragRepaintIntervalMs = 16;
private void OnLoad(object sender, EventArgs e)
{
@@ -128,6 +144,8 @@ private void OnMouseDown(object sender, MouseEventArgs e)
_moving = true;
_movingPoint.X = e.X;
_movingPoint.Y = e.Y;
+ _dragAccumX = 0;
+ _dragAccumY = 0;
Cursor = Cursors.Hand;
}
else
@@ -150,16 +168,25 @@ private void OnMouseMove(object sender, MouseEventArgs e)
{
toolTip1.RemoveAll();
- int deltaX = (int)(-1 * (e.X - _movingPoint.X) / _zoom);
- int deltaY = (int)(-1 * (e.Y - _movingPoint.Y) / _zoom);
+ // Accumulate the fractional part of the drag so high-zoom drags (where 1 mouse pixel
+ // is less than 1 tile) don't lose precision.
+ _dragAccumX += -(e.X - _movingPoint.X) / _zoom;
+ _dragAccumY += -(e.Y - _movingPoint.Y) / _zoom;
+
+ int deltaX = (int)_dragAccumX;
+ int deltaY = (int)_dragAccumY;
+ _dragAccumX -= deltaX;
+ _dragAccumY -= deltaY;
_movingPoint.X = e.X;
_movingPoint.Y = e.Y;
- hScrollBar.Value = Math.Max(0, Math.Min(hScrollBar.Maximum, hScrollBar.Value + deltaX));
- vScrollBar.Value = Math.Max(0, Math.Min(vScrollBar.Maximum, vScrollBar.Value + deltaY));
-
- pictureBox.Invalidate();
+ if (deltaX != 0 || deltaY != 0)
+ {
+ hScrollBar.Value = Math.Max(0, Math.Min(hScrollBar.Maximum, hScrollBar.Value + deltaX));
+ vScrollBar.Value = Math.Max(0, Math.Min(vScrollBar.Maximum, vScrollBar.Value + deltaY));
+ RequestDragRepaint();
+ }
}
else if (_zoom >= 2 && _currentMap != null)
{
@@ -270,127 +297,181 @@ private void OnPaint(object sender, PaintEventArgs e)
return;
}
- if (showMap1ToolStripMenuItem.Checked)
+ Map drawMap = showMap1ToolStripMenuItem.Checked ? _originalMap : _currentMap;
+ if (drawMap == null)
{
- _map = _originalMap.GetImage(hScrollBar.Value >> 3, vScrollBar.Value >> 3,
- (int)((e.ClipRectangle.Width / _zoom) + 8) >> 3, (int)((e.ClipRectangle.Height / _zoom) + 8) >> 3,
- true);
+ return;
}
- else
+
+ int blockX = hScrollBar.Value >> 3;
+ int blockY = vScrollBar.Value >> 3;
+ // +16 (2 blocks of padding) so the sub-block scroll offset never reveals empty space
+ // along the right/bottom edge of the viewport.
+ int widthBlocks = ((int)Math.Ceiling(e.ClipRectangle.Width / _zoom) + 16) >> 3;
+ int heightBlocks = ((int)Math.Ceiling(e.ClipRectangle.Height / _zoom) + 16) >> 3;
+
+ int bufferPixelW = widthBlocks << 3;
+ int bufferPixelH = heightBlocks << 3;
+ _map = EnsureRenderBuffer(bufferPixelW, bufferPixelH);
+
+ drawMap.GetImage(blockX, blockY, widthBlocks, heightBlocks, _map, true);
+
+ if (_currentMap != null && showDifferencesToolStripMenuItem.Checked && _diffMasks != null)
{
- _map = _currentMap.GetImage(hScrollBar.Value >> 3, vScrollBar.Value >> 3,
- (int)((e.ClipRectangle.Width / _zoom) + 8) >> 3, (int)((e.ClipRectangle.Height / _zoom) + 8) >> 3,
- true);
+ DrawDiffOverlay(blockX, blockY, widthBlocks, heightBlocks);
}
- if (_currentMap != null && showDifferencesToolStripMenuItem.Checked)
+ if (markDiffToolStripMenuItem.Checked)
{
- using (Graphics mapg = Graphics.FromImage(_map))
+ int count = drawMap.Tiles.Patch.LandBlocksCount + drawMap.Tiles.Patch.StaticBlocksCount;
+ if (count > 0)
{
- int maxx = ((int)((e.ClipRectangle.Width / _zoom) + 8) >> 3) + (hScrollBar.Value >> 3);
- int maxy = ((int)((e.ClipRectangle.Height / _zoom) + 8) >> 3) + (vScrollBar.Value >> 3);
- if (maxx > _originalMap.Width >> 3)
+ using (Graphics graphics = Graphics.FromImage(_map))
{
- maxx = _originalMap.Width >> 3;
- }
+ int maxX = Math.Min(blockX + widthBlocks, drawMap.Width >> 3);
+ int maxY = Math.Min(blockY + heightBlocks, drawMap.Height >> 3);
- if (maxy > _originalMap.Height >> 3)
- {
- maxy = _originalMap.Height >> 3;
- }
-
- int gx = 0;
- for (int x = hScrollBar.Value >> 3; x < maxx; x++, gx += 8)
- {
- int gy = 0;
- for (int y = vScrollBar.Value >> 3; y < maxy; y++, gy += 8)
+ int gx = 0;
+ for (int x = blockX; x < maxX; x++, gx += 8)
{
- for (int xb = 0; xb < 8; xb++)
+ int gy = 0;
+ for (int y = blockY; y < maxY; y++, gy += 8)
{
- for (int yb = 0; yb < 8; yb++)
+ if (drawMap.Tiles.Patch.IsLandBlockPatched(x, y))
{
- if (_diffs[x][y][xb][yb])
- {
- mapg.DrawRectangle(Pens.Red, gx + xb, gy + yb, 1, 1);
- mapg.DrawRectangle(Pens.Red, gx + xb, 0, 1, 2);
- mapg.DrawRectangle(Pens.Red, 0, gy + yb, 2, 1);
- }
+ graphics.FillRectangle(Brushes.Azure, gx, gy, 8, 8);
+ graphics.FillRectangle(Brushes.Azure, gx, 0, 8, 2);
+ graphics.FillRectangle(Brushes.Azure, 0, gy, 2, 8);
+ }
+
+ if (drawMap.Tiles.Patch.IsStaticBlockPatched(x, y))
+ {
+ graphics.FillRectangle(Brushes.Azure, gx, gy, 8, 8);
+ graphics.FillRectangle(Brushes.Azure, gx, 0, 8, 2);
+ graphics.FillRectangle(Brushes.Azure, 0, gy, 2, 8);
}
}
}
}
- mapg.Save();
}
}
- if (markDiffToolStripMenuItem.Checked)
+ Bitmap toDraw = Math.Abs(_zoom - 1.0) < 1e-6 ? _map : ZoomMap(_map, _zoom);
+
+ // The render buffer starts at the block boundary (blockX * 8). Shift the draw position
+ // by the sub-block portion of the scroll so viewport pixel 0 maps to the exact tile
+ // the scrollbar points at.
+ int subTileX = hScrollBar.Value - (blockX << 3);
+ int subTileY = vScrollBar.Value - (blockY << 3);
+ int drawOffsetX = (int)Math.Round(subTileX * _zoom);
+ int drawOffsetY = (int)Math.Round(subTileY * _zoom);
+
+ e.Graphics.DrawImageUnscaled(toDraw, -drawOffsetX, -drawOffsetY);
+ }
+
+ ///
+ /// Writes the red "diff" markers onto by locking its
+ /// pixel buffer once and writing 16bpp pixels directly. Replaces the
+ /// per-tile Graphics.DrawRectangle path that dominated drag/scroll cost
+ /// when "Show Differences" was enabled.
+ ///
+ private unsafe void DrawDiffOverlay(int blockX, int blockY, int widthBlocks, int heightBlocks)
+ {
+ int maxX = Math.Min(blockX + widthBlocks, _diffWidthBlocks);
+ int maxY = Math.Min(blockY + heightBlocks, _diffHeightBlocks);
+ if (maxX <= blockX || maxY <= blockY)
+ {
+ return;
+ }
+
+ BitmapData bd = _map.LockBits(
+ new Rectangle(0, 0, _map.Width, _map.Height),
+ ImageLockMode.ReadWrite,
+ PixelFormat.Format16bppRgb555);
+ try
{
- Map drawMap = showMap1ToolStripMenuItem.Checked
- ? _originalMap
- : _currentMap;
+ ushort* basePtr = (ushort*)bd.Scan0;
+ int stride = bd.Stride >> 1; // pixels per row
+ const ushort red = 0x7C00; // R=31, G=0, B=0 in 5-5-5
+ int mapHeightBlocks = _diffHeightBlocks;
- if (drawMap != null)
+ int gx = 0;
+ for (int x = blockX; x < maxX; x++, gx += 8)
{
- int count = drawMap.Tiles.Patch.LandBlocksCount + drawMap.Tiles.Patch.StaticBlocksCount;
- if (count > 0)
+ int colBase = x * mapHeightBlocks;
+ int gy = 0;
+ for (int y = blockY; y < maxY; y++, gy += 8)
{
- using (Graphics graphics = Graphics.FromImage(_map))
+ ulong mask = _diffMasks[colBase + y];
+ if (mask == 0)
{
- int maxX = ((int)((e.ClipRectangle.Width / _zoom) + 8) >> 3) + (hScrollBar.Value >> 3);
- int maxY = ((int)((e.ClipRectangle.Height / _zoom) + 8) >> 3) + (vScrollBar.Value >> 3);
-
- if (maxX > drawMap.Width >> 3)
- {
- maxX = drawMap.Width >> 3;
- }
-
- if (maxY > drawMap.Height >> 3)
- {
- maxY = drawMap.Height >> 3;
- }
-
- int gx = 0;
- for (int x = hScrollBar.Value >> 3; x < maxX; x++, gx += 8)
- {
- int gy = 0;
- for (int y = vScrollBar.Value >> 3; y < maxY; y++, gy += 8)
- {
- if (drawMap.Tiles.Patch.IsLandBlockPatched(x, y))
- {
- graphics.FillRectangle(Brushes.Azure, gx, gy, 8, 8);
- graphics.FillRectangle(Brushes.Azure, gx, 0, 8, 2);
- graphics.FillRectangle(Brushes.Azure, 0, gy, 2, 8);
- }
+ continue;
+ }
- if (drawMap.Tiles.Patch.IsStaticBlockPatched(x, y))
- {
- graphics.FillRectangle(Brushes.Azure, gx, gy, 8, 8);
- graphics.FillRectangle(Brushes.Azure, gx, 0, 8, 2);
- graphics.FillRectangle(Brushes.Azure, 0, gy, 2, 8);
- }
- }
- }
+ while (mask != 0)
+ {
+ int bit = BitOperations.TrailingZeroCount(mask);
+ mask &= mask - 1; // clear lowest set bit
+ int xb = bit >> 3;
+ int yb = bit & 7;
+
+ int px = gx + xb;
+ int py = gy + yb;
+
+ // 1x1 tile pixel
+ basePtr[py * stride + px] = red;
+ // Top-edge column marker (1 col wide, 2 rows tall)
+ basePtr[px] = red;
+ basePtr[stride + px] = red;
+ // Left-edge row marker (2 cols wide, 1 row tall)
+ basePtr[py * stride + 0] = red;
+ basePtr[py * stride + 1] = red;
}
}
}
}
+ finally
+ {
+ _map.UnlockBits(bd);
+ }
+ }
- ZoomMap(ref _map);
+ private Bitmap EnsureRenderBuffer(int pixelWidth, int pixelHeight)
+ {
+ if (_renderBuffer != null
+ && _renderBuffer.Width == pixelWidth
+ && _renderBuffer.Height == pixelHeight)
+ {
+ return _renderBuffer;
+ }
- e.Graphics.DrawImageUnscaledAndClipped(_map, e.ClipRectangle);
+ _renderBuffer?.Dispose();
+ _renderBuffer = new Bitmap(pixelWidth, pixelHeight, PixelFormat.Format16bppRgb555);
+ return _renderBuffer;
}
- private void ZoomMap(ref Bitmap bmp0)
+ private Bitmap ZoomMap(Bitmap source, double effectiveZoom)
{
- Bitmap bmp1 = new Bitmap((int)(_map.Width * _zoom), (int)(_map.Height * _zoom));
- using (Graphics graph = Graphics.FromImage(bmp1))
+ int targetWidth = (int)(source.Width * effectiveZoom);
+ int targetHeight = (int)(source.Height * effectiveZoom);
+
+ if (targetWidth <= 0 || targetHeight <= 0)
{
- graph.InterpolationMode = InterpolationMode.NearestNeighbor;
- graph.PixelOffsetMode = PixelOffsetMode.Half;
- graph.DrawImage(bmp0, new Rectangle(0, 0, bmp1.Width, bmp1.Height));
+ return source;
}
- bmp0 = bmp1;
+ if (_zoomBuffer == null || _zoomBuffer.Width != targetWidth || _zoomBuffer.Height != targetHeight)
+ {
+ _zoomBufferGraphics?.Dispose();
+ _zoomBuffer?.Dispose();
+ _zoomBuffer = new Bitmap(targetWidth, targetHeight, PixelFormat.Format32bppArgb);
+ _zoomBufferGraphics = Graphics.FromImage(_zoomBuffer);
+ _zoomBufferGraphics.InterpolationMode = InterpolationMode.NearestNeighbor;
+ _zoomBufferGraphics.PixelOffsetMode = PixelOffsetMode.Half;
+ }
+
+ _zoomBufferGraphics.DrawImage(source, new Rectangle(0, 0, targetWidth, targetHeight));
+ return _zoomBuffer;
}
private void OnResize(object sender, EventArgs e)
@@ -450,8 +531,42 @@ private void SetScrollBarValues()
vScrollBar.Value = 0;
}
+ private const double MinZoom = 0.25;
+ private const double MaxZoom = 4;
+
+ private void OnMouseWheel(object sender, MouseEventArgs e)
+ {
+ // Position-from-cursor for DoZoom's recenter math; mirrors what the
+ // context-menu opening handler does so wheel and right-click+zoom
+ // land at the same place.
+ UpdateCurrentPointFromMouse();
+
+ if (e.Delta > 0)
+ {
+ OnZoomPlus(sender, EventArgs.Empty);
+ }
+ else if (e.Delta < 0)
+ {
+ OnZoomMinus(sender, EventArgs.Empty);
+ }
+ }
+
+ private void UpdateCurrentPointFromMouse()
+ {
+ _currentPoint = pictureBox.PointToClient(MousePosition);
+ _currentPoint.X = (int)(_currentPoint.X / _zoom);
+ _currentPoint.Y = (int)(_currentPoint.Y / _zoom);
+ _currentPoint.X += hScrollBar.Value;
+ _currentPoint.Y += vScrollBar.Value;
+ }
+
private void OnZoomPlus(object sender, EventArgs e)
{
+ if (_zoom * 2 > MaxZoom)
+ {
+ return;
+ }
+
_zoom *= 2;
DoZoom();
@@ -459,6 +574,11 @@ private void OnZoomPlus(object sender, EventArgs e)
private void OnZoomMinus(object sender, EventArgs e)
{
+ if (_zoom / 2 < MinZoom)
+ {
+ return;
+ }
+
_zoom /= 2;
DoZoom();
@@ -484,13 +604,7 @@ private void DoZoom()
private void OnOpeningContext(object sender, CancelEventArgs e)
{
- _currentPoint = pictureBox.PointToClient(MousePosition);
-
- _currentPoint.X = (int)(_currentPoint.X / _zoom);
- _currentPoint.Y = (int)(_currentPoint.Y / _zoom);
-
- _currentPoint.X += hScrollBar.Value;
- _currentPoint.Y += vScrollBar.Value;
+ UpdateCurrentPointFromMouse();
}
private void OnClickBrowseLoc(object sender, EventArgs e)
@@ -677,94 +791,95 @@ private void OnClickMarkDiff(object sender, EventArgs e)
private bool BlockDiff(int x, int y)
{
- if (_diffs == null)
+ if (_diffMasks == null)
{
return false;
}
- if (x < 0 || y < 0 || x >= _diffs.GetLength(0) || y >= _diffs[x].GetLength(0))
+ if (x < 0 || y < 0 || x >= _diffWidthBlocks || y >= _diffHeightBlocks)
{
return false;
}
- for (int xb = 0; xb < 8; xb++)
- {
- for (int yb = 0; yb < 8; yb++)
- {
- if (_diffs[x][y][xb][yb])
- {
- return true;
- }
- }
- }
- return false;
+ return _diffMasks[x * _diffHeightBlocks + y] != 0;
}
private void CalculateDiffs()
{
- int width = _currentMap.Width >> 3;
- int height = _currentMap.Height >> 3;
-
- _diffs = new bool[width][][][];
-
if (_currentMap == null || _originalMap == null)
{
+ _diffMasks = null;
+ _diffWidthBlocks = 0;
+ _diffHeightBlocks = 0;
return;
}
+ int width = _currentMap.Width >> 3;
+ int height = _currentMap.Height >> 3;
+ var masks = new ulong[width * height];
+
Cursor.Current = Cursors.WaitCursor;
for (int x = 0; x < width; ++x)
{
- _diffs[x] = new bool[height][][];
-
for (int y = 0; y < height; ++y)
{
- _diffs[x][y] = new bool[8][];
-
Tile[] customTiles = _currentMap.Tiles.GetLandBlock(x, y);
Tile[] origTiles = _originalMap.Tiles.GetLandBlock(x, y);
HuedTile[][][] customStatics = _currentMap.Tiles.GetStaticBlock(x, y);
HuedTile[][][] origStatics = _originalMap.Tiles.GetStaticBlock(x, y);
+ ulong mask = 0;
for (int xb = 0; xb < 8; xb++)
{
- _diffs[x][y][xb] = new bool[8];
-
+ HuedTile[][] customCol = customStatics[xb];
+ HuedTile[][] origCol = origStatics[xb];
for (int yb = 0; yb < 8; yb++)
{
- if (customTiles[((yb & 0x7) << 3) + (xb & 0x7)].Id != origTiles[((yb & 0x7) << 3) + (xb & 0x7)].Id
- || customTiles[((yb & 0x7) << 3) + (xb & 0x7)].Z != origTiles[((yb & 0x7) << 3) + (xb & 0x7)].Z)
+ int tileIdx = (yb << 3) + xb;
+ bool isDiff;
+
+ if (customTiles[tileIdx].Id != origTiles[tileIdx].Id
+ || customTiles[tileIdx].Z != origTiles[tileIdx].Z)
{
- _diffs[x][y][xb][yb] = true;
+ isDiff = true;
+ }
+ else if (customCol[yb].Length != origCol[yb].Length)
+ {
+ isDiff = true;
}
else
{
- if (customStatics[xb][yb].Length != origStatics[xb][yb].Length)
- {
- _diffs[x][y][xb][yb] = true;
- }
- else
+ isDiff = false;
+ HuedTile[] cs = customCol[yb];
+ HuedTile[] os = origCol[yb];
+ for (int i = 0; i < cs.Length; i++)
{
- for (int i = 0; i < customStatics[xb][yb].Length; i++)
+ if (cs[i].Id != os[i].Id
+ || cs[i].Z != os[i].Z
+ || cs[i].Hue != os[i].Hue)
{
- if (customStatics[xb][yb][i].Id != origStatics[xb][yb][i].Id
- || customStatics[xb][yb][i].Z != origStatics[xb][yb][i].Z
- || customStatics[xb][yb][i].Hue != origStatics[xb][yb][i].Hue)
- {
- _diffs[x][y][xb][yb] = true;
-
- break;
- }
+ isDiff = true;
+ break;
}
}
}
+
+ if (isDiff)
+ {
+ mask |= 1UL << ((xb << 3) | yb);
+ }
}
}
+
+ masks[x * height + y] = mask;
}
}
+ _diffMasks = masks;
+ _diffWidthBlocks = width;
+ _diffHeightBlocks = height;
Cursor.Current = Cursors.Default;
}
@@ -772,5 +887,44 @@ private void HandleScroll(object sender, ScrollEventArgs e)
{
pictureBox.Invalidate();
}
+
+ private void RequestDragRepaint()
+ {
+ if (!_dragRepaintTimer.IsRunning || _dragRepaintTimer.ElapsedMilliseconds >= DragRepaintIntervalMs)
+ {
+ _dragInvalidatePending = false;
+ _dragRepaintTimer.Restart();
+ pictureBox.Invalidate();
+ return;
+ }
+
+ // Coalesce into a trailing-edge repaint so the final position always lands on screen.
+ if (_dragInvalidatePending)
+ {
+ return;
+ }
+
+ _dragInvalidatePending = true;
+ if (_dragTrailTimer == null)
+ {
+ _dragTrailTimer = new System.Windows.Forms.Timer { Interval = DragRepaintIntervalMs };
+ _dragTrailTimer.Tick += OnDragTrailTick;
+ }
+ _dragTrailTimer.Stop();
+ _dragTrailTimer.Interval = Math.Max(1, DragRepaintIntervalMs - (int)_dragRepaintTimer.ElapsedMilliseconds);
+ _dragTrailTimer.Start();
+ }
+
+ private void OnDragTrailTick(object sender, EventArgs e)
+ {
+ _dragTrailTimer.Stop();
+ if (!_dragInvalidatePending)
+ {
+ return;
+ }
+ _dragInvalidatePending = false;
+ _dragRepaintTimer.Restart();
+ pictureBox.Invalidate();
+ }
}
}
diff --git a/UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs b/UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs
index d1cca15..6c7c217 100644
--- a/UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs
+++ b/UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs
@@ -513,7 +513,20 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t
if (offsets[i].CompressionFlag == (short)CompressionFlag.Mythic)
{
- chunkData = MythicDecompress.Decompress(chunkData);
+ uint mythicLen = MythicDecompress.PeekDecompressedLength(chunkData);
+ if (mythicLen == 0 || mythicLen > int.MaxValue)
+ {
+ throw new InvalidDataException(
+ $"Mythic header reports invalid decompressed length {mythicLen} for chunk {chunkId}.");
+ }
+
+ byte[] mythicOutput = new byte[mythicLen];
+ if (!MythicDecompress.TryDecompress(chunkData, mythicOutput, out _))
+ {
+ throw new InvalidDataException(
+ $"Mythic decompression failed for chunk {chunkId}.");
+ }
+ chunkData = mythicOutput;
}
if (type == FileType.MapLegacyMul)
From d407a6772d1004d5540bc2d0d3245bd39aa62944 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 18:59:04 +0200
Subject: [PATCH 15/21] Move compare map strip menu to context menu for easier
access.
---
.../CompareMapControl.Designer.cs | 78 +++++++++----------
1 file changed, 37 insertions(+), 41 deletions(-)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.Designer.cs
index dfc67f7..36addf5 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.Designer.cs
@@ -46,14 +46,7 @@ private void InitializeComponent()
this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components);
this.zoomToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.zoomToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
- this.toolTip1 = new System.Windows.Forms.ToolTip(this.components);
- this.toolStrip1 = new System.Windows.Forms.ToolStrip();
- this.toolStripTextBox1 = new System.Windows.Forms.ToolStripTextBox();
- this.toolStripButton1 = new System.Windows.Forms.ToolStripButton();
- this.toolStripButton2 = new System.Windows.Forms.ToolStripButton();
- this.CoordsLabel = new System.Windows.Forms.ToolStripLabel();
- this.ZoomLabel = new System.Windows.Forms.ToolStripLabel();
- this.toolStripDropDownButton1 = new System.Windows.Forms.ToolStripDropDownButton();
+ this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
this.showDifferencesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.showMap1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -66,6 +59,13 @@ private void InitializeComponent()
this.tokunoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.terMurToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.markDiffToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.toolTip1 = new System.Windows.Forms.ToolTip(this.components);
+ this.toolStrip1 = new System.Windows.Forms.ToolStrip();
+ this.toolStripTextBox1 = new System.Windows.Forms.ToolStripTextBox();
+ this.toolStripButton1 = new System.Windows.Forms.ToolStripButton();
+ this.toolStripButton2 = new System.Windows.Forms.ToolStripButton();
+ this.CoordsLabel = new System.Windows.Forms.ToolStripLabel();
+ this.ZoomLabel = new System.Windows.Forms.ToolStripLabel();
((System.ComponentModel.ISupportInitialize)(this.pictureBox)).BeginInit();
this.contextMenuStrip1.SuspendLayout();
this.toolStrip1.SuspendLayout();
@@ -104,14 +104,27 @@ private void InitializeComponent()
this.pictureBox.MouseDown += new System.Windows.Forms.MouseEventHandler(this.OnMouseDown);
this.pictureBox.MouseMove += new System.Windows.Forms.MouseEventHandler(this.OnMouseMove);
this.pictureBox.MouseUp += new System.Windows.Forms.MouseEventHandler(this.OnMouseUp);
- //
+ //
// contextMenuStrip1
- //
+ //
this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.zoomToolStripMenuItem,
- this.zoomToolStripMenuItem1});
+ this.zoomToolStripMenuItem1,
+ this.toolStripSeparator3,
+ this.showDifferencesToolStripMenuItem,
+ this.toolStripSeparator2,
+ this.showMap1ToolStripMenuItem,
+ this.showMap2ToolStripMenuItem,
+ this.toolStripSeparator1,
+ this.feluccaToolStripMenuItem,
+ this.trammelToolStripMenuItem,
+ this.ilshenarToolStripMenuItem,
+ this.malasToolStripMenuItem,
+ this.tokunoToolStripMenuItem,
+ this.terMurToolStripMenuItem,
+ this.markDiffToolStripMenuItem});
this.contextMenuStrip1.Name = "contextMenuStrip1";
- this.contextMenuStrip1.Size = new System.Drawing.Size(115, 48);
+ this.contextMenuStrip1.Size = new System.Drawing.Size(166, 248);
this.contextMenuStrip1.Opening += new System.ComponentModel.CancelEventHandler(this.OnOpeningContext);
//
// zoomToolStripMenuItem
@@ -142,8 +155,7 @@ private void InitializeComponent()
this.toolStripButton1,
this.toolStripButton2,
this.CoordsLabel,
- this.ZoomLabel,
- this.toolStripDropDownButton1});
+ this.ZoomLabel});
this.toolStrip1.Location = new System.Drawing.Point(0, 365);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.RenderMode = System.Windows.Forms.ToolStripRenderMode.System;
@@ -173,42 +185,26 @@ private void InitializeComponent()
this.toolStripButton2.Size = new System.Drawing.Size(37, 22);
this.toolStripButton2.Text = "Load";
this.toolStripButton2.Click += new System.EventHandler(this.OnClickLoad);
- //
+ //
// CoordsLabel
- //
+ //
this.CoordsLabel.AutoSize = false;
this.CoordsLabel.Name = "CoordsLabel";
this.CoordsLabel.Size = new System.Drawing.Size(120, 17);
this.CoordsLabel.Text = "Coords: 0,0";
this.CoordsLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
- //
+ //
// ZoomLabel
- //
+ //
this.ZoomLabel.Name = "ZoomLabel";
this.ZoomLabel.Size = new System.Drawing.Size(42, 22);
this.ZoomLabel.Text = "Zoom:";
- //
- // toolStripDropDownButton1
- //
- this.toolStripDropDownButton1.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
- this.toolStripDropDownButton1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.showDifferencesToolStripMenuItem,
- this.toolStripSeparator2,
- this.showMap1ToolStripMenuItem,
- this.showMap2ToolStripMenuItem,
- this.toolStripSeparator1,
- this.feluccaToolStripMenuItem,
- this.trammelToolStripMenuItem,
- this.ilshenarToolStripMenuItem,
- this.malasToolStripMenuItem,
- this.tokunoToolStripMenuItem,
- this.terMurToolStripMenuItem,
- this.markDiffToolStripMenuItem});
- this.toolStripDropDownButton1.ImageTransparentColor = System.Drawing.Color.Magenta;
- this.toolStripDropDownButton1.Name = "toolStripDropDownButton1";
- this.toolStripDropDownButton1.Size = new System.Drawing.Size(44, 22);
- this.toolStripDropDownButton1.Text = "Map";
- //
+ //
+ // toolStripSeparator3
+ //
+ this.toolStripSeparator3.Name = "toolStripSeparator3";
+ this.toolStripSeparator3.Size = new System.Drawing.Size(162, 6);
+ //
// showDifferencesToolStripMenuItem
//
this.showDifferencesToolStripMenuItem.CheckOnClick = true;
@@ -331,9 +327,9 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.ToolStripButton toolStripButton1;
private System.Windows.Forms.ToolStripButton toolStripButton2;
- private System.Windows.Forms.ToolStripDropDownButton toolStripDropDownButton1;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
private System.Windows.Forms.ToolStripTextBox toolStripTextBox1;
private System.Windows.Forms.ToolTip toolTip1;
private System.Windows.Forms.ToolStripMenuItem trammelToolStripMenuItem;
From 92303a203aa633a9943b1942127b61168489dc69 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 22:51:43 +0200
Subject: [PATCH 16/21] Update multi-select operations in tile view controls.
---
.../UserControls/ItemsControl.cs | 200 +++++++++++++++---
.../UserControls/LandTilesControl.Designer.cs | 2 +-
.../UserControls/LandTilesControl.cs | 200 +++++++++++++++---
.../RadarColorControl.Designer.cs | 2 +
.../UserControls/RadarColorControl.cs | 21 +-
.../UserControls/TexturesControl.Designer.cs | 2 +-
.../UserControls/TexturesControl.cs | 194 +++++++++++++++--
.../UserControls/TileView/TileViewControl.cs | 103 ++++++++-
.../Classes/CompareColors.cs | 28 +++
.../Classes/CompareFiles.cs | 65 ++++++
.../UserControls/CompareAnimDataControl.cs | 26 ++-
.../UserControls/CompareCliLocControl.cs | 23 ++
.../UserControls/CompareGumpControl.cs | 27 ++-
.../UserControls/CompareHuesControl.cs | 11 +
.../UserControls/CompareItemControl.cs | 27 ++-
.../UserControls/CompareLandControl.cs | 27 ++-
.../UserControls/CompareMapControl.cs | 14 ++
.../UserControls/CompareRadarColControl.cs | 29 ++-
.../UserControls/CompareTextureControl.cs | 27 ++-
.../UserControls/CompareTileDataControl.cs | 34 ++-
20 files changed, 954 insertions(+), 108 deletions(-)
create mode 100644 UoFiddler.Plugin.Compare/Classes/CompareColors.cs
create mode 100644 UoFiddler.Plugin.Compare/Classes/CompareFiles.cs
diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs
index a38a27e..56c54c4 100644
--- a/UoFiddler.Controls/UserControls/ItemsControl.cs
+++ b/UoFiddler.Controls/UserControls/ItemsControl.cs
@@ -15,6 +15,7 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
@@ -507,6 +508,12 @@ private void OnClickFindFree(object sender, EventArgs e)
private void OnClickReplace(object sender, EventArgs e)
{
+ if (ItemsTileView.SelectedIndices.Count > 1)
+ {
+ ReplaceMultipleSelected();
+ return;
+ }
+
if (_selectedGraphicId < 0)
{
return;
@@ -562,29 +569,132 @@ private void OnClickReplace(object sender, EventArgs e)
}
}
+ private void ReplaceMultipleSelected()
+ {
+ var ids = GetSelectedGraphicIds();
+ if (ids.Count == 0)
+ {
+ return;
+ }
+
+ using (OpenFileDialog dialog = new OpenFileDialog())
+ {
+ dialog.Multiselect = true;
+ dialog.Title = $"Choose {ids.Count} image files to replace selected items";
+ dialog.CheckFileExists = true;
+ dialog.Filter = "Image files (*.tif;*.tiff;*.bmp;*.png)|*.tif;*.tiff;*.bmp;*.png";
+
+ if (dialog.ShowDialog() != DialogResult.OK)
+ {
+ return;
+ }
+
+ var files = dialog.FileNames.OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase).ToArray();
+
+ if (files.Length != ids.Count)
+ {
+ MessageBox.Show(
+ $"Selected {ids.Count} items but chose {files.Length} images.\n\nNo changes made.",
+ "Selection Mismatch",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
+ // Load and validate all images first; abort the whole batch on any failure so no partial writes happen.
+ var bitmaps = new List(ids.Count);
+ try
+ {
+ for (int i = 0; i < ids.Count; ++i)
+ {
+ using (var bmpTemp = new Bitmap(files[i]))
+ {
+ Bitmap bitmap = new Bitmap(bmpTemp);
+
+ if (files[i].Contains(".bmp"))
+ {
+ bitmap = Utils.ConvertBmp(bitmap);
+ }
+
+ if (!Art.ValidateStaticSize(bitmap, out int estimatedSize))
+ {
+ bitmap.Dispose();
+ MessageBox.Show(
+ $"Image is too large for MUL format!\n\n" +
+ $"File: {Path.GetFileName(files[i])}\n" +
+ $"Encoded size: {estimatedSize:N0} ushorts\n" +
+ $"Maximum allowed: 65,535 ushorts\n\n" +
+ $"No changes made.",
+ "Image Too Large",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
+ bitmaps.Add(bitmap);
+ }
+ }
+ }
+ catch
+ {
+ foreach (var bmp in bitmaps)
+ {
+ bmp.Dispose();
+ }
+ throw;
+ }
+
+ for (int i = 0; i < ids.Count; ++i)
+ {
+ Art.ReplaceStatic(ids[i], bitmaps[i]);
+ ControlEvents.FireItemChangeEvent(this, ids[i]);
+ }
+
+ ItemsTileView.Invalidate();
+ UpdateToolStripLabels(_selectedGraphicId);
+ UpdateDetail(_selectedGraphicId);
+
+ Options.ChangedUltimaClass["Art"] = true;
+ }
+ }
+
private void OnClickRemove(object sender, EventArgs e)
{
- if (!Art.IsValidStatic(_selectedGraphicId))
+ var ids = GetSelectedGraphicIds().Where(Art.IsValidStatic).ToList();
+ if (ids.Count == 0)
{
return;
}
- DialogResult result = MessageBox.Show($"Are you sure to remove 0x{_selectedGraphicId:X}", "Save",
+ string prompt = ids.Count == 1
+ ? $"Are you sure to remove 0x{ids[0]:X}"
+ : $"Are you sure to remove {ids.Count} items?";
+
+ DialogResult result = MessageBox.Show(prompt, "Save",
MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (result != DialogResult.Yes)
{
return;
}
- Art.RemoveStatic(_selectedGraphicId);
- ControlEvents.FireItemChangeEvent(this, _selectedGraphicId);
+ foreach (int id in ids)
+ {
+ Art.RemoveStatic(id);
+ ControlEvents.FireItemChangeEvent(this, id);
+
+ if (!_showFreeSlots)
+ {
+ _itemList.Remove(id);
+ }
+ }
+
+ ItemsTileView.SelectedIndices.Clear();
if (!_showFreeSlots)
{
- _itemList.Remove(_selectedGraphicId);
ItemsTileView.VirtualListSize = _itemList.Count;
- var moveToIndex = --_selectedGraphicId;
- SelectedGraphicId = moveToIndex <= 0 ? 0 : _selectedGraphicId; // TODO: get last index visible instead just curr -1
+ int moveToId = ids[0] - 1;
+ SelectedGraphicId = moveToId <= 0 ? 0 : moveToId; // TODO: get last index visible instead just curr -1
}
ItemsTileView.Invalidate();
@@ -719,42 +829,61 @@ private void OnClickShowFreeSlots(object sender, EventArgs e)
private void Extract_Image_ClickBmp(object sender, EventArgs e)
{
- if (_selectedGraphicId == -1)
- {
- return;
- }
-
- ExportItemImage(_selectedGraphicId, ImageFormat.Bmp);
+ ExportSelected(ImageFormat.Bmp);
}
private void Extract_Image_ClickTiff(object sender, EventArgs e)
{
- if (_selectedGraphicId == -1)
- {
- return;
- }
-
- ExportItemImage(_selectedGraphicId, ImageFormat.Tiff);
+ ExportSelected(ImageFormat.Tiff);
}
private void Extract_Image_ClickJpg(object sender, EventArgs e)
{
- if (_selectedGraphicId == -1)
+ ExportSelected(ImageFormat.Jpeg);
+ }
+
+ private void Extract_Image_ClickPng(object sender, EventArgs e)
+ {
+ ExportSelected(ImageFormat.Png);
+ }
+
+ private void ExportSelected(ImageFormat imageFormat)
+ {
+ var ids = GetSelectedGraphicIds().Where(Art.IsValidStatic).ToList();
+ if (ids.Count == 0)
{
return;
}
- ExportItemImage(_selectedGraphicId, ImageFormat.Jpeg);
+ if (ids.Count == 1)
+ {
+ ExportItemImage(ids[0], imageFormat);
+ return;
+ }
+
+ ExportMultipleItemImages(ids, imageFormat);
}
- private void Extract_Image_ClickPng(object sender, EventArgs e)
+ private void ExportMultipleItemImages(List ids, ImageFormat imageFormat)
{
- if (_selectedGraphicId == -1)
+ string fileExtension = Utils.GetFileExtensionFor(imageFormat);
+
+ foreach (int index in ids)
{
- return;
+ var artBitmap = Art.GetStatic(index);
+ if (artBitmap is null)
+ {
+ continue;
+ }
+
+ string fileName = Path.Combine(Options.OutputPath, $"Item {Utils.FormatExportId(index)}.{fileExtension}");
+ using (Bitmap bit = new Bitmap(artBitmap))
+ {
+ bit.Save(fileName, imageFormat);
+ }
}
- ExportItemImage(_selectedGraphicId, ImageFormat.Png);
+ FileSavedDialog.Show(FindForm(), Options.OutputPath, $"{ids.Count} items saved successfully.");
}
private static void ExportItemImage(int index, ImageFormat imageFormat)
@@ -993,6 +1122,23 @@ private void ItemsTileView_FocusSelectionChanged(object sender, TileViewControl.
UpdateSelection(e.FocusedItemIndex);
}
+ ///
+ /// Resolves the current tile selection to a sorted list of graphic IDs.
+ ///
+ private List GetSelectedGraphicIds()
+ {
+ var ids = new List();
+ foreach (int idx in ItemsTileView.SelectedIndices)
+ {
+ if (idx >= 0 && idx < _itemList.Count)
+ {
+ ids.Add(_itemList[idx]);
+ }
+ }
+ ids.Sort();
+ return ids;
+ }
+
private void UpdateSelection(int itemIndex)
{
if (_itemList.Count == 0)
@@ -1076,6 +1222,10 @@ private void SelectInGumpsTabFemaleToolStripMenuItem_Click(object sender, EventA
private void TileViewContextMenuStrip_Opening(object sender, CancelEventArgs e)
{
+ int selectedCount = ItemsTileView.SelectedIndices.Count;
+ removeToolStripMenuItem.Text = selectedCount > 1 ? $"Remove {selectedCount}" : "Remove";
+ extractToolStripMenuItem.Text = selectedCount > 1 ? $"Export {selectedCount} Images..." : "Export Image..";
+
if (SelectedGraphicId <= 0)
{
selectInGumpsTabMaleToolStripMenuItem.Enabled = false;
diff --git a/UoFiddler.Controls/UserControls/LandTilesControl.Designer.cs b/UoFiddler.Controls/UserControls/LandTilesControl.Designer.cs
index 00fb1d7..2dbea64 100644
--- a/UoFiddler.Controls/UserControls/LandTilesControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/LandTilesControl.Designer.cs
@@ -417,7 +417,7 @@ private void InitializeComponent()
LandTilesTileView.FocusIndex = -1;
LandTilesTileView.Location = new System.Drawing.Point(0, 0);
LandTilesTileView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- LandTilesTileView.MultiSelect = false;
+ LandTilesTileView.MultiSelect = true;
LandTilesTileView.Name = "LandTilesTileView";
LandTilesTileView.Size = new System.Drawing.Size(716, 354);
LandTilesTileView.TabIndex = 9;
diff --git a/UoFiddler.Controls/UserControls/LandTilesControl.cs b/UoFiddler.Controls/UserControls/LandTilesControl.cs
index c59594a..7063bb5 100644
--- a/UoFiddler.Controls/UserControls/LandTilesControl.cs
+++ b/UoFiddler.Controls/UserControls/LandTilesControl.cs
@@ -15,6 +15,7 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
@@ -339,28 +340,42 @@ private void OnClickFindFree(object sender, EventArgs e)
private void OnClickRemove(object sender, EventArgs e)
{
- if (!Art.IsValidLand(_selectedGraphicId))
+ var ids = GetSelectedGraphicIds().Where(Art.IsValidLand).ToList();
+ if (ids.Count == 0)
{
return;
}
+ string prompt = ids.Count == 1
+ ? $"Are you sure to remove {ids[0]}"
+ : $"Are you sure to remove {ids.Count} land tiles?";
+
DialogResult result =
- MessageBox.Show($"Are you sure to remove {_selectedGraphicId}", "Save",
+ MessageBox.Show(prompt, "Save",
MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
if (result != DialogResult.Yes)
{
return;
}
- Art.RemoveLand(_selectedGraphicId);
- ControlEvents.FireLandTileChangeEvent(this, _selectedGraphicId);
+ foreach (int id in ids)
+ {
+ Art.RemoveLand(id);
+ ControlEvents.FireLandTileChangeEvent(this, id);
+
+ if (!_showFreeSlots)
+ {
+ _tileList.Remove(id);
+ }
+ }
+
+ LandTilesTileView.SelectedIndices.Clear();
if (!_showFreeSlots)
{
- _tileList.Remove(_selectedGraphicId);
LandTilesTileView.VirtualListSize = _tileList.Count;
- var moveToIndex = --_selectedGraphicId;
- SelectedGraphicId = moveToIndex <= 0 ? 0 : _selectedGraphicId; // TODO: get last index visible instead just curr -1
+ int moveToId = ids[0] - 1;
+ SelectedGraphicId = moveToId <= 0 ? 0 : moveToId; // TODO: get last index visible instead just curr -1
}
LandTilesTileView.Invalidate();
@@ -369,6 +384,12 @@ private void OnClickRemove(object sender, EventArgs e)
private void OnClickReplace(object sender, EventArgs e)
{
+ if (LandTilesTileView.SelectedIndices.Count > 1)
+ {
+ ReplaceMultipleSelected();
+ return;
+ }
+
if (_selectedGraphicId < 0)
{
return;
@@ -420,6 +441,95 @@ private void OnClickReplace(object sender, EventArgs e)
}
}
+ private void ReplaceMultipleSelected()
+ {
+ var ids = GetSelectedGraphicIds();
+ if (ids.Count == 0)
+ {
+ return;
+ }
+
+ using (OpenFileDialog dialog = new OpenFileDialog())
+ {
+ dialog.Multiselect = true;
+ dialog.Title = $"Choose {ids.Count} image files to replace selected land tiles";
+ dialog.CheckFileExists = true;
+ dialog.Filter = "Image files (*.tif;*.tiff;*.bmp;*.png)|*.tif;*.tiff;*.bmp;*.png";
+
+ if (dialog.ShowDialog() != DialogResult.OK)
+ {
+ return;
+ }
+
+ var files = dialog.FileNames.OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase).ToArray();
+
+ if (files.Length != ids.Count)
+ {
+ MessageBox.Show(
+ $"Selected {ids.Count} land tiles but chose {files.Length} images.\n\nNo changes made.",
+ "Selection Mismatch",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
+ // Load and validate all images first; abort the whole batch on any failure so no partial writes happen.
+ var bitmaps = new List(ids.Count);
+ try
+ {
+ for (int i = 0; i < ids.Count; ++i)
+ {
+ using (var bmpTemp = new Bitmap(files[i]))
+ {
+ Bitmap bitmap = new Bitmap(bmpTemp);
+
+ if (files[i].Contains(".bmp"))
+ {
+ bitmap = Utils.ConvertBmp(bitmap);
+ }
+
+ if (!Art.ValidateStaticSize(bitmap, out int estimatedSize))
+ {
+ bitmap.Dispose();
+ MessageBox.Show(
+ $"Image is too large for MUL format!\n\n" +
+ $"File: {Path.GetFileName(files[i])}\n" +
+ $"Estimated encoded size: {estimatedSize:N0} ushorts\n" +
+ $"Maximum allowed: 65,535 ushorts\n\n" +
+ $"Note: Land tiles should typically be 44x44 pixels.\n" +
+ $"No changes made.",
+ "Image Too Large",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
+ bitmaps.Add(bitmap);
+ }
+ }
+ }
+ catch
+ {
+ foreach (var bmp in bitmaps)
+ {
+ bmp.Dispose();
+ }
+ throw;
+ }
+
+ for (int i = 0; i < ids.Count; ++i)
+ {
+ Art.ReplaceLand(ids[i], bitmaps[i]);
+ ControlEvents.FireLandTileChangeEvent(this, ids[i]);
+ }
+
+ LandTilesTileView.Invalidate();
+ UpdateToolStripLabels(_selectedGraphicId);
+
+ Options.ChangedUltimaClass["Art"] = true;
+ }
+ }
+
private void OnTextChangedInsert(object sender, EventArgs e)
{
Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
@@ -534,42 +644,61 @@ private void OnClickSave(object sender, EventArgs e)
private void OnClickExportBmp(object sender, EventArgs e)
{
- if (_selectedGraphicId < 0)
- {
- return;
- }
-
- ExportLandTileImage(_selectedGraphicId, ImageFormat.Bmp);
+ ExportSelected(ImageFormat.Bmp);
}
private void OnClickExportTiff(object sender, EventArgs e)
{
- if (_selectedGraphicId < 0)
- {
- return;
- }
-
- ExportLandTileImage(_selectedGraphicId, ImageFormat.Tiff);
+ ExportSelected(ImageFormat.Tiff);
}
private void OnClickExportJpg(object sender, EventArgs e)
{
- if (_selectedGraphicId < 0)
+ ExportSelected(ImageFormat.Jpeg);
+ }
+
+ private void OnClickExportPng(object sender, EventArgs e)
+ {
+ ExportSelected(ImageFormat.Png);
+ }
+
+ private void ExportSelected(ImageFormat imageFormat)
+ {
+ var ids = GetSelectedGraphicIds().Where(Art.IsValidLand).ToList();
+ if (ids.Count == 0)
{
return;
}
- ExportLandTileImage(_selectedGraphicId, ImageFormat.Jpeg);
+ if (ids.Count == 1)
+ {
+ ExportLandTileImage(ids[0], imageFormat);
+ return;
+ }
+
+ ExportMultipleLandTileImages(ids, imageFormat);
}
- private void OnClickExportPng(object sender, EventArgs e)
+ private void ExportMultipleLandTileImages(List ids, ImageFormat imageFormat)
{
- if (_selectedGraphicId < 0)
+ string fileExtension = Utils.GetFileExtensionFor(imageFormat);
+
+ foreach (int index in ids)
{
- return;
+ var landTile = Art.GetLand(index);
+ if (landTile is null)
+ {
+ continue;
+ }
+
+ string fileName = Path.Combine(Options.OutputPath, $"Landtile {Utils.FormatExportId(index)}.{fileExtension}");
+ using (Bitmap bit = new Bitmap(landTile))
+ {
+ bit.Save(fileName, imageFormat);
+ }
}
- ExportLandTileImage(_selectedGraphicId, ImageFormat.Png);
+ FileSavedDialog.Show(FindForm(), Options.OutputPath, $"{ids.Count} land tiles saved successfully.");
}
private static void ExportLandTileImage(int index, ImageFormat imageFormat)
@@ -623,6 +752,10 @@ private void OnClickSelectTexture(object sender, EventArgs e)
private void LandTilesContextMenuStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e)
{
+ int selectedCount = LandTilesTileView.SelectedIndices.Count;
+ removeToolStripMenuItem.Text = selectedCount > 1 ? $"Remove {selectedCount}" : "Remove";
+ exportImageToolStripMenuItem.Text = selectedCount > 1 ? $"Export {selectedCount} Images..." : "Export Image..";
+
bool hasTexture = _selectedGraphicId >= 0
&& TileData.LandTable[_selectedGraphicId].TextureId != 0
&& Textures.TestTexture(TileData.LandTable[_selectedGraphicId].TextureId);
@@ -752,6 +885,23 @@ private void LandTilesTileView_DrawItem(object sender, TileView.TileViewControl.
}
}
+ ///
+ /// Resolves the current tile selection to a sorted list of graphic IDs.
+ ///
+ private List GetSelectedGraphicIds()
+ {
+ var ids = new List();
+ foreach (int idx in LandTilesTileView.SelectedIndices)
+ {
+ if (idx >= 0 && idx < _tileList.Count)
+ {
+ ids.Add(_tileList[idx]);
+ }
+ }
+ ids.Sort();
+ return ids;
+ }
+
private void LandTilesTileView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
if (!e.IsSelected)
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs b/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
index 149870a..e046dfb 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.Designer.cs
@@ -158,6 +158,7 @@ private void InitializeComponent()
tileViewItem.Name = "tileViewItem";
tileViewItem.Size = new System.Drawing.Size(259, 289);
tileViewItem.TabIndex = 0;
+ tileViewItem.MultiSelect = true;
tileViewItem.ShowCheckBoxes = true;
tileViewItem.TileHighLightOpacity = 0D;
tileViewItem.FocusSelectionChanged += OnItemFocusChanged;
@@ -208,6 +209,7 @@ private void InitializeComponent()
tileViewLand.Name = "tileViewLand";
tileViewLand.Size = new System.Drawing.Size(228, 164);
tileViewLand.TabIndex = 0;
+ tileViewLand.MultiSelect = true;
tileViewLand.ShowCheckBoxes = true;
tileViewLand.TileHighLightOpacity = 0D;
tileViewLand.FocusSelectionChanged += OnLandFocusChanged;
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.cs b/UoFiddler.Controls/UserControls/RadarColorControl.cs
index 83e6d2c..e13d811 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.cs
@@ -111,9 +111,13 @@ private static void ConfigureTileView(TileView.TileViewControl tv)
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
// Suppress the default focus-rectangle (DarkRed 1px outline). DrawRow already
- // renders a SystemBrushes.Highlight fill for the focused row, so the extra
- // border just adds a red line at the row edges.
+ // renders a highlight fill for the focused row, so the extra border just adds a
+ // red line at the row edges.
tv.TileFocusColor = Color.Transparent;
+ // Mark checked (multi-selected) rows with the configured selection color, matching
+ // the Items/LandTiles tabs.
+ tv.TileHighlightColor = Options.TileSelectionColor;
+ tv.TileHighLightOpacity = 0.4;
}
private void OnTileViewSizeChanged(object sender, EventArgs e)
@@ -164,14 +168,21 @@ private void OnDrawLandRow(object sender, TileView.TileViewControl.DrawTileListI
private const int SwatchSize = 12;
private const int SwatchGap = 4;
+ // Perceived-luminance test so focused-row text/border stays readable on any selection color.
+ private static bool IsDarkColor(Color c) => (0.299 * c.R + 0.587 * c.G + 0.114 * c.B) < 128;
+
private static void DrawRow(TileView.TileViewControl.DrawTileListItemEventArgs e, int graphic, string name, bool modified, ushort radarHue)
{
bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ Color highlightColor = Options.TileSelectionColor;
+ Color highlightTextColor = IsDarkColor(highlightColor) ? Color.White : Color.Black;
+
// Row background.
if (focused)
{
- e.Graphics.FillRectangle(SystemBrushes.Highlight, e.Bounds);
+ using var highlightBrush = new SolidBrush(highlightColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
@@ -187,7 +198,7 @@ private static void DrawRow(TileView.TileViewControl.DrawTileListItemEventArgs e
{
e.Graphics.FillRectangle(swatchBrush, swatchRect);
}
- using (var swatchBorder = new Pen(focused ? SystemColors.HighlightText : SystemColors.ControlDark))
+ using (var swatchBorder = new Pen(focused ? highlightTextColor : SystemColors.ControlDark))
{
e.Graphics.DrawRectangle(swatchBorder, swatchRect);
}
@@ -196,7 +207,7 @@ private static void DrawRow(TileView.TileViewControl.DrawTileListItemEventArgs e
Color textColor;
if (focused)
{
- textColor = SystemColors.HighlightText;
+ textColor = highlightTextColor;
}
else if (modified)
{
diff --git a/UoFiddler.Controls/UserControls/TexturesControl.Designer.cs b/UoFiddler.Controls/UserControls/TexturesControl.Designer.cs
index 26bed25..20a4a8b 100644
--- a/UoFiddler.Controls/UserControls/TexturesControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/TexturesControl.Designer.cs
@@ -316,7 +316,7 @@ private void InitializeComponent()
TextureTileView.FocusIndex = -1;
TextureTileView.Location = new System.Drawing.Point(0, 0);
TextureTileView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
- TextureTileView.MultiSelect = false;
+ TextureTileView.MultiSelect = true;
TextureTileView.Name = "TextureTileView";
TextureTileView.Size = new System.Drawing.Size(675, 347);
TextureTileView.TabIndex = 6;
diff --git a/UoFiddler.Controls/UserControls/TexturesControl.cs b/UoFiddler.Controls/UserControls/TexturesControl.cs
index 6f26925..3fac256 100644
--- a/UoFiddler.Controls/UserControls/TexturesControl.cs
+++ b/UoFiddler.Controls/UserControls/TexturesControl.cs
@@ -15,6 +15,7 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using System.Linq;
using System.Windows.Forms;
using Ultima;
using UoFiddler.Controls.Classes;
@@ -246,32 +247,41 @@ private void OnClickFindNext(object sender, EventArgs e)
private void OnClickRemove(object sender, EventArgs e)
{
- if (_selectedTextureId < 0)
+ var ids = GetSelectedTextureIds().Where(Textures.TestTexture).ToList();
+ if (ids.Count == 0)
{
return;
}
- if (!Textures.TestTexture(_selectedTextureId))
- {
- return;
- }
+ string prompt = ids.Count == 1
+ ? $"Are you sure to remove 0x{ids[0]:X}"
+ : $"Are you sure to remove {ids.Count} textures?";
- DialogResult result = MessageBox.Show($"Are you sure to remove 0x{_selectedTextureId:X}", "Save",
+ DialogResult result = MessageBox.Show(prompt, "Save",
MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (result != DialogResult.Yes)
{
return;
}
- Textures.Remove(_selectedTextureId);
- ControlEvents.FireTextureChangeEvent(this, _selectedTextureId);
+ foreach (int id in ids)
+ {
+ Textures.Remove(id);
+ ControlEvents.FireTextureChangeEvent(this, id);
+
+ if (!_showFreeSlots)
+ {
+ _textureList.Remove(id);
+ }
+ }
+
+ TextureTileView.SelectedIndices.Clear();
if (!_showFreeSlots)
{
- _textureList.Remove(_selectedTextureId);
TextureTileView.VirtualListSize = _textureList.Count;
- var moveToIndex = --_selectedTextureId;
- SelectedTextureId = moveToIndex <= 0 ? 0 : _selectedTextureId; // TODO: get last index visible instead just curr -1
+ int moveToId = ids[0] - 1;
+ SelectedTextureId = moveToId <= 0 ? 0 : moveToId; // TODO: get last index visible instead just curr -1
}
TextureTileView.Invalidate();
@@ -281,6 +291,12 @@ private void OnClickRemove(object sender, EventArgs e)
private void OnClickReplace(object sender, EventArgs e)
{
+ if (TextureTileView.SelectedIndices.Count > 1)
+ {
+ ReplaceMultipleSelected();
+ return;
+ }
+
if (_selectedTextureId < 0)
{
return;
@@ -317,6 +333,94 @@ private void OnClickReplace(object sender, EventArgs e)
}
}
+ private void ReplaceMultipleSelected()
+ {
+ var ids = GetSelectedTextureIds();
+ if (ids.Count == 0)
+ {
+ return;
+ }
+
+ using (OpenFileDialog dialog = new OpenFileDialog())
+ {
+ dialog.Multiselect = true;
+ dialog.Title = $"Choose {ids.Count} image files to replace selected textures";
+ dialog.CheckFileExists = true;
+ dialog.Filter = "Image files (*.tif;*.tiff;*.bmp;*.png)|*.tif;*.tiff;*.bmp;*.png";
+
+ if (dialog.ShowDialog() != DialogResult.OK)
+ {
+ return;
+ }
+
+ var files = dialog.FileNames.OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase).ToArray();
+
+ if (files.Length != ids.Count)
+ {
+ MessageBox.Show(
+ $"Selected {ids.Count} textures but chose {files.Length} images.\n\nNo changes made.",
+ "Selection Mismatch",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
+ // Load and validate all images first; abort the whole batch on any failure so no partial writes happen.
+ var bitmaps = new List(ids.Count);
+ try
+ {
+ for (int i = 0; i < ids.Count; ++i)
+ {
+ using (var bmpTemp = new Bitmap(files[i]))
+ {
+ bool validSize = (bmpTemp.Width == 64 && bmpTemp.Height == 64)
+ || (bmpTemp.Width == 128 && bmpTemp.Height == 128);
+
+ if (!validSize)
+ {
+ MessageBox.Show(
+ $"Invalid texture dimensions!\n\n" +
+ $"File: {Path.GetFileName(files[i])} ({bmpTemp.Width}x{bmpTemp.Height})\n" +
+ $"Textures must be 64x64 or 128x128 pixels.\n\n" +
+ $"No changes made.",
+ "Invalid Size",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
+ Bitmap bitmap = new Bitmap(bmpTemp);
+
+ if (files[i].Contains(".bmp"))
+ {
+ bitmap = Utils.ConvertBmp(bitmap);
+ }
+
+ bitmaps.Add(bitmap);
+ }
+ }
+ }
+ catch
+ {
+ foreach (var bmp in bitmaps)
+ {
+ bmp.Dispose();
+ }
+ throw;
+ }
+
+ for (int i = 0; i < ids.Count; ++i)
+ {
+ Textures.Replace(ids[i], bitmaps[i]);
+ ControlEvents.FireTextureChangeEvent(this, ids[i]);
+ }
+
+ TextureTileView.Invalidate();
+
+ Options.ChangedUltimaClass["Texture"] = true;
+ }
+ }
+
private void OnTextChangedInsert(object sender, EventArgs e)
{
Color invalidColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
@@ -421,22 +525,61 @@ private void OnClickSave(object sender, EventArgs e)
private void OnClickExportBmp(object sender, EventArgs e)
{
- ExportTextureImage(_selectedTextureId, ImageFormat.Bmp);
+ ExportSelected(ImageFormat.Bmp);
}
private void OnClickExportTiff(object sender, EventArgs e)
{
- ExportTextureImage(_selectedTextureId, ImageFormat.Tiff);
+ ExportSelected(ImageFormat.Tiff);
}
private void OnClickExportJpg(object sender, EventArgs e)
{
- ExportTextureImage(_selectedTextureId, ImageFormat.Jpeg);
+ ExportSelected(ImageFormat.Jpeg);
}
private void OnClickExportPng(object sender, EventArgs e)
{
- ExportTextureImage(_selectedTextureId, ImageFormat.Png);
+ ExportSelected(ImageFormat.Png);
+ }
+
+ private void ExportSelected(ImageFormat imageFormat)
+ {
+ var ids = GetSelectedTextureIds().Where(Textures.TestTexture).ToList();
+ if (ids.Count == 0)
+ {
+ return;
+ }
+
+ if (ids.Count == 1)
+ {
+ ExportTextureImage(ids[0], imageFormat);
+ return;
+ }
+
+ ExportMultipleTextureImages(ids, imageFormat);
+ }
+
+ private void ExportMultipleTextureImages(List ids, ImageFormat imageFormat)
+ {
+ string fileExtension = Utils.GetFileExtensionFor(imageFormat);
+
+ foreach (int index in ids)
+ {
+ var texture = Textures.GetTexture(index);
+ if (texture is null)
+ {
+ continue;
+ }
+
+ string fileName = Path.Combine(Options.OutputPath, $"Texture {Utils.FormatExportId(index)}.{fileExtension}");
+ using (Bitmap bit = new Bitmap(texture))
+ {
+ bit.Save(fileName, imageFormat);
+ }
+ }
+
+ FileSavedDialog.Show(FindForm(), Options.OutputPath, $"{ids.Count} textures saved successfully.");
}
private static void ExportTextureImage(int index, ImageFormat imageFormat)
@@ -458,6 +601,23 @@ private static void ExportTextureImage(int index, ImageFormat imageFormat)
MessageBoxDefaultButton.Button1);
}
+ ///
+ /// Resolves the current tile selection to a sorted list of texture IDs.
+ ///
+ private List GetSelectedTextureIds()
+ {
+ var ids = new List();
+ foreach (int idx in TextureTileView.SelectedIndices)
+ {
+ if (idx >= 0 && idx < _textureList.Count)
+ {
+ ids.Add(_textureList[idx]);
+ }
+ }
+ ids.Sort();
+ return ids;
+ }
+
private void TextureTileView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
if (!e.IsSelected)
@@ -774,6 +934,10 @@ private void OnClickSelectInLandTiles(object sender, EventArgs e)
private void contextMenuStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e)
{
+ int selectedCount = TextureTileView.SelectedIndices.Count;
+ removeToolStripMenuItem.Text = selectedCount > 1 ? $"Remove {selectedCount}" : "Remove";
+ exportImageToolStripMenuItem.Text = selectedCount > 1 ? $"Export {selectedCount} Images..." : "Export Image..";
+
bool hasLandTile = _selectedTextureId >= 0
&& _selectedTextureId < 0x4000
&& Art.IsValidLand(_selectedTextureId);
diff --git a/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs b/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
index bec15b1..7f1977f 100644
--- a/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
+++ b/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
@@ -23,6 +23,8 @@ public class TileViewControl : ScrollableControl
private int _focusIndex = -1;
+ private int _selectionAnchorIndex = -1;
+
///
/// Get or Set SelectedIndex, setting this property to -1 will remove selection, -2 is reserved for "do nothing".
///
@@ -163,6 +165,7 @@ public int VirtualListSize
}
SelectedIndices.Clear();
+ _selectionAnchorIndex = -1;
_cachedIndices.Clear();
UpdateAutoScrollSize();
}
@@ -367,16 +370,44 @@ public TileViewControl()
{
int idx = GetIndexAtLocation(e.Location);
- if (_showCheckBoxes && idx >= 0 && e.Button == MouseButtons.Left && IsInCheckBoxRegion(e.Location, idx))
+ if (_showCheckBoxes && idx >= 0 && e.Button == MouseButtons.Left)
{
- if (SelectedIndices.Contains(idx))
+ Keys mods = ModifierKeys;
+
+ // Shift (with or without Ctrl) applies the clicked row's resulting state to the whole
+ // range from the anchor, so a contiguous block can be checked or unchecked in one
+ // action. The anchor is the last row that was clicked/focused.
+ if ((mods & Keys.Shift) == Keys.Shift)
{
- SelectedIndices.Remove(idx);
+ int anchor = _selectionAnchorIndex >= 0 && _selectionAnchorIndex < _virtualListSize
+ ? _selectionAnchorIndex
+ : idx;
+ bool select = !SelectedIndices.Contains(idx);
+ SetRangeSelection(anchor, idx, select);
+ FocusIndex = idx;
+ return;
}
- else
+
+ // Ctrl-click anywhere on the row, or a click directly on the checkbox, toggles that row.
+ if ((mods & Keys.Control) == Keys.Control || IsInCheckBoxRegion(e.Location, idx))
{
- SelectedIndices.Add(idx);
+ if (SelectedIndices.Contains(idx))
+ {
+ SelectedIndices.Remove(idx);
+ }
+ else
+ {
+ SelectedIndices.Add(idx);
+ }
+ _selectionAnchorIndex = idx;
+ FocusIndex = idx;
+ return;
}
+
+ // Plain click on the row body moves focus and sets the anchor for a later Shift range,
+ // keeping the current checkbox selection.
+ _selectionAnchorIndex = idx;
+ FocusIndex = idx;
return;
}
@@ -527,7 +558,20 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
private void SelectIndex(int index)
{
- switch (ModifierKeys)
+ Keys modifiers = ModifierKeys;
+
+ // Range selection: Shift extends from the anchor. Ctrl+Shift adds the range to the
+ // existing selection, plain Shift replaces it. The anchor is left untouched so further
+ // Shift-clicks keep extending from the same starting point.
+ if (_multiSelect && (modifiers & Keys.Shift) == Keys.Shift)
+ {
+ int anchor = _selectionAnchorIndex >= 0 ? _selectionAnchorIndex : index;
+ bool additive = (modifiers & Keys.Control) == Keys.Control;
+ SelectRange(anchor, index, additive);
+ return;
+ }
+
+ switch (modifiers)
{
case Keys.Control:
if (_multiSelect)
@@ -550,6 +594,8 @@ private void SelectIndex(int index)
}
}
+ _selectionAnchorIndex = index;
+
break;
default:
// When the checkbox column is visible, the selection set is owned by the checkboxes;
@@ -565,10 +611,55 @@ private void SelectIndex(int index)
SelectedIndices.Add(index);
}
+ _selectionAnchorIndex = index;
+
break;
}
}
+ private void SelectRange(int fromIndex, int toIndex, bool additive)
+ {
+ if (!additive)
+ {
+ SelectedIndices.Clear();
+ }
+
+ int start = Math.Min(fromIndex, toIndex);
+ int end = Math.Max(fromIndex, toIndex);
+
+ for (int i = start; i <= end; ++i)
+ {
+ if (!SelectedIndices.Contains(i))
+ {
+ SelectedIndices.Add(i);
+ }
+ }
+ }
+
+ ///
+ /// Forces every index in the inclusive range to the given selected state, leaving items
+ /// outside the range untouched. Used by checkbox-mode Shift selection so a range can be
+ /// both checked and unchecked.
+ ///
+ private void SetRangeSelection(int fromIndex, int toIndex, bool select)
+ {
+ int start = Math.Min(fromIndex, toIndex);
+ int end = Math.Max(fromIndex, toIndex);
+
+ for (int i = start; i <= end; ++i)
+ {
+ bool contains = SelectedIndices.Contains(i);
+ if (select && !contains)
+ {
+ SelectedIndices.Add(i);
+ }
+ else if (!select && contains)
+ {
+ SelectedIndices.Remove(i);
+ }
+ }
+ }
+
///
/// Redraw Tile with given index
///
diff --git a/UoFiddler.Plugin.Compare/Classes/CompareColors.cs b/UoFiddler.Plugin.Compare/Classes/CompareColors.cs
new file mode 100644
index 0000000..af67312
--- /dev/null
+++ b/UoFiddler.Plugin.Compare/Classes/CompareColors.cs
@@ -0,0 +1,28 @@
+/***************************************************************************
+ *
+ * $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.Drawing;
+
+namespace UoFiddler.Plugin.Compare.Classes
+{
+ public static class CompareColors
+ {
+ ///
+ /// Perceived-luminance test so highlighted-row text stays readable on any selection color.
+ ///
+ public static bool IsDarkColor(Color c) => (0.299 * c.R + 0.587 * c.G + 0.114 * c.B) < 128;
+
+ ///
+ /// Returns a readable text brush (white on dark, black on light) for the given background.
+ ///
+ public static Brush ContrastBrush(Color background) => IsDarkColor(background) ? Brushes.White : Brushes.Black;
+ }
+}
diff --git a/UoFiddler.Plugin.Compare/Classes/CompareFiles.cs b/UoFiddler.Plugin.Compare/Classes/CompareFiles.cs
new file mode 100644
index 0000000..e7dd9be
--- /dev/null
+++ b/UoFiddler.Plugin.Compare/Classes/CompareFiles.cs
@@ -0,0 +1,65 @@
+/***************************************************************************
+ *
+ * $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.IO;
+using Ultima;
+
+namespace UoFiddler.Plugin.Compare.Classes
+{
+ ///
+ /// Helpers to guard against comparing a file against itself in the compare plugin.
+ ///
+ public static class CompareFiles
+ {
+ ///
+ /// Returns true when both paths resolve to the same physical file (case-insensitive, normalized).
+ ///
+ public static bool IsSamePath(string a, string b)
+ {
+ if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b))
+ {
+ return false;
+ }
+
+ try
+ {
+ return string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Returns true when the chosen path matches the currently loaded client file resolved
+ /// from any of the given client file names (via ).
+ ///
+ public static bool IsLoadedClientFile(string chosenPath, params string[] clientFileNames)
+ {
+ if (string.IsNullOrEmpty(chosenPath))
+ {
+ return false;
+ }
+
+ foreach (string name in clientFileNames)
+ {
+ if (IsSamePath(Files.GetFilePath(name), chosenPath))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
index df3c4a7..33c8275 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
@@ -31,7 +31,6 @@ private void OnLoad(object sender, EventArgs e)
ConfigureTileView(tileViewSec);
PopulateOrgList();
- tileViewSec.MultiSelect = true;
tileViewSec.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
contextMenuStrip1.Opening += (s, ev) =>
{
@@ -52,11 +51,15 @@ private static void ConfigureTileView(TileViewControl tv)
tv.TileMargin = new Padding(0);
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
+ tv.TileFocusColor = Color.Transparent;
+ tv.TileHighlightColor = Options.TileSelectionColor;
+ tv.TileHighLightOpacity = 0.4;
}
private void OnChangeMultiSelect(object sender, EventArgs e)
{
tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileViewSec.MultiSelect = chkMultiSelect.Checked;
if (!chkMultiSelect.Checked)
{
tileViewSec.SelectedIndices.Clear();
@@ -139,16 +142,20 @@ private void OnDrawItemSec(object sender, TileViewControl.DrawTileListItemEventA
private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int id)
{
- if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ if (focused)
{
- e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
+ using var highlightBrush = new SolidBrush(Options.TileSelectionColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
}
- Brush fontBrush = GetEntryBrush(id);
+ Brush fontBrush = focused
+ ? CompareColors.ContrastBrush(Options.TileSelectionColor)
+ : GetEntryBrush(id);
string text = $"0x{id:X4} ({id})";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(text, e.Font).Height) / 2f;
e.Graphics.DrawString(text, e.Font, fontBrush, new PointF(e.ContentLeft + 4, y));
@@ -308,6 +315,17 @@ private void OnClickLoadSecond(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsLoadedClientFile(path, "animdata.mul"))
+ {
+ MessageBox.Show(
+ "The selected file is the same as the currently loaded animdata.mul.\n\n" +
+ "Choose a different file to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
if (!SecondAnimdata.Initialize(path))
{
MessageBox.Show("Failed to load the selected animdata.mul file.", "Error",
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.cs
index 21d50bc..2cc23d7 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.cs
@@ -16,6 +16,7 @@
using System.Windows.Forms;
using Ultima;
using UoFiddler.Controls.Classes;
+using UoFiddler.Plugin.Compare.Classes;
namespace UoFiddler.Plugin.Compare.UserControls
{
@@ -52,6 +53,17 @@ private void OnLoad(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsSamePath(path, textBox2.Text))
+ {
+ MessageBox.Show(
+ "Both sides point to the same cliloc file.\n\n" +
+ "Choose a different file to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
_cliloc1 = new StringList("1", path, false);
_cliloc1.Entries.Sort(new StringList.NumberComparer(false));
@@ -74,6 +86,17 @@ private void OnLoad2(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsSamePath(path, textBox1.Text))
+ {
+ MessageBox.Show(
+ "Both sides point to the same cliloc file.\n\n" +
+ "Choose a different file to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
_cliloc2 = new StringList("2", path, false);
_cliloc2.Entries.Sort(new StringList.NumberComparer(false));
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
index ea266dc..7236bbe 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
@@ -67,7 +67,6 @@ private void OnLoad(object sender, EventArgs e)
if (!_loaded)
{
- tileView2.MultiSelect = true;
tileView2.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
contextMenuStrip1.Opening += (s, ev) =>
{
@@ -91,11 +90,15 @@ private static void ConfigureTileView(TileViewControl tv)
tv.TileMargin = new Padding(0);
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
+ tv.TileFocusColor = Color.Transparent;
+ tv.TileHighlightColor = Options.TileSelectionColor;
+ tv.TileHighLightOpacity = 0.4;
}
private void OnChangeMultiSelect(object sender, EventArgs e)
{
tileView2.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileView2.MultiSelect = chkMultiSelect.Checked;
if (!chkMultiSelect.Checked)
{
tileView2.SelectedIndices.Clear();
@@ -173,9 +176,11 @@ private void OnDrawItem2(object sender, TileViewControl.DrawTileListItemEventArg
private void DrawGumpItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
- if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ if (focused)
{
- e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
+ using var highlightBrush = new SolidBrush(Options.TileSelectionColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
@@ -209,6 +214,11 @@ private void DrawGumpItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
fontBrush = Options.DarkMode ? Brushes.OrangeRed : Brushes.Red;
}
+ if (focused)
+ {
+ fontBrush = CompareColors.ContrastBrush(Options.TileSelectionColor);
+ }
+
string label = $"0x{i:X}";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, Font).Height) / 2f;
e.Graphics.DrawString(label, Font, fontBrush, new PointF(e.ContentLeft + 85, y));
@@ -307,6 +317,17 @@ private void Load_Click(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsLoadedClientFile(resolvedMul, "gumpart.mul") || CompareFiles.IsLoadedClientFile(resolvedUop, "gumpartLegacyMUL.uop"))
+ {
+ MessageBox.Show(
+ "The selected files are the same as the currently loaded gump files.\n\n" +
+ "Choose a different directory to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
SecondGump.SetFileIndex(resolvedIdx, resolvedMul, resolvedUop);
LoadSecond();
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
index 1df2c80..9ced706 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
@@ -259,6 +259,17 @@ private void OnClickLoad(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsLoadedClientFile(file, "hues.mul"))
+ {
+ MessageBox.Show(
+ "The selected file is the same as the currently loaded hues.mul.\n\n" +
+ "Choose a different directory to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
SecondHue.Initialize(file);
_hue2Loaded = true;
vScrollBar.Value = 0;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
index af1e328..2d59371 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
@@ -54,7 +54,6 @@ private void OnLoad(object sender, EventArgs e)
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = 0;
- tileViewSec.MultiSelect = true;
tileViewSec.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
contextMenuStrip1.Opening += (s, ev) =>
{
@@ -81,11 +80,15 @@ private static void ConfigureTileView(TileViewControl tv)
tv.TileMargin = new Padding(0);
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
+ tv.TileFocusColor = Color.Transparent;
+ tv.TileHighlightColor = Options.TileSelectionColor;
+ tv.TileHighLightOpacity = 0.4;
}
private void OnChangeMultiSelect(object sender, EventArgs e)
{
tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileViewSec.MultiSelect = chkMultiSelect.Checked;
if (!chkMultiSelect.Checked)
{
tileViewSec.SelectedIndices.Clear();
@@ -169,9 +172,11 @@ private void OnDrawItemSec(object sender, TileViewControl.DrawTileListItemEventA
private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
- if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ if (focused)
{
- e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
+ using var highlightBrush = new SolidBrush(Options.TileSelectionColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
@@ -190,6 +195,11 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
fontBrush = Options.DarkMode ? Brushes.CornflowerBlue : Brushes.Blue;
}
+ if (focused)
+ {
+ fontBrush = CompareColors.ContrastBrush(Options.TileSelectionColor);
+ }
+
string label = $"0x{i:X}";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, Font).Height) / 2f;
e.Graphics.DrawString(label, Font, fontBrush, new PointF(e.ContentLeft + 5, y));
@@ -279,6 +289,17 @@ private void OnClickLoadSecond(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsLoadedClientFile(resolvedMul, "art.mul") || CompareFiles.IsLoadedClientFile(resolvedUop, "artLegacyMUL.uop"))
+ {
+ MessageBox.Show(
+ "The selected files are the same as the currently loaded art files.\n\n" +
+ "Choose a different directory to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
SecondArt.SetFileIndex(resolvedIdx, resolvedMul, resolvedUop);
LoadSecond();
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
index 4368bba..e01482b 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
@@ -53,7 +53,6 @@ private void OnLoad(object sender, EventArgs e)
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = 0;
- tileViewSec.MultiSelect = true;
tileViewSec.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
contextMenuStrip1.Opening += (s, ev) =>
{
@@ -80,11 +79,15 @@ private static void ConfigureTileView(TileViewControl tv)
tv.TileMargin = new Padding(0);
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
+ tv.TileFocusColor = Color.Transparent;
+ tv.TileHighlightColor = Options.TileSelectionColor;
+ tv.TileHighLightOpacity = 0.4;
}
private void OnChangeMultiSelect(object sender, EventArgs e)
{
tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileViewSec.MultiSelect = chkMultiSelect.Checked;
if (!chkMultiSelect.Checked)
{
tileViewSec.SelectedIndices.Clear();
@@ -168,9 +171,11 @@ private void OnDrawItemSec(object sender, TileViewControl.DrawTileListItemEventA
private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
- if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ if (focused)
{
- e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
+ using var highlightBrush = new SolidBrush(Options.TileSelectionColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
@@ -189,6 +194,11 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
fontBrush = Options.DarkMode ? Brushes.CornflowerBlue : Brushes.Blue;
}
+ if (focused)
+ {
+ fontBrush = CompareColors.ContrastBrush(Options.TileSelectionColor);
+ }
+
string label = $"0x{i:X}";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, Font).Height) / 2f;
e.Graphics.DrawString(label, Font, fontBrush, new PointF(e.ContentLeft + 5, y));
@@ -262,6 +272,17 @@ private void OnClickLoadSecond(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsLoadedClientFile(resolvedMul, "art.mul") || CompareFiles.IsLoadedClientFile(resolvedUop, "artLegacyMUL.uop"))
+ {
+ MessageBox.Show(
+ "The selected files are the same as the currently loaded art files.\n\n" +
+ "Choose a different directory to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
SecondArt.SetFileIndex(resolvedIdx, resolvedMul, resolvedUop);
LoadSecond();
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
index 8404564..22188a4 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
@@ -19,6 +19,7 @@
using System.Windows.Forms;
using Ultima;
using UoFiddler.Controls.Classes;
+using UoFiddler.Plugin.Compare.Classes;
namespace UoFiddler.Plugin.Compare.UserControls
{
@@ -622,6 +623,19 @@ private void OnClickBrowseLoc(object sender, EventArgs e)
private void OnClickLoad(object sender, EventArgs e)
{
+ string path = toolStripTextBox1.Text;
+ if (Directory.Exists(path)
+ && CompareFiles.IsLoadedClientFile(Path.Combine(path, $"map{_currentMapId}.mul"), $"map{_currentMapId}.mul"))
+ {
+ MessageBox.Show(
+ "The selected directory contains the same map file that is currently loaded.\n\n" +
+ "Choose a different directory to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
ChangeMap();
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
index 1d6c8f2..686549a 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
@@ -41,8 +41,6 @@ private void OnLoad(object sender, EventArgs e)
ConfigureTileView(tileViewItemSec);
PopulateOrgOnly(isLand: true);
- tileViewSec.MultiSelect = true;
- tileViewItemSec.MultiSelect = true;
tileViewSec.SelectedIndices.CollectionChanged += OnLandSecSelectedIndicesChanged;
tileViewItemSec.SelectedIndices.CollectionChanged += OnItemSecSelectedIndicesChanged;
contextMenuStripSec.Opening += (s, ev) =>
@@ -64,12 +62,17 @@ private static void ConfigureTileView(TileViewControl tv)
tv.TileMargin = new Padding(0);
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
+ tv.TileFocusColor = Color.Transparent;
+ tv.TileHighlightColor = Options.TileSelectionColor;
+ tv.TileHighLightOpacity = 0.4;
}
private void OnChangeMultiSelect(object sender, EventArgs e)
{
tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
tileViewItemSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileViewSec.MultiSelect = chkMultiSelect.Checked;
+ tileViewItemSec.MultiSelect = chkMultiSelect.Checked;
if (!chkMultiSelect.Checked)
{
tileViewSec.SelectedIndices.Clear();
@@ -245,7 +248,8 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int idx,
bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
if (focused)
{
- e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
+ using var highlightBrush = new SolidBrush(Options.TileSelectionColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
@@ -274,9 +278,11 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int idx,
? $"0x{displayId:X4} ({displayId})"
: $"0x{displayId:X4} ({displayId}) {name}";
- Brush fontBrush = SecondRadarCol.IsLoaded && IsDifferent(idx)
- ? (Options.DarkMode ? Brushes.CornflowerBlue : Brushes.Blue)
- : Brushes.Gray;
+ Brush fontBrush = focused
+ ? CompareColors.ContrastBrush(Options.TileSelectionColor)
+ : SecondRadarCol.IsLoaded && IsDifferent(idx)
+ ? (Options.DarkMode ? Brushes.CornflowerBlue : Brushes.Blue)
+ : Brushes.Gray;
int textX = swatchX + SwatchSize + SwatchGap;
float textY = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(text, e.Font).Height) / 2f;
@@ -463,6 +469,17 @@ private void OnClickLoadSecond(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsLoadedClientFile(path, "radarcol.mul"))
+ {
+ MessageBox.Show(
+ "The selected file is the same as the currently loaded radarcol.mul.\n\n" +
+ "Choose a different file to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
Cursor.Current = Cursors.WaitCursor;
bool ok = SecondRadarCol.Initialize(path);
Cursor.Current = Cursors.Default;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
index c1701b4..3b4d68d 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
@@ -53,7 +53,6 @@ private void OnLoad(object sender, EventArgs e)
tileViewOrg.VirtualListSize = _displayIndices.Count;
tileViewSec.VirtualListSize = 0;
- tileViewSec.MultiSelect = true;
tileViewSec.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
contextMenuStrip1.Opening += (s, ev) =>
{
@@ -74,11 +73,15 @@ private static void ConfigureTileView(TileViewControl tv)
tv.TileMargin = new Padding(0);
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
+ tv.TileFocusColor = Color.Transparent;
+ tv.TileHighlightColor = Options.TileSelectionColor;
+ tv.TileHighLightOpacity = 0.4;
}
private void OnChangeMultiSelect(object sender, EventArgs e)
{
tileViewSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileViewSec.MultiSelect = chkMultiSelect.Checked;
if (!chkMultiSelect.Checked)
{
tileViewSec.SelectedIndices.Clear();
@@ -150,9 +153,11 @@ private void OnDrawItemSec(object sender, TileViewControl.DrawTileListItemEventA
private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
- if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ if (focused)
{
- e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
+ using var highlightBrush = new SolidBrush(Options.TileSelectionColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
@@ -171,6 +176,11 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
fontBrush = Options.DarkMode ? Brushes.CornflowerBlue : Brushes.Blue;
}
+ if (focused)
+ {
+ fontBrush = CompareColors.ContrastBrush(Options.TileSelectionColor);
+ }
+
string label = $"0x{i:X}";
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, Font).Height) / 2f;
e.Graphics.DrawString(label, Font, fontBrush, new PointF(e.ContentLeft + 5, y));
@@ -237,6 +247,17 @@ private void OnClickLoadSecond(object sender, EventArgs e)
string file2 = Path.Combine(path, "texidx.mul");
if (File.Exists(file) && File.Exists(file2))
{
+ if (CompareFiles.IsLoadedClientFile(file, "texmaps.mul") || CompareFiles.IsLoadedClientFile(file2, "texidx.mul"))
+ {
+ MessageBox.Show(
+ "The selected files are the same as the currently loaded texture files.\n\n" +
+ "Choose a different directory to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
SecondTexture.SetFileIndex(file2, file);
LoadSecond();
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
index c57d584..c39d62c 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
@@ -101,8 +101,6 @@ private void OnLoad(object sender, EventArgs e)
BuildRulesPanel();
SetInnerSplitterPositions();
- tileViewLandSec.MultiSelect = true;
- tileViewItemSec.MultiSelect = true;
tileViewLandSec.SelectedIndices.CollectionChanged += OnLandSecSelectedIndicesChanged;
tileViewItemSec.SelectedIndices.CollectionChanged += OnItemSecSelectedIndicesChanged;
}
@@ -115,12 +113,17 @@ private static void ConfigureTileView(TileViewControl tv)
tv.TileMargin = new Padding(0);
tv.TilePadding = new Padding(0);
tv.TileBorderWidth = 0f;
+ tv.TileFocusColor = Color.Transparent;
+ tv.TileHighlightColor = Options.TileSelectionColor;
+ tv.TileHighLightOpacity = 0.4;
}
private void OnChangeMultiSelect(object sender, EventArgs e)
{
tileViewLandSec.ShowCheckBoxes = chkMultiSelect.Checked;
tileViewItemSec.ShowCheckBoxes = chkMultiSelect.Checked;
+ tileViewLandSec.MultiSelect = chkMultiSelect.Checked;
+ tileViewItemSec.MultiSelect = chkMultiSelect.Checked;
if (!chkMultiSelect.Checked)
{
tileViewLandSec.SelectedIndices.Clear();
@@ -432,6 +435,17 @@ private void OnClickLoad(object sender, EventArgs e)
return;
}
+ if (CompareFiles.IsLoadedClientFile(tileFile, "tiledata.mul"))
+ {
+ MessageBox.Show(
+ "The selected file is the same as the currently loaded tiledata.mul.\n\n" +
+ "Choose a different directory to compare against.",
+ "Same File",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
string mulFile = Path.Combine(path, "art.mul");
string idxFile = Path.Combine(path, "artidx.mul");
string uopFile = Path.Combine(path, "artLegacyMUL.uop");
@@ -723,16 +737,18 @@ private void OnDrawItemLandSec(object sender, TileViewControl.DrawTileListItemEv
private void DrawLandItem(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
- if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ if (focused)
{
- e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
+ using var highlightBrush = new SolidBrush(Options.TileSelectionColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
}
- Brush brush = GetLandBrush(i, isSecondary);
+ Brush brush = focused ? CompareColors.ContrastBrush(Options.TileSelectionColor) : GetLandBrush(i, isSecondary);
string label = GetLandLabel(i, isSecondary);
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, e.Font).Height) / 2f;
@@ -804,16 +820,18 @@ private void OnDrawItemItemSec(object sender, TileViewControl.DrawTileListItemEv
private void DrawItemEntry(TileViewControl.DrawTileListItemEventArgs e, int i, bool isSecondary)
{
- if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ bool focused = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
+ if (focused)
{
- e.Graphics.FillRectangle(Brushes.LightSteelBlue, e.Bounds);
+ using var highlightBrush = new SolidBrush(Options.TileSelectionColor);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
else
{
e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
}
- Brush brush = GetItemBrush(i);
+ Brush brush = focused ? CompareColors.ContrastBrush(Options.TileSelectionColor) : GetItemBrush(i);
string label = GetItemLabel(i, isSecondary);
float y = e.Bounds.Y + (e.Bounds.Height - e.Graphics.MeasureString(label, e.Font).Height) / 2f;
From b46aa43bb041dcb739a385c822f8f445cb3b7b19 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 23:05:37 +0200
Subject: [PATCH 17/21] Fix replace with multi-select.
---
UoFiddler.Controls/UserControls/ItemsControl.cs | 1 +
UoFiddler.Controls/UserControls/LandTilesControl.cs | 1 +
UoFiddler.Controls/UserControls/TexturesControl.cs | 1 +
3 files changed, 3 insertions(+)
diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs
index 56c54c4..a323cfa 100644
--- a/UoFiddler.Controls/UserControls/ItemsControl.cs
+++ b/UoFiddler.Controls/UserControls/ItemsControl.cs
@@ -1225,6 +1225,7 @@ private void TileViewContextMenuStrip_Opening(object sender, CancelEventArgs e)
int selectedCount = ItemsTileView.SelectedIndices.Count;
removeToolStripMenuItem.Text = selectedCount > 1 ? $"Remove {selectedCount}" : "Remove";
extractToolStripMenuItem.Text = selectedCount > 1 ? $"Export {selectedCount} Images..." : "Export Image..";
+ replaceToolStripMenuItem.Text = selectedCount > 1 ? $"Replace {selectedCount}..." : "Replace...";
if (SelectedGraphicId <= 0)
{
diff --git a/UoFiddler.Controls/UserControls/LandTilesControl.cs b/UoFiddler.Controls/UserControls/LandTilesControl.cs
index 7063bb5..4939ae0 100644
--- a/UoFiddler.Controls/UserControls/LandTilesControl.cs
+++ b/UoFiddler.Controls/UserControls/LandTilesControl.cs
@@ -755,6 +755,7 @@ private void LandTilesContextMenuStrip_Opening(object sender, System.ComponentMo
int selectedCount = LandTilesTileView.SelectedIndices.Count;
removeToolStripMenuItem.Text = selectedCount > 1 ? $"Remove {selectedCount}" : "Remove";
exportImageToolStripMenuItem.Text = selectedCount > 1 ? $"Export {selectedCount} Images..." : "Export Image..";
+ replaceToolStripMenuItem.Text = selectedCount > 1 ? $"Replace {selectedCount}" : "Replace";
bool hasTexture = _selectedGraphicId >= 0
&& TileData.LandTable[_selectedGraphicId].TextureId != 0
diff --git a/UoFiddler.Controls/UserControls/TexturesControl.cs b/UoFiddler.Controls/UserControls/TexturesControl.cs
index 3fac256..50e0f1f 100644
--- a/UoFiddler.Controls/UserControls/TexturesControl.cs
+++ b/UoFiddler.Controls/UserControls/TexturesControl.cs
@@ -937,6 +937,7 @@ private void contextMenuStrip_Opening(object sender, System.ComponentModel.Cance
int selectedCount = TextureTileView.SelectedIndices.Count;
removeToolStripMenuItem.Text = selectedCount > 1 ? $"Remove {selectedCount}" : "Remove";
exportImageToolStripMenuItem.Text = selectedCount > 1 ? $"Export {selectedCount} Images..." : "Export Image..";
+ replaceToolStripMenuItem.Text = selectedCount > 1 ? $"Replace {selectedCount}" : "Replace";
bool hasLandTile = _selectedTextureId >= 0
&& _selectedTextureId < 0x4000
From 529df8422efd2731d436c19817822e300d8539b1 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 23:32:40 +0200
Subject: [PATCH 18/21] Add WaitCursorScope.
---
UoFiddler.Controls/Classes/WaitCursorScope.cs | 46 ++++
UoFiddler.Controls/Forms/AnimationEditForm.cs | 8 +-
.../Forms/MapReplaceTilesForm.cs | 68 +++---
.../Forms/TileDataSyncPreviewForm.cs | 60 +++---
.../UserControls/AnimDataControl.cs | 122 +++++------
.../UserControls/AnimationListControl.cs | 44 ++--
.../UserControls/ClilocControl.cs | 70 +++---
.../UserControls/DressControl.cs | 65 +++---
.../UserControls/FontsControl.cs | 91 ++++----
.../UserControls/GumpControl.cs | 76 +++----
.../UserControls/ItemsControl.cs | 124 +++++------
.../UserControls/LandTilesControl.cs | 83 +++----
.../UserControls/LightControl.cs | 53 ++---
UoFiddler.Controls/UserControls/MapControl.cs | 86 ++++----
.../UserControls/MultiMapControl.cs | 98 ++++-----
.../UserControls/MultisControl.cs | 41 ++--
.../UserControls/SkillGroupControl.cs | 79 +++----
.../UserControls/SkillsControl.cs | 53 ++---
.../UserControls/SoundsControl.cs | 119 +++++-----
.../UserControls/TexturesControl.cs | 75 +++----
.../UserControls/CompareAnimDataControl.cs | 90 ++++----
.../UserControls/CompareCliLocControl.cs | 5 +-
.../UserControls/CompareGumpControl.cs | 186 ++++++++--------
.../UserControls/CompareItemControl.cs | 167 +++++++-------
.../UserControls/CompareLandControl.cs | 161 +++++++-------
.../UserControls/CompareMapControl.cs | 94 ++++----
.../UserControls/CompareRadarColControl.cs | 93 ++++----
.../UserControls/CompareTextureControl.cs | 114 +++++-----
.../UserControls/CompareTileDataControl.cs | 203 +++++++++---------
.../Forms/MassImportForm.cs | 158 +++++++-------
UoFiddler/Forms/MainForm.cs | 145 +++++++------
31 files changed, 1463 insertions(+), 1414 deletions(-)
create mode 100644 UoFiddler.Controls/Classes/WaitCursorScope.cs
diff --git a/UoFiddler.Controls/Classes/WaitCursorScope.cs b/UoFiddler.Controls/Classes/WaitCursorScope.cs
new file mode 100644
index 0000000..eac8250
--- /dev/null
+++ b/UoFiddler.Controls/Classes/WaitCursorScope.cs
@@ -0,0 +1,46 @@
+/***************************************************************************
+ *
+ * $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.Windows.Forms;
+
+namespace UoFiddler.Controls.Classes
+{
+ ///
+ /// Shows a wait cursor for the duration of a blocking UI-thread operation.
+ /// Setting the parent form's survives mouse movement
+ /// (unlike alone, which is reset on the next WM_SETCURSOR),
+ /// and Dispose restores it even if the operation throws.
+ ///
+ public sealed class WaitCursorScope : IDisposable
+ {
+ private readonly Form _form;
+
+ public WaitCursorScope(Control control)
+ {
+ _form = control?.FindForm();
+ if (_form != null)
+ {
+ _form.UseWaitCursor = true;
+ }
+ Cursor.Current = Cursors.WaitCursor;
+ }
+
+ public void Dispose()
+ {
+ if (_form != null)
+ {
+ _form.UseWaitCursor = false;
+ }
+ Cursor.Current = Cursors.Default;
+ }
+ }
+}
diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs
index d0a0e46..8120c3e 100644
--- a/UoFiddler.Controls/Forms/AnimationEditForm.cs
+++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs
@@ -668,16 +668,10 @@ private void OnAnimChanged(object sender, EventArgs e)
}
_fileType = selected;
- Cursor previous = Cursor.Current;
- Cursor.Current = Cursors.WaitCursor;
- try
+ using (new WaitCursorScope(this))
{
OnLoad(this, EventArgs.Empty);
}
- finally
- {
- Cursor.Current = previous;
- }
}
private void OnDirectionChanged(object sender, EventArgs e)
diff --git a/UoFiddler.Controls/Forms/MapReplaceTilesForm.cs b/UoFiddler.Controls/Forms/MapReplaceTilesForm.cs
index 64d4f58..b9209a7 100644
--- a/UoFiddler.Controls/Forms/MapReplaceTilesForm.cs
+++ b/UoFiddler.Controls/Forms/MapReplaceTilesForm.cs
@@ -33,50 +33,50 @@ public MapReplaceTilesForm(Map map)
private void OnReplace(object sender, EventArgs e)
{
- try
+ using (new WaitCursorScope(this))
{
- Cursor.Current = Cursors.WaitCursor;
-
- button2.Enabled = false;
+ try
+ {
+ button2.Enabled = false;
- richTextBox1.Text = string.Empty;
- richTextBox1.AppendText("Replacement start...\r\n");
+ richTextBox1.Text = string.Empty;
+ richTextBox1.AppendText("Replacement start...\r\n");
- string file = textBox1.Text;
- if (string.IsNullOrEmpty(file))
- {
- richTextBox1.AppendText("Please specify XML file with replacements.\r\n");
- return;
- }
+ string file = textBox1.Text;
+ if (string.IsNullOrEmpty(file))
+ {
+ richTextBox1.AppendText("Please specify XML file with replacements.\r\n");
+ return;
+ }
- if (!File.Exists(file))
- {
- richTextBox1.AppendText("Specified file does not exist.\r\n");
- return;
- }
+ if (!File.Exists(file))
+ {
+ richTextBox1.AppendText("Specified file does not exist.\r\n");
+ return;
+ }
- if (!LoadFile(file))
- {
- richTextBox1.AppendText("Could not load replacement file.\r\n");
- return;
- }
+ if (!LoadFile(file))
+ {
+ richTextBox1.AppendText("Could not load replacement file.\r\n");
+ return;
+ }
- string path = Options.OutputPath;
+ string path = Options.OutputPath;
- richTextBox1.AppendText("Replacing map tiles...\r\n");
- ReplaceMap(path, _map.FileIndex, _map.Width, _map.Height);
+ richTextBox1.AppendText("Replacing map tiles...\r\n");
+ ReplaceMap(path, _map.FileIndex, _map.Width, _map.Height);
- richTextBox1.AppendText("Replacing static tiles...\r\n");
- ReplaceStatic(path, _map.FileIndex, _map.Width, _map.Height);
+ richTextBox1.AppendText("Replacing static tiles...\r\n");
+ ReplaceStatic(path, _map.FileIndex, _map.Width, _map.Height);
- richTextBox1.AppendText("Done.");
+ richTextBox1.AppendText("Done.");
- FileSavedDialog.Show(FindForm(), Options.OutputPath, "Files saved successfully.");
- }
- finally
- {
- button2.Enabled = true;
- Cursor.Current = Cursors.Default;
+ FileSavedDialog.Show(FindForm(), Options.OutputPath, "Files saved successfully.");
+ }
+ finally
+ {
+ button2.Enabled = true;
+ }
}
}
diff --git a/UoFiddler.Controls/Forms/TileDataSyncPreviewForm.cs b/UoFiddler.Controls/Forms/TileDataSyncPreviewForm.cs
index 297d469..0bc681f 100644
--- a/UoFiddler.Controls/Forms/TileDataSyncPreviewForm.cs
+++ b/UoFiddler.Controls/Forms/TileDataSyncPreviewForm.cs
@@ -170,49 +170,51 @@ private void UpdateSummary()
private void SetCheckedForAll(bool value)
{
- Cursor.Current = Cursors.WaitCursor;
- _suppressEvents = true;
- changesListView.BeginUpdate();
- try
+ using (new WaitCursorScope(this))
{
- foreach (var row in _rows)
+ _suppressEvents = true;
+ changesListView.BeginUpdate();
+ try
{
- row.Item.Checked = value;
- row.Checked = value;
+ foreach (var row in _rows)
+ {
+ row.Item.Checked = value;
+ row.Checked = value;
+ }
}
+ finally
+ {
+ changesListView.EndUpdate();
+ _suppressEvents = false;
+ }
+ UpdateSummary();
}
- finally
- {
- changesListView.EndUpdate();
- _suppressEvents = false;
- }
- UpdateSummary();
- Cursor.Current = Cursors.Default;
}
private void SetCheckedForKind(TileDataSyncKind kind, bool value)
{
- Cursor.Current = Cursors.WaitCursor;
- _suppressEvents = true;
- changesListView.BeginUpdate();
- try
+ using (new WaitCursorScope(this))
{
- foreach (var row in _rows)
+ _suppressEvents = true;
+ changesListView.BeginUpdate();
+ try
{
- if (row.Change.Kind == kind)
+ foreach (var row in _rows)
{
- row.Item.Checked = value;
- row.Checked = value;
+ if (row.Change.Kind == kind)
+ {
+ row.Item.Checked = value;
+ row.Checked = value;
+ }
}
}
+ finally
+ {
+ changesListView.EndUpdate();
+ _suppressEvents = false;
+ }
+ UpdateSummary();
}
- finally
- {
- changesListView.EndUpdate();
- _suppressEvents = false;
- }
- UpdateSummary();
- Cursor.Current = Cursors.Default;
}
private void OnCheckAll(object sender, EventArgs e)
diff --git a/UoFiddler.Controls/UserControls/AnimDataControl.cs b/UoFiddler.Controls/UserControls/AnimDataControl.cs
index 9fe59e5..4b1701f 100644
--- a/UoFiddler.Controls/UserControls/AnimDataControl.cs
+++ b/UoFiddler.Controls/UserControls/AnimDataControl.cs
@@ -180,78 +180,79 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["Animdata"] = true;
- Options.LoadedUltimaClass["TileData"] = true;
- Options.LoadedUltimaClass["Art"] = true;
-
- treeView1.BeginUpdate();
- treeView1.Nodes.Clear();
+ using (new WaitCursorScope(this))
+ {
+ Options.LoadedUltimaClass["Animdata"] = true;
+ Options.LoadedUltimaClass["TileData"] = true;
+ Options.LoadedUltimaClass["Art"] = true;
- treeView1.TreeViewNodeSorter = new AnimdataSorter();
+ treeView1.BeginUpdate();
+ treeView1.Nodes.Clear();
- foreach (int id in Animdata.AnimData.Keys)
- {
- Animdata.AnimdataEntry animdataEntry = Animdata.AnimData[id];
-
- TreeNode node = new TreeNode
- {
- Tag = id,
- Text = $"0x{id:X4} {TileData.ItemTable[id].Name}"
- };
+ treeView1.TreeViewNodeSorter = new AnimdataSorter();
- if (!Art.IsValidStatic(id))
+ foreach (int id in Animdata.AnimData.Keys)
{
- node.ForeColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
- }
- else if ((TileData.ItemTable[id].Flags & TileFlag.Animation) == 0)
- {
- node.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
- }
+ Animdata.AnimdataEntry animdataEntry = Animdata.AnimData[id];
- // TODO: find a better approach to this
- // we need to fix invalid entries as there cannot be more than 64 frames
- if (animdataEntry.FrameCount > 64)
- {
- animdataEntry.FrameCount = 64;
- Options.ChangedUltimaClass["Animdata"] = true;
- }
+ TreeNode node = new TreeNode
+ {
+ Tag = id,
+ Text = $"0x{id:X4} {TileData.ItemTable[id].Name}"
+ };
- treeView1.Nodes.Add(node);
+ if (!Art.IsValidStatic(id))
+ {
+ node.ForeColor = Options.DarkMode ? Color.OrangeRed : Color.Red;
+ }
+ else if ((TileData.ItemTable[id].Flags & TileFlag.Animation) == 0)
+ {
+ node.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
+ }
- for (int i = 0; i < animdataEntry.FrameCount; ++i)
- {
- int frame = id + animdataEntry.FrameData[i];
- if (Art.IsValidStatic(frame))
+ // TODO: find a better approach to this
+ // we need to fix invalid entries as there cannot be more than 64 frames
+ if (animdataEntry.FrameCount > 64)
{
- TreeNode subNode = new TreeNode
- {
- Text = $"0x{frame:X4} {TileData.ItemTable[frame].Name}"
- };
- node.Nodes.Add(subNode);
+ animdataEntry.FrameCount = 64;
+ Options.ChangedUltimaClass["Animdata"] = true;
}
- else
+
+ treeView1.Nodes.Add(node);
+
+ for (int i = 0; i < animdataEntry.FrameCount; ++i)
{
- break;
+ int frame = id + animdataEntry.FrameData[i];
+ if (Art.IsValidStatic(frame))
+ {
+ TreeNode subNode = new TreeNode
+ {
+ Text = $"0x{frame:X4} {TileData.ItemTable[frame].Name}"
+ };
+ node.Nodes.Add(subNode);
+ }
+ else
+ {
+ break;
+ }
}
}
- }
- treeView1.EndUpdate();
+ treeView1.EndUpdate();
- if (treeView1.Nodes.Count > 0)
- {
- treeView1.SelectedNode = treeView1.Nodes[0];
- _currentSelect = (int)treeView1.Nodes[0].Tag;
- }
+ if (treeView1.Nodes.Count > 0)
+ {
+ treeView1.SelectedNode = treeView1.Nodes[0];
+ _currentSelect = (int)treeView1.Nodes[0].Tag;
+ }
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- }
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ }
- _loaded = true;
- Cursor.Current = Cursors.Default;
+ _loaded = true;
+ }
}
private void OnFilePathChangeEvent()
@@ -564,10 +565,11 @@ private void OnClickRemove(object sender, EventArgs e)
private void OnClickSave(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- Animdata.Save(Options.OutputPath);
- Cursor.Current = Cursors.Default;
- Options.ChangedUltimaClass["Animdata"] = false;
+ using (new WaitCursorScope(this))
+ {
+ Animdata.Save(Options.OutputPath);
+ Options.ChangedUltimaClass["Animdata"] = false;
+ }
FileSavedDialog.Show(FindForm(), Options.OutputPath, "File saved successfully.");
}
diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs
index 69adfe0..198ffc5 100644
--- a/UoFiddler.Controls/UserControls/AnimationListControl.cs
+++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs
@@ -176,33 +176,33 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["Animations"] = true;
- Options.LoadedUltimaClass["Hues"] = true;
- TreeViewMobs.TreeViewNodeSorter = new GraphicSorter();
- if (!LoadXml())
+ using (new WaitCursorScope(this))
{
- Cursor.Current = Cursors.Default;
- return;
- }
+ Options.LoadedUltimaClass["Animations"] = true;
+ Options.LoadedUltimaClass["Hues"] = true;
+ TreeViewMobs.TreeViewNodeSorter = new GraphicSorter();
+ if (!LoadXml())
+ {
+ return;
+ }
- LoadListView();
+ LoadListView();
- _currentSelect = 0;
- _currentSelectAction = 0;
- if (TreeViewMobs.Nodes[0].Nodes.Count > 0)
- {
- TreeViewMobs.SelectedNode = TreeViewMobs.Nodes[0].Nodes[0];
- }
+ _currentSelect = 0;
+ _currentSelectAction = 0;
+ if (TreeViewMobs.Nodes[0].Nodes.Count > 0)
+ {
+ TreeViewMobs.SelectedNode = TreeViewMobs.Nodes[0].Nodes[0];
+ }
- FacingBar.Value = (_facing + 3) & 7;
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- }
+ FacingBar.Value = (_facing + 3) & 7;
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ }
- _loaded = true;
- Cursor.Current = Cursors.Default;
+ _loaded = true;
+ }
}
private void OnFilePathChangeEvent()
diff --git a/UoFiddler.Controls/UserControls/ClilocControl.cs b/UoFiddler.Controls/UserControls/ClilocControl.cs
index c93cd2b..794489d 100644
--- a/UoFiddler.Controls/UserControls/ClilocControl.cs
+++ b/UoFiddler.Controls/UserControls/ClilocControl.cs
@@ -98,40 +98,40 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- _sortOrder = SortOrder.Ascending;
- _sortColumn = 0;
- LangComboBox.SelectedIndex = 0;
- Lang = 0;
- _cliloc.Entries.Sort(new StringList.NumberComparer(false));
- _source.DataSource = _cliloc.Entries;
- dataGridView1.DataSource = _source;
- if (dataGridView1.Columns.Count > 0)
+ using (new WaitCursorScope(this))
{
- dataGridView1.Columns[0].HeaderCell.SortGlyphDirection = SortOrder.Ascending;
- dataGridView1.Columns[0].Width = 60;
- dataGridView1.Columns[1].HeaderCell.SortGlyphDirection = SortOrder.None;
- dataGridView1.Columns[2].HeaderCell.SortGlyphDirection = SortOrder.None;
- dataGridView1.Columns[2].Width = 60;
- dataGridView1.Columns[2].ReadOnly = true;
- }
- dataGridView1.Invalidate();
- LangComboBox.Items[2] = Files.GetFilePath("cliloc.custom1") != null
- ? $"Custom 1 ({Path.GetExtension(Files.GetFilePath("cliloc.custom1"))})"
- : "Custom 1";
-
- LangComboBox.Items[3] = Files.GetFilePath("cliloc.custom2") != null
- ? $"Custom 2 ({Path.GetExtension(Files.GetFilePath("cliloc.custom2"))})"
- : "Custom 2";
+ _sortOrder = SortOrder.Ascending;
+ _sortColumn = 0;
+ LangComboBox.SelectedIndex = 0;
+ Lang = 0;
+ _cliloc.Entries.Sort(new StringList.NumberComparer(false));
+ _source.DataSource = _cliloc.Entries;
+ dataGridView1.DataSource = _source;
+ if (dataGridView1.Columns.Count > 0)
+ {
+ dataGridView1.Columns[0].HeaderCell.SortGlyphDirection = SortOrder.Ascending;
+ dataGridView1.Columns[0].Width = 60;
+ dataGridView1.Columns[1].HeaderCell.SortGlyphDirection = SortOrder.None;
+ dataGridView1.Columns[2].HeaderCell.SortGlyphDirection = SortOrder.None;
+ dataGridView1.Columns[2].Width = 60;
+ dataGridView1.Columns[2].ReadOnly = true;
+ }
+ dataGridView1.Invalidate();
+ LangComboBox.Items[2] = Files.GetFilePath("cliloc.custom1") != null
+ ? $"Custom 1 ({Path.GetExtension(Files.GetFilePath("cliloc.custom1"))})"
+ : "Custom 1";
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- }
+ LangComboBox.Items[3] = Files.GetFilePath("cliloc.custom2") != null
+ ? $"Custom 2 ({Path.GetExtension(Files.GetFilePath("cliloc.custom2"))})"
+ : "Custom 2";
- _loaded = true;
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ }
- Cursor.Current = Cursors.Default;
+ _loaded = true;
+ }
}
private void OnFilePathChangeEvent()
@@ -660,11 +660,10 @@ private void TileDataToolStripMenuItem_Click(object sender, EventArgs e)
{
Cursor.Current = Cursors.WaitCursor;
- List changes;
TileDataSyncPreviewForm preview;
try
{
- changes = BuildTileDataSyncPlan();
+ List changes = BuildTileDataSyncPlan();
if (changes.Count == 0)
{
@@ -696,16 +695,11 @@ private void TileDataToolStripMenuItem_Click(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
int added, updated, removed;
- try
+ using (new WaitCursorScope(this))
{
ApplyTileDataSyncPlan(accepted, out added, out updated, out removed);
}
- finally
- {
- Cursor.Current = Cursors.Default;
- }
if (added + updated + removed > 0)
{
diff --git a/UoFiddler.Controls/UserControls/DressControl.cs b/UoFiddler.Controls/UserControls/DressControl.cs
index 0ea7978..d3ae25e 100644
--- a/UoFiddler.Controls/UserControls/DressControl.cs
+++ b/UoFiddler.Controls/UserControls/DressControl.cs
@@ -238,43 +238,44 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["TileData"] = true;
- Options.LoadedUltimaClass["Art"] = true;
- Options.LoadedUltimaClass["Hues"] = true;
- Options.LoadedUltimaClass["Animations"] = true;
- Options.LoadedUltimaClass["Gumps"] = true;
+ using (new WaitCursorScope(this))
+ {
+ Options.LoadedUltimaClass["TileData"] = true;
+ Options.LoadedUltimaClass["Art"] = true;
+ Options.LoadedUltimaClass["Hues"] = true;
+ Options.LoadedUltimaClass["Animations"] = true;
+ Options.LoadedUltimaClass["Gumps"] = true;
- checkBoxGargoyle.Visible = Art.IsUOAHS();
+ checkBoxGargoyle.Visible = Art.IsUOAHS();
- extractAnimationToolStripMenuItem.Visible = false;
- DressPic.Image = new Bitmap(DressPic.Width, DressPic.Height);
- pictureBoxDress.Image = new Bitmap(pictureBoxDress.Width, pictureBoxDress.Height);
+ extractAnimationToolStripMenuItem.Visible = false;
+ DressPic.Image = new Bitmap(DressPic.Width, DressPic.Height);
+ pictureBoxDress.Image = new Bitmap(pictureBoxDress.Width, pictureBoxDress.Height);
- checkedListBoxWear.BeginUpdate();
- checkedListBoxWear.Items.Clear();
- for (int i = 0; i < _layers.Length; ++i)
- {
- _layers[i] = 0;
- checkedListBoxWear.Items.Add($"0x{i:X2}", true);
- _layerVisible[i] = true;
- }
- checkedListBoxWear.EndUpdate();
+ checkedListBoxWear.BeginUpdate();
+ checkedListBoxWear.Items.Clear();
+ for (int i = 0; i < _layers.Length; ++i)
+ {
+ _layers[i] = 0;
+ checkedListBoxWear.Items.Add($"0x{i:X2}", true);
+ _layerVisible[i] = true;
+ }
+ checkedListBoxWear.EndUpdate();
- checkBoxHuman.Checked = true;
- checkBoxElve.Checked = false;
- checkBoxGargoyle.Checked = false;
- checkBoxfemale.Checked = false;
+ checkBoxHuman.Checked = true;
+ checkBoxElve.Checked = false;
+ checkBoxGargoyle.Checked = false;
+ checkBoxfemale.Checked = false;
- groupBoxAnimate.Visible = false;
- animateToolStripMenuItem.Visible = false;
- FacingBar.Value = (_facing + 3) & 7;
- ActionBar.Value = _action;
- toolTip1.SetToolTip(FacingBar, FacingBar.Value.ToString());
- BuildDressList();
- DrawPaperdoll();
- _loaded = true;
- Cursor.Current = Cursors.Default;
+ groupBoxAnimate.Visible = false;
+ animateToolStripMenuItem.Visible = false;
+ FacingBar.Value = (_facing + 3) & 7;
+ ActionBar.Value = _action;
+ toolTip1.SetToolTip(FacingBar, FacingBar.Value.ToString());
+ BuildDressList();
+ DrawPaperdoll();
+ _loaded = true;
+ }
}
private void OnFilePathChangeEvent()
diff --git a/UoFiddler.Controls/UserControls/FontsControl.cs b/UoFiddler.Controls/UserControls/FontsControl.cs
index a4ce16b..4c75b72 100644
--- a/UoFiddler.Controls/UserControls/FontsControl.cs
+++ b/UoFiddler.Controls/UserControls/FontsControl.cs
@@ -84,71 +84,72 @@ private void OnLoad(object sender, EventArgs e)
FontsTileView.BackColor = Options.DarkMode ? Color.LightGray : Color.White;
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["ASCIIFont"] = true;
- Options.LoadedUltimaClass["UnicodeFont"] = true;
-
- treeView.BeginUpdate();
- try
+ using (new WaitCursorScope(this))
{
- treeView.Nodes.Clear();
-
- TreeNode node = new TreeNode("ASCII")
- {
- Tag = 0
- };
- treeView.Nodes.Add(node);
+ Options.LoadedUltimaClass["ASCIIFont"] = true;
+ Options.LoadedUltimaClass["UnicodeFont"] = true;
- for (int i = 0; i < AsciiText.Fonts.Length; ++i)
+ treeView.BeginUpdate();
+ try
{
- node = new TreeNode(i.ToString())
- {
- Tag = i
- };
- treeView.Nodes[0].Nodes.Add(node);
- }
+ treeView.Nodes.Clear();
- if (LoadUnicodeFontsCheckBox.Checked)
- {
- node = new TreeNode("Unicode")
+ TreeNode node = new TreeNode("ASCII")
{
- Tag = 1
+ Tag = 0
};
treeView.Nodes.Add(node);
- for (int i = 0; i < UnicodeFonts.Fonts.Length; ++i)
+ for (int i = 0; i < AsciiText.Fonts.Length; ++i)
{
- if (UnicodeFonts.Fonts[i] == null)
- {
- continue;
- }
-
node = new TreeNode(i.ToString())
{
Tag = i
};
- treeView.Nodes[1].Nodes.Add(node);
+ treeView.Nodes[0].Nodes.Add(node);
+ }
+
+ if (LoadUnicodeFontsCheckBox.Checked)
+ {
+ node = new TreeNode("Unicode")
+ {
+ Tag = 1
+ };
+ treeView.Nodes.Add(node);
+
+ for (int i = 0; i < UnicodeFonts.Fonts.Length; ++i)
+ {
+ if (UnicodeFonts.Fonts[i] == null)
+ {
+ continue;
+ }
+
+ node = new TreeNode(i.ToString())
+ {
+ Tag = i
+ };
+ treeView.Nodes[1].Nodes.Add(node);
+ }
}
+
+ treeView.ExpandAll();
+ }
+ finally
+ {
+ treeView.EndUpdate();
}
- treeView.ExpandAll();
- }
- finally
- {
- treeView.EndUpdate();
- }
+ treeView.SelectedNode = treeView.Nodes[0].Nodes[0];
- treeView.SelectedNode = treeView.Nodes[0].Nodes[0];
+ UpdateTileView();
- UpdateTileView();
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ }
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ _loaded = true;
}
-
- _loaded = true;
- Cursor.Current = Cursors.Default;
}
private void OnFilePathChangeEvent()
diff --git a/UoFiddler.Controls/UserControls/GumpControl.cs b/UoFiddler.Controls/UserControls/GumpControl.cs
index a0a5b81..5ddec3b 100644
--- a/UoFiddler.Controls/UserControls/GumpControl.cs
+++ b/UoFiddler.Controls/UserControls/GumpControl.cs
@@ -108,23 +108,24 @@ protected override void OnLoad(EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["Gumps"] = true;
- _showFreeSlots = false;
- showFreeSlotsToolStripMenuItem.Checked = false;
+ using (new WaitCursorScope(this))
+ {
+ Options.LoadedUltimaClass["Gumps"] = true;
+ _showFreeSlots = false;
+ showFreeSlotsToolStripMenuItem.Checked = false;
- PopulateListBox(true);
- LoadGumpXml();
+ PopulateListBox(true);
+ LoadGumpXml();
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- ControlEvents.GumpChangeEvent += OnGumpChangeEvent;
- ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
- }
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ ControlEvents.GumpChangeEvent += OnGumpChangeEvent;
+ ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
+ }
- _loaded = true;
- Cursor.Current = Cursors.Default;
+ _loaded = true;
+ }
}
private void PopulateListBox(bool showOnlyValid)
@@ -559,11 +560,13 @@ private void OnClickSave(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- ProgressBarDialog barDialog = new ProgressBarDialog(Gumps.GetCount(), "Save");
- Gumps.Save(Options.OutputPath);
- barDialog.Dispose();
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ ProgressBarDialog barDialog = new ProgressBarDialog(Gumps.GetCount(), "Save");
+ Gumps.Save(Options.OutputPath);
+ barDialog.Dispose();
+ }
+
Options.ChangedUltimaClass["Gumps"] = false;
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Files saved successfully.");
}
@@ -783,31 +786,30 @@ private void ExportAllGumps(ImageFormat imageFormat)
return;
}
- Cursor.Current = Cursors.WaitCursor;
-
- for (int i = 0; i < listBox.Items.Count; ++i)
+ using (new WaitCursorScope(this))
{
- int index = int.Parse(listBox.Items[i].ToString());
- if (index < 0)
+ for (int i = 0; i < listBox.Items.Count; ++i)
{
- continue;
- }
+ int index = int.Parse(listBox.Items[i].ToString());
+ if (index < 0)
+ {
+ continue;
+ }
- string fileName = Path.Combine(dialog.SelectedPath, $"Gump {Utils.FormatExportId(index)}.{fileExtension}");
- var gump = Gumps.GetGump(index);
- if (gump is null)
- {
- continue;
- }
+ string fileName = Path.Combine(dialog.SelectedPath, $"Gump {Utils.FormatExportId(index)}.{fileExtension}");
+ var gump = Gumps.GetGump(index);
+ if (gump is null)
+ {
+ continue;
+ }
- using (Bitmap bit = new Bitmap(gump))
- {
- bit.Save(fileName, imageFormat);
+ using (Bitmap bit = new Bitmap(gump))
+ {
+ bit.Save(fileName, imageFormat);
+ }
}
}
- Cursor.Current = Cursors.Default;
-
FileSavedDialog.Show(FindForm(), dialog.SelectedPath, "All Gumps saved successfully.");
}
}
diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs
index a323cfa..18d6252 100644
--- a/UoFiddler.Controls/UserControls/ItemsControl.cs
+++ b/UoFiddler.Controls/UserControls/ItemsControl.cs
@@ -200,51 +200,52 @@ public void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["TileData"] = true;
- Options.LoadedUltimaClass["Art"] = true;
- Options.LoadedUltimaClass["Animdata"] = true;
- Options.LoadedUltimaClass["Hues"] = true;
-
- if (!IsLoaded) // only once
+ using (new WaitCursorScope(this))
{
- Plugin.PluginEvents.FireModifyItemShowContextMenuEvent(TileViewContextMenuStrip);
- }
+ Options.LoadedUltimaClass["TileData"] = true;
+ Options.LoadedUltimaClass["Art"] = true;
+ Options.LoadedUltimaClass["Animdata"] = true;
+ Options.LoadedUltimaClass["Hues"] = true;
- UpdateTileView();
+ if (!IsLoaded) // only once
+ {
+ Plugin.PluginEvents.FireModifyItemShowContextMenuEvent(TileViewContextMenuStrip);
+ }
- _showFreeSlots = false;
- showFreeSlotsToolStripMenuItem.Checked = false;
+ UpdateTileView();
- var prevSelected = SelectedGraphicId;
+ _showFreeSlots = false;
+ showFreeSlotsToolStripMenuItem.Checked = false;
- int staticLength = Art.GetMaxItemId();
- _itemList = new List(staticLength);
- for (int i = 0; i <= staticLength; ++i)
- {
- if (Art.IsValidStatic(i))
+ var prevSelected = SelectedGraphicId;
+
+ int staticLength = Art.GetMaxItemId();
+ _itemList = new List(staticLength);
+ for (int i = 0; i <= staticLength; ++i)
{
- _itemList.Add(i);
+ if (Art.IsValidStatic(i))
+ {
+ _itemList.Add(i);
+ }
}
- }
- ItemsTileView.VirtualListSize = _itemList.Count;
+ ItemsTileView.VirtualListSize = _itemList.Count;
- if (prevSelected >= 0)
- {
- SelectedGraphicId = _itemList.Contains(prevSelected) ? prevSelected : 0;
- }
+ if (prevSelected >= 0)
+ {
+ SelectedGraphicId = _itemList.Contains(prevSelected) ? prevSelected : 0;
+ }
- if (!IsLoaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- ControlEvents.ItemChangeEvent += OnItemChangeEvent;
- ControlEvents.TileDataChangeEvent += OnTileDataChangeEvent;
- ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
- }
+ if (!IsLoaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ ControlEvents.ItemChangeEvent += OnItemChangeEvent;
+ ControlEvents.TileDataChangeEvent += OnTileDataChangeEvent;
+ ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
+ }
- IsLoaded = true;
- Cursor.Current = Cursors.Default;
+ IsLoaded = true;
+ }
}
///
@@ -780,11 +781,13 @@ private void OnClickSave(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- ProgressBarDialog barDialog = new ProgressBarDialog(Art.GetIdxLength(), "Save");
- Art.Save(Options.OutputPath);
- barDialog.Dispose();
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ ProgressBarDialog barDialog = new ProgressBarDialog(Art.GetIdxLength(), "Save");
+ Art.Save(Options.OutputPath);
+ barDialog.Dispose();
+ }
+
Options.ChangedUltimaClass["Art"] = false;
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Files saved successfully.");
@@ -954,37 +957,36 @@ private void ExportAllItemImages(ImageFormat imageFormat)
return;
}
- Cursor.Current = Cursors.WaitCursor;
-
- using (new ProgressBarDialog(_itemList.Count, $"Export to {fileExtension}", false))
+ using (new WaitCursorScope(this))
{
- foreach (var artItemIndex in _itemList)
+ using (new ProgressBarDialog(_itemList.Count, $"Export to {fileExtension}", false))
{
- ControlEvents.FireProgressChangeEvent();
- Application.DoEvents();
-
- int index = artItemIndex;
- if (index < 0)
+ foreach (var artItemIndex in _itemList)
{
- continue;
- }
+ ControlEvents.FireProgressChangeEvent();
+ Application.DoEvents();
- string fileName = Path.Combine(dialog.SelectedPath, $"Item {Utils.FormatExportId(index)}.{fileExtension}");
- var artBitmap = Art.GetStatic(index);
- if (artBitmap is null)
- {
- continue;
- }
+ int index = artItemIndex;
+ if (index < 0)
+ {
+ continue;
+ }
- using (Bitmap bit = new Bitmap(artBitmap))
- {
- bit.Save(fileName, imageFormat);
+ string fileName = Path.Combine(dialog.SelectedPath, $"Item {Utils.FormatExportId(index)}.{fileExtension}");
+ var artBitmap = Art.GetStatic(index);
+ if (artBitmap is null)
+ {
+ continue;
+ }
+
+ using (Bitmap bit = new Bitmap(artBitmap))
+ {
+ bit.Save(fileName, imageFormat);
+ }
}
}
}
- Cursor.Current = Cursors.Default;
-
FileSavedDialog.Show(FindForm(), dialog.SelectedPath, "All items saved successfully.");
}
}
diff --git a/UoFiddler.Controls/UserControls/LandTilesControl.cs b/UoFiddler.Controls/UserControls/LandTilesControl.cs
index 4939ae0..0f49d91 100644
--- a/UoFiddler.Controls/UserControls/LandTilesControl.cs
+++ b/UoFiddler.Controls/UserControls/LandTilesControl.cs
@@ -172,34 +172,35 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["TileData"] = true;
- Options.LoadedUltimaClass["Art"] = true;
+ using (new WaitCursorScope(this))
+ {
+ Options.LoadedUltimaClass["TileData"] = true;
+ Options.LoadedUltimaClass["Art"] = true;
- _showFreeSlots = false;
- showFreeSlotsToolStripMenuItem.Checked = false;
+ _showFreeSlots = false;
+ showFreeSlotsToolStripMenuItem.Checked = false;
- for (int i = 0; i < _landTileMax; ++i)
- {
- if (Art.IsValidLand(i))
+ for (int i = 0; i < _landTileMax; ++i)
{
- _tileList.Add(i);
+ if (Art.IsValidLand(i))
+ {
+ _tileList.Add(i);
+ }
}
- }
- LandTilesTileView.VirtualListSize = _tileList.Count;
- UpdateTileView();
+ LandTilesTileView.VirtualListSize = _tileList.Count;
+ UpdateTileView();
- if (!IsLoaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- ControlEvents.LandTileChangeEvent += OnLandTileChangeEvent;
- ControlEvents.TileDataChangeEvent += OnTileDataChangeEvent;
- ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
- }
+ if (!IsLoaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ ControlEvents.LandTileChangeEvent += OnLandTileChangeEvent;
+ ControlEvents.TileDataChangeEvent += OnTileDataChangeEvent;
+ ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
+ }
- IsLoaded = true;
- Cursor.Current = Cursors.Default;
+ IsLoaded = true;
+ }
}
private void OnFilePathChangeEvent()
@@ -635,9 +636,10 @@ private void OnClickSave(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Art.Save(Options.OutputPath);
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ Art.Save(Options.OutputPath);
+ }
Options.ChangedUltimaClass["Art"] = false;
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Files saved successfully.");
}
@@ -796,30 +798,29 @@ private void ExportAllLandTiles(ImageFormat imageFormat)
return;
}
- Cursor.Current = Cursors.WaitCursor;
-
- foreach (var index in _tileList)
+ using (new WaitCursorScope(this))
{
- if (!Art.IsValidLand(index))
+ foreach (var index in _tileList)
{
- continue;
- }
+ if (!Art.IsValidLand(index))
+ {
+ continue;
+ }
- string fileName = Path.Combine(dialog.SelectedPath, $"Landtile {Utils.FormatExportId(index)}.{fileExtension}");
- var landTile = Art.GetLand(index);
- if (landTile is null)
- {
- continue;
- }
+ string fileName = Path.Combine(dialog.SelectedPath, $"Landtile {Utils.FormatExportId(index)}.{fileExtension}");
+ var landTile = Art.GetLand(index);
+ if (landTile is null)
+ {
+ continue;
+ }
- using (Bitmap bit = new Bitmap(landTile))
- {
- bit.Save(fileName, imageFormat);
+ using (Bitmap bit = new Bitmap(landTile))
+ {
+ bit.Save(fileName, imageFormat);
+ }
}
}
- Cursor.Current = Cursors.Default;
-
FileSavedDialog.Show(FindForm(), dialog.SelectedPath, "All land tiles saved successfully.");
}
}
diff --git a/UoFiddler.Controls/UserControls/LightControl.cs b/UoFiddler.Controls/UserControls/LightControl.cs
index 6f3206a..3045f73 100644
--- a/UoFiddler.Controls/UserControls/LightControl.cs
+++ b/UoFiddler.Controls/UserControls/LightControl.cs
@@ -56,41 +56,42 @@ private void OnLoad(object sender, EventArgs e)
pictureBoxPreview.BackColor = Options.DarkMode ? Color.LightGray : Color.White;
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["Light"] = true;
-
- listViewLights.BeginUpdate();
- try
+ using (new WaitCursorScope(this))
{
- listViewLights.Items.Clear();
- for (int i = 0; i < Ultima.Light.GetCount(); ++i)
+ Options.LoadedUltimaClass["Light"] = true;
+
+ listViewLights.BeginUpdate();
+ try
{
- if (!Ultima.Light.TestLight(i))
+ listViewLights.Items.Clear();
+ for (int i = 0; i < Ultima.Light.GetCount(); ++i)
{
- continue;
+ if (!Ultima.Light.TestLight(i))
+ {
+ continue;
+ }
+
+ listViewLights.Items.Add(new ListViewItem(i.ToString()) { Tag = i });
}
+ }
+ finally
+ {
+ listViewLights.EndUpdate();
+ }
- listViewLights.Items.Add(new ListViewItem(i.ToString()) { Tag = i });
+ if (listViewLights.Items.Count > 0)
+ {
+ listViewLights.Items[0].Selected = true;
+ listViewLights.Items[0].EnsureVisible();
}
- }
- finally
- {
- listViewLights.EndUpdate();
- }
- if (listViewLights.Items.Count > 0)
- {
- listViewLights.Items[0].Selected = true;
- listViewLights.Items[0].EnsureVisible();
- }
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ }
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ _loaded = true;
}
-
- _loaded = true;
- Cursor.Current = Cursors.Default;
}
private void OnFilePathChangeEvent()
diff --git a/UoFiddler.Controls/UserControls/MapControl.cs b/UoFiddler.Controls/UserControls/MapControl.cs
index 234e3eb..eaaefae 100644
--- a/UoFiddler.Controls/UserControls/MapControl.cs
+++ b/UoFiddler.Controls/UserControls/MapControl.cs
@@ -141,24 +141,25 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- LoadMapOverlays();
- Options.LoadedUltimaClass["Map"] = true;
- Options.LoadedUltimaClass["RadarColor"] = true;
-
- CurrentMap = Map.Felucca;
- feluccaToolStripMenuItem.Checked = true;
- trammelToolStripMenuItem.Checked = false;
- ilshenarToolStripMenuItem.Checked = false;
- malasToolStripMenuItem.Checked = false;
- tokunoToolStripMenuItem.Checked = false;
- PreloadMap.Visible = true;
- ChangeMapNames();
- ZoomLabel.Text = $"Zoom: {Zoom}";
- SetScrollBarValues();
- Refresh();
- pictureBox.Invalidate();
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ LoadMapOverlays();
+ Options.LoadedUltimaClass["Map"] = true;
+ Options.LoadedUltimaClass["RadarColor"] = true;
+
+ CurrentMap = Map.Felucca;
+ feluccaToolStripMenuItem.Checked = true;
+ trammelToolStripMenuItem.Checked = false;
+ ilshenarToolStripMenuItem.Checked = false;
+ malasToolStripMenuItem.Checked = false;
+ tokunoToolStripMenuItem.Checked = false;
+ PreloadMap.Visible = true;
+ ChangeMapNames();
+ ZoomLabel.Text = $"Zoom: {Zoom}";
+ SetScrollBarValues();
+ Refresh();
+ pictureBox.Invalidate();
+ }
if (!_loaded)
{
@@ -1005,12 +1006,10 @@ private void ExtractMapPng(object sender, EventArgs e)
private void ExtractMapImage(ImageFormat imageFormat)
{
- Cursor.Current = Cursors.WaitCursor;
-
string fileExtension = Utils.GetFileExtensionFor(imageFormat);
string fileName = Path.Combine(Options.OutputPath, $"{Options.MapNames[_currentMapId]}.{fileExtension}");
- try
+ using (new WaitCursorScope(this))
{
Bitmap extract;
@@ -1041,10 +1040,6 @@ private void ExtractMapImage(ImageFormat imageFormat)
}
extract.Save(fileName, imageFormat);
}
- finally
- {
- Cursor.Current = Cursors.Default;
- }
FileSavedDialog.Show(FindForm(), fileName, "Map saved successfully.");
}
@@ -1315,20 +1310,22 @@ private void OnChangeView(object sender, EventArgs e)
private void OnClickDefragStatics(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- Map.DefragStatics(Options.OutputPath,
- CurrentMap, CurrentMap.Width, CurrentMap.Height, false);
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ Map.DefragStatics(Options.OutputPath,
+ CurrentMap, CurrentMap.Width, CurrentMap.Height, false);
+ }
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Statics saved successfully.");
}
private void OnClickDefragRemoveStatics(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- Map.DefragStatics(Options.OutputPath,
- CurrentMap, CurrentMap.Width, CurrentMap.Height, true);
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ Map.DefragStatics(Options.OutputPath,
+ CurrentMap, CurrentMap.Width, CurrentMap.Height, true);
+ }
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Statics saved successfully.");
}
@@ -1350,26 +1347,29 @@ private void OnResizeMap(object sender, EventArgs e)
private void OnClickRewriteMap(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- Map.RewriteMap(Options.OutputPath,
- _currentMapId, CurrentMap.Width, CurrentMap.Height);
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ Map.RewriteMap(Options.OutputPath,
+ _currentMapId, CurrentMap.Width, CurrentMap.Height);
+ }
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Files saved successfully.");
}
private void OnClickReportInvisStatics(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- CurrentMap.ReportInvisibleStatics(Options.OutputPath);
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ CurrentMap.ReportInvisibleStatics(Options.OutputPath);
+ }
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Report saved successfully.");
}
private void OnClickReportInvalidMapIDs(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- CurrentMap.ReportInvalidMapIDs(Options.OutputPath);
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ CurrentMap.ReportInvalidMapIDs(Options.OutputPath);
+ }
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Report saved successfully.");
}
diff --git a/UoFiddler.Controls/UserControls/MultiMapControl.cs b/UoFiddler.Controls/UserControls/MultiMapControl.cs
index 02a377e..cbcf3b2 100644
--- a/UoFiddler.Controls/UserControls/MultiMapControl.cs
+++ b/UoFiddler.Controls/UserControls/MultiMapControl.cs
@@ -272,28 +272,28 @@ private void ShowImage(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
-
- multiMapToolStripMenuItem.Checked =
- facet00ToolStripMenuItem.Checked =
- facet01ToolStripMenuItem.Checked =
- facet02ToolStripMenuItem.Checked =
- facet03ToolStripMenuItem.Checked =
- facet04ToolStripMenuItem.Checked =
- facet05ToolStripMenuItem.Checked = false;
+ using (new WaitCursorScope(this))
+ {
+ multiMapToolStripMenuItem.Checked =
+ facet00ToolStripMenuItem.Checked =
+ facet01ToolStripMenuItem.Checked =
+ facet02ToolStripMenuItem.Checked =
+ facet03ToolStripMenuItem.Checked =
+ facet04ToolStripMenuItem.Checked =
+ facet05ToolStripMenuItem.Checked = false;
- strip.Checked = true;
+ strip.Checked = true;
- pictureBox.Image = (int)strip.Tag == -1
- ? Ultima.MultiMap.GetMultiMap()
- : Ultima.MultiMap.GetFacetImage((int)strip.Tag);
+ pictureBox.Image = (int)strip.Tag == -1
+ ? Ultima.MultiMap.GetMultiMap()
+ : Ultima.MultiMap.GetFacetImage((int)strip.Tag);
- if (pictureBox.Image != null)
- {
- DisplayScrollBars();
- SetScrollBarValues();
+ if (pictureBox.Image != null)
+ {
+ DisplayScrollBars();
+ SetScrollBarValues();
+ }
}
- Cursor.Current = Cursors.Default;
}
private void OnClickGenerateRLE(object sender, EventArgs e)
@@ -308,37 +308,32 @@ private void OnClickGenerateRLE(object sender, EventArgs e)
try
{
- Cursor.Current = Cursors.WaitCursor;
-
- Bitmap image = new Bitmap(dialog.FileName);
-
- if (image.Height != 2048 || image.Width != 2560)
+ using (new WaitCursorScope(this))
{
- MessageBox.Show("Invalid image height or width", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
- return;
+ Bitmap image = new Bitmap(dialog.FileName);
+
+ if (image.Height != 2048 || image.Width != 2560)
+ {
+ MessageBox.Show("Invalid image height or width", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
+ return;
+ }
+
+ string path = Options.OutputPath;
+ string fileName = Path.Combine(path, "MultiMap.rle");
+ using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Write))
+ {
+ BinaryWriter bin = new BinaryWriter(fs, Encoding.Unicode);
+ Ultima.MultiMap.SaveMultiMap(image, bin);
+ }
+
+ FileSavedDialog.Show(FindForm(), fileName, "MultiMap saved successfully.");
}
-
- string path = Options.OutputPath;
- string fileName = Path.Combine(path, "MultiMap.rle");
- using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Write))
- {
- BinaryWriter bin = new BinaryWriter(fs, Encoding.Unicode);
- Ultima.MultiMap.SaveMultiMap(image, bin);
- }
-
- Cursor.Current = Cursors.Default;
-
- FileSavedDialog.Show(FindForm(), fileName, "MultiMap saved successfully.");
}
catch (FileNotFoundException)
{
MessageBox.Show("No image found", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error,
MessageBoxDefaultButton.Button1);
}
- finally
- {
- Cursor.Current = Cursors.Default;
- }
}
}
@@ -354,26 +349,21 @@ private void OnClickGenerateFacetFromImage(object sender, EventArgs e)
try
{
- Cursor.Current = Cursors.WaitCursor;
-
- Bitmap image = new Bitmap(dialog.FileName);
- string path = Options.OutputPath;
- string fileName = Path.Combine(path, "facet.mul");
- Ultima.MultiMap.SaveFacetImage(fileName, image);
-
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ Bitmap image = new Bitmap(dialog.FileName);
+ string path = Options.OutputPath;
+ string fileName = Path.Combine(path, "facet.mul");
+ Ultima.MultiMap.SaveFacetImage(fileName, image);
- FileSavedDialog.Show(FindForm(), fileName, "Facet saved successfully.");
+ FileSavedDialog.Show(FindForm(), fileName, "Facet saved successfully.");
+ }
}
catch (FileNotFoundException)
{
MessageBox.Show("No image found", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error,
MessageBoxDefaultButton.Button1);
}
- finally
- {
- Cursor.Current = Cursors.Default;
- }
}
}
diff --git a/UoFiddler.Controls/UserControls/MultisControl.cs b/UoFiddler.Controls/UserControls/MultisControl.cs
index e57760d..3a26d85 100644
--- a/UoFiddler.Controls/UserControls/MultisControl.cs
+++ b/UoFiddler.Controls/UserControls/MultisControl.cs
@@ -237,32 +237,31 @@ private void OnLoad(object sender, EventArgs e)
ApplyDarkModeIfNeeded();
- Cursor.Current = Cursors.WaitCursor;
-
- Options.LoadedUltimaClass["TileData"] = true;
- Options.LoadedUltimaClass["Art"] = true;
- Options.LoadedUltimaClass["Multis"] = true;
- Options.LoadedUltimaClass["Hues"] = true;
-
- RebuildMulIds(includeEmpty: false);
-
- if (_mulIds.Length > 0)
+ using (new WaitCursorScope(this))
{
- SelectMulRow(0);
- }
+ Options.LoadedUltimaClass["TileData"] = true;
+ Options.LoadedUltimaClass["Art"] = true;
+ Options.LoadedUltimaClass["Multis"] = true;
+ Options.LoadedUltimaClass["Hues"] = true;
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- ControlEvents.MultiChangeEvent += OnMultiChangeEvent;
- ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
- }
+ RebuildMulIds(includeEmpty: false);
- _loaded = true;
+ if (_mulIds.Length > 0)
+ {
+ SelectMulRow(0);
+ }
+
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ ControlEvents.MultiChangeEvent += OnMultiChangeEvent;
+ ControlEvents.PreviewBackgroundColorChangeEvent += OnPreviewBackgroundColorChanged;
+ }
- LoadUopTree();
+ _loaded = true;
- Cursor.Current = Cursors.Default;
+ LoadUopTree();
+ }
}
private void OnFilePathChangeEvent()
diff --git a/UoFiddler.Controls/UserControls/SkillGroupControl.cs b/UoFiddler.Controls/UserControls/SkillGroupControl.cs
index 2986ad2..aa593be 100644
--- a/UoFiddler.Controls/UserControls/SkillGroupControl.cs
+++ b/UoFiddler.Controls/UserControls/SkillGroupControl.cs
@@ -50,61 +50,62 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["SkillGrp"] = true;
-
- treeView1.BeginUpdate();
- treeView1.Nodes.Clear();
- List cache = new List();
-
- foreach (SkillGroup group in SkillGroups.List)
+ using (new WaitCursorScope(this))
{
- TreeNode groupNode = new TreeNode
- {
- Text = group.Name
- };
+ Options.LoadedUltimaClass["SkillGrp"] = true;
- if (string.Equals("Misc", group.Name))
- {
- groupNode.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
- }
+ treeView1.BeginUpdate();
+ treeView1.Nodes.Clear();
+ List cache = new List();
- for (int i = 0; i < SkillGroups.SkillList.Count; ++i)
+ foreach (SkillGroup group in SkillGroups.List)
{
- if (SkillGroups.SkillList[i] != cache.Count)
+ TreeNode groupNode = new TreeNode
{
- continue;
- }
-
- var skillInfo = Skills.GetSkill(i);
+ Text = group.Name
+ };
- if (skillInfo == null)
+ if (string.Equals("Misc", group.Name))
{
- continue;
+ groupNode.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
}
- TreeNode skillNode = new TreeNode
+ for (int i = 0; i < SkillGroups.SkillList.Count; ++i)
{
- Text = skillInfo.Name,
- Tag = i
- };
+ if (SkillGroups.SkillList[i] != cache.Count)
+ {
+ continue;
+ }
+
+ var skillInfo = Skills.GetSkill(i);
- groupNode.Nodes.Add(skillNode);
+ if (skillInfo == null)
+ {
+ continue;
+ }
+
+ TreeNode skillNode = new TreeNode
+ {
+ Text = skillInfo.Name,
+ Tag = i
+ };
+
+ groupNode.Nodes.Add(skillNode);
+ }
+
+ cache.Add(groupNode);
}
- cache.Add(groupNode);
- }
+ treeView1.Nodes.AddRange(cache.ToArray());
+ treeView1.EndUpdate();
- treeView1.Nodes.AddRange(cache.ToArray());
- treeView1.EndUpdate();
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ }
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ _loaded = true;
}
-
- _loaded = true;
- Cursor.Current = Cursors.Default;
}
private void OnFilePathChangeEvent()
diff --git a/UoFiddler.Controls/UserControls/SkillsControl.cs b/UoFiddler.Controls/UserControls/SkillsControl.cs
index 5b188c7..697803f 100644
--- a/UoFiddler.Controls/UserControls/SkillsControl.cs
+++ b/UoFiddler.Controls/UserControls/SkillsControl.cs
@@ -48,36 +48,37 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["Skills"] = true;
+ using (new WaitCursorScope(this))
+ {
+ Options.LoadedUltimaClass["Skills"] = true;
- _source.DataSource = Skills.SkillEntries;
- dataGridView1.DataSource = _source;
- dataGridView1.Invalidate();
+ _source.DataSource = Skills.SkillEntries;
+ dataGridView1.DataSource = _source;
+ dataGridView1.Invalidate();
- if (dataGridView1.Columns.Count > 0)
- {
- dataGridView1.Columns[0].MinimumWidth = 40;
- dataGridView1.Columns[0].FillWeight = 10.82822F;
- dataGridView1.Columns[0].ReadOnly = true;
- dataGridView1.Columns[0].HeaderText = "ID";
- dataGridView1.Columns[1].MinimumWidth = 60;
- dataGridView1.Columns[1].FillWeight = 10.80126F;
- dataGridView1.Columns[1].ReadOnly = false;
- dataGridView1.Columns[1].HeaderText = "is Action";
- dataGridView1.Columns[2].FillWeight = 54.86799F;
- dataGridView1.Columns[2].ReadOnly = false;
- dataGridView1.Columns[3].Visible = false; // extraFlag
- }
+ if (dataGridView1.Columns.Count > 0)
+ {
+ dataGridView1.Columns[0].MinimumWidth = 40;
+ dataGridView1.Columns[0].FillWeight = 10.82822F;
+ dataGridView1.Columns[0].ReadOnly = true;
+ dataGridView1.Columns[0].HeaderText = "ID";
+ dataGridView1.Columns[1].MinimumWidth = 60;
+ dataGridView1.Columns[1].FillWeight = 10.80126F;
+ dataGridView1.Columns[1].ReadOnly = false;
+ dataGridView1.Columns[1].HeaderText = "is Action";
+ dataGridView1.Columns[2].FillWeight = 54.86799F;
+ dataGridView1.Columns[2].ReadOnly = false;
+ dataGridView1.Columns[3].Visible = false; // extraFlag
+ }
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- _source.ListChanged += Source_ListChanged;
- }
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ _source.ListChanged += Source_ListChanged;
+ }
- _loaded = true;
- Cursor.Current = Cursors.Default;
+ _loaded = true;
+ }
}
private static void Source_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e)
diff --git a/UoFiddler.Controls/UserControls/SoundsControl.cs b/UoFiddler.Controls/UserControls/SoundsControl.cs
index dc78698..4302bc0 100644
--- a/UoFiddler.Controls/UserControls/SoundsControl.cs
+++ b/UoFiddler.Controls/UserControls/SoundsControl.cs
@@ -77,75 +77,75 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["Sound"] = true;
-
- int? oldItem = null;
-
- if (listView.SelectedItems.Count > 0)
+ using (new WaitCursorScope(this))
{
- oldItem = (int)listView.SelectedItems[0].Tag;
- }
+ Options.LoadedUltimaClass["Sound"] = true;
- listView.BeginUpdate();
- try
- {
- listView.Items.Clear();
+ int? oldItem = null;
- _soundIdOffset = GetSoundIdOffset();
+ if (listView.SelectedItems.Count > 0)
+ {
+ oldItem = (int)listView.SelectedItems[0].Tag;
+ }
- var cache = new List();
- for (int i = 0; i < _soundsLength; ++i)
+ listView.BeginUpdate();
+ try
{
- if (Sounds.IsValidSound(i, out string name, out bool translated))
- {
- var item = new ListViewItem($"0x{i + _soundIdOffset:X3} {name}") { Tag = i };
+ listView.Items.Clear();
- if (translated)
- {
- item.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
- item.Font = new Font(Font, FontStyle.Underline);
- }
+ _soundIdOffset = GetSoundIdOffset();
- cache.Add(item);
- }
- else if (showFreeSlotsToolStripMenuItem.Checked)
+ var cache = new List();
+ for (int i = 0; i < _soundsLength; ++i)
{
- cache.Add(new ListViewItem($"0x{i:X3} ")
+ if (Sounds.IsValidSound(i, out string name, out bool translated))
{
- Tag = i,
- ForeColor = Options.DarkMode ? Color.OrangeRed : Color.Red
- });
- }
- }
+ var item = new ListViewItem($"0x{i + _soundIdOffset:X3} {name}") { Tag = i };
- listView.Items.AddRange(cache.ToArray());
- }
- finally
- {
- listView.EndUpdate();
- }
+ if (translated)
+ {
+ item.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
+ item.Font = new Font(Font, FontStyle.Underline);
+ }
- if (listView.Items.Count > 0)
- {
- listView.Items[0].Selected = true;
- listView.Items[0].EnsureVisible();
- }
+ cache.Add(item);
+ }
+ else if (showFreeSlotsToolStripMenuItem.Checked)
+ {
+ cache.Add(new ListViewItem($"0x{i:X3} ")
+ {
+ Tag = i,
+ ForeColor = Options.DarkMode ? Color.OrangeRed : Color.Red
+ });
+ }
+ }
- _sp = new System.Media.SoundPlayer();
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- }
+ listView.Items.AddRange(cache.ToArray());
+ }
+ finally
+ {
+ listView.EndUpdate();
+ }
- _loaded = true;
- _playing = false;
+ if (listView.Items.Count > 0)
+ {
+ listView.Items[0].Selected = true;
+ listView.Items[0].EnsureVisible();
+ }
- Cursor.Current = Cursors.Default;
+ _sp = new System.Media.SoundPlayer();
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ }
- if (oldItem != null)
- {
- SearchId(oldItem.Value);
+ _loaded = true;
+ _playing = false;
+
+ if (oldItem != null)
+ {
+ SearchId(oldItem.Value);
+ }
}
}
@@ -434,11 +434,12 @@ private void OnClickExtract(object sender, EventArgs e)
private void OnClickSave(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- string path = Options.OutputPath;
- Sounds.Save(path);
- Cursor.Current = Cursors.Default;
- Options.ChangedUltimaClass["Sound"] = false;
+ using (new WaitCursorScope(this))
+ {
+ string path = Options.OutputPath;
+ Sounds.Save(path);
+ Options.ChangedUltimaClass["Sound"] = false;
+ }
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Files saved successfully.");
}
diff --git a/UoFiddler.Controls/UserControls/TexturesControl.cs b/UoFiddler.Controls/UserControls/TexturesControl.cs
index 50e0f1f..19d9b08 100644
--- a/UoFiddler.Controls/UserControls/TexturesControl.cs
+++ b/UoFiddler.Controls/UserControls/TexturesControl.cs
@@ -114,30 +114,30 @@ private void OnLoad(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["Texture"] = true;
-
- for (int i = 0; i < Textures.GetIdxLength(); ++i)
+ using (new WaitCursorScope(this))
{
- if (Textures.TestTexture(i))
+ Options.LoadedUltimaClass["Texture"] = true;
+
+ for (int i = 0; i < Textures.GetIdxLength(); ++i)
{
- _textureList.Add(i);
+ if (Textures.TestTexture(i))
+ {
+ _textureList.Add(i);
+ }
}
- }
- TextureTileView.VirtualListSize = _textureList.Count;
+ TextureTileView.VirtualListSize = _textureList.Count;
- UpdateTileView();
+ UpdateTileView();
- if (!_loaded)
- {
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- ControlEvents.TextureChangeEvent += OnTextureChangeEvent;
- }
-
- _loaded = true;
+ if (!_loaded)
+ {
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ ControlEvents.TextureChangeEvent += OnTextureChangeEvent;
+ }
- Cursor.Current = Cursors.Default;
+ _loaded = true;
+ }
}
private void OnTextureChangeEvent(object sender, int index)
@@ -515,9 +515,11 @@ private void OnKeyDownInsert(object sender, KeyEventArgs e)
private void OnClickSave(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- Textures.Save(Options.OutputPath);
- Cursor.Current = Cursors.Default;
+ using (new WaitCursorScope(this))
+ {
+ Textures.Save(Options.OutputPath);
+ }
+
Options.ChangedUltimaClass["Texture"] = false;
FileSavedDialog.Show(FindForm(), Options.OutputPath, "Files saved successfully.");
@@ -728,30 +730,29 @@ private void ExportAllTextures(ImageFormat imageFormat)
return;
}
- Cursor.Current = Cursors.WaitCursor;
-
- foreach (var index in _textureList)
+ using (new WaitCursorScope(this))
{
- if (!Textures.TestTexture(index))
+ foreach (var index in _textureList)
{
- continue;
- }
+ if (!Textures.TestTexture(index))
+ {
+ continue;
+ }
- string fileName = Path.Combine(dialog.SelectedPath, $"Texture {Utils.FormatExportId(index)}.{fileExtension}");
- var texture = Textures.GetTexture(index);
- if (texture is null)
- {
- continue;
- }
+ string fileName = Path.Combine(dialog.SelectedPath, $"Texture {Utils.FormatExportId(index)}.{fileExtension}");
+ var texture = Textures.GetTexture(index);
+ if (texture is null)
+ {
+ continue;
+ }
- using (Bitmap bit = new Bitmap(texture))
- {
- bit.Save(fileName, imageFormat);
+ using (Bitmap bit = new Bitmap(texture))
+ {
+ bit.Save(fileName, imageFormat);
+ }
}
}
- Cursor.Current = Cursors.Default;
-
MessageBox.Show($"All textures saved to {dialog.SelectedPath}", "Saved", MessageBoxButtons.OK,
MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
index 33c8275..bf3c953 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
@@ -369,23 +369,24 @@ private void OnChangeShowDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- var allIds = Animdata.AnimData.Keys
- .Union(SecondAnimdata.GetKeys())
- .OrderBy(k => k);
-
- _displayIndices.Clear();
- foreach (int id in allIds)
+ using (new WaitCursorScope(this))
{
- if (!checkBoxShowDiff.Checked || !Compare(id))
+ var allIds = Animdata.AnimData.Keys
+ .Union(SecondAnimdata.GetKeys())
+ .OrderBy(k => k);
+
+ _displayIndices.Clear();
+ foreach (int id in allIds)
{
- _displayIndices.Add(id);
+ if (!checkBoxShowDiff.Checked || !Compare(id))
+ {
+ _displayIndices.Add(id);
+ }
}
- }
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- Cursor.Current = Cursors.Default;
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
+ }
}
private bool Compare(int id)
@@ -436,47 +437,48 @@ private void OnClickCopySelected(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int lastId = -1;
- bool changed = false;
-
- foreach (int focusIdx in targets)
+ using (new WaitCursorScope(this))
{
- if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ int lastId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- continue;
- }
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
- int id = _displayIndices[focusIdx];
- CopyEntry(id);
- lastId = id;
- changed = true;
- }
+ int id = _displayIndices[focusIdx];
+ CopyEntry(id);
+ lastId = id;
+ changed = true;
+ }
- if (checkBoxShowDiff.Checked && changed)
- {
- foreach (int idx in targets.OrderByDescending(x => x))
+ if (checkBoxShowDiff.Checked && changed)
{
- if (idx >= 0 && idx < _displayIndices.Count)
+ foreach (int idx in targets.OrderByDescending(x => x))
{
- _displayIndices.RemoveAt(idx);
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
}
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
+ }
+ else
+ {
+ tileViewSec.SelectedIndices.Clear();
}
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- }
- else
- {
- tileViewSec.SelectedIndices.Clear();
- }
- tileViewOrg.Invalidate();
- tileViewSec.Invalidate();
- if (lastId >= 0)
- {
- UpdateDetailPanel(lastId);
+ tileViewOrg.Invalidate();
+ tileViewSec.Invalidate();
+ if (lastId >= 0)
+ {
+ UpdateDetailPanel(lastId);
+ }
}
- Cursor.Current = Cursors.Default;
}
private void OnClickCopyAllDiff(object sender, EventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.cs
index 2cc23d7..d2b74fb 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareCliLocControl.cs
@@ -259,7 +259,10 @@ private void OnClickDirFile2(object sender, EventArgs e)
private void OnClickShowOnlyDiff(object sender, EventArgs e)
{
_showOnlyDiff = !_showOnlyDiff;
- BuildList();
+ using (new WaitCursorScope(this))
+ {
+ BuildList();
+ }
}
private void OnClickFindNextDiff(object sender, EventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
index 7236bbe..205b569 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
@@ -40,46 +40,47 @@ public CompareGumpControl()
private void OnLoad(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- Options.LoadedUltimaClass["Gumps"] = true;
+ using (new WaitCursorScope(this))
+ {
+ Options.LoadedUltimaClass["Gumps"] = true;
- ConfigureTileView(tileView1);
- ConfigureTileView(tileView2);
+ ConfigureTileView(tileView1);
+ ConfigureTileView(tileView2);
- _displayIndices.Clear();
- for (int i = 0; i < 0x10000; i++)
- {
- _displayIndices.Add(i);
- }
+ _displayIndices.Clear();
+ for (int i = 0; i < 0x10000; i++)
+ {
+ _displayIndices.Add(i);
+ }
- tileView1.VirtualListSize = _displayIndices.Count;
- tileView2.VirtualListSize = 0;
+ tileView1.VirtualListSize = _displayIndices.Count;
+ tileView2.VirtualListSize = 0;
- if (_displayIndices.Count > 0)
- {
- tileView1.FocusIndex = 0;
- }
+ if (_displayIndices.Count > 0)
+ {
+ tileView1.FocusIndex = 0;
+ }
- if (comboBoxFileMode.SelectedIndex < 0)
- {
- comboBoxFileMode.SelectedIndex = 0;
- }
+ if (comboBoxFileMode.SelectedIndex < 0)
+ {
+ comboBoxFileMode.SelectedIndex = 0;
+ }
- if (!_loaded)
- {
- tileView2.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
- contextMenuStrip1.Opening += (s, ev) =>
+ if (!_loaded)
{
- int count = tileView2.SelectedIndices.Count;
- copyGump2To1ToolStripMenuItem.Text = tileView2.ShowCheckBoxes && count > 1
- ? $"Copy {count} Gumps to left"
- : "Copy Gump to left";
- };
- ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
- }
+ tileView2.SelectedIndices.CollectionChanged += OnSecSelectedIndicesChanged;
+ contextMenuStrip1.Opening += (s, ev) =>
+ {
+ int count = tileView2.SelectedIndices.Count;
+ copyGump2To1ToolStripMenuItem.Text = tileView2.ShowCheckBoxes && count > 1
+ ? $"Copy {count} Gumps to left"
+ : "Copy Gump to left";
+ };
+ ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
+ }
- _loaded = true;
- Cursor.Current = Cursors.Default;
+ _loaded = true;
+ }
}
// TileViewControl exposes TileSize/Margin/Padding/Border with DesignerSerializationVisibility.Hidden,
@@ -328,8 +329,11 @@ private void Load_Click(object sender, EventArgs e)
return;
}
- SecondGump.SetFileIndex(resolvedIdx, resolvedMul, resolvedUop);
- LoadSecond();
+ using (new WaitCursorScope(this))
+ {
+ SecondGump.SetFileIndex(resolvedIdx, resolvedMul, resolvedUop);
+ LoadSecond();
+ }
}
private void LoadSecond()
@@ -385,29 +389,30 @@ private void ShowDiff_OnClick(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- _displayIndices.Clear();
- if (checkBox1.Checked)
+ using (new WaitCursorScope(this))
{
- for (int i = 0; i < 0x10000; i++)
+ _displayIndices.Clear();
+ if (checkBox1.Checked)
{
- if (!Compare(i))
+ for (int i = 0; i < 0x10000; i++)
{
- _displayIndices.Add(i);
+ if (!Compare(i))
+ {
+ _displayIndices.Add(i);
+ }
}
}
- }
- else
- {
- for (int i = 0; i < 0x10000; i++)
+ else
{
- _displayIndices.Add(i);
+ for (int i = 0; i < 0x10000; i++)
+ {
+ _displayIndices.Add(i);
+ }
}
- }
- tileView1.VirtualListSize = _displayIndices.Count;
- tileView2.VirtualListSize = _displayIndices.Count;
- Cursor.Current = Cursors.Default;
+ tileView1.VirtualListSize = _displayIndices.Count;
+ tileView2.VirtualListSize = _displayIndices.Count;
+ }
}
private void Export_Bmp(object sender, EventArgs e)
@@ -502,60 +507,61 @@ private void OnClickCopy(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int lastCopiedId = -1;
- bool changed = false;
-
- foreach (int focusIdx in targets)
+ using (new WaitCursorScope(this))
{
- if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ int lastCopiedId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- continue;
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
+
+ int i = _displayIndices[focusIdx];
+ if (!SecondGump.IsValidIndex(i))
+ {
+ continue;
+ }
+
+ Bitmap copy = new Bitmap(SecondGump.GetGump(i));
+ Gumps.ReplaceGump(i, copy);
+ ControlEvents.FireGumpChangeEvent(this, i);
+ _compare[i] = true;
+ lastCopiedId = i;
+ changed = true;
}
- int i = _displayIndices[focusIdx];
- if (!SecondGump.IsValidIndex(i))
+ if (changed)
{
- continue;
+ Options.ChangedUltimaClass["Gumps"] = true;
}
- Bitmap copy = new Bitmap(SecondGump.GetGump(i));
- Gumps.ReplaceGump(i, copy);
- ControlEvents.FireGumpChangeEvent(this, i);
- _compare[i] = true;
- lastCopiedId = i;
- changed = true;
- }
-
- if (changed)
- {
- Options.ChangedUltimaClass["Gumps"] = true;
- }
-
- if (checkBox1.Checked && changed)
- {
- foreach (int idx in targets.OrderByDescending(x => x))
+ if (checkBox1.Checked && changed)
{
- if (idx >= 0 && idx < _displayIndices.Count)
+ foreach (int idx in targets.OrderByDescending(x => x))
{
- _displayIndices.RemoveAt(idx);
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
}
+ tileView1.VirtualListSize = _displayIndices.Count;
+ tileView2.VirtualListSize = _displayIndices.Count;
+ }
+ else
+ {
+ tileView2.SelectedIndices.Clear();
}
- tileView1.VirtualListSize = _displayIndices.Count;
- tileView2.VirtualListSize = _displayIndices.Count;
- }
- else
- {
- tileView2.SelectedIndices.Clear();
- }
- tileView1.Invalidate();
- tileView2.Invalidate();
- if (lastCopiedId >= 0)
- {
- UpdatePictureBox(pictureBox1, lastCopiedId, isSecondary: false);
+ tileView1.Invalidate();
+ tileView2.Invalidate();
+ if (lastCopiedId >= 0)
+ {
+ UpdatePictureBox(pictureBox1, lastCopiedId, isSecondary: false);
+ }
}
- Cursor.Current = Cursors.Default;
}
}
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
index 2d59371..815db4d 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
@@ -362,30 +362,31 @@ private void OnChangeShowDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int maxId = Math.Max(Art.GetMaxItemId(), SecondArt.GetMaxItemId());
- _displayIndices.Clear();
- if (checkBox1.Checked)
+ using (new WaitCursorScope(this))
{
- for (int i = 0; i < maxId; i++)
+ int maxId = Math.Max(Art.GetMaxItemId(), SecondArt.GetMaxItemId());
+ _displayIndices.Clear();
+ if (checkBox1.Checked)
{
- if (!Compare(i))
+ for (int i = 0; i < maxId; i++)
{
- _displayIndices.Add(i);
+ if (!Compare(i))
+ {
+ _displayIndices.Add(i);
+ }
}
}
- }
- else
- {
- for (int i = 0; i < maxId; i++)
+ else
{
- _displayIndices.Add(i);
+ for (int i = 0; i < maxId; i++)
+ {
+ _displayIndices.Add(i);
+ }
}
- }
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- Cursor.Current = Cursors.Default;
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
+ }
}
private void ExportAsBmp(object sender, EventArgs e)
@@ -473,61 +474,62 @@ private void OnClickCopy(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int maxId = Art.GetMaxItemId() + 1;
- int lastCopiedId = -1;
- bool changed = false;
-
- foreach (int focusIdx in targets)
+ using (new WaitCursorScope(this))
{
- if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ int maxId = Art.GetMaxItemId() + 1;
+ int lastCopiedId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- continue;
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
+
+ int i = _displayIndices[focusIdx];
+ if (!SecondArt.IsValidStatic(i) || i >= maxId)
+ {
+ continue;
+ }
+
+ Bitmap copy = new Bitmap(SecondArt.GetStatic(i));
+ Art.ReplaceStatic(i, copy);
+ ControlEvents.FireItemChangeEvent(this, i);
+ _compare[i] = true;
+ lastCopiedId = i;
+ changed = true;
}
- int i = _displayIndices[focusIdx];
- if (!SecondArt.IsValidStatic(i) || i >= maxId)
+ if (changed)
{
- continue;
+ Options.ChangedUltimaClass["Art"] = true;
}
- Bitmap copy = new Bitmap(SecondArt.GetStatic(i));
- Art.ReplaceStatic(i, copy);
- ControlEvents.FireItemChangeEvent(this, i);
- _compare[i] = true;
- lastCopiedId = i;
- changed = true;
- }
-
- if (changed)
- {
- Options.ChangedUltimaClass["Art"] = true;
- }
-
- if (checkBox1.Checked && changed)
- {
- foreach (int idx in targets.OrderByDescending(x => x))
+ if (checkBox1.Checked && changed)
{
- if (idx >= 0 && idx < _displayIndices.Count)
+ foreach (int idx in targets.OrderByDescending(x => x))
{
- _displayIndices.RemoveAt(idx);
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
}
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
+ }
+ else
+ {
+ tileViewSec.SelectedIndices.Clear();
}
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- }
- else
- {
- tileViewSec.SelectedIndices.Clear();
- }
- tileViewOrg.Invalidate();
- tileViewSec.Invalidate();
- if (lastCopiedId >= 0)
- {
- pictureBoxOrg.BackgroundImage = Art.IsValidStatic(lastCopiedId) ? Art.GetStatic(lastCopiedId) : null;
+ tileViewOrg.Invalidate();
+ tileViewSec.Invalidate();
+ if (lastCopiedId >= 0)
+ {
+ pictureBoxOrg.BackgroundImage = Art.IsValidStatic(lastCopiedId) ? Art.GetStatic(lastCopiedId) : null;
+ }
}
- Cursor.Current = Cursors.Default;
}
private void OnDoubleClickSec(object sender, MouseEventArgs e)
@@ -546,40 +548,41 @@ private void OnClickCopyAllDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int maxId = Art.GetMaxItemId() + 1;
- for (int i = 0; i < maxId; i++)
+ using (new WaitCursorScope(this))
{
- if (!SecondArt.IsValidStatic(i) || Compare(i))
+ int maxId = Art.GetMaxItemId() + 1;
+ for (int i = 0; i < maxId; i++)
{
- continue;
- }
+ if (!SecondArt.IsValidStatic(i) || Compare(i))
+ {
+ continue;
+ }
- Bitmap copy = new Bitmap(SecondArt.GetStatic(i));
- Art.ReplaceStatic(i, copy);
- ControlEvents.FireItemChangeEvent(this, i);
- _compare[i] = true;
- }
+ Bitmap copy = new Bitmap(SecondArt.GetStatic(i));
+ Art.ReplaceStatic(i, copy);
+ ControlEvents.FireItemChangeEvent(this, i);
+ _compare[i] = true;
+ }
- Options.ChangedUltimaClass["Art"] = true;
+ Options.ChangedUltimaClass["Art"] = true;
- if (checkBox1.Checked)
- {
- _displayIndices.Clear();
- for (int i = 0; i < maxId; i++)
+ if (checkBox1.Checked)
{
- if (!Compare(i))
+ _displayIndices.Clear();
+ for (int i = 0; i < maxId; i++)
{
- _displayIndices.Add(i);
+ if (!Compare(i))
+ {
+ _displayIndices.Add(i);
+ }
}
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
}
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- }
- tileViewOrg.Invalidate();
- tileViewSec.Invalidate();
- Cursor.Current = Cursors.Default;
+ tileViewOrg.Invalidate();
+ tileViewSec.Invalidate();
+ }
}
private void OnClickBrowse(object sender, EventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
index e01482b..3201145 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
@@ -328,29 +328,30 @@ private void OnChangeShowDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- _displayIndices.Clear();
- if (checkBox1.Checked)
+ using (new WaitCursorScope(this))
{
- for (int i = 0; i < 0x4000; i++)
+ _displayIndices.Clear();
+ if (checkBox1.Checked)
{
- if (!Compare(i))
+ for (int i = 0; i < 0x4000; i++)
{
- _displayIndices.Add(i);
+ if (!Compare(i))
+ {
+ _displayIndices.Add(i);
+ }
}
}
- }
- else
- {
- for (int i = 0; i < 0x4000; i++)
+ else
{
- _displayIndices.Add(i);
+ for (int i = 0; i < 0x4000; i++)
+ {
+ _displayIndices.Add(i);
+ }
}
- }
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- Cursor.Current = Cursors.Default;
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
+ }
}
private void ExportAsBmp(object sender, EventArgs e)
@@ -451,60 +452,61 @@ private void OnClickCopy(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int lastCopiedId = -1;
- bool changed = false;
-
- foreach (int focusIdx in targets)
+ using (new WaitCursorScope(this))
{
- if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ int lastCopiedId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- continue;
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
+
+ int i = _displayIndices[focusIdx];
+ if (!SecondArt.IsValidLand(i))
+ {
+ continue;
+ }
+
+ Bitmap copy = new Bitmap(SecondArt.GetLand(i));
+ Art.ReplaceLand(i, copy);
+ ControlEvents.FireLandTileChangeEvent(this, i);
+ _compare[i] = true;
+ lastCopiedId = i;
+ changed = true;
}
- int i = _displayIndices[focusIdx];
- if (!SecondArt.IsValidLand(i))
+ if (changed)
{
- continue;
+ Options.ChangedUltimaClass["Art"] = true;
}
- Bitmap copy = new Bitmap(SecondArt.GetLand(i));
- Art.ReplaceLand(i, copy);
- ControlEvents.FireLandTileChangeEvent(this, i);
- _compare[i] = true;
- lastCopiedId = i;
- changed = true;
- }
-
- if (changed)
- {
- Options.ChangedUltimaClass["Art"] = true;
- }
-
- if (checkBox1.Checked && changed)
- {
- foreach (int idx in targets.OrderByDescending(x => x))
+ if (checkBox1.Checked && changed)
{
- if (idx >= 0 && idx < _displayIndices.Count)
+ foreach (int idx in targets.OrderByDescending(x => x))
{
- _displayIndices.RemoveAt(idx);
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
}
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
+ }
+ else
+ {
+ tileViewSec.SelectedIndices.Clear();
}
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- }
- else
- {
- tileViewSec.SelectedIndices.Clear();
- }
- tileViewOrg.Invalidate();
- tileViewSec.Invalidate();
- if (lastCopiedId >= 0)
- {
- pictureBoxOrg.BackgroundImage = Art.IsValidLand(lastCopiedId) ? Art.GetLand(lastCopiedId) : null;
+ tileViewOrg.Invalidate();
+ tileViewSec.Invalidate();
+ if (lastCopiedId >= 0)
+ {
+ pictureBoxOrg.BackgroundImage = Art.IsValidLand(lastCopiedId) ? Art.GetLand(lastCopiedId) : null;
+ }
}
- Cursor.Current = Cursors.Default;
}
private void OnDoubleClickSec(object sender, MouseEventArgs e)
@@ -523,39 +525,40 @@ private void OnClickCopyAllDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- for (int i = 0; i < 0x4000; i++)
+ using (new WaitCursorScope(this))
{
- if (!SecondArt.IsValidLand(i) || Compare(i))
+ for (int i = 0; i < 0x4000; i++)
{
- continue;
- }
+ if (!SecondArt.IsValidLand(i) || Compare(i))
+ {
+ continue;
+ }
- Bitmap copy = new Bitmap(SecondArt.GetLand(i));
- Art.ReplaceLand(i, copy);
- ControlEvents.FireLandTileChangeEvent(this, i);
- _compare[i] = true;
- }
+ Bitmap copy = new Bitmap(SecondArt.GetLand(i));
+ Art.ReplaceLand(i, copy);
+ ControlEvents.FireLandTileChangeEvent(this, i);
+ _compare[i] = true;
+ }
- Options.ChangedUltimaClass["Art"] = true;
+ Options.ChangedUltimaClass["Art"] = true;
- if (checkBox1.Checked)
- {
- _displayIndices.Clear();
- for (int i = 0; i < 0x4000; i++)
+ if (checkBox1.Checked)
{
- if (!Compare(i))
+ _displayIndices.Clear();
+ for (int i = 0; i < 0x4000; i++)
{
- _displayIndices.Add(i);
+ if (!Compare(i))
+ {
+ _displayIndices.Add(i);
+ }
}
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
}
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- }
- tileViewOrg.Invalidate();
- tileViewSec.Invalidate();
- Cursor.Current = Cursors.Default;
+ tileViewOrg.Invalidate();
+ tileViewSec.Invalidate();
+ }
}
}
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
index 22188a4..09541ac 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.cs
@@ -832,69 +832,69 @@ private void CalculateDiffs()
int height = _currentMap.Height >> 3;
var masks = new ulong[width * height];
- Cursor.Current = Cursors.WaitCursor;
-
- for (int x = 0; x < width; ++x)
+ using (new WaitCursorScope(this))
{
- for (int y = 0; y < height; ++y)
+ for (int x = 0; x < width; ++x)
{
- Tile[] customTiles = _currentMap.Tiles.GetLandBlock(x, y);
- Tile[] origTiles = _originalMap.Tiles.GetLandBlock(x, y);
+ for (int y = 0; y < height; ++y)
+ {
+ Tile[] customTiles = _currentMap.Tiles.GetLandBlock(x, y);
+ Tile[] origTiles = _originalMap.Tiles.GetLandBlock(x, y);
- HuedTile[][][] customStatics = _currentMap.Tiles.GetStaticBlock(x, y);
- HuedTile[][][] origStatics = _originalMap.Tiles.GetStaticBlock(x, y);
+ HuedTile[][][] customStatics = _currentMap.Tiles.GetStaticBlock(x, y);
+ HuedTile[][][] origStatics = _originalMap.Tiles.GetStaticBlock(x, y);
- ulong mask = 0;
- for (int xb = 0; xb < 8; xb++)
- {
- HuedTile[][] customCol = customStatics[xb];
- HuedTile[][] origCol = origStatics[xb];
- for (int yb = 0; yb < 8; yb++)
+ ulong mask = 0;
+ for (int xb = 0; xb < 8; xb++)
{
- int tileIdx = (yb << 3) + xb;
- bool isDiff;
-
- if (customTiles[tileIdx].Id != origTiles[tileIdx].Id
- || customTiles[tileIdx].Z != origTiles[tileIdx].Z)
- {
- isDiff = true;
- }
- else if (customCol[yb].Length != origCol[yb].Length)
+ HuedTile[][] customCol = customStatics[xb];
+ HuedTile[][] origCol = origStatics[xb];
+ for (int yb = 0; yb < 8; yb++)
{
- isDiff = true;
- }
- else
- {
- isDiff = false;
- HuedTile[] cs = customCol[yb];
- HuedTile[] os = origCol[yb];
- for (int i = 0; i < cs.Length; i++)
+ int tileIdx = (yb << 3) + xb;
+ bool isDiff;
+
+ if (customTiles[tileIdx].Id != origTiles[tileIdx].Id
+ || customTiles[tileIdx].Z != origTiles[tileIdx].Z)
+ {
+ isDiff = true;
+ }
+ else if (customCol[yb].Length != origCol[yb].Length)
{
- if (cs[i].Id != os[i].Id
- || cs[i].Z != os[i].Z
- || cs[i].Hue != os[i].Hue)
+ isDiff = true;
+ }
+ else
+ {
+ isDiff = false;
+ HuedTile[] cs = customCol[yb];
+ HuedTile[] os = origCol[yb];
+ for (int i = 0; i < cs.Length; i++)
{
- isDiff = true;
- break;
+ if (cs[i].Id != os[i].Id
+ || cs[i].Z != os[i].Z
+ || cs[i].Hue != os[i].Hue)
+ {
+ isDiff = true;
+ break;
+ }
}
}
- }
- if (isDiff)
- {
- mask |= 1UL << ((xb << 3) | yb);
+ if (isDiff)
+ {
+ mask |= 1UL << ((xb << 3) | yb);
+ }
}
}
- }
- masks[x * height + y] = mask;
+ masks[x * height + y] = mask;
+ }
}
- }
- _diffMasks = masks;
- _diffWidthBlocks = width;
- _diffHeightBlocks = height;
- Cursor.Current = Cursors.Default;
+ _diffMasks = masks;
+ _diffWidthBlocks = width;
+ _diffHeightBlocks = height;
+ }
}
private void HandleScroll(object sender, ScrollEventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
index 686549a..8d76936 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
@@ -181,8 +181,7 @@ private void PopulateOrgOnly(bool isLand)
private void PopulateSection(bool isLand, bool showDiffOnly)
{
- Cursor.Current = Cursors.WaitCursor;
- try
+ using (new WaitCursorScope(this))
{
int totalCount = Math.Max(RadarCol.Colors?.Length ?? 0,
SecondRadarCol.IsLoaded ? SecondRadarCol.Length : 0);
@@ -209,10 +208,6 @@ private void PopulateSection(bool isLand, bool showDiffOnly)
orgView.VirtualListSize = indices.Count;
secView.VirtualListSize = SecondRadarCol.IsLoaded ? indices.Count : 0;
}
- finally
- {
- Cursor.Current = Cursors.Default;
- }
}
private void OnTileViewSizeChanged(object sender, EventArgs e)
@@ -480,19 +475,20 @@ private void OnClickLoadSecond(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- bool ok = SecondRadarCol.Initialize(path);
- Cursor.Current = Cursors.Default;
-
- if (!ok)
+ using (new WaitCursorScope(this))
{
- MessageBox.Show("Failed to load the selected radarcol.mul file.", "Error",
- MessageBoxButtons.OK, MessageBoxIcon.Error);
- return;
- }
+ bool ok = SecondRadarCol.Initialize(path);
- _compare.Clear();
- PopulateSection(IsLandSection, checkBoxShowDiff.Checked);
+ if (!ok)
+ {
+ MessageBox.Show("Failed to load the selected radarcol.mul file.", "Error",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ return;
+ }
+
+ _compare.Clear();
+ PopulateSection(IsLandSection, checkBoxShowDiff.Checked);
+ }
}
private void OnChangeShowDiff(object sender, EventArgs e)
@@ -548,47 +544,48 @@ private void OnClickCopySelected(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int lastIdx = -1;
- bool changed = false;
-
- foreach (int focusIdx in targets)
+ using (new WaitCursorScope(this))
{
- if (focusIdx < 0 || focusIdx >= indices.Count)
+ int lastIdx = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- continue;
- }
+ if (focusIdx < 0 || focusIdx >= indices.Count)
+ {
+ continue;
+ }
- int idx = indices[focusIdx];
- CopySecToOrg(idx);
- lastIdx = idx;
- changed = true;
- }
+ int idx = indices[focusIdx];
+ CopySecToOrg(idx);
+ lastIdx = idx;
+ changed = true;
+ }
- if (checkBoxShowDiff.Checked && changed)
- {
- foreach (int displayIdx in targets.OrderByDescending(x => x))
+ if (checkBoxShowDiff.Checked && changed)
{
- if (displayIdx >= 0 && displayIdx < indices.Count)
+ foreach (int displayIdx in targets.OrderByDescending(x => x))
{
- indices.RemoveAt(displayIdx);
+ if (displayIdx >= 0 && displayIdx < indices.Count)
+ {
+ indices.RemoveAt(displayIdx);
+ }
}
+ orgView.VirtualListSize = indices.Count;
+ secView.VirtualListSize = indices.Count;
+ }
+ else
+ {
+ secView.SelectedIndices.Clear();
}
- orgView.VirtualListSize = indices.Count;
- secView.VirtualListSize = indices.Count;
- }
- else
- {
- secView.SelectedIndices.Clear();
- }
- orgView.Invalidate();
- secView.Invalidate();
- if (lastIdx >= 0)
- {
- UpdateDetailPanel(lastIdx);
+ orgView.Invalidate();
+ secView.Invalidate();
+ if (lastIdx >= 0)
+ {
+ UpdateDetailPanel(lastIdx);
+ }
}
- Cursor.Current = Cursors.Default;
}
private void OnClickCopy1To2(object sender, EventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
index 3b4d68d..99489f8 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
@@ -302,29 +302,30 @@ private void OnChangeShowDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- _displayIndices.Clear();
- if (checkBox1.Checked)
+ using (new WaitCursorScope(this))
{
- for (int i = 0; i < 0x4000; i++)
+ _displayIndices.Clear();
+ if (checkBox1.Checked)
{
- if (!Compare(i))
+ for (int i = 0; i < 0x4000; i++)
{
- _displayIndices.Add(i);
+ if (!Compare(i))
+ {
+ _displayIndices.Add(i);
+ }
}
}
- }
- else
- {
- for (int i = 0; i < 0x4000; i++)
+ else
{
- _displayIndices.Add(i);
+ for (int i = 0; i < 0x4000; i++)
+ {
+ _displayIndices.Add(i);
+ }
}
- }
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- Cursor.Current = Cursors.Default;
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
+ }
}
private void ExportAsBmp(object sender, EventArgs e)
@@ -424,60 +425,61 @@ private void OnClickCopy(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int lastCopiedId = -1;
- bool changed = false;
-
- foreach (int focusIdx in targets)
+ using (new WaitCursorScope(this))
{
- if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ int lastCopiedId = -1;
+ bool changed = false;
+
+ foreach (int focusIdx in targets)
{
- continue;
+ if (focusIdx < 0 || focusIdx >= _displayIndices.Count)
+ {
+ continue;
+ }
+
+ int i = _displayIndices[focusIdx];
+ if (!SecondTexture.IsValidTexture(i))
+ {
+ continue;
+ }
+
+ Bitmap copy = new Bitmap(SecondTexture.GetTexture(i));
+ Textures.Replace(i, copy);
+ ControlEvents.FireTextureChangeEvent(this, i);
+ _compare[i] = true;
+ lastCopiedId = i;
+ changed = true;
}
- int i = _displayIndices[focusIdx];
- if (!SecondTexture.IsValidTexture(i))
+ if (changed)
{
- continue;
+ Options.ChangedUltimaClass["Texture"] = true;
}
- Bitmap copy = new Bitmap(SecondTexture.GetTexture(i));
- Textures.Replace(i, copy);
- ControlEvents.FireTextureChangeEvent(this, i);
- _compare[i] = true;
- lastCopiedId = i;
- changed = true;
- }
-
- if (changed)
- {
- Options.ChangedUltimaClass["Texture"] = true;
- }
-
- if (checkBox1.Checked && changed)
- {
- foreach (int idx in targets.OrderByDescending(x => x))
+ if (checkBox1.Checked && changed)
{
- if (idx >= 0 && idx < _displayIndices.Count)
+ foreach (int idx in targets.OrderByDescending(x => x))
{
- _displayIndices.RemoveAt(idx);
+ if (idx >= 0 && idx < _displayIndices.Count)
+ {
+ _displayIndices.RemoveAt(idx);
+ }
}
+ tileViewOrg.VirtualListSize = _displayIndices.Count;
+ tileViewSec.VirtualListSize = _displayIndices.Count;
+ }
+ else
+ {
+ tileViewSec.SelectedIndices.Clear();
}
- tileViewOrg.VirtualListSize = _displayIndices.Count;
- tileViewSec.VirtualListSize = _displayIndices.Count;
- }
- else
- {
- tileViewSec.SelectedIndices.Clear();
- }
- tileViewOrg.Invalidate();
- tileViewSec.Invalidate();
- if (lastCopiedId >= 0)
- {
- pictureBoxOrg.BackgroundImage = Textures.TestTexture(lastCopiedId) ? Textures.GetTexture(lastCopiedId) : null;
+ tileViewOrg.Invalidate();
+ tileViewSec.Invalidate();
+ if (lastCopiedId >= 0)
+ {
+ pictureBoxOrg.BackgroundImage = Textures.TestTexture(lastCopiedId) ? Textures.GetTexture(lastCopiedId) : null;
+ }
}
- Cursor.Current = Cursors.Default;
}
private void CopyToLeft_Click(object sender, MouseEventArgs e)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
index c39d62c..74f0ae3 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
@@ -676,22 +676,22 @@ private void OnChangeShowDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- if (IsLandTab)
+ using (new WaitCursorScope(this))
{
- RefreshLandLists();
- }
- else
- {
- RefreshItemLists();
+ if (IsLandTab)
+ {
+ RefreshLandLists();
+ }
+ else
+ {
+ RefreshItemLists();
+ }
}
- Cursor.Current = Cursors.Default;
}
private void OnTabChanged(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
- try
+ using (new WaitCursorScope(this))
{
if (chkShowDiff.Checked && _secondTileData != null)
{
@@ -705,10 +705,6 @@ private void OnTabChanged(object sender, EventArgs e)
}
}
}
- finally
- {
- Cursor.Current = Cursors.Default;
- }
}
// ── Owner-draw helpers ────────────────────────────────────────────────────
@@ -1192,45 +1188,46 @@ private void OnClickCopyLandSelected(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int lastId = -1;
-
- foreach (int focusIdx in targets)
+ using (new WaitCursorScope(this))
{
- if (focusIdx < 0 || focusIdx >= _landDisplayIndices.Count)
+ int lastId = -1;
+
+ foreach (int focusIdx in targets)
{
- continue;
- }
+ if (focusIdx < 0 || focusIdx >= _landDisplayIndices.Count)
+ {
+ continue;
+ }
- int id = _landDisplayIndices[focusIdx];
- CopyLandEntry(id);
- lastId = id;
- }
+ int id = _landDisplayIndices[focusIdx];
+ CopyLandEntry(id);
+ lastId = id;
+ }
- if (chkShowDiff.Checked && lastId >= 0)
- {
- foreach (int displayIdx in targets.OrderByDescending(x => x))
+ if (chkShowDiff.Checked && lastId >= 0)
{
- if (displayIdx >= 0 && displayIdx < _landDisplayIndices.Count)
+ foreach (int displayIdx in targets.OrderByDescending(x => x))
{
- _landDisplayIndices.RemoveAt(displayIdx);
+ if (displayIdx >= 0 && displayIdx < _landDisplayIndices.Count)
+ {
+ _landDisplayIndices.RemoveAt(displayIdx);
+ }
}
+ tileViewLandOrg.VirtualListSize = _landDisplayIndices.Count;
+ tileViewLandSec.VirtualListSize = _landDisplayIndices.Count;
+ }
+ else
+ {
+ tileViewLandSec.SelectedIndices.Clear();
}
- tileViewLandOrg.VirtualListSize = _landDisplayIndices.Count;
- tileViewLandSec.VirtualListSize = _landDisplayIndices.Count;
- }
- else
- {
- tileViewLandSec.SelectedIndices.Clear();
- }
- tileViewLandOrg.Invalidate();
- tileViewLandSec.Invalidate();
- if (lastId >= 0)
- {
- UpdateLandDetail(lastId);
+ tileViewLandOrg.Invalidate();
+ tileViewLandSec.Invalidate();
+ if (lastId >= 0)
+ {
+ UpdateLandDetail(lastId);
+ }
}
- Cursor.Current = Cursors.Default;
}
private void OnClickCopyLandAllDiff(object sender, EventArgs e)
@@ -1240,24 +1237,25 @@ private void OnClickCopyLandAllDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int total = Math.Max(TileData.LandTable.Length, _secondTileData.LandTable.Length);
- for (int i = 0; i < total; i++)
+ using (new WaitCursorScope(this))
{
- if (!CompareLand(i) && i < _secondTileData.LandTable.Length)
+ int total = Math.Max(TileData.LandTable.Length, _secondTileData.LandTable.Length);
+ for (int i = 0; i < total; i++)
{
- CopyLandEntry(i);
+ if (!CompareLand(i) && i < _secondTileData.LandTable.Length)
+ {
+ CopyLandEntry(i);
+ }
}
- }
- if (chkShowDiff.Checked)
- {
- RefreshLandLists();
- }
+ if (chkShowDiff.Checked)
+ {
+ RefreshLandLists();
+ }
- Cursor.Current = Cursors.Default;
- tileViewLandOrg.Invalidate();
- tileViewLandSec.Invalidate();
+ tileViewLandOrg.Invalidate();
+ tileViewLandSec.Invalidate();
+ }
}
private void CopyLandEntry(int id)
@@ -1286,45 +1284,46 @@ private void OnClickCopyItemSelected(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int lastId = -1;
-
- foreach (int focusIdx in targets)
+ using (new WaitCursorScope(this))
{
- if (focusIdx < 0 || focusIdx >= _itemDisplayIndices.Count)
+ int lastId = -1;
+
+ foreach (int focusIdx in targets)
{
- continue;
- }
+ if (focusIdx < 0 || focusIdx >= _itemDisplayIndices.Count)
+ {
+ continue;
+ }
- int id = _itemDisplayIndices[focusIdx];
- CopyItemEntry(id);
- lastId = id;
- }
+ int id = _itemDisplayIndices[focusIdx];
+ CopyItemEntry(id);
+ lastId = id;
+ }
- if (chkShowDiff.Checked && lastId >= 0)
- {
- foreach (int displayIdx in targets.OrderByDescending(x => x))
+ if (chkShowDiff.Checked && lastId >= 0)
{
- if (displayIdx >= 0 && displayIdx < _itemDisplayIndices.Count)
+ foreach (int displayIdx in targets.OrderByDescending(x => x))
{
- _itemDisplayIndices.RemoveAt(displayIdx);
+ if (displayIdx >= 0 && displayIdx < _itemDisplayIndices.Count)
+ {
+ _itemDisplayIndices.RemoveAt(displayIdx);
+ }
}
+ tileViewItemOrg.VirtualListSize = _itemDisplayIndices.Count;
+ tileViewItemSec.VirtualListSize = _itemDisplayIndices.Count;
+ }
+ else
+ {
+ tileViewItemSec.SelectedIndices.Clear();
}
- tileViewItemOrg.VirtualListSize = _itemDisplayIndices.Count;
- tileViewItemSec.VirtualListSize = _itemDisplayIndices.Count;
- }
- else
- {
- tileViewItemSec.SelectedIndices.Clear();
- }
- tileViewItemOrg.Invalidate();
- tileViewItemSec.Invalidate();
- if (lastId >= 0)
- {
- UpdateItemDetail(lastId);
+ tileViewItemOrg.Invalidate();
+ tileViewItemSec.Invalidate();
+ if (lastId >= 0)
+ {
+ UpdateItemDetail(lastId);
+ }
}
- Cursor.Current = Cursors.Default;
}
private void OnClickCopyItemAllDiff(object sender, EventArgs e)
@@ -1334,24 +1333,25 @@ private void OnClickCopyItemAllDiff(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- int total = Math.Max(TileData.ItemTable.Length, _secondTileData.ItemTable.Length);
- for (int i = 0; i < total; i++)
+ using (new WaitCursorScope(this))
{
- if (!CompareItem(i) && i < _secondTileData.ItemTable.Length)
+ int total = Math.Max(TileData.ItemTable.Length, _secondTileData.ItemTable.Length);
+ for (int i = 0; i < total; i++)
{
- CopyItemEntry(i);
+ if (!CompareItem(i) && i < _secondTileData.ItemTable.Length)
+ {
+ CopyItemEntry(i);
+ }
}
- }
- if (chkShowDiff.Checked)
- {
- RefreshItemLists();
- }
+ if (chkShowDiff.Checked)
+ {
+ RefreshItemLists();
+ }
- Cursor.Current = Cursors.Default;
- tileViewItemOrg.Invalidate();
- tileViewItemSec.Invalidate();
+ tileViewItemOrg.Invalidate();
+ tileViewItemSec.Invalidate();
+ }
}
private void OnDoubleClickItemSec(object sender, MouseEventArgs e)
@@ -1438,8 +1438,7 @@ private void OnClickApplyRules(object sender, EventArgs e)
_options.IgnoredFlags = mask;
- Cursor.Current = Cursors.WaitCursor;
- try
+ using (new WaitCursorScope(this))
{
InvalidateCompareCache();
if (IsLandTab)
@@ -1451,10 +1450,6 @@ private void OnClickApplyRules(object sender, EventArgs e)
RefreshItemLists();
}
}
- finally
- {
- Cursor.Current = Cursors.Default;
- }
}
private void OnClickResetRules(object sender, EventArgs e)
diff --git a/UoFiddler.Plugin.MassImport/Forms/MassImportForm.cs b/UoFiddler.Plugin.MassImport/Forms/MassImportForm.cs
index 16f7dfb..951a7b6 100644
--- a/UoFiddler.Plugin.MassImport/Forms/MassImportForm.cs
+++ b/UoFiddler.Plugin.MassImport/Forms/MassImportForm.cs
@@ -269,95 +269,95 @@ private void StartOnClick(object sender, EventArgs e)
return;
}
- Cursor.Current = Cursors.WaitCursor;
- OutputBox.Clear();
-
- Dictionary changedUltimaClass = new Dictionary
- {
- {"Animations", false},
- {"Animdata", false},
- {"Art", false},
- {"ASCIIFont", false},
- {"UnicodeFont", false},
- {"Gumps", false},
- {"Hues", false},
- {"Light", false},
- {"Map", false},
- {"Multis", false},
- {"Skills", false},
- {"Sound", false},
- {"Speech", false},
- {"CliLoc", false},
- {"Texture", false},
- {"TileData", false},
- {"RadarColor", false}
- };
-
- OutputBox.AppendText("Importing");
-
- foreach (ImportEntry entry in _importList)
- {
- if (!entry.Valid)
- {
- continue;
- }
-
- try
- {
- OutputBox.AppendText(".");
- entry.Import(checkBoxDirectSave.Checked, ref changedUltimaClass);
- }
- catch (Exception ex)
- {
- OutputBox.AppendText(
- $"{Environment.NewLine}Error importing {entry.Name} (index {entry.Index}): {ex.Message}{Environment.NewLine}");
- }
- }
-
- OutputBox.AppendText($"Done{Environment.NewLine}");
-
- if (checkBoxDirectSave.Checked)
+ using (new WaitCursorScope(this))
{
- if (changedUltimaClass["Art"])
- {
- OutputBox.AppendText($"Saving Items/LandTiles..{Environment.NewLine}");
- Ultima.Art.Save(Options.OutputPath);
- }
+ OutputBox.Clear();
- if (changedUltimaClass["Texture"])
+ Dictionary changedUltimaClass = new Dictionary
{
- OutputBox.AppendText($"Saving Textures..{Environment.NewLine}");
- Ultima.Textures.Save(Options.OutputPath);
- }
-
- if (changedUltimaClass["Gumps"])
- {
- OutputBox.AppendText($"Saving Gumps..{Environment.NewLine}");
- Ultima.Gumps.Save(Options.OutputPath);
- }
-
- if (changedUltimaClass["TileData"])
+ {"Animations", false},
+ {"Animdata", false},
+ {"Art", false},
+ {"ASCIIFont", false},
+ {"UnicodeFont", false},
+ {"Gumps", false},
+ {"Hues", false},
+ {"Light", false},
+ {"Map", false},
+ {"Multis", false},
+ {"Skills", false},
+ {"Sound", false},
+ {"Speech", false},
+ {"CliLoc", false},
+ {"Texture", false},
+ {"TileData", false},
+ {"RadarColor", false}
+ };
+
+ OutputBox.AppendText("Importing");
+
+ foreach (ImportEntry entry in _importList)
{
- OutputBox.AppendText($"Saving TileData..{Environment.NewLine}");
- Ultima.TileData.SaveTileData(Path.Combine(Options.OutputPath, "tiledata.mul"));
+ if (!entry.Valid)
+ {
+ continue;
+ }
+
+ try
+ {
+ OutputBox.AppendText(".");
+ entry.Import(checkBoxDirectSave.Checked, ref changedUltimaClass);
+ }
+ catch (Exception ex)
+ {
+ OutputBox.AppendText(
+ $"{Environment.NewLine}Error importing {entry.Name} (index {entry.Index}): {ex.Message}{Environment.NewLine}");
+ }
}
- if (changedUltimaClass["Hues"])
- {
- OutputBox.AppendText($"Saving Hues..{Environment.NewLine}");
- Ultima.Hues.Save(Options.OutputPath);
- }
+ OutputBox.AppendText($"Done{Environment.NewLine}");
- if (changedUltimaClass["Multis"])
+ if (checkBoxDirectSave.Checked)
{
- OutputBox.AppendText($"Saving Multis..{Environment.NewLine}");
- Ultima.Multis.Save(Options.OutputPath);
+ if (changedUltimaClass["Art"])
+ {
+ OutputBox.AppendText($"Saving Items/LandTiles..{Environment.NewLine}");
+ Ultima.Art.Save(Options.OutputPath);
+ }
+
+ if (changedUltimaClass["Texture"])
+ {
+ OutputBox.AppendText($"Saving Textures..{Environment.NewLine}");
+ Ultima.Textures.Save(Options.OutputPath);
+ }
+
+ if (changedUltimaClass["Gumps"])
+ {
+ OutputBox.AppendText($"Saving Gumps..{Environment.NewLine}");
+ Ultima.Gumps.Save(Options.OutputPath);
+ }
+
+ if (changedUltimaClass["TileData"])
+ {
+ OutputBox.AppendText($"Saving TileData..{Environment.NewLine}");
+ Ultima.TileData.SaveTileData(Path.Combine(Options.OutputPath, "tiledata.mul"));
+ }
+
+ if (changedUltimaClass["Hues"])
+ {
+ OutputBox.AppendText($"Saving Hues..{Environment.NewLine}");
+ Ultima.Hues.Save(Options.OutputPath);
+ }
+
+ if (changedUltimaClass["Multis"])
+ {
+ OutputBox.AppendText($"Saving Multis..{Environment.NewLine}");
+ Ultima.Multis.Save(Options.OutputPath);
+ }
+
+ OutputBox.AppendText($"Done{Environment.NewLine}");
}
-
- OutputBox.AppendText($"Done{Environment.NewLine}");
}
-
- Cursor.Current = Cursors.Default;
}
}
}
\ No newline at end of file
diff --git a/UoFiddler/Forms/MainForm.cs b/UoFiddler/Forms/MainForm.cs
index 0ed44a6..4f228c7 100644
--- a/UoFiddler/Forms/MainForm.cs
+++ b/UoFiddler/Forms/MainForm.cs
@@ -147,97 +147,96 @@ private void OnClickDarkMode(object sender, EventArgs e)
private void ReloadFiles(object sender, EventArgs e)
{
- Cursor.Current = Cursors.WaitCursor;
-
- Verdata.Initialize();
-
- if (Options.LoadedUltimaClass["Art"] || Options.LoadedUltimaClass["TileData"])
+ using (new WaitCursorScope(this))
{
- // Looks like we have to reload art first to have proper tiledata loading
- // and order here is important
- Art.Reload();
- TileData.Initialize();
- }
+ Verdata.Initialize();
- if (Options.LoadedUltimaClass["Hues"])
- {
- Hues.Initialize();
- }
+ if (Options.LoadedUltimaClass["Art"] || Options.LoadedUltimaClass["TileData"])
+ {
+ // Looks like we have to reload art first to have proper tiledata loading
+ // and order here is important
+ Art.Reload();
+ TileData.Initialize();
+ }
- if (Options.LoadedUltimaClass["ASCIIFont"])
- {
- AsciiText.Initialize();
- }
+ if (Options.LoadedUltimaClass["Hues"])
+ {
+ Hues.Initialize();
+ }
- if (Options.LoadedUltimaClass["UnicodeFont"])
- {
- UnicodeFonts.Initialize();
- }
+ if (Options.LoadedUltimaClass["ASCIIFont"])
+ {
+ AsciiText.Initialize();
+ }
- if (Options.LoadedUltimaClass["Animdata"])
- {
- Animdata.Initialize();
- }
+ if (Options.LoadedUltimaClass["UnicodeFont"])
+ {
+ UnicodeFonts.Initialize();
+ }
- if (Options.LoadedUltimaClass["Light"])
- {
- Light.Reload();
- }
+ if (Options.LoadedUltimaClass["Animdata"])
+ {
+ Animdata.Initialize();
+ }
- if (Options.LoadedUltimaClass["Skills"])
- {
- Skills.Reload();
- }
+ if (Options.LoadedUltimaClass["Light"])
+ {
+ Light.Reload();
+ }
- if (Options.LoadedUltimaClass["Sound"])
- {
- Sounds.Initialize();
- }
+ if (Options.LoadedUltimaClass["Skills"])
+ {
+ Skills.Reload();
+ }
- if (Options.LoadedUltimaClass["Texture"])
- {
- Textures.Reload();
- }
+ if (Options.LoadedUltimaClass["Sound"])
+ {
+ Sounds.Initialize();
+ }
- if (Options.LoadedUltimaClass["Gumps"])
- {
- Gumps.Reload();
- }
+ if (Options.LoadedUltimaClass["Texture"])
+ {
+ Textures.Reload();
+ }
- if (Options.LoadedUltimaClass["Animations"])
- {
- Animations.Reload();
- }
+ if (Options.LoadedUltimaClass["Gumps"])
+ {
+ Gumps.Reload();
+ }
- if (Options.LoadedUltimaClass["RadarColor"])
- {
- RadarCol.Initialize();
- }
+ if (Options.LoadedUltimaClass["Animations"])
+ {
+ Animations.Reload();
+ }
- if (Options.LoadedUltimaClass["Map"])
- {
- MapHelper.CheckForNewMapSize();
- Map.Reload();
- }
+ if (Options.LoadedUltimaClass["RadarColor"])
+ {
+ RadarCol.Initialize();
+ }
- if (Options.LoadedUltimaClass["Multis"])
- {
- Multis.Reload();
- }
+ if (Options.LoadedUltimaClass["Map"])
+ {
+ MapHelper.CheckForNewMapSize();
+ Map.Reload();
+ }
- if (Options.LoadedUltimaClass["Speech"])
- {
- SpeechList.Initialize();
- }
+ if (Options.LoadedUltimaClass["Multis"])
+ {
+ Multis.Reload();
+ }
- if (Options.LoadedUltimaClass["AnimationEdit"])
- {
- AnimationEdit.Reload();
- }
+ if (Options.LoadedUltimaClass["Speech"])
+ {
+ SpeechList.Initialize();
+ }
- ControlEvents.FireFilePathChangeEvent();
+ if (Options.LoadedUltimaClass["AnimationEdit"])
+ {
+ AnimationEdit.Reload();
+ }
- Cursor.Current = Cursors.Default;
+ ControlEvents.FireFilePathChangeEvent();
+ }
}
///
From ff9b05072a4b4569f81b2c4d8062aee035a6ce95 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Tue, 26 May 2026 23:37:35 +0200
Subject: [PATCH 19/21] Improve multi-select for compare hues control.
---
.../UserControls/CompareHuesControl.cs | 43 ++++++++++++++++++-
1 file changed, 41 insertions(+), 2 deletions(-)
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
index 9ced706..5654372 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareHuesControl.cs
@@ -44,6 +44,7 @@ public CompareHuesControl()
private bool _hue2Loaded;
private readonly Dictionary _compare = new Dictionary();
private readonly HashSet _multiSelected = new HashSet();
+ private int _multiSelectAnchor = -1;
private bool _multiSelectEnabled;
private bool _loaded;
@@ -284,6 +285,7 @@ private void OnChangeMultiSelect(object sender, EventArgs e)
if (!_multiSelectEnabled)
{
_multiSelected.Clear();
+ _multiSelectAnchor = -1;
}
PaintBox1();
if (_hue2Loaded)
@@ -304,6 +306,7 @@ private void OnMouseClick1(object sender, MouseEventArgs e)
_multiSelected.Clear();
_selected = index;
+ _multiSelectAnchor = index;
PaintBox1();
if (_hue2Loaded)
{
@@ -323,17 +326,30 @@ private void OnMouseClick2(object sender, MouseEventArgs e)
if (_multiSelectEnabled)
{
- bool inCheckBox = e.X < CheckBoxColumnWidth;
- if (inCheckBox || (Control.ModifierKeys & Keys.Control) == Keys.Control)
+ Keys mods = Control.ModifierKeys;
+
+ // Shift (with or without Ctrl) applies the clicked row's resulting state to the whole
+ // range from the anchor, so a contiguous block can be selected or deselected in one action.
+ if ((mods & Keys.Shift) == Keys.Shift)
+ {
+ int anchor = _multiSelectAnchor >= 0 && _multiSelectAnchor < Hues.List.Length
+ ? _multiSelectAnchor
+ : index;
+ bool select = !_multiSelected.Contains(index);
+ SetRangeSelection(anchor, index, select);
+ }
+ else if (e.X < CheckBoxColumnWidth || (mods & Keys.Control) == Keys.Control)
{
if (!_multiSelected.Remove(index))
{
_multiSelected.Add(index);
}
+ _multiSelectAnchor = index;
}
else
{
_selected = index;
+ _multiSelectAnchor = index;
}
}
else
@@ -348,6 +364,29 @@ private void OnMouseClick2(object sender, MouseEventArgs e)
}
}
+ private void SetRangeSelection(int fromIndex, int toIndex, bool select)
+ {
+ int start = Math.Min(fromIndex, toIndex);
+ int end = Math.Max(fromIndex, toIndex);
+
+ for (int i = start; i <= end; i++)
+ {
+ if (i < 0 || i >= Hues.List.Length)
+ {
+ continue;
+ }
+
+ if (select)
+ {
+ _multiSelected.Add(i);
+ }
+ else
+ {
+ _multiSelected.Remove(i);
+ }
+ }
+ }
+
private bool Compare(int index)
{
if (_compare.ContainsKey(index))
From d4986b314ff6aadea35a0ea11880eeb9e163c413 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Wed, 27 May 2026 00:12:04 +0200
Subject: [PATCH 20/21] Use more caching in animations and few other smaller
optimizations.
---
Ultima/Animations.cs | 49 ++++
Ultima/AnimationsUopLoader.cs | 21 +-
Ultima/Caching/LruAnimationCache.cs | 236 ++++++++++++++++++
Ultima/Caching/LruBitmapCache.cs | 3 +-
Ultima/Files.cs | 10 +
Ultima/Gumps.cs | 2 +-
.../UserControls/AnimDataControl.cs | 6 +
.../UserControls/AnimatedPictureBox.cs | 8 +
.../UserControls/AnimationListControl.cs | 5 +-
.../UserControls/VerdataControl.cs | 4 +-
10 files changed, 334 insertions(+), 10 deletions(-)
create mode 100644 Ultima/Caching/LruAnimationCache.cs
diff --git a/Ultima/Animations.cs b/Ultima/Animations.cs
index 01e6669..7cfe2c2 100644
--- a/Ultima/Animations.cs
+++ b/Ultima/Animations.cs
@@ -1,6 +1,7 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
+using Ultima.Caching;
namespace Ultima
{
@@ -9,6 +10,40 @@ public static class Animations
public const int _maxAnimationValue = 2048; // bodyconv.def says it's maximum animation value so max bodyId?
public static readonly int PaletteCapacity = 0x100;
+ // LRU decode cache shared by the MUL and UOP paths. Bitmaps it returns
+ // are cache-owned and borrowed by callers — do NOT dispose them; clone
+ // first if you need an owned copy (e.g. to feed AnimatedPictureBox).
+ private static LruAnimationCache _cache = new LruAnimationCache(Files.CacheCapacityAnimations);
+
+ internal static LruAnimationCache Cache => _cache;
+
+ ///
+ /// Override the LRU cap for the animation decode cache. Lower values
+ /// bound the working set on memory-constrained machines at the cost of
+ /// more re-decodes during long browsing sessions.
+ ///
+ public static void SetCacheCapacity(int capacity)
+ {
+ _cache.SetCapacity(capacity);
+ }
+
+ ///
+ /// Packs the parameters that uniquely identify a decoded frame set into
+ /// a single cache key. For the MUL path pass the post-Translate body,
+ /// fileType and resolved hue; for the UOP path pass the raw body with
+ /// set (fileType is irrelevant there).
+ ///
+ internal static long BuildAnimationKey(int body, int action, int direction, int fileType, bool firstFrame, int hue, bool isUop)
+ {
+ return ((long)(body & 0xFFFFF))
+ | ((long)(action & 0x7F) << 20)
+ | ((long)(direction & 0x7) << 27)
+ | ((long)(fileType & 0x7) << 30)
+ | ((firstFrame ? 1L : 0L) << 33)
+ | ((long)(hue & 0xFFFF) << 34)
+ | ((isUop ? 1L : 0L) << 50);
+ }
+
private static FileIndex _fileIndex = new FileIndex("Anim.idx", "Anim.mul", 0x40000, 6);
private static FileIndex _fileIndex2 = new FileIndex("Anim2.idx", "Anim2.mul", 0x10000, -1);
private static FileIndex _fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", 0x20000, -1);
@@ -30,6 +65,8 @@ public static void Reload()
_fileIndex5?.Dispose();
_fileIndex6?.Dispose();
+ _cache?.Clear();
+
_fileIndex = new FileIndex("Anim.idx", "Anim.mul", 0x40000, 6);
_fileIndex2 = new FileIndex("Anim2.idx", "Anim2.mul", 0x10000, -1);
_fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", 0x20000, -1);
@@ -72,6 +109,16 @@ public static AnimationFrame[] GetAnimation(int body, int action, int direction,
int fileType = BodyConverter.Convert(ref body);
+ // Key off the post-Translate inputs; the decode below mutates `hue`
+ // into its resolved index, so reproduce that on a cache hit.
+ int lookupHue = hue;
+ long cacheKey = BuildAnimationKey(body, action, direction, fileType, firstFrame, lookupHue, isUop: false);
+ if (_cache.TryGet(cacheKey, out AnimationFrame[] cachedFrames))
+ {
+ hue = (lookupHue & 0x3FFF) - 1;
+ return cachedFrames;
+ }
+
GetFileIndex(body, action, direction, fileType, out FileIndex fileIndex, out int index);
Stream stream = fileIndex.Seek(index, out int length, out int _, out bool _);
@@ -146,6 +193,8 @@ public static AnimationFrame[] GetAnimation(int body, int action, int direction,
memoryStream.Close();
+ _cache.Set(cacheKey, frames);
+
return frames;
}
diff --git a/Ultima/AnimationsUopLoader.cs b/Ultima/AnimationsUopLoader.cs
index 98eeb1b..200ebf0 100644
--- a/Ultima/AnimationsUopLoader.cs
+++ b/Ultima/AnimationsUopLoader.cs
@@ -222,7 +222,7 @@ private static void LoadAnimationSequence()
while (true);
// Scan all plausible body IDs for sequence entries
- for (int animId = 0; animId < Animations._maxAnimationValue; animId++)
+ for (int animId = 0; animId < Animations.MaxAnimationValue; animId++)
{
ulong hash = UopUtils.HashFileName($"build/animationsequence/{animId:D8}.bin");
if (!seqEntries.TryGetValue(hash, out var entry))
@@ -393,6 +393,14 @@ public static AnimationFrame[] GetAnimation(int body, int action, int direction,
return null;
}
+ // UOP path leaves `hue` unchanged for the caller, so the key uses
+ // the raw input hue and the hit path does not touch `hue`.
+ long cacheKey = Animations.BuildAnimationKey(body, action, direction, 0, firstFrame, hue, isUop: true);
+ if (Animations.Cache.TryGet(cacheKey, out AnimationFrame[] cachedFrames))
+ {
+ return cachedFrames;
+ }
+
int resolved = GetResolvedAction(body, action);
ulong hash = UopUtils.HashFileName($"build/animationlegacyframe/{body:D6}/{resolved:D2}.bin");
@@ -430,12 +438,13 @@ public static AnimationFrame[] GetAnimation(int body, int action, int direction,
}
}
- if (firstFrame && frames.Length > 1)
- {
- return new[] { frames[0] };
- }
+ AnimationFrame[] result = firstFrame && frames.Length > 1
+ ? new[] { frames[0] }
+ : frames;
+
+ Animations.Cache.Set(cacheKey, result);
- return frames;
+ return result;
}
private static byte[] ReadEntryData(UopEntry entry)
diff --git a/Ultima/Caching/LruAnimationCache.cs b/Ultima/Caching/LruAnimationCache.cs
new file mode 100644
index 0000000..63966e6
--- /dev/null
+++ b/Ultima/Caching/LruAnimationCache.cs
@@ -0,0 +1,236 @@
+// /***************************************************************************
+// *
+// * "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.Threading;
+
+namespace Ultima.Caching
+{
+ ///
+ /// Bounded LRU cache for decoded animation frame sets. The sibling of
+ /// , but keyed by a packed
+ /// (body/action/direction/hue/firstFrame) and storing whole
+ /// arrays instead of a single bitmap, since
+ /// Animations.GetAnimation returns all frames of a direction at once.
+ ///
+ /// Eviction policy mirrors : by default the
+ /// evicted array's bitmaps are NOT disposed — the SDK cannot know whether
+ /// the UI still holds a reference, and disposing an in-use GDI handle
+ /// crashes the renderer. Consumers borrow the returned bitmaps and must not
+ /// dispose them. always disposes every owned bitmap;
+ /// is never disposed (shared singleton).
+ ///
+ /// Thread safety: every public member is guarded by a single lock.
+ ///
+ public sealed class LruAnimationCache : IDisposable
+ {
+ private readonly Lock _lock = new();
+ private readonly LinkedList> _list =
+ new LinkedList>();
+ private readonly Dictionary>> _map;
+
+ private int _capacity;
+ private int _evictedCount;
+ private bool _disposed;
+
+ public LruAnimationCache(int capacity)
+ {
+ if (capacity < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be non-negative.");
+ }
+ _capacity = capacity;
+ _map = new Dictionary>>(Math.Min(capacity, 4096));
+ }
+
+ public int Capacity
+ {
+ get { lock (_lock) { return _capacity; } }
+ }
+
+ public int Count
+ {
+ get { lock (_lock) { return _map.Count; } }
+ }
+
+ ///
+ /// If true, frame arrays evicted by the LRU policy or by
+ /// have their bitmaps disposed before being dropped. Off by default —
+ /// see class remarks.
+ ///
+ public bool DisposeOnEvict { get; set; }
+
+ public int EvictedCount
+ {
+ get { lock (_lock) { return _evictedCount; } }
+ }
+
+ public bool TryGet(long key, out AnimationFrame[] value)
+ {
+ lock (_lock)
+ {
+ if (_disposed || _capacity == 0)
+ {
+ value = null;
+ return false;
+ }
+ if (_map.TryGetValue(key, out var node))
+ {
+ _list.Remove(node);
+ _list.AddFirst(node);
+ value = node.Value.Value;
+ return true;
+ }
+ value = null;
+ return false;
+ }
+ }
+
+ public void Set(long key, AnimationFrame[] value)
+ {
+ if (value == null)
+ {
+ Remove(key);
+ return;
+ }
+ lock (_lock)
+ {
+ if (_disposed || _capacity == 0)
+ {
+ return;
+ }
+
+ if (_map.TryGetValue(key, out var existing))
+ {
+ AnimationFrame[] previous = existing.Value.Value;
+ _list.Remove(existing);
+ var replacement = new LinkedListNode>(new KeyValuePair(key, value));
+ _list.AddFirst(replacement);
+ _map[key] = replacement;
+
+ if (DisposeOnEvict && !ReferenceEquals(previous, value))
+ {
+ DisposeFrames(previous);
+ }
+ return;
+ }
+
+ var node = new LinkedListNode>(new KeyValuePair(key, value));
+ _list.AddFirst(node);
+ _map[key] = node;
+ EvictWhileOverCapacityNoLock();
+ }
+ }
+
+ public bool Remove(long key)
+ {
+ lock (_lock)
+ {
+ if (_map.TryGetValue(key, out var node))
+ {
+ AnimationFrame[] frames = node.Value.Value;
+ _list.Remove(node);
+ _map.Remove(key);
+ if (DisposeOnEvict)
+ {
+ DisposeFrames(frames);
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+
+ ///
+ /// Drops every entry. Disposes the bitmaps iff
+ /// is true. Use for soft resets where consumers may still hold references.
+ ///
+ public void Clear()
+ {
+ lock (_lock)
+ {
+ if (DisposeOnEvict)
+ {
+ foreach (var kvp in _list)
+ {
+ DisposeFrames(kvp.Value);
+ }
+ }
+ _list.Clear();
+ _map.Clear();
+ }
+ }
+
+ public void SetCapacity(int newCapacity)
+ {
+ if (newCapacity < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(newCapacity));
+ }
+ lock (_lock)
+ {
+ _capacity = newCapacity;
+ EvictWhileOverCapacityNoLock();
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_lock)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+ _disposed = true;
+ foreach (var kvp in _list)
+ {
+ DisposeFrames(kvp.Value);
+ }
+ _list.Clear();
+ _map.Clear();
+ }
+ }
+
+ private void EvictWhileOverCapacityNoLock()
+ {
+ while (_map.Count > _capacity)
+ {
+ var lru = _list.Last;
+ if (lru == null)
+ {
+ break;
+ }
+ _list.RemoveLast();
+ _map.Remove(lru.Value.Key);
+ _evictedCount++;
+ if (DisposeOnEvict)
+ {
+ DisposeFrames(lru.Value.Value);
+ }
+ }
+ }
+
+ private static void DisposeFrames(AnimationFrame[] frames)
+ {
+ if (frames == null)
+ {
+ return;
+ }
+ foreach (var frame in frames)
+ {
+ if (frame != null && !ReferenceEquals(frame, AnimationFrame.Empty))
+ {
+ frame.Bitmap?.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/Ultima/Caching/LruBitmapCache.cs b/Ultima/Caching/LruBitmapCache.cs
index 73f9508..1a927ec 100644
--- a/Ultima/Caching/LruBitmapCache.cs
+++ b/Ultima/Caching/LruBitmapCache.cs
@@ -10,6 +10,7 @@
using System;
using System.Collections.Generic;
using System.Drawing;
+using System.Threading;
namespace Ultima.Caching
{
@@ -35,7 +36,7 @@ namespace Ultima.Caching
///
public sealed class LruBitmapCache : IDisposable
{
- private readonly object _lock = new object();
+ private readonly Lock _lock = new();
private readonly LinkedList> _list =
new LinkedList>();
private readonly Dictionary>> _map;
diff --git a/Ultima/Files.cs b/Ultima/Files.cs
index 89e1ee2..5e9a0e3 100644
--- a/Ultima/Files.cs
+++ b/Ultima/Files.cs
@@ -38,6 +38,16 @@ public static void FireFileSaveEvent()
///
public static int CacheCapacityGumps { get; set; } = 2048;
+ ///
+ /// Initial LRU capacity for the Animations frame cache (the only major
+ /// file format previously without a decode cache). Counts whole
+ /// AnimationFrame[] entries — thumbnails are 1 frame, player directions
+ /// a handful. Default 1024 keeps the visible grid + scroll working set
+ /// warm. Adjust via at
+ /// runtime.
+ ///
+ public static int CacheCapacityAnimations { get; set; } = 1024;
+
///
/// Contains the path infos
///
diff --git a/Ultima/Gumps.cs b/Ultima/Gumps.cs
index 6ae188d..a075640 100644
--- a/Ultima/Gumps.cs
+++ b/Ultima/Gumps.cs
@@ -878,7 +878,7 @@ public static void PreloadParallel(int parallelism, Action progressCallback
int done = 0;
int reportEvery = Math.Max(1, total / 200);
int nextReport = reportEvery;
- object reportLock = new object();
+ Lock reportLock = new();
var options = new ParallelOptions { MaxDegreeOfParallelism = parallelism };
diff --git a/UoFiddler.Controls/UserControls/AnimDataControl.cs b/UoFiddler.Controls/UserControls/AnimDataControl.cs
index 4b1701f..7e0ad8c 100644
--- a/UoFiddler.Controls/UserControls/AnimDataControl.cs
+++ b/UoFiddler.Controls/UserControls/AnimDataControl.cs
@@ -146,6 +146,12 @@ private void SetPicture()
Hue hueObject = Hues.List[_customHue - 1];
hueObject.ApplyTo(frame, _hueOnlyGray);
}
+ else
+ {
+ // Art.GetStatic returns cache-owned bitmaps; clone so the
+ // picture box can own and dispose its frames safely.
+ frame = new Bitmap(frame);
+ }
frames.Add(new AnimatedFrame(frame, center));
}
diff --git a/UoFiddler.Controls/UserControls/AnimatedPictureBox.cs b/UoFiddler.Controls/UserControls/AnimatedPictureBox.cs
index 5052408..1e70c2c 100644
--- a/UoFiddler.Controls/UserControls/AnimatedPictureBox.cs
+++ b/UoFiddler.Controls/UserControls/AnimatedPictureBox.cs
@@ -41,6 +41,14 @@ public List Frames
get => _frames;
set
{
+ if (!ReferenceEquals(value, _frames) && _frames != null)
+ {
+ foreach (var frame in _frames)
+ {
+ frame.Bitmap?.Dispose();
+ }
+ }
+
_frameIndex = 0;
_frames = value ?? [];
diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs
index 198ffc5..9773f0b 100644
--- a/UoFiddler.Controls/UserControls/AnimationListControl.cs
+++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs
@@ -318,8 +318,10 @@ private void SetPicture()
int hue = _customHue;
bool preserveHue = hue != 0;
+ // GetAnimation returns cache-owned bitmaps; clone them so the
+ // picture box can own and dispose its frames without corrupting the cache.
MainPictureBox.Frames = Animations.GetAnimation(_currentSelect, _currentSelectAction, _facing, ref hue, preserveHue, false)
- ?.Select(animation => new AnimatedFrame(animation.Bitmap, animation.Center)).ToList();
+ ?.Select(animation => new AnimatedFrame(new Bitmap(animation.Bitmap), animation.Center)).ToList();
if (!preserveHue)
{
@@ -611,6 +613,7 @@ private void ListViewDrawItem(object sender, TileViewControl.DrawTileListItemEve
}
int hue = 0;
+ // Cache-owned bitmap — borrowed for drawing only, never disposed here.
Bitmap bmp = Animations.GetAnimation(graphic, 0, 1, ref hue, false, true)?[0].Bitmap;
if (bmp != null)
{
diff --git a/UoFiddler.Controls/UserControls/VerdataControl.cs b/UoFiddler.Controls/UserControls/VerdataControl.cs
index 557971e..65d3f7c 100644
--- a/UoFiddler.Controls/UserControls/VerdataControl.cs
+++ b/UoFiddler.Controls/UserControls/VerdataControl.cs
@@ -322,8 +322,10 @@ private void LoadAnimationFrames()
int dir = trackBarDirection.Value;
int hue = 0;
+ // GetAnimation returns cache-owned bitmaps; clone so the picture box
+ // can own and dispose its frames without corrupting the cache.
var frames = Animations.GetAnimation(_currentAnimBody, action, dir, ref hue, false, false)
- ?.Select(f => new AnimatedFrame(f.Bitmap, f.Center)).ToList();
+ ?.Select(f => new AnimatedFrame(new Bitmap(f.Bitmap), f.Center)).ToList();
bool wasAnimating = animatedPictureBox.Animate;
animatedPictureBox.Animate = false;
From 541960c679db3ee9eedf78d147f584a4fed25805 Mon Sep 17 00:00:00 2001
From: AsY!um- <377468+AsYlum-@users.noreply.github.com>
Date: Wed, 27 May 2026 00:21:48 +0200
Subject: [PATCH 21/21] Fix some GDI leaks.
---
Ultima/Animations.cs | 4 ++--
.../Classes/AnimatedFrameListExtensions.cs | 4 ++--
UoFiddler.Controls/Forms/AnimationEditForm.cs | 10 +++++---
.../UserControls/AnimatedPictureBox.cs | 3 ++-
.../UserControls/AnimationListControl.cs | 8 ++++---
.../UserControls/ItemsControl.cs | 7 +++---
.../UserControls/LandTilesControl.cs | 7 +++---
UoFiddler.Controls/UserControls/MapControl.cs | 24 +++++++------------
.../UserControls/RadarColorControl.cs | 3 ++-
.../UserControls/SoundsControl.Designer.cs | 5 ++--
.../UserControls/SoundsControl.cs | 9 ++++++-
.../UserControls/TexturesControl.cs | 7 +++---
.../UserControls/TileView/TileViewControl.cs | 9 +++++++
.../UserControls/CompareAnimDataControl.cs | 3 ++-
.../UserControls/CompareGumpControl.cs | 3 ++-
.../UserControls/CompareItemControl.cs | 3 ++-
.../UserControls/CompareLandControl.cs | 3 ++-
.../CompareMapControl.Designer.cs | 9 +++++++
.../UserControls/CompareRadarColControl.cs | 3 ++-
.../UserControls/CompareTextureControl.cs | 3 ++-
.../UserControls/CompareTileDataControl.cs | 6 +++--
.../UserControls/MultiEditorControl.cs | 2 +-
22 files changed, 84 insertions(+), 51 deletions(-)
diff --git a/Ultima/Animations.cs b/Ultima/Animations.cs
index 7cfe2c2..052dddd 100644
--- a/Ultima/Animations.cs
+++ b/Ultima/Animations.cs
@@ -7,7 +7,7 @@ namespace Ultima
{
public static class Animations
{
- public const int _maxAnimationValue = 2048; // bodyconv.def says it's maximum animation value so max bodyId?
+ public const int MaxAnimationValue = 2048; // bodyconv.def says it's maximum animation value so max bodyId?
public static readonly int PaletteCapacity = 0x100;
// LRU decode cache shared by the MUL and UOP paths. Bitmaps it returns
@@ -301,7 +301,7 @@ public static void Translate(ref int body, ref int hue)
private static void LoadTable()
{
- _table = new int[_maxAnimationValue + 1];
+ _table = new int[MaxAnimationValue + 1];
for (int i = 0; i < _table.Length; ++i)
{
diff --git a/UoFiddler.Controls/Classes/AnimatedFrameListExtensions.cs b/UoFiddler.Controls/Classes/AnimatedFrameListExtensions.cs
index fc723c5..bfa01ca 100644
--- a/UoFiddler.Controls/Classes/AnimatedFrameListExtensions.cs
+++ b/UoFiddler.Controls/Classes/AnimatedFrameListExtensions.cs
@@ -63,8 +63,8 @@ public static void ToGif(this IEnumerable frames, string outputFi
if (showFrameBounds)
{
- g.FillRectangle(new SolidBrush(Color.Red), new Rectangle(drawCenter, new Size(3, 3)));
- g.DrawRectangle(new Pen(Color.Red), new Rectangle(location, new Size(frame.Bitmap.Width - 1, frame.Bitmap.Height - 1)));
+ g.FillRectangle(Brushes.Red, new Rectangle(drawCenter, new Size(3, 3)));
+ g.DrawRectangle(Pens.Red, new Rectangle(location, new Size(frame.Bitmap.Width - 1, frame.Bitmap.Height - 1)));
}
gif.AddFrame(target, delay: -1, quality: GifQuality.Bit8);
diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs
index 8120c3e..e080b0d 100644
--- a/UoFiddler.Controls/Forms/AnimationEditForm.cs
+++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs
@@ -463,8 +463,9 @@ private void GalleryTileViewDrawItem(object sender, UoFiddler.Controls.UserContr
int body = _galleryBodies[e.Index];
Point itemPoint = new Point(e.Bounds.X + GalleryTileView.TilePadding.Left, e.Bounds.Y + GalleryTileView.TilePadding.Top);
Rectangle tileRect = new Rectangle(itemPoint, GalleryTileView.TileSize);
- var previousClip = e.Graphics.Clip;
- e.Graphics.Clip = new Region(tileRect);
+ using var previousClip = e.Graphics.Clip;
+ using var clipRegion = new Region(tileRect);
+ e.Graphics.Clip = clipRegion;
if (!GalleryTileView.SelectedIndices.Contains(e.Index))
{
@@ -643,7 +644,10 @@ private void DrawFrameItem(object sender, DrawListViewItemEventArgs e)
Bitmap[] currentBits = edit.GetFrames();
Bitmap bmp = currentBits[(int)e.Item.Tag];
var penColor = FramesListView.SelectedItems.Contains(e.Item) ? Color.Red : Color.Gray;
- e.Graphics.DrawRectangle(new Pen(penColor), e.Bounds.X, e.Bounds.Y, e.Bounds.Width, e.Bounds.Height);
+ using (var borderPen = new Pen(penColor))
+ {
+ e.Graphics.DrawRectangle(borderPen, e.Bounds.X, e.Bounds.Y, e.Bounds.Width, e.Bounds.Height);
+ }
e.Graphics.DrawImage(bmp, e.Bounds.X, e.Bounds.Y, bmp.Width, bmp.Height);
e.DrawText(TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter);
}
diff --git a/UoFiddler.Controls/UserControls/AnimatedPictureBox.cs b/UoFiddler.Controls/UserControls/AnimatedPictureBox.cs
index 1e70c2c..b3b75a8 100644
--- a/UoFiddler.Controls/UserControls/AnimatedPictureBox.cs
+++ b/UoFiddler.Controls/UserControls/AnimatedPictureBox.cs
@@ -236,7 +236,8 @@ protected override void OnPaint(PaintEventArgs e)
if (_showFrameBounds)
{
- e.Graphics.DrawRectangle(new Pen(Color.Red), new Rectangle(location, frame.Bitmap.Size));
+ using var boundsPen = new Pen(Color.Red);
+ e.Graphics.DrawRectangle(boundsPen, new Rectangle(location, frame.Bitmap.Size));
}
}
diff --git a/UoFiddler.Controls/UserControls/AnimationListControl.cs b/UoFiddler.Controls/UserControls/AnimationListControl.cs
index 9773f0b..1d22e42 100644
--- a/UoFiddler.Controls/UserControls/AnimationListControl.cs
+++ b/UoFiddler.Controls/UserControls/AnimationListControl.cs
@@ -603,8 +603,9 @@ private void ListViewDrawItem(object sender, TileViewControl.DrawTileListItemEve
int graphic = _listViewGraphics[e.Index];
Point itemPoint = new Point(e.Bounds.X + listView.TilePadding.Left, e.Bounds.Y + listView.TilePadding.Top);
Rectangle tileRect = new Rectangle(itemPoint, listView.TileSize);
- var previousClip = e.Graphics.Clip;
- e.Graphics.Clip = new Region(tileRect);
+ using var previousClip = e.Graphics.Clip;
+ using var clipRegion = new Region(tileRect);
+ e.Graphics.Clip = clipRegion;
if (!listView.SelectedIndices.Contains(e.Index))
{
@@ -709,7 +710,8 @@ private void Frames_ListView_DrawItem(object sender, DrawListViewItemEventArgs e
if (listView1.SelectedItems.Contains(e.Item))
{
- e.Graphics.FillRectangle(new SolidBrush(SystemColors.Highlight), e.Bounds);
+ using var highlightBrush = new SolidBrush(SystemColors.Highlight);
+ e.Graphics.FillRectangle(highlightBrush, e.Bounds);
}
e.Graphics.DrawImage(bmp, e.Bounds.X, e.Bounds.Y, width, height);
diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs
index 18d6252..b5a4069 100644
--- a/UoFiddler.Controls/UserControls/ItemsControl.cs
+++ b/UoFiddler.Controls/UserControls/ItemsControl.cs
@@ -1046,9 +1046,10 @@ private void ItemsTileView_DrawItem(object sender, TileViewControl.DrawTileListI
Rectangle rect = new Rectangle(itemPoint, ItemsTileView.TileSize);
- var previousClip = e.Graphics.Clip;
+ using var previousClip = e.Graphics.Clip;
- e.Graphics.Clip = new Region(rect);
+ using var clipRegion = new Region(rect);
+ e.Graphics.Clip = clipRegion;
var selected = ItemsTileView.SelectedIndices.Contains(e.Index);
if (!selected)
@@ -1059,8 +1060,6 @@ private void ItemsTileView_DrawItem(object sender, TileViewControl.DrawTileListI
var bitmap = Art.GetStatic(_itemList[e.Index], out bool patched);
if (bitmap == null)
{
- e.Graphics.Clip = new Region(rect);
-
rect.X += 5;
rect.Y += 5;
diff --git a/UoFiddler.Controls/UserControls/LandTilesControl.cs b/UoFiddler.Controls/UserControls/LandTilesControl.cs
index 0f49d91..8a16c8a 100644
--- a/UoFiddler.Controls/UserControls/LandTilesControl.cs
+++ b/UoFiddler.Controls/UserControls/LandTilesControl.cs
@@ -848,9 +848,10 @@ private void LandTilesTileView_DrawItem(object sender, TileView.TileViewControl.
Size itemSize = new Size(fixedTileSize, fixedTileSize);
Rectangle itemRec = new Rectangle(itemPoint, itemSize);
- var previousClip = e.Graphics.Clip;
+ using var previousClip = e.Graphics.Clip;
- e.Graphics.Clip = new Region(itemRec);
+ using var clipRegion = new Region(itemRec);
+ e.Graphics.Clip = clipRegion;
var selected = LandTilesTileView.SelectedIndices.Contains(e.Index);
if (!selected)
@@ -862,8 +863,6 @@ private void LandTilesTileView_DrawItem(object sender, TileView.TileViewControl.
if (bitmap == null)
{
- e.Graphics.Clip = new Region(itemRec);
-
itemRec.X += 5;
itemRec.Y += 5;
diff --git a/UoFiddler.Controls/UserControls/MapControl.cs b/UoFiddler.Controls/UserControls/MapControl.cs
index eaaefae..712013e 100644
--- a/UoFiddler.Controls/UserControls/MapControl.cs
+++ b/UoFiddler.Controls/UserControls/MapControl.cs
@@ -1011,23 +1011,16 @@ private void ExtractMapImage(ImageFormat imageFormat)
using (new WaitCursorScope(this))
{
- Bitmap extract;
-
// Use altitude-aware rendering if mode is not Normal
- if (_altitudeMode != MapAltitudeMode.Normal)
- {
- extract = CurrentMap.GetImageWithAltitude(0, 0, CurrentMap.Width >> 3, CurrentMap.Height >> 3,
- showStaticsToolStripMenuItem1.Checked, _altitudeMode);
- }
- else
- {
- extract = CurrentMap.GetImage(0, 0, CurrentMap.Width >> 3, CurrentMap.Height >> 3,
+ using Bitmap extract = _altitudeMode != MapAltitudeMode.Normal
+ ? CurrentMap.GetImageWithAltitude(0, 0, CurrentMap.Width >> 3, CurrentMap.Height >> 3,
+ showStaticsToolStripMenuItem1.Checked, _altitudeMode)
+ : CurrentMap.GetImage(0, 0, CurrentMap.Width >> 3, CurrentMap.Height >> 3,
showStaticsToolStripMenuItem1.Checked);
- }
if (showMarkersToolStripMenuItem.Checked)
{
- Graphics g = Graphics.FromImage(extract);
+ using Graphics g = Graphics.FromImage(extract);
foreach (TreeNode obj in OverlayObjectTree.Nodes[_currentMapId].Nodes)
{
OverlayObject o = (OverlayObject)obj.Tag;
@@ -1684,7 +1677,10 @@ public class OverlayCursor : OverlayObject, IDisposable
private readonly Color _col;
private readonly Pen _pen;
private readonly Brush _brush;
- private static Brush _background;
+ // Shared, immutable label backdrop — created once and never disposed
+ // per-instance (it was previously static yet reallocated/disposed per
+ // marker, which leaked brushes and could dispose one still in use).
+ private static readonly Brush _background = new SolidBrush(Color.FromArgb(100, Color.White));
public OverlayCursor(Point location, int m, string t, Color c)
{
@@ -1695,7 +1691,6 @@ public OverlayCursor(Point location, int m, string t, Color c)
Visible = true;
_brush = new SolidBrush(_col);
_pen = new Pen(_brush);
- _background = new SolidBrush(Color.FromArgb(100, Color.White));
}
public override bool IsVisible(Rectangle bounds, int m, int hScrollBar, int vScrollBar, double zoom)
@@ -1747,7 +1742,6 @@ public void Dispose()
{
_pen?.Dispose();
_brush?.Dispose();
- _background?.Dispose();
}
}
}
diff --git a/UoFiddler.Controls/UserControls/RadarColorControl.cs b/UoFiddler.Controls/UserControls/RadarColorControl.cs
index e13d811..7ba310b 100644
--- a/UoFiddler.Controls/UserControls/RadarColorControl.cs
+++ b/UoFiddler.Controls/UserControls/RadarColorControl.cs
@@ -186,7 +186,8 @@ private static void DrawRow(TileView.TileViewControl.DrawTileListItemEventArgs e
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
// Color swatch — a small filled rectangle showing this row's radar color.
diff --git a/UoFiddler.Controls/UserControls/SoundsControl.Designer.cs b/UoFiddler.Controls/UserControls/SoundsControl.Designer.cs
index 370fff8..173a64e 100644
--- a/UoFiddler.Controls/UserControls/SoundsControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/SoundsControl.Designer.cs
@@ -24,9 +24,10 @@ partial class SoundsControl
/// true if managed resources should be disposed; otherwise, false.
protected override void Dispose(bool disposing)
{
- if (disposing && (components != null))
+ if (disposing)
{
- components.Dispose();
+ components?.Dispose();
+ _underlineFont?.Dispose();
}
base.Dispose(disposing);
}
diff --git a/UoFiddler.Controls/UserControls/SoundsControl.cs b/UoFiddler.Controls/UserControls/SoundsControl.cs
index 4302bc0..612b360 100644
--- a/UoFiddler.Controls/UserControls/SoundsControl.cs
+++ b/UoFiddler.Controls/UserControls/SoundsControl.cs
@@ -37,6 +37,10 @@ public partial class SoundsControl : UserControl
private int _soundIdOffset;
+ // Shared underline font for translated entries — one instance reused
+ // across all list items, recreated each reload (the control Font may change).
+ private Font _underlineFont;
+
public SoundsControl()
{
InitializeComponent();
@@ -95,6 +99,9 @@ private void OnLoad(object sender, EventArgs e)
_soundIdOffset = GetSoundIdOffset();
+ _underlineFont?.Dispose();
+ _underlineFont = new Font(Font, FontStyle.Underline);
+
var cache = new List();
for (int i = 0; i < _soundsLength; ++i)
{
@@ -105,7 +112,7 @@ private void OnLoad(object sender, EventArgs e)
if (translated)
{
item.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
- item.Font = new Font(Font, FontStyle.Underline);
+ item.Font = _underlineFont;
}
cache.Add(item);
diff --git a/UoFiddler.Controls/UserControls/TexturesControl.cs b/UoFiddler.Controls/UserControls/TexturesControl.cs
index 19d9b08..21ad321 100644
--- a/UoFiddler.Controls/UserControls/TexturesControl.cs
+++ b/UoFiddler.Controls/UserControls/TexturesControl.cs
@@ -657,16 +657,15 @@ private void TextureTileView_DrawItem(object sender, TileView.TileViewControl.Dr
Size defaultTileSize = new Size(defaultTileWidth, defaultTileWidth);
Rectangle tileRectangle = new Rectangle(itemPoint, defaultTileSize);
- var previousClip = e.Graphics.Clip;
+ using var previousClip = e.Graphics.Clip;
- e.Graphics.Clip = new Region(tileRectangle);
+ using var clipRegion = new Region(tileRectangle);
+ e.Graphics.Clip = clipRegion;
Bitmap bitmap = Textures.GetTexture(_textureList[e.Index], out bool patched);
if (bitmap == null)
{
- e.Graphics.Clip = new Region(tileRectangle);
-
tileRectangle.X += 5;
tileRectangle.Y += 5;
diff --git a/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs b/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
index 7f1977f..a286d96 100644
--- a/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
+++ b/UoFiddler.Controls/UserControls/TileView/TileViewControl.cs
@@ -881,6 +881,15 @@ protected override void OnPaint(PaintEventArgs e)
}
}
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _tileBorder?.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
public class ListViewFocusedItemSelectionChangedEventArgs : EventArgs
{
public int FocusedItemIndex { get; }
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
index bf3c953..f9a542a 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareAnimDataControl.cs
@@ -150,7 +150,8 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int id)
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
Brush fontBrush = focused
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
index 205b569..b615b31 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareGumpControl.cs
@@ -185,7 +185,8 @@ private void DrawGumpItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
bool valid = isSecondary ? SecondGump.IsValidIndex(i) : Gumps.IsValidIndex(i);
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
index 815db4d..4263f5e 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareItemControl.cs
@@ -180,7 +180,8 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
Brush fontBrush = Brushes.Gray;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
index 3201145..3f21a7c 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareLandControl.cs
@@ -179,7 +179,8 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
Brush fontBrush = Brushes.Gray;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.Designer.cs b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.Designer.cs
index 36addf5..2825a76 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.Designer.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareMapControl.Designer.cs
@@ -28,6 +28,15 @@ protected override void Dispose(bool disposing)
{
components.Dispose();
}
+ if (disposing)
+ {
+ _zoomBufferGraphics?.Dispose();
+ _zoomBuffer?.Dispose();
+ _renderBuffer?.Dispose();
+ _zoomBufferGraphics = null;
+ _zoomBuffer = null;
+ _renderBuffer = null;
+ }
base.Dispose(disposing);
}
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
index 8d76936..4a0738c 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareRadarColControl.cs
@@ -248,7 +248,8 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int idx,
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
// Color swatch.
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
index 99489f8..eefaf6d 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTextureControl.cs
@@ -161,7 +161,8 @@ private void DrawListItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
Brush fontBrush = Brushes.Gray;
diff --git a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
index 74f0ae3..0586d9b 100644
--- a/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
+++ b/UoFiddler.Plugin.Compare/UserControls/CompareTileDataControl.cs
@@ -741,7 +741,8 @@ private void DrawLandItem(TileViewControl.DrawTileListItemEventArgs e, int i, bo
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
Brush brush = focused ? CompareColors.ContrastBrush(Options.TileSelectionColor) : GetLandBrush(i, isSecondary);
@@ -824,7 +825,8 @@ private void DrawItemEntry(TileViewControl.DrawTileListItemEventArgs e, int i, b
}
else
{
- e.Graphics.FillRectangle(new SolidBrush(e.BackColor), e.Bounds);
+ using var backBrush = new SolidBrush(e.BackColor);
+ e.Graphics.FillRectangle(backBrush, e.Bounds);
}
Brush brush = focused ? CompareColors.ContrastBrush(Options.TileSelectionColor) : GetItemBrush(i);
diff --git a/UoFiddler.Plugin.MultiEditor/UserControls/MultiEditorControl.cs b/UoFiddler.Plugin.MultiEditor/UserControls/MultiEditorControl.cs
index 313bb09..720b9fd 100644
--- a/UoFiddler.Plugin.MultiEditor/UserControls/MultiEditorControl.cs
+++ b/UoFiddler.Plugin.MultiEditor/UserControls/MultiEditorControl.cs
@@ -2033,7 +2033,7 @@ private void PictureBoxDrawTiles_OnPaint(object sender, PaintEventArgs e)
Size size = new Size(_drawTileSizeWidth - 1, _drawTileSizeHeight - 1);
Rectangle rect = new Rectangle(loc, size);
- e.Graphics.Clip = new Region(rect);
+ e.Graphics.SetClip(rect);
if (index == _drawTile.Id)
{