Faction Test v1.0.1.0
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
namespace Barotrauma.Utils;
|
||||
|
||||
public struct CoordinateSpace2D
|
||||
{
|
||||
public static readonly CoordinateSpace2D CanonicalSpace = new CoordinateSpace2D
|
||||
{
|
||||
Origin = Vector2.Zero,
|
||||
I = Vector2.UnitX,
|
||||
J = Vector2.UnitY
|
||||
};
|
||||
|
||||
public Vector2 Origin;
|
||||
public Vector2 I;
|
||||
public Vector2 J;
|
||||
|
||||
public Matrix LocalToCanonical
|
||||
=> new Matrix(
|
||||
m11: I.X, m12: I.Y, m13: 0f, m14: 0f,
|
||||
m21: J.X, m22: J.Y, m23: 0f, m24: 0f,
|
||||
m31: 0f, m32: 0f, m33: 1f, m34: 0f,
|
||||
m41: 0f, m42: 0f, m43: 0f, m44: 1f)
|
||||
* Matrix.CreateTranslation(Origin.X, Origin.Y, 0f);
|
||||
|
||||
public Matrix CanonicalToLocal => Matrix.Invert(LocalToCanonical);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace Barotrauma
|
||||
if (!Done) { Mre.WaitOne(); }
|
||||
}
|
||||
}
|
||||
private static List<Task> enqueuedTasks;
|
||||
private static readonly List<Task> enqueuedTasks;
|
||||
|
||||
static CrossThread() { enqueuedTasks = new List<Task>(); }
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace Barotrauma
|
||||
|
||||
static class MathUtils
|
||||
{
|
||||
public static Vector2 DiscardZ(this Vector3 vector)
|
||||
=> new Vector2(vector.X, vector.Y);
|
||||
|
||||
public static float Percentage(float portion, float total)
|
||||
{
|
||||
return portion / total * 100;
|
||||
@@ -152,7 +155,6 @@ namespace Barotrauma
|
||||
|
||||
public static float CurveAngle(float from, float to, float step)
|
||||
{
|
||||
|
||||
from = WrapAngleTwoPi(from);
|
||||
to = WrapAngleTwoPi(to);
|
||||
|
||||
@@ -186,13 +188,7 @@ namespace Barotrauma
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
while (angle < 0)
|
||||
angle += MathHelper.TwoPi;
|
||||
while (angle >= MathHelper.TwoPi)
|
||||
angle -= MathHelper.TwoPi;
|
||||
|
||||
return angle;
|
||||
return PositiveModulo(angle, MathHelper.TwoPi);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -204,13 +200,9 @@ namespace Barotrauma
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
// Ensure that -pi <= angle < pi for both "from" and "to"
|
||||
while (angle < -MathHelper.Pi)
|
||||
angle += MathHelper.TwoPi;
|
||||
while (angle >= MathHelper.Pi)
|
||||
angle -= MathHelper.TwoPi;
|
||||
|
||||
return angle;
|
||||
float min = -MathHelper.Pi;
|
||||
float diffFromMin = angle - min;
|
||||
return diffFromMin - (MathF.Floor(diffFromMin / MathHelper.TwoPi) * MathHelper.TwoPi) + min;
|
||||
}
|
||||
|
||||
public static float GetShortestAngle(float from, float to)
|
||||
@@ -339,13 +331,13 @@ namespace Barotrauma
|
||||
|
||||
if (axisAligned1.Y < axisAligned2.Y)
|
||||
{
|
||||
if (y < axisAligned1.Y) return false;
|
||||
if (y > axisAligned2.Y) return false;
|
||||
if (y < axisAligned1.Y) { return false; }
|
||||
if (y > axisAligned2.Y) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (y > axisAligned1.Y) return false;
|
||||
if (y < axisAligned2.Y) return false;
|
||||
if (y > axisAligned1.Y) { return false; }
|
||||
if (y < axisAligned2.Y) { return false; }
|
||||
}
|
||||
|
||||
intersection = new Vector2(axisAligned1.X, y);
|
||||
@@ -361,13 +353,13 @@ namespace Barotrauma
|
||||
|
||||
if (axisAligned1.X < axisAligned2.X)
|
||||
{
|
||||
if (x < axisAligned1.X) return false;
|
||||
if (x > axisAligned2.X) return false;
|
||||
if (x < axisAligned1.X) { return false; }
|
||||
if (x > axisAligned2.X) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (x > axisAligned1.X) return false;
|
||||
if (x < axisAligned2.X) return false;
|
||||
if (x > axisAligned1.X) { return false; }
|
||||
if (x < axisAligned2.X) { return false; }
|
||||
}
|
||||
|
||||
intersection = new Vector2(x, axisAligned1.Y);
|
||||
@@ -898,23 +890,30 @@ namespace Barotrauma
|
||||
// https://stackoverflow.com/questions/3874627/floating-point-comparison-functions-for-c-sharp
|
||||
public static bool NearlyEqual(float a, float b, float epsilon = 0.0001f)
|
||||
{
|
||||
float diff = Math.Abs(a - b);
|
||||
if (a == b)
|
||||
{
|
||||
// shortcut, handles infinities
|
||||
//shortcut, handles infinities
|
||||
return true;
|
||||
}
|
||||
else if (a == 0 || b == 0 || diff < float.Epsilon)
|
||||
|
||||
if (a == 0 || b == 0)
|
||||
{
|
||||
// a or b is zero or both are extremely close to it
|
||||
// relative error is less meaningful here
|
||||
return diff < epsilon;
|
||||
//if a or b is zero, relative error is less meaningful
|
||||
return Math.Abs(a - b) < epsilon;
|
||||
}
|
||||
else
|
||||
|
||||
float absA = Math.Abs(a);
|
||||
float absB = Math.Abs(b);
|
||||
float absAB = absA + absB;
|
||||
if (absAB < epsilon)
|
||||
{
|
||||
// use relative error
|
||||
return diff / (Math.Abs(a) + Math.Abs(b)) < epsilon;
|
||||
// a and b extremely close to zero, relative error is less meaningful
|
||||
return true;
|
||||
}
|
||||
|
||||
float diff = Math.Abs(a - b);
|
||||
// use relative error
|
||||
return diff / absAB < epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.IO;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -12,52 +10,6 @@ namespace Barotrauma
|
||||
{
|
||||
public class Md5Hash
|
||||
{
|
||||
public static class Cache
|
||||
{
|
||||
private const string cachePath = "Data/hashcache.txt";
|
||||
|
||||
private readonly static List<(string Path, Md5Hash Hash, DateTime DateTime)> Entries
|
||||
= new List<(string Path, Md5Hash Hash, DateTime DateTime)>();
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
if (!File.Exists(cachePath)) { return; }
|
||||
var lines = File.ReadAllLines(cachePath);
|
||||
if (Version.TryParse(lines[0], out var cacheVersion) && cacheVersion == GameMain.Version)
|
||||
{
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
string[] split = lines[i].Split('|');
|
||||
string path = split[0].CleanUpPathCrossPlatform();
|
||||
Md5Hash hash = Md5Hash.StringAsHash(split[1]);
|
||||
DateTime? dateTime = null;
|
||||
if (long.TryParse(split[2], out long dateTimeUlong))
|
||||
{
|
||||
dateTime = DateTime.FromBinary(dateTimeUlong);
|
||||
}
|
||||
|
||||
if (File.Exists(path) && dateTime.HasValue && dateTime >= File.GetLastWriteTime(path))
|
||||
{
|
||||
Entries.Add((path, hash, dateTime.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Add(string path, Md5Hash hash, DateTime dateTime)
|
||||
{
|
||||
path = path.CleanUpPathCrossPlatform();
|
||||
Remove(path);
|
||||
Entries.Add((path, hash, dateTime));
|
||||
}
|
||||
|
||||
public static void Remove(string path)
|
||||
{
|
||||
path = path.CleanUpPathCrossPlatform();
|
||||
Entries.RemoveAll(e => e.Path == path);
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32));
|
||||
|
||||
private static string RemoveWhitespace(string s)
|
||||
@@ -212,8 +164,13 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return ShortRepresentation.GetHashCode(StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool operator ==(Md5Hash? a, Md5Hash? b)
|
||||
=> (a is null == b is null) && (a?.Equals(b) ?? true);
|
||||
=> Equals(a, b);
|
||||
|
||||
public static bool operator !=(Md5Hash? a, Md5Hash? b) => !(a == b);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace Barotrauma
|
||||
{
|
||||
public sealed class None<T> : Option<T>
|
||||
{
|
||||
private None() { }
|
||||
|
||||
public static Option<T> Create() => new None<T>();
|
||||
|
||||
public override Option<T> Fallback(Option<T> fallback) => fallback;
|
||||
public override T Fallback(T fallback) => fallback;
|
||||
|
||||
public override bool ValueEquals(T value) => false;
|
||||
|
||||
public override string ToString()
|
||||
=> $"None<{typeof(T).Name}>";
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,83 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <a href="https://en.wikipedia.org/wiki/Option_type">Option type</a>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Credit <a href="https://github.com/Jlobblet/FunctionalStuff/tree/main/src/FunctionalStuff/Option">Jlobblet</a>
|
||||
/// </remarks>
|
||||
public abstract class Option<T>
|
||||
public readonly struct Option<T> where T : notnull
|
||||
{
|
||||
public static Option<T> Some(T value) => Some<T>.Create(value);
|
||||
public static Option<T> None() => None<T>.Create();
|
||||
public bool IsNone() => this is None<T>;
|
||||
public bool IsSome() => this is Some<T>;
|
||||
private readonly bool hasValue;
|
||||
private readonly T? value;
|
||||
|
||||
public bool TryUnwrap(out T outValue) => TryUnwrap<T>(out outValue);
|
||||
|
||||
public bool TryUnwrap<T1>(out T1 outValue) where T1 : T
|
||||
private Option(bool hasValue, T? value)
|
||||
{
|
||||
switch (this)
|
||||
{
|
||||
case Some<T> { Value: T1 value }:
|
||||
outValue = value;
|
||||
return true;
|
||||
default:
|
||||
outValue = default!;
|
||||
return false;
|
||||
}
|
||||
this.hasValue = hasValue;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Option<TType> Select<TType>(Func<T, TType> selector) =>
|
||||
this switch
|
||||
public bool IsSome() => hasValue;
|
||||
public bool IsNone() => !IsSome();
|
||||
|
||||
public bool TryUnwrap<T1>([NotNullWhen(returnValue: true)] out T1? outValue) where T1 : T
|
||||
{
|
||||
bool hasValueOfGivenType = false;
|
||||
outValue = default;
|
||||
|
||||
if (hasValue && value is T1 t1)
|
||||
{
|
||||
Some<T> { Value: var value } => Option<TType>.Some(selector.Invoke(value)),
|
||||
None<T> _ => Option<TType>.None(),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
hasValueOfGivenType = true;
|
||||
outValue = t1;
|
||||
}
|
||||
|
||||
return hasValueOfGivenType;
|
||||
}
|
||||
|
||||
public bool TryUnwrap([NotNullWhen(returnValue: true)] out T? outValue)
|
||||
=> TryUnwrap<T>(out outValue);
|
||||
|
||||
public Option<TType> Select<TType>(Func<T, TType> selector) where TType : notnull
|
||||
=> TryUnwrap(out T? selfValue) ? Option.Some(selector(selfValue)) : Option.None;
|
||||
|
||||
public Option<TType> Bind<TType>(Func<T, Option<TType>> binder) where TType : notnull
|
||||
=> TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None;
|
||||
|
||||
public T Fallback(T fallback)
|
||||
=> TryUnwrap(out var v) ? v : fallback;
|
||||
|
||||
public Option<T> Fallback(Option<T> fallback)
|
||||
=> IsSome() ? this : fallback;
|
||||
|
||||
public static Option<T> Some(T value)
|
||||
=> typeof(T) switch
|
||||
{
|
||||
var t when t == typeof(bool)
|
||||
=> throw new Exception("Option type rejects booleans"),
|
||||
{IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Option<>)
|
||||
=> throw new Exception("Option type rejects nested Option"),
|
||||
{IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Nullable<>)
|
||||
=> throw new Exception("Option type rejects Nullable"),
|
||||
_
|
||||
=> new Option<T>(hasValue: true, value: value ?? throw new Exception("Option type rejects null"))
|
||||
};
|
||||
|
||||
public abstract Option<T> Fallback(Option<T> fallback);
|
||||
public abstract T Fallback(T fallback);
|
||||
|
||||
public abstract bool ValueEquals(T value);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj switch
|
||||
{
|
||||
Some<T> { Value: var value } => this is Some<T> { Value: { } selfValue } && selfValue.Equals(value),
|
||||
None<T> _ => IsNone(),
|
||||
T value => this is Some<T> { Value: { } selfValue } && selfValue.Equals(value),
|
||||
_ => false
|
||||
Option<T> otherOption when otherOption.IsNone()
|
||||
=> IsNone(),
|
||||
Option<T> otherOption when otherOption.TryUnwrap(out var otherValue)
|
||||
=> ValueEquals(otherValue),
|
||||
T otherValue
|
||||
=> ValueEquals(otherValue),
|
||||
_
|
||||
=> false
|
||||
};
|
||||
|
||||
public bool ValueEquals(T otherValue)
|
||||
=> TryUnwrap(out T? selfValue) && selfValue.Equals(otherValue);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> this is Some<T> { Value: { } value } ? value.GetHashCode() : 0;
|
||||
=> TryUnwrap(out T? selfValue) ? selfValue.GetHashCode() : 0;
|
||||
|
||||
public static bool operator ==(Option<T> a, Option<T> b)
|
||||
=> a.Equals(b);
|
||||
@@ -62,22 +85,28 @@ namespace Barotrauma
|
||||
public static bool operator !=(Option<T> a, Option<T> b)
|
||||
=> !(a == b);
|
||||
|
||||
public abstract override string ToString();
|
||||
|
||||
public static implicit operator Option<T>(Option.UnspecifiedNone _)
|
||||
public static Option<T> None()
|
||||
=> default;
|
||||
|
||||
public static implicit operator Option<T>(in Option.UnspecifiedNone _)
|
||||
=> None();
|
||||
|
||||
public override string ToString()
|
||||
=> TryUnwrap(out var selfValue)
|
||||
? $"Some<{typeof(T).Name}>({selfValue})"
|
||||
: $"None<{typeof(T).Name}>";
|
||||
}
|
||||
|
||||
public static class Option
|
||||
{
|
||||
public sealed class UnspecifiedNone
|
||||
public static Option<T> Some<T>(T value) where T : notnull
|
||||
=> Option<T>.Some(value);
|
||||
|
||||
public static UnspecifiedNone None
|
||||
=> default;
|
||||
|
||||
public readonly ref struct UnspecifiedNone
|
||||
{
|
||||
private UnspecifiedNone() { }
|
||||
internal static readonly UnspecifiedNone Instance = new();
|
||||
}
|
||||
|
||||
public static UnspecifiedNone None => UnspecifiedNone.Instance;
|
||||
|
||||
public static Option<T> Some<T>(T value) => Option<T>.Some(value);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public sealed class Some<T> : Option<T>
|
||||
{
|
||||
public readonly T Value;
|
||||
|
||||
private Some(T value)
|
||||
{
|
||||
if (value is null) { throw new ArgumentNullException(nameof(value), "Some<T> cannot contain null"); }
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static Option<T> Create(T value) => new Some<T>(value);
|
||||
|
||||
public override Option<T> Fallback(Option<T> fallback) => this;
|
||||
public override T Fallback(T fallback) => Value;
|
||||
|
||||
public override bool ValueEquals(T value) => Value.Equals(value);
|
||||
|
||||
public override string ToString()
|
||||
=> $"Some<{typeof(T).Name}>({Value})";
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace Barotrauma
|
||||
return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T)));
|
||||
}
|
||||
|
||||
public static Option<TBase> ParseDerived<TBase, TInput>(TInput input) where TInput : notnull
|
||||
public static Option<TBase> ParseDerived<TBase, TInput>(TInput input) where TInput : notnull where TBase : notnull
|
||||
{
|
||||
static Option<TBase> none() => Option<TBase>.None();
|
||||
|
||||
@@ -54,10 +54,10 @@ namespace Barotrauma
|
||||
f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs);
|
||||
|
||||
return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) })
|
||||
as Option<TBase> ?? none();
|
||||
as Option<TBase>? ?? none();
|
||||
}
|
||||
|
||||
return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none();
|
||||
return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome());
|
||||
}
|
||||
|
||||
public static string NameWithGenerics(this Type t)
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Xml.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Barotrauma.IO;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -292,10 +293,11 @@ namespace Barotrauma
|
||||
}
|
||||
if (doc?.Root == null)
|
||||
{
|
||||
saveInfos.Add(new CampaignMode.SaveInfo()
|
||||
{
|
||||
FilePath = file
|
||||
});
|
||||
saveInfos.Add(new CampaignMode.SaveInfo(
|
||||
FilePath: file,
|
||||
SaveTime: Option.None,
|
||||
SubmarineName: "",
|
||||
EnabledContentPackageNames: ImmutableArray<string>.Empty));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -326,13 +328,11 @@ namespace Barotrauma
|
||||
enabledContentPackageNames.Add(packageName.Replace(@"\|", "|"));
|
||||
}
|
||||
|
||||
saveInfos.Add(new CampaignMode.SaveInfo()
|
||||
{
|
||||
FilePath = file,
|
||||
SubmarineName = doc?.Root?.GetAttributeStringUnrestricted("submarine", ""),
|
||||
SaveTime = doc.Root.GetAttributeInt("savetime", 0),
|
||||
EnabledContentPackageNames = enabledContentPackageNames.ToArray(),
|
||||
});
|
||||
saveInfos.Add(new CampaignMode.SaveInfo(
|
||||
FilePath: file,
|
||||
SaveTime: doc.Root.GetAttributeDateTime("savetime"),
|
||||
SubmarineName: doc?.Root?.GetAttributeStringUnrestricted("submarine", ""),
|
||||
EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Barotrauma.Networking;
|
||||
*/
|
||||
|
||||
[NetworkSerialize]
|
||||
public readonly record struct Segment<T>(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct;
|
||||
public readonly record struct Segment<T>(T Identifier, int Pointer) : INetSerializableStruct where T : struct;
|
||||
|
||||
readonly ref struct SegmentTableWriter<T> where T : struct
|
||||
{
|
||||
@@ -94,7 +94,7 @@ readonly ref struct SegmentTableWriter<T> where T : struct
|
||||
public static SegmentTableWriter<T> StartWriting(IWriteMessage msg)
|
||||
{
|
||||
var retVal = new SegmentTableWriter<T>(msg, msg.BitPosition);
|
||||
msg.WriteUInt16(0); //reserve space for the table pointer
|
||||
msg.WriteInt32(0); //reserve space for the table pointer
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@@ -104,28 +104,22 @@ readonly ref struct SegmentTableWriter<T> where T : struct
|
||||
{
|
||||
throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>");
|
||||
}
|
||||
|
||||
if (message.BitPosition - PointerLocation > UInt16.MaxValue)
|
||||
{
|
||||
throw new OverflowException(
|
||||
$"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void StartNewSegment(T value)
|
||||
{
|
||||
ThrowOnInvalidState();
|
||||
segments.Add(new Segment<T>(value, (UInt16)(message.BitPosition-PointerLocation)));
|
||||
segments.Add(new Segment<T>(value, message.BitPosition - PointerLocation));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ThrowOnInvalidState();
|
||||
int tablePosition = message.BitPosition;
|
||||
|
||||
|
||||
//rewrite the table pointer now that we know where the table ends
|
||||
message.BitPosition = PointerLocation;
|
||||
message.WriteUInt16((UInt16)(tablePosition-PointerLocation));
|
||||
message.WriteInt32(tablePosition - PointerLocation);
|
||||
|
||||
//write the table
|
||||
message.BitPosition = tablePosition;
|
||||
@@ -274,7 +268,7 @@ readonly ref struct SegmentTableReader<T> where T : struct
|
||||
ExceptionHandler? exceptionHandler = null)
|
||||
{
|
||||
int pointerLocation = msg.BitPosition;
|
||||
int tablePointer = msg.ReadUInt16();
|
||||
int tablePointer = msg.ReadInt32();
|
||||
int tableLocation = pointerLocation + tablePointer;
|
||||
|
||||
int returnPosition = msg.BitPosition;
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public readonly struct SerializableTimeZone
|
||||
{
|
||||
/// <summary>
|
||||
/// Diff from UTC
|
||||
/// </summary>
|
||||
public readonly TimeSpan Value;
|
||||
|
||||
private readonly int hours;
|
||||
private readonly int minutes;
|
||||
private readonly char sign;
|
||||
|
||||
public SerializableTimeZone(TimeSpan value)
|
||||
{
|
||||
Value = new TimeSpan(
|
||||
hours: value.Hours,
|
||||
minutes: value.Minutes,
|
||||
seconds: 0);
|
||||
|
||||
hours = Math.Abs(value.Hours);
|
||||
minutes = Math.Abs(value.Minutes);
|
||||
sign = Value.Ticks < 0 ? '-' : '+';
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> (hours, minutes) switch
|
||||
{
|
||||
(0, 0) => "UTC",
|
||||
(_, 0) => $"UTC{sign}{hours}",
|
||||
(_, < 10) => $"UTC{sign}{hours}:0{minutes}",
|
||||
_ => $"UTC{sign}{hours}:{minutes}"
|
||||
};
|
||||
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(Value.Ticks < 0, hours, minutes);
|
||||
|
||||
public static SerializableTimeZone FromDateTime(DateTime dateTime)
|
||||
{
|
||||
if (dateTime.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot determine timezone for {nameof(DateTime)} " +
|
||||
$"of unspecified kind");
|
||||
}
|
||||
var utcDateTime = dateTime.ToUniversalTime();
|
||||
return new SerializableTimeZone(dateTime - utcDateTime);
|
||||
}
|
||||
|
||||
public static SerializableTimeZone LocalTimeZone
|
||||
=> FromDateTime(DateTime.Now);
|
||||
|
||||
public static Option<SerializableTimeZone> Parse(string str)
|
||||
{
|
||||
if (!str.StartsWith("UTC", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Option<SerializableTimeZone>.None();
|
||||
}
|
||||
string timeZoneStr = str[3..];
|
||||
bool negative = timeZoneStr.StartsWith("-");
|
||||
bool valid = negative || timeZoneStr.StartsWith("+");
|
||||
|
||||
if (!valid) { return Option<SerializableTimeZone>.None(); }
|
||||
|
||||
timeZoneStr = str[4..];
|
||||
|
||||
TimeSpan makeTimeSpan(int hours, int minutes)
|
||||
=> new TimeSpan(
|
||||
ticks: (hours * TimeSpan.TicksPerHour + minutes * TimeSpan.TicksPerMinute)
|
||||
* (negative ? -1L : 1L));
|
||||
|
||||
if (timeZoneStr.IndexOf(':') is var hrMinSeparator && hrMinSeparator > 0)
|
||||
{
|
||||
if (int.TryParse(timeZoneStr[..hrMinSeparator], out int timeZoneHours)
|
||||
&& int.TryParse(timeZoneStr[(hrMinSeparator + 1)..], out int timeZoneMinutes))
|
||||
{
|
||||
return Option<SerializableTimeZone>.Some(
|
||||
new SerializableTimeZone(makeTimeSpan(timeZoneHours, timeZoneMinutes)));
|
||||
}
|
||||
}
|
||||
else if (int.TryParse(timeZoneStr, out int timeZoneHours))
|
||||
{
|
||||
return Option<SerializableTimeZone>.Some(
|
||||
new SerializableTimeZone(makeTimeSpan(timeZoneHours, 0)));
|
||||
}
|
||||
return Option<SerializableTimeZone>.None();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DateTime wrapper that tries to offer a reliable
|
||||
/// string representation that's also human-friendly
|
||||
/// </summary>
|
||||
public readonly struct SerializableDateTime : IComparable<SerializableDateTime>
|
||||
{
|
||||
public bool Equals(SerializableDateTime other)
|
||||
=> ToUtc().value.Equals(other.ToUtc().value);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is SerializableDateTime other && Equals(other);
|
||||
|
||||
private static DateTime UnixEpoch(DateTimeKind kind)
|
||||
=> new DateTime(1970, 1, 1, 0, 0, 0, kind);
|
||||
|
||||
private readonly DateTime value;
|
||||
public readonly SerializableTimeZone TimeZone;
|
||||
|
||||
public SerializableDateTime(DateTime value) : this(value, default)
|
||||
{
|
||||
if (value.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
throw new Exception($"Timezone required when constructing {nameof(SerializableDateTime)} " +
|
||||
$"from {nameof(DateTime)} of unspecified kind");
|
||||
}
|
||||
TimeZone = SerializableTimeZone.FromDateTime(value);
|
||||
}
|
||||
|
||||
public SerializableDateTime(DateTime value, SerializableTimeZone timeZone)
|
||||
{
|
||||
this.value = new DateTime(
|
||||
value.Year, value.Month, value.Day,
|
||||
value.Hour, value.Minute, value.Second,
|
||||
DateTimeKind.Unspecified);
|
||||
TimeZone = timeZone;
|
||||
}
|
||||
|
||||
public static SerializableDateTime LocalNow
|
||||
=> new SerializableDateTime(DateTime.Now);
|
||||
|
||||
public static SerializableDateTime UtcNow
|
||||
=> new SerializableDateTime(DateTime.UtcNow);
|
||||
|
||||
public SerializableDateTime ToUtc()
|
||||
=> new SerializableDateTime(
|
||||
DateTime.SpecifyKind(value - TimeZone.Value, DateTimeKind.Utc));
|
||||
|
||||
public SerializableDateTime ToLocal()
|
||||
=> new SerializableDateTime(
|
||||
new DateTime(ticks: value.Ticks) - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value,
|
||||
SerializableTimeZone.LocalTimeZone);
|
||||
|
||||
public long Ticks => value.Ticks;
|
||||
|
||||
public DateTime ToUtcValue() => ToUtc().value;
|
||||
public DateTime ToLocalValue() => ToLocal().value;
|
||||
|
||||
public static SerializableDateTime FromLocalUnixTime(long unixTime)
|
||||
=> new SerializableDateTime(UnixEpoch(DateTimeKind.Local) + TimeSpan.FromSeconds(unixTime));
|
||||
|
||||
public static SerializableDateTime FromUtcUnixTime(long unixTime)
|
||||
=> new SerializableDateTime(UnixEpoch(DateTimeKind.Utc) + TimeSpan.FromSeconds(unixTime));
|
||||
|
||||
public long ToUnixTime()
|
||||
=> (value - UnixEpoch(value.Kind)).Ticks / TimeSpan.TicksPerSecond;
|
||||
|
||||
private static string MakeString(params (long Value, string Suffix)[] parts)
|
||||
=> string.Join(' ',
|
||||
parts.Select(p => $"{p.Value.ToString().PadLeft(2, '0')}{p.Suffix}"));
|
||||
|
||||
public override string ToString()
|
||||
=> MakeString(
|
||||
// Let's go out of our way to tag
|
||||
// the year, month and day so nobody
|
||||
// gets confused about the meaning of
|
||||
// each number
|
||||
(value.Year, "Y"),
|
||||
(value.Month, "M"),
|
||||
(value.Day, "D"),
|
||||
|
||||
(value.Hour, "HR"),
|
||||
(value.Minute, "MIN"),
|
||||
(value.Second, "SEC"))
|
||||
+ $" {TimeZone}";
|
||||
|
||||
public string ToLocalUserString()
|
||||
=> ToLocalValue().ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(
|
||||
value.Year, value.Month, value.Day,
|
||||
value.Hour, value.Minute, value.Second,
|
||||
TimeZone.GetHashCode());
|
||||
|
||||
public static Option<SerializableDateTime> Parse(string str)
|
||||
{
|
||||
if (long.TryParse(str, out long unixTime)
|
||||
&& unixTime > 0
|
||||
&& unixTime < (DateTime.MaxValue - UnixEpoch(DateTimeKind.Utc)).TotalSeconds)
|
||||
{
|
||||
return Option<SerializableDateTime>.Some(FromUtcUnixTime(unixTime));
|
||||
}
|
||||
|
||||
string[] split = str.Split(' ');
|
||||
|
||||
int year = 0; int month = 0; int day = 0;
|
||||
int hour = 0; int minute = 0; int second = 0;
|
||||
SerializableTimeZone timeZone = default;
|
||||
foreach (var part in split)
|
||||
{
|
||||
if (SerializableTimeZone.Parse(part).TryUnwrap(out var parsedTimeZone))
|
||||
{
|
||||
timeZone = parsedTimeZone;
|
||||
continue;
|
||||
}
|
||||
|
||||
Identifier suffix = string.Join("", part.Where(char.IsLetter)).ToIdentifier();
|
||||
if (!part.EndsWith(suffix.Value)) { continue; }
|
||||
if (!int.TryParse(
|
||||
part[..^suffix.Value.Length],
|
||||
NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture,
|
||||
out int value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (suffix == "Y") { year = value; }
|
||||
else if (suffix == "M") { month = value; }
|
||||
else if (suffix == "D") { day = value; }
|
||||
else if (suffix == "HR") { hour = value; }
|
||||
else if (suffix == "MIN") { minute = value; }
|
||||
else if (suffix == "SEC") { second = value; }
|
||||
}
|
||||
|
||||
if (year > 0 && month > 0 && day > 0)
|
||||
{
|
||||
return Option<SerializableDateTime>.Some(
|
||||
new SerializableDateTime(
|
||||
new DateTime(year, month, day, hour, minute, second),
|
||||
timeZone));
|
||||
}
|
||||
|
||||
return Option<SerializableDateTime>.None();
|
||||
}
|
||||
|
||||
public int CompareTo(SerializableDateTime other)
|
||||
=> ToUtc().value.CompareTo(other.ToUtc().value);
|
||||
|
||||
public static bool operator <(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> a.CompareTo(b) < 0;
|
||||
|
||||
public static bool operator >(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> a.CompareTo(b) > 0;
|
||||
|
||||
public static bool operator ==(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> a.CompareTo(b) == 0;
|
||||
|
||||
public static bool operator !=(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> !(a == b);
|
||||
|
||||
public static SerializableDateTime operator +(in SerializableDateTime dt, in TimeSpan ts)
|
||||
=> new SerializableDateTime(dt.value + ts, dt.TimeZone);
|
||||
|
||||
public static SerializableDateTime operator -(in SerializableDateTime dt, in TimeSpan ts)
|
||||
=> new SerializableDateTime(dt.value - ts, dt.TimeZone);
|
||||
|
||||
public static TimeSpan operator -(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> a.ToUtc().value - b.ToUtc().value;
|
||||
}
|
||||
}
|
||||
@@ -29,49 +29,6 @@ namespace Barotrauma
|
||||
|
||||
static partial class ToolBox
|
||||
{
|
||||
internal static class Epoch
|
||||
{
|
||||
private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current Unix Epoch (Coordinated Universal Time)
|
||||
/// </summary>
|
||||
public static int NowUTC
|
||||
{
|
||||
get
|
||||
{
|
||||
return (int)(DateTime.UtcNow.Subtract(epoch).TotalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current Unix Epoch (user's current time)
|
||||
/// </summary>
|
||||
public static int NowLocal
|
||||
{
|
||||
get
|
||||
{
|
||||
return (int)(DateTime.Now.Subtract(epoch).TotalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an epoch to a datetime
|
||||
/// </summary>
|
||||
public static DateTime ToDateTime(decimal unixTime)
|
||||
{
|
||||
return epoch.AddSeconds((long)unixTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a DateTime to a unix time
|
||||
/// </summary>
|
||||
public static uint FromDateTime(DateTime dt)
|
||||
{
|
||||
return (uint)(dt.Subtract(epoch).TotalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsProperFilenameCase(string filename)
|
||||
{
|
||||
//File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds.
|
||||
|
||||
Reference in New Issue
Block a user