1
0
mirror of https://github.com/oliverbooth/VpSharp synced 2024-11-22 19:38:47 +00:00

Add Coordinates struct

This commit is contained in:
Oliver Booth 2022-11-29 15:12:57 +00:00
parent a064328418
commit 027ce96ed3
No known key found for this signature in database
GPG Key ID: 32A00B35503AF634
4 changed files with 540 additions and 2 deletions

View File

@ -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);
}
}
}

View File

@ -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<char>.Empty);
Span<char> chars = stackalloc char[count];
Serialize(coordinates, format, chars);
return chars.ToString();
}
public static int Serialize(in Coordinates coordinates, string format, Span<char> 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<byte> bytes = builder.AsSpan();
Span<char> 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<char> 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<byte> bytes = builder.AsSpan();
Span<char> 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;
}
}
}
}
/// <summary>
/// Returns a value indicating whether the specified span of characters represents a relative unit string.
/// </summary>
/// <param name="bytes">The span of characters to validate.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="bytes" /> represents a valid relative unit string; otherwise,
/// <see langword="false" />.
/// </returns>
public static bool IsAbsoluteUnit(ReadOnlySpan<byte> bytes)
{
Span<char> chars = stackalloc char[bytes.Length];
Encoding.UTF8.GetChars(bytes, chars);
return IsRelativeUnit(chars);
}
/// <summary>
/// Returns a value indicating whether the specified span of characters represents a relative unit string.
/// </summary>
/// <param name="chars">The span of characters to validate.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="chars" /> represents a valid relative unit string; otherwise,
/// <see langword="false" />.
/// </returns>
public static bool IsAbsoluteUnit(ReadOnlySpan<char> chars)
{
ReadOnlySpan<char> validChars = "nNeEwWsSaA";
return double.TryParse(chars, out _) ||
(validChars.Contains(chars[^1]) &&
double.TryParse(chars[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out _));
}
/// <summary>
/// Returns a value indicating whether the specified span of characters represents a relative unit string.
/// </summary>
/// <param name="bytes">The span of characters to validate.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="bytes" /> represents a valid relative unit string; otherwise,
/// <see langword="false" />.
/// </returns>
public static bool IsRelativeUnit(ReadOnlySpan<byte> bytes)
{
Span<char> chars = stackalloc char[bytes.Length];
Encoding.UTF8.GetChars(bytes, chars);
return IsRelativeUnit(chars);
}
/// <summary>
/// Returns a value indicating whether the specified span of characters represents a relative unit string.
/// </summary>
/// <param name="chars">The span of characters to validate.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="chars" /> represents a valid relative unit string; otherwise,
/// <see langword="false" />.
/// </returns>
public static bool IsRelativeUnit(ReadOnlySpan<char> chars)
{
return (chars[0] == '+' || chars[0] == '-') &&
double.TryParse(chars, NumberStyles.Float, CultureInfo.InvariantCulture, out _);
}
/// <summary>
/// Returns a value indicating whether the specified span of characters represents a valid coordinate unit string.
/// </summary>
/// <param name="bytes">The span of characters to validate.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="bytes" /> represents a valid coordinate unit string; otherwise,
/// <see langword="false" />.
/// </returns>
public static bool IsUnitString(ReadOnlySpan<byte> bytes)
{
Span<char> chars = stackalloc char[bytes.Length];
Encoding.UTF8.GetChars(bytes, chars);
return IsUnitString(chars);
}
/// <summary>
/// Returns a value indicating whether the specified span of characters represents a valid coordinate unit string.
/// </summary>
/// <param name="chars">The span of characters to validate.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="chars" /> represents a valid coordinate unit string; otherwise,
/// <see langword="false" />.
/// </returns>
public static bool IsUnitString(ReadOnlySpan<char> 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);
}
}
}

146
VpSharp/src/Coordinates.cs Normal file
View File

