diff --git a/VpSharp.Tests/src/CoordinateTests.cs b/VpSharp.Tests/src/CoordinateTests.cs new file mode 100644 index 0000000..8f50f8c --- /dev/null +++ b/VpSharp.Tests/src/CoordinateTests.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; + +namespace VpSharp.Tests; + +[TestClass] +public sealed class CoordinateTests +{ + [TestMethod] + public void TestAbsolute() + { + TestCoordinates("asdf", 0.0, 0.0, 0.0, 0.0, world: "asdf"); + TestCoordinates("1n 1w", 1.0, 0.0, 1.0); + TestCoordinates("test 10n 5e 1a 123", -5, 1, 10, 123, world: "test"); + TestCoordinates("4s 6w -10a 45", 6, -10, -4, 45); + TestCoordinates(" 100n 100w 1.5a 180", 100, 1.5, 100, 180); + TestCoordinates("2355.71S 3429.68E -0.37a 0", -3429.68, -0.37, -2355.71); + } + + [TestMethod] + public void TestRelative() + { + TestCoordinates("-1.1 +0 -1.2a", 0.0, -1.2, -1.1, 0.0, true); + TestCoordinates("+0 +0 +5a", 0.0, 5.0, 0.0, 0.0, true); + TestCoordinates("+1 +1 +1a", 1.0, 1.0, 1.0, 0.0, true); + } + + private static void TestCoordinates( + string input, + double x, + double y, + double z, + double yaw = 0.0, + bool isRelative = false, + string? world = null + ) + { + Coordinates coordinates = Coordinates.Parse(input); + + Trace.WriteLine("----"); + Trace.WriteLine($"Input: {input}"); + Trace.WriteLine($"Parsed: {coordinates}"); + Trace.WriteLine("----"); + + Assert.AreEqual(x, coordinates.X); + Assert.AreEqual(y, coordinates.Y); + Assert.AreEqual(z, coordinates.Z); + Assert.AreEqual(yaw, coordinates.Yaw); + Assert.AreEqual(isRelative, coordinates.IsRelative); + if (string.IsNullOrWhiteSpace(world)) + { + Assert.IsTrue(string.IsNullOrWhiteSpace(coordinates.World)); + } + else + { + Assert.AreEqual(world, coordinates.World); + } + } +} diff --git a/VpSharp/src/Coordinates.Serialization.cs b/VpSharp/src/Coordinates.Serialization.cs new file mode 100644 index 0000000..a914ee7 --- /dev/null +++ b/VpSharp/src/Coordinates.Serialization.cs @@ -0,0 +1,304 @@ +using System.Globalization; +using System.Text; +using Cysharp.Text; +using X10D.Linq; +using X10D.Text; + +namespace VpSharp; + +public readonly partial struct Coordinates +{ + private static class Serializer + { + public static string Serialize(in Coordinates coordinates, string format) + { + int count = Serialize(coordinates, format, Span.Empty); + Span chars = stackalloc char[count]; + Serialize(coordinates, format, chars); + return chars.ToString(); + } + + public static int Serialize(in Coordinates coordinates, string format, Span destination) + { + using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder(); + + if (!string.IsNullOrWhiteSpace(coordinates.World)) + { + builder.Append(coordinates.World); + builder.Append(' '); + } + + bool north = coordinates.Z >= 0.0; + bool west = coordinates.X >= 0.0; + bool up = coordinates.Y >= 0.0; + bool dir = coordinates.Yaw >= 0.0; + + if (coordinates.IsRelative) + { + if (north) + { + builder.Append('+'); + } + + builder.Append(string.Format(CultureInfo.InvariantCulture, format, coordinates.Z)); + builder.Append(' '); + + if (west) + { + builder.Append('+'); + } + + builder.Append(string.Format(CultureInfo.InvariantCulture, format, coordinates.X)); + builder.Append(' '); + + if (up) + { + builder.Append('+'); + } + + builder.Append(string.Format(CultureInfo.InvariantCulture, format, coordinates.Y)); + builder.Append("a "); + + if (dir) + { + builder.Append('+'); + } + + builder.Append(string.Format(CultureInfo.InvariantCulture, format, coordinates.Yaw)); + } + else + { + char zChar = north ? 'n' : 's'; + char xChar = west ? 'w' : 'e'; + + builder.Append(string.Format(CultureInfo.InvariantCulture, format, Math.Abs(coordinates.Z))); + builder.Append(zChar); + builder.Append(' '); + + builder.Append(string.Format(CultureInfo.InvariantCulture, format, Math.Abs(coordinates.X))); + builder.Append(xChar); + builder.Append(' '); + + builder.Append(string.Format(CultureInfo.InvariantCulture, format, coordinates.Y)); + builder.Append("a "); + + builder.Append(string.Format(CultureInfo.InvariantCulture, format, coordinates.Yaw)); + } + + ReadOnlySpan bytes = builder.AsSpan(); + Span chars = stackalloc char[bytes.Length]; + Encoding.UTF8.GetChars(bytes, chars); + + for (var index = 0; index < destination.Length; index++) + { + destination[index] = chars[index]; + } + + return builder.Length; + } + + public static Coordinates Deserialize(ReadOnlySpan value) + { + using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder(); + string? world = null; + var isRelative = false; + double x = 0.0, y = 0.0, z = 0.0, yaw = 0.0; + + var word = 0; + for (var index = 0; index < value.Length; index++) + { + char current = value[index]; + bool atEnd = index == value.Length - 1; + + if (atEnd || char.IsWhiteSpace(current)) + { + if (!builder.AsSpan().All(b => char.IsWhiteSpace((char)b))) + { + if (atEnd) + { + builder.Append(current); + } + + ProcessBuffer(); + word++; + } + + builder.Clear(); + } + else + { + builder.Append(current); + } + } + + return new Coordinates(world, x, y, z, yaw, isRelative); + + void ProcessBuffer() + { + ReadOnlySpan bytes = builder.AsSpan(); + Span chars = stackalloc char[bytes.Length]; + Encoding.UTF8.GetChars(bytes, chars); + bool hasWorld = !string.IsNullOrWhiteSpace(world); + + if (word == 0 && !IsUnitString(bytes)) + { + world = chars.ToString().AsNullIfWhiteSpace(); + } + else if (IsRelativeUnit(bytes)) + { + isRelative = true; + + switch (word) + { + case 0 when !hasWorld: + case 1 when hasWorld: + double.TryParse(chars, NumberStyles.Float, CultureInfo.InvariantCulture, out z); + break; + case 1 when !hasWorld: + case 2 when hasWorld: + double.TryParse(chars, NumberStyles.Float, CultureInfo.InvariantCulture, out x); + break; + case 2 when !hasWorld: + case 3 when hasWorld: + double.TryParse(chars, NumberStyles.Float, CultureInfo.InvariantCulture, out y); + break; + case 3 when !hasWorld: + case 4 when hasWorld: + double.TryParse(chars, NumberStyles.Float, CultureInfo.InvariantCulture, out yaw); + break; + } + } + else + { + if (((!hasWorld && word == 1) || (hasWorld && word == 2)) && chars[^1] is 'x' or 'X' or 'w' or 'W') + { + _ = double.TryParse(chars[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out x); + } + else if (((!hasWorld && word == 0) || (hasWorld && word == 1)) && chars[^1] is 'z' or 'Z' or 'n' or 'N') + { + _ = double.TryParse(chars[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out z); + } + else if (((!hasWorld && word == 1) || (hasWorld && word == 2)) && chars[^1] is 'e' or 'E') + { + _ = double.TryParse(chars[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out x); + x = -x; + } + else if (((!hasWorld && word == 0) || (hasWorld && word == 1)) && chars[^1] is 's' or 'S') + { + _ = double.TryParse(chars[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out z); + z = -z; + } + else if (((!hasWorld && word == 2) || (hasWorld && word == 3)) && chars[^1] is 'a' or 'A') + { + _ = double.TryParse(chars[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out y); + } + else if (((!hasWorld && word == 3) || (hasWorld && word == 4)) && double.TryParse(chars, NumberStyles.Float, CultureInfo.InvariantCulture, out double temp)) + { + yaw = temp; + } + } + } + } + + /// + /// Returns a value indicating whether the specified span of characters represents a relative unit string. + /// + /// The span of characters to validate. + /// + /// if represents a valid relative unit string; otherwise, + /// . + /// + public static bool IsAbsoluteUnit(ReadOnlySpan bytes) + { + Span chars = stackalloc char[bytes.Length]; + Encoding.UTF8.GetChars(bytes, chars); + return IsRelativeUnit(chars); + } + + /// + /// Returns a value indicating whether the specified span of characters represents a relative unit string. + /// + /// The span of characters to validate. + /// + /// if represents a valid relative unit string; otherwise, + /// . + /// + public static bool IsAbsoluteUnit(ReadOnlySpan chars) + { + ReadOnlySpan validChars = "nNeEwWsSaA"; + return double.TryParse(chars, out _) || + (validChars.Contains(chars[^1]) && + double.TryParse(chars[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out _)); + } + + /// + /// Returns a value indicating whether the specified span of characters represents a relative unit string. + /// + /// The span of characters to validate. + /// + /// if represents a valid relative unit string; otherwise, + /// . + /// + public static bool IsRelativeUnit(ReadOnlySpan bytes) + { + Span chars = stackalloc char[bytes.Length]; + Encoding.UTF8.GetChars(bytes, chars); + return IsRelativeUnit(chars); + } + + /// + /// Returns a value indicating whether the specified span of characters represents a relative unit string. + /// + /// The span of characters to validate. + /// + /// if represents a valid relative unit string; otherwise, + /// . + /// + public static bool IsRelativeUnit(ReadOnlySpan chars) + { + return (chars[0] == '+' || chars[0] == '-') && + double.TryParse(chars, NumberStyles.Float, CultureInfo.InvariantCulture, out _); + } + + /// + /// Returns a value indicating whether the specified span of characters represents a valid coordinate unit string. + /// + /// The span of characters to validate. + /// + /// if represents a valid coordinate unit string; otherwise, + /// . + /// + public static bool IsUnitString(ReadOnlySpan bytes) + { + Span chars = stackalloc char[bytes.Length]; + Encoding.UTF8.GetChars(bytes, chars); + return IsUnitString(chars); + } + + /// + /// Returns a value indicating whether the specified span of characters represents a valid coordinate unit string. + /// + /// The span of characters to validate. + /// + /// if represents a valid coordinate unit string; otherwise, + /// . + /// + public static bool IsUnitString(ReadOnlySpan chars) + { + chars = chars.Trim(); + + if (chars.Length == 0) + { + return false; + } + + if (!char.IsDigit(chars[0]) && chars[0] != '+' && chars[0] != '-') + { + return false; + } + + // thicc char span + return IsRelativeUnit(chars) || IsAbsoluteUnit(chars); + } + } +} diff --git a/VpSharp/src/Coordinates.cs b/VpSharp/src/Coordinates.cs new file mode 100644 index 0000000..9409c25 --- /dev/null +++ b/VpSharp/src/Coordinates.cs @@ -0,0 +1,146 @@ +namespace VpSharp; + +/// +/// Represents a set of coordinates. +/// +public readonly partial struct Coordinates +{ + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate. + /// The Y coordinate. + /// The Z coordinate. + /// The yaw. + /// + /// if these coordinates represent relative coordinates; otherwise. + /// + public Coordinates(double x, double y, double z, double yaw, bool isRelative = false) + : this(null, x, y, z, yaw, isRelative) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The world name. + /// The X coordinate. + /// The Y coordinate. + /// The Z coordinate. + /// The yaw. + /// + /// if these coordinates represent relative coordinates; otherwise. + /// + public Coordinates(string? world, double x, double y, double z, double yaw, bool isRelative = false) + { + World = world; + X = x; + Y = y; + Z = z; + Yaw = yaw; + IsRelative = isRelative; + } + + /// + /// Gets or initializes a value indicating whether this instance represents relative coordinates. + /// + /// + /// if this instance represents relative coordinates; otherwise, . + /// + public bool IsRelative { get; init; } + + /// + /// Gets or initializes the world. + /// + /// The world. + public string? World { get; init; } + + /// + /// Gets or initializes the X coordinate. + /// + /// The X coordinate. + public double X { get; init; } + + /// + /// Gets or initializes the Y coordinate. + /// + /// The Y coordinate. + public double Y { get; init; } + + /// + /// Gets or initializes the yaw. + /// + /// The yaw. + public double Yaw { get; init; } + + /// + /// Gets or initializes the Z coordinate. + /// + /// The Z coordinate. + public double Z { get; init; } + + public static bool operator ==(Coordinates left, Coordinates right) => + left.Equals(right); + + public static bool operator !=(Coordinates left, Coordinates right) => + !(left == right); + + /// + /// Parses a coordinate string. + /// + /// The coordinates to parse. + /// An instance of . + public static Coordinates Parse(string coordinates) + { + return Serializer.Deserialize(coordinates); + } + + /// + /// Returns a value indicating whether this instance of and another instance of + /// are equal. + /// + /// The instance against which to compare. + /// + /// if this instance is equal to ; otherwise, . + /// + public bool Equals(Coordinates other) + { + return X.Equals(other.X) && + Y.Equals(other.Y) && + Z.Equals(other.Z) && + Yaw.Equals(other.Yaw) && + IsRelative.Equals(other.IsRelative) && + string.Equals(World, other.World); + } + + /// + public override bool Equals(object? obj) + { + return obj is Coordinates other && Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(World, X, Y, Z, Yaw); + } + + /// + /// Returns the string representation of these coordinates. + /// + /// A representation of these coordinates. + public override string ToString() + { + return ToString("{0}"); + } + + /// + /// Returns the string representation of these coordinates. + /// + /// The format to apply to each component. + /// A representation of these coordinates. + public string ToString(string format) + { + return Serializer.Serialize(this, format); + } +} diff --git a/VpSharp/src/Location.cs b/VpSharp/src/Location.cs index 93d106e..4ed4cea 100644 --- a/VpSharp/src/Location.cs +++ b/VpSharp/src/Location.cs @@ -1,5 +1,7 @@ using System.Numerics; using VpSharp.Entities; +using VpSharp.Extensions; +using X10D.Math; namespace VpSharp; @@ -13,6 +15,23 @@ public readonly struct Location : IEquatable /// public static readonly Location Nowhere = new(VirtualParadiseWorld.Nowhere); + /// + /// Initializes a new instance of the struct. + /// + /// The world. + /// + /// The coordinates which contains the position and rotation. The property in this value + /// is ignored. Fetch the world using , and pass that into the + /// parameter. + /// + /// is . + public Location(VirtualParadiseWorld world, in Coordinates coordinates) + { + World = world ?? throw new ArgumentNullException(nameof(world)); + Position = new Vector3d(coordinates.X, coordinates.Y, coordinates.Z); + Rotation = Quaternion.CreateFromYawPitchRoll((float)coordinates.Yaw.DegreesToRadians(), 0, 0); + } + /// /// Initializes a new instance of the struct. /// @@ -34,8 +53,8 @@ public readonly struct Location : IEquatable { get { - var x = (int) Math.Floor(Position.X); - var z = (int) Math.Floor(Position.Z); + var x = (int)Math.Floor(Position.X); + var z = (int)Math.Floor(Position.Z); return new Cell(x, z); } } @@ -110,6 +129,17 @@ public readonly struct Location : IEquatable return HashCode.Combine(Position, Rotation, World); } + /// + /// Converts this to an instance of . + /// + /// The result of the conversion to . + public Coordinates ToCoordinates() + { + (double x, double y, double z) = Position; + (_, double yaw, _) = Rotation.ToEulerAngles(); + return new Coordinates(World?.Name, x, y, z, yaw); + } + /// public override string ToString() {