v1.3.0.1 (Epic Store release)

This commit is contained in:
Regalis11
2024-03-28 18:34:33 +02:00
parent 81ca8637be
commit 3791670c42
269 changed files with 13160 additions and 2966 deletions
@@ -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})" : "");
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}