#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 steamworksAuthTicket; private static Option eosConnectExpirationNotifyId, eosConnectStatusChangedNotifyId; private static Option 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 field, Action clearAction) { if (field.TryUnwrap(out var notificationId)) { clearAction(notificationId); } field = Option.None; } static void StoreNotificationId(out Option field, ulong value) { bool isValid = value is not Epic.OnlineServices.Common.InvalidNotificationid; field = isValid ? Option.Some(value) : Option.None; } } internal static readonly ConcurrentDictionary PuidToPrimaryExternalId = new(); private readonly record struct LoginParams( Epic.OnlineServices.Connect.Credentials Credentials, AccountId ExternalAccountId); private static async Task> 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, 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(); 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)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)new LoginParams( Credentials: new Epic.OnlineServices.Connect.Credentials { Token = token.JsonWebToken, Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken }, ExternalAccountId: externalAccountId)); } public static async Task, 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, 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) 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)eosContinuanceToken) : loginSuccess.TryGet(out EosInterface.ProductUserId puid) ? Result.Success((OneOf)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, 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, 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, EosInterface.Login.LoginError>> Login(LoginParams loginParams) { static Result, EosInterface.Login.LoginError> success(EosInterface.ProductUserId id) => Result, EosInterface.Login.LoginError>.Success(id); static Result, EosInterface.Login.LoginError> continuance(EosInterface.EosConnectContinuanceToken token) => Result, EosInterface.Login.LoginError>.Success(token); static Result, EosInterface.Login.LoginError> failure(EosInterface.Login.LoginError error) => Result, 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(); 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? 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(); 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> 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(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> 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(); 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 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> 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(); 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> 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(); 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> 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(); 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> CreateProductAccount(EosInterface.EosConnectContinuanceToken eosContinuanceToken) => TaskScheduler.Schedule(() => LoginPrivate.CreateProductAccount(eosContinuanceToken)); public override Task> LinkExternalAccount(EosInterface.ProductUserId puid, EosInterface.EosConnectContinuanceToken eosContinuanceToken) => TaskScheduler.Schedule(() => LoginPrivate.LinkExternalAccount(puid, eosContinuanceToken)); public override Task> UnlinkExternalAccount(EosInterface.ProductUserId puid) => TaskScheduler.Schedule(() => LoginPrivate.UnlinkExternalAccount(puid)); public override Task, EosInterface.Login.LoginError>> LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags flags) => TaskScheduler.Schedule(() => LoginPrivate.LoginEpicWithLinkedSteamAccount(flags)); public override Task, EosInterface.Login.LoginError>> LoginEpicExchangeCode(string exchangeCode) => TaskScheduler.Schedule(() => LoginPrivate.LoginEpicExchangeCode(exchangeCode)); public override Task, EosInterface.Login.LoginError>> LoginEpicIdToken(EosInterface.EgsIdToken token) => TaskScheduler.Schedule(() => LoginPrivate.LoginEpicIdToken(token)); public override Task, EosInterface.Login.LoginError>> LoginSteam() => TaskScheduler.Schedule(LoginPrivate.LoginSteam); public override Task> LinkExternalAccountToEpicAccount(EosInterface.EgsAuthContinuanceToken continuanceToken) => TaskScheduler.Schedule(() => LoginPrivate.LinkExternalAccountToEpicAccount(continuanceToken)); public override Task> 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); } }