@ -0,0 +1,146 @@
namespace VpSharp;
/// <summary>
/// Represents a set of coordinates.
/// </summary>
public readonly partial struct Coordinates
{
/// <summary>
/// Initializes a new instance of the <see cref="Coordinates" /> struct.
/// </summary>
/// <param name="x">The X coordinate.</param>
/// <param name="y">The Y coordinate.</param>
/// <param name="z">The Z coordinate.</param>
/// <param name="yaw">The yaw.</param>
/// <param name="isRelative">
/// <see langword="true" /> if these coordinates represent relative coordinates; <see langword="false" /> otherwise.
/// </param>
public Coordinates(double x, double y, double z, double yaw, bool isRelative = false)
: this(null, x, y, z, yaw, isRelative)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Coordinates" /> struct.
/// </summary>
/// <param name="world">The world name.</param>
/// <param name="x">The X coordinate.</param>
/// <param name="y">The Y coordinate.</param>
/// <param name="z">The Z coordinate.</param>
/// <param name="yaw">The yaw.</param>
/// <param name="isRelative">
/// <see langword="true" /> if these coordinates represent relative coordinates; <see langword="false" /> otherwise.
/// </param>
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;
}
/// <summary>
/// Gets or initializes a value indicating whether this instance represents relative coordinates.
/// </summary>
/// <value>
/// <see langword="true" /> if this instance represents relative coordinates; otherwise, <see langword="false" />.
/// </value>
public bool IsRelative { get; init; }
/// <summary>
/// Gets or initializes the world.
/// </summary>
/// <value>The world.</value>
public string? World { get; init; }
/// <summary>
/// Gets or initializes the X coordinate.
/// </summary>
/// <value>The X coordinate.</value>
public double X { get; init; }
/// <summary>
/// Gets or initializes the Y coordinate.
/// </summary>
/// <value>The Y coordinate.</value>
public double Y { get; init; }
/// <summary>
/// Gets or initializes the yaw.
/// </summary>
/// <value>The yaw.</value>
public double Yaw { get; init; }
/// <summary>
/// Gets or initializes the Z coordinate.
/// </summary>
/// <value>The Z coordinate.</value>
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);
/// <summary>
/// Parses a coordinate string.
/// </summary>
/// <param name="coordinates">The coordinates to parse.</param>
/// <returns>An instance of <see cref="Coordinates" />.</returns>
public static Coordinates Parse(string coordinates)
{
return Serializer.Deserialize(coordinates);
}
/// <summary>
/// Returns a value indicating whether this instance of <see cref="Coordinates" /> and another instance of
/// <see cref="Coordinates" /> are equal.
/// </summary>
/// <param name="other">The instance against which to compare.</param>
/// <returns>
/// <see langword="true" /> if this instance is equal to <paramref name="other" />; otherwise, <see langword="false" />.
/// </returns>
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);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is Coordinates other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(World, X, Y, Z, Yaw);
}
/// <summary>
/// Returns the string representation of these coordinates.
/// </summary>
/// <returns>A <see cref="string" /> representation of these coordinates.</returns>
public override string ToString()
{
return ToString("{0}");
}
/// <summary>
/// Returns the string representation of these coordinates.
/// </summary>
/// <param name="format">The format to apply to each component.</param>
/// <returns>A <see cref="string" /> representation of these coordinates.</returns>
public string ToString(string format)
{
return Serializer.Serialize(this, format);
}
}

View File

@ -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<Location>
/// </summary>
public static readonly Location Nowhere = new(VirtualParadiseWorld.Nowhere);
/// <summary>
/// Initializes a new instance of the <see cref="Location" /> struct.
/// </summary>
/// <param name="world">The world.</param>
/// <param name="coordinates">
/// The coordinates which contains the position and rotation. The <see cref="Coordinates.World" /> property in this value
/// is ignored. Fetch the world using <see cref="VirtualParadiseClient.GetWorldAsync" />, and pass that into the
/// <paramref name="world" /> parameter.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="world" /> is <see langword="null" />.</exception>
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);
}
/// <summary>
/// Initializes a new instance of the <see cref="Location" /> struct.
/// </summary>
@ -34,8 +53,8 @@ public readonly struct Location : IEquatable<Location>
{
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<Location>
return HashCode.Combine(Position, Rotation, World);
}
/// <summary>
/// Converts this <see cref="Location" /> to an instance of <see cref="Coordinates" />.
/// </summary>
/// <returns>The result of the conversion to <see cref="Coordinates" />.</returns>
public Coordinates ToCoordinates()
{
(double x, double y, double z) = Position;
(_, double yaw, _) = Rotation.ToEulerAngles();
return new Coordinates(World?.Name, x, y, z, yaw);
}
/// <inheritdoc />
public override string ToString()
{