diff --git a/Assets/Mirror/Core/NetworkReaderExtensions.cs b/Assets/Mirror/Core/NetworkReaderExtensions.cs index 6b9a4128f..7d3d00c31 100644 --- a/Assets/Mirror/Core/NetworkReaderExtensions.cs +++ b/Assets/Mirror/Core/NetworkReaderExtensions.cs @@ -62,6 +62,8 @@ public static class NetworkReaderExtensions public static decimal ReadDecimal(this NetworkReader reader) => reader.ReadBlittable(); public static decimal? ReadDecimalNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + public static Half ReadHalf(this NetworkReader reader) => new Half(reader.ReadUShort()); + /// if an invalid utf8 string is sent public static string ReadString(this NetworkReader reader) { diff --git a/Assets/Mirror/Core/NetworkWriterExtensions.cs b/Assets/Mirror/Core/NetworkWriterExtensions.cs index 34a815104..6ee5be32a 100644 --- a/Assets/Mirror/Core/NetworkWriterExtensions.cs +++ b/Assets/Mirror/Core/NetworkWriterExtensions.cs @@ -57,6 +57,8 @@ public static class NetworkWriterExtensions public static void WriteDecimal(this NetworkWriter writer, decimal value) => writer.WriteBlittable(value); public static void WriteDecimalNullable(this NetworkWriter writer, decimal? value) => writer.WriteBlittableNullable(value); + public static void WriteHalf(this NetworkWriter writer, Half value) => writer.WriteUShort(value._value); + public static void WriteString(this NetworkWriter writer, string value) { // we offset count by '1' to easily support null without writing another byte. diff --git a/Assets/Mirror/Core/Tools/Half.cs b/Assets/Mirror/Core/Tools/Half.cs new file mode 100644 index 000000000..33eb08e92 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Half.cs @@ -0,0 +1,773 @@ +// half float from .NET 5: +// https://devblogs.microsoft.com/dotnet/introducing-the-half-type/ +// +// drop in from dotnet/runtime source: +// https://github.com/dotnet/runtime/blob/e188d6ac90fe56320cca51c53709ef1c72f063d5/src/libraries/System.Private.CoreLib/src/System/Half.cs#L17 +// removing all the stuff that's not in Unity though. + + + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace System +{ + // Portions of the code implemented below are based on the 'Berkeley SoftFloat Release 3e' algorithms. + + /// + /// Represents a half-precision floating-point number. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Half + : IComparable, + IComparable, + IEquatable + { + private const NumberStyles DefaultParseStyle = NumberStyles.Float | NumberStyles.AllowThousands; + + // Constants for manipulating the private bit-representation + + internal const ushort SignMask = 0x8000; + internal const int SignShift = 15; + internal const byte ShiftedSignMask = SignMask >> SignShift; + + internal const ushort BiasedExponentMask = 0x7C00; + internal const int BiasedExponentShift = 10; + internal const int BiasedExponentLength = 5; + internal const byte ShiftedBiasedExponentMask = BiasedExponentMask >> BiasedExponentShift; + + internal const ushort TrailingSignificandMask = 0x03FF; + + internal const byte MinSign = 0; + internal const byte MaxSign = 1; + + internal const byte MinBiasedExponent = 0x00; + internal const byte MaxBiasedExponent = 0x1F; + + internal const byte ExponentBias = 15; + + internal const sbyte MinExponent = -14; + internal const sbyte MaxExponent = +15; + + internal const ushort MinTrailingSignificand = 0x0000; + internal const ushort MaxTrailingSignificand = 0x03FF; + + internal const int TrailingSignificandLength = 10; + internal const int SignificandLength = TrailingSignificandLength + 1; + + // Constants representing the private bit-representation for various default values + + private const ushort PositiveZeroBits = 0x0000; + private const ushort NegativeZeroBits = 0x8000; + + private const ushort EpsilonBits = 0x0001; + + private const ushort PositiveInfinityBits = 0x7C00; + private const ushort NegativeInfinityBits = 0xFC00; + + private const ushort PositiveQNaNBits = 0x7E00; + private const ushort NegativeQNaNBits = 0xFE00; + + private const ushort MinValueBits = 0xFBFF; + private const ushort MaxValueBits = 0x7BFF; + + private const ushort PositiveOneBits = 0x3C00; + private const ushort NegativeOneBits = 0xBC00; + + private const ushort SmallestNormalBits = 0x0400; + + private const ushort EBits = 0x4170; + private const ushort PiBits = 0x4248; + private const ushort TauBits = 0x4648; + + // Well-defined and commonly used values + + public static Half Epsilon => new Half(EpsilonBits); // 5.9604645E-08 + + public static Half PositiveInfinity => new Half(PositiveInfinityBits); // 1.0 / 0.0; + + public static Half NegativeInfinity => new Half(NegativeInfinityBits); // -1.0 / 0.0 + + public static Half NaN => new Half(NegativeQNaNBits); // 0.0 / 0.0 + + public static Half MinValue => new Half(MinValueBits); // -65504 + + public static Half MaxValue => new Half(MaxValueBits); // 65504 + + internal readonly ushort _value; // internal representation + + internal Half(ushort value) + { + _value = value; + } + + private Half(bool sign, ushort exp, ushort sig) => _value = (ushort)(((sign ? 1 : 0) << SignShift) + (exp << BiasedExponentShift) + sig); + + internal byte BiasedExponent + { + get + { + ushort bits = _value; + return ExtractBiasedExponentFromBits(bits); + } + } + + internal sbyte Exponent + { + get + { + return (sbyte)(BiasedExponent - ExponentBias); + } + } + + internal ushort Significand + { + get + { + return (ushort)(TrailingSignificand | ((BiasedExponent != 0) ? (1U << BiasedExponentShift) : 0U)); + } + } + + internal ushort TrailingSignificand + { + get + { + ushort bits = _value; + return ExtractTrailingSignificandFromBits(bits); + } + } + + internal static byte ExtractBiasedExponentFromBits(ushort bits) + { + return (byte)((bits >> BiasedExponentShift) & ShiftedBiasedExponentMask); + } + + internal static ushort ExtractTrailingSignificandFromBits(ushort bits) + { + return (ushort)(bits & TrailingSignificandMask); + } + + public static bool operator <(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is unordered with respect to everything, including itself. + return false; + } + + bool leftIsNegative = IsNegative(left); + + if (leftIsNegative != IsNegative(right)) + { + // When the signs of left and right differ, we know that left is less than right if it is + // the negative value. The exception to this is if both values are zero, in which case IEEE + // says they should be equal, even if the signs differ. + return leftIsNegative && !AreZero(left, right); + } + + return (left._value != right._value) && ((left._value < right._value) ^ leftIsNegative); + } + + public static bool operator >(Half left, Half right) + { + return right < left; + } + + public static bool operator <=(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is unordered with respect to everything, including itself. + return false; + } + + bool leftIsNegative = IsNegative(left); + + if (leftIsNegative != IsNegative(right)) + { + // When the signs of left and right differ, we know that left is less than right if it is + // the negative value. The exception to this is if both values are zero, in which case IEEE + // says they should be equal, even if the signs differ. + return leftIsNegative || AreZero(left, right); + } + + return (left._value == right._value) || ((left._value < right._value) ^ leftIsNegative); + } + + public static bool operator >=(Half left, Half right) + { + return right <= left; + } + + public static bool operator ==(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is not equal to anything, including itself. + return false; + } + + // IEEE defines that positive and negative zero are equivalent. + return (left._value == right._value) || AreZero(left, right); + } + + public static bool operator !=(Half left, Half right) + { + return !(left == right); + } + + /// Determines whether the specified value is finite (zero, subnormal, or normal). + /// This effectively checks the value is not NaN and not infinite. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFinite(Half value) + { + uint bits = value._value; + return (~bits & PositiveInfinityBits) != 0; + } + + /// Determines whether the specified value is infinite. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInfinity(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) == PositiveInfinityBits; + } + + /// Determines whether the specified value is NaN. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNaN(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) > PositiveInfinityBits; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsNaNOrZero(Half value) + { + uint bits = value._value; + return ((bits - 1) & ~SignMask) >= PositiveInfinityBits; + } + + /// Determines whether the specified value is negative. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNegative(Half value) + { + return (short)(value._value) < 0; + } + + /// Determines whether the specified value is negative infinity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNegativeInfinity(Half value) + { + return value._value == NegativeInfinityBits; + } + + /// Determines whether the specified value is normal (finite, but not zero or subnormal). + /// This effectively checks the value is not NaN, not infinite, not subnormal, and not zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNormal(Half value) + { + uint bits = value._value; + return (ushort)((bits & ~SignMask) - SmallestNormalBits) < (PositiveInfinityBits - SmallestNormalBits); + } + + /// Determines whether the specified value is positive infinity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPositiveInfinity(Half value) + { + return value._value == PositiveInfinityBits; + } + + /// Determines whether the specified value is subnormal (finite, but not zero or normal). + /// This effectively checks the value is not NaN, not infinite, not normal, and not zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSubnormal(Half value) + { + uint bits = value._value; + return (ushort)((bits & ~SignMask) - 1) < MaxTrailingSignificand; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsZero(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) == 0; + } + + private static bool AreZero(Half left, Half right) + { + // IEEE defines that positive and negative zero are equal, this gives us a quick equality check + // for two values by or'ing the private bits together and stripping the sign. They are both zero, + // and therefore equivalent, if the resulting value is still zero. + return ((left._value | right._value) & ~SignMask) == 0; + } + + /// + /// Compares this object to another object, returning an integer that indicates the relationship. + /// + /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . + public int CompareTo(object obj) + { + if (obj is Half other) + { + return CompareTo(other); + } + return (obj is null) ? 1 : throw new ArgumentException("SR.Arg_MustBeHalf"); + } + + /// + /// Compares this object to another object, returning an integer that indicates the relationship. + /// + /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . + public int CompareTo(Half other) + { + if (this < other) + { + return -1; + } + + if (this > other) + { + return 1; + } + + if (this == other) + { + return 0; + } + + if (IsNaN(this)) + { + return IsNaN(other) ? 0 : -1; + } + + return 1; + } + + /// + /// Returns a value that indicates whether this instance is equal to a specified . + /// + public override bool Equals(object obj) + { + return (obj is Half other) && Equals(other); + } + + /// + /// Returns a value that indicates whether this instance is equal to a specified value. + /// + public bool Equals(Half other) + { + return _value == other._value + || AreZero(this, other) + || (IsNaN(this) && IsNaN(other)); + } + + /// + /// Serves as the default hash function. + /// + public override int GetHashCode() + { + uint bits = _value; + + if (IsNaNOrZero(this)) + { + // Ensure that all NaNs and both zeros have the same hash code + bits &= PositiveInfinityBits; + } + + return (int)bits; + } + + /// + /// Returns a string representation of the current value. + /// + public override string ToString() + { + return ((float)this).ToString(); + } + + // + // Explicit Convert To Half + // + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + public static explicit operator Half(char value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + public static explicit operator Half(decimal value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(short value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(int value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(long value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(float value) + { + // Unity implement this! + return new Half(Mathf.FloatToHalf(value)); + } + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(ushort value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(uint value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(ulong value) => (Half)(float)value; + + // + // Explicit Convert From Half + // + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator byte(Half value) => (byte)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator char(Half value) => (char)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator decimal(Half value) => (decimal)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator short(Half value) => (short)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator int(Half value) => (int)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator long(Half value) => (long)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator sbyte(Half value) => (sbyte)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator ushort(Half value) => (ushort)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator uint(Half value) => (uint)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator ulong(Half value) => (ulong)(float)value; + + // + // Implicit Convert To Half + // + + /// Implicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static implicit operator Half(byte value) => (Half)(float)value; + + /// Implicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static implicit operator Half(sbyte value) => (Half)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator float(Half value) + { + return Mathf.HalfToFloat(value._value); + } + + // IEEE 754 specifies NaNs to be propagated + internal static Half Negate(Half value) + { + return IsNaN(value) ? value : new Half((ushort)(value._value ^ SignMask)); + } + + public static Half operator +(Half left, Half right) => (Half)((float)left + (float)right); + + // + // IDecrementOperators + // + + public static Half operator --(Half value) + { + var tmp = (float)value; + --tmp; + return (Half)tmp; + } + + // + // IDivisionOperators + // + + public static Half operator /(Half left, Half right) => (Half)((float)left / (float)right); + + // + // IExponentialFunctions + // + + public static Half Exp(Half x) => (Half)Math.Exp((float)x); + + // + // IFloatingPoint + // + + public static Half Ceiling(Half x) => (Half)Math.Ceiling((float)x); + + public static Half Floor(Half x) => (Half)Math.Floor((float)x); + + public static Half Round(Half x) => (Half)Math.Round((float)x); + + public static Half Round(Half x, int digits) => (Half)Math.Round((float)x, digits); + + public static Half Round(Half x, MidpointRounding mode) => (Half)Math.Round((float)x, mode); + + public static Half Round(Half x, int digits, MidpointRounding mode) => (Half)Math.Round((float)x, digits, mode); + + public static Half Truncate(Half x) => (Half)Math.Truncate((float)x); + + // + // IFloatingPointConstants + // + + public static Half E => new Half(EBits); + + public static Half Pi => new Half(PiBits); + + public static Half Tau => new Half(TauBits); + + // + // IFloatingPointIeee754 + // + + public static Half NegativeZero => new Half(NegativeZeroBits); + + public static Half Atan2(Half y, Half x) => (Half)Math.Atan2((float)y, (float)x); + + public static Half Lerp(Half value1, Half value2, Half amount) => (Half)Mathf.Lerp((float)value1, (float)value2, (float)amount); + + // + // IHyperbolicFunctions + // + + public static Half Cosh(Half x) => (Half)Math.Cosh((float)x); + + public static Half Sinh(Half x) => (Half)Math.Sinh((float)x); + + public static Half Tanh(Half x) => (Half)Math.Tanh((float)x); + + // + // IIncrementOperators + // + + public static Half operator ++(Half value) + { + var tmp = (float)value; + ++tmp; + return (Half)tmp; + } + + // + // ILogarithmicFunctions + // + + public static Half Log(Half x) => (Half)Math.Log((float)x); + + public static Half Log(Half x, Half newBase) => (Half)Math.Log((float)x, (float)newBase); + + // + // IModulusOperators + // + + public static Half operator %(Half left, Half right) => (Half)((float)left % (float)right); + + // + // IMultiplicativeIdentity + // + + public static Half MultiplicativeIdentity => new Half(PositiveOneBits); + + // + // IMultiplyOperators + // + + public static Half operator *(Half left, Half right) => (Half)((float)left * (float)right); + + // + // INumber + // + + public static Half Clamp(Half value, Half min, Half max) => (Half)Mathf.Clamp((float)value, (float)min, (float)max); + + public static Half CopySign(Half value, Half sign) + { + // This method is required to work for all inputs, + // including NaN, so we operate on the raw bits. + uint xbits = value._value; + uint ybits = sign._value; + + // Remove the sign from x, and remove everything but the sign from y + // Then, simply OR them to get the correct sign + return new Half((ushort)((xbits & ~SignMask) | (ybits & SignMask))); + } + + public static Half Max(Half x, Half y) => (Half)Math.Max((float)x, (float)y); + + public static Half MaxNumber(Half x, Half y) + { + // This matches the IEEE 754:2019 `maximumNumber` function + // + // It does not propagate NaN inputs back to the caller and + // otherwise returns the larger of the inputs. It + // treats +0 as larger than -0 as per the specification. + + if (x != y) + { + if (!IsNaN(y)) + { + return y < x ? x : y; + } + + return x; + } + + return IsNegative(y) ? x : y; + } + + public static Half Min(Half x, Half y) => (Half)Math.Min((float)x, (float)y); + + public static Half MinNumber(Half x, Half y) + { + // This matches the IEEE 754:2019 `minimumNumber` function + // + // It does not propagate NaN inputs back to the caller and + // otherwise returns the larger of the inputs. It + // treats +0 as larger than -0 as per the specification. + + if (x != y) + { + if (!IsNaN(y)) + { + return x < y ? x : y; + } + + return x; + } + + return IsNegative(x) ? x : y; + } + + public static int Sign(Half value) + { + if (IsNaN(value)) + { + throw new ArithmeticException("SR.Arithmetic_NaN"); + } + + if (IsZero(value)) + { + return 0; + } + else if (IsNegative(value)) + { + return -1; + } + + return +1; + } + + // + // INumberBase + // + + public static Half One => new Half(PositiveOneBits); + + public static Half Zero => new Half(PositiveZeroBits); + + public static Half Abs(Half value) => new Half((ushort)(value._value & ~SignMask)); + + public static bool IsPositive(Half value) => (short)(value._value) >= 0; + + public static bool IsRealNumber(Half value) + { + // A NaN will never equal itself so this is an + // easy and efficient way to check for a real number. + +#pragma warning disable CS1718 + return value == value; +#pragma warning restore CS1718 + } + + // + // IPowerFunctions + // + + public static Half Pow(Half x, Half y) => (Half)Math.Pow((float)x, (float)y); + + // + // IRootFunctions + // + + public static Half Sqrt(Half x) => (Half)Math.Sqrt((float)x); + + // + // ISignedNumber + // + + public static Half NegativeOne => new Half(NegativeOneBits); + + // + // ISubtractionOperators + // + + public static Half operator -(Half left, Half right) => (Half)((float)left - (float)right); + + // + // ITrigonometricFunctions + // + + public static Half Acos(Half x) => (Half)Math.Acos((float)x); + + public static Half Asin(Half x) => (Half)Math.Asin((float)x); + + public static Half Atan(Half x) => (Half)Math.Atan((float)x); + + public static Half Cos(Half x) => (Half)Math.Cos((float)x); + + public static Half Sin(Half x) => (Half)Math.Sin((float)x); + + public static Half Tan(Half x) => (Half)Math.Tan((float)x); + + // + // IUnaryNegationOperators + // + + public static Half operator -(Half value) => (Half)(-(float)value); + + // + // IUnaryPlusOperators + // + + public static Half operator +(Half value) => value; + } +} diff --git a/Assets/Mirror/Core/Tools/Half.cs.meta b/Assets/Mirror/Core/Tools/Half.cs.meta new file mode 100644 index 000000000..a9a519d5e --- /dev/null +++ b/Assets/Mirror/Core/Tools/Half.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 70b5947c49174f3c90121edd21ea6b15 +timeCreated: 1729783714 \ No newline at end of file diff --git a/Assets/Mirror/Tests/Editor/Tools/HalfTests.cs b/Assets/Mirror/Tests/Editor/Tools/HalfTests.cs new file mode 100644 index 000000000..705a0a250 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Tools/HalfTests.cs @@ -0,0 +1,35 @@ +// we are using the dotnet/runtime implementation which is already tested. +// basic test to ensure it generally works as expected. + +using System; +using NUnit.Framework; + +namespace Mirror.Tests.Tools +{ + public class HalfTests + { + [Test] + public void BasicTest() + { + // start from a float + Half half = (Half)42.0f; + Half other = (Half)0.5f; + + // add + Half value = half + other; + Assert.That((float)value, Is.EqualTo(42.5f)); + + // sub + value = half - other; + Assert.That((float)value, Is.EqualTo(41.5f)); + + // compare + Assert.That(half < other, Is.False); + Assert.That(half > other, Is.True); + Assert.That(half == other, Is.False); + Assert.That(half != other, Is.True); + Assert.That(half, Is.EqualTo((Half)42.0f)); + Assert.That(half == (Half)42.0f, Is.True); + } + } +} diff --git a/Assets/Mirror/Tests/Editor/Tools/HalfTests.cs.meta b/Assets/Mirror/Tests/Editor/Tools/HalfTests.cs.meta new file mode 100644 index 000000000..88b06e8b3 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Tools/HalfTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d3c60776399c4bfcb5714b29a1ab7b9f +timeCreated: 1729787685 \ No newline at end of file