From 03fb36c869d8547c030144dfe388ee26e2ab018d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 12 Nov 2022 00:26:02 -0800 Subject: [PATCH] fix: Adds ISpanParsable and fixes command conditionals (#1241) * Adds `ISpanParsable` * Removes `[Parsable]` * Fixes querying by serial, body, and a few others. * Adds `Parse` to `Rectangle3D` * Fixes AutoArchive NPE Closes #1209 --- .../Tests/Geometry/Point2DTests.cs | 40 +++++ .../Tests/Geometry/Point3DTests.cs | 56 +++++++ .../Tests/Geometry/Rectangle2DTests.cs | 60 ++++++++ .../Tests/Geometry/Rectangle3DTests.cs | 68 +++++++++ .../Tests/Geometry/WorldLocationTests.cs | 82 +++++++++- Projects/Server/Attributes.cs | 5 - Projects/Server/Geometry/Point2D.cs | 94 +++++++++--- Projects/Server/Geometry/Point3D.cs | 134 ++++++++++++++--- Projects/Server/Geometry/Rectangle2D.cs | 89 +++++++---- Projects/Server/Geometry/Rectangle3D.cs | 62 +++++++- Projects/Server/Geometry/WorldLocation.cs | 112 ++++++++++---- Projects/Server/Maps/Map.cs | 140 ++++++++++++------ Projects/Server/Mobiles/Body.cs | 29 +++- Projects/Server/Poison.cs | 56 ++++++- Projects/Server/Race.cs | 121 ++++++++------- Projects/Server/Serial.cs | 27 +++- Projects/Server/Text/OrdinalStringHelpers.cs | 11 ++ Projects/Server/Text/TextDefinition.cs | 59 ++++++-- .../Compilers/ConditionalCompiler.cs | 91 ++++++++---- Projects/UOContent/Utilities/Types.cs | 47 ++++-- Projects/UOContent/World Saves/AutoArchive.cs | 7 +- 21 files changed, 1119 insertions(+), 271 deletions(-) diff --git a/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs b/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs index bc78a3c0d..0d1426275 100644 --- a/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs +++ b/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs @@ -65,4 +65,44 @@ public class Point2DTests Assert.False(p4.TryFormat(array, out var cp4, null, null)); Assert.Equal(0, cp4); } + + [Fact] + public void TestPoint2DTryParse() + { + // Happy Path + Assert.True(Point2D.TryParse("(101, 23)", null, out var p)); + Assert.Equal(new Point2D(101, 23), p); + Assert.Equal(new Point2D(101, 23), Point2D.Parse("(101, 23)", null)); + + // Trimming + Assert.True(Point2D.TryParse(" (101,23) ", null, out p)); + Assert.Equal(new Point2D(101, 23), p); + Assert.Equal(new Point2D(101, 23), Point2D.Parse(" (101,23) ", null)); + + // No parenthesis + Assert.False(Point2D.TryParse("101, 23)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point2D.Parse("101, 23)", null)); + + Assert.False(Point2D.TryParse("(101, 23", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point2D.Parse("(101, 23", null)); + + // No numbers + Assert.False(Point2D.TryParse("()", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point2D.Parse("()", null)); + + Assert.False(Point2D.TryParse("(101)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point2D.Parse("(101)", null)); + + Assert.False(Point2D.TryParse("(101,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point2D.Parse("(101,)", null)); + + Assert.False(Point2D.TryParse("(,23)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point2D.Parse("(,23)", null)); + } } diff --git a/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs b/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs index 07a6da0a6..9271e89d3 100644 --- a/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs +++ b/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs @@ -69,4 +69,60 @@ public class Point3DTests Assert.False(p4.TryFormat(array, out var cp4, null, null)); Assert.Equal(0, cp4); } + + [Fact] + public void TestPoint3DTryParse() + { + // Happy Path + Assert.True(Point3D.TryParse("(101, 23, 55)", null, out var p)); + Assert.Equal(new Point3D(101, 23, 55), p); + Assert.Equal(new Point3D(101, 23, 55), Point3D.Parse("(101, 23, 55)", null)); + + // Trimming + Assert.True(Point3D.TryParse(" (101,23 ,55) ", null, out p)); + Assert.Equal(new Point3D(101, 23, 55), p); + Assert.Equal(new Point3D(101, 23, 55), Point3D.Parse(" (101,23 ,55) ", null)); + + // No parenthesis + Assert.False(Point3D.TryParse("101, 23 55)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("101, 23, 55)", null)); + + Assert.False(Point3D.TryParse("(101, 23, 55", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("(101, 23, 55", null)); + + // No numbers + Assert.False(Point3D.TryParse("()", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("()", null)); + + Assert.False(Point3D.TryParse("(,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("(,)", null)); + + Assert.False(Point3D.TryParse("(|)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("(|)", null)); + + Assert.False(Point3D.TryParse("(101)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("(101)", null)); + + Assert.False(Point3D.TryParse("(101,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("(101,)", null)); + + Assert.False(Point3D.TryParse("(,23)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("(,23)", null)); + + Assert.False(Point3D.TryParse("(,23,55)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("(,23,55)", null)); + + Assert.False(Point3D.TryParse("(,23,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Point3D.Parse("(,23,)", null)); + } } diff --git a/Projects/Server.Tests/Tests/Geometry/Rectangle2DTests.cs b/Projects/Server.Tests/Tests/Geometry/Rectangle2DTests.cs index c0b0c293b..b009ac0af 100644 --- a/Projects/Server.Tests/Tests/Geometry/Rectangle2DTests.cs +++ b/Projects/Server.Tests/Tests/Geometry/Rectangle2DTests.cs @@ -69,4 +69,64 @@ public class Rectangle2DTests Assert.False(p4.TryFormat(array, out var cp4, null, null)); Assert.Equal(0, cp4); } + + [Fact] + public void TestRectangle2DTryParse() + { + // Happy Path + Assert.True(Rectangle2D.TryParse("(101, 23)+(55, 89)", null, out var p)); + Assert.Equal(new Rectangle2D(new Point2D(101, 23), new Point2D(55, 89)), p); + Assert.Equal(new Rectangle2D(new Point2D(101, 23), new Point2D(55, 89)), Rectangle2D.Parse("(101, 23)+(55, 89)", null)); + + // Trimming + Assert.True(Rectangle2D.TryParse(" (101,23)+ (55, 89) ", null, out p)); + Assert.Equal(new Rectangle2D(new Point2D(101, 23), new Point2D(55, 89)), p); + Assert.Equal(new Rectangle2D(new Point2D(101, 23), new Point2D(55, 89)), Rectangle2D.Parse(" (101,23)+ (55, 89) ", null)); + + // No parenthesis + Assert.False(Rectangle2D.TryParse("101, 23)+( 55, 89)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("101, 23)+( 55, 89)", null)); + + Assert.False(Rectangle2D.TryParse("(101, 23)+(55, 89", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(101, 23)+(55, 89", null)); + + // No numbers + Assert.False(Rectangle2D.TryParse("()", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("()", null)); + + Assert.False(Rectangle2D.TryParse("(,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(,)", null)); + + Assert.False(Rectangle2D.TryParse("(|)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(|)", null)); + + Assert.False(Rectangle2D.TryParse("(101)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(101)", null)); + + Assert.False(Rectangle2D.TryParse("(101,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(101,)", null)); + + Assert.False(Rectangle2D.TryParse("(,23)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(,23)", null)); + + Assert.False(Rectangle2D.TryParse("(,23)+(,55)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(,23)+(,55)", null)); + + Assert.False(Rectangle2D.TryParse("(,23,)+(,89)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(,23,)+(,89)", null)); + + Assert.False(Rectangle2D.TryParse("(,,)+(55,89)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle2D.Parse("(,,)+(55,89)", null)); + } } diff --git a/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs b/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs index 682abf493..8dcbef2a6 100644 --- a/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs +++ b/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs @@ -69,4 +69,72 @@ public class Rectangle3DTests Assert.False(p4.TryFormat(array, out var cp4, null, null)); Assert.Equal(0, cp4); } + + [Fact] + public void TestRectangle3DTryParse() + { + // Happy Path + Assert.True(Rectangle3D.TryParse("(101, 23, 10)+(55, 89, 1)", null, out var p)); + Assert.Equal(new Rectangle3D(new Point3D(101, 23, 10), new Point3D(55, 89, 1)), p); + Assert.Equal(new Rectangle3D(new Point3D(101, 23, 10), new Point3D(55, 89, 1)), Rectangle3D.Parse("(101, 23, 10)+(55, 89, 1)", null)); + + // Trimming + Assert.True(Rectangle3D.TryParse(" (101,23,10)+ (55, 89,1) ", null, out p)); + Assert.Equal(new Rectangle3D(new Point3D(101, 23, 10), new Point3D(55, 89, 1)), p); + Assert.Equal(new Rectangle3D(new Point3D(101, 23, 10), new Point3D(55, 89, 1)), Rectangle3D.Parse(" (101,23,10)+ (55, 89,1) ", null)); + + // No parenthesis + Assert.False(Rectangle3D.TryParse("101, 23, 10)+( 55, 89, 1)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("101, 23, 10)+( 55, 89, 1)", null)); + + Assert.False(Rectangle3D.TryParse("(101, 23, 10)+(55, 89, 1", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(101, 23, 10)+(55, 89, 1", null)); + + // No numbers + Assert.False(Rectangle3D.TryParse("()", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("()", null)); + + Assert.False(Rectangle3D.TryParse("(,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(,)", null)); + + Assert.False(Rectangle3D.TryParse("(|)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(|)", null)); + + Assert.False(Rectangle3D.TryParse("(101)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(101)", null)); + + Assert.False(Rectangle3D.TryParse("(101,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(101,)", null)); + + Assert.False(Rectangle3D.TryParse("(101,23)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(101,23)", null)); + + Assert.False(Rectangle3D.TryParse("(,23)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(,23)", null)); + + Assert.False(Rectangle3D.TryParse("(,23)+(,55)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(,23)+(,55)", null)); + + Assert.False(Rectangle3D.TryParse("(,23,)+(,89)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(,23,)+(,89)", null)); + + Assert.False(Rectangle3D.TryParse("(101,,10)+(55, 89 1)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(,23,)+(,89)", null)); + + Assert.False(Rectangle3D.TryParse("(,,)+(55,89, 1)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => Rectangle3D.Parse("(,,)+(55,89, 1)", null)); + } } diff --git a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs index 07d9ee5d9..1425e7b24 100644 --- a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs +++ b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs @@ -3,7 +3,7 @@ using Xunit; namespace Server.Tests; -public sealed class WorldLocationTests +public sealed class WorldLocationTests : IClassFixture { private static Map CreateMap(string name) => new(0, 0, 0, 1, 1, 0, name, MapRules.Internal); @@ -102,4 +102,84 @@ public sealed class WorldLocationTests Assert.False(p4.TryFormat(array, out var cp4, null, null)); Assert.Equal(0, cp4); } + + [Fact] + public void TestWorldLocationTryParse() + { + var validMap = Map.Felucca; + // Happy Path + Assert.True(WorldLocation.TryParse("(101, 23, 55) [Felucca]", null, out var p)); + Assert.Equal(new WorldLocation(101, 23, 55, validMap), p); + Assert.Equal(new WorldLocation(101, 23, 55, validMap), WorldLocation.Parse("(101, 23, 55) [Felucca]", null)); + + // Trimming + Assert.True(WorldLocation.TryParse(" (101,23 ,55) [Felucca] ", null, out p)); + Assert.Equal(new WorldLocation(101, 23, 55, validMap), p); + Assert.Equal(new WorldLocation(101, 23, 55, validMap), WorldLocation.Parse(" (101,23 ,55) [Felucca] ", null)); + + // Null Map + Assert.True(WorldLocation.TryParse(" (101,23 ,55) [(-null-)]", null, out p)); + Assert.Equal(new WorldLocation(101, 23, 55, null), p); + Assert.Equal(new WorldLocation(101, 23, 55, null), WorldLocation.Parse(" (101,23 ,55) [(-null-)]", null)); + + // No Brackets + Assert.False(WorldLocation.TryParse("(101, 23 55) Felucca]", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(101, 23, 55) Felucca]", null)); + + Assert.False(WorldLocation.TryParse("(101, 23 55) [Felucca", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(101, 23, 55) [Felucca", null)); + + // No parenthesis + Assert.False(WorldLocation.TryParse("101, 23 55) [Felucca]", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("101, 23, 55) [Felucca]", null)); + + Assert.False(WorldLocation.TryParse("(101, 23, 55 [Felucca]", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(101, 23, 55 [Felucca]", null)); + + // No Map - We don't support maps with no name, sorry. + Assert.False(WorldLocation.TryParse("(101, 23, 55) []", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(101, 23, 55) []", null)); + + // No numbers + Assert.False(WorldLocation.TryParse("()", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("()", null)); + + Assert.False(WorldLocation.TryParse("(,)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(,)", null)); + + Assert.False(WorldLocation.TryParse("(|)", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(|)", null)); + + Assert.False(WorldLocation.TryParse("() []", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("() []", null)); + + Assert.False(WorldLocation.TryParse("(101) [Felucca]", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(101) [Felucca]", null)); + + Assert.False(WorldLocation.TryParse("(101,) [Felucca]", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(101,) [Felucca]", null)); + + Assert.False(WorldLocation.TryParse("(,23) [Felucca]", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(,23) [Felucca]", null)); + + Assert.False(WorldLocation.TryParse("(,23,55) [Felucca]", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(,23,55) [Felucca]", null)); + + Assert.False(WorldLocation.TryParse("(,23,) [Felucca]", null, out p)); + Assert.Equal(default, p); + Assert.Throws(() => WorldLocation.Parse("(,23,) [Felucca]", null)); + } } diff --git a/Projects/Server/Attributes.cs b/Projects/Server/Attributes.cs index f6b9275d4..8751f3cd6 100644 --- a/Projects/Server/Attributes.cs +++ b/Projects/Server/Attributes.cs @@ -88,11 +88,6 @@ public class TypeAliasAttribute : Attribute public string[] Aliases { get; } } -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public class ParsableAttribute : Attribute -{ -} - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] public class CustomEnumAttribute : Attribute { diff --git a/Projects/Server/Geometry/Point2D.cs b/Projects/Server/Geometry/Point2D.cs index c4d1b97b0..fa29da554 100644 --- a/Projects/Server/Geometry/Point2D.cs +++ b/Projects/Server/Geometry/Point2D.cs @@ -14,13 +14,13 @@ *************************************************************************/ using System; +using System.Runtime.CompilerServices; namespace Server; -[Parsable] public struct Point2D : IPoint2D, IComparable, IComparable, IEquatable, IEquatable, - IEquatable, ISpanFormattable + IEquatable, ISpanFormattable, ISpanParsable { internal int m_X; internal int m_Y; @@ -51,21 +51,6 @@ public struct Point2D { } - public static Point2D Parse(string value) - { - var start = value.IndexOfOrdinal('('); - var end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var x); - - start = end; - end = value.IndexOf(')', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var y); - - return new Point2D(x, y); - } - public bool Equals(Point2D other) => m_X == other.m_X && m_Y == other.m_Y; public bool Equals(IPoint2D other) => m_X == other?.X && m_Y == other.Y; @@ -130,4 +115,79 @@ public struct Point2D // default ToString implementation. return ToString(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point2D Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point2D Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Point2D result) => + TryParse(s.AsSpan(), provider, out result); + + public static Point2D Parse(ReadOnlySpan s, IFormatProvider provider) + { + s = s.Trim(); + + if (!s.StartsWithOrdinal('(') || !s.EndsWithOrdinal(')')) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var comma = s.IndexOfOrdinal(','); + if (comma == -1) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var first = s.Slice(1, comma - 1).Trim(); + if (!Utility.ToInt32(first, out var x)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var second = s.Slice(comma + 1, s.Length - comma - 2).Trim(); + if (!Utility.ToInt32(second, out var y)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + return new Point2D(x, y); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Point2D result) + { + s = s.Trim(); + + if (!s.StartsWithOrdinal('(') || !s.EndsWithOrdinal(')')) + { + result = default; + return false; + } + + var comma = s.IndexOfOrdinal(','); + if (comma == -1) + { + result = default; + return false; + } + + var first = s.Slice(1, comma - 1).Trim(); + if (!Utility.ToInt32(first, out var x)) + { + result = default; + return false; + } + + var second = s.Slice(comma + 1, s.Length - comma - 2).Trim(); + if (!Utility.ToInt32(second, out var y)) + { + result = default; + return false; + } + + result = new Point2D(x, y); + return true; + } } diff --git a/Projects/Server/Geometry/Point3D.cs b/Projects/Server/Geometry/Point3D.cs index 430624068..acffdcee3 100644 --- a/Projects/Server/Geometry/Point3D.cs +++ b/Projects/Server/Geometry/Point3D.cs @@ -18,10 +18,9 @@ using System.Runtime.CompilerServices; namespace Server; -[Parsable] public struct Point3D : IPoint3D, IComparable, IComparable, IEquatable, IEquatable, - IEquatable, ISpanFormattable + IEquatable, ISpanFormattable, ISpanParsable { internal int m_X; internal int m_Y; @@ -79,26 +78,6 @@ public struct Point3D public override int GetHashCode() => HashCode.Combine(m_X, m_Y, m_Z); - public static Point3D Parse(string value) - { - var start = value.IndexOfOrdinal('('); - var end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var x); - - start = end; - end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var y); - - start = end; - end = value.IndexOf(')', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var z); - - return new Point3D(x, y, z); - } - public static bool operator ==(Point3D l, Point3D r) => l.m_X == r.m_X && l.m_Y == r.m_Y && l.m_Z == r.m_Z; public static bool operator ==(Point3D l, IPoint3D r) => @@ -183,4 +162,115 @@ public struct Point3D // default ToString implementation. return ToString(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point3D Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point3D Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Point3D result) => + TryParse(s.AsSpan(), provider, out result); + + public static Point3D Parse(ReadOnlySpan s, IFormatProvider provider) + { + s = s.Trim(); + + if (!s.StartsWithOrdinal('(') || !s.EndsWithOrdinal(')')) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var firstComma = s.IndexOfOrdinal(','); + if (firstComma == -1) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var first = s.Slice(1, firstComma - 1).Trim(); + + if (!Utility.ToInt32(first, out var x)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var offset = firstComma + 1; + + var secondComma = s[offset..].IndexOfOrdinal(','); + if (secondComma == -1 || offset == secondComma) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var second = s.Slice(firstComma + 1, secondComma).Trim(); + + if (!Utility.ToInt32(second, out var y)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + offset += secondComma + 1; + + var third = s.Slice(offset, s.Length - offset - 1).Trim(); + if (!Utility.ToInt32(third, out var z)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + return new Point3D(x, y, z); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Point3D result) + { + s = s.Trim(); + + if (!s.StartsWithOrdinal('(') || !s.EndsWithOrdinal(')')) + { + result = default; + return false; + } + + var firstComma = s.IndexOfOrdinal(','); + if (firstComma == -1) + { + result = default; + return false; + } + + var first = s.Slice(1, firstComma - 1).Trim(); + if (!Utility.ToInt32(first, out var x)) + { + result = default; + return false; + } + + var offset = firstComma + 1; + + var secondComma = s[offset..].IndexOfOrdinal(','); + if (secondComma == -1 || offset == secondComma) + { + result = default; + return false; + } + + var second = s.Slice(firstComma + 1, secondComma).Trim(); + if (!Utility.ToInt32(second, out var y)) + { + result = default; + return false; + } + + offset += secondComma + 1; + + var third = s.Slice(offset, s.Length - offset - 1).Trim(); + if (!Utility.ToInt32(third, out var z)) + { + result = default; + return false; + } + + result = new Point3D(x, y, z); + return true; + } } diff --git a/Projects/Server/Geometry/Rectangle2D.cs b/Projects/Server/Geometry/Rectangle2D.cs index 9cb8d9bbb..0aefd7ba6 100644 --- a/Projects/Server/Geometry/Rectangle2D.cs +++ b/Projects/Server/Geometry/Rectangle2D.cs @@ -14,13 +14,13 @@ *************************************************************************/ using System; +using System.Runtime.CompilerServices; namespace Server; [NoSort] -[Parsable] [PropertyObject] -public struct Rectangle2D : IEquatable, ISpanFormattable +public struct Rectangle2D : IEquatable, ISpanFormattable, ISpanParsable { private Point2D _start; private Point2D _end; @@ -118,31 +118,6 @@ public struct Rectangle2D : IEquatable, ISpanFormattable public static bool operator !=(Rectangle2D l, Rectangle2D r) => l._start != r._start || l._end != r._end; - public static Rectangle2D Parse(string value) - { - var start = value.IndexOfOrdinal('('); - var end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var x); - - start = end; - end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var y); - - start = end; - end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var w); - - start = end; - end = value.IndexOf(')', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var h); - - return new Rectangle2D(x, y, w, h); - } - public bool Contains(Point3D p) => _start.m_X <= p.m_X && _start.m_Y <= p.m_Y && _end.m_X > p.m_X && _end.m_Y > p.m_Y; @@ -172,4 +147,64 @@ public struct Rectangle2D : IEquatable, ISpanFormattable // default ToString implementation. return ToString(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle2D Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle2D Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Rectangle2D result) => + TryParse(s.AsSpan(), provider, out result); + + public static Rectangle2D Parse(ReadOnlySpan s, IFormatProvider provider) + { + s = s.Trim(); + + var delimiter = s.IndexOfOrdinal('+'); + if (delimiter == -1) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + if (!Point2D.TryParse(s[..delimiter], provider, out var start)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + if (!Point2D.TryParse(s[(delimiter + 1)..], provider, out var end)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + return new Rectangle2D(start, end); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Rectangle2D result) + { + s = s.Trim(); + + var delimiter = s.IndexOfOrdinal('+'); + if (delimiter == -1) + { + result = default; + return false; + } + + if (!Point2D.TryParse(s[..delimiter], provider, out var start)) + { + result = default; + return false; + } + + if (!Point2D.TryParse(s[(delimiter + 1)..], provider, out var end)) + { + result = default; + return false; + } + + result = new Rectangle2D(start, end); + return true; + } } diff --git a/Projects/Server/Geometry/Rectangle3D.cs b/Projects/Server/Geometry/Rectangle3D.cs index ef9345c3f..6a4ccd679 100644 --- a/Projects/Server/Geometry/Rectangle3D.cs +++ b/Projects/Server/Geometry/Rectangle3D.cs @@ -14,11 +14,11 @@ *************************************************************************/ using System; +using System.Runtime.CompilerServices; namespace Server; [NoSort] -[Parsable] [PropertyObject] public struct Rectangle3D : IEquatable, ISpanFormattable { @@ -172,4 +172,64 @@ public struct Rectangle3D : IEquatable, ISpanFormattable // default ToString implementation. return ToString(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle3D Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle3D Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Rectangle3D result) => + TryParse(s.AsSpan(), provider, out result); + + public static Rectangle3D Parse(ReadOnlySpan s, IFormatProvider provider) + { + s = s.Trim(); + + var delimiter = s.IndexOfOrdinal('+'); + if (delimiter == -1) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + if (!Point3D.TryParse(s[..delimiter], provider, out var start)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + if (!Point3D.TryParse(s[(delimiter + 1)..], provider, out var end)) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + return new Rectangle3D(start, end); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Rectangle3D result) + { + s = s.Trim(); + + var delimiter = s.IndexOfOrdinal('+'); + if (delimiter == -1) + { + result = default; + return false; + } + + if (!Point3D.TryParse(s[..delimiter], provider, out var start)) + { + result = default; + return false; + } + + if (!Point3D.TryParse(s[(delimiter + 1)..], provider, out var end)) + { + result = default; + return false; + } + + result = new Rectangle3D(start, end); + return true; + } } diff --git a/Projects/Server/Geometry/WorldLocation.cs b/Projects/Server/Geometry/WorldLocation.cs index 016dc70ea..2ad349227 100644 --- a/Projects/Server/Geometry/WorldLocation.cs +++ b/Projects/Server/Geometry/WorldLocation.cs @@ -16,12 +16,12 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Server; -[Parsable] public struct WorldLocation : IPoint3D, IComparable, IEquatable, IEquatable, - ISpanFormattable + ISpanFormattable, ISpanParsable { internal Point3D _loc; internal Map _map; @@ -63,7 +63,7 @@ public struct WorldLocation : IPoint3D, IComparable, IEquatable _map = value; } - public WorldLocation(IEntity e) : this(e.Location.X, e.Location.Y, e.Location.Z, e.Map) + public WorldLocation(IEntity e) : this(e.Location, e.Map) { } @@ -75,8 +75,10 @@ public struct WorldLocation : IPoint3D, IComparable, IEquatable, IEquatable - _loc.Equals(other._loc) && _map.MapID == other._map.MapID; + _loc.Equals(other._loc) && _map?.MapID == other._map?.MapID; public bool Equals(IEntity other) => !ReferenceEquals(other, null) && _loc == other.Location && - _map.MapID == other.Map.MapID; + _map?.MapID == other.Map?.MapID; public override bool Equals(object obj) => obj is WorldLocation other && Equals(other); @@ -138,31 +140,6 @@ public struct WorldLocation : IPoint3D, IComparable, IEquatable !ReferenceEquals(r, null) && l._loc <= r.Location && l._map == r.Map; - public static WorldLocation Parse(string value) - { - var start = value.IndexOfOrdinal('('); - var end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var x); - - start = end; - end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var y); - - start = end; - end = value.IndexOf(',', start + 1); - - Utility.ToInt32(value.AsSpan(start + 1, end - (start + 1)).Trim(), out var z); - - start = end; - end = value.IndexOf(')', start + 1); - - var map = Map.Parse(value.AsSpan(start + 1, end - (start + 1)).Trim()); - - return new WorldLocation(x, y, z, map); - } - public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) { if (_map == null) @@ -208,4 +185,77 @@ public struct WorldLocation : IPoint3D, IComparable, IEquatable Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static WorldLocation Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out WorldLocation result) => + TryParse(s.AsSpan(), provider, out result); + + public static WorldLocation Parse(ReadOnlySpan s, IFormatProvider provider) + { + s = s.Trim(); + + if (!s.EndsWithOrdinal(']')) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var mapStartBracket = s.IndexOf('['); + if (mapStartBracket == -1) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + var loc = Point3D.Parse(s[..(mapStartBracket - 1)], provider); + + var mapSlice = s.Slice(mapStartBracket + 1, s.Length - mapStartBracket - 2); + return mapSlice.EqualsOrdinal("(-null-)") + ? new WorldLocation(loc, null) + : new WorldLocation(loc, Map.Parse(mapSlice, provider)); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out WorldLocation result) + { + s = s.Trim(); + + if (!s.EndsWithOrdinal(']')) + { + result = default; + return false; + } + + var mapStartBracket = s.IndexOf('['); + if (mapStartBracket == -1) + { + result = default; + return false; + } + + if (!Point3D.TryParse(s[..(mapStartBracket - 1)], provider, out var loc)) + { + result = default; + return false; + } + + var mapSlice = s.Slice(mapStartBracket + 1, s.Length - mapStartBracket - 2); + if (mapSlice.EqualsOrdinal("(-null-)")) + { + result = new WorldLocation(loc, null); + return true; + } + + if (!Map.TryParse(mapSlice, provider, out var map)) + { + result = default; + return false; + } + + result = new WorldLocation(loc, map); + return true; + } } diff --git a/Projects/Server/Maps/Map.cs b/Projects/Server/Maps/Map.cs index 1b95cb977..6804b7bd9 100644 --- a/Projects/Server/Maps/Map.cs +++ b/Projects/Server/Maps/Map.cs @@ -313,8 +313,7 @@ public static class PooledEnumeration } } -[Parsable] -public sealed class Map : IComparable, ISpanFormattable +public sealed class Map : IComparable, ISpanFormattable, ISpanParsable { public const int SectorSize = 16; public const int SectorShift = 4; @@ -482,49 +481,6 @@ public sealed class Map : IComparable, ISpanFormattable return mapValues; } - // Handles null checks - public static Map Parse(string value) => Parse(value ?? ReadOnlySpan.Empty); - - public static Map Parse(ReadOnlySpan value) - { - value = value.Trim(); - - if (value.Length == 0) - { - return null; - } - - if (value.InsensitiveEquals("Internal")) - { - return Internal; - } - - if (!int.TryParse(value, out var index)) - { - index = -1; - } - else if (index == 127) - { - return Internal; - } - - for (int i = 0; i < Maps.Length; i++) - { - var map = Maps[i]; - if (map == null) - { - continue; - } - - if (index >= 0 && map.MapIndex == index || value.InsensitiveEquals(map.Name)) - { - return map; - } - } - - return null; - } - public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) { if (destination.Length >= Name.Length) @@ -1709,4 +1665,98 @@ public sealed class Map : IComparable, ISpanFormattable } } #pragma warning restore CA1000 // Do not declare static members on generic types + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Map Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Map Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Map result) => + TryParse(s.AsSpan(), provider, out result); + + public static Map Parse(ReadOnlySpan s, IFormatProvider provider) + { + s = s.Trim(); + + if (s.Length == 0) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + if (s.InsensitiveEquals("Internal")) + { + return Internal; + } + + if (!int.TryParse(s, provider, out var index)) + { + index = -1; + } + else if (index == 127) + { + return Internal; + } + + for (int i = 0; i < Maps.Length; i++) + { + var map = Maps[i]; + if (map == null) + { + continue; + } + + if (index >= 0 && map.MapIndex == index || s.InsensitiveEquals(map.Name)) + { + return map; + } + } + + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Map result) + { + s = s.Trim(); + + if (s.Length == 0) + { + result = default; + return false; + } + + if (s.InsensitiveEquals("Internal")) + { + result = Internal; + return true; + } + + if (!int.TryParse(s, provider, out var index)) + { + index = -1; + } + else if (index == 127) + { + result = Internal; + return true; + } + + for (int i = 0; i < Maps.Length; i++) + { + var map = Maps[i]; + if (map == null) + { + continue; + } + + if (index >= 0 && map.MapIndex == index || s.InsensitiveEquals(map.Name)) + { + result = map; + return true; + } + } + + result = default; + return false; + } } diff --git a/Projects/Server/Mobiles/Body.cs b/Projects/Server/Mobiles/Body.cs index 41e1861ec..8b2767bbc 100644 --- a/Projects/Server/Mobiles/Body.cs +++ b/Projects/Server/Mobiles/Body.cs @@ -30,8 +30,8 @@ public enum BodyType : byte Equipment } -[Parsable] -public readonly struct Body : IEquatable, IEquatable, IEquatable, IComparable, IComparable +public readonly struct Body : IEquatable, IEquatable, IEquatable, + IComparable, IComparable, ISpanParsable { private static readonly ILogger logger = LogFactory.GetLogger(typeof(Body)); @@ -148,5 +148,28 @@ public readonly struct Body : IEquatable, IEquatable, IEquatable BodyID.CompareTo(other); - public static Body Parse(string value) => Utility.ToInt32(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Body Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Body Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Body result) => + TryParse(s.AsSpan(), provider, out result); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Body Parse(ReadOnlySpan s, IFormatProvider provider) => Utility.ToInt32(s); + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Body result) + { + if (Utility.ToInt32(s, out var value)) + { + result = value; + return true; + } + + result = default; + return false; + } } diff --git a/Projects/Server/Poison.cs b/Projects/Server/Poison.cs index f97d15467..64d804870 100644 --- a/Projects/Server/Poison.cs +++ b/Projects/Server/Poison.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Server; -[Parsable] -public abstract class Poison +public abstract class Poison : ISpanParsable { /*public abstract TimeSpan Interval{ get; } public abstract TimeSpan Duration{ get; }*/ @@ -44,9 +44,6 @@ public abstract class Poison Poisons.Add(reg); } - public static Poison Parse(string value) => - (int.TryParse(value, out var plevel) ? GetPoison(plevel) : null) ?? GetPoison(value); - public static Poison GetPoison(int level) { for (var i = 0; i < Poisons.Count; ++i) @@ -62,13 +59,13 @@ public abstract class Poison return null; } - public static Poison GetPoison(string name) + public static Poison GetPoison(ReadOnlySpan name) { for (var i = 0; i < Poisons.Count; ++i) { var p = Poisons[i]; - if (Utility.InsensitiveCompare(p.Name, name) == 0) + if (name.InsensitiveEquals(p.Name)) { return p; } @@ -76,4 +73,49 @@ public abstract class Poison return null; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Poison Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Poison Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Poison result) => + TryParse(s.AsSpan(), provider, out result); + + public static Poison Parse(ReadOnlySpan s, IFormatProvider provider) + { + if (int.TryParse(s, provider, out var pLevel)) + { + var result = GetPoison(pLevel); + if (result != null) + { + return result; + } + } + + var poison = GetPoison(s.Trim()); + if (poison == null) + { + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + return poison; + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Poison result) + { + if (int.TryParse(s, provider, out var pLevel)) + { + result = GetPoison(pLevel); + if (result != null) + { + return true; + } + } + + result = GetPoison(s.Trim()); + return result != null; + } } diff --git a/Projects/Server/Race.cs b/Projects/Server/Race.cs index d0c3e49af..5bebc554a 100644 --- a/Projects/Server/Race.cs +++ b/Projects/Server/Race.cs @@ -4,12 +4,8 @@ using System.Runtime.CompilerServices; namespace Server; -[Parsable] -public abstract class Race +public abstract class Race : ISpanParsable { - private static string[] m_RaceNames; - private static Race[] m_RaceValues; - protected Race( int raceID, int raceIndex, string name, string pluralName, int maleBody, int femaleBody, int maleGhostBody, int femaleGhostBody, Expansion requiredExpansion @@ -68,58 +64,6 @@ public abstract class Race [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsAllowedRace(Race race, int allowedRaceFlags) => (allowedRaceFlags & race.RaceFlag) != 0; - public static string[] GetRaceNames() - { - CheckNamesAndValues(); - return m_RaceNames; - } - - public static Race[] GetRaceValues() - { - CheckNamesAndValues(); - return m_RaceValues; - } - - public static Race Parse(string value) - { - CheckNamesAndValues(); - - for (var i = 0; i < m_RaceNames.Length; ++i) - { - if (m_RaceNames[i].InsensitiveEquals(value)) - { - return m_RaceValues[i]; - } - } - - if (int.TryParse(value, out var index) && index >= 0 && index < Races.Length && - Races[index] != null) - { - return Races[index]; - } - - throw new ArgumentException("Invalid race name"); - } - - private static void CheckNamesAndValues() - { - if (m_RaceNames?.Length == AllRaces.Count) - { - return; - } - - m_RaceNames = new string[AllRaces.Count]; - m_RaceValues = new Race[AllRaces.Count]; - - for (var i = 0; i < AllRaces.Count; ++i) - { - var race = AllRaces[i]; - - m_RaceNames[i] = race.Name; - m_RaceValues[i] = race; - } - } - public override string ToString() => Name; public virtual bool ValidateHair(Mobile m, int itemID) => ValidateHair(m.Female, itemID); @@ -153,4 +97,67 @@ public abstract class Race public virtual int GhostBody(Mobile m) => GhostBody(m.Female); public virtual int GhostBody(bool female) => female ? FemaleGhostBody : MaleGhostBody; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Race Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Race Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Race result) => + TryParse(s.AsSpan(), provider, out result); + + public static Race Parse(ReadOnlySpan s, IFormatProvider provider) + { + if (int.TryParse(s, out var index) && index >= 0 && index < Races.Length) + { + var race = Races[index]; + if (race != null) + { + return race; + } + } + + s = s.Trim(); + + for (var i = 0; i < Races.Length; ++i) + { + var race = Races[i]; + if (s.InsensitiveEquals(race.Name) || s.InsensitiveEquals(race.PluralName)) + { + return race; + } + } + + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Race result) + { + if (int.TryParse(s, out var index) && index >= 0 && index < Races.Length) + { + var race = Races[index]; + if (race != null) + { + result = default; + return false; + } + } + + s = s.Trim(); + + for (var i = 0; i < Races.Length; ++i) + { + var race = Races[i]; + if (s.InsensitiveEquals(race.Name) || s.InsensitiveEquals(race.PluralName)) + { + result = race; + return true; + } + } + + result = default; + return false; + } } diff --git a/Projects/Server/Serial.cs b/Projects/Server/Serial.cs index 42c00a33d..a11c459d9 100644 --- a/Projects/Server/Serial.cs +++ b/Projects/Server/Serial.cs @@ -18,7 +18,8 @@ using System.Runtime.CompilerServices; namespace Server; -public readonly struct Serial : IComparable, IComparable, IEquatable, ISpanFormattable +public readonly struct Serial : IComparable, IComparable, + IEquatable, ISpanFormattable, ISpanParsable { public static readonly Serial MinusOne = new(0xFFFFFFFF); public static readonly Serial Zero = new(0); @@ -152,4 +153,28 @@ public readonly struct Serial : IComparable, IComparable, IEquatab [MethodImpl(MethodImplOptions.AggressiveInlining)] public int ToInt32() => (int)Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Serial Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Serial Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Serial result) => + TryParse(s.AsSpan(), provider, out result); + + public static Serial Parse(ReadOnlySpan s, IFormatProvider provider) => new(Utility.ToUInt32(s)); + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Serial result) + { + if (Utility.ToUInt32(s, out var value)) + { + result = new Serial(value); + return true; + } + + result = default; + return false; + } } diff --git a/Projects/Server/Text/OrdinalStringHelpers.cs b/Projects/Server/Text/OrdinalStringHelpers.cs index 18a1ada4b..18e688320 100644 --- a/Projects/Server/Text/OrdinalStringHelpers.cs +++ b/Projects/Server/Text/OrdinalStringHelpers.cs @@ -35,6 +35,10 @@ public static class OrdinalStringHelpers public static bool EqualsOrdinal(this string a, string b) => a?.Equals(b, StringComparison.Ordinal) ?? b == null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool StartsWithOrdinal(this ReadOnlySpan a, char b) => + a.StartsWithOrdinal(new ReadOnlySpan(b)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool StartsWithOrdinal(this ReadOnlySpan a, ReadOnlySpan b) => a.StartsWith(b, StringComparison.Ordinal); @@ -47,6 +51,9 @@ public static class OrdinalStringHelpers public static bool EndsWithOrdinal(this string a, string b) => a?.EndsWith(b, StringComparison.Ordinal) == true; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool EndsWithOrdinal(this ReadOnlySpan a, char b) => a.EndsWithOrdinal(new ReadOnlySpan(b)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EndsWithOrdinal(this ReadOnlySpan a, ReadOnlySpan b) => a.EndsWith(b, StringComparison.Ordinal); @@ -77,6 +84,10 @@ public static class OrdinalStringHelpers public static int IndexOfOrdinal(this string a, string b, int startIndex) => a?.IndexOf(b, startIndex, StringComparison.Ordinal) ?? -1; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfOrdinal(this ReadOnlySpan a, char b) => + a.IndexOfOrdinal(new ReadOnlySpan(b)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int IndexOfOrdinal(this ReadOnlySpan a, ReadOnlySpan b) => a.IndexOf(b, StringComparison.Ordinal); diff --git a/Projects/Server/Text/TextDefinition.cs b/Projects/Server/Text/TextDefinition.cs index 47fee2544..e5a48209e 100644 --- a/Projects/Server/Text/TextDefinition.cs +++ b/Projects/Server/Text/TextDefinition.cs @@ -18,9 +18,8 @@ using System.Runtime.CompilerServices; namespace Server; -[Parsable] [PropertyObject] -public class TextDefinition : IEquatable, IEquatable +public class TextDefinition : IEquatable, IEquatable, ISpanParsable { public static readonly TextDefinition Empty = new(); @@ -30,9 +29,15 @@ public class TextDefinition : IEquatable, IEquatable [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TextDefinition Of(string text) => Of(0, text); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TextDefinition Of(ReadOnlySpan text) => Of(0, text); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TextDefinition Of(int number, string text) => new(number, text); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TextDefinition Of(int number, ReadOnlySpan text) => new(number, text); + private TextDefinition() { } @@ -43,6 +48,12 @@ public class TextDefinition : IEquatable, IEquatable String = text; } + private TextDefinition(int number, ReadOnlySpan text) + { + Number = number; + String = text.ToString(); + } + [CommandProperty(AccessLevel.GameMaster)] public int Number { get; } @@ -67,16 +78,6 @@ public class TextDefinition : IEquatable, IEquatable public static implicit operator string(TextDefinition m) => m?.String; - public static TextDefinition Parse(string value) - { - if (value == null) - { - return null; - } - - return Utility.ToInt32(value, out var i) ? Of(i) : Of(value); - } - public void Deconstruct(out int number, out string s) { if (Number > 0) @@ -118,4 +119,38 @@ public class TextDefinition : IEquatable, IEquatable public static bool operator ==(TextDefinition left, TextDefinition right) => Equals(left, right); public static bool operator !=(TextDefinition left, TextDefinition right) => !Equals(left, right); + + public static TextDefinition Parse(string value) + { + if (value == null) + { + return null; + } + + return Utility.ToInt32(value, out var i) ? Of(i) : Of(value); + } + + public static TextDefinition Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + public static bool TryParse(string s, IFormatProvider provider, out TextDefinition result) => + TryParse(s.AsSpan(), provider, out result); + + public static TextDefinition Parse(ReadOnlySpan s, IFormatProvider provider) + { + // We don't trim + return int.TryParse(s, provider, out var label) ? Of(label) : Of(s); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out TextDefinition result) + { + if (int.TryParse(s, provider, out var label)) + { + result = Of(label); + return true; + } + + // We don't trim + result = Of(s); + return true; + } } diff --git a/Projects/UOContent/Commands/Generic/Extensions/Compilers/ConditionalCompiler.cs b/Projects/UOContent/Commands/Generic/Extensions/Compilers/ConditionalCompiler.cs index 58ea93f35..76b14a2a5 100644 --- a/Projects/UOContent/Commands/Generic/Extensions/Compilers/ConditionalCompiler.cs +++ b/Projects/UOContent/Commands/Generic/Extensions/Compilers/ConditionalCompiler.cs @@ -140,7 +140,7 @@ namespace Server.Commands.Generic "Parse", BindingFlags.Public | BindingFlags.Static, null, - new[] { typeof(string), typeof(NumberStyles) }, + Types.ParseStringNumericParamTypes, null ); @@ -163,12 +163,12 @@ namespace Server.Commands.Generic "Parse", BindingFlags.Public | BindingFlags.Static, null, - new[] { typeof(string) }, + Types.ParseStringParamTypes, null ); parseMethod = parseGeneral; - parseArgs = new object[] { toParse }; + parseArgs = new object[] { toParse, null }; } if (parseMethod != null) @@ -183,16 +183,21 @@ namespace Server.Commands.Generic FieldAttributes.Private | FieldAttributes.InitOnly ); - // parseMethod.Invoke(null, - // parseArgs.Length == 2 ? new object[] {toParse, (int) parseArgs[1]} : new object[] {toParse}); - il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldstr, toParse); if (parseArgs.Length == 2) // dirty evil hack :-( { - il.Emit(OpCodes.Ldc_I4, (int)parseArgs[1]); + if (parseArgs[1]?.GetType() == typeof(NumberStyles)) + { + il.Emit(OpCodes.Ldc_I4, (int)parseArgs[1]); + } + else + { + // IFormatProvider for `IParsable.Parse()` method. + il.Emit(OpCodes.Ldnull); + } } il.Emit(OpCodes.Call, parseMethod); @@ -322,7 +327,9 @@ namespace Server.Commands.Generic } default: - throw new InvalidOperationException("Invalid string comparison operator."); + { + throw new InvalidOperationException("Invalid string comparison operator."); + } } if (m_Operator is StringOperator.Equal or StringOperator.NotEqual) @@ -437,34 +444,48 @@ namespace Server.Commands.Generic switch (m_Operator) { case ComparisonOperator.Equal: - emitter.Compare(OpCodes.Ceq); - break; + { + emitter.Compare(OpCodes.Ceq); + break; + } case ComparisonOperator.NotEqual: - emitter.Compare(OpCodes.Ceq); - inverse = true; - break; + { + emitter.Compare(OpCodes.Ceq); + inverse = true; + break; + } case ComparisonOperator.Greater: - emitter.Compare(OpCodes.Cgt); - break; + { + emitter.Compare(OpCodes.Cgt); + break; + } case ComparisonOperator.GreaterEqual: - emitter.Compare(OpCodes.Clt); - inverse = true; - break; + { + emitter.Compare(OpCodes.Clt); + inverse = true; + break; + } case ComparisonOperator.Lesser: - emitter.Compare(OpCodes.Clt); - break; + { + emitter.Compare(OpCodes.Clt); + break; + } case ComparisonOperator.LesserEqual: - emitter.Compare(OpCodes.Cgt); - inverse = true; - break; + { + emitter.Compare(OpCodes.Cgt); + inverse = true; + break; + } default: - throw new InvalidOperationException("Invalid comparison operator."); + { + throw new InvalidOperationException("Invalid comparison operator."); + } } } else @@ -477,22 +498,30 @@ namespace Server.Commands.Generic switch (m_Operator) { case ComparisonOperator.Equal: - emitter.Compare(OpCodes.Ceq); - break; + { + emitter.Compare(OpCodes.Ceq); + break; + } case ComparisonOperator.NotEqual: - emitter.Compare(OpCodes.Ceq); - inverse = true; - break; + { + emitter.Compare(OpCodes.Ceq); + inverse = true; + break; + } case ComparisonOperator.Greater: case ComparisonOperator.GreaterEqual: case ComparisonOperator.Lesser: case ComparisonOperator.LesserEqual: - throw new InvalidOperationException("Property does not support relational comparisons."); + { + throw new InvalidOperationException("Property does not support relational comparisons."); + } default: - throw new InvalidOperationException("Invalid operator."); + { + throw new InvalidOperationException("Invalid operator."); + } } } diff --git a/Projects/UOContent/Utilities/Types.cs b/Projects/UOContent/Utilities/Types.cs index fc707a7bc..517f224ca 100644 --- a/Projects/UOContent/Utilities/Types.cs +++ b/Projects/UOContent/Utilities/Types.cs @@ -1,12 +1,16 @@ using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; using System.Runtime.CompilerServices; namespace Server { public static class Types { - private static readonly Type[] _parseStringParamTypes = { typeof(string) }; - private static readonly object[] _parseParams = new object[1]; + public static readonly Type[] ParseStringParamTypes = { typeof(string), typeof(IFormatProvider) }; + public static readonly Type[] ParseStringNumericParamTypes = { typeof(string), typeof(NumberStyles) }; + private static object[] _parseParams = { null, null }; public static readonly Type OfByte = typeof(byte); public static readonly Type OfSByte = typeof(sbyte); @@ -33,7 +37,6 @@ namespace Server public static readonly Type OfCPA = typeof(CommandPropertyAttribute); public static readonly Type OfText = typeof(TextDefinition); - public static readonly Type OfParsable = typeof(ParsableAttribute); public static readonly Type OfMobile = typeof(Mobile); public static readonly Type OfItem = typeof(Item); public static readonly Type OfCustomEnum = typeof(CustomEnumAttribute); @@ -72,6 +75,8 @@ namespace Server OfULong }; + private static Dictionary _isParsable; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsType(Type type, Type check) => check.IsAssignableFrom(type); @@ -84,10 +89,25 @@ namespace Server [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsText(Type t) => IsType(t, OfText); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsParsable(Type t) => - IsChar(t) || IsType(t, OfGuid) || - IsType(t, OfTimeSpan) || IsNumeric(t) || IsDecimal(t) || t.IsDefined(OfParsable, false); + public static bool IsParsable(Type t) + { + _isParsable ??= new(); + if (_isParsable.TryGetValue(t, out var isParsable)) + { + return isParsable; + } + + foreach (var x in t.GetInterfaces()) + { + if (x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IParsable<>)) + { + isParsable = true; + break; + } + } + + return _isParsable[t] = isParsable; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsDecimal(Type t) => Array.IndexOf(DecimalTypes, t) >= 0; @@ -98,9 +118,16 @@ namespace Server [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsEntity(Type t) => OfEntity.IsAssignableFrom(t); + private static Dictionary _parseMethods; + public static object Parse(Type t, string value) { - var method = t.GetMethod("Parse", _parseStringParamTypes); + _parseMethods ??= new(); + if (!_parseMethods.TryGetValue(t, out var method)) + { + _parseMethods[t] = method = t.GetMethod("Parse", ParseStringParamTypes); + } + _parseParams[0] = value; return method?.Invoke(null, _parseParams); } @@ -160,11 +187,11 @@ namespace Server return null; } - if (value.StartsWithOrdinal("0x") && IsNumeric(type)) + if (IsNumeric(type)) { try { - constructed = Convert.ChangeType(Convert.ToUInt64(value[2..], 16), type); + constructed = Convert.ChangeType(Convert.ToUInt64(value), type); return null; } catch diff --git a/Projects/UOContent/World Saves/AutoArchive.cs b/Projects/UOContent/World Saves/AutoArchive.cs index 0eaf3b180..e0c3aa60e 100755 --- a/Projects/UOContent/World Saves/AutoArchive.cs +++ b/Projects/UOContent/World Saves/AutoArchive.cs @@ -416,7 +416,12 @@ namespace Server.Saves var i = 0; foreach (var part in value.Tokenize('-')) { - parts[i++] = int.Parse(part); + if (!int.TryParse(part, out var partValue)) + { + break; + } + + parts[i++] = partValue; } if (i == 0)