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) {