diff --git a/Ultima/SkillGroups.cs b/Ultima/SkillGroups.cs
index fad1c120..f28b03f3 100644
--- a/Ultima/SkillGroups.cs
+++ b/Ultima/SkillGroups.cs
@@ -15,12 +15,47 @@ public SkillGroup(string name)
}
}
+ ///
+ /// skillgrp.mul: a group count, the names of groups 1..count-1 (group 0, Misc, has no stored name),
+ /// then one int32 group id per skill. The order of skills inside a group is not stored.
+ ///
public sealed class SkillGroups
{
+ ///
+ /// Skill entries the client reads and writes, whatever skills.idx holds (measured on client 7.0.117.0).
+ /// Skills past this are not shown in the client's skill groups.
+ ///
+ public const int ClientSkillLimit = 58;
+
+ ///
+ /// Groups the client allows, Misc included. Its loader does not check this, so a file with more
+ /// groups overruns the client's name table (measured on client 7.0.117.0).
+ ///
+ public const int ClientGroupLimit = 12;
+
+ ///
+ /// Longest group name: names are 17-character slots that the client reads up to a NUL.
+ ///
+ public const int MaxNameLength = 16;
+
+ private const int _nameSlot = 17;
+
+ ///
+ /// Groups in file order; index 0 is always Misc.
+ ///
public static List List { get; private set; }
+
+ ///
+ /// Group index of each skill, by skill id.
+ ///
public static List SkillList { get; private set; }
- private static bool _unicode;
+ ///
+ /// True when the file stores group names in 16-bit slots (count preceded by -1). This is not Unicode
+ /// support: the client keeps only the low byte of each character and saves the file back as 8-bit.
+ /// Names should be ASCII in either form.
+ ///
+ public static bool Unicode { get; private set; }
static SkillGroups()
{
@@ -31,8 +66,9 @@ public static void Initialize()
{
string path = Files.GetFilePath("skillgrp.mul");
- List = new List();
+ List = new List { new SkillGroup("Misc") };
SkillList = new List();
+ Unicode = false;
if (path == null)
{
@@ -42,58 +78,60 @@ public static void Initialize()
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var bin = new BinaryReader(fs))
{
+ if (fs.Length < 4)
+ {
+ return;
+ }
+
int start = 4;
- int strLen = 17;
int count = bin.ReadInt32();
if (count == -1)
{
- _unicode = true;
+ Unicode = true;
count = bin.ReadInt32();
- start *= 2;
- strLen *= 2;
+ start = 8;
}
- List.Add(new SkillGroup("Misc"));
+ int charSize = Unicode ? 2 : 1;
+ int slotSize = _nameSlot * charSize;
+ int names = Math.Max(0, count - 1);
- for (int i = 0; i < count - 1; ++i)
+ for (int i = 0; i < names; ++i)
{
- int strBuild;
-
- fs.Seek(start + (i * strLen), SeekOrigin.Begin);
-
- var builder2 = new StringBuilder(17);
- if (_unicode)
+ long offset = start + ((long)i * slotSize);
+ if (offset + slotSize > fs.Length)
{
- while ((strBuild = bin.ReadInt16()) != 0)
- {
- builder2.Append((char)strBuild);
- }
+ break;
}
- else
+
+ fs.Seek(offset, SeekOrigin.Begin);
+
+ var builder = new StringBuilder(_nameSlot);
+ for (int c = 0; c < _nameSlot; ++c)
{
- while ((strBuild = bin.ReadByte()) != 0)
+ int ch = Unicode ? bin.ReadUInt16() : bin.ReadByte();
+ if (ch == 0)
{
- builder2.Append((char)strBuild);
+ break;
}
+
+ builder.Append((char)ch);
}
- List.Add(new SkillGroup(builder2.ToString()));
+ List.Add(new SkillGroup(builder.ToString()));
}
- fs.Seek((start + ((count - 1) * strLen)), SeekOrigin.Begin);
-
- try
+ long skillsStart = start + ((long)names * slotSize);
+ if (skillsStart >= fs.Length)
{
- while (bin.BaseStream.Length != bin.BaseStream.Position)
- {
- SkillList.Add(bin.ReadInt32());
- }
+ return;
}
- catch // just for safety
+
+ fs.Seek(skillsStart, SeekOrigin.Begin);
+ while (fs.Length - fs.Position >= 4)
{
- // TODO: ignored?
- // ignored
+ SkillList.Add(bin.ReadInt32());
}
}
}
@@ -105,47 +143,17 @@ public static void Save(string path)
using (var fs = new FileStream(mul, FileMode.Create, FileAccess.Write, FileShare.Write))
using (var bin = new BinaryWriter(fs))
{
- if (_unicode)
+ if (Unicode)
{
bin.Write(-1);
}
bin.Write(List.Count);
- foreach (SkillGroup group in List)
+ // Group 0 is Misc and has no stored name
+ for (int i = 1; i < List.Count; ++i)
{
- if (group.Name == "Misc")
- {
- continue;
- }
-
- byte[] name = _unicode ? new byte[34] : new byte[17];
-
- if (group.Name != null)
- {
- if (_unicode)
- {
- byte[] bb = Encoding.Unicode.GetBytes(group.Name);
- if (bb.Length > 34)
- {
- Array.Resize(ref bb, 34);
- }
-
- bb.CopyTo(name, 0);
- }
- else
- {
- byte[] bb = Encoding.ASCII.GetBytes(group.Name);
- if (bb.Length > 17)
- {
- Array.Resize(ref bb, 17);
- }
-
- bb.CopyTo(name, 0);
- }
- }
-
- bin.Write(name);
+ bin.Write(EncodeName(List[i].Name));
}
foreach (int group in SkillList)
@@ -154,5 +162,25 @@ public static void Save(string path)
}
}
}
+
+ private static byte[] EncodeName(string name)
+ {
+ var slot = new byte[_nameSlot * (Unicode ? 2 : 1)];
+ if (string.IsNullOrEmpty(name))
+ {
+ return slot;
+ }
+
+ if (name.Length > MaxNameLength)
+ {
+ name = name.Substring(0, MaxNameLength);
+ }
+
+ // 8-bit names are written back byte for byte as loaded (Latin-1)
+ byte[] bytes = Unicode ? Encoding.Unicode.GetBytes(name) : Encoding.Latin1.GetBytes(name);
+ bytes.CopyTo(slot, 0);
+
+ return slot;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Ultima/Skills.cs b/Ultima/Skills.cs
index 96abdb98..8d13a160 100644
--- a/Ultima/Skills.cs
+++ b/Ultima/Skills.cs
@@ -92,7 +92,8 @@ private static string ReadNameString(BinaryReader bin, int length)
//;
}
- return Encoding.ASCII.GetString(_stringBuffer, 0, count);
+ // Names are single bytes; Latin-1 keeps every byte as loaded so saving writes it back unchanged
+ return Encoding.Latin1.GetString(_stringBuffer, 0, count);
}
public static void Save(string path)
@@ -120,7 +121,7 @@ public static void Save(string path)
var length = (int)fsmul.Position;
binmul.Write(skill.IsAction);
- byte[] nameBytes = Encoding.ASCII.GetBytes(skill.Name);
+ byte[] nameBytes = Encoding.Latin1.GetBytes(skill.Name);
binmul.Write(nameBytes);
binmul.Write((byte)0); // null terminated
diff --git a/UoFiddler.Controls/UserControls/SkillGroupControl.Designer.cs b/UoFiddler.Controls/UserControls/SkillGroupControl.Designer.cs
index 4ff2f292..02c54880 100644
--- a/UoFiddler.Controls/UserControls/SkillGroupControl.Designer.cs
+++ b/UoFiddler.Controls/UserControls/SkillGroupControl.Designer.cs
@@ -44,11 +44,10 @@ private void InitializeComponent()
this.addToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.renameToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.toolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
- this.SkillIDTextBox = new System.Windows.Forms.ToolStripTextBox();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.treeView1 = new System.Windows.Forms.TreeView();
+ this.StatusLabel = new System.Windows.Forms.Label();
this.contextMenuStrip1.SuspendLayout();
this.SuspendLayout();
//
@@ -58,57 +57,42 @@ private void InitializeComponent()
this.addToolStripMenuItem,
this.removeToolStripMenuItem,
this.renameToolStripMenuItem,
- this.toolStripMenuItem1,
this.toolStripSeparator1,
this.saveToolStripMenuItem});
this.contextMenuStrip1.Name = "contextMenuStrip1";
- this.contextMenuStrip1.Size = new System.Drawing.Size(118, 120);
+ this.contextMenuStrip1.Size = new System.Drawing.Size(158, 98);
this.contextMenuStrip1.Opening += new System.ComponentModel.CancelEventHandler(this.OnOpeningContext);
//
// addToolStripMenuItem
//
this.addToolStripMenuItem.Name = "addToolStripMenuItem";
- this.addToolStripMenuItem.Size = new System.Drawing.Size(117, 22);
- this.addToolStripMenuItem.Text = "Add";
+ this.addToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
+ this.addToolStripMenuItem.Text = "Add Group";
this.addToolStripMenuItem.Click += new System.EventHandler(this.OnClickAdd);
//
// removeToolStripMenuItem
//
this.removeToolStripMenuItem.Name = "removeToolStripMenuItem";
- this.removeToolStripMenuItem.Size = new System.Drawing.Size(117, 22);
- this.removeToolStripMenuItem.Text = "Remove";
+ this.removeToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
+ this.removeToolStripMenuItem.Text = "Remove Group";
this.removeToolStripMenuItem.Click += new System.EventHandler(this.OnClickRemove);
//
// renameToolStripMenuItem
//
this.renameToolStripMenuItem.Name = "renameToolStripMenuItem";
- this.renameToolStripMenuItem.Size = new System.Drawing.Size(117, 22);
- this.renameToolStripMenuItem.Text = "Rename";
+ this.renameToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
+ this.renameToolStripMenuItem.Text = "Rename Group";
this.renameToolStripMenuItem.Click += new System.EventHandler(this.OnClickRename);
//
- // toolStripMenuItem1
- //
- this.toolStripMenuItem1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.SkillIDTextBox});
- this.toolStripMenuItem1.Name = "toolStripMenuItem1";
- this.toolStripMenuItem1.Size = new System.Drawing.Size(117, 22);
- this.toolStripMenuItem1.Text = "SkillID";
- //
- // SkillIDTextBox
- //
- this.SkillIDTextBox.Name = "SkillIDTextBox";
- this.SkillIDTextBox.Size = new System.Drawing.Size(100, 23);
- this.SkillIDTextBox.KeyDown += new System.Windows.Forms.KeyEventHandler(this.OnKeyDownSkillID);
- //
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
- this.toolStripSeparator1.Size = new System.Drawing.Size(114, 6);
+ this.toolStripSeparator1.Size = new System.Drawing.Size(154, 6);
//
// saveToolStripMenuItem
//
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem";
- this.saveToolStripMenuItem.Size = new System.Drawing.Size(117, 22);
+ this.saveToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.saveToolStripMenuItem.Text = "Save";
this.saveToolStripMenuItem.Click += new System.EventHandler(this.OnClickSave);
//
@@ -121,18 +105,30 @@ private void InitializeComponent()
this.treeView1.Location = new System.Drawing.Point(0, 0);
this.treeView1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.treeView1.Name = "treeView1";
- this.treeView1.Size = new System.Drawing.Size(737, 386);
+ this.treeView1.ShowNodeToolTips = true;
+ this.treeView1.Size = new System.Drawing.Size(737, 326);
this.treeView1.TabIndex = 1;
this.treeView1.AfterLabelEdit += new System.Windows.Forms.NodeLabelEditEventHandler(this.AfterLabelEdit);
this.treeView1.ItemDrag += new System.Windows.Forms.ItemDragEventHandler(this.OnItemDrag);
+ this.treeView1.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.OnNodeMouseClick);
this.treeView1.DragDrop += new System.Windows.Forms.DragEventHandler(this.OnDragDrop);
- this.treeView1.DragEnter += new System.Windows.Forms.DragEventHandler(this.OnDragEnter);
+ this.treeView1.DragOver += new System.Windows.Forms.DragEventHandler(this.OnDragOver);
+ //
+ // StatusLabel
+ //
+ this.StatusLabel.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this.StatusLabel.Location = new System.Drawing.Point(0, 326);
+ this.StatusLabel.Name = "StatusLabel";
+ this.StatusLabel.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2);
+ this.StatusLabel.Size = new System.Drawing.Size(737, 60);
+ this.StatusLabel.TabIndex = 2;
//
// SkillGroupControl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.treeView1);
+ this.Controls.Add(this.StatusLabel);
this.DoubleBuffered = true;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "SkillGroupControl";
@@ -150,9 +146,8 @@ private void InitializeComponent()
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem renameToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem;
- private System.Windows.Forms.ToolStripTextBox SkillIDTextBox;
- private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem1;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.TreeView treeView1;
+ private System.Windows.Forms.Label StatusLabel;
}
}
diff --git a/UoFiddler.Controls/UserControls/SkillGroupControl.cs b/UoFiddler.Controls/UserControls/SkillGroupControl.cs
index aa593beb..ac0451fc 100644
--- a/UoFiddler.Controls/UserControls/SkillGroupControl.cs
+++ b/UoFiddler.Controls/UserControls/SkillGroupControl.cs
@@ -1,9 +1,9 @@
/***************************************************************************
*
* $Author: Turley
- *
+ *
* "THE BEER-WARE LICENSE"
- * As long as you retain this notice you can do whatever you want with
+ * 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.
*
@@ -12,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.Drawing;
+using System.Linq;
using System.Windows.Forms;
using Ultima;
using UoFiddler.Controls.Classes;
@@ -20,6 +21,10 @@
namespace UoFiddler.Controls.UserControls
{
+ ///
+ /// Edits skillgrp.mul as a tree: groups at the root (Misc first and fixed), every skill under exactly one group.
+ /// Skills come from the file and skills.idx; they can be moved between groups but not removed.
+ ///
public partial class SkillGroupControl : UserControl
{
public SkillGroupControl()
@@ -30,7 +35,9 @@ public SkillGroupControl()
}
private bool _loaded;
- private TreeNode _sourceNode;
+ private int _invalidGroupCount;
+
+ private static Color MiscColor => Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
///
/// ReLoads if loaded
@@ -54,51 +61,38 @@ private void OnLoad(object sender, EventArgs e)
{
Options.LoadedUltimaClass["SkillGrp"] = true;
- treeView1.BeginUpdate();
- treeView1.Nodes.Clear();
- List cache = new List();
-
+ var groupNodes = new List();
foreach (SkillGroup group in SkillGroups.List)
{
- TreeNode groupNode = new TreeNode
- {
- Text = group.Name
- };
+ groupNodes.Add(new TreeNode(group.Name));
+ }
- if (string.Equals("Misc", group.Name))
- {
- groupNode.ForeColor = Options.DarkMode ? Color.CornflowerBlue : Color.Blue;
- }
+ groupNodes[0].ForeColor = MiscColor;
+ groupNodes[0].ToolTipText = "Skills without a group. Misc is always the first group and can't be renamed or removed.";
- for (int i = 0; i < SkillGroups.SkillList.Count; ++i)
+ // Every skill gets a node: those in the file and those only in skills.idx
+ int entryCount = Math.Max(SkillGroups.SkillList.Count, Skills.SkillEntries.Count);
+ _invalidGroupCount = 0;
+ for (int id = 0; id < entryCount; ++id)
+ {
+ int group = id < SkillGroups.SkillList.Count ? SkillGroups.SkillList[id] : 0;
+ if (group < 0 || group >= groupNodes.Count)
{
- if (SkillGroups.SkillList[i] != cache.Count)
- {
- continue;
- }
-
- var skillInfo = Skills.GetSkill(i);
-
- if (skillInfo == null)
- {
- continue;
- }
-
- TreeNode skillNode = new TreeNode
- {
- Text = skillInfo.Name,
- Tag = i
- };
-
- groupNode.Nodes.Add(skillNode);
+ // Points at a group that doesn't exist; show it under Misc, where saving puts it
+ group = 0;
+ _invalidGroupCount++;
}
- cache.Add(groupNode);
+ groupNodes[group].Nodes.Add(CreateSkillNode(id));
}
- treeView1.Nodes.AddRange(cache.ToArray());
+ treeView1.BeginUpdate();
+ treeView1.Nodes.Clear();
+ treeView1.Nodes.AddRange(groupNodes.ToArray());
treeView1.EndUpdate();
+ UpdateStatus();
+
if (!_loaded)
{
ControlEvents.FilePathChangeEvent += OnFilePathChangeEvent;
@@ -113,228 +107,267 @@ private void OnFilePathChangeEvent()
Reload();
}
- private void OnClickSave(object sender, EventArgs e)
+ private static TreeNode CreateSkillNode(int id)
{
- SkillGroups.List.Clear();
- int skillCount = int.MinValue;
- foreach (TreeNode root in treeView1.Nodes)
+ string name = Skills.GetSkill(id)?.Name;
+ var node = new TreeNode(name == null ? $"{id}: (not in skills.idx)" : $"{id}: {name}")
{
- foreach (TreeNode child in root.Nodes)
- {
- int id = (int)child.Tag;
- if (id > skillCount)
- {
- skillCount = id;
- }
- }
+ Tag = id
+ };
+
+ if (id >= SkillGroups.ClientSkillLimit)
+ {
+ node.ForeColor = Color.Gray;
+ node.ToolTipText = $"The client only reads the first {SkillGroups.ClientSkillLimit} skill entries; this skill won't appear in its skill groups.";
}
- ++skillCount;
- SkillGroups.SkillList.Clear();
- for (int i = 0; i < skillCount; ++i)
+
+ return node;
+ }
+
+ private static bool IsSkill(TreeNode node) => node?.Tag is int;
+
+ private static bool IsMisc(TreeNode node) => node != null && node.Parent == null && node.Index == 0;
+
+ ///
+ /// Adds a skill to a group, keeping the group's skills in id order.
+ ///
+ private static void InsertSkill(TreeNode group, TreeNode skill)
+ {
+ int id = (int)skill.Tag;
+ int index = 0;
+ while (index < group.Nodes.Count && (int)group.Nodes[index].Tag < id)
+ {
+ index++;
+ }
+
+ group.Nodes.Insert(index, skill);
+ }
+
+ private void MarkChanged()
+ {
+ Options.ChangedUltimaClass["SkillGrp"] = true;
+ UpdateStatus();
+ }
+
+ private void UpdateStatus()
+ {
+ int groups = treeView1.Nodes.Count;
+ int entries = treeView1.Nodes.Cast().Sum(group => group.Nodes.Count);
+
+ var lines = new List
{
- SkillGroups.SkillList.Add(0);
+ $"{groups} groups including Misc (client limit {SkillGroups.ClientGroupLimit}). " +
+ $"{entries} skills, {Skills.SkillEntries.Count} in skills.idx; the client reads the first {SkillGroups.ClientSkillLimit}.",
+ };
+
+ bool warn = false;
+ if (groups > SkillGroups.ClientGroupLimit)
+ {
+ lines.Add($"More than {SkillGroups.ClientGroupLimit} groups: the client can't load this file safely.");
+ warn = true;
}
- foreach (TreeNode root in treeView1.Nodes)
+
+ if (_invalidGroupCount > 0)
+ {
+ lines.Add($"{_invalidGroupCount} skills pointed at a missing group and are shown under Misc.");
+ warn = true;
+ }
+
+ lines.Add(@"After replacing skillgrp.mul, delete the copies the client keeps in its Desktop\\\ and Desktop\common folders.");
+
+ StatusLabel.Text = string.Join(Environment.NewLine, lines);
+ StatusLabel.ForeColor = warn ? (Options.DarkMode ? Color.OrangeRed : Color.Red) : SystemColors.ControlText;
+ }
+
+ private void OnClickSave(object sender, EventArgs e)
+ {
+ int entryCount = Math.Max(SkillGroups.SkillList.Count,
+ treeView1.Nodes.Cast().SelectMany(group => group.Nodes.Cast()).Select(skill => (int)skill.Tag + 1).DefaultIfEmpty(0).Max());
+
+ var assignment = new int[entryCount];
+ SkillGroups.List.Clear();
+ foreach (TreeNode group in treeView1.Nodes)
{
- SkillGroups.List.Add(new SkillGroup(root.Text));
- foreach (TreeNode skill in root.Nodes)
+ SkillGroups.List.Add(new SkillGroup(group.Text));
+ foreach (TreeNode skill in group.Nodes)
{
- SkillGroups.SkillList[(int)skill.Tag] = root.Index;
+ assignment[(int)skill.Tag] = group.Index;
}
}
+
+ SkillGroups.SkillList.Clear();
+ SkillGroups.SkillList.AddRange(assignment);
SkillGroups.Save(Options.OutputPath);
+
+ _invalidGroupCount = 0;
Options.ChangedUltimaClass["SkillGrp"] = false;
+ UpdateStatus();
FileSavedDialog.Show(FindForm(), Options.OutputPath, "SkillGrp saved successfully.");
}
- private void OnItemDrag(object sender, ItemDragEventArgs e)
+ private void OnNodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
- _sourceNode = (TreeNode)e.Item;
- if (_sourceNode == null)
+ // Right-click acts on the node under the mouse
+ if (e.Button == MouseButtons.Right)
{
- return;
+ treeView1.SelectedNode = e.Node;
}
+ }
- if (string.Equals("Misc", _sourceNode.Text))
+ private void OnItemDrag(object sender, ItemDragEventArgs e)
+ {
+ if (e.Item is TreeNode node && !IsMisc(node))
{
- return;
+ DoDragDrop(node, DragDropEffects.Move);
}
-
- DoDragDrop(e.Item.ToString(), DragDropEffects.Move | DragDropEffects.Copy);
}
- private void OnDragEnter(object sender, DragEventArgs e)
+ private void OnDragOver(object sender, DragEventArgs e)
{
- e.Effect = e.Data.GetDataPresent(DataFormats.Text) ? DragDropEffects.Move : DragDropEffects.None;
+ e.Effect = e.Data.GetDataPresent(typeof(TreeNode)) ? DragDropEffects.Move : DragDropEffects.None;
}
private void OnDragDrop(object sender, DragEventArgs e)
{
- Point pos = treeView1.PointToClient(new Point(e.X, e.Y));
- TreeNode targetNode = treeView1.GetNodeAt(pos);
-
- if (targetNode == null)
- {
- return;
- }
-
- var nodeCopy = new TreeNode(_sourceNode.Text, _sourceNode.ImageIndex, _sourceNode.SelectedImageIndex)
- {
- Tag = _sourceNode.Tag
- };
-
- bool targetIsSkill = targetNode.Tag != null;
- bool sourceIsSkill = _sourceNode.Tag != null;
- if (targetIsSkill && !sourceIsSkill)
+ var source = (TreeNode)e.Data.GetData(typeof(TreeNode));
+ TreeNode target = treeView1.GetNodeAt(treeView1.PointToClient(new Point(e.X, e.Y)));
+ if (source == null || target == null || source == target)
{
return;
}
- if (targetIsSkill && sourceIsSkill)
+ if (IsSkill(source))
{
- if (_sourceNode.Index > targetNode.Index)
- {
- targetNode.Parent.Nodes.Insert(targetNode.Index, nodeCopy);
- }
- else
+ // A skill dropped on a group or on one of its skills joins that group
+ TreeNode group = IsSkill(target) ? target.Parent : target;
+ if (group == source.Parent)
{
- targetNode.Parent.Nodes.Insert(targetNode.Index + 1, nodeCopy);
+ return;
}
+
+ source.Remove();
+ InsertSkill(group, source);
}
- else if (!targetIsSkill && sourceIsSkill)
- {
- targetNode.Nodes.Add(nodeCopy);
- }
- else if (!targetIsSkill && !sourceIsSkill)
+ else
{
- if (string.Equals("Misc", targetNode.Text))
+ // A group dropped on another group takes its place; Misc stays first
+ TreeNode group = IsSkill(target) ? target.Parent : target;
+ if (group == source)
{
- treeView1.Invalidate();
return;
}
- if (_sourceNode.Index > targetNode.Index)
- {
- treeView1.Nodes.Insert(targetNode.Index, nodeCopy);
- }
- else
- {
- treeView1.Nodes.Insert(targetNode.Index + 1, nodeCopy);
- }
- while (_sourceNode.GetNodeCount(false) > 0)
- {
- TreeNode node = _sourceNode.FirstNode;
- node.Remove();
- nodeCopy.Nodes.Add(node);
- }
+ int index = Math.Max(1, group.Index);
+ source.Remove();
+ treeView1.Nodes.Insert(index, source);
}
- _sourceNode.Remove();
- nodeCopy.EnsureVisible();
- treeView1.SelectedNode = nodeCopy;
- treeView1.Invalidate();
- Options.ChangedUltimaClass["SkillGrp"] = true;
+ source.EnsureVisible();
+ treeView1.SelectedNode = source;
+ MarkChanged();
}
- private void OnClickRename(object sender, EventArgs e)
+ private void OnOpeningContext(object sender, System.ComponentModel.CancelEventArgs e)
{
- if (treeView1.SelectedNode != null && treeView1.SelectedNode.Parent == null &&
- !string.Equals("Misc", treeView1.SelectedNode.Text))
- {
- treeView1.LabelEdit = true;
- treeView1.SelectedNode.BeginEdit();
- }
+ TreeNode node = treeView1.SelectedNode;
+ bool editableGroup = node != null && !IsSkill(node) && !IsMisc(node);
+ removeToolStripMenuItem.Enabled = editableGroup;
+ renameToolStripMenuItem.Enabled = editableGroup;
}
- private void AfterLabelEdit(object sender, NodeLabelEditEventArgs e)
+ private void OnClickAdd(object sender, EventArgs e)
{
- if (e.Label == null)
+ if (treeView1.Nodes.Count >= SkillGroups.ClientGroupLimit)
{
+ MessageBox.Show($"The client supports at most {SkillGroups.ClientGroupLimit} groups, Misc included.",
+ "Skill Groups", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
- if (string.Equals("Misc", e.Label))
- {
- e.CancelEdit = true;
- MessageBox.Show("Invalid name. Name is reserved.", "SkillGroup Edit");
- e.Node.BeginEdit();
- }
- else if (e.Label.Length > 0)
- {
- e.Node.EndEdit(false);
- }
- else
- {
- e.CancelEdit = true;
- MessageBox.Show("Invalid name. Name cannot be empty.", "SkillGroup Edit");
- e.Node.BeginEdit();
- }
- treeView1.LabelEdit = false;
- }
+ var group = new TreeNode("New Group");
+ treeView1.Nodes.Add(group);
+ treeView1.SelectedNode = group;
+ MarkChanged();
- private void OnClickAdd(object sender, EventArgs e)
- {
- TreeNode newNode = new TreeNode("new");
- treeView1.Nodes.Add(newNode);
treeView1.LabelEdit = true;
- newNode.BeginEdit();
+ group.BeginEdit();
}
private void OnClickRemove(object sender, EventArgs e)
{
- if (treeView1.SelectedNode == null)
+ TreeNode group = treeView1.SelectedNode;
+ if (group == null || IsSkill(group) || IsMisc(group))
{
return;
}
- while (treeView1.SelectedNode.GetNodeCount(false) > 0)
+ // Its skills go back to Misc; skills are never dropped from the file
+ TreeNode misc = treeView1.Nodes[0];
+ while (group.Nodes.Count > 0)
{
- TreeNode node = treeView1.SelectedNode.FirstNode;
- node.Remove();
+ TreeNode skill = group.Nodes[0];
+ skill.Remove();
+ InsertSkill(misc, skill);
}
- treeView1.SelectedNode.Remove();
+
+ group.Remove();
+ MarkChanged();
}
- private void OnOpeningContext(object sender, System.ComponentModel.CancelEventArgs e)
+ private void OnClickRename(object sender, EventArgs e)
{
- if (treeView1.SelectedNode != null)
+ TreeNode group = treeView1.SelectedNode;
+ if (group == null || IsSkill(group) || IsMisc(group))
{
- if (treeView1.SelectedNode.Tag == null)
- {
- SkillIDTextBox.Text = "";
- }
- else
- {
- int id = (int)treeView1.SelectedNode.Tag;
- SkillIDTextBox.Text = id.ToString();
- }
- SkillIDTextBox.Enabled = true;
- }
- else
- {
- SkillIDTextBox.Text = "";
- SkillIDTextBox.Enabled = false;
+ return;
}
+
+ treeView1.LabelEdit = true;
+ group.BeginEdit();
}
- private void OnKeyDownSkillID(object sender, KeyEventArgs e)
+ private void AfterLabelEdit(object sender, NodeLabelEditEventArgs e)
{
- if (treeView1.SelectedNode == null)
+ treeView1.LabelEdit = false;
+
+ if (e.Label == null)
{
return;
}
- if (e.KeyCode != Keys.Enter)
+ string error = null;
+ string name = e.Label.Trim();
+ if (name.Length == 0)
+ {
+ error = "Name cannot be empty.";
+ }
+ else if (name.Length > SkillGroups.MaxNameLength)
+ {
+ error = $"Name can be at most {SkillGroups.MaxNameLength} characters.";
+ }
+ else if (name.Any(c => c > 127))
+ {
+ // Even the 16-bit variant of the file is narrowed to single bytes by the client
+ error = "Use ASCII characters only: the client stores group names as single bytes.";
+ }
+
+ if (error != null)
{
+ e.CancelEdit = true;
+ MessageBox.Show(error, "Skill Groups", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ treeView1.LabelEdit = true;
+ e.Node.BeginEdit();
return;
}
- string line = SkillIDTextBox.Text.Trim();
- if (line.Length > 0 && int.TryParse(line, out int id))
+ if (name != e.Label)
{
- treeView1.SelectedNode.Tag = id;
+ e.CancelEdit = true;
+ e.Node.Text = name;
}
+
+ MarkChanged();
}
}
}
diff --git a/UoFiddler.Controls/UserControls/SkillsControl.cs b/UoFiddler.Controls/UserControls/SkillsControl.cs
index 697803f4..84baf0f8 100644
--- a/UoFiddler.Controls/UserControls/SkillsControl.cs
+++ b/UoFiddler.Controls/UserControls/SkillsControl.cs
@@ -10,6 +10,7 @@
***************************************************************************/
using System;
+using System.Linq;
using System.Windows.Forms;
using Ultima;
using UoFiddler.Controls.Classes;
@@ -25,6 +26,26 @@ public SkillsControl()
InitializeComponent();
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
_source = new BindingSource();
+ dataGridView1.CellValidating += OnCellValidating;
+ }
+
+ private void OnCellValidating(object sender, DataGridViewCellValidatingEventArgs e)
+ {
+ // Only typed values; a name loaded with other bytes must not trap the cursor
+ if (!dataGridView1.IsCurrentCellInEditMode ||
+ dataGridView1.Columns[e.ColumnIndex].DataPropertyName != nameof(SkillInfo.Name))
+ {
+ return;
+ }
+
+ // skills.mul stores names as single bytes; the client has no Unicode for them
+ string name = e.FormattedValue as string ?? string.Empty;
+ if (name.Any(c => c > 127))
+ {
+ e.Cancel = true;
+ MessageBox.Show("Use ASCII characters only: skills.mul stores names as single bytes.", "Skills",
+ MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
}
private bool _loaded;