using System.Globalization; using System.Linq; using Robust.Shared.Serialization; using Robust.Shared.Utility; namespace Content.Shared.FixedPoint { /// /// Represents a quantity of something, to a precision of 0.01. /// To enforce this level of precision, floats are shifted by 2 decimal points, rounded, and converted to an int. /// [Serializable, CopyByRef] public struct FixedPoint4 : ISelfSerialize, IComparable, IEquatable, IFormattable { public long Value { get; private set; } private const long Shift = 4; private const long ShiftConstant = 10000; // Must be equal to pow(10, Shift) public static FixedPoint4 MaxValue { get; } = new(long.MaxValue); public static FixedPoint4 Epsilon { get; } = new(1); public static FixedPoint4 Zero { get; } = new(0); // This value isn't picked by any proper testing, don't @ me. private const float FloatEpsilon = 0.00001f; #if DEBUG static FixedPoint4() { // ReSharper disable once CompareOfFloatsByEqualityOperator DebugTools.Assert(Math.Pow(10, Shift) == ShiftConstant, "ShiftConstant must be equal to pow(10, Shift)"); } #endif private readonly double ShiftDown() { return Value / (double) ShiftConstant; } private FixedPoint4(long value) { Value = value; } public static FixedPoint4 New(long value) { return new(value * ShiftConstant); } public static FixedPoint4 FromTenThousandths(long value) => new(value); public static FixedPoint4 New(float value) { return new((long) ApplyFloatEpsilon(value * ShiftConstant)); } private static float ApplyFloatEpsilon(float value) { return value + FloatEpsilon * Math.Sign(value); } private static double ApplyFloatEpsilon(double value) { return value + FloatEpsilon * Math.Sign(value); } /// /// Create the closest for a float value, always rounding up. /// public static FixedPoint4 NewCeiling(float value) { return new((long) MathF.Ceiling(value * ShiftConstant)); } public static FixedPoint4 New(double value) { return new((long) ApplyFloatEpsilon(value * ShiftConstant)); } public static FixedPoint4 New(string value) { return New(Parse.Float(value)); } public static FixedPoint4 operator +(FixedPoint4 a) => a; public static FixedPoint4 operator -(FixedPoint4 a) => new(-a.Value); public static FixedPoint4 operator +(FixedPoint4 a, FixedPoint4 b) => new(a.Value + b.Value); public static FixedPoint4 operator -(FixedPoint4 a, FixedPoint4 b) => new(a.Value - b.Value); public static FixedPoint4 operator *(FixedPoint4 a, FixedPoint4 b) { return new(b.Value * a.Value / ShiftConstant); } public static FixedPoint4 operator *(FixedPoint4 a, float b) { return new((long) ApplyFloatEpsilon(a.Value * b)); } public static FixedPoint4 operator *(FixedPoint4 a, double b) { return new((long) ApplyFloatEpsilon(a.Value * b)); } public static FixedPoint4 operator *(FixedPoint4 a, long b) { return new(a.Value * b); } public static FixedPoint4 operator /(FixedPoint4 a, FixedPoint4 b) { return new((long) (ShiftConstant * (long) a.Value / b.Value)); } public static FixedPoint4 operator /(FixedPoint4 a, float b) { return new((long) ApplyFloatEpsilon(a.Value / b)); } public static bool operator <=(FixedPoint4 a, long b) { return a <= New(b); } public static bool operator >=(FixedPoint4 a, long b) { return a >= New(b); } public static bool operator <(FixedPoint4 a, long b) { return a < New(b); } public static bool operator >(FixedPoint4 a, long b) { return a > New(b); } public static bool operator ==(FixedPoint4 a, long b) { return a == New(b); } public static bool operator !=(FixedPoint4 a, long b) { return a != New(b); } public static bool operator ==(FixedPoint4 a, FixedPoint4 b) { return a.Equals(b); } public static bool operator !=(FixedPoint4 a, FixedPoint4 b) { return !a.Equals(b); } public static bool operator <=(FixedPoint4 a, FixedPoint4 b) { return a.Value <= b.Value; } public static bool operator >=(FixedPoint4 a, FixedPoint4 b) { return a.Value >= b.Value; } public static bool operator <(FixedPoint4 a, FixedPoint4 b) { return a.Value < b.Value; } public static bool operator >(FixedPoint4 a, FixedPoint4 b) { return a.Value > b.Value; } public readonly float Float() { return (float) ShiftDown(); } public readonly double Double() { return ShiftDown(); } public readonly long Long() { return Value / ShiftConstant; } public readonly int Int() { return (int)Long(); } // Implicit operators ftw public static implicit operator FixedPoint4(FixedPoint2 n) => new(n.Value * 100); // Offbrand public static implicit operator FixedPoint4(float n) => New(n); public static implicit operator FixedPoint4(double n) => New(n); public static implicit operator FixedPoint4(int n) => New(n); public static implicit operator FixedPoint4(long n) => New(n); public static explicit operator FixedPoint2(FixedPoint4 n) => FixedPoint2.FromRaw((int)(n.Value / 100)); // Offbrand public static explicit operator float(FixedPoint4 n) => n.Float(); public static explicit operator double(FixedPoint4 n) => n.Double(); public static explicit operator int(FixedPoint4 n) => n.Int(); public static explicit operator long(FixedPoint4 n) => n.Long(); public static FixedPoint4 Min(params FixedPoint4[] fixedPoints) { return fixedPoints.Min(); } public static FixedPoint4 Min(FixedPoint4 a, FixedPoint4 b) { return a < b ? a : b; } public static FixedPoint4 Max(FixedPoint4 a, FixedPoint4 b) { return a > b ? a : b; } public static long Sign(FixedPoint4 value) { if (value < Zero) { return -1; } if (value > Zero) { return 1; } return 0; } public static FixedPoint4 Abs(FixedPoint4 a) { return FromTenThousandths(Math.Abs(a.Value)); } public static FixedPoint4 Dist(FixedPoint4 a, FixedPoint4 b) { return FixedPoint4.Abs(a - b); } public static FixedPoint4 Clamp(FixedPoint4 number, FixedPoint4 min, FixedPoint4 max) { if (min > max) { throw new ArgumentException($"{nameof(min)} {min} cannot be larger than {nameof(max)} {max}"); } return number < min ? min : number > max ? max : number; } public override readonly bool Equals(object? obj) { return obj is FixedPoint4 unit && Value == unit.Value; } public override readonly int GetHashCode() { // ReSharper disable once NonReadonlyMemberInGetHashCode return HashCode.Combine(Value); } public void Deserialize(string value) { // TODO implement "lossless" serializer. // I.e., dont use floats. if (value == "MaxValue") Value = int.MaxValue; else this = New(Parse.Double(value)); } public override readonly string ToString() => $"{ShiftDown().ToString(CultureInfo.InvariantCulture)}"; public string ToString(string? format, IFormatProvider? formatProvider) { return ToString(); } public readonly string Serialize() { // TODO implement "lossless" serializer. // I.e., dont use floats. if (Value == int.MaxValue) return "MaxValue"; return ToString(); } public readonly bool Equals(FixedPoint4 other) { return Value == other.Value; } public readonly int CompareTo(FixedPoint4 other) { if (other.Value > Value) { return -1; } if (other.Value < Value) { return 1; } return 0; } } public static class FixedPoint4EnumerableExt { public static FixedPoint4 Sum(this IEnumerable source) { var acc = FixedPoint4.Zero; foreach (var n in source) { acc += n; } return acc; } } }