From 789cdba8b51f310399752f7a1309e489fadf8dc1 Mon Sep 17 00:00:00 2001 From: Thomas Guillemard Date: Thu, 4 Jul 2019 17:14:17 +0200 Subject: [PATCH] Refactor the friend namespace (#721) * Refactor the friend namespace and UInt128 This commit also: - Fix GetFriendsList arguments ordering. - Add GetFriendListIds. - Expose the permission level of the port instance. - InvalidUUID => InvalidArgument * friend: add all cmds as commments * add Friend structure layout * Rename FriendErr to FriendError * Accurately implement INotificationService * Fix singleton lock of NotificationEventHandler * Address comments * Add comments for IDaemonSuspendSessionService cmds * Explicitly define the Charset when needed Also make "Nickname" a string * Address gdk's comments --- Ryujinx.HLE/FileSystem/SaveHelper.cs | 2 +- Ryujinx.HLE/HOS/Services/Friend/FriendErr.cs | 7 - .../HOS/Services/Friend/FriendError.cs | 8 + .../Friend/FriendServicePermissionLevel.cs | 19 ++ .../Friend/IDaemonSuspendSessionService.cs | 12 +- .../HOS/Services/Friend/IFriendService.cs | 207 ++++++++++++++---- .../Services/Friend/IFriendServiceTypes.cs | 96 +++++++- .../Services/Friend/INotificationService.cs | 151 ++++++++++++- .../HOS/Services/Friend/IServiceCreator.cs | 23 +- .../Friend/NotificationEventHandler.cs | 83 +++++++ .../HOS/Services/FspSrv/IFileSystemProxy.cs | 5 +- Ryujinx.HLE/HOS/Services/ServiceFactory.cs | 13 +- Ryujinx.HLE/Utilities/UInt128.cs | 32 ++- 13 files changed, 576 insertions(+), 82 deletions(-) delete mode 100644 Ryujinx.HLE/HOS/Services/Friend/FriendErr.cs create mode 100644 Ryujinx.HLE/HOS/Services/Friend/FriendError.cs create mode 100644 Ryujinx.HLE/HOS/Services/Friend/FriendServicePermissionLevel.cs create mode 100644 Ryujinx.HLE/HOS/Services/Friend/NotificationEventHandler.cs diff --git a/Ryujinx.HLE/FileSystem/SaveHelper.cs b/Ryujinx.HLE/FileSystem/SaveHelper.cs index 0dfcfd2bb..411d13e25 100644 --- a/Ryujinx.HLE/FileSystem/SaveHelper.cs +++ b/Ryujinx.HLE/FileSystem/SaveHelper.cs @@ -32,7 +32,7 @@ namespace Ryujinx.HLE.FileSystem currentTitleId = context.Process.TitleId; } - string saveAccount = saveMetaData.UserId.IsZero() ? "savecommon" : saveMetaData.UserId.ToString(); + string saveAccount = saveMetaData.UserId.IsNull ? "savecommon" : saveMetaData.UserId.ToString(); string savePath = Path.Combine(baseSavePath, saveMetaData.SaveId.ToString("x16"), diff --git a/Ryujinx.HLE/HOS/Services/Friend/FriendErr.cs b/Ryujinx.HLE/HOS/Services/Friend/FriendErr.cs deleted file mode 100644 index 5ee2a7064..000000000 --- a/Ryujinx.HLE/HOS/Services/Friend/FriendErr.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Friend -{ - static class FriendErr - { - public const int InvalidArgument = 2; - } -} diff --git a/Ryujinx.HLE/HOS/Services/Friend/FriendError.cs b/Ryujinx.HLE/HOS/Services/Friend/FriendError.cs new file mode 100644 index 000000000..49bc4c678 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Friend/FriendError.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Friend +{ + static class FriendError + { + public const int InvalidArgument = 2; + public const int NotificationQueueEmpty = 15; + } +} diff --git a/Ryujinx.HLE/HOS/Services/Friend/FriendServicePermissionLevel.cs b/Ryujinx.HLE/HOS/Services/Friend/FriendServicePermissionLevel.cs new file mode 100644 index 000000000..2ddb0b858 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Friend/FriendServicePermissionLevel.cs @@ -0,0 +1,19 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Friend +{ + [Flags] + enum FriendServicePermissionLevel + { + UserMask = 1, + OverlayMask = 2, + ManagerMask = 4, + SystemMask = 8, + + Admin = -1, + User = UserMask, + Overlay = UserMask | OverlayMask, + Manager = UserMask | OverlayMask | ManagerMask, + System = UserMask | SystemMask + } +} diff --git a/Ryujinx.HLE/HOS/Services/Friend/IDaemonSuspendSessionService.cs b/Ryujinx.HLE/HOS/Services/Friend/IDaemonSuspendSessionService.cs index b1f23dd55..0de74d780 100644 --- a/Ryujinx.HLE/HOS/Services/Friend/IDaemonSuspendSessionService.cs +++ b/Ryujinx.HLE/HOS/Services/Friend/IDaemonSuspendSessionService.cs @@ -7,14 +7,22 @@ namespace Ryujinx.HLE.HOS.Services.Friend { private Dictionary _commands; + private FriendServicePermissionLevel PermissionLevel; + public override IReadOnlyDictionary Commands => _commands; - public IDaemonSuspendSessionService() + public IDaemonSuspendSessionService(FriendServicePermissionLevel permissionLevel) { _commands = new Dictionary { - // ... + //{ 0, Unknown0 }, // 4.0.0+ + //{ 1, Unknown1 }, // 4.0.0+ + //{ 2, Unknown2 }, // 4.0.0+ + //{ 3, Unknown3 }, // 4.0.0+ + //{ 4, Unknown4 }, // 4.0.0+ }; + + PermissionLevel = permissionLevel; } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Friend/IFriendService.cs b/Ryujinx.HLE/HOS/Services/Friend/IFriendService.cs index 35f40818a..58e65ea75 100644 --- a/Ryujinx.HLE/HOS/Services/Friend/IFriendService.cs +++ b/Ryujinx.HLE/HOS/Services/Friend/IFriendService.cs @@ -1,8 +1,13 @@ +using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Utilities; using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; + +using static Ryujinx.HLE.HOS.ErrorCode; namespace Ryujinx.HLE.HOS.Services.Friend { @@ -10,64 +15,174 @@ namespace Ryujinx.HLE.HOS.Services.Friend { private Dictionary _commands; + private FriendServicePermissionLevel _permissionLevel; + public override IReadOnlyDictionary Commands => _commands; - public IFriendService() + public IFriendService(FriendServicePermissionLevel permissionLevel) { _commands = new Dictionary { - { 10101, GetFriendList }, - { 10600, DeclareOpenOnlinePlaySession }, - { 10601, DeclareCloseOnlinePlaySession }, - { 10610, UpdateUserPresence } + //{ 0, GetCompletionEvent }, + //{ 1, Cancel }, + { 10100, GetFriendListIds }, + { 10101, GetFriendList }, + //{ 10102, UpdateFriendInfo }, + //{ 10110, GetFriendProfileImage }, + //{ 10200, SendFriendRequestForApplication }, + //{ 10211, AddFacedFriendRequestForApplication }, + //{ 10400, GetBlockedUserListIds }, + //{ 10500, GetProfileList }, + { 10600, DeclareOpenOnlinePlaySession }, + { 10601, DeclareCloseOnlinePlaySession }, + { 10610, UpdateUserPresence }, + //{ 10700, GetPlayHistoryRegistrationKey }, + //{ 10701, GetPlayHistoryRegistrationKeyWithNetworkServiceAccountId }, + //{ 10702, AddPlayHistory }, + //{ 11000, GetProfileImageUrl }, + //{ 20100, GetFriendCount }, + //{ 20101, GetNewlyFriendCount }, + //{ 20102, GetFriendDetailedInfo }, + //{ 20103, SyncFriendList }, + //{ 20104, RequestSyncFriendList }, + //{ 20110, LoadFriendSetting }, + //{ 20200, GetReceivedFriendRequestCount }, + //{ 20201, GetFriendRequestList }, + //{ 20300, GetFriendCandidateList }, + //{ 20301, GetNintendoNetworkIdInfo }, // 3.0.0+ + //{ 20302, GetSnsAccountLinkage }, // 5.0.0+ + //{ 20303, GetSnsAccountProfile }, // 5.0.0+ + //{ 20304, GetSnsAccountFriendList }, // 5.0.0+ + //{ 20400, GetBlockedUserList }, + //{ 20401, SyncBlockedUserList }, + //{ 20500, GetProfileExtraList }, + //{ 20501, GetRelationship }, + //{ 20600, GetUserPresenceView }, + //{ 20700, GetPlayHistoryList }, + //{ 20701, GetPlayHistoryStatistics }, + //{ 20800, LoadUserSetting }, + //{ 20801, SyncUserSetting }, + //{ 20900, RequestListSummaryOverlayNotification }, + //{ 21000, GetExternalApplicationCatalog }, + //{ 30100, DropFriendNewlyFlags }, + //{ 30101, DeleteFriend }, + //{ 30110, DropFriendNewlyFlag }, + //{ 30120, ChangeFriendFavoriteFlag }, + //{ 30121, ChangeFriendOnlineNotificationFlag }, + //{ 30200, SendFriendRequest }, + //{ 30201, SendFriendRequestWithApplicationInfo }, + //{ 30202, CancelFriendRequest }, + //{ 30203, AcceptFriendRequest }, + //{ 30204, RejectFriendRequest }, + //{ 30205, ReadFriendRequest }, + //{ 30210, GetFacedFriendRequestRegistrationKey }, + //{ 30211, AddFacedFriendRequest }, + //{ 30212, CancelFacedFriendRequest }, + //{ 30213, GetFacedFriendRequestProfileImage }, + //{ 30214, GetFacedFriendRequestProfileImageFromPath }, + //{ 30215, SendFriendRequestWithExternalApplicationCatalogId }, + //{ 30216, ResendFacedFriendRequest }, + //{ 30217, SendFriendRequestWithNintendoNetworkIdInfo }, // 3.0.0+ + //{ 30300, GetSnsAccountLinkPageUrl }, // 5.0.0+ + //{ 30301, UnlinkSnsAccount }, // 5.0.0+ + //{ 30400, BlockUser }, + //{ 30401, BlockUserWithApplicationInfo }, + //{ 30402, UnblockUser }, + //{ 30500, GetProfileExtraFromFriendCode }, + //{ 30700, DeletePlayHistory }, + //{ 30810, ChangePresencePermission }, + //{ 30811, ChangeFriendRequestReception }, + //{ 30812, ChangePlayLogPermission }, + //{ 30820, IssueFriendCode }, + //{ 30830, ClearPlayLog }, + //{ 49900, DeleteNetworkServiceAccountCache }, }; + + _permissionLevel = permissionLevel; } - // nn::friends::GetFriendListGetFriendListIds(nn::account::Uid, int Unknown0, nn::friends::detail::ipc::SizedFriendFilter, ulong Unknown1) -> int CounterIds, array - public long GetFriendList(ServiceCtx context) + // nn::friends::GetFriendListIds(int offset, nn::account::Uid userUUID, nn::friends::detail::ipc::SizedFriendFilter friendFilter, ulong pidPlaceHolder, pid) -> int outCount, array + public long GetFriendListIds(ServiceCtx context) { - UInt128 uuid = new UInt128( - context.RequestData.ReadInt64(), - context.RequestData.ReadInt64()); + int offset = context.RequestData.ReadInt32(); - int unknown0 = context.RequestData.ReadInt32(); + // Padding + context.RequestData.ReadInt32(); - FriendFilter filter = new FriendFilter + UInt128 uuid = context.RequestData.ReadStruct(); + FriendFilter filter = context.RequestData.ReadStruct(); + + // Pid placeholder + context.RequestData.ReadInt64(); + + if (uuid.IsNull) { - PresenceStatus = (PresenceStatusFilter)context.RequestData.ReadInt32(), - IsFavoriteOnly = context.RequestData.ReadBoolean(), - IsSameAppPresenceOnly = context.RequestData.ReadBoolean(), - IsSameAppPlayedOnly = context.RequestData.ReadBoolean(), - IsArbitraryAppPlayedOnly = context.RequestData.ReadBoolean(), - PresenceGroupId = context.RequestData.ReadInt64() - }; - - long unknown1 = context.RequestData.ReadInt64(); + return MakeError(ErrorModule.Friends, FriendError.InvalidArgument); + } // There are no friends online, so we return 0 because the nn::account::NetworkServiceAccountId array is empty. context.ResponseData.Write(0); - Logger.PrintStub(LogClass.ServiceFriend, new { + Logger.PrintStub(LogClass.ServiceFriend, new + { UserId = uuid.ToString(), - unknown0, + offset, filter.PresenceStatus, filter.IsFavoriteOnly, filter.IsSameAppPresenceOnly, filter.IsSameAppPlayedOnly, filter.IsArbitraryAppPlayedOnly, filter.PresenceGroupId, - unknown1 }); return 0; } - // DeclareOpenOnlinePlaySession(nn::account::Uid) + // nn::friends::GetFriendList(int offset, nn::account::Uid userUUID, nn::friends::detail::ipc::SizedFriendFilter friendFilter, ulong pidPlaceHolder, pid) -> int outCount, array + public long GetFriendList(ServiceCtx context) + { + int offset = context.RequestData.ReadInt32(); + + // Padding + context.RequestData.ReadInt32(); + + UInt128 uuid = context.RequestData.ReadStruct(); + FriendFilter filter = context.RequestData.ReadStruct(); + + // Pid placeholder + context.RequestData.ReadInt64(); + + if (uuid.IsNull) + { + return MakeError(ErrorModule.Friends, FriendError.InvalidArgument); + } + + // There are no friends online, so we return 0 because the nn::account::NetworkServiceAccountId array is empty. + context.ResponseData.Write(0); + + Logger.PrintStub(LogClass.ServiceFriend, new { + UserId = uuid.ToString(), + offset, + filter.PresenceStatus, + filter.IsFavoriteOnly, + filter.IsSameAppPresenceOnly, + filter.IsSameAppPlayedOnly, + filter.IsArbitraryAppPlayedOnly, + filter.PresenceGroupId, + }); + + return 0; + } + + // nn::friends::DeclareOpenOnlinePlaySession(nn::account::Uid) public long DeclareOpenOnlinePlaySession(ServiceCtx context) { - UInt128 uuid = new UInt128( - context.RequestData.ReadInt64(), - context.RequestData.ReadInt64()); + UInt128 uuid = context.RequestData.ReadStruct(); + + if (uuid.IsNull) + { + return MakeError(ErrorModule.Friends, FriendError.InvalidArgument); + } if (context.Device.System.State.Account.TryGetUser(uuid, out UserProfile profile)) { @@ -79,12 +194,15 @@ namespace Ryujinx.HLE.HOS.Services.Friend return 0; } - // DeclareCloseOnlinePlaySession(nn::account::Uid) + // nn::friends::DeclareCloseOnlinePlaySession(nn::account::Uid) public long DeclareCloseOnlinePlaySession(ServiceCtx context) { - UInt128 uuid = new UInt128( - context.RequestData.ReadInt64(), - context.RequestData.ReadInt64()); + UInt128 uuid = context.RequestData.ReadStruct(); + + if (uuid.IsNull) + { + return MakeError(ErrorModule.Friends, FriendError.InvalidArgument); + } if (context.Device.System.State.Account.TryGetUser(uuid, out UserProfile profile)) { @@ -96,21 +214,32 @@ namespace Ryujinx.HLE.HOS.Services.Friend return 0; } - // UpdateUserPresence(nn::account::Uid, ulong Unknown0) -> buffer + // nn::friends::UpdateUserPresence(nn::account::Uid, u64, pid, buffer) public long UpdateUserPresence(ServiceCtx context) { - UInt128 uuid = new UInt128( - context.RequestData.ReadInt64(), - context.RequestData.ReadInt64()); + UInt128 uuid = context.RequestData.ReadStruct(); - long unknown0 = context.RequestData.ReadInt64(); + // Pid placeholder + context.RequestData.ReadInt64(); long position = context.Request.PtrBuff[0].Position; long size = context.Request.PtrBuff[0].Size; - // TODO: Write the buffer content. + byte[] bufferContent = context.Memory.ReadBytes(position, size); - Logger.PrintStub(LogClass.ServiceFriend, new { UserId = uuid.ToString(), unknown0 }); + if (uuid.IsNull) + { + return MakeError(ErrorModule.Friends, FriendError.InvalidArgument); + } + + int elementCount = bufferContent.Length / Marshal.SizeOf(); + + using (BinaryReader bufferReader = new BinaryReader(new MemoryStream(bufferContent))) + { + UserPresence[] userPresenceInputArray = bufferReader.ReadStructArray(elementCount); + + Logger.PrintStub(LogClass.ServiceFriend, new { UserId = uuid.ToString(), userPresenceInputArray }); + } return 0; } diff --git a/Ryujinx.HLE/HOS/Services/Friend/IFriendServiceTypes.cs b/Ryujinx.HLE/HOS/Services/Friend/IFriendServiceTypes.cs index 31459f7d6..7eb1b43bf 100644 --- a/Ryujinx.HLE/HOS/Services/Friend/IFriendServiceTypes.cs +++ b/Ryujinx.HLE/HOS/Services/Friend/IFriendServiceTypes.cs @@ -1,6 +1,9 @@ +using Ryujinx.HLE.Utilities; +using System.Runtime.InteropServices; + namespace Ryujinx.HLE.HOS.Services.Friend { - enum PresenceStatusFilter + enum PresenceStatusFilter : uint { None, Online, @@ -8,13 +11,94 @@ namespace Ryujinx.HLE.HOS.Services.Friend OnlineOrOnlinePlay } + enum PresenceStatus : uint + { + Offline, + Online, + OnlinePlay, + } + + [StructLayout(LayoutKind.Sequential)] struct FriendFilter { public PresenceStatusFilter PresenceStatus; - public bool IsFavoriteOnly; - public bool IsSameAppPresenceOnly; - public bool IsSameAppPlayedOnly; - public bool IsArbitraryAppPlayedOnly; - public long PresenceGroupId; + + [MarshalAs(UnmanagedType.I1)] + public bool IsFavoriteOnly; + + [MarshalAs(UnmanagedType.I1)] + public bool IsSameAppPresenceOnly; + + [MarshalAs(UnmanagedType.I1)] + public bool IsSameAppPlayedOnly; + + [MarshalAs(UnmanagedType.I1)] + public bool IsArbitraryAppPlayedOnly; + + public long PresenceGroupId; + } + + [StructLayout(LayoutKind.Sequential, Pack = 0x8, CharSet = CharSet.Ansi)] + struct UserPresence + { + public UInt128 UserId; + public long LastTimeOnlineTimestamp; + public PresenceStatus Status; + + [MarshalAs(UnmanagedType.I1)] + public bool SamePresenceGroupApplication; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3)] + char[] Unknown; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0xC0)] + public char[] AppKeyValueStorage; + + public override string ToString() + { + return $"UserPresence {{ UserId: {UserId}, LastTimeOnlineTimestamp: {LastTimeOnlineTimestamp}, Status: {Status}, AppKeyValueStorage: {AppKeyValueStorage} }}"; + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 0x8, Size = 0x200, CharSet = CharSet.Ansi)] + struct Friend + { + public UInt128 UserId; + public long NetworkUserId; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x21)] + public string Nickname; + + public UserPresence presence; + + [MarshalAs(UnmanagedType.I1)] + public bool IsFavourite; + + [MarshalAs(UnmanagedType.I1)] + public bool IsNew; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x6)] + char[] Unknown; + + [MarshalAs(UnmanagedType.I1)] + public bool IsValid; + } + + enum NotificationEventType : uint + { + Invalid = 0x0, + FriendListUpdate = 0x1, + NewFriendRequest = 0x65, + } + + [StructLayout(LayoutKind.Sequential, Pack = 0x8, Size = 0x10)] + struct NotificationInfo + { + public NotificationEventType Type; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x4)] + char[] Padding; + + public long NetworkUserIdPlaceholder; } } diff --git a/Ryujinx.HLE/HOS/Services/Friend/INotificationService.cs b/Ryujinx.HLE/HOS/Services/Friend/INotificationService.cs index 8b684e6b8..68893efeb 100644 --- a/Ryujinx.HLE/HOS/Services/Friend/INotificationService.cs +++ b/Ryujinx.HLE/HOS/Services/Friend/INotificationService.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Threading; @@ -5,37 +6,55 @@ using Ryujinx.HLE.Utilities; using System; using System.Collections.Generic; +using static Ryujinx.HLE.HOS.ErrorCode; + namespace Ryujinx.HLE.HOS.Services.Friend { - class INotificationService : IpcService + class INotificationService : IpcService, IDisposable { - private UInt128 _userId; + private readonly UInt128 _userId; + private readonly FriendServicePermissionLevel _permissionLevel; + + private readonly object _lock = new object(); private KEvent _notificationEvent; private int _notificationEventHandle = 0; + + private LinkedList _notifications; + + private bool _hasNewFriendRequest; + private bool _hasFriendListUpdate; + private Dictionary _commands; public override IReadOnlyDictionary Commands => _commands; - public INotificationService(UInt128 userId) + public INotificationService(ServiceCtx context, UInt128 userId, FriendServicePermissionLevel permissionLevel) { _commands = new Dictionary { { 0, GetEvent }, // 2.0.0+ - //{ 1, Clear }, // 2.0.0+ - //{ 2, Pop }, // 2.0.0+ + { 1, Clear }, // 2.0.0+ + { 2, Pop }, // 2.0.0+ }; - _userId = userId; + _userId = userId; + _permissionLevel = permissionLevel; + _notifications = new LinkedList(); + _notificationEvent = new KEvent(context.Device.System); + + _hasNewFriendRequest = false; + _hasFriendListUpdate = false; + + NotificationEventHandler.Instance.RegisterNotificationService(this); } + // nn::friends::detail::ipc::INotificationService::GetEvent() -> handle public long GetEvent(ServiceCtx context) { if (_notificationEventHandle == 0) { - _notificationEvent = new KEvent(context.Device.System); - if (context.Process.HandleTable.GenerateHandle(_notificationEvent.ReadableEvent, out _notificationEventHandle) != KernelResult.Success) { throw new InvalidOperationException("Out of handles!"); @@ -46,5 +65,121 @@ namespace Ryujinx.HLE.HOS.Services.Friend return 0; } + + // nn::friends::detail::ipc::INotificationService::Clear() + public long Clear(ServiceCtx context) + { + lock (_lock) + { + _hasNewFriendRequest = false; + _hasFriendListUpdate = false; + + _notifications.Clear(); + } + + return 0; + } + + // nn::friends::detail::ipc::INotificationService::Pop() -> nn::friends::detail::ipc::SizedNotificationInfo + public long Pop(ServiceCtx context) + { + lock (_lock) + { + if (_notifications.Count >= 1) + { + NotificationInfo notificationInfo = _notifications.First.Value; + _notifications.RemoveFirst(); + + if (notificationInfo.Type == NotificationEventType.FriendListUpdate) + { + _hasFriendListUpdate = false; + } + else if (notificationInfo.Type == NotificationEventType.NewFriendRequest) + { + _hasNewFriendRequest = false; + } + + context.ResponseData.WriteStruct(notificationInfo); + + return 0; + } + } + + return MakeError(ErrorModule.Friends, FriendError.NotificationQueueEmpty); + } + + public void SignalFriendListUpdate(UInt128 targetId) + { + lock (_lock) + { + if (_userId == targetId) + { + if (!_hasFriendListUpdate) + { + NotificationInfo friendListNotification = new NotificationInfo(); + + if (_notifications.Count != 0) + { + friendListNotification = _notifications.First.Value; + _notifications.RemoveFirst(); + } + + friendListNotification.Type = NotificationEventType.FriendListUpdate; + _hasFriendListUpdate = true; + + if (_hasNewFriendRequest) + { + NotificationInfo newFriendRequestNotification = new NotificationInfo(); + + if (_notifications.Count != 0) + { + newFriendRequestNotification = _notifications.First.Value; + _notifications.RemoveFirst(); + } + + newFriendRequestNotification.Type = NotificationEventType.NewFriendRequest; + _notifications.AddFirst(newFriendRequestNotification); + } + + // We defer this to make sure we are on top of the queue. + _notifications.AddFirst(friendListNotification); + } + + _notificationEvent.ReadableEvent.Signal(); + } + } + } + + public void SignalNewFriendRequest(UInt128 targetId) + { + lock (_lock) + { + if ((_permissionLevel & FriendServicePermissionLevel.OverlayMask) != 0 && _userId == targetId) + { + if (!_hasNewFriendRequest) + { + if (_notifications.Count == 100) + { + SignalFriendListUpdate(targetId); + } + + NotificationInfo newFriendRequestNotification = new NotificationInfo + { + Type = NotificationEventType.NewFriendRequest + }; + + _notifications.AddLast(newFriendRequestNotification); + _hasNewFriendRequest = true; + } + + _notificationEvent.ReadableEvent.Signal(); + } + } + } + + public void Dispose() + { + NotificationEventHandler.Instance.UnregisterNotificationService(this); + } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs b/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs index 81281dc2d..dea9d9ae1 100644 --- a/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs +++ b/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.Utilities; using System.Collections.Generic; @@ -10,9 +11,11 @@ namespace Ryujinx.HLE.HOS.Services.Friend { private Dictionary _commands; + private FriendServicePermissionLevel _permissionLevel; + public override IReadOnlyDictionary Commands => _commands; - public IServiceCreator() + public IServiceCreator(FriendServicePermissionLevel permissionLevel) { _commands = new Dictionary { @@ -20,35 +23,37 @@ namespace Ryujinx.HLE.HOS.Services.Friend { 1, CreateNotificationService }, // 2.0.0+ { 2, CreateDaemonSuspendSessionService }, // 4.0.0+ }; + + _permissionLevel = permissionLevel; } // CreateFriendService() -> object - public static long CreateFriendService(ServiceCtx context) + public long CreateFriendService(ServiceCtx context) { - MakeObject(context, new IFriendService()); + MakeObject(context, new IFriendService(_permissionLevel)); return 0; } // CreateNotificationService(nn::account::Uid) -> object - public static long CreateNotificationService(ServiceCtx context) + public long CreateNotificationService(ServiceCtx context) { - UInt128 userId = new UInt128(context.RequestData.ReadBytes(0x10)); + UInt128 userId = context.RequestData.ReadStruct(); if (userId.IsNull) { - return MakeError(ErrorModule.Friends, FriendErr.InvalidArgument); + return MakeError(ErrorModule.Friends, FriendError.InvalidArgument); } - MakeObject(context, new INotificationService(userId)); + MakeObject(context, new INotificationService(context, userId, _permissionLevel)); return 0; } // CreateDaemonSuspendSessionService() -> object - public static long CreateDaemonSuspendSessionService(ServiceCtx context) + public long CreateDaemonSuspendSessionService(ServiceCtx context) { - MakeObject(context, new IDaemonSuspendSessionService()); + MakeObject(context, new IDaemonSuspendSessionService(_permissionLevel)); return 0; } diff --git a/Ryujinx.HLE/HOS/Services/Friend/NotificationEventHandler.cs b/Ryujinx.HLE/HOS/Services/Friend/NotificationEventHandler.cs new file mode 100644 index 000000000..8582a074b --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Friend/NotificationEventHandler.cs @@ -0,0 +1,83 @@ +using Ryujinx.HLE.Utilities; + +namespace Ryujinx.HLE.HOS.Services.Friend +{ + public sealed class NotificationEventHandler + { + private static NotificationEventHandler instance; + private static object instanceLock = new object(); + + private INotificationService[] _registry; + + public static NotificationEventHandler Instance + { + get + { + lock (instanceLock) + { + if (instance == null) + { + instance = new NotificationEventHandler(); + } + + return instance; + } + } + } + + NotificationEventHandler() + { + _registry = new INotificationService[0x20]; + } + + internal void RegisterNotificationService(INotificationService service) + { + // NOTE: in case there isn't space anymore in the registry array, Nintendo doesn't return any errors. + for (int i = 0; i < _registry.Length; i++) + { + if (_registry[i] == null) + { + _registry[i] = service; + break; + } + } + } + + internal void UnregisterNotificationService(INotificationService service) + { + // NOTE: in case there isn't the entry in the registry array, Nintendo doesn't return any errors. + for (int i = 0; i < _registry.Length; i++) + { + if (_registry[i] == service) + { + _registry[i] = null; + break; + } + } + } + + // TODO: Use this when we will have enough things to go online. + public void SignalFriendListUpdate(UInt128 targetId) + { + for (int i = 0; i < _registry.Length; i++) + { + if (_registry[i] != null) + { + _registry[i].SignalFriendListUpdate(targetId); + } + } + } + + // TODO: Use this when we will have enough things to go online. + public void SignalNewFriendRequest(UInt128 targetId) + { + for (int i = 0; i < _registry.Length; i++) + { + if (_registry[i] != null) + { + _registry[i].SignalNewFriendRequest(targetId); + } + } + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystemProxy.cs b/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystemProxy.cs index 7207aaf02..a1afe9beb 100644 --- a/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystemProxy.cs +++ b/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystemProxy.cs @@ -1,6 +1,7 @@ using LibHac; using LibHac.Fs; using LibHac.Fs.NcaUtils; +using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Ipc; @@ -234,9 +235,7 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv long titleId = context.RequestData.ReadInt64(); - UInt128 userId = new UInt128( - context.RequestData.ReadInt64(), - context.RequestData.ReadInt64()); + UInt128 userId = context.RequestData.ReadStruct(); long saveId = context.RequestData.ReadInt64(); SaveDataType saveDataType = (SaveDataType)context.RequestData.ReadByte(); diff --git a/Ryujinx.HLE/HOS/Services/ServiceFactory.cs b/Ryujinx.HLE/HOS/Services/ServiceFactory.cs index 7cd943e02..3edb5619e 100644 --- a/Ryujinx.HLE/HOS/Services/ServiceFactory.cs +++ b/Ryujinx.HLE/HOS/Services/ServiceFactory.cs @@ -100,10 +100,19 @@ namespace Ryujinx.HLE.HOS.Services return new IeTicketService(); case "friend:a": - return new Friend.IServiceCreator(); + return new Friend.IServiceCreator(Friend.FriendServicePermissionLevel.Admin); case "friend:u": - return new Friend.IServiceCreator(); + return new Friend.IServiceCreator(Friend.FriendServicePermissionLevel.User); + + case "friend:v": + return new Friend.IServiceCreator(Friend.FriendServicePermissionLevel.Overlay); + + case "friend:m": + return new Friend.IServiceCreator(Friend.FriendServicePermissionLevel.Manager); + + case "friend:s": + return new Friend.IServiceCreator(Friend.FriendServicePermissionLevel.System); case "fsp-srv": return new IFileSystemProxy(); diff --git a/Ryujinx.HLE/Utilities/UInt128.cs b/Ryujinx.HLE/Utilities/UInt128.cs index 8f5fc28fc..22d87f6b1 100644 --- a/Ryujinx.HLE/Utilities/UInt128.cs +++ b/Ryujinx.HLE/Utilities/UInt128.cs @@ -1,13 +1,15 @@ using System; using System.IO; using System.Linq; +using System.Runtime.InteropServices; namespace Ryujinx.HLE.Utilities { - public struct UInt128 + [StructLayout(LayoutKind.Sequential)] + public struct UInt128 : IEquatable { - public long High { get; private set; } - public long Low { get; private set; } + public readonly long Low; + public readonly long High; public bool IsNull => (Low | High) == 0; @@ -45,9 +47,29 @@ namespace Ryujinx.HLE.Utilities return High.ToString("x16") + Low.ToString("x16"); } - public bool IsZero() + public static bool operator ==(UInt128 x, UInt128 y) { - return (Low | High) == 0; + return x.Equals(y); + } + + public static bool operator !=(UInt128 x, UInt128 y) + { + return !x.Equals(y); + } + + public override bool Equals(object obj) + { + return obj is UInt128 uint128 && Equals(uint128); + } + + public bool Equals(UInt128 cmpObj) + { + return Low == cmpObj.Low && High == cmpObj.High; + } + + public override int GetHashCode() + { + return HashCode.Combine(Low, High); } } } \ No newline at end of file