Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2024-03-28 14:26:18 -03:00
271 changed files with 13174 additions and 3021 deletions
@@ -0,0 +1,59 @@
#nullable enable
using System;
using System.Collections.Immutable;
namespace Barotrauma;
public enum AchievementStat
{
GameLaunchCount,
MonstersKilled,
HumansKilled,
KMsTraveled,
HoursInEditor,
MetersTraveled,
MinutesInEditor
}
public static class AchievementStatExtension
{
public static readonly ImmutableArray<AchievementStat> SteamStats = new []
{
AchievementStat.KMsTraveled,
AchievementStat.HoursInEditor,
AchievementStat.HumansKilled,
AchievementStat.MonstersKilled
}.ToImmutableArray();
public static readonly ImmutableArray<AchievementStat> EosStats = new []
{
AchievementStat.MetersTraveled,
AchievementStat.MinutesInEditor,
AchievementStat.HumansKilled,
AchievementStat.MonstersKilled
}.ToImmutableArray();
public static bool IsFloatStat(this AchievementStat stat) =>
stat switch
{
AchievementStat.KMsTraveled => true,
AchievementStat.HoursInEditor => true,
_ => false
};
public static AchievementStat FromIdentifier(Identifier identifier) =>
Enum.TryParse(value: identifier.ToString().ToLowerInvariant(), ignoreCase: true, result: out AchievementStat stat)
? stat
: throw new ArgumentException($"Invalid achievement stat identifier \"{identifier}\"");
public static (AchievementStat Stat, int Value) ToEos(this AchievementStat stat, float value) =>
stat switch
{
AchievementStat.KMsTraveled => (AchievementStat.MetersTraveled, (int)MathF.Floor(value * 1000f)),
AchievementStat.HoursInEditor => (AchievementStat.MinutesInEditor, (int)MathF.Floor(value * 60f)),
_ => (stat, (int)value)
};
public static (AchievementStat Stat, float Value) ToSteam(this AchievementStat stat, float value) =>
(stat, value);
}
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Barotrauma</RootNamespace>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugType>full</DebugType>
<WarningsAsErrors>;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugType>full</DebugType>
<WarningsAsErrors>;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\XNATypes\XNATypes.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,54 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class ColorExtensions
{
public static Color Multiply(this Color color, float value, bool onlyAlpha = false)
{
return onlyAlpha ?
new Color(color.R, color.G, color.B, (byte)(color.A * value)) :
new Color((byte)(color.R * value), (byte)(color.G * value), (byte)(color.B * value), (byte)(color.A * value));
}
public static Color Multiply(this Color thisColor, Color color)
{
return new Color((byte)(thisColor.R * color.R / 255f), (byte)(thisColor.G * color.G / 255f), (byte)(thisColor.B * color.B / 255f), (byte)(thisColor.A * color.A / 255f));
}
public static Color Opaque(this Color color)
{
return new Color(color.R, color.G, color.B, (byte)255);
}
private static bool IsFirstColorChannelDominant(byte first, byte second, byte third, float minimumRatio = 2)
=> first > second * minimumRatio && first > third * minimumRatio;
/// <summary>
/// Is the value of the red channel at least 'minimumRatio' larger than the blue and green
/// </summary>
public static bool IsRedDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0)
=> color.A > minimumAlpha &&
IsFirstColorChannelDominant(
first: color.R,
color.G, color.B, minimumRatio);
/// <summary>
/// Is the value of the green channel at least 'minimumRatio' larger than the red and blue
/// </summary>
public static bool IsGreenDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0)
=> color.A > minimumAlpha &&
IsFirstColorChannelDominant(
first: color.G,
color.R, color.B, minimumRatio);
/// <summary>
/// Is the value of the blue channel at least 'minimumRatio' larger than the red and green
/// </summary>
public static bool IsBlueDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0)
=> color.A > minimumAlpha &&
IsFirstColorChannelDominant(
first: color.B,
color.G, color.R, minimumRatio);
}
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma.Extensions;
public static class EnumerableExtensionsCore
{
public static ImmutableDictionary<TKey, TValue> ToImmutableDictionary<TKey, TValue>(this IEnumerable<(TKey, TValue)> enumerable)
where TKey : notnull
{
return enumerable.ToDictionary().ToImmutableDictionary();
}
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<(TKey, TValue)> enumerable)
where TKey : notnull
{
var dictionary = new Dictionary<TKey, TValue>();
foreach (var (k,v) in enumerable)
{
dictionary.Add(k, v);
}
return dictionary;
}
[return: NotNullIfNotNull("immutableDictionary")]
public static Dictionary<TKey, TValue>? ToMutable<TKey, TValue>(this ImmutableDictionary<TKey, TValue>? immutableDictionary)
where TKey : notnull
{
if (immutableDictionary == null) { return null; }
return new Dictionary<TKey, TValue>(immutableDictionary);
}
}
@@ -0,0 +1,61 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class PointExtensions
{
public static Point Multiply(this Point p, float f)
{
return new Point((int)(p.X * f), (int)(p.Y * f));
}
public static Point Multiply(this Point p, int i)
{
return new Point(p.X * i, p.Y * i);
}
public static Point Multiply(this Point p, Vector2 v)
{
return new Point((int)(p.X * v.X), (int)(p.Y * v.Y));
}
public static Point Divide(this Point p, int i)
{
if (i == 0) { return Point.Zero; }
return new Point(p.X / i, p.Y / i);
}
public static Point Divide(this Point p, float f)
{
if (f == 0) { return Point.Zero; }
return new Point((int)(p.X / f), (int)(p.Y / f));
}
public static Point Divide(this Point p, Vector2 v)
{
if (v.X == 0 || v.Y == 0) { return Point.Zero; }
return new Point((int)(p.X / v.X), (int)(p.Y / v.Y));
}
/// <summary>
/// Negates the X and Y components.
/// </summary>
public static Point Inverse(this Point p)
{
return new Point(-p.X, -p.Y);
}
/// <summary>
/// Flips the X and Y components.
/// </summary>
public static Point Flip(this Point p)
{
return new Point(p.Y, p.X);
}
public static Point Clamp(this Point p, Point min, Point max)
{
return new Point(MathHelper.Clamp(p.X, min.X, max.X), MathHelper.Clamp(p.Y, min.Y, max.Y));
}
}
}
@@ -0,0 +1,99 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class RectangleExtensions
{
public static Rectangle Multiply(this Rectangle rect, float f)
{
Vector2 location = new Vector2(rect.X, rect.Y) * f;
return new Rectangle(new Point((int)location.X, (int)location.Y), rect.MultiplySize(f));
}
public static Rectangle Divide(this Rectangle rect, float f)
{
Vector2 location = new Vector2(rect.X, rect.Y) / f;
return new Rectangle(new Point((int)location.X, (int)location.Y), rect.DivideSize(f));
}
public static Point DivideSize(this Rectangle rect, float f)
{
return new Point((int)(rect.Width / f), (int)(rect.Height / f));
}
public static Point DivideSize(this Rectangle rect, Vector2 f)
{
return new Point((int)(rect.Width / f.X), (int)(rect.Height / f.Y));
}
public static Point MultiplySize(this Rectangle rect, float f)
{
return new Point((int)(rect.Width * f), (int)(rect.Height * f));
}
public static Point MultiplySize(this Rectangle rect, Vector2 f)
{
return new Point((int)(rect.Width * f.X), (int)(rect.Height * f.Y));
}
public static Vector2 CalculateRelativeSize(this Rectangle rect, Rectangle relativeRect)
{
return new Vector2(rect.Width, rect.Height) / new Vector2(relativeRect.Width, relativeRect.Height);
}
public static Rectangle ScaleSize(this Rectangle rect, Rectangle relativeTo)
{
return rect.ScaleSize(rect.CalculateRelativeSize(relativeTo));
}
public static Rectangle ScaleSize(this Rectangle rect, Vector2 scale)
{
var size = rect.MultiplySize(scale);
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
}
public static Rectangle ScaleSize(this Rectangle rect, float scale)
{
var size = rect.MultiplySize(scale);
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
}
public static bool IntersectsWorld(this Rectangle rect, Rectangle value)
{
int bottom = rect.Y - rect.Height;
int otherBottom = value.Y - value.Height;
return value.Left < rect.Right && rect.Left < value.Right &&
value.Top > bottom && rect.Top > otherBottom;
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Rectangle other)
{
return
(rect.X <= other.X) && ((other.X + other.Width) <= (rect.X + rect.Width)) &&
(rect.Y >= other.Y) && ((other.Y - other.Height) >= (rect.Y - rect.Height));
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Vector2 point)
{
return
(rect.X <= point.X) && (point.X < (rect.X + rect.Width)) &&
(rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height));
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Point point)
{
return
(rect.X <= point.X) && (point.X < (rect.X + rect.Width)) &&
(rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height));
}
}
}
@@ -0,0 +1,11 @@
using System;
namespace Barotrauma.Extensions;
public static class RngExtensions
{
public static float Range(this Random rng, float minimum, float maximum)
=> (float)rng.Range((double)minimum, (double)maximum);
public static double Range(this Random rng, double minimum, double maximum)
=> rng.NextDouble() * (maximum - minimum) + minimum;
}
@@ -0,0 +1,66 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Barotrauma
{
public static class StringExtensions
{
[return: NotNullIfNotNull("fallback")]
public static string? FallbackNullOrEmpty(this string? s, string? fallback) => string.IsNullOrEmpty(s) ? fallback : s;
public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrEmpty(s);
public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrWhiteSpace(s);
public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal)
=> s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s;
public static bool IsTrueString(this string s)
=> s.Length == 4
&& s[0] is 'T' or 't'
&& s[1] is 'R' or 'r'
&& s[2] is 'U' or 'u'
&& s[3] is 'E' or 'e';
public static string JoinEscaped(this IEnumerable<string> strings, char joiner)
{
return string.Join(
joiner,
strings.Select(s => s
.Replace("\\", "\\\\")
.Replace(joiner.ToString(), $"\\{joiner}")));
}
public static IReadOnlyList<string> SplitEscaped(this string str, char joiner)
{
bool isEscape(int i)
{
return i >= 0 && str[i] == '\\' && !isEscape(i - 1);
}
var retVal = new List<string>();
int lastSplitIndex = 0;
for (int i = 0; i < str.Length; i++)
{
if (str[i] == joiner && !isEscape(i - 1))
{
retVal.Add(str[lastSplitIndex..i]);
lastSplitIndex = i + 1;
}
if (isEscape(i) && (i >= str.Length - 1 || (str[i+1] != joiner && str[i+1] != '\\')))
{
throw new ArgumentOutOfRangeException($"The string \"{str}\" could not have been produced by a call to {nameof(JoinEscaped)} with joiner '{joiner}'");
}
}
retVal.Add(str[lastSplitIndex..]);
for (int i = 0; i < retVal.Count; i++)
{
retVal[i] = retVal[i]
.Replace($"\\{joiner}", joiner.ToString())
.Replace("\\\\", "\\");
}
return retVal;
}
}
}
@@ -0,0 +1,179 @@
using System.Globalization;
using Microsoft.Xna.Framework;
using System.Linq;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
public static class StringFormatter
{
public static string Replace(this string s, string replacement, Func<char, bool> predicate)
{
var newString = new string[s.Length];
for (int i = 0; i < s.Length; i++)
{
char letter = s[i];
string newLetter = letter.ToString();
if (predicate(letter))
{
newLetter = replacement;
}
newString[i] = newLetter;
}
return new string(newString.SelectMany(str => str.ToCharArray()).ToArray());
}
public static string Remove(this string s, string substring, StringComparison comparisonType = StringComparison.Ordinal)
{
return s.Replace(substring, string.Empty, comparisonType);
}
public static string Remove(this string s, Func<char, bool> predicate)
{
return new string(s.ToCharArray().Where(c => !predicate(c)).ToArray());
}
public static string RemoveWhitespace(this string s)
{
return s.Remove(c => char.IsWhiteSpace(c));
}
public static string FormatSingleDecimal(this float value)
{
return value.ToString("F1", CultureInfo.InvariantCulture);
}
public static string FormatDoubleDecimal(this float value)
{
return value.ToString("F2", CultureInfo.InvariantCulture);
}
public static string FormatZeroDecimal(this float value)
{
return value.ToString("F0", CultureInfo.InvariantCulture);
}
public static string Format(this float value, int decimalCount)
{
return value.ToString($"F{decimalCount}", CultureInfo.InvariantCulture);
}
public static string FormatSingleDecimal(this Vector2 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()})";
}
public static string FormatSingleDecimal(this Vector3 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()}, {value.Z.FormatSingleDecimal()})";
}
public static string FormatSingleDecimal(this Vector4 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()}, {value.Z.FormatSingleDecimal()}, {value.W.FormatSingleDecimal()})";
}
public static string FormatDoubleDecimal(this Vector2 value)
{
return $"({value.X.FormatDoubleDecimal()}, {value.Y.FormatDoubleDecimal()})";
}
public static string FormatZeroDecimal(this Vector2 value)
{
return $"({value.X.FormatZeroDecimal()}, {value.Y.FormatZeroDecimal()})";
}
public static string Format(this Vector2 value, int decimalCount)
{
return $"({value.X.Format(decimalCount)}, {value.Y.Format(decimalCount)})";
}
/// <summary>
/// Capitalises the first letter (invariant) and forces the rest to lower case (invariant).
/// </summary>
public static string CapitaliseFirstInvariant(this string s)
{
if (string.IsNullOrEmpty(s)) { return string.Empty; }
return s.Substring(0, 1).ToUpperInvariant() + s.Substring(1, s.Length - 1).ToLowerInvariant();
}
/// <summary>
/// Adds spaces into a CamelCase string.
/// </summary>
public static string FormatCamelCaseWithSpaces(this string str)
{
return new string(InsertSpacesBeforeCaps(str).ToArray());
IEnumerable<char> InsertSpacesBeforeCaps(IEnumerable<char> input)
{
int i = 0;
int lastChar = input.Count() - 1;
foreach (char c in input)
{
if (char.IsUpper(c) && i > 0)
{
yield return ' ';
}
yield return c;
i++;
}
}
}
public static ICollection<string> ParseCommaSeparatedStringToCollection(string input, ICollection<string>? texts = null, bool convertToLowerInvariant = true)
{
if (texts == null)
{
texts = new HashSet<string>();
}
else
{
texts.Clear();
}
if (!string.IsNullOrWhiteSpace(input))
{
foreach (string value in input.Split(','))
{
if (string.IsNullOrWhiteSpace(value)) { continue; }
if (convertToLowerInvariant)
{
texts.Add(value.ToLowerInvariant());
}
else
{
texts.Add(value);
}
}
}
return texts;
}
public static ICollection<string> ParseSeparatedStringToCollection(string input, string[] separators, ICollection<string>? texts = null, bool convertToLowerInvariant = true)
{
if (texts == null)
{
texts = new HashSet<string>();
}
else
{
texts.Clear();
}
if (!string.IsNullOrWhiteSpace(input))
{
foreach (string value in input.Split(separators, StringSplitOptions.RemoveEmptyEntries))
{
if (convertToLowerInvariant)
{
texts.Add(value.ToLowerInvariant());
}
else
{
texts.Add(value);
}
}
}
return texts;
}
}
}
@@ -0,0 +1,16 @@
namespace Barotrauma.Extensions
{
public static class StructExtensions
{
public static bool TryGetValue<T>(this T? nullableStruct, out T nonNullable) where T : struct
{
if (nullableStruct.HasValue)
{
nonNullable = nullableStruct.Value;
return true;
}
nonNullable = default(T);
return false;
}
}
}
@@ -0,0 +1,103 @@
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class VectorExtensions
{
/// <summary>
/// Unity's Angle implementation without the conversion to degrees.
/// Returns the angle in radians between two vectors.
/// 0 - Pi.
/// </summary>
public static float Angle(this Vector2 from, Vector2 to)
{
return (float)Math.Acos(MathHelper.Clamp(Vector2.Dot(Vector2.Normalize(from), Vector2.Normalize(to)), -1f, 1f));
}
/// <summary>
/// Creates a forward pointing vector based on the rotation (in radians).
/// </summary>
public static Vector2 Forward(float radians, float length = 1)
{
return new Vector2((float)Math.Cos(radians), (float)Math.Sin(radians)) * length;
}
/// <summary>
/// Creates a backward pointing vector based on the rotation (in radians).
/// </summary>
public static Vector2 Backward(float radians, float length = 1)
{
return -Forward(radians, length);
}
/// <summary>
/// Creates a forward pointing vector based on the rotation (in radians). TODO: remove when the implications have been neutralized
/// </summary>
public static Vector2 ForwardFlipped(float radians, float length = 1)
{
return new Vector2((float)Math.Sin(radians), (float)Math.Cos(radians)) * length;
}
/// <summary>
/// Creates a backward pointing vector based on the rotation (in radians). TODO: remove when the implications have been neutralized
/// </summary>
public static Vector2 BackwardFlipped(float radians, float length = 1)
{
return -ForwardFlipped(radians, length);
}
/// <summary>
/// Creates a normalized perpendicular vector to the right from a forward vector.
/// </summary>
public static Vector2 Right(this Vector2 forward)
{
var normV = Vector2.Normalize(forward);
return new Vector2(normV.Y, -normV.X);
}
/// <summary>
/// Creates a normalized perpendicular vector to the left from a forward vector.
/// </summary>
public static Vector2 Left(this Vector2 forward)
{
return -forward.Right();
}
/// <summary>
/// Transforms a vector relative to the given up vector.
/// </summary>
public static Vector2 TransformVector(this Vector2 v, Vector2 up)
{
up = Vector2.Normalize(up);
return (up * v.Y) + (up.Right() * v.X);
}
/// <summary>
/// Flips the x and y components.
/// </summary>
public static Vector2 Flip(this Vector2 v) => new Vector2(v.Y, v.X);
/// <summary>
/// Returns the sum of the x and y components.
/// </summary>
public static float Combine(this Vector2 v) => v.X + v.Y;
public static Vector2 Clamp(this Vector2 v, Vector2 min, Vector2 max)
{
return Vector2.Clamp(v, min, max);
}
public static bool NearlyEquals(this Vector2 v, Vector2 other)
{
return MathUtils.NearlyEqual(v.X, other.X) && MathUtils.NearlyEqual(v.Y, other.Y);
}
public static Vector2 Pad(this Vector2 v, Vector4 padding)
{
v.X += padding.X + padding.Z;
v.Y += padding.Y + padding.W;
return v;
}
}
}
@@ -0,0 +1,27 @@
#nullable enable
namespace Barotrauma.Networking
{
public abstract class AccountId
{
public abstract string StringRepresentation { get; }
public abstract string EosStringRepresentation { get; }
public static Option<AccountId> Parse(string str)
=> ReflectionUtils.ParseDerived<AccountId, string>(str);
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static bool operator ==(AccountId? a, AccountId? b)
=> a is null
? b is null
: a.Equals(b);
public static bool operator !=(AccountId? a, AccountId? b)
=> !(a == b);
}
}
@@ -0,0 +1,32 @@
#nullable enable
using System;
namespace Barotrauma.Networking;
public sealed class EpicAccountId : AccountId
{
private EpicAccountId(string value)
{
EosStringRepresentation = value.ToLowerInvariant();
}
private const string prefix = "EPIC_";
public override string StringRepresentation => $"{prefix}{EosStringRepresentation.ToUpperInvariant()}";
public override string EosStringRepresentation { get; }
public override bool Equals(object? obj)
=> obj is EpicAccountId otherId
&& otherId.EosStringRepresentation.Equals(EosStringRepresentation, StringComparison.OrdinalIgnoreCase);
public override int GetHashCode()
=> EosStringRepresentation.GetHashCode(StringComparison.OrdinalIgnoreCase);
public new static Option<EpicAccountId> Parse(string str)
{
if (str.IsNullOrWhiteSpace()) { return Option.None; }
if (str.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { str = str[prefix.Length..]; }
if (!str.IsHexString()) { return Option.None; }
return Option.Some(new EpicAccountId(str));
}
}
@@ -0,0 +1,124 @@
#nullable enable
using System;
using System.Globalization;
namespace Barotrauma.Networking
{
public sealed class SteamId : AccountId
{
public readonly UInt64 Value;
public override string StringRepresentation { get; }
public override string EosStringRepresentation => Value.ToString(CultureInfo.InvariantCulture);
/// Based on information found here: https://developer.valvesoftware.com/wiki/SteamID
/// ------------------------------------------------------------------------------------
/// A SteamID is a 64-bit value (16 hexadecimal digits) that's broken up as follows:
///
/// | a | b | c | d |
/// Most significant - | 01 | 1 | 00001 | 0546779D | - Least significant
///
/// a) 8 bits representing the universe the account belongs to.
/// b) 4 bits representing the type of account. Typically 1.
/// c) 20 bits representing the instance of the account. Typically 1.
/// d) 32 bits representing the account number.
///
/// The account number is additionally broken up as follows:
///
/// | e | f |
/// Most significant - | 0000010101000110011101111001110 | 1 | - Least significant
///
/// e) These are the 31 most significant bits of the account number.
/// f) This is the least significant bit of the account number, discriminated under the name Y for some reason.
///
/// Barotrauma supports two textual representations of SteamIDs:
/// 1. STEAM40: Given this name as it represents 40 of the 64 bits in the ID. The account type and instance both
/// have an implied value of 1. The format is "STEAM_{universe}:{Y}:{restOfAccountNumber}".
/// 2. STEAM64: If STEAM40 does not suffice to represent an ID (i.e. the account type or instance were different
/// from 1), we use "STEAM64_{fullId}" where fullId is the 64-bit decimal representation of the full
/// ID.
private const string steam64Prefix = "STEAM64_";
private const string steam40Prefix = "STEAM_";
private const UInt64 usualAccountInstance = 1;
private const UInt64 usualAccountType = 1;
static UInt64 ExtractBits(UInt64 id, int offset, int numberOfBits)
=> (id >> offset) & ((1ul << numberOfBits) - 1ul);
static UInt64 ExtractY(UInt64 id)
=> ExtractBits(id, offset: 0, numberOfBits: 1);
static UInt64 ExtractAccountNumberRemainder(UInt64 id)
=> ExtractBits(id, offset: 1, numberOfBits: 31);
static UInt64 ExtractAccountInstance(UInt64 id)
=> ExtractBits(id, offset: 32, numberOfBits: 20);
static UInt64 ExtractAccountType(UInt64 id)
=> ExtractBits(id, offset: 52, numberOfBits: 4);
static UInt64 ExtractUniverse(UInt64 id)
=> ExtractBits(id, offset: 56, numberOfBits: 8);
public SteamId(UInt64 value)
{
Value = value;
if (ExtractAccountInstance(Value) == usualAccountInstance
&& ExtractAccountType(Value) == usualAccountType)
{
UInt64 y = ExtractY(Value);
UInt64 accountNumberRemainder = ExtractAccountNumberRemainder(Value);
UInt64 universe = ExtractUniverse(Value);
StringRepresentation = $"{steam40Prefix}{universe}:{y}:{accountNumberRemainder}";
}
else
{
StringRepresentation = $"{steam64Prefix}{Value}";
}
}
public override string ToString() => StringRepresentation;
public new static Option<SteamId> Parse(string str)
{
if (str.IsNullOrWhiteSpace()) { return Option<SteamId>.None(); }
if (str.StartsWith(steam64Prefix, StringComparison.InvariantCultureIgnoreCase)) { str = str[steam64Prefix.Length..]; }
if (UInt64.TryParse(str, out UInt64 retVal) && ExtractAccountInstance(retVal) > 0)
{
return Option<SteamId>.Some(new SteamId(retVal));
}
if (!str.StartsWith(steam40Prefix, StringComparison.InvariantCultureIgnoreCase)) { return Option<SteamId>.None(); }
string[] split = str[steam40Prefix.Length..].Split(':');
if (split.Length != 3) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[0], out UInt64 universe)) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[1], out UInt64 y)) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[2], out UInt64 accountNumber)) { return Option<SteamId>.None(); }
return Option<SteamId>.Some(
new SteamId((universe << 56)
| usualAccountType << 52
| usualAccountInstance << 32
| (accountNumber << 1)
| y));
}
public override bool Equals(object? obj)
=> obj switch
{
SteamId otherId => this == otherId,
_ => false
};
public override int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==(SteamId a, SteamId b)
=> a.Value == b.Value;
public static bool operator !=(SteamId a, SteamId b)
=> !(a == b);
}
}
@@ -0,0 +1,26 @@
#nullable enable
namespace Barotrauma.Networking
{
public abstract class Address
{
public abstract string StringRepresentation { get; }
public static Option<Address> Parse(string str)
=> ReflectionUtils.ParseDerived<Address, string>(str);
public abstract bool IsLocalHost { get; }
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static bool operator ==(Address a, Address b)
=> a.Equals(b);
public static bool operator !=(Address a, Address b)
=> !(a == b);
}
}
@@ -0,0 +1,38 @@
#nullable enable
using System;
using System.Linq;
using System.Security.Cryptography;
namespace Barotrauma.Networking;
public sealed class EosP2PAddress : P2PAddress
{
private const string prefix = "EOS_";
public readonly string EosStringRepresentation;
public EosP2PAddress(string value)
{
EosStringRepresentation = value.ToLowerInvariant();
}
public new static Option<EosP2PAddress> Parse(string addressStr)
{
if (addressStr.StartsWith(prefix)) { addressStr = addressStr[prefix.Length..]; }
if (!addressStr.IsHexString()) { return Option.None; }
return Option.Some(new EosP2PAddress(addressStr));
}
public override string StringRepresentation => $"{prefix}{EosStringRepresentation}";
public override bool IsLocalHost => false;
public override bool Equals(object? obj)
=> obj is EosP2PAddress other
&& other.EosStringRepresentation.ToString().Equals(EosStringRepresentation.ToString(), StringComparison.OrdinalIgnoreCase);
public override int GetHashCode()
{
using var md5 = MD5.Create();
return unchecked((int)ToolBoxCore.StringToUInt32Hash(EosStringRepresentation, md5));
}
}
@@ -0,0 +1,79 @@
#nullable enable
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace Barotrauma.Networking
{
public sealed class LidgrenAddress : Address
{
public readonly IPAddress NetAddress;
public override string StringRepresentation
=> NetAddress.ToString();
public override bool IsLocalHost => IPAddress.IsLoopback(NetAddress);
public LidgrenAddress(IPAddress netAddress)
{
if (IPAddress.IsLoopback(netAddress)) { netAddress = IPAddress.Loopback; }
if (netAddress.IsIPv4MappedToIPv6) { netAddress = netAddress.MapToIPv4(); }
NetAddress = netAddress;
}
public new static Option<LidgrenAddress> Parse(string endpointStr)
{
if (endpointStr.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
return Option<LidgrenAddress>.Some(new LidgrenAddress(IPAddress.Loopback));
}
else if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint))
{
return Option<LidgrenAddress>.Some(new LidgrenAddress(netEndpoint!));
}
return Option<LidgrenAddress>.None();
}
public static Option<LidgrenAddress> ParseHostName(string endpointStr)
{
try
{
var resolvedAddresses = Dns.GetHostAddresses(endpointStr);
return resolvedAddresses.Any()
? Option<LidgrenAddress>.Some(new LidgrenAddress(resolvedAddresses.First()))
: Option<LidgrenAddress>.None();
}
catch (SocketException)
{
return Option<LidgrenAddress>.None();
}
catch (ArgumentOutOfRangeException)
{
return Option<LidgrenAddress>.None();
}
}
public override bool Equals(object? obj)
=> obj switch
{
LidgrenAddress otherAddress => this == otherAddress,
_ => false
};
public override int GetHashCode()
=> NetAddress.GetHashCode();
public static bool operator ==(LidgrenAddress a, LidgrenAddress b)
{
var addressA = a.NetAddress.MapToIPv6();
var addressB = b.NetAddress.MapToIPv6();
if (IPAddress.IsLoopback(addressA) && IPAddress.IsLoopback(addressB)) { return true; }
return addressA.Equals(addressB);
}
public static bool operator !=(LidgrenAddress a, LidgrenAddress b)
=> !(a == b);
}
}
@@ -0,0 +1,7 @@
namespace Barotrauma.Networking;
public abstract class P2PAddress : Address
{
public new static Option<P2PAddress> Parse(string str)
=> Address.Parse(str).Bind(addr => addr is P2PAddress p2pAddr ? Option.Some(p2pAddr) : Option.None);
}
@@ -0,0 +1,22 @@
#nullable enable
namespace Barotrauma.Networking
{
public sealed class PipeAddress : Address
{
public override string StringRepresentation => "PIPE";
public override bool IsLocalHost => true;
public override bool Equals(object? obj)
=> obj is PipeAddress;
public override int GetHashCode() => 1;
public static bool operator ==(PipeAddress a, PipeAddress b)
=> true;
public static bool operator !=(PipeAddress a, PipeAddress b)
=> !(a == b);
}
}
@@ -0,0 +1,33 @@
#nullable enable
namespace Barotrauma.Networking
{
public sealed class SteamP2PAddress : P2PAddress
{
public readonly SteamId SteamId;
public override string StringRepresentation => SteamId.StringRepresentation;
public override bool IsLocalHost => false;
public SteamP2PAddress(SteamId steamId)
{
SteamId = steamId;
}
public new static Option<SteamP2PAddress> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PAddress(steamId));
public override bool Equals(object? obj)
=> obj is SteamP2PAddress otherAddress && this == otherAddress;
public override int GetHashCode()
=> SteamId.GetHashCode();
public static bool operator ==(SteamP2PAddress a, SteamP2PAddress b)
=> a.SteamId == b.SteamId;
public static bool operator !=(SteamP2PAddress a, SteamP2PAddress b)
=> !(a == b);
}
}
@@ -0,0 +1,16 @@
#nullable enable
namespace Barotrauma.Networking
{
public sealed class UnknownAddress : Address
{
public override string StringRepresentation => "Hidden";
public override bool IsLocalHost => false;
public override bool Equals(object? obj)
=> ReferenceEquals(obj, this);
public override int GetHashCode() => 1;
}
}
@@ -0,0 +1,67 @@
using System;
namespace Barotrauma.Networking
{
public enum DeliveryMethod : int
{
Unreliable = 0x0,
Reliable = 0x1
}
public enum ConnectionInitialization : int
{
//used by all peer implementations
AuthInfoAndVersion = 0x1,
ContentPackageOrder = 0x2,
Password = 0x3,
Success = 0x0,
//used only by P2P implementations
ConnectionStarted = 0x4
}
[Flags]
public enum PacketHeader : int
{
//used by all peer implementations
None = 0x0,
IsCompressed = 0x1,
IsConnectionInitializationStep = 0x2,
//used only by P2P implementations
IsDisconnectMessage = 0x4,
IsServerMessage = 0x8,
IsHeartbeatMessage = 0x10,
IsDataFragment = 0x20
}
public static class NetworkEnumExtensions
{
public static bool IsCompressed(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsCompressed);
public static bool IsConnectionInitializationStep(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsConnectionInitializationStep);
public static bool IsDisconnectMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsDisconnectMessage);
public static bool IsServerMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsServerMessage);
public static bool IsHeartbeatMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsHeartbeatMessage);
public static bool IsDataFragment(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsDataFragment);
}
public static class NetworkMagicStrings
{
// This separator exists because Lidgren's disconnect messages
// can only readily support strings. We want to send something that
// isn't exactly a string, so we use this as part of its encoding.
public const string LidgrenDisconnectSeparator = "}Separator[";
}
}
@@ -0,0 +1,9 @@
namespace Barotrauma;
public enum FriendStatus
{
Offline,
NotPlaying,
PlayingAnotherGame,
PlayingBarotrauma
}
@@ -0,0 +1,124 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
public class CollectionConcat<T> : ICollection<T>
{
protected readonly IEnumerable<T> enumerableA;
protected readonly IEnumerable<T> enumerableB;
public CollectionConcat(IEnumerable<T> a, IEnumerable<T> b)
{
enumerableA = a; enumerableB = b;
}
public int Count => enumerableA.Count()+enumerableB.Count();
public bool IsReadOnly => true;
public void Add(T item) => throw new InvalidOperationException();
public void Clear() => throw new InvalidOperationException();
public bool Remove(T item) => throw new InvalidOperationException();
public bool Contains(T item) => enumerableA.Contains(item) || enumerableB.Contains(item);
public void CopyTo(T[] array, int arrayIndex)
{
void performCopy(IEnumerable<T> enumerable)
{
if (enumerable is ICollection<T> collection)
{
collection.CopyTo(array, arrayIndex);
arrayIndex += collection.Count;
}
else
{
foreach (var item in enumerable)
{
array[arrayIndex] = item;
arrayIndex++;
}
}
}
performCopy(enumerableA);
performCopy(enumerableB);
}
public IEnumerator<T> GetEnumerator()
{
foreach (T item in enumerableA) { yield return item; }
foreach (T item in enumerableB) { yield return item; }
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class ListConcat<T> : CollectionConcat<T>, IList<T>, IReadOnlyList<T>
{
public ListConcat(IEnumerable<T> a, IEnumerable<T> b) : base(a, b) { }
public int IndexOf(T item)
{
int aCount = 0;
if (enumerableA is IList<T> listA)
{
int index = listA.IndexOf(item);
if (index >= 0) { return index; }
aCount = listA.Count;
}
else
{
foreach (var a in enumerableA)
{
if (object.Equals(item, a)) { return aCount; }
aCount++;
}
}
if (enumerableB is IList<T> listB)
{
int index = listB.IndexOf(item);
if (index >= 0) { return index + aCount; }
}
else
{
foreach (var b in enumerableB)
{
if (object.Equals(item, b)) { return aCount; }
aCount++;
}
}
return -1;
}
public void Insert(int index, T item)
{
throw new InvalidOperationException();
}
public void RemoveAt(int index)
{
throw new InvalidOperationException();
}
public T this[int index]
{
get
{
int aCount = enumerableA.Count();
return index < aCount ? enumerableA.ElementAt(index) : enumerableB.ElementAt(index - aCount);
}
set
{
throw new InvalidOperationException();
}
}
}
}
@@ -0,0 +1,113 @@
#nullable enable
using System;
namespace Barotrauma
{
public abstract class Either<T, U> where T : notnull where U : notnull
{
public static implicit operator Either<T, U>(T t) => new EitherT<T, U>(t);
public static implicit operator Either<T, U>(U u) => new EitherU<T, U>(u);
public static explicit operator T(Either<T, U> e) => e.TryGet(out T t) ? t : throw new InvalidCastException($"Contained object is not of type {typeof(T).Name}");
public static explicit operator U(Either<T, U> e) => e.TryGet(out U u) ? u : throw new InvalidCastException($"Contained object is not of type {typeof(U).Name}");
public abstract bool TryGet(out T t);
public abstract bool TryGet(out U u);
public abstract bool TryCast<V>(out V v);
public abstract override string? ToString();
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public static bool operator ==(Either<T, U>? a, Either<T, U>? b)
=> a is null ? b is null : a.Equals(b);
public static bool operator !=(Either<T, U>? a, Either<T, U>? b)
=> !(a == b);
public V Match<V>(Func<T, V> t, Func<U, V> u)
=> this switch
{
EitherT<T, U> e => t(e.Value),
EitherU<T, U> e => u(e.Value),
_ => throw new Exception("Invalid Either type")
};
}
public sealed class EitherT<T, U> : Either<T, U> where T : notnull where U : notnull
{
public readonly T Value;
public EitherT(T value) { Value = value; }
public override string? ToString()
=> $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(T).NameWithGenerics()})";
public override bool TryGet(out T t) { t = Value; return true; }
public override bool TryGet(out U u) { u = default!; return false; }
public override bool TryCast<V>(out V v)
{
if (Value is V result)
{
v = result;
return true;
}
else
{
v = default!;
return false;
}
}
public override bool Equals(object? obj)
=> obj switch
{
EitherT<T, U> other => Value.Equals(other.Value),
T value => Value.Equals(value),
_ => false
};
public override int GetHashCode() => Value.GetHashCode();
}
public sealed class EitherU<T, U> : Either<T, U> where T : notnull where U : notnull
{
public readonly U Value;
public EitherU(U value) { Value = value; }
public override string? ToString()
=> $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(U).NameWithGenerics()})";
public override bool TryGet(out T t) { t = default!; return false; }
public override bool TryGet(out U u) { u = Value; return true; }
public override bool TryCast<V>(out V v)
{
if (Value is V result)
{
v = result;
return true;
}
else
{
v = default!;
return false;
}
}
public override bool Equals(object? obj)
=> obj switch
{
EitherU<T, U> other => Value.Equals(other.Value),
U value => Value.Equals(value),
_ => false
};
public override int GetHashCode() => Value.GetHashCode();
}
}
@@ -0,0 +1,9 @@
using System;
using System.Reflection;
namespace Barotrauma;
public static class GameVersion
{
public static readonly Version CurrentVersion
= Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(0,0,0,0);
}
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Extensions;
public static class IEnumerableExtensionsCore
{
/// <summary>
/// Returns the maximum element in a given enumerable, or null if there
/// aren't any elements in the input.
/// </summary>
/// <param name="enumerable">Input collection</param>
/// <returns>Maximum element or null</returns>
public static T? MaxOrNull<T>(this IEnumerable<T> enumerable) where T : struct, IComparable<T>
{
T? retVal = null;
foreach (T v in enumerable)
{
if (!retVal.HasValue || v.CompareTo(retVal.Value) > 0) { retVal = v; }
}
return retVal;
}
public static TOut? MaxOrNull<TIn, TOut>(this IEnumerable<TIn> enumerable, Func<TIn, TOut> conversion)
where TOut : struct, IComparable<TOut>
=> enumerable.Select(conversion).MaxOrNull();
public static int FindIndex<T>(this IReadOnlyList<T> list, Predicate<T> predicate)
{
for (int i = 0; i < list.Count; i++)
{
if (predicate(list[i])) { return i; }
}
return -1;
}
/// <summary>
/// Same as FirstOrDefault but will always return null instead of default(T) when no element is found
/// </summary>
public static T? FirstOrNull<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : struct
=> source.FirstOrNone(predicate).TryUnwrap(out T t) ? t : null;
public static T? FirstOrNull<T>(this IEnumerable<T> source) where T : struct
=> source.FirstOrNone().TryUnwrap(out T t) ? t : null;
public static Option<T> FirstOrNone<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : notnull
{
foreach (T t in source)
{
if (predicate(t)) { return Option.Some(t); }
}
return Option.None;
}
public static Option<T> FirstOrNone<T>(this IEnumerable<T> source) where T : notnull
{
using IEnumerator<T> enumerator = source.GetEnumerator();
return enumerator.MoveNext()
? Option.Some(enumerator.Current)
: Option.None;
}
public static IEnumerable<T> NotNone<T>(this IEnumerable<Option<T>> source) where T : notnull
{
foreach (var o in source)
{
if (o.TryUnwrap(out var v)) { yield return v; }
}
}
public static IEnumerable<TSuccess> Successes<TSuccess, TFailure>(
this IEnumerable<Result<TSuccess, TFailure>> source)
where TSuccess : notnull
where TFailure : notnull
=> source
.OfType<Success<TSuccess, TFailure>>()
.Select(s => s.Value);
public static IEnumerable<TFailure> Failures<TSuccess, TFailure>(
this IEnumerable<Result<TSuccess, TFailure>> source)
where TSuccess : notnull
where TFailure : notnull
=> source
.OfType<Failure<TSuccess, TFailure>>()
.Select(f => f.Error);
}
@@ -0,0 +1,169 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Barotrauma
{
// Identifier struct to eliminate case-sensitive comparisons
public readonly struct Identifier : IComparable, IEquatable<Identifier>
{
public readonly static Identifier Empty = default;
private readonly static int emptyHash = "".GetHashCode(StringComparison.OrdinalIgnoreCase);
private readonly string? value;
private readonly Lazy<int>? hashCode;
public string Value => value ?? "";
public int HashCode => hashCode?.Value ?? emptyHash;
public Identifier(string? str)
{
value = str;
hashCode = new Lazy<int>(() => (str ?? "").GetHashCode(StringComparison.OrdinalIgnoreCase));
}
public bool IsEmpty => Value.IsNullOrEmpty();
public Identifier IfEmpty(in Identifier id)
=> IsEmpty ? id : this;
public Identifier Replace(in Identifier subStr, in Identifier newStr)
=> Replace(subStr.Value, newStr.Value);
public Identifier Replace(string subStr, string newStr)
=> Value.Replace(subStr, newStr, StringComparison.OrdinalIgnoreCase).ToIdentifier();
public Identifier Remove(Identifier subStr)
=> Remove(subStr.Value);
public Identifier Remove(string subStr)
=> Value.Remove(subStr, StringComparison.OrdinalIgnoreCase).ToIdentifier();
public override bool Equals(object? obj) =>
obj switch
{
Identifier i => this == i,
string s => this == s,
_ => base.Equals(obj)
};
public bool StartsWith(string str) => Value.StartsWith(str, StringComparison.OrdinalIgnoreCase);
public bool StartsWith(Identifier id) => StartsWith(id.Value);
public bool EndsWith(string str) => Value.EndsWith(str, StringComparison.OrdinalIgnoreCase);
public bool EndsWith(Identifier id) => EndsWith(id.Value);
public Identifier AppendIfMissing(string suffix)
=> EndsWith(suffix) ? this : $"{this}{suffix}".ToIdentifier();
public Identifier RemoveFromEnd(string suffix)
=> Value.RemoveFromEnd(suffix, StringComparison.OrdinalIgnoreCase).ToIdentifier();
public bool Contains(string str) => Value.Contains(str, StringComparison.OrdinalIgnoreCase);
public bool Contains(in Identifier id) => Contains(id.Value);
public override string ToString() => Value;
public override int GetHashCode() => HashCode;
public int CompareTo(object? obj)
{
return string.Compare(Value, obj?.ToString() ?? "", StringComparison.InvariantCultureIgnoreCase);
}
public bool Equals([AllowNull] Identifier other)
{
return this == other;
}
private static bool StringEquality(string? a, string? b)
=> (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
public static bool operator ==(in Identifier a, in Identifier b) =>
StringEquality(a.Value, b.Value);
public static bool operator !=(in Identifier a, in Identifier b) =>
!(a == b);
public static bool operator ==(in Identifier identifier, string? str) =>
StringEquality(identifier.Value, str);
public static bool operator !=(in Identifier identifier, string? str) =>
!(identifier == str);
public static bool operator ==(string? str, in Identifier identifier) =>
identifier == str;
public static bool operator !=(string? str, in Identifier identifier) =>
!(identifier == str);
public static bool operator ==(in Identifier? a, in Identifier? b) =>
StringEquality(a?.Value, b?.Value);
public static bool operator !=(in Identifier? a, in Identifier? b) =>
!(a == b);
public static bool operator ==(in Identifier? a, string? b) =>
StringEquality(a?.Value, b);
public static bool operator !=(in Identifier? a, string? b) =>
!(a == b);
public static bool operator ==(string str, in Identifier? identifier) =>
identifier == str;
public static bool operator !=(string str, in Identifier? identifier) =>
!(identifier == str);
public static implicit operator Identifier(string str)
{
return new Identifier(str);
}
public int IndexOf(char c) => Value.IndexOf(c);
public Identifier this[Range range] => Value[range].ToIdentifier();
public Char this[int i] => Value[i];
}
public static class IdentifierExtensions
{
public static IEnumerable<Identifier> ToIdentifiers(this IEnumerable<string> strings)
{
foreach (string s in strings)
{
if (string.IsNullOrEmpty(s)) { continue; }
yield return new Identifier(s);
}
}
public static Identifier[] ToIdentifiers(this string[] strings)
=> ((IEnumerable<string>)strings).ToIdentifiers().ToArray();
public static Identifier ToIdentifier(this string? s)
{
return new Identifier(s);
}
public static Identifier ToIdentifier<T>(this T t) where T: notnull
{
return t.ToString().ToIdentifier();
}
public static bool Contains(this ISet<Identifier> set, string identifier)
{
return set.Contains(identifier.ToIdentifier());
}
public static bool ContainsKey<T>(this IReadOnlyDictionary<Identifier, T> dictionary, string key)
{
return dictionary.ContainsKey(key.ToIdentifier());
}
}
}
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma;
/// <summary>
/// This type is intended to be used in using statements to automatically
/// clean up resources that are allocated incrementally
/// </summary>
public readonly struct Janitor : IDisposable
{
private readonly List<Action> cleanupActions;
private Janitor(List<Action> cleanupActions)
{
this.cleanupActions = cleanupActions;
}
public static Janitor Start()
=> new Janitor(new List<Action>());
/// <summary>
/// Give the janitor a new action to perform when disposed
/// </summary>
public void AddAction([NotNull]Action action)
{
// Null check to punish misuse early instead of having the Janitor blow up upon disposal.
// Make sure you use nullable contexts so the compiler will stop you instead!
if (action is null)
{
throw new ArgumentException($"Cannot add null as an action for {nameof(Janitor)}");
}
cleanupActions.Add(action);
}
/// <summary>
/// Relieve the janitor of all current duties,
/// i.e. all of the currently enqueued cleanup
/// actions are cleared and will not execute
/// </summary>
public void Dismiss()
=> cleanupActions.Clear();
public void Dispose()
{
cleanupActions.ForEach(a => a());
Dismiss();
}
}
@@ -0,0 +1,74 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Barotrauma;
public static class UnixTime
{
public static readonly DateTime UtcEpoch = new DateTime(year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, kind: DateTimeKind.Utc);
public static Option<DateTime> ParseUtc(string str)
{
if (!ulong.TryParse(str, out var seconds)) { return Option.None; }
return Option.Some(UtcEpoch + TimeSpan.FromSeconds(seconds));
}
}
/// <summary>
/// URL-safe Base64. See https://datatracker.ietf.org/doc/html/rfc4648#section-5
/// </summary>
public static class Base64Url
{
public static bool IsBase64Url(this string str)
=> str.All(c
=> c is
(>= 'A' and <= 'Z')
or (>= 'a' and <= 'z')
or (>= '0' and <= '9')
or '-' or '_');
public static Option<string> DecodeUtf8String(string encodedForm)
{
return DecodeBytes(encodedForm).Select(bytes => Encoding.UTF8.GetString(bytes.AsSpan()));
}
public static Option<ImmutableArray<byte>> DecodeBytes(string encodedForm)
{
if (!encodedForm.IsBase64Url()) { return Option.None; }
string base64Form = encodedForm.Replace("-", "+").Replace("_", "/");
base64Form += new string('=', (4 - (base64Form.Length % 4)) % 4);
return Option.Some(Convert.FromBase64String(base64Form).ToImmutableArray());
}
}
/// <summary>
/// Rudimentary JSON Web Token implementation. See https://en.wikipedia.org/wiki/JSON_Web_Token.
/// This is used by continuance tokens and ID tokens as part of their internal representation.
/// We can use the data encoded in them for some things, such as determining a token's expiry time.
/// </summary>
public readonly record struct JsonWebToken(
string Header,
string Payload,
string Signature)
{
public bool IsValid => Header.IsBase64Url() && Payload.IsBase64Url() && Signature.IsBase64Url();
public override string ToString()
=> $"{Header}.{Payload}.{Signature}";
public string HeaderDecoded => Base64Url.DecodeUtf8String(Header).Fallback("");
public string PayloadDecoded => Base64Url.DecodeUtf8String(Payload).Fallback("");
public static Option<JsonWebToken> Parse(string str)
{
if (str.Split(".") is not { Length: 3 } split) { return Option.None; }
var newToken = new JsonWebToken(
Header: split[0],
Payload: split[1],
Signature: split[2]);
if (!newToken.IsValid) { return Option.None; }
return Option.Some(newToken);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
using System;
using System.Collections.Concurrent;
namespace Barotrauma
{
public sealed class NamedEvent<T> : IDisposable
{
private readonly ConcurrentDictionary<Identifier, Action<T>> events = new ConcurrentDictionary<Identifier, Action<T>>();
public void Register(Identifier identifier, Action<T> action)
{
if (!events.TryAdd(identifier, action))
{
throw new ArgumentException($"Event with the identifier \"{identifier}\" has already been registered.", nameof(identifier));
}
}
public void RegisterOverwriteExisting(Identifier identifier, Action<T> action)
{
events.AddOrUpdate(identifier, action, (k, v) => action);
}
public void Deregister(Identifier identifier)
{
events.TryRemove(identifier, out _);
}
public void TryDeregister(Identifier identifier)
{
if (!HasEvent(identifier)) { return; }
Deregister(identifier);
}
public bool HasEvent(Identifier identifier)
=> events.ContainsKey(identifier);
public void Invoke(T data)
{
foreach (var (_, action) in events)
{
action?.Invoke(data);
}
}
public void Dispose()
{
events.Clear();
}
}
}
@@ -0,0 +1,49 @@
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma;
/// <summary>
/// Discriminated union of three types.
/// Essentially the same thing as Either&lt;T1, T2&gt;, except for three types instead of two types.
/// </summary>
public readonly struct OneOf<T1, T2, T3>
where T1 : notnull
where T2 : notnull
where T3 : notnull
{
private readonly Option<T1> value1;
private readonly Option<T2> value2;
private readonly Option<T3> value3;
private OneOf(Option<T1> value1, Option<T2> value2, Option<T3> value3)
{
this.value1 = value1;
this.value2 = value2;
this.value3 = value3;
}
public static implicit operator OneOf<T1, T2, T3>(T1 value1)
=> new OneOf<T1, T2, T3>(value1: Option.Some(value1), value2: Option.None, value3: Option.None);
public static implicit operator OneOf<T1, T2, T3>(T2 value2)
=> new OneOf<T1, T2, T3>(value1: Option.None, value2: Option.Some(value2), value3: Option.None);
public static implicit operator OneOf<T1, T2, T3>(T3 value3)
=> new OneOf<T1, T2, T3>(value1: Option.None, value2: Option.None, value3: Option.Some(value3));
public bool TryGet([NotNullWhen(returnValue: true)] out T1? t1)
=> value1.TryUnwrap(out t1);
public bool TryGet([NotNullWhen(returnValue: true)] out T2? t2)
=> value2.TryUnwrap(out t2);
public bool TryGet([NotNullWhen(returnValue: true)] out T3? t3)
=> value3.TryUnwrap(out t3);
private static string ObjectToStringWithType<T>(T obj)
=> $"{obj}: {typeof(T).Name}";
public override string ToString()
=> $"OneOf<{typeof(T1).Name}, {typeof(T2).Name}, {typeof(T3).Name}>("
+ value1.Select(ObjectToStringWithType)
.Fallback(value2.Select(ObjectToStringWithType))
.Fallback(value3.Select(ObjectToStringWithType))
.Fallback("None")
+ ")";
}
@@ -0,0 +1,129 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace Barotrauma
{
public readonly struct Option<T> where T : notnull
{
private readonly bool hasValue;
private readonly T? value;
private Option(bool hasValue, T? value)
{
this.hasValue = hasValue;
this.value = value;
}
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)
{
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 async Task<Option<TType>> Bind<TType>(Func<T, Task<Option<TType>>> binder) where TType : notnull
=> TryUnwrap(out T? selfValue) ? await binder(selfValue) : Option.None;
public T Match(Func<T, T> some, Func<T> none)
=> TryUnwrap(out T? selfValue) ? some(selfValue) : none();
public void Match(Action<T> some, Action none)
{
if (TryUnwrap(out T? selfValue))
{
some(selfValue);
return;
}
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 override bool Equals(object? obj)
=> obj switch
{
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()
=> TryUnwrap(out T? selfValue) ? selfValue.GetHashCode() : 0;
public static bool operator ==(Option<T> a, Option<T> b)
=> a.Equals(b);
public static bool operator !=(Option<T> a, Option<T> b)
=> !(a == b);
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 static Option<T> Some<T>(T value) where T : notnull
=> Option<T>.Some(value);
public static UnspecifiedNone None
=> default;
public readonly ref struct UnspecifiedNone
{
}
}
}
@@ -0,0 +1,51 @@
#nullable enable
using System;
namespace Barotrauma
{
/// <summary>
/// An inclusive range, i.e. [Start, End] where Start <= End
/// </summary>
public struct Range<T> where T : notnull, IComparable<T>
{
private T start; private T end;
public T Start
{
get { return start; }
set
{
start = value;
VerifyStartLessThanEnd();
}
}
public T End
{
get { return end; }
set
{
end = value;
VerifyEndGreaterThanStart();
}
}
public readonly bool Contains(in T v)
=> start.CompareTo(v) <= 0 && end.CompareTo(v) >= 0;
private void VerifyStartLessThanEnd()
{
if (start.CompareTo(end) > 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.Start set to a value greater than End ({start} > {end})"); }
}
private void VerifyEndGreaterThanStart()
{
if (end.CompareTo(start) < 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.End set to a value less than Start ({end} < {start})"); }
}
public Range(T start, T end)
{
this.start = start; this.end = end;
VerifyEndGreaterThanStart();
}
}
}
@@ -0,0 +1,181 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
public static class ReflectionUtils
{
private static readonly ConcurrentDictionary<Assembly, ImmutableArray<Type>> CachedNonAbstractTypes = new();
private static readonly ConcurrentDictionary<string, ImmutableArray<Type>> TypeSearchCache = new();
public static T GetValueFromStaticProperty<T>(this PropertyInfo property)
{
if (property.GetMethod is not { IsStatic: true })
{
throw new ArgumentException($"Property {property} is not static");
}
var value = property.GetValue(obj: null);
if (value is not T castValue)
{
throw new ArgumentException($"Property {property} is null or not of type {typeof(T)}");
}
return castValue;
}
public static IEnumerable<Type> GetDerivedNonAbstract<T>()
{
Type t = typeof(T);
string typeName = t.FullName ?? t.Name;
// search quick lookup cache
if (TypeSearchCache.TryGetValue(typeName, out var value))
{
return value;
}
// doesn't exist so let's add it.
Assembly assembly = typeof(T).Assembly;
if (!CachedNonAbstractTypes.ContainsKey(assembly))
{
AddNonAbstractAssemblyTypes(assembly);
}
// build cache from registered assemblies' types.
var list = CachedNonAbstractTypes.Values
.SelectMany(arr => arr.Where(type => type.IsSubclassOf(t)))
.ToImmutableArray();
if (list.Length == 0)
{
return ImmutableArray<Type>.Empty; // No types, don't add to cache
}
if (!TypeSearchCache.TryAdd(typeName, list))
{
DebugConsole.LogError($"ReflectionUtils::AddNonAbstractAssemblyTypes() | Error while adding to quick lookup cache.");
}
return list;
}
/// <summary>
/// Adds an assembly's Non-Abstract Types to the cache for Barotrauma's Type lookup.
/// </summary>
/// <param name="assembly">Assembly to be added</param>
/// <param name="overwrite">Whether or not to overwrite an entry if the assembly already exists within it.</param>
public static void AddNonAbstractAssemblyTypes(Assembly assembly, bool overwrite = false)
{
if (CachedNonAbstractTypes.ContainsKey(assembly))
{
if (!overwrite)
{
DebugConsole.LogError(
$"ReflectionUtils::AddNonAbstractAssemblyTypes() | The assembly [{assembly.GetName()}] already exists in the cache.");
return;
}
CachedNonAbstractTypes.Remove(assembly, out _);
}
try
{
if (!CachedNonAbstractTypes.TryAdd(assembly, assembly.GetSafeTypes().Where(t => !t.IsAbstract).ToImmutableArray()))
{
DebugConsole.LogError($"ReflectionUtils::AddNonAbstractAssemblyTypes() | Unable to add types from Assembly to cache.");
}
else
{
TypeSearchCache.Clear(); // Needs to be rebuilt to include potential new types
}
}
catch (ReflectionTypeLoadException e)
{
DebugConsole.LogError($"ReflectionUtils::AddNonAbstractAssemblyTypes() | RTFException: Unable to load Assembly Types from {assembly.GetName()}.");
}
}
/// <summary>
/// Removes an assembly from the cache for Barotrauma's Type lookup.
/// </summary>
/// <param name="assembly">Assembly to remove.</param>
public static void RemoveAssemblyFromCache(Assembly assembly)
{
CachedNonAbstractTypes.Remove(assembly, out _);
TypeSearchCache.Clear();
}
/// <summary>
/// Clears all cached assembly data and rebuilds types list only to include base Barotrauma types.
/// </summary>
internal static void ResetCache()
{
CachedNonAbstractTypes.Clear();
CachedNonAbstractTypes.TryAdd(typeof(ReflectionUtils).Assembly, typeof(ReflectionUtils).Assembly.GetSafeTypes().ToImmutableArray());
TypeSearchCache.Clear();
}
public static Type? GetType(string nameWithNamespace)
{
if (Type.GetType(nameWithNamespace) is Type t) { return t; }
var entryAssembly = Assembly.GetEntryAssembly();
if (entryAssembly?.GetType(nameWithNamespace) is Type t2) { return t2; }
return null;
}
public static Option<TBase> ParseDerived<TBase, TInput>(TInput input) where TInput : notnull where TBase : notnull
where TBase : notnull
where TInput : notnull
{
static Option<TBase> none() => Option<TBase>.None();
var derivedTypes = GetDerivedNonAbstract<TBase>();
Option<TBase> parseOfType(Type t)
{
//every TBase type is expected to have a method with the following signature:
// public static Option<T> Parse(TInput str)
var parseFunc = t.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static);
if (parseFunc is null) { return none(); }
var parameters = parseFunc.GetParameters();
if (parameters.Length != 1) { return none(); }
var returnType = parseFunc.ReturnType;
if (!returnType.IsConstructedGenericType) { return none(); }
if (returnType.GetGenericTypeDefinition() != typeof(Option<>)) { return none(); }
if (returnType.GenericTypeArguments[0] != t) { return none(); }
//some hacky business to convert from Option<T2> to Option<TBase> when we only know T2 at runtime
static Option<TBase> convert<T2>(Option<T2> option) where T2 : TBase
=> option.Select(v => (TBase)v);
Func<Option<TBase>, Option<TBase>> f = convert;
var genericArgs = f.Method.GetGenericArguments();
genericArgs[^1] = t;
var constructedConverter =
f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs);
return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) })
as Option<TBase>? ?? none();
}
return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome());
}
public static string NameWithGenerics(this Type t)
{
if (!t.IsGenericType) { return t.Name; }
string result = t.Name[..t.Name.IndexOf('`')];
result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>";
return result;
}
}
}
@@ -0,0 +1,120 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma
{
public abstract class Result<TSuccess, TFailure>
where TSuccess: notnull
where TFailure: notnull
{
public abstract bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public static Success<TSuccess, TFailure> Success(TSuccess value)
=> new Success<TSuccess, TFailure>(value);
public static Failure<TSuccess, TFailure> Failure(TFailure error)
=> new Failure<TSuccess, TFailure>(error);
public abstract bool TryUnwrapSuccess([NotNullWhen(returnValue: true)] out TSuccess? value);
public abstract bool TryUnwrapFailure([NotNullWhen(returnValue: true)] out TFailure? value);
public abstract override string ToString();
public static (Func<TSuccess, Result<TSuccess, TFailure>> Success, Func<TFailure, Result<TSuccess, TFailure>> Failure) GetFactoryMethods()
=> (Success, Failure);
public static implicit operator Result<TSuccess, TFailure>(Result.UnspecifiedSuccess<TSuccess> unspecifiedSuccess)
=> Success(unspecifiedSuccess.Value);
public static implicit operator Result<TSuccess, TFailure>(Result.UnspecifiedFailure<TFailure> unspecifiedFailure)
=> Failure(unspecifiedFailure.Value);
public void Match(Action<TSuccess> success, Action<TFailure> failure)
{
if (TryUnwrapSuccess(out var successValue)) { success(successValue); }
if (TryUnwrapFailure(out var failureValue)) { failure(failureValue); }
}
}
public sealed class Success<TSuccess, TFailure> : Result<TSuccess, TFailure>
where TSuccess: notnull
where TFailure: notnull
{
public readonly TSuccess Value;
public override bool IsSuccess => true;
public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out TSuccess value)
{
value = Value;
return true;
}
public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TFailure value)
{
value = default;
return false;
}
public override string ToString()
=> $"Success<{typeof(TSuccess).NameWithGenerics()}, {typeof(TFailure).NameWithGenerics()}>({Value})";
public Success(TSuccess value)
{
Value = value;
}
}
public sealed class Failure<TSuccess, TFailure> : Result<TSuccess, TFailure>
where TSuccess: notnull
where TFailure: notnull
{
public readonly TFailure Error;
public override bool IsSuccess => false;
public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out TSuccess value)
{
value = default;
return false;
}
public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TFailure value)
{
value = Error;
return true;
}
public override string ToString()
=> $"Failure<{typeof(TSuccess).NameWithGenerics()}, {typeof(TFailure).NameWithGenerics()}>({Error})";
public Failure(TFailure error)
{
Error = error;
}
}
public static class Result
{
public readonly ref struct UnspecifiedSuccess<TSuccess>
where TSuccess : notnull
{
internal readonly TSuccess Value;
internal UnspecifiedSuccess(TSuccess value) { Value = value; }
}
public readonly ref struct UnspecifiedFailure<TFailure>
where TFailure : notnull
{
internal readonly TFailure Value;
internal UnspecifiedFailure(TFailure value) { Value = value; }
}
public static UnspecifiedSuccess<TSuccess> Success<TSuccess>(TSuccess value) where TSuccess : notnull
=> new UnspecifiedSuccess<TSuccess>(value);
public static UnspecifiedFailure<TFailure> Failure<TFailure>(TFailure value) where TFailure : notnull
=> new UnspecifiedFailure<TFailure>(value);
}
}
@@ -0,0 +1,40 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace Barotrauma
{
public static class TaskExtensionsCore
{
public static async Task<Option<T>> ToOptionTask<T>(this Task<T?> nullableTask) where T : struct
{
var nullableResult = await nullableTask;
return nullableResult is { } result
? Option.Some(result)
: Option.None;
}
public static bool TryGetResult<T>(this Task task, [NotNullWhen(returnValue: true)]out T? result) where T : notnull
{
if (task is Task<T> { IsCompletedSuccessfully: true, Result: not null } castTask)
{
result = castTask.Result;
return true;
}
#if DEBUG
if (task.Exception != null)
{
var ex = task.Exception.GetInnermost();
throw new InvalidOperationException($"Failed to get result from task: task failed with exception {ex.Message} ({ex.GetType()}) {ex.StackTrace}");
}
if (task is not Task<T>)
{
throw new InvalidOperationException($"Failed to get result from task: expected Task<{typeof(T).NameWithGenerics()}>, got {task.GetType().NameWithGenerics()}");
}
#endif
result = default;
return false;
}
}
}
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Barotrauma
{
public static class TaskPool
{
/// <summary>
/// Empty callback that can be used when we do not care about the completion status of a task.
/// </summary>
public static void IgnoredCallback(Task task) { }
const int MaxTasks = 5000;
private struct TaskAction
{
public string Name;
public Task Task;
public Action<Task, object?> OnCompletion;
public object? UserData;
}
private static readonly List<TaskAction> taskActions = new List<TaskAction>();
public static void ListTasks(Action<string> log)
{
lock (taskActions)
{
log($"Task count: {taskActions.Count}");
for (int i = 0; i < taskActions.Count; i++)
{
log($" -{i}: {taskActions[i].Name}, {taskActions[i].Task.Status}");
}
}
}
public static bool IsTaskRunning(string name)
{
lock (taskActions)
{
return taskActions.Any(t => t.Name == name);
}
}
private static void AddInternal(string name, Task task, Action<Task, object?> onCompletion, object? userdata, bool addIfFound = true)
{
lock (taskActions)
{
if (!addIfFound)
{
if (taskActions.Any(t => t.Name == name)) { return; }
}
if (taskActions.Count >= MaxTasks)
{
throw new Exception(
"Too many tasks in the TaskPool:\n" + string.Join('\n', taskActions.Select(ta => ta.Name))
);
}
taskActions.Add(new TaskAction() { Name = name, Task = task, OnCompletion = onCompletion, UserData = userdata });
}
}
public static Unit Add(string name, Task task, Action<Task>? onCompletion)
{
AddInternal(name, task, (t, _) => { onCompletion?.Invoke(t); }, null);
return Unit.Value;
}
public static Unit AddWithResult<T>(string name, Task<T> task, Action<T>? onCompletion) where T : notnull
{
AddInternal(name, task, (t, _) =>
{
if (t.TryGetResult(out T? result)) { onCompletion?.Invoke(result); }
}, null);
return Unit.Value;
}
public static Unit AddIfNotFound(string name, Task task, Action<Task> onCompletion)
{
AddInternal(name, task, (t, _) => { onCompletion?.Invoke(t); }, null, addIfFound: false);
return Unit.Value;
}
public static void Update()
{
lock (taskActions)
{
for (int i = 0; i < taskActions.Count; i++)
{
if (taskActions[i].Task.IsCompleted)
{
taskActions[i].OnCompletion?.Invoke(taskActions[i].Task, taskActions[i].UserData);
taskActions[i].Task.Dispose();
taskActions.RemoveAt(i);
i--;
}
}
}
}
public static void PrintTaskExceptions(Task task, string msg, Action<string> throwError)
{
throwError(msg);
foreach (Exception e in task.Exception?.InnerExceptions ?? Enumerable.Empty<Exception>())
{
throwError($"{e.Message}\n{e.StackTrace}");
}
}
}
}
@@ -0,0 +1,34 @@
using System.Threading;
namespace Barotrauma.Threading
{
public readonly ref struct ReadLock
{
private readonly ReaderWriterLockSlim rwl;
public ReadLock(ReaderWriterLockSlim rwl)
{
this.rwl = rwl;
rwl.EnterReadLock();
}
public void Dispose()
{
rwl.ExitReadLock();
}
}
public readonly ref struct WriteLock
{
private readonly ReaderWriterLockSlim rwl;
public WriteLock(ReaderWriterLockSlim rwl)
{
this.rwl = rwl;
rwl.EnterWriteLock();
}
public void Dispose()
{
rwl.ExitWriteLock();
}
}
}
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Xna.Framework;
namespace Barotrauma;
public static class ToolBoxCore
{
public static string ByteArrayToHexString(IReadOnlyList<byte> ba)
{
var hex = new StringBuilder(ba.Count * 2);
foreach (byte b in ba)
{
hex.AppendFormat("{0:X2}", b);
}
return hex.ToString();
}
public static byte[] HexStringToByteArray(string str)
{
var byteRepresentation = new byte[str.Length / 2];
for (int i = 0; i < byteRepresentation.Length; i++)
{
byteRepresentation[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
}
return byteRepresentation;
}
public static bool IsHexadecimalDigit(this char c)
=> char.IsDigit(c)
|| c is (>= 'a' and <= 'f') or (>= 'A' and <= 'F');
public static bool IsHexString(this string s)
=> !s.IsNullOrEmpty() && s.All(IsHexadecimalDigit);
public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5)
=> StringToUInt32Hash(id.Value.ToLowerInvariant(), md5);
public static UInt32 StringToUInt32Hash(string str, MD5 md5)
{
//calculate key based on MD5 hash instead of string.GetHashCode
//to ensure consistent results across platforms
byte[] inputBytes = Encoding.UTF8.GetBytes(str);
byte[] hash = md5.ComputeHash(inputBytes);
UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead?
key |= (UInt32)(hash[hash.Length - 3] << 16);
key |= (UInt32)(hash[hash.Length - 2] << 8);
key |= (UInt32)(hash[hash.Length - 1]);
return key;
}
/// <summary>
/// Convert a HSV value into a RGB value.
/// </summary>
/// <param name="hue">Value between 0 and 360</param>
/// <param name="saturation">Value between 0 and 1</param>
/// <param name="value">Value between 0 and 1</param>
/// <see href="https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB">Reference</see>
/// <returns></returns>
public static Color HSVToRGB(float hue, float saturation, float value)
{
float c = value * saturation;
float h = Math.Clamp(hue, 0, 360) / 60f;
float x = c * (1 - Math.Abs(h % 2 - 1));
float r = 0,
g = 0,
b = 0;
if (0 <= h && h <= 1) { r = c; g = x; b = 0; }
else if (1 < h && h <= 2) { r = x; g = c; b = 0; }
else if (2 < h && h <= 3) { r = 0; g = c; b = x; }
else if (3 < h && h <= 4) { r = 0; g = x; b = c; }
else if (4 < h && h <= 5) { r = x; g = 0; b = c; }
else if (5 < h && h <= 6) { r = c; g = 0; b = x; }
float m = value - c;
return new Color(r + m, g + m, b + m);
}
public static Exception GetInnermost(this Exception e)
{
while (e.InnerException != null) { e = e.InnerException; }
return e;
}
}
@@ -0,0 +1,8 @@
namespace Barotrauma;
/// <summary>
/// Unit type, i.e. type with only one possible value.
/// Can be used instead of void to form expressions and
/// fill in generic parameters.
/// </summary>
public enum Unit { Value }
@@ -0,0 +1,8 @@
using System;
namespace Barotrauma;
public sealed class UnreachableCodeException : Exception
{
public UnreachableCodeException() : base(message: "Code that was supposed to be unreachable was executed.") { }
}