feat(types): support legacy RunUO Parse(string) types; AdvancedSearch reuses it
Server.Types.Parse now discovers either the modern IParsable Parse(string, IFormatProvider) or a legacy RunUO Parse(string) (cached in the ConcurrentDictionary), and TryParse gates on the discovered method instead of the IParsable interface. This restores parsing for pre-IParsable types like Faction and Town, which have only a static Parse(string) and were previously unreachable (they fell through to Convert.ChangeType and failed) — fixing them for [set, spawners, and the conditional compiler as well. AdvancedSearch's CompareValues now delegates all non-hot-path types to Types unconditionally, so reference types, IParsable value types, and legacy Parse(string) types are all searchable and compared by value. Adds a legacy-Parse test. Full suite 535/535.
This commit is contained in:
parent
1adf282c6b
commit
8997aaf498
3 changed files with 52 additions and 14 deletions
|
|
@ -96,4 +96,24 @@ public class AdvancedSearchUtilitiesTests
|
|||
Assert.True(AdvancedSearchUtilities.CompareValues(typeof(Guid), g, "00000000-0000-0000-0000-000000000001", "="));
|
||||
Assert.False(AdvancedSearchUtilities.CompareValues(typeof(Guid), g, "00000000-0000-0000-0000-000000000002", "="));
|
||||
}
|
||||
|
||||
// A reference type with a legacy RunUO-style static Parse(string) and NO IParsable<> interface —
|
||||
// the Faction/Town shape. Types must still discover its Parse by reflection.
|
||||
private sealed class LegacyParseType
|
||||
{
|
||||
public string Value { get; private init; }
|
||||
public static LegacyParseType Parse(string s) => new() { Value = s };
|
||||
public override bool Equals(object obj) => obj is LegacyParseType o && o.Value == Value;
|
||||
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareValues_LegacyParseString_ParsedViaTypes()
|
||||
{
|
||||
// Pre-IParsable types (only a static Parse(string)) must still be searchable: Types binds the
|
||||
// legacy Parse by reflection, so we compare against a real parsed instance, not the raw text.
|
||||
var prop = LegacyParseType.Parse("alpha");
|
||||
Assert.True(AdvancedSearchUtilities.CompareValues(typeof(LegacyParseType), prop, "alpha", "="));
|
||||
Assert.False(AdvancedSearchUtilities.CompareValues(typeof(LegacyParseType), prop, "beta", "="));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,17 +128,13 @@ public static class AdvancedSearchUtilities
|
|||
};
|
||||
}
|
||||
// Anything the hot typed paths above didn't handle — reference types (Poison, Map, entity
|
||||
// properties resolved by serial, ...) and value types exposing IParsable (Guid,
|
||||
// DateTimeOffset, ...). Delegate parsing to the shared, thread-safe Types converter so the
|
||||
// target is parsed into the property's real type, then compare by value. A string is
|
||||
// allocated here, but this is the uncommon path; the common types never reach it.
|
||||
if (!propertyType.IsValueType || Types.IsParsable(propertyType))
|
||||
{
|
||||
return Types.TryParse(propertyType, valuePart.ToString(), out var parsedValue) == null &&
|
||||
CompareReference(propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
|
||||
return false;
|
||||
// properties resolved by serial), IParsable value types (Guid, decimal, ...), and legacy
|
||||
// RunUO types with a static Parse(string) (Faction, Town, ...). Delegate to the shared,
|
||||
// thread-safe Types converter so the target is parsed into the property's real type, then
|
||||
// compare by value. A string is allocated here, but this is the uncommon path; the common
|
||||
// types never reach it. Types returns a non-null message when it can't parse -> no match.
|
||||
return Types.TryParse(propertyType, valuePart.ToString(), out var parsed) == null &&
|
||||
CompareReference(propertyValue!, parsed, operatorSpan);
|
||||
}
|
||||
|
||||
public static bool CompareNumeric<T>(T propertyValue, T parsedValue, ReadOnlySpan<char> operatorSpan) where T : INumber<T> =>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ namespace Server
|
|||
{
|
||||
public static readonly Type[] ParseStringParamTypes = { typeof(string), typeof(IFormatProvider) };
|
||||
public static readonly Type[] ParseStringNumericParamTypes = { typeof(string), typeof(NumberStyles) };
|
||||
// Legacy RunUO signature: a static Parse(string) that predates IParsable<T> (e.g. Faction, Town).
|
||||
public static readonly Type[] ParseStringSingleParamTypes = { typeof(string) };
|
||||
|
||||
public static readonly Type OfByte = typeof(byte);
|
||||
public static readonly Type OfSByte = typeof(sbyte);
|
||||
|
|
@ -116,12 +118,29 @@ namespace Server
|
|||
|
||||
private static readonly ConcurrentDictionary<Type, MethodInfo> _parseMethods = new();
|
||||
|
||||
// A static string Parse method: the modern IParsable<T> Parse(string, IFormatProvider), or a
|
||||
// legacy RunUO Parse(string). Cached per type; null if the type has neither. (Span-based Parse
|
||||
// can't be reflection-invoked — a ReadOnlySpan can't be boxed into the args array — so the
|
||||
// string overloads are what we bind to.)
|
||||
public static MethodInfo GetParseMethod(Type t) =>
|
||||
_parseMethods.GetOrAdd(
|
||||
t,
|
||||
static type => type.GetMethod("Parse", ParseStringParamTypes)
|
||||
?? type.GetMethod("Parse", ParseStringSingleParamTypes)
|
||||
);
|
||||
|
||||
public static object Parse(Type t, string value)
|
||||
{
|
||||
var method = _parseMethods.GetOrAdd(t, static type => type.GetMethod("Parse", ParseStringParamTypes));
|
||||
var method = GetParseMethod(t);
|
||||
if (method == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fresh args array per call — a shared static array would race across concurrent callers.
|
||||
return method?.Invoke(null, new object[] { value, null });
|
||||
// Arg shape depends on which overload we bound to (IParsable 2-arg vs legacy 1-arg).
|
||||
var args = method.GetParameters().Length == 2 ? new object[] { value, null } : new object[] { value };
|
||||
return method.Invoke(null, args);
|
||||
}
|
||||
|
||||
// Do not use this in "Parse" methods, it may cause a stack overflow
|
||||
|
|
@ -221,7 +240,10 @@ namespace Server
|
|||
}
|
||||
}
|
||||
|
||||
if (IsParsable(type))
|
||||
// IParsable<T> (Parse(string, IFormatProvider)) or a legacy RunUO Parse(string). Gating on
|
||||
// the discovered method rather than the IParsable interface keeps pre-IParsable types
|
||||
// (Faction, Town, ...) parseable for backwards compatibility.
|
||||
if (GetParseMethod(type) != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue