#nullable enable using System.Linq; using System.Threading.Tasks; using Barotrauma.Extensions; namespace Barotrauma.Eos; /// /// Handles a player that owns a copy of Barotrauma on Steam, /// and wishes to link their Steam account to an Epic account /// to interact with friends on Epic Games' account system. /// static class EosEpicSecondaryLogin { public enum ProbeResult { NoAccount, LinkedExternalAccountsButNoPuid, LoggedIn } public static async Task ProbeLinkedEpicAccount() { var loginResult = await EosInterface.Login.LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags.FailWithoutOpeningBrowser); if (!loginResult.TryUnwrapSuccess(out var success)) { return ProbeResult.NoAccount; } if (success.TryGet(out EosInterface.ProductUserId _)) { // Make Steam account the primary external account just in case await EosInterface.Login.LoginSteam(); return ProbeResult.LoggedIn; } if (success.TryGet(out EosInterface.EosConnectContinuanceToken? _)) { return ProbeResult.LinkedExternalAccountsButNoPuid; } return ProbeResult.NoAccount; } public enum LoginErrorDesc { NoPrimaryPuid, FailedToLogInViaLinkedEpicAccount, FailedToForceSteamAsPrimaryExternalAccountId, SteamIsNoLongerLinkedToPuid, SteamPuidMismatchedPreviousPrimaryPuid, FailedToLinkSteamAccountToEpicAccount, FailedToCreatePuidForEpicAccount, UnhandledErrorCondition } public readonly record struct LoginError( LoginErrorDesc ErrorDesc, Option LoginEosConnectError = default, Option LinkExternalToEpicError = default, Option CreatePuidError = default) { public override string ToString() { string error = $"LoginError ({ErrorDesc}"; if (LoginEosConnectError.TryUnwrap(out var connectError)) { error += $", {connectError}"; } if (LinkExternalToEpicError.TryUnwrap(out var externalToEpicError)) { error += $", {externalToEpicError}"; } if (CreatePuidError.TryUnwrap(out var createPuidError)) { error += $", {createPuidError}"; } return error + ")"; } } public static async Task> LoginToLinkedEpicAccount() { var primaryPuidOption = EosInterface.IdQueries.GetLoggedInPuids().FirstOrNone(); if (!primaryPuidOption.TryUnwrap(out var primaryPuid)) { return Result.Failure(new LoginError(LoginErrorDesc.NoPrimaryPuid)); } // No matter what happens, refresh account IDs when returning using var janitor = Janitor.Start(); janitor.AddAction(static () => EosAccount.RefreshSelfAccountIds()); async Task> makeSteamPrimaryExternalAccount() { // By logging into EOS connect via Steam, we make sure that it's // treated as the primary external account, which means that it's // prioritized over the Epic account ID in other EOS functions var loginSteamResult = await EosInterface.Login.LoginSteam(); if (!loginSteamResult.TryUnwrapSuccess(out var loginSteamSuccess)) { return Result.Failure(new LoginError(LoginErrorDesc.FailedToForceSteamAsPrimaryExternalAccountId)); } if (!loginSteamSuccess.TryGet(out EosInterface.ProductUserId primaryPuidAgain)) { return Result.Failure(new LoginError(LoginErrorDesc.SteamIsNoLongerLinkedToPuid)); } if (primaryPuid != primaryPuidAgain) { return Result.Failure(new LoginError(LoginErrorDesc.SteamPuidMismatchedPreviousPrimaryPuid)); } return Result.Success(Unit.Value); } const int MaxLoginPasses = 5; for (int loginPass = 0; loginPass < MaxLoginPasses; loginPass++) { // Try to log into EOS via Epic via Steam several times, // only stop once we get a PUID that's linked only to the Epic account if (EosInterface.IdQueries.GetLoggedInEpicIds() is { Length: > 0 } loggedInEpicIds) { // Log out of any Epic accounts to reduce chances of ending up in an inconsistent state await Task.WhenAll(loggedInEpicIds.Select(EosInterface.Login.LogoutEpicAccount)); } var loginResult = await EosInterface.Login.LoginEpicWithLinkedSteamAccount( loginPass == 0 ? EosInterface.Login.LoginEpicFlags.None : EosInterface.Login.LoginEpicFlags.FailWithoutOpeningBrowser); if (loginResult.TryUnwrapFailure(out var loginEpicFailure)) { return Result.Failure(new LoginError(LoginErrorDesc.FailedToLogInViaLinkedEpicAccount, LoginEosConnectError: Option.Some(loginEpicFailure))); } if (!loginResult.TryUnwrapSuccess(out var loginEpicSuccess)) { throw new UnreachableCodeException(); } if (loginEpicSuccess.TryGet(out EosInterface.ProductUserId secondPuid)) { if (primaryPuid == secondPuid) { // Somehow we've got two external accounts linked // to the same PUID, let's yank them apart // Given that the latest ID used to log into this account was // the Epic account, this call to UnlinkExternalAccount will // keep the SteamID linked to this PUID and only unlink the // Epic account, which is what we want. await EosInterface.Login.UnlinkExternalAccount(secondPuid); // Once that's done, log back into EOS Connect with // the SteamID as the primary ID if ((await makeSteamPrimaryExternalAccount()).TryUnwrapFailure(out var loginSteamError)) { return Result.Failure(loginSteamError); } } else { // We already have a PUID for this Epic account. We're done here! return Result.Success(Unit.Value); } } else if (loginEpicSuccess.TryGet(out EosInterface.EgsAuthContinuanceToken? egsAuthContinuanceToken)) { // We got an EGS Auth continuance token, which means that the player // has provided an Epic account to link the current Steam account to. var linkExternalToEpicResult = await EosInterface.Login.LinkExternalAccountToEpicAccount(egsAuthContinuanceToken); if (linkExternalToEpicResult.TryUnwrapFailure(out var linkExternalToEpicError)) { return Result.Failure(new LoginError(LoginErrorDesc.FailedToLinkSteamAccountToEpicAccount, LinkExternalToEpicError: Option.Some(linkExternalToEpicError))); } } else if (loginEpicSuccess.TryGet(out EosInterface.EosConnectContinuanceToken? eosConnectContinuanceToken)) { // We got an EOS Connect continuance token, we need // a new Product User ID for the given Epic account. var createPuidResult = await EosInterface.Login.CreateProductAccount(eosConnectContinuanceToken); if (createPuidResult.TryUnwrapFailure(out var createPuidError)) { return Result.Failure(new LoginError(LoginErrorDesc.FailedToCreatePuidForEpicAccount, CreatePuidError: Option.Some(createPuidError))); } // PUID has been created! We're done here! return Result.Success(Unit.Value); } else { throw new UnreachableCodeException(); } } return Result.Failure(new LoginError(LoginErrorDesc.UnhandledErrorCondition)); } }