fix: Fixes adding items with ambiguous type lookup (#2307)
Fix [add command failing with ambiguous type names + refactor for performance ### Problem [add blight would fail with "No type with that name was found" because multiple types contain "blight" (e.g., Server.Items.Blight, Server.Ethics.Evil.Blight, Server.Items.BlightGrippedLongbow, Server.Items.QuiverOfBlight). The old code only succeeded when exactly one type matched the search regardless of constructability and inheriting Mobi les/Items. ### Solution Exact match takes priority: If a type's name exactly equals the search string (case-insensitive), use it directly. Otherwise, show the AddGump with all partial matches. - [add blight → Creates Blight (exact name match) - [add bligh → Shows gump with Blight, BlightGrippedLongbow, etc. ### Refactoring - CommandEventArgs context: Added GetContext<T>/SetContext<T> to pass resolved type through the command chain without method signature changes - Removed TrySetupTarget duplication: Validation now happens only in ValidateArgs, eliminating redundant code paths - Split type matching: - ExactMatch(string) → Returns Type for exact name match (used by [add) - MatchEmptyCtor(string) → Returns ConstructorInfo[] for gump display (empty-callable constructors only) ### Memory & Performance Improvements | Optimization | Benefit | |-------------------------------|----------------------------------------------------------------------------------------------| | _mobileItemTypes cache | Filters Mobile/Item types once per assembly, reused on all subsequent searches | | ReadOnlySpan<string> for args | Avoids string[] heap allocations when slicing arguments | | ValueStringBuilder | Stack-allocated string building, avoids StringBuilder heap allocation | | Single type resolution | Type resolved once in ValidateArgs, passed via context to Execute (was resolved 2-3x before) |
This commit is contained in:
parent
c4c8b45b79
commit
f4a87a8629
12 changed files with 837 additions and 684 deletions
|
|
@ -15,11 +15,11 @@ namespace Server.Commands
|
|||
{
|
||||
if (Mobile.DragEffects)
|
||||
{
|
||||
e.Mobile.SendMessage($"Drag effects are currently enabled.");
|
||||
e.Mobile.SendMessage("Drag effects are currently enabled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Mobile.SendMessage($"Drag effects are currently disabled.");
|
||||
e.Mobile.SendMessage("Drag effects are currently disabled.");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -28,11 +28,11 @@ namespace Server.Commands
|
|||
|
||||
if (Mobile.DragEffects)
|
||||
{
|
||||
e.Mobile.SendMessage($"Drag effects have been enabled.");
|
||||
e.Mobile.SendMessage("Drag effects have been enabled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Mobile.SendMessage($"Drag effects have been disabled.");
|
||||
e.Mobile.SendMessage("Drag effects have been disabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Server.Accounting;
|
||||
using Server.Engines.Help;
|
||||
using Server.Factions;
|
||||
|
|
@ -511,36 +512,39 @@ namespace Server.Commands.Generic
|
|||
|
||||
public override bool ValidateArgs(BaseCommandImplementor impl, CommandEventArgs e)
|
||||
{
|
||||
if (e.Length >= 1)
|
||||
{
|
||||
var t = AssemblyHandler.FindTypeByName(e.GetString(0));
|
||||
|
||||
if (t == null)
|
||||
{
|
||||
e.Mobile.SendMessage("No type with that name was found.");
|
||||
|
||||
var match = e.GetString(0).Trim();
|
||||
|
||||
if (match.Length < 3)
|
||||
{
|
||||
e.Mobile.SendMessage("Invalid search string.");
|
||||
e.Mobile.SendGump(new AddGump(match, 0, Type.EmptyTypes, false));
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Mobile.SendGump(new AddGump(match, 0, AddGump.Match(match), true));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
if (e.Length < 1)
|
||||
{
|
||||
e.Mobile.SendGump(new CategorizedAddGump(e.Mobile));
|
||||
return false;
|
||||
}
|
||||
|
||||
var match = e.GetString(0).Trim();
|
||||
if (e.GetContext<Type>("exactMatch", out var exactMatch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
exactMatch = AddGump.ExactMatch(match);
|
||||
if (exactMatch != null)
|
||||
{
|
||||
e.SetContext("exactMatch", exactMatch);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (match.Length < 3)
|
||||
{
|
||||
e.Mobile.SendMessage("Invalid search string.");
|
||||
e.Mobile.SendGump(new AddGump(match, 0, [], false));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!e.GetContext<ConstructorInfo[]>("matches", out var matches))
|
||||
{
|
||||
matches = e.SetContext("matches", AddGump.MatchEmptyCtor(match));
|
||||
}
|
||||
|
||||
e.Mobile.SendMessage("No type with that name was found.");
|
||||
e.Mobile.SendGump(new AddGump(match, 0, matches, true));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -558,7 +562,13 @@ namespace Server.Commands.Generic
|
|||
_ => new Point3D(ip)
|
||||
};
|
||||
|
||||
Add.Invoke(e.Mobile, p, p, e.Arguments);
|
||||
if (e.GetContext<Type>("exactMatch", out var exactMatch))
|
||||
{
|
||||
Add.Invoke(e.Mobile, p, p, exactMatch, e.Arguments.AsSpan(1));
|
||||
return;
|
||||
}
|
||||
|
||||
e.Mobile.SendMessage("No type with that name was found.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,8 +213,6 @@ namespace Server.Commands.Generic
|
|||
|
||||
public void RunCommand(Mobile from, object obj, BaseCommand command, string[] args)
|
||||
{
|
||||
// try
|
||||
// {
|
||||
var e = new CommandEventArgs(from, command.Commands[0], GenerateArgString(args), args);
|
||||
|
||||
if (!command.ValidateArgs(this, e))
|
||||
|
|
@ -256,11 +254,6 @@ namespace Server.Commands.Generic
|
|||
}
|
||||
|
||||
command.Flush(from, flushToLog);
|
||||
// }
|
||||
// catch ( Exception ex )
|
||||
// {
|
||||
// from.SendMessage( ex.Message );
|
||||
// }
|
||||
}
|
||||
|
||||
public virtual void Process(Mobile from, BaseCommand command, string[] args)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ namespace Server.Commands.Generic
|
|||
{
|
||||
e.Mobile.SendMessage("You do not have access to that command.");
|
||||
}
|
||||
else if (command.ValidateArgs(this, e))
|
||||
else
|
||||
{
|
||||
Process(e.Mobile, command, e.Arguments);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -665,11 +665,11 @@ namespace Server.Commands
|
|||
|
||||
if (m.AutoPageNotify)
|
||||
{
|
||||
m.SendMessage($"Your auto-page-notify has been turned on.");
|
||||
m.SendMessage("Your auto-page-notify has been turned on.");
|
||||
}
|
||||
else
|
||||
{
|
||||
m.SendMessage($"Your auto-page-notify has been turned off.");
|
||||
m.SendMessage("Your auto-page-notify has been turned off.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Server.Network;
|
||||
using Server.Targeting;
|
||||
|
||||
|
|
@ -10,13 +11,13 @@ public class AddGump : DynamicGump
|
|||
private static readonly Type _typeofItem = typeof(Item);
|
||||
private static readonly Type _typeofMobile = typeof(Mobile);
|
||||
private readonly int _page;
|
||||
private readonly Type[] _searchResults;
|
||||
private readonly ConstructorInfo[] _searchResults;
|
||||
private readonly string _searchString;
|
||||
private readonly bool _explicitSearch;
|
||||
|
||||
public override bool Singleton => true;
|
||||
|
||||
public AddGump(string searchString, int page, Type[] searchResults, bool explicitSearch) : base(50, 50)
|
||||
public AddGump(string searchString, int page, ConstructorInfo[] searchResults, bool explicitSearch) : base(50, 50)
|
||||
{
|
||||
_searchString = searchString;
|
||||
_searchResults = searchResults;
|
||||
|
|
@ -50,7 +51,7 @@ public class AddGump : DynamicGump
|
|||
{
|
||||
var index = i % 10;
|
||||
|
||||
builder.AddLabel(44, 39 + index * 20, 0x480, _searchResults[i].Name);
|
||||
builder.AddLabel(44, 39 + index * 20, 0x480, _searchResults[i].DeclaringType!.Name);
|
||||
builder.AddButton(10, 39 + index * 20, 4023, 4025, 4 + i);
|
||||
}
|
||||
}
|
||||
|
|
@ -102,42 +103,83 @@ public class AddGump : DynamicGump
|
|||
private static void AddMenu_OnCommand(CommandEventArgs e)
|
||||
{
|
||||
var val = e.ArgString.Trim();
|
||||
Type[] types;
|
||||
ConstructorInfo[] ctors;
|
||||
var explicitSearch = false;
|
||||
|
||||
if (val.Length == 0)
|
||||
{
|
||||
types = Type.EmptyTypes;
|
||||
ctors = [];
|
||||
}
|
||||
else if (val.Length < 3)
|
||||
{
|
||||
e.Mobile.SendMessage("Invalid search string.");
|
||||
types = Type.EmptyTypes;
|
||||
ctors = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
types = Match(val);
|
||||
ctors = MatchEmptyCtor(val);
|
||||
explicitSearch = true;
|
||||
}
|
||||
|
||||
e.Mobile.SendGump(new AddGump(val, 0, types, explicitSearch));
|
||||
e.Mobile.SendGump(new AddGump(val, 0, ctors, explicitSearch));
|
||||
}
|
||||
|
||||
private static void Match(string match, Type[] types, HashSet<Type> results)
|
||||
private static bool ExactMatch(string match, Assembly assembly, out Type type)
|
||||
{
|
||||
if (match.Length == 0)
|
||||
if (!_mobileItemTypes.TryGetValue(assembly, out var mobileItemTypes))
|
||||
{
|
||||
return;
|
||||
List<Type> typeList = [];
|
||||
var types = AssemblyHandler.GetTypeCache(assembly).Types;
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var t = types[i];
|
||||
if (_typeofMobile.IsAssignableFrom(t) || _typeofItem.IsAssignableFrom(t))
|
||||
{
|
||||
typeList.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
_mobileItemTypes[assembly] = mobileItemTypes = typeList.ToArray();
|
||||
}
|
||||
|
||||
match = match.ToLower();
|
||||
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
for (var i = 0; i < mobileItemTypes.Length; i++)
|
||||
{
|
||||
var t = types[i];
|
||||
var t = mobileItemTypes[i];
|
||||
|
||||
if (!(_typeofMobile.IsAssignableFrom(t) || _typeofItem.IsAssignableFrom(t)) ||
|
||||
!t.Name.InsensitiveContains(match) || results.Contains(t))
|
||||
if (t.Name.InsensitiveEquals(match))
|
||||
{
|
||||
type = t;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
type = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void MatchEmptyCtor(string match, Assembly assembly, List<ConstructorInfo> results)
|
||||
{
|
||||
if (!_mobileItemTypes.TryGetValue(assembly, out var mobileItemTypes))
|
||||
{
|
||||
List<Type> typeList = [];
|
||||
var types = AssemblyHandler.GetTypeCache(assembly).Types;
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var t = types[i];
|
||||
if (_typeofMobile.IsAssignableFrom(t) || _typeofItem.IsAssignableFrom(t))
|
||||
{
|
||||
typeList.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
_mobileItemTypes[assembly] = mobileItemTypes = typeList.ToArray();
|
||||
}
|
||||
|
||||
for (var i = 0; i < mobileItemTypes.Length; i++)
|
||||
{
|
||||
var t = mobileItemTypes[i];
|
||||
|
||||
if (!t.Name.InsensitiveContains(match))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -158,47 +200,70 @@ public class AddGump : DynamicGump
|
|||
}
|
||||
}
|
||||
|
||||
if (isEmptyCtor && ctors[j].IsDefined(typeof(ConstructibleAttribute), false))
|
||||
if (isEmptyCtor && ctor.GetCustomAttributes(Types.OfConstructible, false).Length > 0)
|
||||
{
|
||||
results.Add(t);
|
||||
results.Add(ctor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Type[] Match(string match)
|
||||
private static readonly Dictionary<Assembly, Type[]> _mobileItemTypes = [];
|
||||
|
||||
public static ConstructorInfo[] MatchEmptyCtor(string match)
|
||||
{
|
||||
var results = new HashSet<Type>();
|
||||
Type[] types;
|
||||
|
||||
var asms = AssemblyHandler.Assemblies;
|
||||
|
||||
for (var i = 0; i < asms.Length; ++i)
|
||||
if (string.IsNullOrWhiteSpace(match))
|
||||
{
|
||||
types = AssemblyHandler.GetTypeCache(asms[i]).Types;
|
||||
Match(match, types, results);
|
||||
return [];
|
||||
}
|
||||
|
||||
types = AssemblyHandler.GetTypeCache(Core.Assembly).Types;
|
||||
Match(match, types, results);
|
||||
match = match.ToLower();
|
||||
|
||||
List<ConstructorInfo> results = [];
|
||||
MatchEmptyCtor(match, Core.Assembly, results);
|
||||
|
||||
for (var i = 0; i < AssemblyHandler.Assemblies.Length; ++i)
|
||||
{
|
||||
MatchEmptyCtor(match, AssemblyHandler.Assemblies[i], results);
|
||||
}
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
return Array.Empty<Type>();
|
||||
return [];
|
||||
}
|
||||
|
||||
var finalResults = new Type[results.Count];
|
||||
var index = 0;
|
||||
foreach (var t in results)
|
||||
{
|
||||
finalResults[index++] = t;
|
||||
}
|
||||
var finalResults = results.ToArray();
|
||||
Array.Sort(finalResults, ConstructorNameComparer.Instance);
|
||||
|
||||
Array.Sort(finalResults, TypeNameComparer.Instance);
|
||||
return finalResults;
|
||||
}
|
||||
|
||||
public static Type ExactMatch(string match)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(match))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
match = match.ToLower();
|
||||
|
||||
if (ExactMatch(match, Core.Assembly, out var type))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
for (var i = 0; i < AssemblyHandler.Assemblies.Length; ++i)
|
||||
{
|
||||
if (ExactMatch(match, AssemblyHandler.Assemblies[i], out type))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState sender, in RelayInfo info)
|
||||
{
|
||||
var from = sender.Mobile;
|
||||
|
|
@ -216,7 +281,7 @@ public class AddGump : DynamicGump
|
|||
}
|
||||
else
|
||||
{
|
||||
from.SendGump(new AddGump(match, 0, Match(match), true));
|
||||
from.SendGump(new AddGump(match, 0, MatchEmptyCtor(match), true));
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -259,29 +324,30 @@ public class AddGump : DynamicGump
|
|||
}
|
||||
}
|
||||
|
||||
private class TypeNameComparer : IComparer<Type>
|
||||
private class ConstructorNameComparer : IComparer<ConstructorInfo>
|
||||
{
|
||||
public static readonly TypeNameComparer Instance = new();
|
||||
public int Compare(Type x, Type y) => string.CompareOrdinal(x?.Name, y?.Name);
|
||||
public static readonly ConstructorNameComparer Instance = new();
|
||||
public int Compare(ConstructorInfo x, ConstructorInfo y) =>
|
||||
x?.DeclaringType!.Name.CompareOrdinal(y?.DeclaringType!.Name) ?? 0;
|
||||
}
|
||||
|
||||
public class InternalTarget : Target
|
||||
{
|
||||
private readonly int m_Page;
|
||||
private readonly Type[] m_SearchResults;
|
||||
private readonly string m_SearchString;
|
||||
private readonly Type m_Type;
|
||||
private readonly int _page;
|
||||
private readonly ConstructorInfo[] _searchResults;
|
||||
private readonly string _searchString;
|
||||
private readonly ConstructorInfo _ctor;
|
||||
|
||||
public InternalTarget(Type type, Type[] searchResults, string searchString, int page) : base(
|
||||
public InternalTarget(ConstructorInfo ctor, ConstructorInfo[] searchResults, string searchString, int page) : base(
|
||||
-1,
|
||||
true,
|
||||
TargetFlags.None
|
||||
)
|
||||
{
|
||||
m_Type = type;
|
||||
m_SearchResults = searchResults;
|
||||
m_SearchString = searchString;
|
||||
m_Page = page;
|
||||
_ctor = ctor;
|
||||
_searchResults = searchResults;
|
||||
_searchString = searchString;
|
||||
_page = page;
|
||||
}
|
||||
|
||||
protected override void OnTarget(Mobile from, object o)
|
||||
|
|
@ -298,16 +364,15 @@ public class AddGump : DynamicGump
|
|||
_ => new Point3D(ip)
|
||||
};
|
||||
|
||||
Commands.Add.Invoke(from, new Point3D(p), new Point3D(p), new[] { m_Type.Name });
|
||||
|
||||
from.Target = new InternalTarget(m_Type, m_SearchResults, m_SearchString, m_Page);
|
||||
Commands.Add.Invoke(from, new Point3D(p), new Point3D(p), _ctor);
|
||||
from.Target = new InternalTarget(_ctor, _searchResults, _searchString, _page);
|
||||
}
|
||||
|
||||
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
|
||||
{
|
||||
if (cancelType == TargetCancelType.Canceled)
|
||||
{
|
||||
from.SendGump(new AddGump(m_SearchString, m_Page, m_SearchResults, true));
|
||||
from.SendGump(new AddGump(_searchString, _page, _searchResults, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1764,7 +1764,7 @@ namespace Server.Engines.ConPVP
|
|||
{
|
||||
if (Rematch)
|
||||
{
|
||||
mob.SendMessage(0x22, $"You have rejected the rematch.");
|
||||
mob.SendMessage(0x22, "You have rejected the rematch.");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -263,11 +263,11 @@ namespace Server.Engines.MLQuests
|
|||
|
||||
if (enable)
|
||||
{
|
||||
m.SendMessage($"Serialization for all quests is now enabled.");
|
||||
m.SendMessage("Serialization for all quests is now enabled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
m.SendMessage($"Serialization for all quests is now disabled.");
|
||||
m.SendMessage("Serialization for all quests is now disabled.");
|
||||
}
|
||||
|
||||
if (AutoGenerateNew && !enable)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public partial class GamblingStone : Item
|
|||
}
|
||||
else
|
||||
{
|
||||
from.SendMessage(0x22, $"You need at least 250gp in your backpack to use this.");
|
||||
from.SendMessage(0x22, "You need at least 250gp in your backpack to use this.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ namespace Server.Commands
|
|||
|
||||
if (enabled)
|
||||
{
|
||||
e.Mobile.SendMessage($"Saves have been enabled.");
|
||||
e.Mobile.SendMessage("Saves have been enabled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Mobile.SendMessage($"Saves have been disabled.");
|
||||
e.Mobile.SendMessage("Saves have been disabled.");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue