Files
LuaCsForBarotraumaEP/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/LoginPrivate.cs
2025-06-17 16:38:11 +03:00

709 lines
36 KiB
C#

#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.AuthTicket> 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.GetAuthTicketForWebApiAsync(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.GetAuthTicketForWebApiAsync(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);
}
}