From d7d914df6cffc45239eeff812bf32bdf5e40403a Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 6 Nov 2022 17:37:30 -0800 Subject: [PATCH] fix: Adds ISpanFormattable to Geometry structs (#1231) * Adds ISpanFormattable to geometry structs and makes ToString() near-zero-allocation. * Adds IEquatable, and Parsable to Rectangle3D to get it in-line with the other structs. Closes #1067 --- .../Tests/Geometry/Point2DTests.cs | 71 ++++++- .../Tests/Geometry/Point3DTests.cs | 72 +++++++ .../Tests/Geometry/Rectangle2DTests.cs | 72 +++++++ .../Tests/Geometry/Rectangle3DTests.cs | 72 +++++++ .../Tests/Geometry/WorldLocationTests.cs | 105 ++++++++++ Projects/Server/Geometry/Point2D.cs | 3 +- Projects/Server/Geometry/Point3D.cs | 25 ++- Projects/Server/Geometry/Rectangle2D.cs | 195 ++++++++++-------- Projects/Server/Geometry/Rectangle3D.cs | 139 ++++++++----- Projects/Server/Geometry/WorldLocation.cs | 122 +++++++---- Projects/Server/Maps/Map.cs | 23 ++- Projects/Server/Serial.cs | 17 +- 12 files changed, 721 insertions(+), 195 deletions(-) create mode 100644 Projects/Server.Tests/Tests/Geometry/Point3DTests.cs create mode 100644 Projects/Server.Tests/Tests/Geometry/Rectangle2DTests.cs create mode 100644 Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs create mode 100644 Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs diff --git a/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs b/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs index 251beff74..bc78a3c0d 100644 --- a/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs +++ b/Projects/Server.Tests/Tests/Geometry/Point2DTests.cs @@ -1,17 +1,68 @@ using System; using Xunit; -namespace Server.Tests +namespace Server.Tests; + +public class Point2DTests { - public class Point2DTests + [Fact] + public void TestPoint2DToString() { - [Fact] - public void TestToString() - { - Assert.Equal("(0, 0)", new Point2D(0, 0).ToString()); - Assert.Equal("(1, 1)", new Point2D(1, 1).ToString()); - Assert.Equal($"({Int32.MaxValue}, {Int32.MaxValue})", new Point2D(Int32.MaxValue, Int32.MaxValue).ToString()); - Assert.Equal($"({Int32.MinValue}, {Int32.MinValue})", new Point2D(Int32.MinValue, Int32.MinValue).ToString()); - } + Assert.Equal("(0, 0)", new Point2D(0, 0).ToString()); + Assert.Equal("(1, 1)", new Point2D(1, 1).ToString()); + Assert.Equal($"({int.MaxValue}, {int.MaxValue})", new Point2D(int.MaxValue, int.MaxValue).ToString()); + Assert.Equal($"({int.MinValue}, {int.MaxValue})", new Point2D(int.MinValue, int.MaxValue).ToString()); + Assert.Equal($"({int.MinValue}, {int.MinValue})", new Point2D(int.MinValue, int.MinValue).ToString()); + Assert.Equal($"({int.MaxValue}, {int.MinValue})", new Point2D(int.MaxValue, int.MinValue).ToString()); + } + + [Fact] + public void TestPoint2DTryFormatSucceeds() + { + char[] array = new char[128]; + + var p1 = new Point2D(0, 0); + Assert.True(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(6, cp1); + Array.Clear(array); + + var p2 = new Point2D(1, 1); + Assert.True(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(6, cp2); + Array.Clear(array); + + var p3 = new Point2D(int.MaxValue, int.MaxValue); + Assert.True(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(4 + 20, cp3); + Array.Clear(array); + + var p4 = new Point2D(int.MinValue, int.MinValue); + Assert.True(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(4 + 22, cp4); + } + + [Fact] + public void TestPoint2DTryFormatFails() + { + char[] array = new char[1]; + + var p1 = new Point2D(0, 0); + Assert.False(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(0, cp1); + Array.Clear(array); + + var p2 = new Point2D(1, 1); + Assert.False(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(0, cp2); + Array.Clear(array); + + var p3 = new Point2D(int.MaxValue, int.MaxValue); + Assert.False(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(0, cp3); + Array.Clear(array); + + var p4 = new Point2D(int.MinValue, int.MinValue); + Assert.False(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(0, cp4); } } diff --git a/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs b/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs new file mode 100644 index 000000000..07a6da0a6 --- /dev/null +++ b/Projects/Server.Tests/Tests/Geometry/Point3DTests.cs @@ -0,0 +1,72 @@ +using System; +using Xunit; + +namespace Server.Tests; + +public class Point3DTests +{ + [Fact] + public void TestPoint3DToString() + { + const int max = int.MaxValue; + const int min = int.MinValue; + Assert.Equal("(0, 0, 0)", new Point3D(0, 0, 0).ToString()); + Assert.Equal("(1, 1, 1)", new Point3D(1, 1, 1).ToString()); + Assert.Equal($"({max}, {max}, {max})", new Point3D(max, max, max).ToString()); + Assert.Equal($"({min}, {min}, {min})", new Point3D(min, min, min).ToString()); + } + + [Fact] + public void TestPoint3DTryFormatSucceeds() + { + const int max = int.MaxValue; + const int min = int.MinValue; + char[] array = new char[128]; + + var p1 = new Point3D(0, 0, 0); + Assert.True(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(6 + 3, cp1); + Array.Clear(array); + + var p2 = new Point3D(1, 1, 1); + Assert.True(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(6 + 3, cp2); + Array.Clear(array); + + var p3 = new Point3D(max, max, max); + Assert.True(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(6 + 30, cp3); + Array.Clear(array); + + var p4 = new Point3D(min, min, min); + Assert.True(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(6 + 33, cp4); + } + + [Fact] + public void TestPoint3DTryFormatFails() + { + const int max = int.MaxValue; + const int min = int.MinValue; + char[] array = new char[1]; + + var p1 = new Point3D(0, 0, 0); + Assert.False(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(0, cp1); + Array.Clear(array); + + var p2 = new Point3D(1, 1, 1); + Assert.False(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(0, cp2); + Array.Clear(array); + + var p3 = new Point3D(max, max, max); + Assert.False(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(0, cp3); + Array.Clear(array); + + var p4 = new Point3D(min, min, min); + Assert.False(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(0, cp4); + } +} diff --git a/Projects/Server.Tests/Tests/Geometry/Rectangle2DTests.cs b/Projects/Server.Tests/Tests/Geometry/Rectangle2DTests.cs new file mode 100644 index 000000000..c0b0c293b --- /dev/null +++ b/Projects/Server.Tests/Tests/Geometry/Rectangle2DTests.cs @@ -0,0 +1,72 @@ +using System; +using Xunit; + +namespace Server.Tests; + +public class Rectangle2DTests +{ + [Fact] + public void TestRectangle2DToString() + { + const int max = int.MaxValue; + const int min = int.MinValue; + Assert.Equal("(0, 0)+(0, 0)", new Rectangle2D(0, 0, 0, 0).ToString()); + Assert.Equal("(1, 1)+(1, 1)", new Rectangle2D(1, 1, 1, 1).ToString()); + Assert.Equal($"({max}, {max})+({max}, {max})", new Rectangle2D(max, max, max, max).ToString()); + Assert.Equal($"({min}, {min})+({min}, {min})", new Rectangle2D(min, min, min, min).ToString()); + } + + [Fact] + public void TestRectangle2DTryFormatSucceeds() + { + const int max = int.MaxValue; + const int min = int.MinValue; + char[] array = new char[128]; + + var p1 = new Rectangle2D(0, 0, 0, 0); + Assert.True(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(9 + 4, cp1); + Array.Clear(array); + + var p2 = new Rectangle2D(1, 1, 1, 1); + Assert.True(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(9 + 4, cp2); + Array.Clear(array); + + var p3 = new Rectangle2D(max, max, max, max); + Assert.True(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(9 + 4 * 10, cp3); + Array.Clear(array); + + var p4 = new Rectangle2D(min, min, min, min); + Assert.True(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(9 + 4 * 11, cp4); + } + + [Fact] + public void TestRectangle2DTryFormatFails() + { + const int max = int.MaxValue; + const int min = int.MinValue; + char[] array = new char[1]; + + var p1 = new Rectangle2D(0, 0, 0, 0); + Assert.False(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(0, cp1); + Array.Clear(array); + + var p2 = new Rectangle2D(1, 1, 1, 1); + Assert.False(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(0, cp2); + Array.Clear(array); + + var p3 = new Rectangle2D(max, max, max, max); + Assert.False(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(0, cp3); + Array.Clear(array); + + var p4 = new Rectangle2D(min, min, min, min); + Assert.False(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(0, cp4); + } +} diff --git a/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs b/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs new file mode 100644 index 000000000..682abf493 --- /dev/null +++ b/Projects/Server.Tests/Tests/Geometry/Rectangle3DTests.cs @@ -0,0 +1,72 @@ +using System; +using Xunit; + +namespace Server.Tests; + +public class Rectangle3DTests +{ + [Fact] + public void TestRectangle3DToString() + { + const int max = int.MaxValue; + const int min = int.MinValue; + Assert.Equal("(0, 0, 0)+(0, 0, 0)", new Rectangle3D(0, 0, 0, 0, 0, 0).ToString()); + Assert.Equal("(1, 1, 1)+(1, 1, 1)", new Rectangle3D(1, 1, 1, 1, 1, 1).ToString()); + Assert.Equal($"({max}, {max}, {max})+({max}, {max}, {max})", new Rectangle3D(max, max, max, max, max, max).ToString()); + Assert.Equal($"({min}, {min}, {min})+({min}, {min}, {min})", new Rectangle3D(min, min, min, min, min, min).ToString()); + } + + [Fact] + public void TestRectangle3DTryFormatSucceeds() + { + const int max = int.MaxValue; + const int min = int.MinValue; + char[] array = new char[128]; + + var p1 = new Rectangle3D(0, 0, 0, 0, 0, 0); + Assert.True(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(13 + 6, cp1); + Array.Clear(array); + + var p2 = new Rectangle3D(1, 1, 1, 1, 1, 1); + Assert.True(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(13 + 6, cp2); + Array.Clear(array); + + var p3 = new Rectangle3D(max, max, max, max, max, max); + Assert.True(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(13 + 6 * 10, cp3); + Array.Clear(array); + + var p4 = new Rectangle3D(min, min, min, min, min, min); + Assert.True(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(13 + 6 * 11, cp4); + } + + [Fact] + public void TestRectangle3DTryFormatFails() + { + const int max = int.MaxValue; + const int min = int.MinValue; + char[] array = new char[1]; + + var p1 = new Rectangle3D(0, 0, 0, 0, 0, 0); + Assert.False(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(0, cp1); + Array.Clear(array); + + var p2 = new Rectangle3D(1, 1, 1, 1, 1, 1); + Assert.False(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(0, cp2); + Array.Clear(array); + + var p3 = new Rectangle3D(max, max, max, max, max, max); + Assert.False(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(0, cp3); + Array.Clear(array); + + var p4 = new Rectangle3D(min, min, min, min, min, min); + Assert.False(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(0, cp4); + } +} diff --git a/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs new file mode 100644 index 000000000..07d9ee5d9 --- /dev/null +++ b/Projects/Server.Tests/Tests/Geometry/WorldLocationTests.cs @@ -0,0 +1,105 @@ +using System; +using Xunit; + +namespace Server.Tests; + +public sealed class WorldLocationTests +{ + private static Map CreateMap(string name) => new(0, 0, 0, 1, 1, 0, name, MapRules.Internal); + + private const string _nullMap = "(-null-)"; + + private const string _superLongMapName = + "Some super long map name that nobody should be using, but probably someone will just because you can!"; + + [Fact] + public void TestWorldLocationToString() + { + const string mapName = "Internal Map"; + var map = CreateMap(mapName); + + const int max = int.MaxValue; + const int min = int.MinValue; + Assert.Equal($"(0, 0, 0) [{mapName}]", new WorldLocation(0, 0, 0, map).ToString()); + Assert.Equal($"(1, 1, 1) [{mapName}]", new WorldLocation(1, 1, 1, map).ToString()); + + Assert.Equal($"(0, 0, 0) [{_nullMap}]", new WorldLocation(0, 0, 0, null).ToString()); + Assert.Equal($"(1, 1, 1) [{_nullMap}]", new WorldLocation(1, 1, 1, null).ToString()); + + Assert.Equal($"({max}, {max}, {max}) [{mapName}]", new WorldLocation(max, max, max, map).ToString()); + Assert.Equal($"({min}, {min}, {min}) [{mapName}]", new WorldLocation(min, min, min, map).ToString()); + + Assert.Equal($"({max}, {max}, {max}) [{_nullMap}]", new WorldLocation(max, max, max, null).ToString()); + Assert.Equal($"({min}, {min}, {min}) [{_nullMap}]", new WorldLocation(min, min, min, null).ToString()); + + map = CreateMap(_superLongMapName); + + Assert.Equal($"({max}, {max}, {max}) [{_superLongMapName}]", new WorldLocation(max, max, max, map).ToString()); + Assert.Equal($"({min}, {min}, {min}) [{_superLongMapName}]", new WorldLocation(min, min, min, map).ToString()); + } + + [Theory] + [InlineData(null)] + [InlineData("Internal Map")] + [InlineData(_superLongMapName)] + public void TestWorldLocationTryFormatSucceeds(string mapName) + { + var map = mapName != null ? CreateMap(mapName) : null; + var mapNameLength = mapName?.Length ?? _nullMap.Length; + + const int max = int.MaxValue; + const int min = int.MinValue; + char[] array = new char[128]; + + var p1 = new WorldLocation(0, 0, 0, map); + Assert.True(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(9 + 3 + mapNameLength, cp1); + Array.Clear(array); + + var p2 = new WorldLocation(1, 1, 1, map); + Assert.True(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(9 + 3 + mapNameLength, cp2); + Array.Clear(array); + + array = new char[256]; // We need a bigger buffer! + var p3 = new WorldLocation(max, max, max, map); + Assert.True(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(9 + 30 + mapNameLength, cp3); + Array.Clear(array); + + var p4 = new WorldLocation(min, min, min, map); + Assert.True(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(9 + 33 + mapNameLength, cp4); + } + + [Theory] + [InlineData(null)] + [InlineData("Internal Map")] + public void TestWorldLocationTryFormatFails(string mapName) + { + var map = mapName != null ? CreateMap(mapName) : null; + + const int max = int.MaxValue; + const int min = int.MinValue; + char[] array = new char[1]; + + var p1 = new WorldLocation(0, 0, 0, map); + Assert.False(p1.TryFormat(array, out var cp1, null, null)); + Assert.Equal(0, cp1); + Array.Clear(array); + + var p2 = new WorldLocation(1, 1, 1, map); + Assert.False(p2.TryFormat(array, out var cp2, null, null)); + Assert.Equal(0, cp2); + Array.Clear(array); + + var p3 = new WorldLocation(max, max, max, map); + Assert.False(p3.TryFormat(array, out var cp3, null, null)); + Assert.Equal(0, cp3); + Array.Clear(array); + + var p4 = new WorldLocation(min, min, min, map); + Assert.False(p4.TryFormat(array, out var cp4, null, null)); + Assert.Equal(0, cp4); + } +} diff --git a/Projects/Server/Geometry/Point2D.cs b/Projects/Server/Geometry/Point2D.cs index efa9dbcbc..c4d1b97b0 100644 --- a/Projects/Server/Geometry/Point2D.cs +++ b/Projects/Server/Geometry/Point2D.cs @@ -113,7 +113,8 @@ public struct Point2D public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) => destination.TryWrite(provider, $"({m_X}, {m_Y})", out charsWritten); - public override string ToString() { + public override string ToString() + { // Maximum number of characters that are needed to represent this: // 4 characters for (, ) // Up to 11 characters to represent each integer diff --git a/Projects/Server/Geometry/Point3D.cs b/Projects/Server/Geometry/Point3D.cs index e08ca3861..430624068 100644 --- a/Projects/Server/Geometry/Point3D.cs +++ b/Projects/Server/Geometry/Point3D.cs @@ -21,7 +21,7 @@ namespace Server; [Parsable] public struct Point3D : IPoint3D, IComparable, IComparable, IEquatable, IEquatable, - IEquatable + IEquatable, ISpanFormattable { internal int m_X; internal int m_Y; @@ -70,8 +70,6 @@ public struct Point3D m_Z = z; } - public override string ToString() => $"({m_X}, {m_Y}, {m_Z})"; - public bool Equals(Point3D other) => m_X == other.m_X && m_Y == other.m_Y && m_Z == other.m_Z; public bool Equals(IPoint3D other) => @@ -164,4 +162,25 @@ public struct Point3D return m_Z.CompareTo(other.Z); } + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + => destination.TryWrite(provider, $"({m_X}, {m_Y}, {m_Z})", out charsWritten); + + public override string ToString() + { + // Maximum number of characters that are needed to represent this: + // 6 characters for (, , ) + // Up to 11 characters to represent each integer + const int maxLength = 6 + 11 * 3; + Span span = stackalloc char[maxLength]; + TryFormat(span, out var charsWritten, null, null); + return span[..charsWritten].ToString(); + } + + public string ToString(string format, IFormatProvider formatProvider) + { + // format and formatProvider are not doing anything right now, so use the + // default ToString implementation. + return ToString(); + } } diff --git a/Projects/Server/Geometry/Rectangle2D.cs b/Projects/Server/Geometry/Rectangle2D.cs index 8079c210f..9cb8d9bbb 100644 --- a/Projects/Server/Geometry/Rectangle2D.cs +++ b/Projects/Server/Geometry/Rectangle2D.cs @@ -20,37 +20,104 @@ namespace Server; [NoSort] [Parsable] [PropertyObject] -public struct Rectangle2D +public struct Rectangle2D : IEquatable, ISpanFormattable { - public bool Equals(Rectangle2D other) => m_Start == other.m_Start && m_End == other.m_End; - - public static bool operator ==(Rectangle2D l, Rectangle2D r) => l.m_Start == r.m_Start && l.m_End == r.m_End; - - public static bool operator !=(Rectangle2D l, Rectangle2D r) => l.m_Start != r.m_Start || l.m_End != r.m_End; - - public override int GetHashCode() => HashCode.Combine(m_Start, m_End); - - private Point2D m_Start; - private Point2D m_End; + private Point2D _start; + private Point2D _end; public Rectangle2D(Point2D start, Point2D end) { - m_Start = start; - m_End = end; + _start = start; + _end = end; } public Rectangle2D(int x, int y, int width, int height) { - m_Start = new Point2D(x, y); - m_End = new Point2D(x + width, y + height); + _start = new Point2D(x, y); + _end = new Point2D(x + width, y + height); } public void Set(int x, int y, int width, int height) { - m_Start = new Point2D(x, y); - m_End = new Point2D(x + width, y + height); + _start = new Point2D(x, y); + _end = new Point2D(x + width, y + height); } + [CommandProperty(AccessLevel.Counselor)] + public Point2D Start + { + get => _start; + set => _start = value; + } + + [CommandProperty(AccessLevel.Counselor)] + public Point2D End + { + get => _end; + set => _end = value; + } + + [CommandProperty(AccessLevel.Counselor)] + public int X + { + get => _start.m_X; + set => _start.m_X = value; + } + + [CommandProperty(AccessLevel.Counselor)] + public int Y + { + get => _start.m_Y; + set => _start.m_Y = value; + } + + [CommandProperty(AccessLevel.Counselor)] + public int Width + { + get => _end.m_X - _start.m_X; + set => _end.m_X = _start.m_X + value; + } + + [CommandProperty(AccessLevel.Counselor)] + public int Height + { + get => _end.m_Y - _start.m_Y; + set => _end.m_Y = _start.m_Y + value; + } + + public void MakeHold(Rectangle2D r) + { + if (r._start.m_X < _start.m_X) + { + _start.m_X = r._start.m_X; + } + + if (r._start.m_Y < _start.m_Y) + { + _start.m_Y = r._start.m_Y; + } + + if (r._end.m_X > _end.m_X) + { + _end.m_X = r._end.m_X; + } + + if (r._end.m_Y > _end.m_Y) + { + _end.m_Y = r._end.m_Y; + } + } + + public bool Equals(Rectangle2D other) => _start == other._start && _end == other._end; + + public override bool Equals(object obj) => obj is Rectangle2D other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(_start, _end); + + public static bool operator ==(Rectangle2D l, Rectangle2D r) => l._start == r._start && l._end == r._end; + + 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('('); @@ -76,79 +143,33 @@ public struct Rectangle2D return new Rectangle2D(x, y, w, h); } - [CommandProperty(AccessLevel.Counselor)] - public Point2D Start + 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; + + public bool Contains(Point2D 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; + + public bool Contains(int x, int y) => + _start.m_X <= x && _start.m_Y <= y && _end.m_X > x && _end.m_Y > y; + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + => destination.TryWrite(provider, $"({X}, {Y})+({Width}, {Height})", out charsWritten); + + public override string ToString() { - get => m_Start; - set => m_Start = value; + // Maximum number of characters that are needed to represent this: + // 9 characters for (, )+(, ) + // Up to 11 characters to represent each integer + const int maxLength = 9 + 11 * 4; + Span span = stackalloc char[maxLength]; + TryFormat(span, out var charsWritten, null, null); + return span[..charsWritten].ToString(); } - [CommandProperty(AccessLevel.Counselor)] - public Point2D End + public string ToString(string format, IFormatProvider formatProvider) { - get => m_End; - set => m_End = value; + // format and formatProvider are not doing anything right now, so use the + // default ToString implementation. + return ToString(); } - - [CommandProperty(AccessLevel.Counselor)] - public int X - { - get => m_Start.m_X; - set => m_Start.m_X = value; - } - - [CommandProperty(AccessLevel.Counselor)] - public int Y - { - get => m_Start.m_Y; - set => m_Start.m_Y = value; - } - - [CommandProperty(AccessLevel.Counselor)] - public int Width - { - get => m_End.m_X - m_Start.m_X; - set => m_End.m_X = m_Start.m_X + value; - } - - [CommandProperty(AccessLevel.Counselor)] - public int Height - { - get => m_End.m_Y - m_Start.m_Y; - set => m_End.m_Y = m_Start.m_Y + value; - } - - public void MakeHold(Rectangle2D r) - { - if (r.m_Start.m_X < m_Start.m_X) - { - m_Start.m_X = r.m_Start.m_X; - } - - if (r.m_Start.m_Y < m_Start.m_Y) - { - m_Start.m_Y = r.m_Start.m_Y; - } - - if (r.m_End.m_X > m_End.m_X) - { - m_End.m_X = r.m_End.m_X; - } - - if (r.m_End.m_Y > m_End.m_Y) - { - m_End.m_Y = r.m_End.m_Y; - } - } - - public readonly bool Contains(Point3D p) => - m_Start.m_X <= p.m_X && m_Start.m_Y <= p.m_Y && m_End.m_X > p.m_X && m_End.m_Y > p.m_Y; - - public readonly bool Contains(Point2D p) => - m_Start.m_X <= p.m_X && m_Start.m_Y <= p.m_Y && m_End.m_X > p.m_X && m_End.m_Y > p.m_Y; - - public readonly bool Contains(int x, int y) => - m_Start.m_X <= x && m_Start.m_Y <= y && m_End.m_X > x && m_End.m_Y > y; - - public override string ToString() => $"({X}, {Y})+({Width}, {Height})"; } diff --git a/Projects/Server/Geometry/Rectangle3D.cs b/Projects/Server/Geometry/Rectangle3D.cs index 9ea17ea96..58d5059f6 100644 --- a/Projects/Server/Geometry/Rectangle3D.cs +++ b/Projects/Server/Geometry/Rectangle3D.cs @@ -13,129 +13,164 @@ * along with this program. If not, see . * *************************************************************************/ +using System; +using System.Buffers; + namespace Server; [NoSort] +[Parsable] [PropertyObject] -public struct Rectangle3D +public struct Rectangle3D : IEquatable, ISpanFormattable { - private Point3D m_Start; - private Point3D m_End; + private Point3D _start; + private Point3D _end; public Rectangle3D(Point3D start, Point3D end) { - m_Start = start; - m_End = end; + _start = start; + _end = end; } public Rectangle3D(int x, int y, int z, int width, int height, int depth) { - m_Start = new Point3D(x, y, z); - m_End = new Point3D(x + width, y + height, z + depth); + _start = new Point3D(x, y, z); + _end = new Point3D(x + width, y + height, z + depth); } [CommandProperty(AccessLevel.Counselor)] public Point3D Start { - get => m_Start; - set => m_Start = value; + get => _start; + set => _start = value; } [CommandProperty(AccessLevel.Counselor)] public Point3D End { - get => m_End; - set => m_End = value; + get => _end; + set => _end = value; } [CommandProperty(AccessLevel.Counselor)] public int X { - get => m_Start.m_X; - set => m_Start.m_X = value; + get => _start.m_X; + set => _start.m_X = value; } [CommandProperty(AccessLevel.Counselor)] public int Y { - get => m_Start.m_Y; - set => m_Start.m_Y = value; + get => _start.m_Y; + set => _start.m_Y = value; } [CommandProperty(AccessLevel.Counselor)] public int Z { - get => m_Start.m_Z; - set => m_Start.m_Z = value; + get => _start.m_Z; + set => _start.m_Z = value; } [CommandProperty(AccessLevel.Counselor)] - public int Width => m_End.X - m_Start.X; + public int Width => _end.X - _start.X; [CommandProperty(AccessLevel.Counselor)] - public int Height => m_End.Y - m_Start.Y; + public int Height => _end.Y - _start.Y; [CommandProperty(AccessLevel.Counselor)] - public int Depth => m_End.Z - m_Start.Z; + public int Depth => _end.Z - _start.Z; + + public bool Equals(Rectangle3D other) => _start == other._start && _end == other._end; + + public override bool Equals(object obj) => obj is Rectangle3D other && Equals(other); + + public static bool operator ==(Rectangle3D l, Rectangle3D r) => l._start == r._start && l._end == r._end; + + public static bool operator !=(Rectangle3D l, Rectangle3D r) => l._start != r._start || l._end != r._end; + + public override int GetHashCode() => HashCode.Combine(_start, _end); public void MakeHold(Rectangle3D r) { - if (r.m_Start.m_X < m_Start.m_X) + if (r._start.m_X < _start.m_X) { - m_Start.m_X = r.m_Start.m_X; + _start.m_X = r._start.m_X; } - if (r.m_Start.m_Y < m_Start.m_Y) + if (r._start.m_Y < _start.m_Y) { - m_Start.m_Y = r.m_Start.m_Y; + _start.m_Y = r._start.m_Y; } - if (r.m_Start.m_Z < m_Start.m_Z) + if (r._start.m_Z < _start.m_Z) { - m_Start.m_Z = r.m_Start.m_Z; + _start.m_Z = r._start.m_Z; } - if (r.m_End.m_X > m_End.m_X) + if (r._end.m_X > _end.m_X) { - m_End.m_X = r.m_End.m_X; + _end.m_X = r._end.m_X; } - if (r.m_End.m_Y > m_End.m_Y) + if (r._end.m_Y > _end.m_Y) { - m_End.m_Y = r.m_End.m_Y; + _end.m_Y = r._end.m_Y; } - if (r.m_End.m_Z < m_End.m_Z) + if (r._end.m_Z < _end.m_Z) { - m_End.m_Z = r.m_End.m_Z; + _end.m_Z = r._end.m_Z; } } public bool Contains(Point3D p) => - p.m_X >= m_Start.m_X - && p.m_X < m_End.m_X - && p.m_Y >= m_Start.m_Y - && p.m_Y < m_End.m_Y - && p.m_Z >= m_Start.m_Z - && p.m_Z < m_End.m_Z; + p.m_X >= _start.m_X + && p.m_X < _end.m_X + && p.m_Y >= _start.m_Y + && p.m_Y < _end.m_Y + && p.m_Z >= _start.m_Z + && p.m_Z < _end.m_Z; public bool Contains(Point2D p) => - p.m_X >= m_Start.m_X - && p.m_X < m_End.m_X - && p.m_Y >= m_Start.m_Y - && p.m_Y < m_End.m_Y; + p.m_X >= _start.m_X + && p.m_X < _end.m_X + && p.m_Y >= _start.m_Y + && p.m_Y < _end.m_Y; public bool Contains(IPoint2D p) => - p.X >= m_Start.m_X - && p.X < m_End.m_X - && p.Y >= m_Start.m_Y - && p.Y < m_End.m_Y; + p.X >= _start.m_X + && p.X < _end.m_X + && p.Y >= _start.m_Y + && p.Y < _end.m_Y; public bool Contains(IPoint3D p) => - p.X >= m_Start.m_X - && p.X < m_End.m_X - && p.Y >= m_Start.m_Y - && p.Y < m_End.m_Y - && p.Z >= m_Start.m_Z - && p.Z < m_End.m_Z; + p.X >= _start.m_X + && p.X < _end.m_X + && p.Y >= _start.m_Y + && p.Y < _end.m_Y + && p.Z >= _start.m_Z + && p.Z < _end.m_Z; + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + => destination.TryWrite(provider, $"({X}, {Y}, {Z})+({Width}, {Height}, {Depth})", out charsWritten); + + public override string ToString() + { + // Maximum number of characters that are needed to represent this: + // 13 characters for (, , )+(, , ) + // Up to 11 characters to represent each integer + const int maxLength = 13 + 11 * 6; + Span span = stackalloc char[maxLength]; + TryFormat(span, out var charsWritten, null, null); + return span[..charsWritten].ToString(); + } + + public string ToString(string format, IFormatProvider formatProvider) + { + // format and formatProvider are not doing anything right now, so use the + // default ToString implementation. + return ToString(); + } } diff --git a/Projects/Server/Geometry/WorldLocation.cs b/Projects/Server/Geometry/WorldLocation.cs index 9b6a5cd7c..016dc70ea 100644 --- a/Projects/Server/Geometry/WorldLocation.cs +++ b/Projects/Server/Geometry/WorldLocation.cs @@ -14,52 +14,53 @@ *************************************************************************/ using System; +using System.Buffers; using System.Collections.Generic; namespace Server; [Parsable] -public struct WorldLocation - : IPoint3D, IComparable, IEquatable, IEquatable, IEquatable +public struct WorldLocation : IPoint3D, IComparable, IEquatable, IEquatable, + ISpanFormattable { - internal Point3D m_Loc; - internal Map m_Map; + internal Point3D _loc; + internal Map _map; public static readonly WorldLocation Zero = new(0, 0, 0, Map.Internal); [CommandProperty(AccessLevel.Counselor)] public Point3D Location { - get => m_Loc; - set => m_Loc = value; + get => _loc; + set => _loc = value; } [CommandProperty(AccessLevel.Counselor)] public int X { - get => m_Loc.m_X; - set => m_Loc.m_X = value; + get => _loc.m_X; + set => _loc.m_X = value; } [CommandProperty(AccessLevel.Counselor)] public int Y { - get => m_Loc.m_Y; - set => m_Loc.m_Y = value; + get => _loc.m_Y; + set => _loc.m_Y = value; } [CommandProperty(AccessLevel.Counselor)] public int Z { - get => m_Loc.m_Z; - set => m_Loc.m_Z = value; + get => _loc.m_Z; + set => _loc.m_Z = value; } [CommandProperty(AccessLevel.Counselor)] public Map Map { - get => m_Map; - set => m_Map = value; + get => _map; + set => _map = value; } public WorldLocation(IEntity e) : this(e.Location.X, e.Location.Y, e.Location.Z, e.Map) @@ -80,65 +81,62 @@ public struct WorldLocation public WorldLocation(int x, int y, int z, Map map) { - m_Loc.m_X = x; - m_Loc.m_Y = y; - m_Loc.m_Z = z; - m_Map = map; + _loc.m_X = x; + _loc.m_Y = y; + _loc.m_Z = z; + _map = map; } - public override string ToString() => - $"({m_Loc.m_X}, {m_Loc.m_Y}, {m_Loc.m_Z}, {m_Map?.ToString() ?? "(-null-)"})"; - public bool Equals(WorldLocation other) => - m_Loc.Equals(other.m_Loc) && m_Map.MapID == other.m_Map.MapID; + _loc.Equals(other._loc) && _map.MapID == other._map.MapID; public bool Equals(IEntity other) => - !ReferenceEquals(other, null) && m_Loc == other.Location && - m_Map.MapID == other.Map.MapID; + !ReferenceEquals(other, null) && _loc == other.Location && + _map.MapID == other.Map.MapID; public override bool Equals(object obj) => obj is WorldLocation other && Equals(other); - public override int GetHashCode() => HashCode.Combine(m_Loc, m_Map); + public override int GetHashCode() => HashCode.Combine(_loc, _map); public int CompareTo(WorldLocation other) { - var locComparison = m_Loc.CompareTo(other.m_Loc); - return locComparison != 0 ? locComparison : Comparer.Default.Compare(m_Map, other.m_Map); + var locComparison = _loc.CompareTo(other._loc); + return locComparison != 0 ? locComparison : Comparer.Default.Compare(_map, other._map); } public static implicit operator Point3D(WorldLocation worldLocation) => worldLocation.Location; public static bool operator ==(WorldLocation l, WorldLocation r) => - l.m_Loc == r.m_Loc && l.m_Map == r.m_Map; + l._loc == r._loc && l._map == r._map; public static bool operator ==(WorldLocation l, IEntity r) => - !ReferenceEquals(r, null) && l.m_Loc == r.Location && l.m_Map == r.Map; + !ReferenceEquals(r, null) && l._loc == r.Location && l._map == r.Map; - public static bool operator !=(WorldLocation l, WorldLocation r) => l.m_Loc != r.m_Loc && l.m_Map != r.m_Map; + public static bool operator !=(WorldLocation l, WorldLocation r) => l._loc != r._loc && l._map != r._map; public static bool operator !=(WorldLocation l, IEntity r) => - !ReferenceEquals(r, null) && l.m_Loc != r.Location && l.m_Map != r.Map; + !ReferenceEquals(r, null) && l._loc != r.Location && l._map != r.Map; - public static bool operator >(WorldLocation l, WorldLocation r) => l.m_Loc > r.m_Loc && l.m_Map == r.m_Map; + public static bool operator >(WorldLocation l, WorldLocation r) => l._loc > r._loc && l._map == r._map; public static bool operator >(WorldLocation l, IEntity r) => - !ReferenceEquals(r, null) && l.m_Loc > r.Location && l.m_Map == r.Map; + !ReferenceEquals(r, null) && l._loc > r.Location && l._map == r.Map; - public static bool operator <(WorldLocation l, WorldLocation r) => l.m_Loc < r.m_Loc && l.m_Map == r.m_Map; + public static bool operator <(WorldLocation l, WorldLocation r) => l._loc < r._loc && l._map == r._map; public static bool operator <(WorldLocation l, IEntity r) => - !ReferenceEquals(r, null) && l.m_Loc < r.Location && l.m_Map == r.Map; + !ReferenceEquals(r, null) && l._loc < r.Location && l._map == r.Map; - public static bool operator >=(WorldLocation l, WorldLocation r) => l.m_Loc >= r.m_Loc && l.m_Map == r.m_Map; + public static bool operator >=(WorldLocation l, WorldLocation r) => l._loc >= r._loc && l._map == r._map; public static bool operator >=(WorldLocation l, IEntity r) => - !ReferenceEquals(r, null) && l.m_Loc >= r.Location && l.m_Map == r.Map; + !ReferenceEquals(r, null) && l._loc >= r.Location && l._map == r.Map; - public static bool operator <=(WorldLocation l, WorldLocation r) => l.m_Loc <= r.m_Loc && l.m_Map == r.m_Map; + public static bool operator <=(WorldLocation l, WorldLocation r) => l._loc <= r._loc && l._map == r._map; public static bool operator <=(WorldLocation l, IEntity r) => - !ReferenceEquals(r, null) && l.m_Loc <= r.Location && l.m_Map == r.Map; + !ReferenceEquals(r, null) && l._loc <= r.Location && l._map == r.Map; public static WorldLocation Parse(string value) { @@ -164,4 +162,50 @@ public struct WorldLocation return new WorldLocation(x, y, z, map); } + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + { + if (_map == null) + { + return destination.TryWrite(provider, $"({_loc.m_X}, {_loc.m_Y}, {_loc.m_Z}) [(-null-)]", out charsWritten); + } + + return destination.TryWrite(provider, $"({_loc.m_X}, {_loc.m_Y}, {_loc.m_Z}) [{_map}]", out charsWritten); + } + + public override string ToString() + { + if (_map == null) + { + // Maximum number of characters that are needed to represent this: + // 9 characters for (, , ) [(-null-)] + const int staticLength = 17; + // Up to 11 characters to represent each integer + const int maxLength = staticLength + 11 * 3; + Span span = stackalloc char[maxLength]; + TryFormat(span, out var charsWritten, null, null); + return span[..charsWritten].ToString(); + } + else + { + + int charsWritten; + char[] array = ArrayPool.Shared.Rent(128); + Span span = array.AsSpan(); + while (!TryFormat(span, out charsWritten, null, null)) + { + array = ArrayPool.Shared.Rent(array.Length * 2); + span = array.AsSpan(); + } + + return span[..charsWritten].ToString(); + } + } + + public string ToString(string format, IFormatProvider formatProvider) + { + // format and formatProvider are not doing anything right now, so use the + // default ToString implementation. + return ToString(); + } } diff --git a/Projects/Server/Maps/Map.cs b/Projects/Server/Maps/Map.cs index 88ef42d10..1b95cb977 100644 --- a/Projects/Server/Maps/Map.cs +++ b/Projects/Server/Maps/Map.cs @@ -314,7 +314,7 @@ public static class PooledEnumeration } [Parsable] -public sealed class Map : IComparable +public sealed class Map : IComparable, ISpanFormattable { public const int SectorSize = 16; public const int SectorShift = 4; @@ -525,8 +525,29 @@ public sealed class Map : IComparable return null; } + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + { + if (destination.Length >= Name.Length) + { + Name.CopyTo(destination); + charsWritten = Name.Length; + return true; + } + + charsWritten = 0; + return false; + } + + public override string ToString() => Name; + public string ToString(string format, IFormatProvider formatProvider) + { + // format and formatProvider are not doing anything right now, so use the + // default ToString implementation. + return ToString(); + } + public int GetAverageZ(int x, int y) { GetAverageZ(x, y, out _, out var avg, out _); diff --git a/Projects/Server/Serial.cs b/Projects/Server/Serial.cs index 1ad089243..0da142d65 100644 --- a/Projects/Server/Serial.cs +++ b/Projects/Server/Serial.cs @@ -118,9 +118,22 @@ public readonly struct Serial : IComparable, IComparable, IEquatab public static Serial operator --(Serial l) => (Serial)(l.Value - 1); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override string ToString() => $"{this}"; + public override string ToString() + { + // Maximum number of characters that are needed to represent this: + // 2 characters for 0x + // Up to 8 characters to represent the value in hex + Span span = stackalloc char[10]; + TryFormat(span, out var charsWritten, null, null); + return span[..charsWritten].ToString(); + } - public string ToString(string format, IFormatProvider formatProvider) => ToString(); + public string ToString(string format, IFormatProvider formatProvider) + { + // format and formatProvider are not doing anything right now, so use the + // default ToString implementation. + return ToString(); + } public bool TryFormat( Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider