diff --git a/Assets/Mirror/Core/Tools/Compression.cs b/Assets/Mirror/Core/Tools/Compression.cs index d04b566b1..f6bd06fa5 100644 --- a/Assets/Mirror/Core/Tools/Compression.cs +++ b/Assets/Mirror/Core/Tools/Compression.cs @@ -71,6 +71,24 @@ public static bool ScaleToLong(Vector3 value, float precision, out long x, out l return result; } + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Quaternion value, float precision, out long x, out long y, out long z, out long w) + { + // attempt to convert every component. + // do not return early if one conversion returned 'false'. + // the return value is optional. always attempt to convert all. + bool result = true; + result &= ScaleToLong(value.x, precision, out x); + result &= ScaleToLong(value.y, precision, out y); + result &= ScaleToLong(value.z, precision, out z); + result &= ScaleToLong(value.w, precision, out w); + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ScaleToLong(Vector3 value, float precision, out Vector3Long quantized) { @@ -78,6 +96,13 @@ public static bool ScaleToLong(Vector3 value, float precision, out Vector3Long q return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Quaternion value, float precision, out Vector4Long quantized) + { + quantized = Vector4Long.zero; + return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z, out quantized.w); + } + // multiple by precision. // for example, 0.1 cm precision converts '50' long to '5.0f' float. public static float ScaleToFloat(long value, float precision) @@ -103,10 +128,25 @@ public static Vector3 ScaleToFloat(long x, long y, long z, float precision) return v; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion ScaleToFloat(long x, long y, long z, long w, float precision) + { + Quaternion v; + v.x = ScaleToFloat(x, precision); + v.y = ScaleToFloat(y, precision); + v.z = ScaleToFloat(z, precision); + v.w = ScaleToFloat(w, precision); + return v; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3 ScaleToFloat(Vector3Long value, float precision) => ScaleToFloat(value.x, value.y, value.z, precision); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion ScaleToFloat(Vector4Long value, float precision) => + ScaleToFloat(value.x, value.y, value.z, value.w, precision); + // scale a float within min/max range to an ushort between min/max range // note: can also use this for byte range from byte.MinValue to byte.MaxValue public static ushort ScaleFloatToUShort(float value, float minValue, float maxValue, ushort minTarget, ushort maxTarget) diff --git a/Assets/Mirror/Core/Tools/DeltaCompression.cs b/Assets/Mirror/Core/Tools/DeltaCompression.cs index a59370a2b..50b9b2f86 100644 --- a/Assets/Mirror/Core/Tools/DeltaCompression.cs +++ b/Assets/Mirror/Core/Tools/DeltaCompression.cs @@ -26,6 +26,16 @@ public static void Compress(NetworkWriter writer, Vector3Long last, Vector3Long Compress(writer, last.z, current.z); } + // delta (usually small), then zigzag varint to support +- changes + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, Vector4Long last, Vector4Long current) + { + Compress(writer, last.x, current.x); + Compress(writer, last.y, current.y); + Compress(writer, last.z, current.z); + Compress(writer, last.w, current.w); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3Long Decompress(NetworkReader reader, Vector3Long last) { @@ -34,5 +44,15 @@ public static Vector3Long Decompress(NetworkReader reader, Vector3Long last) long z = Decompress(reader, last.z); return new Vector3Long(x, y, z); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long Decompress(NetworkReader reader, Vector4Long last) + { + long x = Decompress(reader, last.x); + long y = Decompress(reader, last.y); + long z = Decompress(reader, last.z); + long w = Decompress(reader, last.w); + return new Vector4Long(x, y, z, w); + } } } diff --git a/Assets/Mirror/Core/Tools/Vector4Long.cs b/Assets/Mirror/Core/Tools/Vector4Long.cs new file mode 100644 index 000000000..0eba99a32 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector4Long.cs @@ -0,0 +1,126 @@ +#pragma warning disable CS0659 // 'Vector4Long' overrides Object.Equals(object o) but does not override Object.GetHashCode() +#pragma warning disable CS0661 // 'Vector4Long' defines operator == or operator != but does not override Object.GetHashCode() + +// Vector4Long by mischa (based on game engine project) +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public struct Vector4Long + { + public long x; + public long y; + public long z; + public long w; + + public static readonly Vector4Long zero = new Vector4Long(0, 0, 0, 0); + public static readonly Vector4Long one = new Vector4Long(1, 1, 1, 1); + + // constructor ///////////////////////////////////////////////////////// + public Vector4Long(long x, long y, long z, long w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + // operators /////////////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator +(Vector4Long a, Vector4Long b) => + new Vector4Long(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator -(Vector4Long a, Vector4Long b) => + new Vector4Long(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator -(Vector4Long v) => + new Vector4Long(-v.x, -v.y, -v.z, -v.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator *(Vector4Long a, long n) => + new Vector4Long(a.x * n, a.y * n, a.z * n, a.w * n); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator *(long n, Vector4Long a) => + new Vector4Long(a.x * n, a.y * n, a.z * n, a.w * n); + + // == returns true if approximately equal (with epsilon). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector4Long a, Vector4Long b) => + a.x == b.x && + a.y == b.y && + a.z == b.z && + a.w == b.w; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector4Long a, Vector4Long b) => !(a == b); + + // NO IMPLICIT System.Numerics.Vector4Long conversion because double<->float + // would silently lose precision in large worlds. + + // [i] component index. useful for iterating all components etc. + public long this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + case 3: return w; + default: throw new IndexOutOfRangeException($"Vector4Long[{index}] out of range."); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + case 3: + w = value; + break; + default: throw new IndexOutOfRangeException($"Vector4Long[{index}] out of range."); + } + } + } + + // instance functions ////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"({x} {y} {z} {w})"; + + // equality //////////////////////////////////////////////////////////// + // implement Equals & HashCode explicitly for performance. + // calling .Equals (instead of "==") checks for exact equality. + // (API compatibility) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Vector4Long other) => + x == other.x && y == other.y && z == other.z && w == other.w; + + // Equals(object) can reuse Equals(Vector4) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object other) => + other is Vector4Long vector4 && Equals(vector4); + +#if UNITY_2021_3_OR_NEWER + // Unity 2019/2020 don't have HashCode.Combine yet. + // this is only to avoid reflection. without defining, it works too. + // default generated by rider + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(x, y, z, w); +#endif + } +} diff --git a/Assets/Mirror/Core/Tools/Vector4Long.cs.meta b/Assets/Mirror/Core/Tools/Vector4Long.cs.meta new file mode 100644 index 000000000..7d5ede1cf --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector4Long.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9f69bf6b6d73476ab3f31dd635bb6497 +timeCreated: 1726777293 \ No newline at end of file diff --git a/Assets/Mirror/Tests/Editor/Tools/CompressionTests.cs b/Assets/Mirror/Tests/Editor/Tools/CompressionTests.cs index 617902503..06f609f06 100644 --- a/Assets/Mirror/Tests/Editor/Tools/CompressionTests.cs +++ b/Assets/Mirror/Tests/Editor/Tools/CompressionTests.cs @@ -86,6 +86,32 @@ public void ScaleToLong_Vector3_OutOfRange() Assert.That(z, Is.EqualTo(long.MinValue)); } + [Test] + public void ScaleToLong_Quaternion() + { + // 0, positive, negative + Assert.True(Compression.ScaleToLong(new Quaternion(0, 10.5f, -100.5f, -10.5f), 0.1f, out long x, out long y, out long z, out long w)); + Assert.That(x, Is.EqualTo(0)); + Assert.That(y, Is.EqualTo(105)); + Assert.That(z, Is.EqualTo(-1005)); + Assert.That(w, Is.EqualTo(-105)); + } + + [Test] + public void ScaleToLong_Quaternion_OutOfRange() + { + float precision = 0.1f; + float largest = long.MaxValue / 0.1f; + float smallest = long.MinValue / 0.1f; + + // 0, largest, smallest + Assert.False(Compression.ScaleToLong(new Quaternion(0, largest, smallest, 0), precision, out long x, out long y, out long z, out long w)); + Assert.That(x, Is.EqualTo(0)); + Assert.That(y, Is.EqualTo(long.MaxValue)); + Assert.That(z, Is.EqualTo(long.MinValue)); + Assert.That(w, Is.EqualTo(0)); + } + [Test] public void ScaleToFloat() { @@ -130,6 +156,17 @@ public void ScaleToFloat_Vector3() Assert.That(v.z, Is.EqualTo(-100.5f)); } + [Test] + public void ScaleToFloat_Quaternion() + { + // 0, positive, negative + Quaternion q = Compression.ScaleToFloat(0, 105, -1005, -105, 0.1f); + Assert.That(q.x, Is.EqualTo(0)); + Assert.That(q.y, Is.EqualTo(10.5f)); + Assert.That(q.z, Is.EqualTo(-100.5f)); + Assert.That(q.w, Is.EqualTo(-10.5f)); + } + [Test] public void LargestAbsoluteComponentIndex() { diff --git a/Assets/Mirror/Tests/Editor/Tools/DeltaCompressionTests.cs b/Assets/Mirror/Tests/Editor/Tools/DeltaCompressionTests.cs index 7d3c3abed..1d60129e7 100644 --- a/Assets/Mirror/Tests/Editor/Tools/DeltaCompressionTests.cs +++ b/Assets/Mirror/Tests/Editor/Tools/DeltaCompressionTests.cs @@ -59,7 +59,7 @@ public void Compress_Long_Changed_Large() // two components = 8 bytes changed. // delta should compress it down a lot. - // 7000 delta should use a few more bytse. + // 7000 delta should use a few more bytes. Assert.That(writer.Position, Is.EqualTo(3)); // decompress should get original result @@ -123,7 +123,7 @@ public void Compress_Vector3Long_YZChanged_Large() // two components = 8 bytes changed. // delta should compress it down a lot. - // 7000 delta should use a few more bytse. + // 7000 delta should use a few more bytes. Assert.That(writer.Position, Is.EqualTo(5)); // decompress should get original result @@ -131,5 +131,69 @@ public void Compress_Vector3Long_YZChanged_Large() Vector3Long decompressed = DeltaCompression.Decompress(reader, last); Assert.That(decompressed, Is.EqualTo(current)); } + + [Test] + public void Compress_Vector4Long_Unchanged() + { + NetworkWriter writer = new NetworkWriter(); + + Vector4Long last = new Vector4Long(1, 2, 3, 4); + Vector4Long current = new Vector4Long(1, 2, 3, 4); // unchanged + + // delta compress current against last + DeltaCompression.Compress(writer, last, current); + + // nothing changed. + // delta should compress it down a lot. + Assert.That(writer.Position, Is.EqualTo(4)); + + // decompress should get original result + NetworkReader reader = new NetworkReader(writer); + Vector4Long decompressed = DeltaCompression.Decompress(reader, last); + Assert.That(decompressed, Is.EqualTo(current)); + } + + [Test] + public void Compress_Vector4Long_ZWChanged() + { + NetworkWriter writer = new NetworkWriter(); + + Vector4Long last = new Vector4Long(1, 2, 3, 4); + Vector4Long current = new Vector4Long(1, 2, 7, 8); // only 2 components change + + // delta compress current against last + DeltaCompression.Compress(writer, last, current); + + // two components = 8 bytes changed. + // delta should compress it down a lot. + Assert.That(writer.Position, Is.EqualTo(4)); + + // decompress should get original result + NetworkReader reader = new NetworkReader(writer); + Vector4Long decompressed = DeltaCompression.Decompress(reader, last); + Assert.That(decompressed, Is.EqualTo(current)); + } + + [Test] + public void Compress_Vector4Long_ZWChanged_Large() + { + NetworkWriter writer = new NetworkWriter(); + + Vector4Long last = new Vector4Long(1, 2, 3, 4); + Vector4Long current = new Vector4Long(1, 2, 5, 7000); // only 2 components change + + // delta compress current against last + DeltaCompression.Compress(writer, last, current); + + // two components = 8 bytes changed. + // delta should compress it down a lot. + // 7000 delta should use a few more bytes. + Assert.That(writer.Position, Is.EqualTo(6)); + + // decompress should get original result + NetworkReader reader = new NetworkReader(writer); + Vector4Long decompressed = DeltaCompression.Decompress(reader, last); + Assert.That(decompressed, Is.EqualTo(current)); + } } } diff --git a/Assets/Mirror/Tests/Editor/Tools/Vector4LongTests.cs b/Assets/Mirror/Tests/Editor/Tools/Vector4LongTests.cs new file mode 100644 index 000000000..3c8e20cf6 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Tools/Vector4LongTests.cs @@ -0,0 +1,154 @@ +using System; +using NUnit.Framework; + +namespace Mirror.Tests.Tools +{ + public class Vector4LongTests + { + [Test] + public void Constructor() + { + Vector4Long v = new Vector4Long(); + Assert.That(v.x, Is.EqualTo(0)); + Assert.That(v.y, Is.EqualTo(0)); + Assert.That(v.z, Is.EqualTo(0)); + Assert.That(v.w, Is.EqualTo(0)); + } + + [Test] + public void ConstructorXYZ() + { + Vector4Long v = new Vector4Long(1, 2, 3, 4); + Assert.That(v.x, Is.EqualTo(1)); + Assert.That(v.y, Is.EqualTo(2)); + Assert.That(v.z, Is.EqualTo(3)); + Assert.That(v.w, Is.EqualTo(4)); + } + + [Test] + public void OperatorAdd() + { + Vector4Long a = new Vector4Long(1, 2, 3, 4); + Vector4Long b = new Vector4Long(2, 3, 4, 5); + Assert.That(a + b, Is.EqualTo(new Vector4Long(3, 5, 7, 9))); + } + + [Test] + public void OperatorSubtract() + { + Vector4Long a = new Vector4Long(1, 2, 3, 4); + Vector4Long b = new Vector4Long(-2, -3, -4, -5); + Assert.That(a - b, Is.EqualTo(new Vector4Long(3, 5, 7, 9))); + } + + [Test] + public void OperatorInverse() + { + Vector4Long v = new Vector4Long(1, 2, 3, 4); + Assert.That(-v, Is.EqualTo(new Vector4Long(-1, -2, -3, -4))); + } + + [Test] + public void OperatorMultiply() + { + Vector4Long a = new Vector4Long(1, 2, 3, 4); + // a * n, n * a are two different operators. test both. + Assert.That(a * 2, Is.EqualTo(new Vector4Long(2, 4, 6, 8))); + Assert.That(2 * a, Is.EqualTo(new Vector4Long(2, 4, 6, 8))); + } + +#if UNITY_2021_3_OR_NEWER + [Test] + public void OperatorEquals() + { + // two vectors which are approximately the same + Vector4Long a = new Vector4Long(1, 2, 3, 4); + Vector4Long b = new Vector4Long(1, 2, 3, 4); + Assert.That(a == b, Is.True); + + // two vectors which are definitely not the same + Assert.That(a == Vector4Long.one, Is.False); + } + + [Test] + public void OperatorNotEquals() + { + // two vectors which are approximately the same + Vector4Long a = new Vector4Long(1, 2, 3, 4); + Vector4Long b = new Vector4Long(1, 2, 3, 4); + Assert.That(a != b, Is.False); + + // two vectors which are definitely not the same + Assert.That(a != Vector4Long.one, Is.True); + } +#endif + + [Test] + public void OperatorIndexer() + { + Vector4Long a = new Vector4Long(1, 2, 3, 4); + + // get + Assert.That(a[0], Is.EqualTo(1)); + Assert.That(a[1], Is.EqualTo(2)); + Assert.That(a[2], Is.EqualTo(3)); + Assert.That(a[3], Is.EqualTo(4)); + Assert.Throws(() => + { + double _ = a[-1]; + }); + Assert.Throws(() => + { + double _ = a[4]; + }); + + // set + a[0] = -1; + a[1] = -2; + a[2] = -3; + a[3] = -4; + Assert.Throws(() => { a[-1] = 0; }); + Assert.Throws(() => { a[4] = 0; }); + Assert.That(a, Is.EqualTo(new Vector4Long(-1, -2, -3, -4))); + } + + [Test] + public void ToStringTest() + { + // should be rounded to :F2 + Vector4Long v = new Vector4Long(-10, 0, 42, 3); + Assert.That(v.ToString(), Is.EqualTo("(-10 0 42 3)")); + } + +#if UNITY_2021_3_OR_NEWER + [Test] + public void EqualsVector4Long() + { + Assert.That(Vector4Long.one.Equals(Vector4Long.one), Is.True); + Assert.That(Vector4Long.one.Equals(Vector4Long.zero), Is.False); + } + + [Test] + public void EqualsObject() + { + Assert.That(Vector4Long.one.Equals((object)42), Is.False); + Assert.That(Vector4Long.one.Equals((object)Vector4Long.one), Is.True); + Assert.That(Vector4Long.one.Equals((object)Vector4Long.zero), Is.False); + } + + [Test] + public void GetHashCodeTest() + { + // shouldn't be 0 + Assert.That(Vector4Long.zero.GetHashCode(), !Is.EqualTo(0)); + Assert.That(Vector4Long.one.GetHashCode(), !Is.EqualTo(0)); + + // should be same for same vector + Assert.That(Vector4Long.zero.GetHashCode(), Is.EqualTo(Vector4Long.zero.GetHashCode())); + + // should be different for different vectors + Assert.That(Vector4Long.zero.GetHashCode(), !Is.EqualTo(Vector4Long.one.GetHashCode())); + } +#endif + } +} diff --git a/Assets/Mirror/Tests/Editor/Tools/Vector4LongTests.cs.meta b/Assets/Mirror/Tests/Editor/Tools/Vector4LongTests.cs.meta new file mode 100644 index 000000000..b77712d45 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Tools/Vector4LongTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a966f5fd1b865460abc471e46bcee889 +timeCreated: 1666897728 \ No newline at end of file