Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Either.cs
Normal file
113
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Either.cs
Normal file
@@ -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);
|
||||
}
|
||||
169
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Identifier.cs
Normal file
169
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Identifier.cs
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Janitor.cs
Normal file
49
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Janitor.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
1159
Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs
Normal file
1159
Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs
Normal file
File diff suppressed because it is too large
Load Diff
50
Libraries/BarotraumaLibs/BarotraumaCore/Utils/NamedEvent.cs
Normal file
50
Libraries/BarotraumaLibs/BarotraumaCore/Utils/NamedEvent.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Libraries/BarotraumaLibs/BarotraumaCore/Utils/OneOf.cs
Normal file
49
Libraries/BarotraumaLibs/BarotraumaCore/Utils/OneOf.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
/// <summary>
|
||||
/// Discriminated union of three types.
|
||||
/// Essentially the same thing as Either<T1, T2>, 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")
|
||||
+ ")";
|
||||
}
|
||||
129
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Option/Option.cs
Normal file
129
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Option/Option.cs
Normal file
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Range.cs
Normal file
51
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Range.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
181
Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs
Normal file
181
Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Result.cs
Normal file
120
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Result.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskPool.cs
Normal file
111
Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskPool.cs
Normal file
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Threading.cs
Normal file
34
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Threading.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
96
Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs
Normal file
96
Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
8
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Unit.cs
Normal file
8
Libraries/BarotraumaLibs/BarotraumaCore/Utils/Unit.cs
Normal file
@@ -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.") { }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public enum AchievementUnlockError
|
||||
{
|
||||
Unknown,
|
||||
InvalidUser,
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
InvalidParameters,
|
||||
NotFound
|
||||
}
|
||||
|
||||
public enum IngestStatError
|
||||
{
|
||||
Unknown,
|
||||
InvalidUser,
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
InvalidParameters,
|
||||
NotFound
|
||||
}
|
||||
|
||||
public enum QueryStatsError
|
||||
{
|
||||
Unknown,
|
||||
InvalidUser,
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
InvalidParameters,
|
||||
NotFound
|
||||
}
|
||||
|
||||
public enum QueryAchievementsError
|
||||
{
|
||||
Unknown,
|
||||
InvalidUser,
|
||||
InvalidProductUserID,
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
InvalidParameters,
|
||||
NotFound
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public static class Achievements
|
||||
{
|
||||
private static Implementation? LoadedImplementation => Core.LoadedImplementation;
|
||||
|
||||
public static async Task<Result<uint, AchievementUnlockError>> UnlockAchievements(
|
||||
params Identifier[] achievementIds)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.UnlockAchievements(achievementIds)
|
||||
: Result.Failure(AchievementUnlockError.EosNotInitialized);
|
||||
|
||||
public static async Task<Result<Unit, IngestStatError>> IngestStats(
|
||||
params (AchievementStat Stat, int IngestAmount)[] stats)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.IngestStats(stats)
|
||||
: Result.Failure(IngestStatError.EosNotInitialized);
|
||||
|
||||
public static Task<Result<ImmutableDictionary<AchievementStat, int>, QueryStatsError>> QueryStats(
|
||||
params AchievementStat[] stats)
|
||||
=> QueryStats(stats.ToImmutableArray());
|
||||
|
||||
public static async Task<Result<ImmutableDictionary<AchievementStat, int>, QueryStatsError>> QueryStats(
|
||||
ImmutableArray<AchievementStat> stats)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.QueryStats(stats)
|
||||
: Result.Failure(QueryStatsError.EosNotInitialized);
|
||||
|
||||
public static async Task<Result<ImmutableDictionary<Identifier, double>, QueryAchievementsError>>
|
||||
QueryPlayerAchievements()
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.QueryPlayerAchievements()
|
||||
: Result.Failure(QueryAchievementsError.EosNotInitialized);
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Task<Result<uint, AchievementUnlockError>> UnlockAchievements(
|
||||
params Identifier[] achievementIds);
|
||||
|
||||
public abstract Task<Result<Unit, IngestStatError>> IngestStats(
|
||||
params (AchievementStat Stat, int IngestAmount)[] stats);
|
||||
|
||||
public abstract Task<Result<ImmutableDictionary<AchievementStat, int>, QueryStatsError>> QueryStats(
|
||||
ImmutableArray<AchievementStat> stats);
|
||||
|
||||
public abstract Task<Result<ImmutableDictionary<Identifier, double>, QueryAchievementsError>>
|
||||
QueryPlayerAchievements();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public enum ApplicationCredentials
|
||||
{
|
||||
Client,
|
||||
Server
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("EosInterface.Implementation.Win64")]
|
||||
[assembly: InternalsVisibleTo("EosInterface.Implementation.MacOS")]
|
||||
[assembly: InternalsVisibleTo("EosInterface.Implementation.Linux")]
|
||||
258
Libraries/BarotraumaLibs/EosInterface/Core/Core.cs
Normal file
258
Libraries/BarotraumaLibs/EosInterface/Core/Core.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public static class Core
|
||||
{
|
||||
internal static Implementation? LoadedImplementation { get; private set; } = null;
|
||||
private static AssemblyLoadContext? assemblyLoadContext = null;
|
||||
|
||||
private static bool hasShutDown = false;
|
||||
private static bool failedToInitialize = false;
|
||||
|
||||
private static string GetAssemblyPath(string assemblyName)
|
||||
=> Path.Combine(
|
||||
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
|
||||
$"{assemblyName}.dll");
|
||||
|
||||
private static bool resolvingDependency;
|
||||
|
||||
private static Assembly? ResolveDependency(AssemblyLoadContext context, AssemblyName dependencyName)
|
||||
{
|
||||
if (resolvingDependency)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
resolvingDependency = true;
|
||||
Assembly dependency =
|
||||
context.LoadFromAssemblyPath(
|
||||
GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null")));
|
||||
resolvingDependency = false;
|
||||
return dependency;
|
||||
}
|
||||
|
||||
public enum InitError
|
||||
{
|
||||
PlatformInterfaceNotCreated,
|
||||
AlreadyInitialized,
|
||||
UnknownOsPlatform,
|
||||
ImplementationDllLoadFailed,
|
||||
ImplementationDllHasNoValidClasses,
|
||||
ImplementationFailedToInstantiate,
|
||||
NativeDllLoadFailed,
|
||||
CannotRestartAfterShutdown,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public enum Status
|
||||
{
|
||||
NotInitialized,
|
||||
InitializationError,
|
||||
ShutDown,
|
||||
InitializedButOffline,
|
||||
Online
|
||||
}
|
||||
|
||||
public static bool IsInitialized
|
||||
=> LoadedImplementation != null && LoadedImplementation.IsInitialized();
|
||||
|
||||
public static Status CurrentStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
if (hasShutDown)
|
||||
{
|
||||
return Status.ShutDown;
|
||||
}
|
||||
|
||||
if (failedToInitialize)
|
||||
{
|
||||
return Status.InitializationError;
|
||||
}
|
||||
|
||||
if (LoadedImplementation is { CurrentStatus: var status })
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
return Status.NotInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
public static Result<Unit, InitError> Init(ApplicationCredentials applicationCredentials, bool enableOverlay)
|
||||
{
|
||||
var (success, failure) = Result<Unit, InitError>.GetFactoryMethods();
|
||||
if (LoadedImplementation != null)
|
||||
{
|
||||
return !LoadedImplementation.IsInitialized()
|
||||
? LoadedImplementation.Init(applicationCredentials, enableOverlay)
|
||||
: failure(InitError.AlreadyInitialized);
|
||||
}
|
||||
|
||||
if (hasShutDown)
|
||||
{
|
||||
return failure(InitError.CannotRestartAfterShutdown);
|
||||
}
|
||||
|
||||
string platformSuffix;
|
||||
string nativeDllName;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
platformSuffix = "Win64";
|
||||
nativeDllName = "./EOSSDK-Win64-Shipping.dll";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
platformSuffix = "MacOS";
|
||||
nativeDllName = "./libEOSSDK-Mac-Shipping.dylib";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
platformSuffix = "Linux";
|
||||
nativeDllName = "./libEOSSDK-Linux-Shipping.so";
|
||||
}
|
||||
else
|
||||
{
|
||||
failedToInitialize = true;
|
||||
return failure(InitError.UnknownOsPlatform);
|
||||
}
|
||||
|
||||
if (!NativeLibrary.TryLoad(nativeDllName, out var nativeLib))
|
||||
{
|
||||
failedToInitialize = true;
|
||||
return failure(InitError.NativeDllLoadFailed);
|
||||
}
|
||||
|
||||
NativeLibrary.Free(nativeLib);
|
||||
|
||||
string assemblyName = $"EosInterface.Implementation.{platformSuffix}";
|
||||
|
||||
assemblyLoadContext = new AssemblyLoadContext(assemblyName, isCollectible: true);
|
||||
assemblyLoadContext.Resolving += ResolveDependency;
|
||||
|
||||
Assembly implementationAssembly;
|
||||
try
|
||||
{
|
||||
implementationAssembly = assemblyLoadContext.LoadFromAssemblyPath(GetAssemblyPath(assemblyName));
|
||||
}
|
||||
catch
|
||||
{
|
||||
failedToInitialize = true;
|
||||
return failure(InitError.ImplementationDllLoadFailed);
|
||||
}
|
||||
|
||||
var implementationTypes =
|
||||
implementationAssembly.DefinedTypes
|
||||
.Where(t => t.IsSubclassOf(typeof(Implementation)))
|
||||
.Where(t => t is { IsAbstract: false, IsGenericType: false })
|
||||
.ToArray();
|
||||
if (!implementationTypes.Any())
|
||||
{
|
||||
failedToInitialize = true;
|
||||
return failure(InitError.ImplementationDllHasNoValidClasses);
|
||||
}
|
||||
|
||||
Implementation implementationInstance;
|
||||
try
|
||||
{
|
||||
var implementationInstanceNullable =
|
||||
(Implementation?)Activator.CreateInstance(implementationTypes.First());
|
||||
if (implementationInstanceNullable is null)
|
||||
{
|
||||
failedToInitialize = true;
|
||||
return failure(InitError.ImplementationFailedToInstantiate);
|
||||
}
|
||||
|
||||
implementationInstance = implementationInstanceNullable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
failedToInitialize = true;
|
||||
return failure(InitError.ImplementationFailedToInstantiate);
|
||||
}
|
||||
|
||||
LoadedImplementation = implementationInstance;
|
||||
|
||||
var initResult = implementationInstance.Init(applicationCredentials, enableOverlay);
|
||||
if (initResult.IsFailure)
|
||||
{
|
||||
failedToInitialize = true;
|
||||
}
|
||||
|
||||
return initResult;
|
||||
}
|
||||
|
||||
public enum WillRestartThroughLauncher
|
||||
{
|
||||
No,
|
||||
Yes
|
||||
}
|
||||
|
||||
public enum CheckForLauncherAndRestartError
|
||||
{
|
||||
EosNotInitialized,
|
||||
UnexpectedError,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static Result<WillRestartThroughLauncher, CheckForLauncherAndRestartError> CheckForLauncherAndRestart()
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? LoadedImplementation.CheckForLauncherAndRestart()
|
||||
: Result.Failure(CheckForLauncherAndRestartError.EosNotInitialized);
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
if (LoadedImplementation.IsInitialized())
|
||||
{
|
||||
LoadedImplementation.Update();
|
||||
}
|
||||
}
|
||||
|
||||
public static void CleanupAndQuit()
|
||||
{
|
||||
var loadedImplementation = LoadedImplementation;
|
||||
if (!loadedImplementation.IsInitialized())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TaskPool.Add(
|
||||
"CleanupAndQuit",
|
||||
loadedImplementation.CloseAllOwnedSessions(),
|
||||
_ => QuitNow());
|
||||
}
|
||||
|
||||
private static void QuitNow()
|
||||
{
|
||||
hasShutDown = CurrentStatus != Status.NotInitialized;
|
||||
LoadedImplementation?.Quit();
|
||||
LoadedImplementation = null;
|
||||
assemblyLoadContext?.Unload();
|
||||
assemblyLoadContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Core.Status CurrentStatus { get; }
|
||||
public abstract string NativeLibraryName { get; }
|
||||
|
||||
public abstract Result<Unit, Core.InitError> Init(ApplicationCredentials applicationCredentials,
|
||||
bool enableOverlay);
|
||||
|
||||
public abstract Result<Core.WillRestartThroughLauncher, Core.CheckForLauncherAndRestartError>
|
||||
CheckForLauncherAndRestart();
|
||||
|
||||
public abstract void Update();
|
||||
public abstract void Quit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static class EosStatusExtensions
|
||||
{
|
||||
public static bool IsInitialized(this EosInterface.Core.Status status)
|
||||
=> status is EosInterface.Core.Status.InitializedButOffline or EosInterface.Core.Status.Online;
|
||||
|
||||
internal static bool IsInitialized(
|
||||
[NotNullWhen(returnValue: true)] this EosInterface.Implementation? implementation)
|
||||
=> implementation is { CurrentStatus: var status } && status.IsInitialized();
|
||||
}
|
||||
26
Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj
Normal file
26
Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<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' ">
|
||||
<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="..\BarotraumaCore\BarotraumaCore.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
13
Libraries/BarotraumaLibs/EosInterface/Friends/EgsFriend.cs
Normal file
13
Libraries/BarotraumaLibs/EosInterface/Friends/EgsFriend.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public readonly record struct EgsFriend(
|
||||
string DisplayName,
|
||||
EpicAccountId EpicAccountId,
|
||||
FriendStatus Status,
|
||||
string ConnectCommand,
|
||||
string ServerName);
|
||||
}
|
||||
62
Libraries/BarotraumaLibs/EosInterface/Friends/Friends.cs
Normal file
62
Libraries/BarotraumaLibs/EosInterface/Friends/Friends.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public static class Friends
|
||||
{
|
||||
private static Implementation? LoadedImplementation => Core.LoadedImplementation;
|
||||
|
||||
public enum GetFriendsError
|
||||
{
|
||||
EosNotInitialized,
|
||||
|
||||
EgsFriendsQueryTimedOut,
|
||||
EgsFriendsQueryFailed,
|
||||
|
||||
UserInfoQueryTimedOut,
|
||||
UserInfoQueryFailed,
|
||||
CopyUserInfoFailed,
|
||||
DisplayNameIsEmpty,
|
||||
|
||||
EgsPresenceQueryTimedOut,
|
||||
EgsPresenceQueryFailed,
|
||||
CopyPresenceFailed,
|
||||
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static async Task<Result<EgsFriend, GetFriendsError>> GetFriend(
|
||||
EpicAccountId selfEaid,
|
||||
EpicAccountId friendEaid)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.GetFriend(selfEaid, friendEaid)
|
||||
: Result.Failure(GetFriendsError.EosNotInitialized);
|
||||
|
||||
public static async Task<Result<ImmutableArray<EgsFriend>, GetFriendsError>> GetFriends(
|
||||
EpicAccountId epicAccountId)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.GetFriends(epicAccountId)
|
||||
: Result.Failure(GetFriendsError.EosNotInitialized);
|
||||
|
||||
public static async Task<Result<EgsFriend, GetFriendsError>> GetSelfUserInfo(EpicAccountId epicAccountId)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.GetSelfUserInfo(epicAccountId)
|
||||
: Result.Failure(GetFriendsError.EosNotInitialized);
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Task<Result<EgsFriend, Friends.GetFriendsError>> GetFriend(
|
||||
EpicAccountId selfEaid,
|
||||
EpicAccountId friendEaid);
|
||||
|
||||
public abstract Task<Result<ImmutableArray<EgsFriend>, Friends.GetFriendsError>> GetFriends(
|
||||
EpicAccountId epicAccountId);
|
||||
|
||||
public abstract Task<Result<EgsFriend, Friends.GetFriendsError>> GetSelfUserInfo(EpicAccountId epicAccountId);
|
||||
}
|
||||
}
|
||||
105
Libraries/BarotraumaLibs/EosInterface/Friends/Presence.cs
Normal file
105
Libraries/BarotraumaLibs/EosInterface/Friends/Presence.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public static class Presence
|
||||
{
|
||||
public readonly record struct JoinGameInfo(
|
||||
EpicAccountId RecipientId,
|
||||
string JoinCommand);
|
||||
|
||||
public readonly record struct AcceptInviteInfo(
|
||||
EpicAccountId RecipientId,
|
||||
string JoinCommand);
|
||||
|
||||
public readonly record struct ReceiveInviteInfo(
|
||||
EpicAccountId RecipientId,
|
||||
EpicAccountId SenderId,
|
||||
string JoinCommand);
|
||||
|
||||
private static readonly NamedEvent<JoinGameInfo> dummyJoinGameEvent =
|
||||
new NamedEvent<JoinGameInfo>();
|
||||
|
||||
private static readonly NamedEvent<AcceptInviteInfo> dummyAcceptInviteEvent =
|
||||
new NamedEvent<AcceptInviteInfo>();
|
||||
|
||||
private static readonly NamedEvent<ReceiveInviteInfo> dummyReceiveInviteEvent =
|
||||
new NamedEvent<ReceiveInviteInfo>();
|
||||
|
||||
public static NamedEvent<JoinGameInfo> OnJoinGame
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? Core.LoadedImplementation.OnJoinGame
|
||||
: dummyJoinGameEvent;
|
||||
|
||||
public static NamedEvent<AcceptInviteInfo> OnInviteAccepted
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? Core.LoadedImplementation.OnInviteAccepted
|
||||
: dummyAcceptInviteEvent;
|
||||
|
||||
public static NamedEvent<ReceiveInviteInfo> OnInviteReceived
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? Core.LoadedImplementation.OnInviteReceived
|
||||
: dummyReceiveInviteEvent;
|
||||
|
||||
public enum SetJoinCommandError
|
||||
{
|
||||
EosNotInitialized,
|
||||
FailedToSetCustomInvite,
|
||||
FailedToCreatePresenceModification,
|
||||
JoinCommandTooLong,
|
||||
ServerNameTooLong,
|
||||
FailedToSetJoinInfo,
|
||||
FailedToGetPuid,
|
||||
DescTooLong,
|
||||
FailedToSetRichText,
|
||||
FailedToSetRecords,
|
||||
SetPresenceTimedOut,
|
||||
FailedToSetPresence
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, SetJoinCommandError>> SetJoinCommand(
|
||||
EpicAccountId epicAccountId, string desc, string serverName, string joinCommand)
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? await Core.LoadedImplementation.SetJoinCommand(epicAccountId, desc, serverName, joinCommand)
|
||||
: Result.Failure(SetJoinCommandError.EosNotInitialized);
|
||||
|
||||
public enum SendInviteError
|
||||
{
|
||||
EosNotInitialized,
|
||||
FailedToGetSelfPuid,
|
||||
FailedToGetRemotePuid,
|
||||
TimedOut,
|
||||
InternalError
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, SendInviteError>> SendInvite(
|
||||
EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId)
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? await Core.LoadedImplementation.SendInvite(selfEpicAccountId, remoteEpicAccountId)
|
||||
: Result.Failure(SendInviteError.EosNotInitialized);
|
||||
|
||||
public static void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId)
|
||||
{
|
||||
if (Core.LoadedImplementation.IsInitialized())
|
||||
{
|
||||
Core.LoadedImplementation.DeclineInvite(selfEpicAccountId, senderEpicAccountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract NamedEvent<Presence.JoinGameInfo> OnJoinGame { get; }
|
||||
public abstract NamedEvent<Presence.AcceptInviteInfo> OnInviteAccepted { get; }
|
||||
public abstract NamedEvent<Presence.ReceiveInviteInfo> OnInviteReceived { get; }
|
||||
|
||||
public abstract Task<Result<Unit, Presence.SetJoinCommandError>> SetJoinCommand(
|
||||
EpicAccountId epicAccountId, string desc, string joinCommand, string s);
|
||||
public abstract Task<Result<Unit, Presence.SendInviteError>> SendInvite(
|
||||
EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId);
|
||||
public abstract void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public sealed class EgsAuthContinuanceToken
|
||||
{
|
||||
// Got this number by checking a decoded continuance token, may be subject to change
|
||||
public static readonly TimeSpan Duration = TimeSpan.FromMinutes(30);
|
||||
|
||||
public readonly DateTime ExpiryTime;
|
||||
public bool IsValid => value != IntPtr.Zero && DateTime.Now < ExpiryTime;
|
||||
|
||||
private IntPtr value;
|
||||
|
||||
public EgsAuthContinuanceToken(IntPtr value, DateTime expiryTime)
|
||||
{
|
||||
this.value = value;
|
||||
ExpiryTime = expiryTime;
|
||||
}
|
||||
|
||||
public IntPtr Spend()
|
||||
{
|
||||
var retVal = IsValid ? value : IntPtr.Zero;
|
||||
value = IntPtr.Zero;
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"{(IsValid ? "Valid" : "Invalid")} EGS ContinuanceToken"
|
||||
+ (IsValid ? $" (expires on {ExpiryTime})" : "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public enum GetEgsSelfIdTokenError
|
||||
{
|
||||
EosNotInitialized,
|
||||
NotLoggedIn,
|
||||
InvalidToken,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public enum VerifyEgsIdTokenResult
|
||||
{
|
||||
Verified,
|
||||
Failed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Epic Games ID Token, used to authenticate an Epic Account ID.
|
||||
/// This is distinct from <see cref="EosIdToken" />, which represents an EOS ID Token.
|
||||
/// </summary>
|
||||
public abstract class EgsIdToken
|
||||
{
|
||||
public abstract EpicAccountId AccountId { get; }
|
||||
|
||||
public static Option<EgsIdToken> Parse(string str)
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? Core.LoadedImplementation.ParseEgsIdToken(str)
|
||||
: Option.None;
|
||||
|
||||
public static Result<EgsIdToken, GetEgsSelfIdTokenError> FromEpicAccountId(EpicAccountId accountId)
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? Core.LoadedImplementation.GetEgsIdTokenForEpicAccountId(accountId)
|
||||
: Result.Failure(GetEgsSelfIdTokenError.EosNotInitialized);
|
||||
|
||||
public abstract override string ToString();
|
||||
|
||||
public abstract Task<VerifyEgsIdTokenResult> Verify(AccountId accountId);
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Option<EgsIdToken> ParseEgsIdToken(string str);
|
||||
|
||||
public abstract Result<EgsIdToken, GetEgsSelfIdTokenError> GetEgsIdTokenForEpicAccountId(
|
||||
EpicAccountId accountId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public sealed class EosConnectContinuanceToken
|
||||
{
|
||||
// Got this number by checking a decoded continuance token, may be subject to change
|
||||
public static readonly TimeSpan Duration = TimeSpan.FromMinutes(30);
|
||||
|
||||
public readonly AccountId ExternalAccountId;
|
||||
public readonly DateTime ExpiryTime;
|
||||
public bool IsValid => value != IntPtr.Zero && DateTime.Now < ExpiryTime;
|
||||
|
||||
private IntPtr value;
|
||||
|
||||
public EosConnectContinuanceToken(IntPtr value, AccountId externalAccountId, DateTime expiryTime)
|
||||
{
|
||||
this.value = value;
|
||||
this.ExternalAccountId = externalAccountId;
|
||||
ExpiryTime = expiryTime;
|
||||
}
|
||||
|
||||
public IntPtr Spend()
|
||||
{
|
||||
var retVal = IsValid ? value : IntPtr.Zero;
|
||||
value = IntPtr.Zero;
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"{(IsValid ? "Valid" : "Invalid")} {ExternalAccountId} ContinuanceToken"
|
||||
+ (IsValid ? $" (expires on {ExpiryTime})" : "");
|
||||
}
|
||||
}
|
||||
114
Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosIdToken.cs
Normal file
114
Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosIdToken.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public enum GetEosSelfIdTokenError
|
||||
{
|
||||
EosNotInitialized,
|
||||
NotLoggedIn,
|
||||
InvalidToken,
|
||||
CouldNotParseJwt,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public enum VerifyEosIdTokenError
|
||||
{
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
ProductIdDidNotMatch,
|
||||
CouldNotParseExternalAccountId,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an EOS ID Token, used to authenticate a Product User ID.
|
||||
/// This is distinct from <see cref="EgsIdToken" />, which represents an Epic Games ID Token.
|
||||
/// </summary>
|
||||
public readonly record struct EosIdToken(
|
||||
ProductUserId ProductUserId,
|
||||
JsonWebToken JsonWebToken)
|
||||
{
|
||||
public async Task<Result<AccountId, VerifyEosIdTokenError>> Verify()
|
||||
=> Core.LoadedImplementation is { } loadedImplementation
|
||||
? await loadedImplementation.VerifyEosIdToken(this)
|
||||
: Result.Failure(VerifyEosIdTokenError.EosNotInitialized);
|
||||
|
||||
public static Option<EosIdToken> Parse(string str)
|
||||
{
|
||||
var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(str));
|
||||
JsonDocument? jsonDoc = null;
|
||||
try
|
||||
{
|
||||
if (!JsonDocument.TryParseValue(ref jsonReader, out jsonDoc))
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
if (!jsonDoc.RootElement.TryGetProperty(nameof(ProductUserId), out var puidElement))
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
if (!jsonDoc.RootElement.TryGetProperty(nameof(JsonWebToken), out var jwtElement))
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
var puidStr = puidElement.ToString();
|
||||
if (!puidStr.IsHexString())
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
var puid = new ProductUserId(puidStr);
|
||||
|
||||
var jwtStr = jwtElement.ToString();
|
||||
if (!JsonWebToken.Parse(jwtStr).TryUnwrap(out var jsonWebToken))
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
var newToken = new EosIdToken(puid, jsonWebToken);
|
||||
|
||||
return Option.Some(newToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
finally
|
||||
{
|
||||
jsonDoc?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static Result<EosIdToken, GetEosSelfIdTokenError> FromProductUserId(ProductUserId puid)
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? Core.LoadedImplementation.GetEosIdTokenForProductUserId(puid)
|
||||
: Result.Failure(GetEosSelfIdTokenError.EosNotInitialized);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
using var memoryStream = new System.IO.MemoryStream();
|
||||
using var jsonWriter = new Utf8JsonWriter(memoryStream);
|
||||
jsonWriter.WriteStartObject();
|
||||
jsonWriter.WriteString(nameof(ProductUserId), ProductUserId.Value);
|
||||
jsonWriter.WriteString(nameof(JsonWebToken), JsonWebToken.ToString());
|
||||
jsonWriter.WriteEndObject();
|
||||
jsonWriter.Flush();
|
||||
memoryStream.Flush();
|
||||
return Encoding.UTF8.GetString(memoryStream.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Task<Result<AccountId, VerifyEosIdTokenError>> VerifyEosIdToken(EosIdToken token);
|
||||
public abstract Result<EosIdToken, GetEosSelfIdTokenError> GetEosIdTokenForProductUserId(ProductUserId puid);
|
||||
}
|
||||
}
|
||||
64
Libraries/BarotraumaLibs/EosInterface/IdAndAuth/IdQueries.cs
Normal file
64
Libraries/BarotraumaLibs/EosInterface/IdAndAuth/IdQueries.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public static class IdQueries
|
||||
{
|
||||
private static Implementation? LoadedImplementation => Core.LoadedImplementation;
|
||||
|
||||
public static bool IsLoggedIntoEosConnect
|
||||
=> GetLoggedInPuids() is { Length: > 0 };
|
||||
|
||||
/// <summary>
|
||||
/// Gets all of the <see cref="Barotrauma.EosInterface.ProductUserId" />s the player has logged in with.
|
||||
/// For most players, this is expected to return one ID.
|
||||
/// It may return two IDs if a Steam user has chosen to link their account to an Epic Account.
|
||||
/// </summary>
|
||||
public static ImmutableArray<ProductUserId> GetLoggedInPuids()
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? LoadedImplementation.GetLoggedInPuids()
|
||||
: ImmutableArray<ProductUserId>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all of the <see cref="Barotrauma.Networking.EpicAccountId" />s the player has logged in with.
|
||||
/// This is expected to return at most one ID.
|
||||
/// <br /><br />
|
||||
/// This should return exactly one ID for any Epic Games Store player.
|
||||
/// <br />
|
||||
/// Steam players may choose to link their account to only one Epic Games account.
|
||||
/// </summary>
|
||||
public static ImmutableArray<EpicAccountId> GetLoggedInEpicIds()
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? LoadedImplementation.GetLoggedInEpicIds()
|
||||
: ImmutableArray<EpicAccountId>.Empty;
|
||||
|
||||
public enum GetSelfExternalIdError
|
||||
{
|
||||
EosNotInitialized,
|
||||
Inaccessible,
|
||||
Timeout,
|
||||
InvalidUser,
|
||||
ParseError,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static async Task<Result<ImmutableArray<AccountId>, GetSelfExternalIdError>> GetSelfExternalAccountIds(
|
||||
ProductUserId puid)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.GetSelfExternalAccountIds(puid)
|
||||
: Result.Failure(GetSelfExternalIdError.EosNotInitialized);
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract ImmutableArray<ProductUserId> GetLoggedInPuids();
|
||||
public abstract ImmutableArray<EpicAccountId> GetLoggedInEpicIds();
|
||||
|
||||
public abstract Task<Result<ImmutableArray<AccountId>, IdQueries.GetSelfExternalIdError>>
|
||||
GetSelfExternalAccountIds(ProductUserId puid);
|
||||
}
|
||||
}
|
||||
206
Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Login.cs
Normal file
206
Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Login.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public static class Login
|
||||
{
|
||||
private static Implementation? LoadedImplementation => Core.LoadedImplementation;
|
||||
|
||||
public enum CreateProductAccountError
|
||||
{
|
||||
EosNotInitialized,
|
||||
InvalidContinuanceToken,
|
||||
Timeout,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static async Task<Result<ProductUserId, CreateProductAccountError>> CreateProductAccount(
|
||||
EosConnectContinuanceToken eosContinuanceToken)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.CreateProductAccount(eosContinuanceToken)
|
||||
: Result.Failure(CreateProductAccountError.EosNotInitialized);
|
||||
|
||||
public enum LinkExternalAccountError
|
||||
{
|
||||
EosNotInitialized,
|
||||
InvalidContinuanceToken,
|
||||
Timeout,
|
||||
CannotLink,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, LinkExternalAccountError>> LinkExternalAccount(ProductUserId puid,
|
||||
EosConnectContinuanceToken eosContinuanceToken)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.LinkExternalAccount(puid, eosContinuanceToken)
|
||||
: Result.Failure(LinkExternalAccountError.EosNotInitialized);
|
||||
|
||||
public enum UnlinkExternalAccountError
|
||||
{
|
||||
EosNotInitialized,
|
||||
FailedToGetExternalAccounts,
|
||||
NotLoggedInToGivenAccount,
|
||||
Timeout,
|
||||
CannotLink,
|
||||
InvalidUser,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, UnlinkExternalAccountError>> UnlinkExternalAccount(ProductUserId puid)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.UnlinkExternalAccount(puid)
|
||||
: Result.Failure(UnlinkExternalAccountError.EosNotInitialized);
|
||||
|
||||
public enum LoginError
|
||||
{
|
||||
EosNotInitialized,
|
||||
|
||||
SteamNotLoggedIn,
|
||||
FailedToGetSteamSessionTicket,
|
||||
|
||||
EgsLoginTimeout,
|
||||
EgsAccountNotFound,
|
||||
FailedToParseEgsId,
|
||||
FailedToGetEgsIdToken,
|
||||
AuthExchangeCodeNotFound,
|
||||
AuthRequiresOpeningBrowser,
|
||||
|
||||
Timeout,
|
||||
InvalidUser,
|
||||
EgsAccessDenied,
|
||||
EosAccessDenied,
|
||||
UnexpectedContinuanceToken,
|
||||
|
||||
UnhandledFailureCondition
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum LoginEpicFlags
|
||||
{
|
||||
None = 0x0,
|
||||
FailWithoutOpeningBrowser = 0x1
|
||||
}
|
||||
|
||||
public static async
|
||||
Task<Result<OneOf<ProductUserId, EosConnectContinuanceToken, EgsAuthContinuanceToken>, LoginError>>
|
||||
LoginEpicWithLinkedSteamAccount(LoginEpicFlags flags)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.LoginEpicWithLinkedSteamAccount(flags)
|
||||
: Result.Failure(LoginError.EosNotInitialized);
|
||||
|
||||
public static async Task<Result<Either<ProductUserId, EosConnectContinuanceToken>, LoginError>>
|
||||
LoginEpicExchangeCode(string exchangeCode)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.LoginEpicExchangeCode(exchangeCode)
|
||||
: Result.Failure(LoginError.EosNotInitialized);
|
||||
|
||||
public static async Task<Result<Either<ProductUserId, EosConnectContinuanceToken>, LoginError>>
|
||||
LoginEpicIdToken(EgsIdToken token)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.LoginEpicIdToken(token)
|
||||
: Result.Failure(LoginError.EosNotInitialized);
|
||||
|
||||
public static async Task<Result<Either<ProductUserId, EosConnectContinuanceToken>, LoginError>> LoginSteam()
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.LoginSteam()
|
||||
: Result.Failure(LoginError.EosNotInitialized);
|
||||
|
||||
public enum LinkExternalAccountToEpicAccountError
|
||||
{
|
||||
EosNotInitialized,
|
||||
|
||||
TimedOut,
|
||||
FailedToParseEgsAccountId,
|
||||
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static async Task<Result<EpicAccountId, LinkExternalAccountToEpicAccountError>>
|
||||
LinkExternalAccountToEpicAccount(EgsAuthContinuanceToken continuanceToken)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.LinkExternalAccountToEpicAccount(continuanceToken)
|
||||
: Result.Failure(LinkExternalAccountToEpicAccountError.EosNotInitialized);
|
||||
|
||||
public enum LogoutEpicAccountError
|
||||
{
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, LogoutEpicAccountError>> LogoutEpicAccount(EpicAccountId egsId)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.LogoutEpicAccount(egsId)
|
||||
: Result.Failure(LogoutEpicAccountError.EosNotInitialized);
|
||||
|
||||
/// <summary>
|
||||
/// This is essentially a function for logging out, except EOS has no EOS_Connect_Logout function
|
||||
/// so instead we have this to fake it. Once you use this, no methods should return this PUID
|
||||
/// until you log into it again.
|
||||
/// </summary>
|
||||
public static void MarkAsInaccessible(ProductUserId puid)
|
||||
{
|
||||
if (LoadedImplementation.IsInitialized())
|
||||
{
|
||||
LoadedImplementation.MarkAsInaccessible(puid);
|
||||
}
|
||||
}
|
||||
|
||||
public static Option<string> ParseEgsExchangeCode(IReadOnlyList<string> args)
|
||||
{
|
||||
if (args.Contains("-AUTH_TYPE=exchangecode", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return args.FirstOrNone(arg =>
|
||||
arg.StartsWith("-AUTH_PASSWORD=", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(arg => arg["-AUTH_PASSWORD=".Length..]);
|
||||
}
|
||||
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
public static void TestEosSessionTimeoutRecovery(ProductUserId puid)
|
||||
{
|
||||
if (LoadedImplementation.IsInitialized())
|
||||
{
|
||||
LoadedImplementation.TestEosSessionTimeoutRecovery(puid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Task<Result<ProductUserId, Login.CreateProductAccountError>> CreateProductAccount(
|
||||
EosConnectContinuanceToken eosContinuanceToken);
|
||||
|
||||
public abstract Task<Result<Unit, Login.LinkExternalAccountError>> LinkExternalAccount(ProductUserId puid,
|
||||
EosConnectContinuanceToken eosContinuanceToken);
|
||||
|
||||
public abstract Task<Result<Unit, Login.UnlinkExternalAccountError>> UnlinkExternalAccount(ProductUserId puid);
|
||||
|
||||
public abstract Task<Result<Either<ProductUserId, EosConnectContinuanceToken>, Login.LoginError>>
|
||||
LoginEpicExchangeCode(string exchangeCode);
|
||||
|
||||
public abstract
|
||||
Task<Result<OneOf<ProductUserId, EosConnectContinuanceToken, EgsAuthContinuanceToken>, Login.LoginError>>
|
||||
LoginEpicWithLinkedSteamAccount(Login.LoginEpicFlags flags);
|
||||
|
||||
public abstract Task<Result<Either<ProductUserId, EosConnectContinuanceToken>, Login.LoginError>>
|
||||
LoginEpicIdToken(EgsIdToken token);
|
||||
|
||||
public abstract Task<Result<Either<ProductUserId, EosConnectContinuanceToken>, Login.LoginError>> LoginSteam();
|
||||
|
||||
public abstract Task<Result<EpicAccountId, Login.LinkExternalAccountToEpicAccountError>>
|
||||
LinkExternalAccountToEpicAccount(EgsAuthContinuanceToken continuanceToken);
|
||||
|
||||
public abstract Task<Result<Unit, Login.LogoutEpicAccountError>> LogoutEpicAccount(EpicAccountId egsId);
|
||||
public abstract void MarkAsInaccessible(ProductUserId puid);
|
||||
public abstract void TestEosSessionTimeoutRecovery(ProductUserId puid);
|
||||
}
|
||||
}
|
||||
32
Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Ownership.cs
Normal file
32
Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Ownership.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public static class Ownership
|
||||
{
|
||||
private static Implementation? LoadedImplementation => Core.LoadedImplementation;
|
||||
|
||||
public static async Task<Option<Ownership.Token>> GetGameOwnershipToken(EpicAccountId selfEpicAccountId)
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.GetGameOwnershipToken(selfEpicAccountId)
|
||||
: Option.None;
|
||||
|
||||
public readonly record struct Token(JsonWebToken Jwt)
|
||||
{
|
||||
public async Task<Option<EpicAccountId>> Verify()
|
||||
=> LoadedImplementation.IsInitialized()
|
||||
? await LoadedImplementation.VerifyGameOwnershipToken(this)
|
||||
: Option.None;
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Task<Option<Ownership.Token>> GetGameOwnershipToken(EpicAccountId selfEpicAccountId);
|
||||
|
||||
public abstract Task<Option<EpicAccountId>> VerifyGameOwnershipToken(Ownership.Token token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// A Product User ID is an EOS-specific ID that's linked to the SteamID or the Epic Account ID of a player.
|
||||
/// It is used to identify players in many of EOS' interfaces, most notably the P2P networking interface.
|
||||
/// <br /><br />
|
||||
/// A Product User ID used by Barotrauma is only valid for Barotrauma; other games that use EOS get their
|
||||
/// own separate set of Product User IDs.
|
||||
/// </summary>
|
||||
public readonly record struct ProductUserId(string Value);
|
||||
}
|
||||
95
Libraries/BarotraumaLibs/EosInterface/P2P/P2PSocket.cs
Normal file
95
Libraries/BarotraumaLibs/EosInterface/P2P/P2PSocket.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public abstract class P2PSocket : IDisposable
|
||||
{
|
||||
public enum CreationError
|
||||
{
|
||||
EosNotInitialized,
|
||||
UserNotLoggedIn,
|
||||
RequestBindFailed,
|
||||
CloseBindFailed
|
||||
}
|
||||
|
||||
public readonly record struct IncomingConnectionRequest(
|
||||
P2PSocket Socket,
|
||||
ProductUserId RemoteUserId)
|
||||
{
|
||||
public void Accept()
|
||||
=> Socket.AcceptConnectionRequest(this);
|
||||
}
|
||||
|
||||
public readonly record struct RemoteConnectionClosed(
|
||||
ProductUserId RemoteUserId,
|
||||
RemoteConnectionClosed.ConnectionClosedReason Reason)
|
||||
{
|
||||
public enum ConnectionClosedReason
|
||||
{
|
||||
Unknown,
|
||||
ClosedByLocalUser,
|
||||
ClosedByPeer,
|
||||
TimedOut,
|
||||
TooManyConnections,
|
||||
InvalidMessage,
|
||||
InvalidData,
|
||||
ConnectionFailed,
|
||||
ConnectionClosed,
|
||||
NegotiationFailed,
|
||||
UnexpectedError,
|
||||
Unhandled
|
||||
}
|
||||
}
|
||||
|
||||
public readonly NamedEvent<IncomingConnectionRequest> HandleIncomingConnection
|
||||
= new NamedEvent<IncomingConnectionRequest>();
|
||||
|
||||
public readonly NamedEvent<RemoteConnectionClosed> HandleClosedConnection
|
||||
= new NamedEvent<RemoteConnectionClosed>();
|
||||
|
||||
public static Result<P2PSocket, CreationError> Create(ProductUserId puid, SocketId socketId)
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? Core.LoadedImplementation.CreateP2PSocket(puid, socketId)
|
||||
: Result.Failure(CreationError.EosNotInitialized);
|
||||
|
||||
public abstract void AcceptConnectionRequest(IncomingConnectionRequest request);
|
||||
|
||||
public abstract void CloseConnection(ProductUserId remoteUserId);
|
||||
|
||||
public readonly record struct IncomingMessage(
|
||||
byte[] Buffer,
|
||||
int ByteLength,
|
||||
ProductUserId Sender);
|
||||
|
||||
public abstract IEnumerable<IncomingMessage> GetMessageBatch();
|
||||
|
||||
public readonly record struct OutgoingMessage(
|
||||
byte[] Buffer,
|
||||
int ByteLength,
|
||||
ProductUserId Destination,
|
||||
DeliveryMethod DeliveryMethod);
|
||||
|
||||
public enum SendError
|
||||
{
|
||||
EosNotInitialized,
|
||||
InvalidParameters,
|
||||
LimitExceeded,
|
||||
NoConnection,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public abstract Result<Unit, SendError> SendMessage(OutgoingMessage msg);
|
||||
|
||||
public abstract void Dispose();
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Result<P2PSocket, P2PSocket.CreationError> CreateP2PSocket(ProductUserId puid,
|
||||
SocketId socketId);
|
||||
}
|
||||
}
|
||||
6
Libraries/BarotraumaLibs/EosInterface/P2P/SocketId.cs
Normal file
6
Libraries/BarotraumaLibs/EosInterface/P2P/SocketId.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public readonly record struct SocketId(string SocketName);
|
||||
}
|
||||
167
Libraries/BarotraumaLibs/EosInterface/Sessions/Sessions.cs
Normal file
167
Libraries/BarotraumaLibs/EosInterface/Sessions/Sessions.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static partial class EosInterface
|
||||
{
|
||||
public static class Sessions
|
||||
{
|
||||
public const string DefaultBucketName = "BBucket";
|
||||
public const int MinBucketIndex = 0;
|
||||
public const int MaxBucketIndex = 9;
|
||||
|
||||
public sealed record OwnedSession(
|
||||
string BucketId,
|
||||
Identifier InternalId,
|
||||
Identifier GlobalId,
|
||||
Dictionary<Identifier, string> Attributes) : IDisposable
|
||||
{
|
||||
public Option<string> HostAddress = Option.None;
|
||||
|
||||
public ImmutableDictionary<Identifier, string> SyncedAttributes =
|
||||
ImmutableDictionary<Identifier, string>.Empty;
|
||||
|
||||
public async Task<Result<Unit, AttributeUpdateError>> UpdateAttributes()
|
||||
=> Core.LoadedImplementation is { } implementation
|
||||
? await implementation.UpdateOwnedSessionAttributes(this)
|
||||
: Result.Failure(AttributeUpdateError.EosNotInitialized);
|
||||
|
||||
public async Task<Result<Unit, CloseError>> Close()
|
||||
=> Core.LoadedImplementation is { } implementation
|
||||
? await implementation.CloseOwnedSession(this)
|
||||
: Result.Failure(CloseError.EosNotInitialized);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!Core.IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var _ = Close();
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct RemoteSession(
|
||||
string SessionId,
|
||||
string HostAddress,
|
||||
int CurrentPlayers,
|
||||
int MaxPlayers,
|
||||
ImmutableDictionary<Identifier, string> Attributes,
|
||||
string BucketId)
|
||||
{
|
||||
public readonly record struct Query(
|
||||
int BucketIndex,
|
||||
ProductUserId LocalUserId,
|
||||
uint MaxResults,
|
||||
ImmutableDictionary<Identifier, string> Attributes)
|
||||
{
|
||||
public enum Error
|
||||
{
|
||||
EosNotInitialized,
|
||||
|
||||
ExceededMaxAllowedResults,
|
||||
|
||||
InvalidParameters,
|
||||
TimedOut,
|
||||
NotFound,
|
||||
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public async Task<Result<ImmutableArray<RemoteSession>, Error>> Run()
|
||||
=> Core.LoadedImplementation is { } loadedImplementation
|
||||
? await loadedImplementation.RunRemoteSessionQuery(this)
|
||||
: Result.Failure(Error.EosNotInitialized);
|
||||
}
|
||||
}
|
||||
|
||||
public enum CreateError
|
||||
{
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
|
||||
SessionAlreadyExists,
|
||||
|
||||
InvalidParametersForAddAttribute,
|
||||
IncompatibleVersionForAddAttribute,
|
||||
UnhandledErrorConditionForAddAttribute,
|
||||
|
||||
InvalidUser,
|
||||
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public enum AttributeUpdateError
|
||||
{
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
|
||||
FailedToCreateSessionModificationHandle,
|
||||
|
||||
InvalidParametersForRemoveAttribute,
|
||||
IncompatibleVersionForRemoveAttribute,
|
||||
UnhandledErrorConditionForRemoveAttribute,
|
||||
|
||||
InvalidParametersForAddAttribute,
|
||||
IncompatibleVersionForAddAttribute,
|
||||
UnhandledErrorConditionForAddAttribute,
|
||||
|
||||
InvalidParametersForSessionUpdate,
|
||||
SessionsOutOfSync,
|
||||
SessionNotFound,
|
||||
NoConnection,
|
||||
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public enum CloseError
|
||||
{
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
|
||||
InvalidParameters,
|
||||
AlreadyPending,
|
||||
NotFound,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public enum RegisterError
|
||||
{
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public enum UnregisterError
|
||||
{
|
||||
EosNotInitialized,
|
||||
TimedOut,
|
||||
UnhandledErrorCondition
|
||||
}
|
||||
|
||||
public static async Task<Result<OwnedSession, CreateError>> CreateSession(Option<ProductUserId> puidOption,
|
||||
Identifier internalId, int maxPlayers)
|
||||
=> Core.LoadedImplementation.IsInitialized()
|
||||
? await Core.LoadedImplementation.CreateSession(puidOption, internalId, maxPlayers)
|
||||
: Result.Failure(CreateError.EosNotInitialized);
|
||||
}
|
||||
|
||||
internal abstract partial class Implementation
|
||||
{
|
||||
public abstract Task<Result<Sessions.OwnedSession, Sessions.CreateError>> CreateSession(
|
||||
Option<ProductUserId> selfUserIdOption, Identifier internalId, int maxPlayers);
|
||||
|
||||
public abstract Task<Result<Unit, Sessions.AttributeUpdateError>> UpdateOwnedSessionAttributes(
|
||||
Sessions.OwnedSession session);
|
||||
|
||||
public abstract Task<Result<Unit, Sessions.CloseError>> CloseOwnedSession(Sessions.OwnedSession session);
|
||||
public abstract Task CloseAllOwnedSessions();
|
||||
|
||||
public abstract Task<Result<ImmutableArray<Sessions.RemoteSession>, Sessions.RemoteSession.Query.Error>>
|
||||
RunRemoteSessionQuery(Sessions.RemoteSession.Query query);
|
||||
}
|
||||
}
|
||||
2
Libraries/BarotraumaLibs/EosInterfacePrivate/.gitignore
vendored
Normal file
2
Libraries/BarotraumaLibs/EosInterfacePrivate/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
EOS-SDK/*
|
||||
**/ExcludeFromPublicRepo/*
|
||||
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>EosInterface.Implementation.Linux</AssemblyName>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>EosInterfacePrivate</RootNamespace>
|
||||
<ImplicitUsings>false</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<DefineConstants>EOS_PLATFORM_LINUX</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<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>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<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>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="EOS-SDK/Source/Generated/Android/**/*" />
|
||||
<Compile Remove="EOS-SDK/Source/Generated/IOS/**/*" />
|
||||
<Compile Remove="EOS-SDK/Source/Generated/Windows/**/*" />
|
||||
|
||||
<ContentWithTargetPath Include="EOS-SDK/Redist/libEOSSDK-Linux-Shipping.so">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>libEOSSDK-Linux-Shipping.so</TargetPath>
|
||||
</ContentWithTargetPath>
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" />
|
||||
<ProjectReference Include="..\BarotraumaCore\BarotraumaCore.csproj" />
|
||||
<ProjectReference Include="..\EosInterface\EosInterface.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>EosInterface.Implementation.MacOS</AssemblyName>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>EosInterfacePrivate</RootNamespace>
|
||||
<ImplicitUsings>false</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<DefineConstants>EOS_PLATFORM_OSX</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<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>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<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>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="EOS-SDK/Source/Generated/Android/**/*" />
|
||||
<Compile Remove="EOS-SDK/Source/Generated/IOS/**/*" />
|
||||
<Compile Remove="EOS-SDK/Source/Generated/Windows/**/*" />
|
||||
|
||||
<ContentWithTargetPath Include="EOS-SDK/Redist/libEOSSDK-Mac-Shipping.dylib">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>libEOSSDK-Mac-Shipping.dylib</TargetPath>
|
||||
</ContentWithTargetPath>
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" />
|
||||
<ProjectReference Include="..\BarotraumaCore\BarotraumaCore.csproj" />
|
||||
<ProjectReference Include="..\EosInterface\EosInterface.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>EosInterface.Implementation.Win64</AssemblyName>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>EosInterfacePrivate</RootNamespace>
|
||||
<ImplicitUsings>false</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<DefineConstants>EOS_PLATFORM_WINDOWS_64</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<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>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<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>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="EOS-SDK/Source/Generated/Android/**/*" />
|
||||
<Compile Remove="EOS-SDK/Source/Generated/IOS/**/*" />
|
||||
|
||||
<ContentWithTargetPath Include="EOS-SDK/Redist/EOSSDK-Win64-Shipping.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>EOSSDK-Win64-Shipping.dll</TargetPath>
|
||||
</ContentWithTargetPath>
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Facepunch.Steamworks\Facepunch.Steamworks.Win64.csproj" />
|
||||
<ProjectReference Include="..\BarotraumaCore\BarotraumaCore.csproj" />
|
||||
<ProjectReference Include="..\EosInterface\EosInterface.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,266 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
public static class AchievementsPrivate
|
||||
{
|
||||
public static async Task<Result<uint, EosInterface.AchievementUnlockError>> UnlockAchievements(params Identifier[] achievements)
|
||||
{
|
||||
if (CorePrivate.AchievementsInterface is not { } achievementsInterface) { return Result.Failure(EosInterface.AchievementUnlockError.EosNotInitialized); }
|
||||
|
||||
var loggedInUsers = IdQueriesPrivate.GetLoggedInPuids();
|
||||
|
||||
if (loggedInUsers is not { Length: > 0 })
|
||||
{
|
||||
return Result.Failure(EosInterface.AchievementUnlockError.InvalidUser);
|
||||
}
|
||||
var loggedInUser = loggedInUsers[0];
|
||||
|
||||
var achievementUnlockWaiter = new CallbackWaiter<Epic.OnlineServices.Achievements.OnUnlockAchievementsCompleteCallbackInfo>();
|
||||
var options = new Epic.OnlineServices.Achievements.UnlockAchievementsOptions
|
||||
{
|
||||
AchievementIds = achievements.Select(static i => new Epic.OnlineServices.Utf8String(i.Value.ToLowerInvariant())).ToArray(),
|
||||
UserId = Epic.OnlineServices.ProductUserId.FromString(loggedInUser.Value)
|
||||
};
|
||||
|
||||
achievementsInterface.UnlockAchievements(options: ref options, clientData: null, completionDelegate: achievementUnlockWaiter.OnCompletion);
|
||||
var resultOption = await achievementUnlockWaiter.Task;
|
||||
|
||||
if (!resultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.AchievementUnlockError.TimedOut);
|
||||
}
|
||||
|
||||
return callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.Success => Result.Success(callbackResult.AchievementsCount),
|
||||
Epic.OnlineServices.Result.InvalidParameters => Result.Failure(EosInterface.AchievementUnlockError.InvalidParameters),
|
||||
Epic.OnlineServices.Result.InvalidUser => Result.Failure(EosInterface.AchievementUnlockError.InvalidUser),
|
||||
Epic.OnlineServices.Result.NotFound => Result.Failure(EosInterface.AchievementUnlockError.NotFound),
|
||||
var unhandled => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.AchievementUnlockError.Unknown))
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<Result<ImmutableDictionary<AchievementStat, int>, EosInterface.QueryStatsError>> QueryStats(ImmutableArray<AchievementStat> stats)
|
||||
{
|
||||
if (CorePrivate.StatsInterface is not { } statsInterface) { return Result.Failure(EosInterface.QueryStatsError.EosNotInitialized); }
|
||||
|
||||
var loggedInUsers = IdQueriesPrivate.GetLoggedInPuids();
|
||||
|
||||
if (loggedInUsers is not { Length: > 0 })
|
||||
{
|
||||
return Result.Failure(EosInterface.QueryStatsError.InvalidUser);
|
||||
}
|
||||
var loggedInUser = loggedInUsers[0];
|
||||
|
||||
var convertedUserId = Epic.OnlineServices.ProductUserId.FromString(loggedInUser.Value);
|
||||
|
||||
var options = new Epic.OnlineServices.Stats.QueryStatsOptions
|
||||
{
|
||||
LocalUserId = convertedUserId,
|
||||
TargetUserId = convertedUserId,
|
||||
StatNames = stats.Any()
|
||||
? stats.Select(static s => new Epic.OnlineServices.Utf8String(s.ToIdentifier().Value.ToLowerInvariant())).ToArray()
|
||||
: default
|
||||
};
|
||||
|
||||
var queryWaiter = new CallbackWaiter<Epic.OnlineServices.Stats.OnQueryStatsCompleteCallbackInfo>();
|
||||
statsInterface.QueryStats(options: ref options, clientData: null, completionDelegate: queryWaiter.OnCompletion);
|
||||
|
||||
var resultOption = await queryWaiter.Task;
|
||||
|
||||
if (!resultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.QueryStatsError.TimedOut);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidParameters => Result.Failure(EosInterface.QueryStatsError.InvalidParameters),
|
||||
Epic.OnlineServices.Result.InvalidUser => Result.Failure(EosInterface.QueryStatsError.InvalidUser),
|
||||
Epic.OnlineServices.Result.NotFound => Result.Failure(EosInterface.QueryStatsError.NotFound),
|
||||
var unhandled => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.QueryStatsError.Unknown))
|
||||
};
|
||||
}
|
||||
|
||||
var builder = ImmutableDictionary.CreateBuilder<AchievementStat, int>();
|
||||
|
||||
if (stats.Length is 0)
|
||||
{
|
||||
var countOptions = new Epic.OnlineServices.Stats.GetStatCountOptions
|
||||
{
|
||||
TargetUserId = convertedUserId
|
||||
};
|
||||
uint count = statsInterface.GetStatsCount(ref countOptions);
|
||||
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
var copyIndexOptions = new Epic.OnlineServices.Stats.CopyStatByIndexOptions
|
||||
{
|
||||
TargetUserId = convertedUserId,
|
||||
StatIndex = i
|
||||
};
|
||||
var copyResult = statsInterface.CopyStatByIndex(ref copyIndexOptions, out var statOut);
|
||||
|
||||
if (copyResult is Epic.OnlineServices.Result.Success && statOut is { Name: var name, Value: var value })
|
||||
{
|
||||
builder.Add(AchievementStatExtension.FromIdentifier(new Identifier(name)), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (AchievementStat stat in stats)
|
||||
{
|
||||
var copyOptions = new Epic.OnlineServices.Stats.CopyStatByNameOptions
|
||||
{
|
||||
TargetUserId = convertedUserId,
|
||||
Name = new Epic.OnlineServices.Utf8String(stat.ToString().ToLowerInvariant())
|
||||
};
|
||||
var copyResult = statsInterface.CopyStatByName(ref copyOptions, out var statOut);
|
||||
|
||||
if (copyResult is Epic.OnlineServices.Result.Success && statOut is { Name: var name, Value: var value })
|
||||
{
|
||||
builder.Add(AchievementStatExtension.FromIdentifier(new Identifier(name)), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success(builder.ToImmutable());
|
||||
}
|
||||
|
||||
public static async Task<Result<ImmutableDictionary<Identifier, double>, EosInterface.QueryAchievementsError>> QueryPlayerAchievements()
|
||||
{
|
||||
if (CorePrivate.AchievementsInterface is not { } achievementsInterface) { return Result.Failure(EosInterface.QueryAchievementsError.EosNotInitialized); }
|
||||
|
||||
var loggedInUsers = IdQueriesPrivate.GetLoggedInPuids();
|
||||
|
||||
if (loggedInUsers is not { Length: > 0 })
|
||||
{
|
||||
return Result.Failure(EosInterface.QueryAchievementsError.InvalidUser);
|
||||
}
|
||||
var loggedInUser = loggedInUsers[0];
|
||||
|
||||
var convertedUserId = Epic.OnlineServices.ProductUserId.FromString(loggedInUser.Value);
|
||||
|
||||
var options = new Epic.OnlineServices.Achievements.QueryPlayerAchievementsOptions
|
||||
{
|
||||
LocalUserId = convertedUserId,
|
||||
TargetUserId = convertedUserId
|
||||
};
|
||||
|
||||
var queryWaiter = new CallbackWaiter<Epic.OnlineServices.Achievements.OnQueryPlayerAchievementsCompleteCallbackInfo>();
|
||||
achievementsInterface.QueryPlayerAchievements(options: ref options, clientData: null, completionDelegate: queryWaiter.OnCompletion);
|
||||
|
||||
var resultOption = await queryWaiter.Task;
|
||||
|
||||
if (!resultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.QueryAchievementsError.TimedOut);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidParameters => Result.Failure(EosInterface.QueryAchievementsError.InvalidParameters),
|
||||
Epic.OnlineServices.Result.InvalidUser => Result.Failure(EosInterface.QueryAchievementsError.InvalidUser),
|
||||
Epic.OnlineServices.Result.InvalidProductUserID => Result.Failure(EosInterface.QueryAchievementsError.InvalidProductUserID),
|
||||
Epic.OnlineServices.Result.NotFound => Result.Failure(EosInterface.QueryAchievementsError.NotFound),
|
||||
var unhandled => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.QueryAchievementsError.Unknown))
|
||||
};
|
||||
}
|
||||
|
||||
var countOptions = new Epic.OnlineServices.Achievements.GetPlayerAchievementCountOptions
|
||||
{
|
||||
UserId = convertedUserId
|
||||
};
|
||||
uint count = achievementsInterface.GetPlayerAchievementCount(ref countOptions);
|
||||
|
||||
var builder = ImmutableDictionary.CreateBuilder<Identifier, double>();
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
var copyIndexOptions = new Epic.OnlineServices.Achievements.CopyPlayerAchievementByIndexOptions
|
||||
{
|
||||
TargetUserId = convertedUserId,
|
||||
LocalUserId = convertedUserId,
|
||||
AchievementIndex = i
|
||||
};
|
||||
var copyResult = achievementsInterface.CopyPlayerAchievementByIndex(ref copyIndexOptions, out var achievementOut);
|
||||
|
||||
if (copyResult is Epic.OnlineServices.Result.Success && achievementOut is { AchievementId: var name, Progress: var value })
|
||||
{
|
||||
builder.Add(new Identifier(name), value);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success(builder.ToImmutable());
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.IngestStatError>> IngestStats(params (AchievementStat Stat, int IngestAmount)[] stats)
|
||||
{
|
||||
if (CorePrivate.StatsInterface is not { } statsInterface) { return Result.Failure(EosInterface.IngestStatError.EosNotInitialized); }
|
||||
|
||||
var loggedInUsers = IdQueriesPrivate.GetLoggedInPuids();
|
||||
|
||||
if (loggedInUsers is not { Length: > 0 })
|
||||
{
|
||||
return Result.Failure(EosInterface.IngestStatError.InvalidUser);
|
||||
}
|
||||
var loggedInUser = loggedInUsers[0];
|
||||
|
||||
var convertedUserId = Epic.OnlineServices.ProductUserId.FromString(loggedInUser.Value);
|
||||
|
||||
var options = new Epic.OnlineServices.Stats.IngestStatOptions
|
||||
{
|
||||
LocalUserId = convertedUserId,
|
||||
TargetUserId = convertedUserId,
|
||||
Stats = stats.Select(static s => new Epic.OnlineServices.Stats.IngestData
|
||||
{
|
||||
StatName = s.Stat.ToString().ToLowerInvariant(),
|
||||
IngestAmount = s.IngestAmount
|
||||
}).ToArray()
|
||||
};
|
||||
|
||||
var ingestStatWaiter = new CallbackWaiter<Epic.OnlineServices.Stats.IngestStatCompleteCallbackInfo>();
|
||||
statsInterface.IngestStat(options: ref options, clientData: null, completionDelegate: ingestStatWaiter.OnCompletion);
|
||||
|
||||
var resultOption = await ingestStatWaiter.Task;
|
||||
|
||||
if (!resultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.IngestStatError.TimedOut);
|
||||
}
|
||||
|
||||
return callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.Success => Result.Success(Unit.Value),
|
||||
Epic.OnlineServices.Result.InvalidParameters => Result.Failure(EosInterface.IngestStatError.InvalidParameters),
|
||||
Epic.OnlineServices.Result.InvalidUser => Result.Failure(EosInterface.IngestStatError.InvalidUser),
|
||||
Epic.OnlineServices.Result.NotFound => Result.Failure(EosInterface.IngestStatError.NotFound),
|
||||
var unhandled => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.IngestStatError.Unknown))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Result<uint, EosInterface.AchievementUnlockError>> UnlockAchievements(params Identifier[] achievementIds)
|
||||
=> TaskScheduler.Schedule(() => AchievementsPrivate.UnlockAchievements(achievementIds));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.IngestStatError>> IngestStats(params (AchievementStat Stat, int IngestAmount)[] stats)
|
||||
=> TaskScheduler.Schedule(() => AchievementsPrivate.IngestStats(stats));
|
||||
|
||||
public override Task<Result<ImmutableDictionary<AchievementStat, int>, EosInterface.QueryStatsError>> QueryStats(ImmutableArray<AchievementStat> stats)
|
||||
=> TaskScheduler.Schedule(() => AchievementsPrivate.QueryStats(stats));
|
||||
|
||||
public override Task<Result<ImmutableDictionary<Identifier, double>, EosInterface.QueryAchievementsError>> QueryPlayerAchievements()
|
||||
=> TaskScheduler.Schedule(AchievementsPrivate.QueryPlayerAchievements);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Barotrauma.Debugging;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class CorePrivate
|
||||
{
|
||||
public static EosInterface.Core.Status CurrentStatus
|
||||
=> platformInterface is null
|
||||
? EosInterface.Core.Status.NotInitialized
|
||||
: platformInterface.GetNetworkStatus() == Epic.OnlineServices.Platform.NetworkStatus.Online
|
||||
? EosInterface.Core.Status.Online
|
||||
: EosInterface.Core.Status.InitializedButOffline;
|
||||
|
||||
private static Epic.OnlineServices.Platform.Options platformInterfaceOptions;
|
||||
public static Epic.OnlineServices.Platform.Options PlatformInterfaceOptions => platformInterfaceOptions;
|
||||
|
||||
private static Epic.OnlineServices.Platform.PlatformInterface? platformInterface;
|
||||
public static Epic.OnlineServices.Platform.PlatformInterface? PlatformInterface => platformInterface;
|
||||
|
||||
public static Epic.OnlineServices.Connect.ConnectInterface? ConnectInterface
|
||||
=> PlatformInterface?.GetConnectInterface();
|
||||
|
||||
public static Epic.OnlineServices.Auth.AuthInterface? EgsAuthInterface
|
||||
=> PlatformInterface?.GetAuthInterface();
|
||||
|
||||
public static Epic.OnlineServices.Friends.FriendsInterface? EgsFriendsInterface
|
||||
=> PlatformInterface?.GetFriendsInterface();
|
||||
|
||||
public static Epic.OnlineServices.UserInfo.UserInfoInterface? EgsUserInfoInterface
|
||||
=> PlatformInterface?.GetUserInfoInterface();
|
||||
|
||||
public static Epic.OnlineServices.Presence.PresenceInterface? EgsPresenceInterface
|
||||
=> PlatformInterface?.GetPresenceInterface();
|
||||
|
||||
public static Epic.OnlineServices.CustomInvites.CustomInvitesInterface? EgsCustomInvitesInterface
|
||||
=> PlatformInterface?.GetCustomInvitesInterface();
|
||||
|
||||
public static Epic.OnlineServices.UI.UIInterface? EgsUiInterface
|
||||
=> PlatformInterface?.GetUIInterface();
|
||||
|
||||
public static Epic.OnlineServices.Sessions.SessionsInterface? SessionsInterface
|
||||
=> PlatformInterface?.GetSessionsInterface();
|
||||
|
||||
public static Epic.OnlineServices.P2P.P2PInterface? P2PInterface
|
||||
=> PlatformInterface?.GetP2PInterface();
|
||||
|
||||
public static Epic.OnlineServices.Achievements.AchievementsInterface? AchievementsInterface
|
||||
=> PlatformInterface?.GetAchievementsInterface();
|
||||
|
||||
public static Epic.OnlineServices.Stats.StatsInterface? StatsInterface
|
||||
=> PlatformInterface?.GetStatsInterface();
|
||||
|
||||
public static Epic.OnlineServices.Ecom.EcomInterface? EcomInterface
|
||||
=> PlatformInterface?.GetEcomInterface();
|
||||
|
||||
public static Result<Unit, EosInterface.Core.InitError> Init(ImplementationPrivate implementation, EosInterface.ApplicationCredentials applicationCredentials, bool enableOverlay)
|
||||
{
|
||||
var initializeOptions = new Epic.OnlineServices.Platform.InitializeOptions
|
||||
{
|
||||
ProductName = "Barotrauma",
|
||||
ProductVersion = GameVersion.CurrentVersion.ToString(),
|
||||
|
||||
SystemInitializeOptions = IntPtr.Zero,
|
||||
OverrideThreadAffinity = null,
|
||||
|
||||
AllocateMemoryFunction = IntPtr.Zero,
|
||||
ReallocateMemoryFunction = IntPtr.Zero,
|
||||
ReleaseMemoryFunction = IntPtr.Zero
|
||||
};
|
||||
|
||||
var result = Epic.OnlineServices.Platform.PlatformInterface.Initialize(ref initializeOptions);
|
||||
Console.WriteLine(
|
||||
$"{nameof(Epic.OnlineServices.Platform.PlatformInterface)}.{nameof(Epic.OnlineServices.Platform.PlatformInterface.Initialize)} result: {result}");
|
||||
|
||||
platformInterfaceOptions = PlatformInterfaceOptionsPrivate.PlatformOptions[applicationCredentials];
|
||||
if (enableOverlay)
|
||||
{
|
||||
// Some caveats:
|
||||
// - Currently the overlay is not implemented on non-Windows platforms
|
||||
// - If you try to initialize EOS after the window has already been created,
|
||||
// enabling the overlay will result in a crash
|
||||
// - The overlay doesn't do anything if you do not log into an Epic account
|
||||
platformInterfaceOptions.Flags = Epic.OnlineServices.Platform.PlatformFlags.None;
|
||||
}
|
||||
|
||||
platformInterface = Epic.OnlineServices.Platform.PlatformInterface.Create(ref platformInterfaceOptions);
|
||||
|
||||
if (ConnectInterface != null)
|
||||
{
|
||||
LoginPrivate.Init();
|
||||
}
|
||||
|
||||
if (platformInterface is null) { return Result.Failure(EosInterface.Core.InitError.PlatformInterfaceNotCreated); }
|
||||
|
||||
PresencePrivate.Init(implementation);
|
||||
|
||||
var setLogCallbackResult = Epic.OnlineServices.Logging.LoggingInterface.SetCallback(LogCallback);
|
||||
if (setLogCallbackResult == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
Epic.OnlineServices.Logging.LoggingInterface.SetLogLevel(
|
||||
Epic.OnlineServices.Logging.LogCategory.AllCategories,
|
||||
Epic.OnlineServices.Logging.LogLevel.VeryVerbose);
|
||||
}
|
||||
|
||||
return Result.Success(default(Unit));
|
||||
}
|
||||
|
||||
private static void LogCallback(ref Epic.OnlineServices.Logging.LogMessage msg)
|
||||
{
|
||||
DebugConsoleCore.Log($"[EOS {msg.Category} {msg.Level}] {msg.Message}");
|
||||
}
|
||||
|
||||
public static Result<EosInterface.Core.WillRestartThroughLauncher, EosInterface.Core.CheckForLauncherAndRestartError> CheckForLauncherAndRestart()
|
||||
{
|
||||
if (platformInterface is null) { return Result.Failure(EosInterface.Core.CheckForLauncherAndRestartError.EosNotInitialized); }
|
||||
var result = platformInterface.CheckForLauncherAndRestart();
|
||||
if (result == Epic.OnlineServices.Result.Success) { return Result.Success(EosInterface.Core.WillRestartThroughLauncher.Yes); }
|
||||
if (result == Epic.OnlineServices.Result.NoChange) { return Result.Success(EosInterface.Core.WillRestartThroughLauncher.No); }
|
||||
return Result.Failure(result switch
|
||||
{
|
||||
Epic.OnlineServices.Result.UnexpectedError
|
||||
=> EosInterface.Core.CheckForLauncherAndRestartError.UnexpectedError,
|
||||
_
|
||||
=> result.FailAndLogUnhandledError(EosInterface.Core.CheckForLauncherAndRestartError.UnhandledErrorCondition)
|
||||
});
|
||||
}
|
||||
|
||||
private static EosInterface.Core.Status prevTickStatus = EosInterface.Core.Status.NotInitialized;
|
||||
public static void Update()
|
||||
{
|
||||
platformInterface?.Tick();
|
||||
var currentStatus = CurrentStatus;
|
||||
if (currentStatus == EosInterface.Core.Status.Online && prevTickStatus != currentStatus)
|
||||
{
|
||||
// We were offline, but now we are back online so let's update all sessions
|
||||
OwnedSessionsPrivate.ForceUpdateAllOwnedSessions();
|
||||
}
|
||||
prevTickStatus = currentStatus;
|
||||
}
|
||||
|
||||
public static void Quit()
|
||||
{
|
||||
PresencePrivate.Quit();
|
||||
|
||||
platformInterface?.Release();
|
||||
platformInterface = null;
|
||||
Epic.OnlineServices.Platform.PlatformInterface.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
|
||||
public override EosInterface.Core.Status CurrentStatus => CorePrivate.CurrentStatus;
|
||||
|
||||
public override string NativeLibraryName => Epic.OnlineServices.Config.LibraryName;
|
||||
|
||||
public override Result<Unit, EosInterface.Core.InitError> Init(EosInterface.ApplicationCredentials applicationCredentials, bool enableOverlay)
|
||||
=> CorePrivate.Init(this, applicationCredentials, enableOverlay);
|
||||
|
||||
public override Result<EosInterface.Core.WillRestartThroughLauncher, EosInterface.Core.CheckForLauncherAndRestartError> CheckForLauncherAndRestart()
|
||||
=> CorePrivate.CheckForLauncherAndRestart();
|
||||
|
||||
public override void Quit()
|
||||
=> CorePrivate.Quit();
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
CorePrivate.Update();
|
||||
TaskScheduler.RunOnCurrentThread();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
internal sealed partial class ImplementationPrivate : Barotrauma.EosInterface.Implementation
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom TaskScheduler to force every EOS-related task to run on the main thread, because
|
||||
/// the docs say the SDK is not thread-safe even though it's worked fine without this :/
|
||||
///
|
||||
/// See https://dev.epicgames.com/docs/epic-online-services/eos-get-started/eossdkc-sharp-getting-started#threading
|
||||
/// </summary>
|
||||
internal sealed class CustomTaskScheduler : TaskScheduler
|
||||
{
|
||||
private readonly ConcurrentQueue<Task> taskQueue = new ConcurrentQueue<Task>();
|
||||
|
||||
internal Task<T> Schedule<T>(Func<Task<T>> action)
|
||||
{
|
||||
return
|
||||
Task.Factory.StartNew(
|
||||
function: action,
|
||||
cancellationToken: CancellationToken.None,
|
||||
creationOptions: TaskCreationOptions.None,
|
||||
scheduler: this).Unwrap();
|
||||
}
|
||||
|
||||
internal Task Schedule(Func<Task> action)
|
||||
{
|
||||
return
|
||||
Task.Factory.StartNew(
|
||||
function: action,
|
||||
cancellationToken: CancellationToken.None,
|
||||
creationOptions: TaskCreationOptions.None,
|
||||
scheduler: this).Unwrap();
|
||||
}
|
||||
|
||||
internal void RunOnCurrentThread()
|
||||
{
|
||||
while (taskQueue.TryDequeue(out var task))
|
||||
{
|
||||
TryExecuteTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<Task> GetScheduledTasks()
|
||||
=> Enumerable.Empty<Task>();
|
||||
|
||||
protected override void QueueTask(Task task)
|
||||
{
|
||||
taskQueue.Enqueue(task);
|
||||
}
|
||||
|
||||
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
|
||||
{
|
||||
// Never allow executing inline because that means it's not the main thread
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly CustomTaskScheduler TaskScheduler = new CustomTaskScheduler();
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class FriendsPrivate
|
||||
{
|
||||
internal static async Task<Result<Epic.OnlineServices.UserInfo.UserInfoData, EosInterface.Friends.GetFriendsError>> GetUserInfoData(
|
||||
EpicAccountId selfEaid, EpicAccountId friendEaid)
|
||||
{
|
||||
if (CorePrivate.EgsUserInfoInterface is not { } egsUserInfoInterface) { return Result.Failure(EosInterface.Friends.GetFriendsError.EosNotInitialized); }
|
||||
|
||||
var selfEaidInternal = Epic.OnlineServices.EpicAccountId.FromString(selfEaid.EosStringRepresentation);
|
||||
var friendEaidInternal = Epic.OnlineServices.EpicAccountId.FromString(friendEaid.EosStringRepresentation);
|
||||
|
||||
var queryUserInfoOptions = new Epic.OnlineServices.UserInfo.QueryUserInfoOptions
|
||||
{
|
||||
LocalUserId = selfEaidInternal,
|
||||
TargetUserId = friendEaidInternal
|
||||
};
|
||||
var queryUserInfoWaiter = new CallbackWaiter<Epic.OnlineServices.UserInfo.QueryUserInfoCallbackInfo>();
|
||||
egsUserInfoInterface.QueryUserInfo(options: ref queryUserInfoOptions, clientData: null, completionDelegate: queryUserInfoWaiter.OnCompletion);
|
||||
var queryUserInfoResult = await queryUserInfoWaiter.Task;
|
||||
if (!queryUserInfoResult.TryUnwrap(out var queryUserInfo))
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.UserInfoQueryTimedOut);
|
||||
}
|
||||
if (queryUserInfo.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.UserInfoQueryFailed);
|
||||
}
|
||||
|
||||
var copyUserInfoOptions = new Epic.OnlineServices.UserInfo.CopyUserInfoOptions
|
||||
{
|
||||
LocalUserId = selfEaidInternal,
|
||||
TargetUserId = friendEaidInternal
|
||||
};
|
||||
var copyUserInfoResult = egsUserInfoInterface.CopyUserInfo(ref copyUserInfoOptions, out var friendInfoNullable);
|
||||
if (copyUserInfoResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.CopyUserInfoFailed);
|
||||
}
|
||||
if (friendInfoNullable is not { } friendInfo)
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.CopyUserInfoFailed);
|
||||
}
|
||||
|
||||
string displayName = friendInfo.Nickname ?? friendInfo.DisplayName ?? "";
|
||||
if (string.IsNullOrEmpty(displayName))
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.DisplayNameIsEmpty);
|
||||
}
|
||||
|
||||
return Result.Success(friendInfo);
|
||||
}
|
||||
|
||||
internal static async Task<Result<EosInterface.EgsFriend, EosInterface.Friends.GetFriendsError>> GetPresenceFromUserInfoData(
|
||||
EpicAccountId selfEaid, EpicAccountId friendEaid, Epic.OnlineServices.UserInfo.UserInfoData friendInfo)
|
||||
{
|
||||
if (CorePrivate.EgsPresenceInterface is not { } egsPresenceInterface) { return Result.Failure(EosInterface.Friends.GetFriendsError.EosNotInitialized); }
|
||||
|
||||
var selfEaidInternal = Epic.OnlineServices.EpicAccountId.FromString(selfEaid.EosStringRepresentation);
|
||||
var friendEaidInternal = friendInfo.UserId;
|
||||
|
||||
string displayName = friendInfo.Nickname ?? friendInfo.DisplayName ?? "";
|
||||
|
||||
var queryPresenceOptions = new Epic.OnlineServices.Presence.QueryPresenceOptions
|
||||
{
|
||||
LocalUserId = selfEaidInternal,
|
||||
TargetUserId = friendEaidInternal
|
||||
};
|
||||
var queryPresenceWaiter = new CallbackWaiter<Epic.OnlineServices.Presence.QueryPresenceCallbackInfo>();
|
||||
egsPresenceInterface.QueryPresence(options: ref queryPresenceOptions, clientData: null, completionDelegate: queryPresenceWaiter.OnCompletion);
|
||||
var queryPresenceResult = await queryPresenceWaiter.Task;
|
||||
if (!queryPresenceResult.TryUnwrap(out var queryPresence))
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.EgsPresenceQueryTimedOut);
|
||||
}
|
||||
if (queryPresence.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.EgsPresenceQueryFailed);
|
||||
}
|
||||
|
||||
var copyPresenceOptions = new Epic.OnlineServices.Presence.CopyPresenceOptions
|
||||
{
|
||||
LocalUserId = selfEaidInternal,
|
||||
TargetUserId = friendEaidInternal
|
||||
};
|
||||
var copyPresenceResult = egsPresenceInterface.CopyPresence(ref copyPresenceOptions, out var friendPresenceNullable);
|
||||
if (copyPresenceResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.CopyPresenceFailed);
|
||||
}
|
||||
if (friendPresenceNullable is not { } friendPresence)
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.CopyPresenceFailed);
|
||||
}
|
||||
|
||||
string productId = friendPresence.ProductId ?? "";
|
||||
var friendStatus = friendPresence.Status switch
|
||||
{
|
||||
Epic.OnlineServices.Presence.Status.Offline
|
||||
=> FriendStatus.Offline,
|
||||
_
|
||||
=> productId == PlatformInterfaceOptionsPrivate.BasePlatformInterfaceOptions.ProductId
|
||||
? FriendStatus.PlayingBarotrauma
|
||||
: !string.IsNullOrEmpty(productId)
|
||||
? FriendStatus.PlayingAnotherGame
|
||||
: FriendStatus.NotPlaying
|
||||
};
|
||||
|
||||
var records = friendPresence.Records ?? Array.Empty<Epic.OnlineServices.Presence.DataRecord>();
|
||||
|
||||
string getRecordValue(string key)
|
||||
=> records
|
||||
.FirstOrNone(r => string.Equals(r.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(r => r.Value)
|
||||
.Fallback("");
|
||||
var connectCommand = getRecordValue("connectcommand");
|
||||
var serverName = getRecordValue("servername");
|
||||
|
||||
return Result.Success(new EosInterface.EgsFriend(
|
||||
DisplayName: displayName,
|
||||
EpicAccountId: friendEaid,
|
||||
Status: friendStatus,
|
||||
ConnectCommand: connectCommand,
|
||||
ServerName: serverName));
|
||||
}
|
||||
|
||||
public static async Task<Result<EosInterface.EgsFriend, EosInterface.Friends.GetFriendsError>> GetSelfUserInfo(EpicAccountId epicAccount)
|
||||
{
|
||||
var getUserInfoDataResult = await GetUserInfoData(epicAccount, epicAccount);
|
||||
if (getUserInfoDataResult.TryUnwrapFailure(out var error))
|
||||
{
|
||||
return Result.Failure(error);
|
||||
}
|
||||
if (!getUserInfoDataResult.TryUnwrapSuccess(out var friendInfo))
|
||||
{
|
||||
throw new UnreachableCodeException();
|
||||
}
|
||||
|
||||
string displayName = friendInfo.Nickname ?? friendInfo.DisplayName ?? "";
|
||||
|
||||
return Result.Success(new EosInterface.EgsFriend(
|
||||
DisplayName: displayName,
|
||||
EpicAccountId: epicAccount,
|
||||
Status: FriendStatus.PlayingBarotrauma,
|
||||
ConnectCommand: "",
|
||||
ServerName: ""));
|
||||
}
|
||||
|
||||
public static async Task<Result<EosInterface.EgsFriend, EosInterface.Friends.GetFriendsError>> GetFriend(EpicAccountId selfEaid, EpicAccountId friendEaid)
|
||||
{
|
||||
var getUserInfoDataResult = await GetUserInfoData(selfEaid, friendEaid);
|
||||
if (getUserInfoDataResult.TryUnwrapFailure(out var error))
|
||||
{
|
||||
return Result.Failure(error);
|
||||
}
|
||||
if (!getUserInfoDataResult.TryUnwrapSuccess(out var friendInfo))
|
||||
{
|
||||
throw new UnreachableCodeException();
|
||||
}
|
||||
|
||||
return await GetPresenceFromUserInfoData(selfEaid, friendEaid, friendInfo);
|
||||
}
|
||||
|
||||
public static async Task<Result<ImmutableArray<EosInterface.EgsFriend>, EosInterface.Friends.GetFriendsError>> GetFriends(EpicAccountId epicAccount)
|
||||
{
|
||||
if (CorePrivate.EgsFriendsInterface is not { } egsFriendsInterface) { return Result.Failure(EosInterface.Friends.GetFriendsError.EosNotInitialized); }
|
||||
|
||||
var selfEaidInternal = Epic.OnlineServices.EpicAccountId.FromString(epicAccount.EosStringRepresentation);
|
||||
|
||||
var queryFriendsOptions = new Epic.OnlineServices.Friends.QueryFriendsOptions
|
||||
{
|
||||
LocalUserId = selfEaidInternal
|
||||
};
|
||||
var queryFriendsWaiter = new CallbackWaiter<Epic.OnlineServices.Friends.QueryFriendsCallbackInfo>();
|
||||
egsFriendsInterface.QueryFriends(options: ref queryFriendsOptions, clientData: null, completionDelegate: queryFriendsWaiter.OnCompletion);
|
||||
var queryFriendsInfoResult = await queryFriendsWaiter.Task;
|
||||
if (!queryFriendsInfoResult.TryUnwrap(out var queryFriendsInfo))
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.EgsFriendsQueryTimedOut);
|
||||
}
|
||||
|
||||
if (queryFriendsInfo.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Friends.GetFriendsError.EgsFriendsQueryFailed);
|
||||
}
|
||||
|
||||
var getFriendsCountOptions = new Epic.OnlineServices.Friends.GetFriendsCountOptions
|
||||
{
|
||||
LocalUserId = selfEaidInternal
|
||||
};
|
||||
var friendCount = egsFriendsInterface.GetFriendsCount(ref getFriendsCountOptions);
|
||||
var friends = new List<EosInterface.EgsFriend>();
|
||||
|
||||
for (int i = 0; i < friendCount; i++)
|
||||
{
|
||||
var getFriendAtIndexOptions = new Epic.OnlineServices.Friends.GetFriendAtIndexOptions
|
||||
{
|
||||
LocalUserId = selfEaidInternal,
|
||||
Index = i
|
||||
};
|
||||
var friendId = egsFriendsInterface.GetFriendAtIndex(ref getFriendAtIndexOptions);
|
||||
if (friendId == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!EpicAccountId.Parse(friendId.ToString()).TryUnwrap(out var friendIdPublic))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var getUserInfoDataResult = await GetUserInfoData(epicAccount, friendIdPublic);
|
||||
if (!getUserInfoDataResult.TryUnwrapSuccess(out var friendInfo))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var egsFriendPublicResult = await GetPresenceFromUserInfoData(epicAccount, friendIdPublic, friendInfo);
|
||||
if (!egsFriendPublicResult.TryUnwrapSuccess(out var egsFriendPublic))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
friends.Add(egsFriendPublic);
|
||||
}
|
||||
return Result.Success(friends.ToImmutableArray());
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Result<EosInterface.EgsFriend, EosInterface.Friends.GetFriendsError>> GetFriend(EpicAccountId selfEaid, EpicAccountId friendEaid)
|
||||
=> TaskScheduler.Schedule(() => FriendsPrivate.GetFriend(selfEaid, friendEaid));
|
||||
|
||||
public override Task<Result<ImmutableArray<EosInterface.EgsFriend>, EosInterface.Friends.GetFriendsError>> GetFriends(EpicAccountId epicAccountId)
|
||||
=> TaskScheduler.Schedule(() => FriendsPrivate.GetFriends(epicAccountId));
|
||||
|
||||
public override Task<Result<EosInterface.EgsFriend, EosInterface.Friends.GetFriendsError>> GetSelfUserInfo(EpicAccountId epicAccountId)
|
||||
=> TaskScheduler.Schedule(() => FriendsPrivate.GetSelfUserInfo(epicAccountId));
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma;
|
||||
using Barotrauma.Extensions;
|
||||
using EpicAccountId = Barotrauma.Networking.EpicAccountId;
|
||||
using Result = Barotrauma.Result;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class PresencePrivate
|
||||
{
|
||||
internal static readonly NamedEvent<EosInterface.Presence.JoinGameInfo> OnJoinGame = new NamedEvent<EosInterface.Presence.JoinGameInfo>();
|
||||
private static ulong joinGameAcceptedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid;
|
||||
|
||||
internal static readonly NamedEvent<EosInterface.Presence.AcceptInviteInfo> OnInviteAccepted = new NamedEvent<EosInterface.Presence.AcceptInviteInfo>();
|
||||
private static ulong inviteAcceptedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid;
|
||||
private static ulong inviteRejectedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid;
|
||||
|
||||
internal static readonly NamedEvent<EosInterface.Presence.ReceiveInviteInfo> OnInviteReceived = new NamedEvent<EosInterface.Presence.ReceiveInviteInfo>();
|
||||
private static ulong inviteReceivedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid;
|
||||
|
||||
public static void Init(ImplementationPrivate implementation)
|
||||
{
|
||||
var presenceInterface = CorePrivate.EgsPresenceInterface;
|
||||
var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface;
|
||||
if (presenceInterface is null
|
||||
|| customInvitesInterface is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var boilerplate0 = new Epic.OnlineServices.Presence.AddNotifyJoinGameAcceptedOptions();
|
||||
joinGameAcceptedNotificationId = presenceInterface.AddNotifyJoinGameAccepted(ref boilerplate0, null, OnJoinGameAcceptedEos);
|
||||
|
||||
var boilerplate1 = new Epic.OnlineServices.CustomInvites.AddNotifyCustomInviteAcceptedOptions();
|
||||
inviteAcceptedNotificationId = customInvitesInterface.AddNotifyCustomInviteAccepted(ref boilerplate1, implementation, OnInviteAcceptedEos);
|
||||
|
||||
var boilerplate2 = new Epic.OnlineServices.CustomInvites.AddNotifyCustomInviteRejectedOptions();
|
||||
inviteRejectedNotificationId = customInvitesInterface.AddNotifyCustomInviteRejected(ref boilerplate2, null, OnInviteRejectedEos);
|
||||
|
||||
var boilerplate3 = new Epic.OnlineServices.CustomInvites.AddNotifyCustomInviteReceivedOptions();
|
||||
inviteReceivedNotificationId = customInvitesInterface.AddNotifyCustomInviteReceived(ref boilerplate3, implementation, OnInviteReceivedEos);
|
||||
}
|
||||
|
||||
public static void Quit()
|
||||
{
|
||||
OnJoinGame.Dispose();
|
||||
OnInviteAccepted.Dispose();
|
||||
|
||||
var presenceInterface = CorePrivate.EgsPresenceInterface;
|
||||
var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface;
|
||||
if (presenceInterface is null
|
||||
|| customInvitesInterface is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
static void callRemover(Action<ulong> remover, ref ulong id)
|
||||
{
|
||||
remover(id);
|
||||
id = Epic.OnlineServices.Common.InvalidNotificationid;
|
||||
}
|
||||
|
||||
callRemover(presenceInterface.RemoveNotifyJoinGameAccepted, ref joinGameAcceptedNotificationId);
|
||||
callRemover(customInvitesInterface.RemoveNotifyCustomInviteAccepted, ref inviteAcceptedNotificationId);
|
||||
callRemover(customInvitesInterface.RemoveNotifyCustomInviteRejected, ref inviteRejectedNotificationId);
|
||||
callRemover(customInvitesInterface.RemoveNotifyCustomInviteReceived, ref inviteReceivedNotificationId);
|
||||
}
|
||||
|
||||
private static void OnJoinGameAcceptedEos(ref Epic.OnlineServices.Presence.JoinGameAcceptedCallbackInfo data)
|
||||
{
|
||||
if (data.UiEventId != Epic.OnlineServices.UI.UIInterface.EventidInvalid)
|
||||
{
|
||||
// What is this for? I have no idea.
|
||||
// Documentation says it's important tho:
|
||||
// https://dev.epicgames.com/docs/epic-account-services/social-overlay-overview/sdk-integration#invite-lifecycle-and-caveats
|
||||
var egsUiInterface = CorePrivate.EgsUiInterface;
|
||||
if (egsUiInterface != null)
|
||||
{
|
||||
var ack = new Epic.OnlineServices.UI.AcknowledgeEventIdOptions
|
||||
{
|
||||
UiEventId = data.UiEventId,
|
||||
Result = Epic.OnlineServices.Result.Success
|
||||
};
|
||||
egsUiInterface.AcknowledgeEventId(ref ack);
|
||||
}
|
||||
}
|
||||
|
||||
var selfEpicIdOption = EpicAccountId.Parse(data.LocalUserId.ToString());
|
||||
if (!selfEpicIdOption.TryUnwrap(out var selfEpicId)) { return; }
|
||||
|
||||
var joinCommandStr = data.JoinInfo;
|
||||
|
||||
OnJoinGame.Invoke(new EosInterface.Presence.JoinGameInfo(selfEpicId, joinCommandStr));
|
||||
}
|
||||
|
||||
private static void OnInviteAcceptedEos(ref Epic.OnlineServices.CustomInvites.OnCustomInviteAcceptedCallbackInfo data)
|
||||
{
|
||||
if (data.LocalUserId is null) { return; }
|
||||
if (data.ClientData is not ImplementationPrivate implementation) { return; }
|
||||
|
||||
RemoveInvite(
|
||||
recipientPuid: new EosInterface.ProductUserId(data.LocalUserId.ToString()),
|
||||
senderPuid: new EosInterface.ProductUserId(data.TargetUserId.ToString()));
|
||||
|
||||
var joinCommandStr = data.Payload;
|
||||
|
||||
var selfPuid = new EosInterface.ProductUserId(data.LocalUserId.ToString());
|
||||
|
||||
async Task<Option<EosInterface.Presence.AcceptInviteInfo>> prepareCallbackInfo()
|
||||
{
|
||||
var selfExternalAccountIdsTask = IdQueriesPrivate.GetExternalAccountIds(selfPuid, selfPuid);
|
||||
|
||||
await Task.WhenAll(selfExternalAccountIdsTask, selfExternalAccountIdsTask);
|
||||
|
||||
var selfExternalAccountIdsResult = await selfExternalAccountIdsTask;
|
||||
|
||||
if (!selfExternalAccountIdsResult.TryUnwrapSuccess(out var selfExternalAccountIds)
|
||||
|| !selfExternalAccountIds.OfType<EpicAccountId>().FirstOrNone().TryUnwrap(out var selfEpicAccountId))
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
return Option.Some(new EosInterface.Presence.AcceptInviteInfo(
|
||||
selfEpicAccountId,
|
||||
joinCommandStr));
|
||||
}
|
||||
|
||||
TaskPool.Add(
|
||||
$"AcceptedInviteFor{selfPuid.Value}",
|
||||
implementation.TaskScheduler.Schedule(prepareCallbackInfo),
|
||||
t =>
|
||||
{
|
||||
if (!t.TryGetResult(out Option<EosInterface.Presence.AcceptInviteInfo> infoOption)) { return; }
|
||||
if (!infoOption.TryUnwrap(out var info)) { return; }
|
||||
|
||||
OnInviteAccepted.Invoke(info);
|
||||
});
|
||||
}
|
||||
|
||||
private static void OnInviteRejectedEos(ref Epic.OnlineServices.CustomInvites.CustomInviteRejectedCallbackInfo data)
|
||||
{
|
||||
if (data.LocalUserId is null) { return; }
|
||||
|
||||
RemoveInvite(
|
||||
recipientPuid: new EosInterface.ProductUserId(data.LocalUserId.ToString()),
|
||||
senderPuid: new EosInterface.ProductUserId(data.TargetUserId.ToString()));
|
||||
}
|
||||
|
||||
private readonly record struct InviteId(
|
||||
EpicAccountId RecipientEpicId,
|
||||
EpicAccountId SenderEpicId,
|
||||
EosInterface.ProductUserId RecipientPuid,
|
||||
EosInterface.ProductUserId SenderPuid,
|
||||
string IdValue);
|
||||
|
||||
private static readonly List<InviteId> ReceivedInviteIds = new List<InviteId>();
|
||||
|
||||
private static void RemoveInvite(EpicAccountId recipientEpicId, EpicAccountId senderEpicId)
|
||||
{
|
||||
RemoveInvites(ReceivedInviteIds.Where(id => id.RecipientEpicId == recipientEpicId && id.SenderEpicId == senderEpicId).ToImmutableArray());
|
||||
}
|
||||
|
||||
private static void RemoveInvite(EosInterface.ProductUserId recipientPuid, EosInterface.ProductUserId senderPuid)
|
||||
{
|
||||
RemoveInvites(ReceivedInviteIds.Where(id => id.RecipientPuid == recipientPuid && id.SenderPuid == senderPuid).ToImmutableArray());
|
||||
}
|
||||
|
||||
private static void RemoveInvites(ImmutableArray<InviteId> invites)
|
||||
{
|
||||
var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface;
|
||||
if (customInvitesInterface == null) { return; }
|
||||
|
||||
foreach (var invite in invites)
|
||||
{
|
||||
ReceivedInviteIds.Remove(invite);
|
||||
var targetUserId = Epic.OnlineServices.ProductUserId.FromString(invite.SenderPuid.Value);
|
||||
var localUserId = Epic.OnlineServices.ProductUserId.FromString(invite.RecipientPuid.Value);
|
||||
var finalizeInviteOptions = new Epic.OnlineServices.CustomInvites.FinalizeInviteOptions
|
||||
{
|
||||
TargetUserId = targetUserId,
|
||||
LocalUserId = localUserId,
|
||||
CustomInviteId = invite.IdValue,
|
||||
ProcessingResult = Epic.OnlineServices.Result.Success
|
||||
};
|
||||
customInvitesInterface.FinalizeInvite(ref finalizeInviteOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnInviteReceivedEos(ref Epic.OnlineServices.CustomInvites.OnCustomInviteReceivedCallbackInfo data)
|
||||
{
|
||||
if (data.ClientData is not ImplementationPrivate implementation) { return; }
|
||||
var joinCommandStr = data.Payload;
|
||||
|
||||
var selfPuid = new EosInterface.ProductUserId(data.LocalUserId.ToString());
|
||||
var senderPuid = new EosInterface.ProductUserId(data.TargetUserId.ToString());
|
||||
var inviteIdValue = data.CustomInviteId;
|
||||
|
||||
// We can only have one invite for the same recipient-sender pair
|
||||
RemoveInvite(
|
||||
recipientPuid: selfPuid,
|
||||
senderPuid: senderPuid);
|
||||
|
||||
async Task<Option<EosInterface.Presence.ReceiveInviteInfo>> prepareCallbackInfo()
|
||||
{
|
||||
var selfExternalAccountIdsTask = IdQueriesPrivate.GetExternalAccountIds(selfPuid, selfPuid);
|
||||
var senderExternalAccountIdsTask = IdQueriesPrivate.GetExternalAccountIds(selfPuid, senderPuid);
|
||||
|
||||
await Task.WhenAll(selfExternalAccountIdsTask, selfExternalAccountIdsTask);
|
||||
|
||||
var selfExternalAccountIdsResult = await selfExternalAccountIdsTask;
|
||||
var senderExternalAccountIdsResult = await senderExternalAccountIdsTask;
|
||||
|
||||
if (!selfExternalAccountIdsResult.TryUnwrapSuccess(out var selfExternalAccountIds)
|
||||
|| !selfExternalAccountIds.OfType<EpicAccountId>().FirstOrNone().TryUnwrap(out var selfEpicAccountId))
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
if (!senderExternalAccountIdsResult.TryUnwrapSuccess(out var senderExternalAccountIds)
|
||||
|| !senderExternalAccountIds.OfType<EpicAccountId>().FirstOrNone().TryUnwrap(out var senderEpicAccountId))
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
return Option.Some(new EosInterface.Presence.ReceiveInviteInfo(
|
||||
selfEpicAccountId,
|
||||
senderEpicAccountId,
|
||||
joinCommandStr));
|
||||
}
|
||||
|
||||
TaskPool.Add(
|
||||
$"ReceivedInviteFrom{senderPuid.Value}",
|
||||
implementation.TaskScheduler.Schedule(prepareCallbackInfo),
|
||||
t =>
|
||||
{
|
||||
if (!t.TryGetResult(out Option<EosInterface.Presence.ReceiveInviteInfo> infoOption)) { return; }
|
||||
|
||||
if (!infoOption.TryUnwrap(out var info)) { return; }
|
||||
|
||||
ReceivedInviteIds.Add(new InviteId(
|
||||
RecipientEpicId: info.RecipientId,
|
||||
SenderEpicId: info.SenderId,
|
||||
RecipientPuid: selfPuid,
|
||||
SenderPuid: senderPuid,
|
||||
IdValue: inviteIdValue));
|
||||
|
||||
OnInviteReceived.Invoke(info);
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Presence.SetJoinCommandError>> SetJoinCommand(
|
||||
EpicAccountId epicAccountId,
|
||||
string desc,
|
||||
string serverName,
|
||||
string joinCommand)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(joinCommand))
|
||||
{
|
||||
desc = "";
|
||||
}
|
||||
|
||||
if (desc.Length > Epic.OnlineServices.Presence.PresenceInterface.RichTextMaxValueLength)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.DescTooLong);
|
||||
}
|
||||
if (joinCommand.Length > Epic.OnlineServices.Presence.PresenceModification.PresencemodificationJoininfoMaxLength)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.JoinCommandTooLong);
|
||||
}
|
||||
|
||||
if (serverName.Length > Epic.OnlineServices.Presence.PresenceInterface.DataMaxValueLength)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.ServerNameTooLong);
|
||||
}
|
||||
if (joinCommand.Length > Epic.OnlineServices.Presence.PresenceInterface.DataMaxValueLength)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.JoinCommandTooLong);
|
||||
}
|
||||
|
||||
using var janitor = Janitor.Start();
|
||||
|
||||
var presenceInterface = CorePrivate.EgsPresenceInterface;
|
||||
var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface;
|
||||
if (presenceInterface is null
|
||||
|| customInvitesInterface is null)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.EosNotInitialized);
|
||||
}
|
||||
|
||||
var epicAccountIdInternal = Epic.OnlineServices.EpicAccountId.FromString(epicAccountId.EosStringRepresentation);
|
||||
|
||||
var puidResult = await IdQueriesPrivate.GetPuidForExternalId(epicAccountId);
|
||||
if (!puidResult.TryUnwrapSuccess(out var puid))
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToGetPuid);
|
||||
}
|
||||
|
||||
var puidInternal = Epic.OnlineServices.ProductUserId.FromString(puid.Value);
|
||||
|
||||
var setCustomInviteOptions = new Epic.OnlineServices.CustomInvites.SetCustomInviteOptions
|
||||
{
|
||||
LocalUserId = puidInternal,
|
||||
Payload = joinCommand
|
||||
};
|
||||
var setCustomInviteResult = customInvitesInterface.SetCustomInvite(ref setCustomInviteOptions);
|
||||
if (setCustomInviteResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetCustomInvite);
|
||||
}
|
||||
|
||||
var createPresenceModificationOptions = new Epic.OnlineServices.Presence.CreatePresenceModificationOptions
|
||||
{
|
||||
LocalUserId = epicAccountIdInternal
|
||||
};
|
||||
var createPresenceModificationResult = presenceInterface.CreatePresenceModification(ref createPresenceModificationOptions, out var presenceModification);
|
||||
janitor.AddAction(presenceModification.Release);
|
||||
if (createPresenceModificationResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToCreatePresenceModification);
|
||||
}
|
||||
|
||||
var setRichTextOptions = new Epic.OnlineServices.Presence.PresenceModificationSetRawRichTextOptions
|
||||
{
|
||||
RichText = desc
|
||||
};
|
||||
var setRichTextResult = presenceModification.SetRawRichText(ref setRichTextOptions);
|
||||
if (setRichTextResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetRichText);
|
||||
}
|
||||
|
||||
var setDataOptions = new Epic.OnlineServices.Presence.PresenceModificationSetDataOptions
|
||||
{
|
||||
Records = new[]
|
||||
{
|
||||
new Epic.OnlineServices.Presence.DataRecord
|
||||
{
|
||||
Key = "servername",
|
||||
Value = serverName
|
||||
},
|
||||
new Epic.OnlineServices.Presence.DataRecord
|
||||
{
|
||||
Key = "connectcommand",
|
||||
Value = joinCommand
|
||||
}
|
||||
}
|
||||
};
|
||||
var setDataResult = presenceModification.SetData(ref setDataOptions);
|
||||
if (setDataResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetRecords);
|
||||
}
|
||||
|
||||
// This is necessary to make the SDK not choke if given an empty, but not null, joinCommand
|
||||
string? joinCommandNullable = string.IsNullOrWhiteSpace(joinCommand) ? null : joinCommand;
|
||||
|
||||
var setJoinInfoOptions = new Epic.OnlineServices.Presence.PresenceModificationSetJoinInfoOptions
|
||||
{
|
||||
JoinInfo = joinCommandNullable
|
||||
};
|
||||
var setJoinInfoResult = presenceModification.SetJoinInfo(ref setJoinInfoOptions);
|
||||
if (setJoinInfoResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetJoinInfo);
|
||||
}
|
||||
|
||||
var setPresenceOptions = new Epic.OnlineServices.Presence.SetPresenceOptions
|
||||
{
|
||||
LocalUserId = epicAccountIdInternal,
|
||||
PresenceModificationHandle = presenceModification
|
||||
};
|
||||
var setPresenceWaiter = new CallbackWaiter<Epic.OnlineServices.Presence.SetPresenceCallbackInfo>();
|
||||
presenceInterface.SetPresence(options: ref setPresenceOptions, clientData: null, completionDelegate: setPresenceWaiter.OnCompletion);
|
||||
var setPresenceResultOption = await setPresenceWaiter.Task;
|
||||
if (!setPresenceResultOption.TryUnwrap(out var setPresenceResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.SetPresenceTimedOut);
|
||||
}
|
||||
|
||||
if (setPresenceResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetPresence);
|
||||
}
|
||||
|
||||
return Result.Success(Unit.Value);
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Presence.SendInviteError>> SendInvite(EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId)
|
||||
{
|
||||
var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface;
|
||||
if (customInvitesInterface is null)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SendInviteError.EosNotInitialized);
|
||||
}
|
||||
|
||||
var selfPuidResult = await IdQueriesPrivate.GetPuidForExternalId(selfEpicAccountId);
|
||||
if (!selfPuidResult.TryUnwrapSuccess(out var selfPuid))
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SendInviteError.FailedToGetSelfPuid);
|
||||
}
|
||||
|
||||
var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value);
|
||||
|
||||
var remotePuidResult = await IdQueriesPrivate.GetPuidForExternalId(remoteEpicAccountId);
|
||||
if (!remotePuidResult.TryUnwrapSuccess(out var remotePuid))
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SendInviteError.FailedToGetRemotePuid);
|
||||
}
|
||||
|
||||
var remotePuidInternal = Epic.OnlineServices.ProductUserId.FromString(remotePuid.Value);
|
||||
|
||||
var sendCustomInviteOptions = new Epic.OnlineServices.CustomInvites.SendCustomInviteOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
TargetUserIds = new[]
|
||||
{
|
||||
remotePuidInternal
|
||||
}
|
||||
};
|
||||
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.CustomInvites.SendCustomInviteCallbackInfo>();
|
||||
customInvitesInterface.SendCustomInvite(options: ref sendCustomInviteOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
|
||||
var callbackResultOption = await callbackWaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SendInviteError.TimedOut);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Presence.SendInviteError.InternalError);
|
||||
}
|
||||
|
||||
return Result.Success(Unit.Value);
|
||||
}
|
||||
|
||||
public static void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId)
|
||||
{
|
||||
RemoveInvite(
|
||||
recipientEpicId: selfEpicAccountId,
|
||||
senderEpicId: senderEpicAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override NamedEvent<EosInterface.Presence.JoinGameInfo> OnJoinGame => PresencePrivate.OnJoinGame;
|
||||
public override NamedEvent<EosInterface.Presence.AcceptInviteInfo> OnInviteAccepted => PresencePrivate.OnInviteAccepted;
|
||||
public override NamedEvent<EosInterface.Presence.ReceiveInviteInfo> OnInviteReceived => PresencePrivate.OnInviteReceived;
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Presence.SetJoinCommandError>> SetJoinCommand(
|
||||
EpicAccountId epicAccountId, string desc, string serverName, string joinCommand)
|
||||
=> TaskScheduler.Schedule(() => PresencePrivate.SetJoinCommand(epicAccountId, desc, serverName, joinCommand));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Presence.SendInviteError>> SendInvite(
|
||||
EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId)
|
||||
=> TaskScheduler.Schedule(() => PresencePrivate.SendInvite(selfEpicAccountId, remoteEpicAccountId));
|
||||
|
||||
public override void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId)
|
||||
=> PresencePrivate.DeclineInvite(selfEpicAccountId, senderEpicAccountId);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
public sealed class EgsIdTokenPrivate : EosInterface.EgsIdToken
|
||||
{
|
||||
public override EpicAccountId AccountId { get; }
|
||||
|
||||
internal static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
IncludeFields = true
|
||||
};
|
||||
|
||||
internal readonly record struct TokenStruct(
|
||||
string AccountId,
|
||||
string JsonWebToken);
|
||||
|
||||
internal readonly Epic.OnlineServices.Auth.IdToken InternalToken;
|
||||
internal EgsIdTokenPrivate(EpicAccountId accountId, Epic.OnlineServices.Auth.IdToken internalToken)
|
||||
{
|
||||
AccountId = accountId;
|
||||
InternalToken = internalToken;
|
||||
}
|
||||
|
||||
public new static Option<EgsIdTokenPrivate> Parse(string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (JsonSerializer.Deserialize(
|
||||
str,
|
||||
returnType: typeof(TokenStruct),
|
||||
options: JsonSerializerOptions)
|
||||
is not TokenStruct tokenStruct)
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
if (!EpicAccountId.Parse(tokenStruct.AccountId).TryUnwrap(out var accountId)) { return Option.None; }
|
||||
|
||||
var internalToken = new Epic.OnlineServices.Auth.IdToken
|
||||
{
|
||||
AccountId = Epic.OnlineServices.EpicAccountId.FromString(tokenStruct.AccountId),
|
||||
JsonWebToken = tokenStruct.JsonWebToken
|
||||
};
|
||||
|
||||
return Option.Some(new EgsIdTokenPrivate(accountId, internalToken));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var tokenStruct = new TokenStruct(
|
||||
AccountId: InternalToken.AccountId.ToString(),
|
||||
JsonWebToken: InternalToken.JsonWebToken);
|
||||
return JsonSerializer.Serialize(tokenStruct, options: JsonSerializerOptions);
|
||||
}
|
||||
|
||||
public static Result<EosInterface.EgsIdToken, EosInterface.GetEgsSelfIdTokenError> GetEgsIdTokenForEpicAccountId(EpicAccountId accountId)
|
||||
{
|
||||
var (success, failure) = Result<EosInterface.EgsIdToken, EosInterface.GetEgsSelfIdTokenError>.GetFactoryMethods();
|
||||
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return failure(EosInterface.GetEgsSelfIdTokenError.EosNotInitialized); }
|
||||
|
||||
var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions
|
||||
{
|
||||
AccountId = Epic.OnlineServices.EpicAccountId.FromString(accountId.EosStringRepresentation)
|
||||
};
|
||||
var copyIdTokenResult = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var idTokenNullable);
|
||||
|
||||
if (copyIdTokenResult is Epic.OnlineServices.Result.NotFound) { return failure(EosInterface.GetEgsSelfIdTokenError.InvalidToken); }
|
||||
if (copyIdTokenResult != Epic.OnlineServices.Result.Success) { return failure(EosInterface.GetEgsSelfIdTokenError.UnhandledErrorCondition); }
|
||||
if (idTokenNullable is not { } idToken) { return failure(EosInterface.GetEgsSelfIdTokenError.UnhandledErrorCondition); }
|
||||
|
||||
return success(new EgsIdTokenPrivate(accountId, idToken));
|
||||
}
|
||||
|
||||
public override async Task<EosInterface.VerifyEgsIdTokenResult> Verify(AccountId accountId)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return EosInterface.VerifyEgsIdTokenResult.Failed; }
|
||||
|
||||
var verifyIdTokenOptions = new Epic.OnlineServices.Auth.VerifyIdTokenOptions
|
||||
{
|
||||
IdToken = InternalToken
|
||||
};
|
||||
var verifyIdTokenWaiter = new CallbackWaiter<Epic.OnlineServices.Auth.VerifyIdTokenCallbackInfo>();
|
||||
egsAuthInterface.VerifyIdToken(options: ref verifyIdTokenOptions, clientData: null, completionDelegate: verifyIdTokenWaiter.OnCompletion);
|
||||
var result = await verifyIdTokenWaiter.Task;
|
||||
if (!result.TryUnwrap(out var callbackInfo)) { return EosInterface.VerifyEgsIdTokenResult.Failed; }
|
||||
|
||||
if (callbackInfo.ResultCode != Epic.OnlineServices.Result.Success
|
||||
|| callbackInfo.ProductId != CorePrivate.PlatformInterfaceOptions.ProductId)
|
||||
{
|
||||
return EosInterface.VerifyEgsIdTokenResult.Failed;
|
||||
}
|
||||
|
||||
var resultAccountId = IdQueriesPrivate.EosStringToAccountId(callbackInfo.ExternalAccountId, callbackInfo.ExternalAccountIdType);
|
||||
|
||||
return resultAccountId.TryUnwrap(out var resultId) && resultId == accountId
|
||||
? EosInterface.VerifyEgsIdTokenResult.Verified
|
||||
: EosInterface.VerifyEgsIdTokenResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Option<EosInterface.EgsIdToken> ParseEgsIdToken(string str)
|
||||
=> EgsIdTokenPrivate.Parse(str).Select(t => (EosInterface.EgsIdToken)t);
|
||||
|
||||
public override Result<EosInterface.EgsIdToken, EosInterface.GetEgsSelfIdTokenError> GetEgsIdTokenForEpicAccountId(EpicAccountId accountId)
|
||||
=> EgsIdTokenPrivate.GetEgsIdTokenForEpicAccountId(accountId);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class EosIdTokenPrivate
|
||||
{
|
||||
public static Result<EosInterface.EosIdToken, EosInterface.GetEosSelfIdTokenError> GetEosIdTokenForProductUserId(EosInterface.ProductUserId puid)
|
||||
{
|
||||
var (success, failure) = Result<EosInterface.EosIdToken, EosInterface.GetEosSelfIdTokenError>.GetFactoryMethods();
|
||||
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return failure(EosInterface.GetEosSelfIdTokenError.EosNotInitialized); }
|
||||
|
||||
var copyIdTokenOptions = new Epic.OnlineServices.Connect.CopyIdTokenOptions
|
||||
{
|
||||
LocalUserId = Epic.OnlineServices.ProductUserId.FromString(puid.Value)
|
||||
};
|
||||
var copyIdTokenResult = connectInterface.CopyIdToken(ref copyIdTokenOptions, out var idTokenNullable);
|
||||
|
||||
if (copyIdTokenResult is Epic.OnlineServices.Result.NotFound) { return failure(EosInterface.GetEosSelfIdTokenError.InvalidToken); }
|
||||
if (copyIdTokenResult != Epic.OnlineServices.Result.Success) { return failure(EosInterface.GetEosSelfIdTokenError.UnhandledErrorCondition); }
|
||||
if (idTokenNullable is not { } idToken) { return failure(EosInterface.GetEosSelfIdTokenError.UnhandledErrorCondition); }
|
||||
|
||||
if (!JsonWebToken.Parse(idToken.JsonWebToken).TryUnwrap(out var jsonWebToken)) { return failure(EosInterface.GetEosSelfIdTokenError.CouldNotParseJwt); }
|
||||
|
||||
return success(new EosInterface.EosIdToken(new EosInterface.ProductUserId(idToken.ProductUserId.ToString()), jsonWebToken));
|
||||
}
|
||||
|
||||
public static async Task<Result<AccountId, EosInterface.VerifyEosIdTokenError>> Verify(EosInterface.EosIdToken token)
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.VerifyEosIdTokenError.EosNotInitialized); }
|
||||
|
||||
var verifyIdTokenOptions = new Epic.OnlineServices.Connect.VerifyIdTokenOptions
|
||||
{
|
||||
IdToken = new Epic.OnlineServices.Connect.IdToken
|
||||
{
|
||||
ProductUserId = Epic.OnlineServices.ProductUserId.FromString(token.ProductUserId.Value),
|
||||
JsonWebToken = token.JsonWebToken.ToString()
|
||||
}
|
||||
};
|
||||
var verifyIdTokenWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.VerifyIdTokenCallbackInfo>();
|
||||
connectInterface.VerifyIdToken(options: ref verifyIdTokenOptions, clientData: null, completionDelegate: verifyIdTokenWaiter.OnCompletion);
|
||||
var result = await verifyIdTokenWaiter.Task;
|
||||
if (!result.TryUnwrap(out var callbackInfo)) { return Result.Failure(EosInterface.VerifyEosIdTokenError.TimedOut); }
|
||||
|
||||
if (callbackInfo.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.VerifyEosIdTokenError.UnhandledErrorCondition);
|
||||
}
|
||||
|
||||
if (callbackInfo.ProductId != CorePrivate.PlatformInterfaceOptions.ProductId)
|
||||
{
|
||||
return Result.Failure(EosInterface.VerifyEosIdTokenError.ProductIdDidNotMatch);
|
||||
}
|
||||
|
||||
var resultAccountId = IdQueriesPrivate.EosStringToAccountId(callbackInfo.AccountId, callbackInfo.AccountIdType);
|
||||
|
||||
return resultAccountId.TryUnwrap(out var resultId)
|
||||
? Result.Success(resultId)
|
||||
: Result.Failure(EosInterface.VerifyEosIdTokenError.CouldNotParseExternalAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Result<AccountId, EosInterface.VerifyEosIdTokenError>> VerifyEosIdToken(EosInterface.EosIdToken token)
|
||||
=> TaskScheduler.Schedule(() => EosIdTokenPrivate.Verify(token));
|
||||
|
||||
public override Result<EosInterface.EosIdToken, EosInterface.GetEosSelfIdTokenError> GetEosIdTokenForProductUserId(EosInterface.ProductUserId puid)
|
||||
=> EosIdTokenPrivate.GetEosIdTokenForProductUserId(puid);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class IdQueriesPrivate
|
||||
{
|
||||
public static ImmutableArray<EosInterface.ProductUserId> GetLoggedInPuids()
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return ImmutableArray<EosInterface.ProductUserId>.Empty; }
|
||||
|
||||
int count = connectInterface.GetLoggedInUsersCount();
|
||||
var ids = new List<EosInterface.ProductUserId>();
|
||||
foreach (int i in Enumerable.Range(0, count))
|
||||
{
|
||||
if (connectInterface.GetLoggedInUserByIndex(i) is not { } userId) { return ImmutableArray<EosInterface.ProductUserId>.Empty; }
|
||||
var newPuid = new EosInterface.ProductUserId(userId.ToString());
|
||||
if (!LoginPrivate.PuidToPrimaryExternalId.ContainsKey(newPuid)) { continue; }
|
||||
ids.Add(newPuid);
|
||||
}
|
||||
|
||||
return ids.ToImmutableArray();
|
||||
}
|
||||
|
||||
public static ImmutableArray<EpicAccountId> GetLoggedInEpicIds()
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return ImmutableArray<EpicAccountId>.Empty; }
|
||||
|
||||
int count = egsAuthInterface.GetLoggedInAccountsCount();
|
||||
var ids = new List<EpicAccountId>();
|
||||
foreach (int i in Enumerable.Range(0, count))
|
||||
{
|
||||
if (egsAuthInterface.GetLoggedInAccountByIndex(i) is not { } userId) { return ImmutableArray<EpicAccountId>.Empty; }
|
||||
var newEpicIdOption = EpicAccountId.Parse(userId.ToString());
|
||||
if (!newEpicIdOption.TryUnwrap(out var newEpicId)) { return ImmutableArray<EpicAccountId>.Empty; }
|
||||
ids.Add(newEpicId);
|
||||
}
|
||||
|
||||
return ids.ToImmutableArray();
|
||||
}
|
||||
|
||||
public static Task<Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError>>
|
||||
GetSelfExternalAccountIds(
|
||||
EosInterface.ProductUserId productUserId)
|
||||
=> GetExternalAccountIds(productUserId, productUserId);
|
||||
|
||||
internal static async Task<Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError>>
|
||||
GetExternalAccountIds(
|
||||
EosInterface.ProductUserId selfPuid,
|
||||
EosInterface.ProductUserId puidToGetIdsFor)
|
||||
{
|
||||
// If logged only into an Epic account, you cannot fetch SteamIDs.
|
||||
// See Epic.OnlineServices.Connect.ExternalAccountInfo.AccountId
|
||||
|
||||
var (success, failure) = Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError>.GetFactoryMethods();
|
||||
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface)
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.EosNotInitialized);
|
||||
}
|
||||
if (!LoginPrivate.PuidToPrimaryExternalId.ContainsKey(selfPuid))
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.Inaccessible);
|
||||
}
|
||||
|
||||
var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value);
|
||||
var otherPuidInternal = Epic.OnlineServices.ProductUserId.FromString(puidToGetIdsFor.Value);
|
||||
|
||||
var queryProductUserIdMappingsOptions = new Epic.OnlineServices.Connect.QueryProductUserIdMappingsOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
ProductUserIds = new[] { otherPuidInternal }
|
||||
};
|
||||
|
||||
var queryWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.QueryProductUserIdMappingsCallbackInfo>();
|
||||
connectInterface.QueryProductUserIdMappings(options: ref queryProductUserIdMappingsOptions, clientData: null, completionDelegate: queryWaiter.OnCompletion);
|
||||
var queryResultOption = await queryWaiter.Task;
|
||||
if (!queryResultOption.TryUnwrap(out var queryResult))
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.Timeout);
|
||||
}
|
||||
|
||||
if (queryResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return failure(queryResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.NotFound => EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser,
|
||||
Epic.OnlineServices.Result.InvalidUser => EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser,
|
||||
var unhandled => unhandled.FailAndLogUnhandledError(EosInterface.IdQueries.GetSelfExternalIdError.UnhandledErrorCondition)
|
||||
});
|
||||
}
|
||||
|
||||
var getProductUserExternalAccountCountOptions = new Epic.OnlineServices.Connect.GetProductUserExternalAccountCountOptions
|
||||
{
|
||||
TargetUserId = otherPuidInternal
|
||||
};
|
||||
|
||||
uint count = connectInterface.GetProductUserExternalAccountCount(ref getProductUserExternalAccountCountOptions);
|
||||
var accountIds = new AccountId[count];
|
||||
|
||||
foreach (int i in Enumerable.Range(0, (int)count))
|
||||
{
|
||||
var copyProductUserExternalAccountByIndexOptions = new Epic.OnlineServices.Connect.CopyProductUserExternalAccountByIndexOptions
|
||||
{
|
||||
TargetUserId = otherPuidInternal,
|
||||
ExternalAccountInfoIndex = (uint)i
|
||||
};
|
||||
|
||||
connectInterface.CopyProductUserExternalAccountByIndex(
|
||||
ref copyProductUserExternalAccountByIndexOptions,
|
||||
out var externalAccountInfoNullable);
|
||||
if (!externalAccountInfoNullable.TryGetValue(out var externalAccountInfo))
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser);
|
||||
}
|
||||
|
||||
var accountIdOption =
|
||||
EosStringToAccountId(externalAccountInfo.AccountId, externalAccountInfo.AccountIdType);
|
||||
if (!accountIdOption.TryUnwrap(out var accountId))
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.ParseError);
|
||||
}
|
||||
|
||||
accountIds[i] = accountId;
|
||||
}
|
||||
|
||||
return success(accountIds.ToImmutableArray());
|
||||
}
|
||||
|
||||
internal static async Task<Result<EosInterface.ProductUserId, Epic.OnlineServices.Result>> GetPuidForExternalId(AccountId externalId)
|
||||
{
|
||||
var connectInterface = CorePrivate.ConnectInterface;
|
||||
if (connectInterface is null)
|
||||
{
|
||||
return Result.Failure(Epic.OnlineServices.Result.NotConfigured);
|
||||
}
|
||||
|
||||
var externalAccountType = externalId is EpicAccountId
|
||||
? Epic.OnlineServices.ExternalAccountType.Epic
|
||||
: Epic.OnlineServices.ExternalAccountType.Steam;
|
||||
string externalAccountEosRepresentation = externalId.EosStringRepresentation;
|
||||
|
||||
Result<EosInterface.ProductUserId, Epic.OnlineServices.Result> lastError
|
||||
= Result.Failure(Epic.OnlineServices.Result.UnexpectedError);
|
||||
foreach (var selfPuid in GetLoggedInPuids()
|
||||
.OrderByDescending(id => LoginPrivate.PuidToPrimaryExternalId[id].GetType() == externalId.GetType()))
|
||||
{
|
||||
var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value);
|
||||
|
||||
// See https://dev.epicgames.com/docs/en-US/api-ref/functions/eos-connect-query-external-account-mappings
|
||||
// to learn why we need to call this function before we call GetExternalAccountMapping
|
||||
var queryExternalAccountMappingsOptions = new Epic.OnlineServices.Connect.QueryExternalAccountMappingsOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
AccountIdType = externalAccountType,
|
||||
ExternalAccountIds = new Epic.OnlineServices.Utf8String[]
|
||||
{
|
||||
externalAccountEosRepresentation
|
||||
}
|
||||
};
|
||||
|
||||
var queryExternalAccountMappingsWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.QueryExternalAccountMappingsCallbackInfo>();
|
||||
connectInterface.QueryExternalAccountMappings(options: ref queryExternalAccountMappingsOptions, clientData: null, completionDelegate: queryExternalAccountMappingsWaiter.OnCompletion);
|
||||
var resultOption = await queryExternalAccountMappingsWaiter.Task;
|
||||
if (!resultOption.TryUnwrap(out var result))
|
||||
{
|
||||
lastError = Result.Failure(Epic.OnlineServices.Result.TimedOut);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
lastError = Result.Failure(result.ResultCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
var getExternalAccountMappingsOptions = new Epic.OnlineServices.Connect.GetExternalAccountMappingsOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
AccountIdType = externalAccountType,
|
||||
TargetExternalUserId = externalAccountEosRepresentation
|
||||
};
|
||||
var otherPuid = connectInterface.GetExternalAccountMapping(ref getExternalAccountMappingsOptions);
|
||||
if (otherPuid is null)
|
||||
{
|
||||
lastError = Result.Failure(Epic.OnlineServices.Result.NotFound);
|
||||
continue;
|
||||
}
|
||||
return Result.Success(new EosInterface.ProductUserId(otherPuid.ToString()));
|
||||
}
|
||||
|
||||
return lastError;
|
||||
}
|
||||
|
||||
public static Option<AccountId> EosStringToAccountId(
|
||||
string stringRepresentation,
|
||||
Epic.OnlineServices.ExternalAccountType accountType)
|
||||
=> accountType switch
|
||||
{
|
||||
Epic.OnlineServices.ExternalAccountType.Steam => SteamId.Parse(stringRepresentation).Select(id => (AccountId)id),
|
||||
Epic.OnlineServices.ExternalAccountType.Epic => EpicAccountId.Parse(stringRepresentation).Select(id => (AccountId)id),
|
||||
_ => Option.None
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override ImmutableArray<EosInterface.ProductUserId> GetLoggedInPuids()
|
||||
=> IdQueriesPrivate.GetLoggedInPuids();
|
||||
|
||||
public override ImmutableArray<EpicAccountId> GetLoggedInEpicIds()
|
||||
=> IdQueriesPrivate.GetLoggedInEpicIds();
|
||||
|
||||
public override Task<Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError>> GetSelfExternalAccountIds(EosInterface.ProductUserId puid)
|
||||
=> TaskScheduler.Schedule(() => IdQueriesPrivate.GetSelfExternalAccountIds(puid));
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Debugging;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class LoginPrivate
|
||||
{
|
||||
private const string EosLoginSteamIdentity = "BarotraumaEosLogin";
|
||||
private static Option<Steamworks.AuthTicketForWebApi> steamworksAuthTicket;
|
||||
|
||||
private static Option<ulong> eosConnectExpirationNotifyId, eosConnectStatusChangedNotifyId;
|
||||
private static Option<ulong> egsAuthExpirationNotifyId;
|
||||
|
||||
internal static void Init()
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return; }
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return; }
|
||||
|
||||
ClearNotificationId(ref egsAuthExpirationNotifyId, egsAuthInterface.RemoveNotifyLoginStatusChanged);
|
||||
var authExpirationOptions = new Epic.OnlineServices.Auth.AddNotifyLoginStatusChangedOptions();
|
||||
ulong authExpirationNotifyId = egsAuthInterface.AddNotifyLoginStatusChanged(ref authExpirationOptions, null, OnEgsAuthStatusChanged);
|
||||
StoreNotificationId(out egsAuthExpirationNotifyId, authExpirationNotifyId);
|
||||
|
||||
ClearNotificationId(ref eosConnectExpirationNotifyId, connectInterface.RemoveNotifyAuthExpiration);
|
||||
var connectExpirationOptions = new Epic.OnlineServices.Connect.AddNotifyAuthExpirationOptions();
|
||||
ulong connectExpirationNotifyId = connectInterface.AddNotifyAuthExpiration(ref connectExpirationOptions, null, OnConnectExpiration);
|
||||
StoreNotificationId(out eosConnectExpirationNotifyId, connectExpirationNotifyId);
|
||||
|
||||
ClearNotificationId(ref eosConnectStatusChangedNotifyId, connectInterface.RemoveNotifyLoginStatusChanged);
|
||||
var addNotifyConnectStatusChangedOptions = new Epic.OnlineServices.Connect.AddNotifyLoginStatusChangedOptions();
|
||||
var connectChangedNotifyId = connectInterface.AddNotifyLoginStatusChanged(ref addNotifyConnectStatusChangedOptions, null, OnConnectStatusChanged);
|
||||
StoreNotificationId(out eosConnectStatusChangedNotifyId, connectChangedNotifyId);
|
||||
|
||||
static void ClearNotificationId(ref Option<ulong> field, Action<ulong> clearAction)
|
||||
{
|
||||
if (field.TryUnwrap(out var notificationId))
|
||||
{
|
||||
clearAction(notificationId);
|
||||
}
|
||||
field = Option.None;
|
||||
}
|
||||
|
||||
static void StoreNotificationId(out Option<ulong> field, ulong value)
|
||||
{
|
||||
bool isValid = value is not Epic.OnlineServices.Common.InvalidNotificationid;
|
||||
field = isValid
|
||||
? Option.Some(value)
|
||||
: Option.None;
|
||||
}
|
||||
}
|
||||
|
||||
internal static readonly ConcurrentDictionary<EosInterface.ProductUserId, AccountId> PuidToPrimaryExternalId = new();
|
||||
|
||||
private readonly record struct LoginParams(
|
||||
Epic.OnlineServices.Connect.Credentials Credentials,
|
||||
AccountId ExternalAccountId);
|
||||
|
||||
private static async Task<Result<LoginParams, EosInterface.Login.LoginError>> GenCredentialsSteam()
|
||||
{
|
||||
if (!Steamworks.SteamClient.IsValid || !Steamworks.SteamClient.IsLoggedOn) { return Result.Failure(EosInterface.Login.LoginError.SteamNotLoggedIn); }
|
||||
if (steamworksAuthTicket.TryUnwrap(out var oldTicket)) { oldTicket.Cancel(); }
|
||||
var newTicketNullable = await Steamworks.SteamUser.GetAuthTicketForWebApi(EosLoginSteamIdentity);
|
||||
if (newTicketNullable is not { Data: not null } ticket)
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.FailedToGetSteamSessionTicket);
|
||||
}
|
||||
return Result.Success(
|
||||
new LoginParams(
|
||||
Credentials: new Epic.OnlineServices.Connect.Credentials
|
||||
{
|
||||
Token = ToolBoxCore.ByteArrayToHexString(ticket.Data),
|
||||
Type = Epic.OnlineServices.ExternalCredentialType.SteamSessionTicket
|
||||
},
|
||||
ExternalAccountId: new SteamId(Steamworks.SteamClient.SteamId)));
|
||||
}
|
||||
|
||||
private static async Task<Result<Either<LoginParams, Epic.OnlineServices.ContinuanceToken>, EosInterface.Login.LoginError>> GenCredentialsEpic(
|
||||
Epic.OnlineServices.Auth.LoginCredentialType credentialsType,
|
||||
string? credentialsId,
|
||||
string? credentialsToken,
|
||||
Epic.OnlineServices.ExternalCredentialType credentialsExternalType,
|
||||
EosInterface.Login.LoginEpicFlags flags)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LoginError.EosNotInitialized); }
|
||||
|
||||
if (credentialsType is not (
|
||||
Epic.OnlineServices.Auth.LoginCredentialType.ExternalAuth
|
||||
or Epic.OnlineServices.Auth.LoginCredentialType.Developer
|
||||
or Epic.OnlineServices.Auth.LoginCredentialType.ExchangeCode))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.InvalidUser);
|
||||
}
|
||||
|
||||
var authLoginOptions = new Epic.OnlineServices.Auth.LoginOptions
|
||||
{
|
||||
Credentials = new Epic.OnlineServices.Auth.Credentials
|
||||
{
|
||||
Id = credentialsId,
|
||||
Token = credentialsToken,
|
||||
Type = credentialsType,
|
||||
SystemAuthCredentialsOptions = default,
|
||||
ExternalType = credentialsExternalType
|
||||
},
|
||||
ScopeFlags =
|
||||
Epic.OnlineServices.Auth.AuthScopeFlags.BasicProfile
|
||||
| Epic.OnlineServices.Auth.AuthScopeFlags.Presence
|
||||
| Epic.OnlineServices.Auth.AuthScopeFlags.FriendsList,
|
||||
LoginFlags = flags.HasFlag(EosInterface.Login.LoginEpicFlags.FailWithoutOpeningBrowser)
|
||||
? Epic.OnlineServices.Auth.LoginFlags.NoUserInterface
|
||||
: Epic.OnlineServices.Auth.LoginFlags.None
|
||||
};
|
||||
|
||||
var authLoginWaiter = new CallbackWaiter<Epic.OnlineServices.Auth.LoginCallbackInfo>();
|
||||
egsAuthInterface.Login(options: ref authLoginOptions, clientData: null, completionDelegate: authLoginWaiter.OnCompletion);
|
||||
|
||||
// This can time out if authLoginOptions.ScopeFlags is set incorrectly,
|
||||
// because the docs lied and this callback isn't guaranteed to be called
|
||||
var authLoginCallbackInfoOption = await authLoginWaiter.Task;
|
||||
|
||||
if (!authLoginCallbackInfoOption.TryUnwrap(out var authLoginCallbackInfo))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.EgsLoginTimeout);
|
||||
}
|
||||
if (authLoginCallbackInfo is { ResultCode: Epic.OnlineServices.Result.InvalidUser, ContinuanceToken: { } continuanceToken })
|
||||
{
|
||||
return Result.Success((Either<LoginParams, Epic.OnlineServices.ContinuanceToken>)continuanceToken);
|
||||
}
|
||||
|
||||
if (authLoginCallbackInfo.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(authLoginCallbackInfo.ResultCode switch {
|
||||
Epic.OnlineServices.Result.NotFound
|
||||
=> EosInterface.Login.LoginError.EgsAccountNotFound,
|
||||
Epic.OnlineServices.Result.AuthExchangeCodeNotFound
|
||||
=> EosInterface.Login.LoginError.AuthExchangeCodeNotFound,
|
||||
Epic.OnlineServices.Result.AuthUserInterfaceRequired
|
||||
=> EosInterface.Login.LoginError.AuthRequiresOpeningBrowser,
|
||||
Epic.OnlineServices.Result.AccessDenied
|
||||
=> EosInterface.Login.LoginError.EgsAccessDenied,
|
||||
_
|
||||
=> EosInterface.Login.LoginError.UnhandledFailureCondition
|
||||
});
|
||||
}
|
||||
if (!EpicAccountId.Parse(authLoginCallbackInfo.LocalUserId.ToString()).TryUnwrap(out var externalAccountId))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.FailedToParseEgsId);
|
||||
}
|
||||
|
||||
var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions
|
||||
{
|
||||
AccountId = authLoginCallbackInfo.LocalUserId
|
||||
};
|
||||
|
||||
var tokenCopyResult = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var tokenNullable);
|
||||
if (tokenCopyResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
Result.Failure(EosInterface.Login.LoginError.FailedToGetEgsIdToken);
|
||||
}
|
||||
if (tokenNullable is not { } token) { return Result.Failure(EosInterface.Login.LoginError.FailedToGetEgsIdToken); }
|
||||
|
||||
return Result.Success(
|
||||
(Either<LoginParams, Epic.OnlineServices.ContinuanceToken>)new LoginParams(
|
||||
Credentials: new Epic.OnlineServices.Connect.Credentials
|
||||
{
|
||||
Token = token.JsonWebToken,
|
||||
Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken
|
||||
},
|
||||
ExternalAccountId: externalAccountId));
|
||||
}
|
||||
|
||||
public static async Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginSteam()
|
||||
{
|
||||
var credentialsSteamResult = await GenCredentialsSteam();
|
||||
if (credentialsSteamResult.TryUnwrapFailure(out var error))
|
||||
{
|
||||
return Result.Failure(error);
|
||||
}
|
||||
if (!credentialsSteamResult.TryUnwrapSuccess(out var loginParams))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.InvalidUser);
|
||||
}
|
||||
|
||||
var result = await Login(loginParams);
|
||||
if (steamworksAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); }
|
||||
steamworksAuthTicket = Option.None;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<Result<OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags flags)
|
||||
{
|
||||
if (steamworksAuthTicket.TryUnwrap(out var oldTicket)) { oldTicket.Cancel(); }
|
||||
var newTicketNullable = await Steamworks.SteamUser.GetAuthTicketForWebApi(EosLoginSteamIdentity);
|
||||
if (newTicketNullable is not { Data: not null } ticket)
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.FailedToGetSteamSessionTicket);
|
||||
}
|
||||
var epicCredentialsOption = await GenCredentialsEpic(
|
||||
credentialsType: Epic.OnlineServices.Auth.LoginCredentialType.ExternalAuth,
|
||||
credentialsId: null,
|
||||
credentialsToken: ToolBoxCore.ByteArrayToHexString(ticket.Data),
|
||||
credentialsExternalType: Epic.OnlineServices.ExternalCredentialType.SteamSessionTicket,
|
||||
flags: flags);
|
||||
if (epicCredentialsOption.TryUnwrapFailure(out var epicCredentialsFail))
|
||||
{
|
||||
return Result.Failure(epicCredentialsFail);
|
||||
}
|
||||
if (!epicCredentialsOption.TryUnwrapSuccess(out var loginParamsOrContinuanceToken))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition);
|
||||
}
|
||||
|
||||
if (loginParamsOrContinuanceToken.TryGet(out Epic.OnlineServices.ContinuanceToken continuanceToken))
|
||||
{
|
||||
return Result.Success((OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>)
|
||||
new EosInterface.EgsAuthContinuanceToken(continuanceToken.InnerHandle, ExtractExpiryTimeFromContinuanceToken(continuanceToken, EosInterface.EgsAuthContinuanceToken.Duration)));
|
||||
}
|
||||
if (!loginParamsOrContinuanceToken.TryGet(out LoginParams loginParams))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.UnexpectedContinuanceToken);
|
||||
}
|
||||
|
||||
var loginResult = await Login(loginParams);
|
||||
if (loginResult.TryUnwrapSuccess(out var loginSuccess))
|
||||
{
|
||||
return loginSuccess.TryGet(out EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
? Result.Success((OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>)eosContinuanceToken)
|
||||
: loginSuccess.TryGet(out EosInterface.ProductUserId puid)
|
||||
? Result.Success((OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>)puid)
|
||||
: Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition);
|
||||
}
|
||||
return loginResult.TryUnwrapFailure(out var loginFailure)
|
||||
? Result.Failure(loginFailure)
|
||||
: Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition);
|
||||
}
|
||||
|
||||
public static async Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicExchangeCode(string exchangeCode)
|
||||
{
|
||||
var epicCredentialsOption = await GenCredentialsEpic(
|
||||
credentialsType: Epic.OnlineServices.Auth.LoginCredentialType.ExchangeCode,
|
||||
credentialsId: "",
|
||||
credentialsToken: exchangeCode,
|
||||
credentialsExternalType: Epic.OnlineServices.ExternalCredentialType.Epic,
|
||||
flags: EosInterface.Login.LoginEpicFlags.None);
|
||||
if (epicCredentialsOption.TryUnwrapFailure(out var epicCredentialsFail))
|
||||
{
|
||||
return Result.Failure(epicCredentialsFail);
|
||||
}
|
||||
if (!epicCredentialsOption.TryUnwrapSuccess(out var loginParamsOrContinuanceToken))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition);
|
||||
}
|
||||
if (!loginParamsOrContinuanceToken.TryGet(out LoginParams loginParams))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.UnexpectedContinuanceToken);
|
||||
}
|
||||
|
||||
var result = await Login(loginParams);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicIdToken(EosInterface.EgsIdToken egsIdToken)
|
||||
{
|
||||
if (egsIdToken is not EgsIdTokenPrivate privateEgsIdToken) { return Result.Failure(EosInterface.Login.LoginError.InvalidUser); }
|
||||
var credentials = new Epic.OnlineServices.Connect.Credentials
|
||||
{
|
||||
Token = privateEgsIdToken.InternalToken.JsonWebToken,
|
||||
Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken
|
||||
};
|
||||
|
||||
return await Login(new LoginParams(credentials, privateEgsIdToken.AccountId));
|
||||
}
|
||||
|
||||
private static DateTime ExtractExpiryTimeFromContinuanceToken(Epic.OnlineServices.ContinuanceToken continuanceToken, TimeSpan fallbackDuration)
|
||||
{
|
||||
// Not the exact expiry time, but it's a pretty close guess should we fail to decode the continuance token
|
||||
var expiryTime = DateTime.Now + fallbackDuration;
|
||||
|
||||
// This method exists to replace Epic.OnlineServices.ContinuanceToken.ToString because
|
||||
// the generated code is broken, and I don't want to modify it because we risk undoing
|
||||
// a fix when we update the SDK.
|
||||
static string continuanceTokenToString(Epic.OnlineServices.ContinuanceToken continuanceToken)
|
||||
{
|
||||
int inOutBufferLength = 1024;
|
||||
System.IntPtr outBufferAddress = Epic.OnlineServices.Helper.AddAllocation(inOutBufferLength);
|
||||
|
||||
var funcResult = Epic.OnlineServices.Bindings.EOS_ContinuanceToken_ToString(continuanceToken.InnerHandle, outBufferAddress, ref inOutBufferLength);
|
||||
if (funcResult == Epic.OnlineServices.Result.LimitExceeded)
|
||||
{
|
||||
// Buffer wasn't large enough to copy the string.
|
||||
// inOutBufferLength was updated by the last call to be the actual length required.
|
||||
// Generate a new buffer and try again.
|
||||
Epic.OnlineServices.Helper.Dispose(ref outBufferAddress);
|
||||
outBufferAddress = Epic.OnlineServices.Helper.AddAllocation(inOutBufferLength);
|
||||
funcResult = Epic.OnlineServices.Bindings.EOS_ContinuanceToken_ToString(continuanceToken.InnerHandle, outBufferAddress, ref inOutBufferLength);
|
||||
if (funcResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
DebugConsoleCore.Log($"EOS_ContinuanceToken_ToString failed with result {funcResult}");
|
||||
}
|
||||
}
|
||||
|
||||
Epic.OnlineServices.Utf8String outBuffer = "EOS_ContinuanceToken_ToString failed";
|
||||
if (funcResult == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
Epic.OnlineServices.Helper.Get(outBufferAddress, out outBuffer);
|
||||
}
|
||||
Epic.OnlineServices.Helper.Dispose(ref outBufferAddress);
|
||||
|
||||
return outBuffer;
|
||||
}
|
||||
|
||||
var ctDecode = JsonWebToken.Parse(continuanceTokenToString(continuanceToken));
|
||||
if (ctDecode.TryUnwrap(out var jwt))
|
||||
{
|
||||
string decodedPayload = jwt.PayloadDecoded;
|
||||
try
|
||||
{
|
||||
// Ugly regex hack to get expiry time. The right thing to do would be to parse the payload as JSON,
|
||||
// but I don't really care because we're extracting one field out of this whole thing.
|
||||
string expiryTimeUnix = Regex.Match(decodedPayload, @"""exp""\s*:\s*([0-9]+)").Groups[1].Value;
|
||||
expiryTime = UnixTime.ParseUtc(expiryTimeUnix).Fallback(UnixTime.UtcEpoch).ToLocalTime();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// could not extract expiry time, oh well!
|
||||
}
|
||||
}
|
||||
|
||||
return expiryTime;
|
||||
}
|
||||
|
||||
private static async Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> Login(LoginParams loginParams)
|
||||
{
|
||||
static Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError> success(EosInterface.ProductUserId id)
|
||||
=> Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>.Success(id);
|
||||
static Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError> continuance(EosInterface.EosConnectContinuanceToken token)
|
||||
=> Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>.Success(token);
|
||||
static Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError> failure(EosInterface.Login.LoginError error)
|
||||
=> Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>.Failure(error);
|
||||
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return failure(EosInterface.Login.LoginError.EosNotInitialized); }
|
||||
|
||||
var loginOptions = new Epic.OnlineServices.Connect.LoginOptions
|
||||
{
|
||||
Credentials = loginParams.Credentials,
|
||||
UserLoginInfo = null
|
||||
};
|
||||
AccountId primaryExternalId = loginParams.ExternalAccountId;
|
||||
|
||||
var loginWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.LoginCallbackInfo>();
|
||||
connectInterface.Login(options: ref loginOptions, clientData: null, completionDelegate: loginWaiter.OnCompletion);
|
||||
var callbackResultOption = await loginWaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return failure(EosInterface.Login.LoginError.Timeout);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
var retVal = new EosInterface.ProductUserId(callbackResult.LocalUserId.ToString());
|
||||
PuidToPrimaryExternalId[retVal] = primaryExternalId;
|
||||
return success(retVal);
|
||||
}
|
||||
|
||||
if (callbackResult is { ResultCode: Epic.OnlineServices.Result.InvalidUser, ContinuanceToken: { } continuanceToken })
|
||||
{
|
||||
var expiryTime = ExtractExpiryTimeFromContinuanceToken(continuanceToken, EosInterface.EosConnectContinuanceToken.Duration);
|
||||
|
||||
return continuance(new EosInterface.EosConnectContinuanceToken(callbackResult.ContinuanceToken.InnerHandle, primaryExternalId, expiryTime));
|
||||
}
|
||||
|
||||
return callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidUser
|
||||
=> failure(EosInterface.Login.LoginError.InvalidUser),
|
||||
Epic.OnlineServices.Result.AccessDenied
|
||||
=> failure(EosInterface.Login.LoginError.EosAccessDenied),
|
||||
var unhandled
|
||||
=> failure(unhandled.FailAndLogUnhandledError(EosInterface.Login.LoginError.UnhandledFailureCondition))
|
||||
};
|
||||
}
|
||||
|
||||
private static void OnEgsAuthStatusChanged(ref Epic.OnlineServices.Auth.LoginStatusChangedCallbackInfo info)
|
||||
{
|
||||
var eaidOption = EpicAccountId.Parse(info.LocalUserId.ToString());
|
||||
if (!eaidOption.TryUnwrap(out var eaid)) { return; }
|
||||
|
||||
if (info.CurrentStatus == Epic.OnlineServices.LoginStatus.NotLoggedIn)
|
||||
{
|
||||
TaskPool.Add(
|
||||
"UnlogPuidLinkedToEaid",
|
||||
IdQueriesPrivate.GetPuidForExternalId(eaid),
|
||||
t =>
|
||||
{
|
||||
if (!t.TryGetResult(out Result<EosInterface.ProductUserId, Epic.OnlineServices.Result>? result)) { return; }
|
||||
if (!result.TryUnwrapSuccess(out var puid)) { return; }
|
||||
|
||||
MarkAsInaccessible(puid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnConnectExpiration(ref Epic.OnlineServices.Connect.AuthExpirationCallbackInfo info)
|
||||
{
|
||||
var puid = new EosInterface.ProductUserId(info.LocalUserId.ToString());
|
||||
DebugConsoleCore.Log($"OnAuthExpirationNotification {puid}");
|
||||
if (!PuidToPrimaryExternalId.TryGetValue(puid, out var externalId)) { return; }
|
||||
|
||||
switch (externalId)
|
||||
{
|
||||
case SteamId:
|
||||
{
|
||||
static async Task RelogSteam()
|
||||
{
|
||||
var steamCredentialsResult = await GenCredentialsSteam();
|
||||
if (!steamCredentialsResult.TryUnwrapSuccess(out var loginParams)) { return; }
|
||||
await Relog(loginParams);
|
||||
}
|
||||
|
||||
TaskPool.Add(
|
||||
"EosReLoginSteam",
|
||||
RelogSteam(),
|
||||
TaskPool.IgnoredCallback);
|
||||
break;
|
||||
}
|
||||
case EpicAccountId epicAccountId:
|
||||
{
|
||||
if (CopyEpicIdToken(epicAccountId).TryUnwrap(out var token))
|
||||
{
|
||||
var epicLoginCredentials = new Epic.OnlineServices.Connect.Credentials
|
||||
{
|
||||
Token = token.JsonWebToken,
|
||||
Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken
|
||||
};
|
||||
var reLogParams = new LoginParams(Credentials: epicLoginCredentials, ExternalAccountId: externalId);
|
||||
TaskPool.Add("OnAuthExpirationNotification", Relog(reLogParams), onCompletion: TaskPool.IgnoredCallback);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task Relog(LoginParams loginParams)
|
||||
{
|
||||
var loginOptions = new Epic.OnlineServices.Connect.LoginOptions
|
||||
{
|
||||
Credentials = loginParams.Credentials,
|
||||
UserLoginInfo = null
|
||||
};
|
||||
|
||||
var connectLoginWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.LoginCallbackInfo>();
|
||||
CorePrivate.ConnectInterface?.Login(options: ref loginOptions, clientData: null, completionDelegate: connectLoginWaiter.OnCompletion);
|
||||
var resultOption = await connectLoginWaiter.Task;
|
||||
if (resultOption.TryUnwrap(out var result))
|
||||
{
|
||||
string s = $"EOS relog result: {result.ResultCode}";
|
||||
if (result.LocalUserId != null)
|
||||
{
|
||||
s += " : " + result.LocalUserId;
|
||||
}
|
||||
|
||||
if (result.ContinuanceToken != null)
|
||||
{
|
||||
s += " ; " + result.ContinuanceToken;
|
||||
}
|
||||
DebugConsoleCore.Log(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsoleCore.Log("EOS relog timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnConnectStatusChanged(ref Epic.OnlineServices.Connect.LoginStatusChangedCallbackInfo info)
|
||||
{
|
||||
var puid = new EosInterface.ProductUserId(info.LocalUserId.ToString());
|
||||
DebugConsoleCore.Log($"OnLoginStatusChangedNotification {puid} {info.CurrentStatus}");
|
||||
if (info.CurrentStatus == Epic.OnlineServices.LoginStatus.NotLoggedIn)
|
||||
{
|
||||
PuidToPrimaryExternalId.TryRemove(puid, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Result<EpicAccountId, EosInterface.Login.LinkExternalAccountToEpicAccountError>> LinkExternalAccountToEpicAccount(EosInterface.EgsAuthContinuanceToken continuanceToken)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.EosNotInitialized); }
|
||||
|
||||
var linkOptions = new Epic.OnlineServices.Auth.LinkAccountOptions
|
||||
{
|
||||
LinkAccountFlags = Epic.OnlineServices.Auth.LinkAccountFlags.NoFlags,
|
||||
ContinuanceToken = new Epic.OnlineServices.ContinuanceToken(continuanceToken.Spend()),
|
||||
LocalUserId = null
|
||||
};
|
||||
|
||||
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.Auth.LinkAccountCallbackInfo>(timeout: TimeSpan.FromMinutes(5));
|
||||
egsAuthInterface.LinkAccount(options: ref linkOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
|
||||
var resultOption = await callbackWaiter.Task;
|
||||
|
||||
if (!resultOption.TryUnwrap(out var result))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.TimedOut);
|
||||
}
|
||||
|
||||
if (result.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
if (!EpicAccountId.Parse(result.SelectedAccountId.ToString()).TryUnwrap(out var epicAccountId))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.FailedToParseEgsAccountId);
|
||||
}
|
||||
return Result.Success(epicAccountId);
|
||||
}
|
||||
return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.UnhandledErrorCondition);
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Login.LogoutEpicAccountError>> LogoutEpicAccount(EpicAccountId egsId)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LogoutEpicAccountError.EosNotInitialized); }
|
||||
|
||||
var logoutOptions = new Epic.OnlineServices.Auth.LogoutOptions
|
||||
{
|
||||
LocalUserId = Epic.OnlineServices.EpicAccountId.FromString(egsId.EosStringRepresentation)
|
||||
};
|
||||
|
||||
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.Auth.LogoutCallbackInfo>();
|
||||
egsAuthInterface.Logout(options: ref logoutOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
|
||||
var logoutResultOption = await callbackWaiter.Task;
|
||||
if (!logoutResultOption.TryUnwrap(out var logoutResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LogoutEpicAccountError.TimedOut);
|
||||
}
|
||||
if (logoutResult.ResultCode == Epic.OnlineServices.Result.Success) { return Result.Success(Unit.Value); }
|
||||
|
||||
return Result.Failure(logoutResult.ResultCode switch
|
||||
{
|
||||
_
|
||||
=> EosInterface.Login.LogoutEpicAccountError.UnhandledErrorCondition
|
||||
});
|
||||
}
|
||||
|
||||
public static void MarkAsInaccessible(EosInterface.ProductUserId puid)
|
||||
{
|
||||
PuidToPrimaryExternalId.TryRemove(puid, out _);
|
||||
}
|
||||
|
||||
private static Option<Epic.OnlineServices.Auth.IdToken> CopyEpicIdToken(EpicAccountId epicAccountId)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Option.None; }
|
||||
|
||||
var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions
|
||||
{
|
||||
AccountId = Epic.OnlineServices.EpicAccountId.FromString(epicAccountId.EosStringRepresentation)
|
||||
};
|
||||
var result = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var tokenNullable);
|
||||
|
||||
if (result is Epic.OnlineServices.Result.Success && tokenNullable is { } token)
|
||||
{
|
||||
return Option.Some(token);
|
||||
}
|
||||
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
public static async Task<Result<EosInterface.ProductUserId, EosInterface.Login.CreateProductAccountError>> CreateProductAccount(EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.CreateProductAccountError.EosNotInitialized); }
|
||||
if (eosContinuanceToken is not { IsValid: true }) { return Result.Failure(EosInterface.Login.CreateProductAccountError.InvalidContinuanceToken); }
|
||||
|
||||
var internalContinuanceToken = new Epic.OnlineServices.ContinuanceToken(eosContinuanceToken.Spend());
|
||||
var options = new Epic.OnlineServices.Connect.CreateUserOptions
|
||||
{
|
||||
ContinuanceToken = internalContinuanceToken
|
||||
};
|
||||
|
||||
var createUserWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.CreateUserCallbackInfo>();
|
||||
connectInterface.CreateUser(options: ref options, clientData: null, completionDelegate: createUserWaiter.OnCompletion);
|
||||
var callbackResultOption = await createUserWaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.CreateProductAccountError.Timeout);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
var retVal = new EosInterface.ProductUserId(callbackResult.LocalUserId.ToString());
|
||||
PuidToPrimaryExternalId[retVal] = eosContinuanceToken.ExternalAccountId;
|
||||
return Result.Success(retVal);
|
||||
}
|
||||
|
||||
return Result.Failure(EosInterface.Login.CreateProductAccountError.UnhandledErrorCondition);
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Login.LinkExternalAccountError>> LinkExternalAccount(EosInterface.ProductUserId puid, EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.LinkExternalAccountError.EosNotInitialized); }
|
||||
if (eosContinuanceToken is not { IsValid: true }) { return Result.Failure(EosInterface.Login.LinkExternalAccountError.InvalidContinuanceToken); }
|
||||
|
||||
var internalContinuanceToken = new Epic.OnlineServices.ContinuanceToken(eosContinuanceToken.Spend());
|
||||
var internalPuid = Epic.OnlineServices.ProductUserId.FromString(puid.Value);
|
||||
var options = new Epic.OnlineServices.Connect.LinkAccountOptions
|
||||
{
|
||||
LocalUserId = internalPuid,
|
||||
ContinuanceToken = internalContinuanceToken
|
||||
};
|
||||
|
||||
var linkAccountAwaiter = new CallbackWaiter<Epic.OnlineServices.Connect.LinkAccountCallbackInfo>();
|
||||
connectInterface.LinkAccount(options: ref options, clientData: null, completionDelegate: linkAccountAwaiter.OnCompletion);
|
||||
var callbackResultOption = await linkAccountAwaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LinkExternalAccountError.Timeout);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Success(Unit.Value);
|
||||
}
|
||||
|
||||
return Result.Failure(callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.ConnectLinkAccountFailed
|
||||
=> EosInterface.Login.LinkExternalAccountError.CannotLink,
|
||||
_
|
||||
=> EosInterface.Login.LinkExternalAccountError.UnhandledErrorCondition
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Login.UnlinkExternalAccountError>> UnlinkExternalAccount(EosInterface.ProductUserId puid)
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.UnlinkExternalAccountError.EosNotInitialized); }
|
||||
|
||||
var internalPuid = Epic.OnlineServices.ProductUserId.FromString(puid.Value);
|
||||
var options = new Epic.OnlineServices.Connect.UnlinkAccountOptions
|
||||
{
|
||||
LocalUserId = internalPuid
|
||||
};
|
||||
|
||||
var unlinkAccountAwaiter = new CallbackWaiter<Epic.OnlineServices.Connect.UnlinkAccountCallbackInfo>();
|
||||
connectInterface.UnlinkAccount(options: ref options, clientData: null, completionDelegate: unlinkAccountAwaiter.OnCompletion);
|
||||
var callbackResultOption = await unlinkAccountAwaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.UnlinkExternalAccountError.Timeout);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
PuidToPrimaryExternalId.TryRemove(puid, out _);
|
||||
return Result.Success(Unit.Value);
|
||||
}
|
||||
|
||||
return Result.Failure(callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidUser
|
||||
=> EosInterface.Login.UnlinkExternalAccountError.InvalidUser,
|
||||
_
|
||||
=> EosInterface.Login.UnlinkExternalAccountError.UnhandledErrorCondition
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Result<EosInterface.ProductUserId, EosInterface.Login.CreateProductAccountError>> CreateProductAccount(EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.CreateProductAccount(eosContinuanceToken));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Login.LinkExternalAccountError>> LinkExternalAccount(EosInterface.ProductUserId puid, EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LinkExternalAccount(puid, eosContinuanceToken));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Login.UnlinkExternalAccountError>> UnlinkExternalAccount(EosInterface.ProductUserId puid)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.UnlinkExternalAccount(puid));
|
||||
|
||||
public override Task<Result<OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags flags)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LoginEpicWithLinkedSteamAccount(flags));
|
||||
|
||||
public override Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicExchangeCode(string exchangeCode)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LoginEpicExchangeCode(exchangeCode));
|
||||
|
||||
public override Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicIdToken(EosInterface.EgsIdToken token)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LoginEpicIdToken(token));
|
||||
|
||||
public override Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginSteam()
|
||||
=> TaskScheduler.Schedule(LoginPrivate.LoginSteam);
|
||||
|
||||
public override Task<Result<EpicAccountId, EosInterface.Login.LinkExternalAccountToEpicAccountError>> LinkExternalAccountToEpicAccount(EosInterface.EgsAuthContinuanceToken continuanceToken)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LinkExternalAccountToEpicAccount(continuanceToken));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Login.LogoutEpicAccountError>> LogoutEpicAccount(EpicAccountId egsId)
|
||||
=> LoginPrivate.LogoutEpicAccount(egsId);
|
||||
|
||||
public override void MarkAsInaccessible(EosInterface.ProductUserId puid)
|
||||
=> LoginPrivate.MarkAsInaccessible(puid);
|
||||
|
||||
public override void TestEosSessionTimeoutRecovery(EosInterface.ProductUserId puid)
|
||||
{
|
||||
var info = new Epic.OnlineServices.Connect.AuthExpirationCallbackInfo
|
||||
{
|
||||
ClientData = null,
|
||||
LocalUserId = Epic.OnlineServices.ProductUserId.FromString(puid.Value)
|
||||
};
|
||||
LoginPrivate.OnConnectExpiration(ref info);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static partial class OwnershipPrivate
|
||||
{
|
||||
internal static async Task<Option<EosInterface.Ownership.Token>> GetGameOwnershipToken(EpicAccountId selfEpicAccountId)
|
||||
{
|
||||
if (CorePrivate.EcomInterface is not { } ecomInterface) { return Option.None; }
|
||||
|
||||
var epicAccountIdInternal =
|
||||
Epic.OnlineServices.EpicAccountId.FromString(selfEpicAccountId.EosStringRepresentation);
|
||||
|
||||
var queryOwnershipTokenOptions = new Epic.OnlineServices.Ecom.QueryOwnershipTokenOptions
|
||||
{
|
||||
LocalUserId = epicAccountIdInternal,
|
||||
CatalogItemIds = new Epic.OnlineServices.Utf8String[]
|
||||
{
|
||||
AudienceItemId,
|
||||
//"Completely arbitrary string!"
|
||||
|
||||
// IDEA:
|
||||
// As of 2023-06-21, QueryOwnershipToken will succeed even if given obviously fake catalog item IDs.
|
||||
// This could be useful to us! We could use this to add an audience parameter to this method and fix
|
||||
// the impersonation exploit without requiring our own persistent service.
|
||||
// We should ask Epic about this before actually trying it, this is certainly a hack and might get patched.
|
||||
}
|
||||
};
|
||||
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.Ecom.QueryOwnershipTokenCallbackInfo>();
|
||||
ecomInterface.QueryOwnershipToken(options: ref queryOwnershipTokenOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
|
||||
var queryOwnershipTokenResultOption = await callbackWaiter.Task;
|
||||
if (!queryOwnershipTokenResultOption.TryUnwrap(out var queryOwnershipTokenResult)) { return Option.None; }
|
||||
if (queryOwnershipTokenResult.ResultCode != Epic.OnlineServices.Result.Success) { return Option.None; }
|
||||
|
||||
var jwtOption = JsonWebToken.Parse(queryOwnershipTokenResult.OwnershipToken);
|
||||
return jwtOption.Select(jwt => new EosInterface.Ownership.Token(jwt));
|
||||
}
|
||||
|
||||
internal static async Task<Option<EpicAccountId>> VerifyGameOwnershipToken(EosInterface.Ownership.Token token)
|
||||
{
|
||||
JsonWebToken jwt = token.Jwt;
|
||||
|
||||
// Decode header
|
||||
string kidProperty;
|
||||
string algProperty;
|
||||
try
|
||||
{
|
||||
var jsonDoc = JsonDocument.Parse(jwt.HeaderDecoded);
|
||||
kidProperty = jsonDoc.RootElement.GetProperty("kid").GetString() ?? "";
|
||||
algProperty = jsonDoc.RootElement.GetProperty("alg").GetString() ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Header JSON decode failed, can't verify token
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
// Basic header sanity checks
|
||||
if (algProperty != "RS512") { return Option.None; }
|
||||
if (!kidProperty.IsBase64Url()) { return Option.None; }
|
||||
|
||||
// Decode payload
|
||||
string epicAccountIdStr;
|
||||
string catalogItemId;
|
||||
Option<DateTime> expirationOption;
|
||||
bool owned;
|
||||
try
|
||||
{
|
||||
var jsonDoc = JsonDocument.Parse(jwt.PayloadDecoded);
|
||||
epicAccountIdStr = jsonDoc.RootElement.GetProperty("sub").GetString() ?? "";
|
||||
var entProperty = jsonDoc.RootElement.GetProperty("ent").EnumerateArray().First();
|
||||
catalogItemId = entProperty.GetProperty("catalogItemId").GetString() ?? "";
|
||||
expirationOption = UnixTime.ParseUtc(jsonDoc.RootElement.GetProperty("exp").GetUInt64().ToString());
|
||||
owned = entProperty.GetProperty("owned").GetBoolean();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Payload JSON decode failed, can't verify token
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
// Check that the payload is actually what we want
|
||||
if (catalogItemId != AudienceItemId) { return Option.None; }
|
||||
if (!owned) { return Option.None; }
|
||||
if (!expirationOption.TryUnwrap(out var expiration)) { return Option.None; }
|
||||
if (DateTime.UtcNow >= expiration) { return Option.None; }
|
||||
|
||||
// Get the public key required to verify this token
|
||||
string modulus;
|
||||
string exponent;
|
||||
try
|
||||
{
|
||||
string url =
|
||||
"https://ecommerceintegration-public-service-ecomprod02.ol.epicgames.com/ecommerceintegration/api/public/publickeys/"
|
||||
+ kidProperty;
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.SendAsync(new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
new Uri(url)));
|
||||
if (!response.IsSuccessStatusCode) { return Option.None; }
|
||||
var responseStr = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var responseJsonDoc = JsonDocument.Parse(responseStr);
|
||||
if (kidProperty != responseJsonDoc.RootElement.GetProperty("kid").GetString()) { return Option.None; }
|
||||
|
||||
modulus = responseJsonDoc.RootElement.GetProperty("n").GetString() ?? "";
|
||||
exponent = responseJsonDoc.RootElement.GetProperty("e").GetString() ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Failed to query EG Ecom web API, can't verify token
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
// Prepare RSA-SHA512 and verify token
|
||||
var modulusBytesOption = Base64Url.DecodeBytes(modulus);
|
||||
if (!modulusBytesOption.TryUnwrap(out var modulusBytes)) { return Option.None; }
|
||||
var exponentBytesOption = Base64Url.DecodeBytes(exponent);
|
||||
if (!exponentBytesOption.TryUnwrap(out var exponentBytes)) { return Option.None; }
|
||||
|
||||
var signatureBytesOption = Base64Url.DecodeBytes(jwt.Signature);
|
||||
if (!signatureBytesOption.TryUnwrap(out var signatureBytes)) { return Option.None; }
|
||||
|
||||
using var rsa = RSA.Create();
|
||||
using var sha = SHA512.Create();
|
||||
rsa.ImportParameters(new RSAParameters
|
||||
{
|
||||
Exponent = exponentBytes.ToArray(),
|
||||
Modulus = modulusBytes.ToArray(),
|
||||
});
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(jwt.Header + "." + jwt.Payload));
|
||||
var deformatter = new RSAPKCS1SignatureDeformatter(rsa);
|
||||
deformatter.SetHashAlgorithm("SHA512");
|
||||
bool verified = deformatter.VerifySignature(hash, signatureBytes.ToArray());
|
||||
if (!verified) { return Option.None; }
|
||||
|
||||
return EpicAccountId.Parse(epicAccountIdStr);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Option<EosInterface.Ownership.Token>> GetGameOwnershipToken(EpicAccountId selfEpicAccountId)
|
||||
=> TaskScheduler.Schedule(() => OwnershipPrivate.GetGameOwnershipToken(selfEpicAccountId));
|
||||
|
||||
public override Task<Option<EpicAccountId>> VerifyGameOwnershipToken(EosInterface.Ownership.Token token)
|
||||
=> TaskScheduler.Schedule(() => OwnershipPrivate.VerifyGameOwnershipToken(token));
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
#nullable enable
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
public sealed class P2PSocketPrivate : EosInterface.P2PSocket
|
||||
{
|
||||
private readonly record struct CallbackIds(
|
||||
ulong OnConnectionRequested,
|
||||
ulong OnConnectionClosed);
|
||||
private CallbackIds callbackIds;
|
||||
|
||||
private readonly Epic.OnlineServices.P2P.SocketId socketIdInternal;
|
||||
private readonly Epic.OnlineServices.ProductUserId selfPuid;
|
||||
private P2PSocketPrivate(Epic.OnlineServices.P2P.SocketId socketIdInternal, Epic.OnlineServices.ProductUserId selfPuid)
|
||||
{
|
||||
this.socketIdInternal = socketIdInternal;
|
||||
this.selfPuid = selfPuid;
|
||||
}
|
||||
|
||||
internal static Result<EosInterface.P2PSocket, CreationError> CreatePrivate(EosInterface.ProductUserId selfPuid, EosInterface.SocketId socketId)
|
||||
{
|
||||
var p2pInterface = CorePrivate.P2PInterface;
|
||||
if (p2pInterface is null) { return Result.Failure(CreationError.EosNotInitialized); }
|
||||
|
||||
var socketIdInternal = new Epic.OnlineServices.P2P.SocketId { SocketName = socketId.SocketName };
|
||||
var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value);
|
||||
|
||||
using var janitor = Janitor.Start();
|
||||
|
||||
var socket = new P2PSocketPrivate(socketIdInternal, selfPuidInternal);
|
||||
|
||||
var addNotifyPeerConnectionRequestOptions = new Epic.OnlineServices.P2P.AddNotifyPeerConnectionRequestOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
SocketId = socketIdInternal
|
||||
};
|
||||
|
||||
var onConnectionRequestCallbackId = p2pInterface.AddNotifyPeerConnectionRequest(
|
||||
ref addNotifyPeerConnectionRequestOptions,
|
||||
socket,
|
||||
ConnectionRequestHandler);
|
||||
|
||||
if (onConnectionRequestCallbackId == Epic.OnlineServices.Common.InvalidNotificationid)
|
||||
{
|
||||
return Result.Failure(CreationError.RequestBindFailed);
|
||||
}
|
||||
|
||||
janitor.AddAction(() => p2pInterface.RemoveNotifyPeerConnectionRequest(onConnectionRequestCallbackId));
|
||||
|
||||
var addNotifyPeerConnectionClosedOptions = new Epic.OnlineServices.P2P.AddNotifyPeerConnectionClosedOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
SocketId = socketIdInternal
|
||||
};
|
||||
|
||||
var onConnectionClosedCallbackId = p2pInterface.AddNotifyPeerConnectionClosed(
|
||||
ref addNotifyPeerConnectionClosedOptions,
|
||||
socket,
|
||||
ConnectionClosedHandler);
|
||||
|
||||
if (onConnectionClosedCallbackId == Epic.OnlineServices.Common.InvalidNotificationid)
|
||||
{
|
||||
return Result.Failure(CreationError.CloseBindFailed);
|
||||
}
|
||||
|
||||
janitor.AddAction(() => p2pInterface.RemoveNotifyPeerConnectionClosed(onConnectionClosedCallbackId));
|
||||
|
||||
socket.callbackIds = new CallbackIds(
|
||||
OnConnectionRequested: onConnectionRequestCallbackId,
|
||||
OnConnectionClosed: onConnectionClosedCallbackId);
|
||||
|
||||
janitor.Dismiss();
|
||||
|
||||
return Result.Success<EosInterface.P2PSocket>(socket);
|
||||
}
|
||||
|
||||
private static void ConnectionRequestHandler(ref Epic.OnlineServices.P2P.OnIncomingConnectionRequestInfo info)
|
||||
{
|
||||
if (info.ClientData is P2PSocketPrivate p2pSocket
|
||||
&& string.Equals(info.SocketId?.SocketName, p2pSocket.socketIdInternal.SocketName))
|
||||
{
|
||||
p2pSocket.HandleIncomingConnection.Invoke(new IncomingConnectionRequest(
|
||||
Socket: p2pSocket,
|
||||
RemoteUserId: new EosInterface.ProductUserId(info.RemoteUserId.ToString())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConnectionClosedHandler(ref Epic.OnlineServices.P2P.OnRemoteConnectionClosedInfo info)
|
||||
{
|
||||
if (info.ClientData is P2PSocketPrivate p2pSocket
|
||||
&& string.Equals(info.SocketId?.SocketName, p2pSocket.socketIdInternal.SocketName))
|
||||
{
|
||||
p2pSocket.HandleClosedConnection.Invoke(new RemoteConnectionClosed(
|
||||
RemoteUserId: new EosInterface.ProductUserId(info.RemoteUserId.ToString()),
|
||||
Reason: info.Reason switch
|
||||
{
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.Unknown
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.Unknown,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.ClosedByLocalUser
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.ClosedByLocalUser,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.ClosedByPeer
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.ClosedByPeer,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.TimedOut
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.TimedOut,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.TooManyConnections
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.TooManyConnections,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.InvalidMessage
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.InvalidMessage,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.InvalidData
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.InvalidData,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.ConnectionFailed
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.ConnectionFailed,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.ConnectionClosed
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.ConnectionClosed,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.NegotiationFailed
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.NegotiationFailed,
|
||||
Epic.OnlineServices.P2P.ConnectionClosedReason.UnexpectedError
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.UnexpectedError,
|
||||
_
|
||||
=> RemoteConnectionClosed.ConnectionClosedReason.Unhandled
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public override void AcceptConnectionRequest(IncomingConnectionRequest request)
|
||||
{
|
||||
var remoteUserIdInternal = Epic.OnlineServices.ProductUserId.FromString(request.RemoteUserId.Value);
|
||||
|
||||
var acceptConnectionOptions = new Epic.OnlineServices.P2P.AcceptConnectionOptions
|
||||
{
|
||||
LocalUserId = selfPuid,
|
||||
RemoteUserId = remoteUserIdInternal,
|
||||
SocketId = socketIdInternal
|
||||
};
|
||||
CorePrivate.P2PInterface?.AcceptConnection(ref acceptConnectionOptions);
|
||||
}
|
||||
|
||||
public override void CloseConnection(EosInterface.ProductUserId remoteUserId)
|
||||
{
|
||||
var remoteUserIdInternal = Epic.OnlineServices.ProductUserId.FromString(remoteUserId.Value);
|
||||
|
||||
var closeConnectionOptions = new Epic.OnlineServices.P2P.CloseConnectionOptions
|
||||
{
|
||||
LocalUserId = selfPuid,
|
||||
RemoteUserId = remoteUserIdInternal,
|
||||
SocketId = socketIdInternal
|
||||
};
|
||||
CorePrivate.P2PInterface?.CloseConnection(ref closeConnectionOptions);
|
||||
}
|
||||
|
||||
public override IEnumerable<IncomingMessage> GetMessageBatch()
|
||||
{
|
||||
var p2pInterface = CorePrivate.P2PInterface;
|
||||
if (p2pInterface is null) { yield break; }
|
||||
|
||||
var packetQueueOptions = new Epic.OnlineServices.P2P.GetPacketQueueInfoOptions();
|
||||
p2pInterface.GetPacketQueueInfo(ref packetQueueOptions, out var packetQueueInfo);
|
||||
|
||||
byte[] buf = new byte[Epic.OnlineServices.P2P.P2PInterface.MaxPacketSize];
|
||||
|
||||
for (ulong i = 0; i < packetQueueInfo.IncomingPacketQueueCurrentPacketCount; i++)
|
||||
{
|
||||
var receivePacketOptions = new Epic.OnlineServices.P2P.ReceivePacketOptions
|
||||
{
|
||||
LocalUserId = selfPuid,
|
||||
MaxDataSizeBytes = (uint)buf.Length,
|
||||
RequestedChannel = null
|
||||
};
|
||||
|
||||
var result = p2pInterface.ReceivePacket(
|
||||
ref receivePacketOptions,
|
||||
out var senderId,
|
||||
out var senderSocketId,
|
||||
out _,
|
||||
buf,
|
||||
out uint bytesWritten);
|
||||
|
||||
if (result != Epic.OnlineServices.Result.Success) { continue; }
|
||||
if (senderSocketId.SocketName != socketIdInternal.SocketName) { continue; }
|
||||
|
||||
yield return new IncomingMessage(
|
||||
buf, (int)bytesWritten, new EosInterface.ProductUserId(senderId.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
public override Result<Unit, SendError> SendMessage(OutgoingMessage msg)
|
||||
{
|
||||
var p2pInterface = CorePrivate.P2PInterface;
|
||||
if (p2pInterface is null) { return Result.Failure(SendError.EosNotInitialized); }
|
||||
|
||||
var reliability = msg.DeliveryMethod switch
|
||||
{
|
||||
DeliveryMethod.Reliable
|
||||
=> Epic.OnlineServices.P2P.PacketReliability.ReliableOrdered,
|
||||
_
|
||||
=> Epic.OnlineServices.P2P.PacketReliability.UnreliableUnordered
|
||||
};
|
||||
|
||||
var sendPacketOptions = new Epic.OnlineServices.P2P.SendPacketOptions
|
||||
{
|
||||
LocalUserId = selfPuid,
|
||||
RemoteUserId = Epic.OnlineServices.ProductUserId.FromString(msg.Destination.Value),
|
||||
SocketId = socketIdInternal,
|
||||
Channel = 0,
|
||||
Data = new ArraySegment<byte>(array: msg.Buffer, offset: 0, count: msg.ByteLength),
|
||||
AllowDelayedDelivery = true,
|
||||
Reliability = reliability,
|
||||
DisableAutoAcceptConnection = false
|
||||
};
|
||||
var result = p2pInterface.SendPacket(ref sendPacketOptions);
|
||||
|
||||
return result switch
|
||||
{
|
||||
Epic.OnlineServices.Result.Success
|
||||
=> Result.Success(Unit.Value),
|
||||
Epic.OnlineServices.Result.InvalidParameters
|
||||
=> Result.Failure(SendError.InvalidParameters),
|
||||
Epic.OnlineServices.Result.LimitExceeded
|
||||
=> Result.Failure(SendError.LimitExceeded),
|
||||
Epic.OnlineServices.Result.NoConnection
|
||||
=> Result.Failure(SendError.NoConnection),
|
||||
_
|
||||
=> Result.Failure(SendError.UnhandledErrorCondition)
|
||||
};
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
var p2pInterface = CorePrivate.P2PInterface;
|
||||
if (p2pInterface is null) { return; }
|
||||
|
||||
var closeConnectionsOptions = new Epic.OnlineServices.P2P.CloseConnectionsOptions
|
||||
{
|
||||
LocalUserId = selfPuid,
|
||||
SocketId = socketIdInternal
|
||||
};
|
||||
p2pInterface.RemoveNotifyPeerConnectionRequest(callbackIds.OnConnectionRequested);
|
||||
p2pInterface.RemoveNotifyPeerConnectionClosed(callbackIds.OnConnectionClosed);
|
||||
p2pInterface.CloseConnections(ref closeConnectionsOptions);
|
||||
callbackIds = default;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Result<EosInterface.P2PSocket, EosInterface.P2PSocket.CreationError> CreateP2PSocket(EosInterface.ProductUserId puid, EosInterface.SocketId socketId)
|
||||
=> P2PSocketPrivate.CreatePrivate(puid, socketId);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class OwnedSessionsPrivate
|
||||
{
|
||||
private static readonly Random rng = new Random();
|
||||
private static readonly ConcurrentDictionary<Identifier, EosInterface.Sessions.OwnedSession> liveOwnedSessions = new ConcurrentDictionary<Identifier, EosInterface.Sessions.OwnedSession>();
|
||||
|
||||
private static Epic.OnlineServices.Utf8String IdentifierToAttributeKey(Identifier id)
|
||||
{
|
||||
// Attribute keys are always uppercase in the EOS developer page,
|
||||
// so to minimize surprises let's match that here
|
||||
return id.Value.ToUpperInvariant();
|
||||
}
|
||||
|
||||
public static async Task<Result<EosInterface.Sessions.OwnedSession, EosInterface.Sessions.CreateError>> Create(Option<EosInterface.ProductUserId> selfUserIdOption, Identifier internalId, int maxPlayers)
|
||||
{
|
||||
var (success, failure) = Result<EosInterface.Sessions.OwnedSession, EosInterface.Sessions.CreateError>.GetFactoryMethods();
|
||||
|
||||
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return failure(EosInterface.Sessions.CreateError.EosNotInitialized); }
|
||||
|
||||
if (liveOwnedSessions.ContainsKey(internalId)) { return failure(EosInterface.Sessions.CreateError.SessionAlreadyExists); }
|
||||
|
||||
using var janitor = Janitor.Start();
|
||||
|
||||
var bucketIndex = rng.Next(EosInterface.Sessions.MinBucketIndex, EosInterface.Sessions.MaxBucketIndex + 1);
|
||||
string bucketName = EosInterface.Sessions.DefaultBucketName + bucketIndex;
|
||||
var createSessionModificationOptions = new Epic.OnlineServices.Sessions.CreateSessionModificationOptions
|
||||
{
|
||||
SessionName = internalId.Value.ToUpperInvariant(),
|
||||
BucketId = bucketName,
|
||||
MaxPlayers = (uint)maxPlayers,
|
||||
LocalUserId = selfUserIdOption.TryUnwrap(out var selfUserId)
|
||||
? Epic.OnlineServices.ProductUserId.FromString(selfUserId.Value)
|
||||
: null,
|
||||
PresenceEnabled = false,
|
||||
SessionId = null,
|
||||
SanctionsEnabled = false
|
||||
};
|
||||
var sessionCreateResult = sessionsInterface.CreateSessionModification(ref createSessionModificationOptions, out var sessionModificationHandle);
|
||||
if (sessionCreateResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return failure(sessionCreateResult switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidUser => EosInterface.Sessions.CreateError.InvalidUser,
|
||||
Epic.OnlineServices.Result.SessionsSessionAlreadyExists => EosInterface.Sessions.CreateError.SessionAlreadyExists,
|
||||
_ => EosInterface.Sessions.CreateError.UnhandledErrorCondition
|
||||
});
|
||||
}
|
||||
janitor.AddAction(sessionModificationHandle.Release);
|
||||
|
||||
var updateSessionOptions = new Epic.OnlineServices.Sessions.UpdateSessionOptions
|
||||
{
|
||||
SessionModificationHandle = sessionModificationHandle
|
||||
};
|
||||
|
||||
var updateSessionWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.UpdateSessionCallbackInfo>();
|
||||
sessionsInterface.UpdateSession(options: ref updateSessionOptions, clientData: null, completionDelegate: updateSessionWaiter.OnCompletion);
|
||||
var updateSessionResultOption = await updateSessionWaiter.Task;
|
||||
|
||||
if (!updateSessionResultOption.TryUnwrap(out var updateSessionResult)) { return failure(EosInterface.Sessions.CreateError.TimedOut); }
|
||||
|
||||
if (updateSessionResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
var newSession = new EosInterface.Sessions.OwnedSession(
|
||||
BucketId: bucketName,
|
||||
InternalId: updateSessionResult.SessionName.ToIdentifier(),
|
||||
GlobalId: updateSessionResult.SessionId.ToIdentifier(),
|
||||
Attributes: new Dictionary<Identifier, string>());
|
||||
liveOwnedSessions.TryAdd(internalId, newSession);
|
||||
return success(newSession);
|
||||
}
|
||||
return failure(updateSessionResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.CreateError.UnhandledErrorCondition));
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Sessions.AttributeUpdateError>> UpdateOwnedSessionAttributes(EosInterface.Sessions.OwnedSession session)
|
||||
{
|
||||
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.AttributeUpdateError.EosNotInitialized); }
|
||||
|
||||
using var janitor = Janitor.Start();
|
||||
|
||||
var updateSessionModificationOptions = new Epic.OnlineServices.Sessions.UpdateSessionModificationOptions
|
||||
{
|
||||
SessionName = session.InternalId.Value.ToUpperInvariant()
|
||||
};
|
||||
var sessionCreateResult = sessionsInterface.UpdateSessionModification(ref updateSessionModificationOptions, out var sessionModificationHandle);
|
||||
if (sessionCreateResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.Sessions.AttributeUpdateError.FailedToCreateSessionModificationHandle);
|
||||
}
|
||||
janitor.AddAction(() => sessionModificationHandle.Release());
|
||||
|
||||
var keysToRemove = session.SyncedAttributes
|
||||
.Except(session.Attributes)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToArray();
|
||||
|
||||
var attributesToAdd = session.Attributes
|
||||
.Except(session.SyncedAttributes)
|
||||
.ToArray();
|
||||
|
||||
var setBucketIdOptions = new Epic.OnlineServices.Sessions.SessionModificationSetBucketIdOptions
|
||||
{
|
||||
BucketId = session.BucketId
|
||||
};
|
||||
sessionModificationHandle.SetBucketId(ref setBucketIdOptions);
|
||||
|
||||
if (session.HostAddress.TryUnwrap(out var hostAddress))
|
||||
{
|
||||
var setHostAddressOptions = new Epic.OnlineServices.Sessions.SessionModificationSetHostAddressOptions
|
||||
{
|
||||
HostAddress = hostAddress
|
||||
};
|
||||
sessionModificationHandle.SetHostAddress(ref setHostAddressOptions);
|
||||
}
|
||||
|
||||
foreach (Identifier key in keysToRemove)
|
||||
{
|
||||
var removeAttributeOptions = new Epic.OnlineServices.Sessions.SessionModificationRemoveAttributeOptions
|
||||
{
|
||||
Key = IdentifierToAttributeKey(key)
|
||||
};
|
||||
var removeResult = sessionModificationHandle.RemoveAttribute(ref removeAttributeOptions);
|
||||
if (removeResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(
|
||||
removeResult switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidParameters
|
||||
=> EosInterface.Sessions.AttributeUpdateError.InvalidParametersForRemoveAttribute,
|
||||
Epic.OnlineServices.Result.IncompatibleVersion
|
||||
=> EosInterface.Sessions.AttributeUpdateError.IncompatibleVersionForRemoveAttribute,
|
||||
_
|
||||
=> EosInterface.Sessions.AttributeUpdateError.UnhandledErrorConditionForRemoveAttribute
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kvp in attributesToAdd)
|
||||
{
|
||||
// EOS doesn't like empty values so let's skip those
|
||||
if (kvp.Value.IsNullOrEmpty()) { continue; }
|
||||
|
||||
var addAttributeOptions = new Epic.OnlineServices.Sessions.SessionModificationAddAttributeOptions
|
||||
{
|
||||
SessionAttribute = new Epic.OnlineServices.Sessions.AttributeData
|
||||
{
|
||||
Key = IdentifierToAttributeKey(kvp.Key),
|
||||
Value = kvp.Value
|
||||
},
|
||||
AdvertisementType = Epic.OnlineServices.Sessions.SessionAttributeAdvertisementType.Advertise
|
||||
};
|
||||
var addResult = sessionModificationHandle.AddAttribute(ref addAttributeOptions);
|
||||
if (addResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(
|
||||
addResult switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidParameters
|
||||
=> EosInterface.Sessions.AttributeUpdateError.InvalidParametersForAddAttribute,
|
||||
Epic.OnlineServices.Result.IncompatibleVersion
|
||||
=> EosInterface.Sessions.AttributeUpdateError.IncompatibleVersionForAddAttribute,
|
||||
_
|
||||
=> EosInterface.Sessions.AttributeUpdateError.UnhandledErrorConditionForAddAttribute
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var updateSessionOptions = new Epic.OnlineServices.Sessions.UpdateSessionOptions
|
||||
{
|
||||
SessionModificationHandle = sessionModificationHandle
|
||||
};
|
||||
|
||||
var updateSessionWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.UpdateSessionCallbackInfo>();
|
||||
sessionsInterface.UpdateSession(options: ref updateSessionOptions, clientData: null, completionDelegate: updateSessionWaiter.OnCompletion);
|
||||
var updateSessionResultOption = await updateSessionWaiter.Task;
|
||||
|
||||
if (!updateSessionResultOption.TryUnwrap(out var updateSessionResult)) { Result.Failure(EosInterface.Sessions.AttributeUpdateError.TimedOut); }
|
||||
|
||||
if (updateSessionResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return updateSessionResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidParameters
|
||||
=> Result.Failure(EosInterface.Sessions.AttributeUpdateError.InvalidParametersForSessionUpdate),
|
||||
Epic.OnlineServices.Result.SessionsOutOfSync
|
||||
=> Result.Failure(EosInterface.Sessions.AttributeUpdateError.SessionsOutOfSync),
|
||||
Epic.OnlineServices.Result.NotFound
|
||||
=> Result.Failure(EosInterface.Sessions.AttributeUpdateError.SessionNotFound),
|
||||
Epic.OnlineServices.Result.NoConnection
|
||||
=> Result.Failure(EosInterface.Sessions.AttributeUpdateError.NoConnection),
|
||||
var unhandled
|
||||
=> Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.Sessions.AttributeUpdateError.UnhandledErrorCondition))
|
||||
};
|
||||
}
|
||||
|
||||
session.SyncedAttributes = session.Attributes.ToImmutableDictionary();
|
||||
return Result.Success(Unit.Value);
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Sessions.CloseError>> CloseOwnedSession(EosInterface.Sessions.OwnedSession session)
|
||||
{
|
||||
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.CloseError.EosNotInitialized); }
|
||||
|
||||
liveOwnedSessions.TryRemove(session.InternalId, out _);
|
||||
|
||||
var options = new Epic.OnlineServices.Sessions.DestroySessionOptions
|
||||
{
|
||||
SessionName = session.InternalId.Value.ToUpperInvariant()
|
||||
};
|
||||
|
||||
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.DestroySessionCallbackInfo>();
|
||||
sessionsInterface.DestroySession(options: ref options, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
|
||||
var resultOption = await callbackWaiter.Task;
|
||||
|
||||
if (!resultOption.TryUnwrap(out var result)) { return Result.Failure(EosInterface.Sessions.CloseError.TimedOut); }
|
||||
|
||||
return result.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.Success
|
||||
=> Result.Success(Unit.Value),
|
||||
Epic.OnlineServices.Result.InvalidParameters
|
||||
=> Result.Failure(EosInterface.Sessions.CloseError.InvalidParameters),
|
||||
Epic.OnlineServices.Result.AlreadyPending
|
||||
=> Result.Failure(EosInterface.Sessions.CloseError.AlreadyPending),
|
||||
Epic.OnlineServices.Result.NotFound
|
||||
=> Result.Failure(EosInterface.Sessions.CloseError.NotFound),
|
||||
var unhandled
|
||||
=> Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.Sessions.CloseError.UnhandledErrorCondition))
|
||||
};
|
||||
}
|
||||
|
||||
public static Task CloseAllOwnedSessions()
|
||||
{
|
||||
return Task.WhenAll(liveOwnedSessions.Values
|
||||
.ToArray()
|
||||
.Select(CloseOwnedSession));
|
||||
}
|
||||
|
||||
public static Task ForceUpdateAllOwnedSessions()
|
||||
{
|
||||
var sessionsToUpdate = liveOwnedSessions.Values.ToArray();
|
||||
foreach (var session in sessionsToUpdate)
|
||||
{
|
||||
session.SyncedAttributes = ImmutableDictionary<Identifier, string>.Empty;
|
||||
}
|
||||
return Task.WhenAll(sessionsToUpdate
|
||||
.Select(UpdateOwnedSessionAttributes));
|
||||
}
|
||||
|
||||
public static async Task<Result<ImmutableArray<EosInterface.ProductUserId>, EosInterface.Sessions.RegisterError>> RegisterPlayers(EosInterface.Sessions.OwnedSession session, params EosInterface.ProductUserId[] puids)
|
||||
{
|
||||
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.RegisterError.EosNotInitialized); }
|
||||
|
||||
var registerPlayersOptions = new Epic.OnlineServices.Sessions.RegisterPlayersOptions
|
||||
{
|
||||
SessionName = session.InternalId.Value.ToUpperInvariant(),
|
||||
PlayersToRegister = puids.Select(puid => Epic.OnlineServices.ProductUserId.FromString(puid.Value)).ToArray()
|
||||
};
|
||||
var registerPlayersWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.RegisterPlayersCallbackInfo>();
|
||||
sessionsInterface.RegisterPlayers(options: ref registerPlayersOptions, clientData: null, completionDelegate: registerPlayersWaiter.OnCompletion);
|
||||
var registerResultOption = await registerPlayersWaiter.Task;
|
||||
|
||||
if (!registerResultOption.TryUnwrap(out var registerResult)) { return Result.Failure(EosInterface.Sessions.RegisterError.TimedOut); }
|
||||
|
||||
if (registerResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(registerResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.RegisterError.UnhandledErrorCondition));
|
||||
}
|
||||
|
||||
return Result.Success(registerResult.RegisteredPlayers.Select(puid => new EosInterface.ProductUserId(puid.ToString())).ToImmutableArray());
|
||||
}
|
||||
|
||||
public static async Task<Result<ImmutableArray<EosInterface.ProductUserId>, EosInterface.Sessions.UnregisterError>> UnregisterPlayers(EosInterface.Sessions.OwnedSession session, params EosInterface.ProductUserId[] puids)
|
||||
{
|
||||
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.UnregisterError.EosNotInitialized); }
|
||||
|
||||
var unregisterPlayersOptions = new Epic.OnlineServices.Sessions.UnregisterPlayersOptions
|
||||
{
|
||||
SessionName = session.InternalId.Value.ToUpperInvariant(),
|
||||
PlayersToUnregister = puids.Select(puid => Epic.OnlineServices.ProductUserId.FromString(puid.Value)).ToArray()
|
||||
};
|
||||
var unregisterPlayersWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.UnregisterPlayersCallbackInfo>();
|
||||
sessionsInterface.UnregisterPlayers(options: ref unregisterPlayersOptions, clientData: null, completionDelegate: unregisterPlayersWaiter.OnCompletion);
|
||||
var unregisterResultOption = await unregisterPlayersWaiter.Task;
|
||||
|
||||
if (!unregisterResultOption.TryUnwrap(out var unregisterResult)) { return Result.Failure(EosInterface.Sessions.UnregisterError.TimedOut); }
|
||||
|
||||
if (unregisterResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(unregisterResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.UnregisterError.UnhandledErrorCondition));
|
||||
}
|
||||
|
||||
return Result.Success(unregisterResult.UnregisteredPlayers.Select(puid => new EosInterface.ProductUserId(puid.ToString())).ToImmutableArray());
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Result<EosInterface.Sessions.OwnedSession, EosInterface.Sessions.CreateError>> CreateSession(Option<EosInterface.ProductUserId> selfUserIdOption, Identifier internalId, int maxPlayers)
|
||||
=> TaskScheduler.Schedule(() => OwnedSessionsPrivate.Create(selfUserIdOption, internalId, maxPlayers));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Sessions.AttributeUpdateError>> UpdateOwnedSessionAttributes(EosInterface.Sessions.OwnedSession session)
|
||||
=> TaskScheduler.Schedule(() => OwnedSessionsPrivate.UpdateOwnedSessionAttributes(session));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Sessions.CloseError>> CloseOwnedSession(EosInterface.Sessions.OwnedSession session)
|
||||
=> TaskScheduler.Schedule(() => OwnedSessionsPrivate.CloseOwnedSession(session));
|
||||
|
||||
public override Task CloseAllOwnedSessions()
|
||||
=> TaskScheduler.Schedule(OwnedSessionsPrivate.CloseAllOwnedSessions);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class RemoteSessionsPrivate
|
||||
{
|
||||
/// <summary>
|
||||
/// Largest number that can be passed to CreateSessionSearchOptions.MaxSearchResults
|
||||
/// before it will immediately result in an InvalidParameters error.
|
||||
/// </summary>
|
||||
private const uint MaxResultsUpperBound = Epic.OnlineServices.Sessions.SessionsInterface.MaxSearchResults;
|
||||
|
||||
public static async Task<Result<ImmutableArray<EosInterface.Sessions.RemoteSession>, EosInterface.Sessions.RemoteSession.Query.Error>> RunQuery(EosInterface.Sessions.RemoteSession.Query query)
|
||||
{
|
||||
if (CorePrivate.SessionsInterface is not { } sessionsInterface)
|
||||
{
|
||||
return Result.Failure(EosInterface.Sessions.RemoteSession.Query.Error.EosNotInitialized);
|
||||
}
|
||||
|
||||
using var janitor = Janitor.Start();
|
||||
|
||||
var createSessionSearchOptions = new Epic.OnlineServices.Sessions.CreateSessionSearchOptions
|
||||
{
|
||||
MaxSearchResults = query.MaxResults
|
||||
};
|
||||
var createSessionSearchResult = sessionsInterface.CreateSessionSearch(ref createSessionSearchOptions, out var sessionSearchHandle);
|
||||
if (createSessionSearchResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(
|
||||
createSessionSearchResult switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidParameters when query.MaxResults > MaxResultsUpperBound
|
||||
=> EosInterface.Sessions.RemoteSession.Query.Error.ExceededMaxAllowedResults,
|
||||
Epic.OnlineServices.Result.InvalidParameters
|
||||
=> EosInterface.Sessions.RemoteSession.Query.Error.InvalidParameters,
|
||||
_
|
||||
=> createSessionSearchResult.FailAndLogUnhandledError(EosInterface.Sessions.RemoteSession.Query.Error.UnhandledErrorCondition)
|
||||
});
|
||||
}
|
||||
janitor.AddAction(sessionSearchHandle.Release);
|
||||
|
||||
var setParameterOptions = new Epic.OnlineServices.Sessions.SessionSearchSetParameterOptions
|
||||
{
|
||||
Parameter = new Epic.OnlineServices.Sessions.AttributeData
|
||||
{
|
||||
Key = Epic.OnlineServices.Sessions.SessionsInterface.SearchBucketId,
|
||||
Value = new Epic.OnlineServices.Sessions.AttributeDataValue
|
||||
{
|
||||
AsUtf8 = EosInterface.Sessions.DefaultBucketName + query.BucketIndex
|
||||
}
|
||||
},
|
||||
ComparisonOp = Epic.OnlineServices.ComparisonOp.Equal
|
||||
};
|
||||
sessionSearchHandle.SetParameter(ref setParameterOptions);
|
||||
|
||||
var findOptions = new Epic.OnlineServices.Sessions.SessionSearchFindOptions
|
||||
{
|
||||
LocalUserId = Epic.OnlineServices.ProductUserId.FromString(query.LocalUserId.Value)
|
||||
};
|
||||
|
||||
var findCallbackWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.SessionSearchFindCallbackInfo>();
|
||||
sessionSearchHandle.Find(options: ref findOptions, clientData: null, completionDelegate: findCallbackWaiter.OnCompletion);
|
||||
var findResultOption = await findCallbackWaiter.Task;
|
||||
if (!findResultOption.TryUnwrap(out var findResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Sessions.RemoteSession.Query.Error.TimedOut);
|
||||
}
|
||||
if (findResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(
|
||||
findResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.NotFound
|
||||
=> EosInterface.Sessions.RemoteSession.Query.Error.NotFound,
|
||||
Epic.OnlineServices.Result.InvalidParameters
|
||||
=> EosInterface.Sessions.RemoteSession.Query.Error.InvalidParameters,
|
||||
_
|
||||
=> EosInterface.Sessions.RemoteSession.Query.Error.EosNotInitialized
|
||||
});
|
||||
}
|
||||
|
||||
var boilerplate1 = new Epic.OnlineServices.Sessions.SessionSearchGetSearchResultCountOptions();
|
||||
uint resultCount = sessionSearchHandle.GetSearchResultCount(ref boilerplate1);
|
||||
|
||||
var sessions = new List<EosInterface.Sessions.RemoteSession>();
|
||||
foreach (int sessionIndex in Enumerable.Range(0, (int)resultCount))
|
||||
{
|
||||
var attributes = new Dictionary<Identifier, string>();
|
||||
|
||||
var copySessionDetailsOptions = new Epic.OnlineServices.Sessions.SessionSearchCopySearchResultByIndexOptions
|
||||
{
|
||||
SessionIndex = (uint)sessionIndex
|
||||
};
|
||||
var detailsCopyResult = sessionSearchHandle.CopySearchResultByIndex(ref copySessionDetailsOptions, out var sessionDetails);
|
||||
if (detailsCopyResult != Epic.OnlineServices.Result.Success) { break; }
|
||||
janitor.AddAction(sessionDetails.Release);
|
||||
|
||||
var copyInfoOptions = new Epic.OnlineServices.Sessions.SessionDetailsCopyInfoOptions();
|
||||
var infoCopyResult = sessionDetails.CopyInfo(ref copyInfoOptions, out var sessionInfo);
|
||||
if (infoCopyResult != Epic.OnlineServices.Result.Success) { break; }
|
||||
|
||||
if (sessionInfo is not
|
||||
{
|
||||
Settings:
|
||||
{
|
||||
BucketId: { } bucketId,
|
||||
NumPublicConnections: var numPublicConnections
|
||||
},
|
||||
NumOpenPublicConnections: var numOpenPublicConnections,
|
||||
SessionId: { } sessionId,
|
||||
HostAddress: { } hostAddress
|
||||
})
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var boilerplate2 = new Epic.OnlineServices.Sessions.SessionDetailsGetSessionAttributeCountOptions();
|
||||
var attributeCount = sessionDetails.GetSessionAttributeCount(ref boilerplate2);
|
||||
|
||||
foreach (var attributeIndex in Enumerable.Range(0, (int)attributeCount))
|
||||
{
|
||||
var copyAttributeOptions =
|
||||
new Epic.OnlineServices.Sessions.SessionDetailsCopySessionAttributeByIndexOptions
|
||||
{
|
||||
AttrIndex = (uint)attributeIndex
|
||||
};
|
||||
|
||||
var attributeCopyResult = sessionDetails.CopySessionAttributeByIndex(ref copyAttributeOptions, out var attributeNullable);
|
||||
if (attributeCopyResult != Epic.OnlineServices.Result.Success) { break; }
|
||||
if (attributeNullable?.Data is not { } attributeData
|
||||
|| attributeData.Value.ValueType != Epic.OnlineServices.AttributeType.String)
|
||||
{
|
||||
break;
|
||||
}
|
||||
attributes.Add(attributeData.Key.ToIdentifier(), attributeData.Value.AsUtf8);
|
||||
}
|
||||
sessions.Add(new EosInterface.Sessions.RemoteSession(
|
||||
SessionId: sessionId,
|
||||
HostAddress: hostAddress,
|
||||
CurrentPlayers: (int)(numPublicConnections - numOpenPublicConnections),
|
||||
MaxPlayers: (int)numPublicConnections,
|
||||
Attributes: attributes.ToImmutableDictionary(),
|
||||
BucketId: bucketId));
|
||||
}
|
||||
|
||||
return Result.Success(sessions.ToImmutableArray());
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Result<ImmutableArray<EosInterface.Sessions.RemoteSession>, EosInterface.Sessions.RemoteSession.Query.Error>> RunRemoteSessionQuery(EosInterface.Sessions.RemoteSession.Query query)
|
||||
=> TaskScheduler.Schedule(() => RemoteSessionsPrivate.RunQuery(query));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a task that returns the result of a callback.
|
||||
/// This is meant to be used with EOS' asynchronous methods,
|
||||
/// which are all callback-based because this is a C library.
|
||||
/// </summary>
|
||||
internal class CallbackWaiter<T> where T : notnull
|
||||
{
|
||||
private readonly object mutex = new object();
|
||||
private Option<T> result = Option.None;
|
||||
private readonly DateTime timeout;
|
||||
|
||||
public readonly Task<Option<T>> Task;
|
||||
|
||||
public CallbackWaiter(TimeSpan timeout = default)
|
||||
{
|
||||
this.timeout = DateTime.Now + (timeout == default
|
||||
? TimeSpan.FromSeconds(60)
|
||||
: timeout);
|
||||
this.Task = System.Threading.Tasks.Task.Run(RunTask);
|
||||
}
|
||||
|
||||
public void OnCompletion(ref T result)
|
||||
{
|
||||
lock (mutex)
|
||||
{
|
||||
this.result = Option<T>.Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Option<T>> RunTask()
|
||||
{
|
||||
while (DateTime.Now < timeout)
|
||||
{
|
||||
lock (mutex)
|
||||
{
|
||||
if (result.IsSome()) { return result; }
|
||||
}
|
||||
await System.Threading.Tasks.Task.Delay(32);
|
||||
}
|
||||
return Option.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Barotrauma.Debugging;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
public static class ResultExtension
|
||||
{
|
||||
public static T FailAndLogUnhandledError<T>(this Epic.OnlineServices.Result result, T unknown, [CallerMemberName] string caller = null)
|
||||
{
|
||||
DebugConsoleCore.NewMessage($"Result \"{result}\" was not handled by \"{caller}\".", Color.Red);
|
||||
return unknown;
|
||||
}
|
||||
}
|
||||
@@ -403,7 +403,7 @@ namespace Steamworks
|
||||
/// <summary>
|
||||
/// Forget this guy. They're no longer in the game.
|
||||
/// </summary>
|
||||
public static void EndSession( SteamId steamid )
|
||||
public static void EndAuthSession( SteamId steamid )
|
||||
{
|
||||
Internal?.EndAuthSession( steamid );
|
||||
}
|
||||
|
||||
@@ -200,11 +200,11 @@ namespace Steamworks
|
||||
/// to that value. Steam doesn't provide a mechanism for atomically increasing
|
||||
/// stats like this, this functionality is added here as a convenience.
|
||||
/// </summary>
|
||||
public static bool AddStat( string name, int amount = 1 )
|
||||
public static bool AddStatInt( string name, int amount = 1 )
|
||||
{
|
||||
var val = GetStatInt( name );
|
||||
val += amount;
|
||||
return SetStat( name, val );
|
||||
return SetStatInt( name, val );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -212,17 +212,17 @@ namespace Steamworks
|
||||
/// to that value. Steam doesn't provide a mechanism for atomically increasing
|
||||
/// stats like this, this functionality is added here as a convenience.
|
||||
/// </summary>
|
||||
public static bool AddStat( string name, float amount = 1.0f )
|
||||
public static bool AddStatFloat( string name, float amount = 1.0f )
|
||||
{
|
||||
var val = GetStatFloat( name );
|
||||
val += amount;
|
||||
return SetStat( name, val );
|
||||
return SetStatFloat(name, val) || SetStatInt( name, (int)val );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a stat value. This will automatically call <see cref="StoreStats"/> after a successful call.
|
||||
/// </summary>
|
||||
public static bool SetStat( string name, int value )
|
||||
public static bool SetStatInt( string name, int value )
|
||||
{
|
||||
return Internal != null && Internal.SetStat( name, value );
|
||||
}
|
||||
@@ -230,7 +230,7 @@ namespace Steamworks
|
||||
/// <summary>
|
||||
/// Set a stat value. This will automatically call <see cref="StoreStats"/> after a successful call.
|
||||
/// </summary>
|
||||
public static bool SetStat( string name, float value )
|
||||
public static bool SetStatFloat( string name, float value )
|
||||
{
|
||||
return Internal != null && Internal.SetStat( name, value );
|
||||
}
|
||||
@@ -250,9 +250,12 @@ namespace Steamworks
|
||||
/// </summary>
|
||||
public static float GetStatFloat( string name )
|
||||
{
|
||||
float data = 0;
|
||||
Internal?.GetStat( name, ref data );
|
||||
return data;
|
||||
float dataFloat = 0;
|
||||
if (Internal?.GetStat(name, ref dataFloat) is true)
|
||||
{
|
||||
return dataFloat;
|
||||
}
|
||||
return GetStatInt(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user