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) |
324 lines
9.1 KiB
C#
324 lines
9.1 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2024 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: Commands.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using Server.Logging;
|
|
|
|
namespace Server;
|
|
|
|
public delegate void CommandEventHandler(CommandEventArgs e);
|
|
|
|
public class CommandEventArgs
|
|
{
|
|
public CommandEventArgs(Mobile mobile, string command, string argString, string[] arguments)
|
|
{
|
|
Mobile = mobile;
|
|
Command = command;
|
|
ArgString = argString;
|
|
Arguments = arguments;
|
|
}
|
|
|
|
public Mobile Mobile { get; }
|
|
|
|
public string Command { get; }
|
|
|
|
public string ArgString { get; }
|
|
|
|
public string[] Arguments { get; }
|
|
|
|
private Dictionary<string, object> _context;
|
|
|
|
public int Length => Arguments.Length;
|
|
|
|
public string GetString(int index) => index < 0 || index >= Arguments.Length ? "" : Arguments[index];
|
|
|
|
public int GetInt32(int index) => index < 0 || index >= Arguments.Length ? 0 : Utility.ToInt32(Arguments[index]);
|
|
|
|
public uint GetUInt32(int index) =>
|
|
index < 0 || index >= Arguments.Length ? 0 : Utility.ToUInt32(Arguments[index]);
|
|
|
|
public bool GetBoolean(int index) => index >= 0 && index < Arguments.Length && Utility.ToBoolean(Arguments[index]);
|
|
|
|
public double GetDouble(int index) =>
|
|
index < 0 || index >= Arguments.Length ? 0.0 : Utility.ToDouble(Arguments[index]);
|
|
|
|
public TimeSpan GetTimeSpan(int index) =>
|
|
index < 0 || index >= Arguments.Length ? TimeSpan.Zero : Utility.ToTimeSpan(Arguments[index]);
|
|
|
|
public bool GetContext<T>(string key, out T t)
|
|
{
|
|
if (_context != null && _context.TryGetValue(key, out var value) && value is T tValue)
|
|
{
|
|
t = tValue;
|
|
return true;
|
|
}
|
|
|
|
t = default;
|
|
return false;
|
|
}
|
|
|
|
public T SetContext<T>(string key, T value)
|
|
{
|
|
_context ??= new Dictionary<string, object>();
|
|
_context[key] = value;
|
|
return value;
|
|
}
|
|
}
|
|
|
|
public static partial class EventSink
|
|
{
|
|
public static event Action<CommandEventArgs> Command;
|
|
public static void InvokeCommand(CommandEventArgs e) => Command?.Invoke(e);
|
|
}
|
|
|
|
public class CommandEntry : IComparable<CommandEntry>
|
|
{
|
|
public CommandEntry(string command, CommandEventHandler handler, AccessLevel accessLevel)
|
|
{
|
|
Command = command;
|
|
Handler = handler;
|
|
AccessLevel = accessLevel;
|
|
}
|
|
|
|
public string Command { get; }
|
|
|
|
public CommandEventHandler Handler { get; }
|
|
|
|
public AccessLevel AccessLevel { get; }
|
|
|
|
public int CompareTo(CommandEntry e) => string.CompareOrdinal(Command, e?.Command);
|
|
|
|
public static List<CommandEntry> GetList()
|
|
{
|
|
var commands = new List<CommandEntry>(CommandSystem.Entries.Values);
|
|
|
|
commands.Sort();
|
|
commands.Reverse();
|
|
|
|
for (var i = 0; i < commands.Count; ++i)
|
|
{
|
|
var e = commands[i];
|
|
|
|
for (var j = i + 1; j < commands.Count; ++j)
|
|
{
|
|
var c = commands[j];
|
|
|
|
if (e.Handler.Method == c.Handler.Method)
|
|
{
|
|
commands.RemoveAt(j);
|
|
--j;
|
|
}
|
|
}
|
|
}
|
|
|
|
return commands;
|
|
}
|
|
}
|
|
|
|
public record CommandInfo(AccessLevel AccessLevel, string Name, string[] Aliases, string Usage, string[] Modifiers, string Description);
|
|
|
|
public class CommandInfoSorter : IComparer<CommandInfo>
|
|
{
|
|
public int Compare(CommandInfo a, CommandInfo b)
|
|
{
|
|
if (a == null && b == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var v = b?.AccessLevel.CompareTo(a?.AccessLevel) ?? 1;
|
|
|
|
return v != 0 ? v : string.CompareOrdinal(a?.Name, b?.Name);
|
|
}
|
|
}
|
|
|
|
public static class CommandSystem
|
|
{
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(CommandSystem));
|
|
|
|
public static string Prefix { get; set; } = "[";
|
|
|
|
public static Dictionary<string, CommandEntry> Entries { get; } = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public static AccessLevel BadCommandIgnoreLevel { get; set; } = AccessLevel.Player;
|
|
|
|
public static string[] Split(string value)
|
|
{
|
|
var array = value.ToCharArray();
|
|
var list = new List<string>();
|
|
|
|
var start = 0;
|
|
|
|
while (start < array.Length)
|
|
{
|
|
var c = array[start];
|
|
|
|
if (c == '"')
|
|
{
|
|
++start;
|
|
var end = start;
|
|
|
|
while (end < array.Length)
|
|
{
|
|
if (array[end] != '"' || array[end - 1] == '\\')
|
|
{
|
|
++end;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
list.Add(value.Substring(start, end - start));
|
|
|
|
start = end + 2;
|
|
}
|
|
else if (c != ' ')
|
|
{
|
|
var end = start;
|
|
|
|
while (end < array.Length)
|
|
{
|
|
if (array[end] != ' ')
|
|
{
|
|
++end;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
list.Add(value.Substring(start, end - start));
|
|
|
|
start = end + 1;
|
|
}
|
|
else
|
|
{
|
|
++start;
|
|
}
|
|
}
|
|
|
|
return list.ToArray();
|
|
}
|
|
|
|
public static void Register(string command, AccessLevel access, CommandEventHandler handler)
|
|
{
|
|
DoRegister(command, access, handler);
|
|
|
|
var mi = handler.Method;
|
|
var aliasesAttr = mi.GetCustomAttribute(typeof(AliasesAttribute), false) as AliasesAttribute;
|
|
var aliases = aliasesAttr?.Aliases;
|
|
|
|
if (aliases == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var alias in aliases)
|
|
{
|
|
DoRegister(alias, access, handler);
|
|
}
|
|
}
|
|
|
|
private static void DoRegister(string command, AccessLevel accessLevel, CommandEventHandler handler)
|
|
{
|
|
ref var commandEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(Entries, command, out var exists);
|
|
if (exists)
|
|
{
|
|
if (commandEntry.AccessLevel == accessLevel && commandEntry.Handler == handler)
|
|
{
|
|
return;
|
|
}
|
|
|
|
logger.Warning("Command {Command} already registered to {Handler}.", command, commandEntry.Handler.Method.Name);
|
|
return;
|
|
}
|
|
|
|
commandEntry = new CommandEntry(command, handler, accessLevel);
|
|
}
|
|
|
|
public static bool Handle(Mobile from, string text, MessageType type = MessageType.Regular)
|
|
{
|
|
if (!text.StartsWithOrdinal(Prefix) && type != MessageType.Command)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (type != MessageType.Command)
|
|
{
|
|
text = text[Prefix.Length..];
|
|
}
|
|
|
|
var indexOf = text.IndexOfOrdinal(' ');
|
|
|
|
string command;
|
|
string[] args;
|
|
string argString;
|
|
|
|
if (indexOf >= 0)
|
|
{
|
|
argString = text[(indexOf + 1)..];
|
|
|
|
command = text[..indexOf];
|
|
args = Split(argString);
|
|
}
|
|
else
|
|
{
|
|
argString = "";
|
|
command = text.ToLower();
|
|
args = Array.Empty<string>();
|
|
}
|
|
|
|
Entries.TryGetValue(command, out var entry);
|
|
|
|
if (entry != null)
|
|
{
|
|
if (from.AccessLevel >= entry.AccessLevel)
|
|
{
|
|
if (entry.Handler != null)
|
|
{
|
|
var e = new CommandEventArgs(from, command, argString, args);
|
|
entry.Handler(e);
|
|
EventSink.InvokeCommand(e);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (from.AccessLevel <= BadCommandIgnoreLevel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
from.SendMessage("You do not have access to that command.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (from.AccessLevel <= BadCommandIgnoreLevel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
from.SendMessage("That is not a valid command.");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|