#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 OnJoinGame = new NamedEvent(); private static ulong joinGameAcceptedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid; internal static readonly NamedEvent OnInviteAccepted = new NamedEvent(); private static ulong inviteAcceptedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid; private static ulong inviteRejectedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid; internal static readonly NamedEvent OnInviteReceived = new NamedEvent(); 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 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> prepareCallbackInfo() { var selfExternalAccountIdsTask = IdQueriesPrivate.GetExternalAccountIds(selfPuid, selfPuid); await Task.WhenAll(selfExternalAccountIdsTask, selfExternalAccountIdsTask); var selfExternalAccountIdsResult = await selfExternalAccountIdsTask; if (!selfExternalAccountIdsResult.TryUnwrapSuccess(out var selfExternalAccountIds) || !selfExternalAccountIds.OfType().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 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 ReceivedInviteIds = new List(); 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 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> 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().FirstOrNone().TryUnwrap(out var selfEpicAccountId)) { return Option.None; } if (!senderExternalAccountIdsResult.TryUnwrapSuccess(out var senderExternalAccountIds) || !senderExternalAccountIds.OfType().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 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> 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(); 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> 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(); 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 OnJoinGame => PresencePrivate.OnJoinGame; public override NamedEvent OnInviteAccepted => PresencePrivate.OnInviteAccepted; public override NamedEvent OnInviteReceived => PresencePrivate.OnInviteReceived; public override Task> SetJoinCommand( EpicAccountId epicAccountId, string desc, string serverName, string joinCommand) => TaskScheduler.Schedule(() => PresencePrivate.SetJoinCommand(epicAccountId, desc, serverName, joinCommand)); public override Task> SendInvite( EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId) => TaskScheduler.Schedule(() => PresencePrivate.SendInvite(selfEpicAccountId, remoteEpicAccountId)); public override void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId) => PresencePrivate.DeclineInvite(selfEpicAccountId, senderEpicAccountId); }