aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator
diff options
context:
space:
mode:
authorTSR Berry <20988865+TSRBerry@users.noreply.github.com>2023-04-08 01:22:00 +0200
committerMary <thog@protonmail.com>2023-04-27 23:51:14 +0200
commitcee712105850ac3385cd0091a923438167433f9f (patch)
tree4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator
parentcd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff)
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator')
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs29
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs24
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatus.cs9
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatusFilter.cs10
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs34
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs12
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs352
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs178
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs83
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationEventType.cs9
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs13
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/FriendServicePermissionLevel.cs19
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/PlayHistoryRegistrationKey.cs16
13 files changed, 788 insertions, 0 deletions
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs
new file mode 100644
index 00000000..87f54bf3
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs
@@ -0,0 +1,29 @@
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 0x8, Size = 0x200, CharSet = CharSet.Ansi)]
+ struct Friend
+ {
+ public UserId 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;
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs
new file mode 100644
index 00000000..261bf7bf
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs
@@ -0,0 +1,24 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
+{
+ [StructLayout(LayoutKind.Sequential)]
+ struct FriendFilter
+ {
+ public PresenceStatusFilter PresenceStatus;
+
+ [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;
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatus.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatus.cs
new file mode 100644
index 00000000..df2e6525
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatus.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
+{
+ enum PresenceStatus : uint
+ {
+ Offline,
+ Online,
+ OnlinePlay
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatusFilter.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatusFilter.cs
new file mode 100644
index 00000000..24da7fd3
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatusFilter.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
+{
+ enum PresenceStatusFilter : uint
+ {
+ None,
+ Online,
+ OnlinePlay,
+ OnlineOrOnlinePlay
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs
new file mode 100644
index 00000000..d36b3f31
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs
@@ -0,0 +1,34 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 0x8)]
+ struct UserPresence
+ {
+ public UserId UserId;
+ public long LastTimeOnlineTimestamp;
+ public PresenceStatus Status;
+
+ [MarshalAs(UnmanagedType.I1)]
+ public bool SamePresenceGroupApplication;
+
+ public Array3<byte> Unknown;
+ private AppKeyValueStorageHolder _appKeyValueStorage;
+
+ public Span<byte> AppKeyValueStorage => MemoryMarshal.Cast<AppKeyValueStorageHolder, byte>(MemoryMarshal.CreateSpan(ref _appKeyValueStorage, AppKeyValueStorageHolder.Size));
+
+ [StructLayout(LayoutKind.Sequential, Pack = 0x1, Size = Size)]
+ private struct AppKeyValueStorageHolder
+ {
+ public const int Size = 0xC0;
+ }
+
+ public override string ToString()
+ {
+ return $"UserPresence {{ UserId: {UserId}, LastTimeOnlineTimestamp: {LastTimeOnlineTimestamp}, Status: {Status} }}";
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs
new file mode 100644
index 00000000..42b34312
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
+{
+ class IDaemonSuspendSessionService : IpcService
+ {
+ private FriendServicePermissionLevel PermissionLevel;
+
+ public IDaemonSuspendSessionService(FriendServicePermissionLevel permissionLevel)
+ {
+ PermissionLevel = permissionLevel;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
new file mode 100644
index 00000000..2858aa46
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
@@ -0,0 +1,352 @@
+using LibHac.Ns;
+using Ryujinx.Common;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Ipc;
+using Ryujinx.HLE.HOS.Kernel.Threading;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService;
+using Ryujinx.Horizon.Common;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
+{
+ class IFriendService : IpcService
+ {
+ private FriendServicePermissionLevel _permissionLevel;
+ private KEvent _completionEvent;
+
+ public IFriendService(FriendServicePermissionLevel permissionLevel)
+ {
+ _permissionLevel = permissionLevel;
+ }
+
+ [CommandCmif(0)]
+ // GetCompletionEvent() -> handle<copy>
+ public ResultCode GetCompletionEvent(ServiceCtx context)
+ {
+ if (_completionEvent == null)
+ {
+ _completionEvent = new KEvent(context.Device.System.KernelContext);
+ }
+
+ if (context.Process.HandleTable.GenerateHandle(_completionEvent.ReadableEvent, out int completionEventHandle) != Result.Success)
+ {
+ throw new InvalidOperationException("Out of handles!");
+ }
+
+ context.Response.HandleDesc = IpcHandleDesc.MakeCopy(completionEventHandle);
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(1)]
+ // nn::friends::Cancel()
+ public ResultCode Cancel(ServiceCtx context)
+ {
+ // TODO: Original service sets an internal field to 1 here. Determine usage.
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend);
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10100)]
+ // nn::friends::GetFriendListIds(int offset, nn::account::Uid userId, nn::friends::detail::ipc::SizedFriendFilter friendFilter, ulong pidPlaceHolder, pid)
+ // -> int outCount, array<nn::account::NetworkServiceAccountId, 0xa>
+ public ResultCode GetFriendListIds(ServiceCtx context)
+ {
+ int offset = context.RequestData.ReadInt32();
+
+ // Padding
+ context.RequestData.ReadInt32();
+
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+ FriendFilter filter = context.RequestData.ReadStruct<FriendFilter>();
+
+ // Pid placeholder
+ context.RequestData.ReadInt64();
+
+ if (userId.IsNull)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ // There are no friends online, so we return 0 because the nn::account::NetworkServiceAccountId array is empty.
+ context.ResponseData.Write(0);
+
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new
+ {
+ UserId = userId.ToString(),
+ offset,
+ filter.PresenceStatus,
+ filter.IsFavoriteOnly,
+ filter.IsSameAppPresenceOnly,
+ filter.IsSameAppPlayedOnly,
+ filter.IsArbitraryAppPlayedOnly,
+ filter.PresenceGroupId,
+ });
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10101)]
+ // nn::friends::GetFriendList(int offset, nn::account::Uid userId, nn::friends::detail::ipc::SizedFriendFilter friendFilter, ulong pidPlaceHolder, pid)
+ // -> int outCount, array<nn::friends::detail::FriendImpl, 0x6>
+ public ResultCode GetFriendList(ServiceCtx context)
+ {
+ int offset = context.RequestData.ReadInt32();
+
+ // Padding
+ context.RequestData.ReadInt32();
+
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+ FriendFilter filter = context.RequestData.ReadStruct<FriendFilter>();
+
+ // Pid placeholder
+ context.RequestData.ReadInt64();
+
+ if (userId.IsNull)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ // There are no friends online, so we return 0 because the nn::account::NetworkServiceAccountId array is empty.
+ context.ResponseData.Write(0);
+
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new {
+ UserId = userId.ToString(),
+ offset,
+ filter.PresenceStatus,
+ filter.IsFavoriteOnly,
+ filter.IsSameAppPresenceOnly,
+ filter.IsSameAppPlayedOnly,
+ filter.IsArbitraryAppPlayedOnly,
+ filter.PresenceGroupId,
+ });
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10120)] // 10.0.0+
+ // nn::friends::IsFriendListCacheAvailable(nn::account::Uid userId) -> bool
+ public ResultCode IsFriendListCacheAvailable(ServiceCtx context)
+ {
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+
+ if (userId.IsNull)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ // TODO: Service mount the friends:/ system savedata and try to load friend.cache file, returns true if exists, false otherwise.
+ // NOTE: If no cache is available, guest then calls nn::friends::EnsureFriendListAvailable, we can avoid that by faking the cache check.
+ context.ResponseData.Write(true);
+
+ // TODO: Since we don't support friend features, it's fine to stub it for now.
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() });
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10121)] // 10.0.0+
+ // nn::friends::EnsureFriendListAvailable(nn::account::Uid userId)
+ public ResultCode EnsureFriendListAvailable(ServiceCtx context)
+ {
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+
+ if (userId.IsNull)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ // TODO: Service mount the friends:/ system savedata and create a friend.cache file for the given user id.
+ // Since we don't support friend features, it's fine to stub it for now.
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() });
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10400)]
+ // nn::friends::GetBlockedUserListIds(int offset, nn::account::Uid userId) -> (u32, buffer<nn::account::NetworkServiceAccountId, 0xa>)
+ public ResultCode GetBlockedUserListIds(ServiceCtx context)
+ {
+ int offset = context.RequestData.ReadInt32();
+
+ // Padding
+ context.RequestData.ReadInt32();
+
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+
+ // There are no friends blocked, so we return 0 because the nn::account::NetworkServiceAccountId array is empty.
+ context.ResponseData.Write(0);
+
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { offset, UserId = userId.ToString() });
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10600)]
+ // nn::friends::DeclareOpenOnlinePlaySession(nn::account::Uid userId)
+ public ResultCode DeclareOpenOnlinePlaySession(ServiceCtx context)
+ {
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+
+ if (userId.IsNull)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ context.Device.System.AccountManager.OpenUserOnlinePlay(userId);
+
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() });
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10601)]
+ // nn::friends::DeclareCloseOnlinePlaySession(nn::account::Uid userId)
+ public ResultCode DeclareCloseOnlinePlaySession(ServiceCtx context)
+ {
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+
+ if (userId.IsNull)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ context.Device.System.AccountManager.CloseUserOnlinePlay(userId);
+
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() });
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10610)]
+ // nn::friends::UpdateUserPresence(nn::account::Uid, u64, pid, buffer<nn::friends::detail::UserPresenceImpl, 0x19>)
+ public ResultCode UpdateUserPresence(ServiceCtx context)
+ {
+ UserId uuid = context.RequestData.ReadStruct<UserId>();
+
+ // Pid placeholder
+ context.RequestData.ReadInt64();
+
+ ulong position = context.Request.PtrBuff[0].Position;
+ ulong size = context.Request.PtrBuff[0].Size;
+
+ ReadOnlySpan<UserPresence> userPresenceInputArray = MemoryMarshal.Cast<byte, UserPresence>(context.Memory.GetSpan(position, (int)size));
+
+ if (uuid.IsNull)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = uuid.ToString(), userPresenceInputArray = userPresenceInputArray.ToArray() });
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10700)]
+ // nn::friends::GetPlayHistoryRegistrationKey(b8 unknown, nn::account::Uid) -> buffer<nn::friends::PlayHistoryRegistrationKey, 0x1a>
+ public ResultCode GetPlayHistoryRegistrationKey(ServiceCtx context)
+ {
+ bool unknownBool = context.RequestData.ReadBoolean();
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+
+ context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(0x40UL);
+
+ ulong bufferPosition = context.Request.RecvListBuff[0].Position;
+
+ if (userId.IsNull)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ // NOTE: Calls nn::friends::detail::service::core::PlayHistoryManager::GetInstance and stores the instance.
+
+ byte[] randomBytes = new byte[8];
+
+ Random.Shared.NextBytes(randomBytes);
+
+ // NOTE: Calls nn::friends::detail::service::core::UuidManager::GetInstance and stores the instance.
+ // Then call nn::friends::detail::service::core::AccountStorageManager::GetInstance and store the instance.
+ // Then it checks if an Uuid is already stored for the UserId, if not it generates a random Uuid.
+ // And store it in the savedata 8000000000000080 in the friends:/uid.bin file.
+
+ Array16<byte> randomGuid = new Array16<byte>();
+
+ Guid.NewGuid().ToByteArray().AsSpan().CopyTo(randomGuid.AsSpan());
+
+ PlayHistoryRegistrationKey playHistoryRegistrationKey = new PlayHistoryRegistrationKey
+ {
+ Type = 0x101,
+ KeyIndex = (byte)(randomBytes[0] & 7),
+ UserIdBool = 0, // TODO: Find it.
+ UnknownBool = (byte)(unknownBool ? 1 : 0), // TODO: Find it.
+ Reserved = new Array11<byte>(),
+ Uuid = randomGuid
+ };
+
+ ReadOnlySpan<byte> playHistoryRegistrationKeyBuffer = SpanHelpers.AsByteSpan(ref playHistoryRegistrationKey);
+
+ /*
+
+ NOTE: The service uses the KeyIndex to get a random key from a keys buffer (since the key index is stored in the returned buffer).
+ We currently don't support play history and online services so we can use a blank key for now.
+ Code for reference:
+
+ byte[] hmacKey = new byte[0x20];
+
+ HMACSHA256 hmacSha256 = new HMACSHA256(hmacKey);
+ byte[] hmacHash = hmacSha256.ComputeHash(playHistoryRegistrationKeyBuffer);
+
+ */
+
+ context.Memory.Write(bufferPosition, playHistoryRegistrationKeyBuffer);
+ context.Memory.Write(bufferPosition + 0x20, new byte[0x20]); // HmacHash
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(10702)]
+ // nn::friends::AddPlayHistory(nn::account::Uid, u64, pid, buffer<nn::friends::PlayHistoryRegistrationKey, 0x19>, buffer<nn::friends::InAppScreenName, 0x19>, buffer<nn::friends::InAppScreenName, 0x19>)
+ public ResultCode AddPlayHistory(ServiceCtx context)
+ {
+ UserId userId = context.RequestData.ReadStruct<UserId>();
+
+ // Pid placeholder
+ context.RequestData.ReadInt64();
+ ulong pid = context.Request.HandleDesc.PId;
+
+ ulong playHistoryRegistrationKeyPosition = context.Request.PtrBuff[0].Position;
+ ulong PlayHistoryRegistrationKeySize = context.Request.PtrBuff[0].Size;
+
+ ulong inAppScreenName1Position = context.Request.PtrBuff[1].Position;
+ ulong inAppScreenName1Size = context.Request.PtrBuff[1].Size;
+
+ ulong inAppScreenName2Position = context.Request.PtrBuff[2].Position;
+ ulong inAppScreenName2Size = context.Request.PtrBuff[2].Size;
+
+ if (userId.IsNull || inAppScreenName1Size > 0x48 || inAppScreenName2Size > 0x48)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+ ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+ /*
+
+ NOTE: The service calls nn::friends::detail::service::core::PlayHistoryManager to store informations using the registration key computed in GetPlayHistoryRegistrationKey.
+ Then calls nn::friends::detail::service::core::FriendListManager to update informations on the friend list.
+ We currently don't support play history and online services so it's fine to do nothing.
+
+ */
+
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend);
+
+ return ResultCode.Success;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs
new file mode 100644
index 00000000..063750c6
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs
@@ -0,0 +1,178 @@
+using Ryujinx.Common;
+using Ryujinx.HLE.HOS.Ipc;
+using Ryujinx.HLE.HOS.Kernel.Threading;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService;
+using Ryujinx.Horizon.Common;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
+{
+ class INotificationService : DisposableIpcService
+ {
+ private readonly UserId _userId;
+ private readonly FriendServicePermissionLevel _permissionLevel;
+
+ private readonly object _lock = new object();
+
+ private KEvent _notificationEvent;
+ private int _notificationEventHandle = 0;
+
+ private LinkedList<NotificationInfo> _notifications;
+
+ private bool _hasNewFriendRequest;
+ private bool _hasFriendListUpdate;
+
+ public INotificationService(ServiceCtx context, UserId userId, FriendServicePermissionLevel permissionLevel)
+ {
+ _userId = userId;
+ _permissionLevel = permissionLevel;
+ _notifications = new LinkedList<NotificationInfo>();
+ _notificationEvent = new KEvent(context.Device.System.KernelContext);
+
+ _hasNewFriendRequest = false;
+ _hasFriendListUpdate = false;
+
+ NotificationEventHandler.Instance.RegisterNotificationService(this);
+ }
+
+ [CommandCmif(0)] //2.0.0+
+ // nn::friends::detail::ipc::INotificationService::GetEvent() -> handle<copy>
+ public ResultCode GetEvent(ServiceCtx context)
+ {
+ if (_notificationEventHandle == 0)
+ {
+ if (context.Process.HandleTable.GenerateHandle(_notificationEvent.ReadableEvent, out _notificationEventHandle) != Result.Success)
+ {
+ throw new InvalidOperationException("Out of handles!");
+ }
+ }
+
+ context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_notificationEventHandle);
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(1)] //2.0.0+
+ // nn::friends::detail::ipc::INotificationService::Clear()
+ public ResultCode Clear(ServiceCtx context)
+ {
+ lock (_lock)
+ {
+ _hasNewFriendRequest = false;
+ _hasFriendListUpdate = false;
+
+ _notifications.Clear();
+ }
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(2)] // 2.0.0+
+ // nn::friends::detail::ipc::INotificationService::Pop() -> nn::friends::detail::ipc::SizedNotificationInfo
+ public ResultCode 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 ResultCode.Success;
+ }
+ }
+
+ return ResultCode.NotificationQueueEmpty;
+ }
+
+ public void SignalFriendListUpdate(UserId 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(UserId targetId)
+ {
+ lock (_lock)
+ {
+ if ((_permissionLevel & FriendServicePermissionLevel.ViewerMask) != 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();
+ }
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ if (isDisposing)
+ {
+ NotificationEventHandler.Instance.UnregisterNotificationService(this);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs
new file mode 100644
index 00000000..383ad006
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs
@@ -0,0 +1,83 @@
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService
+{
+ 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(UserId 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(UserId targetId)
+ {
+ for (int i = 0; i < _registry.Length; i++)
+ {
+ if (_registry[i] != null)
+ {
+ _registry[i].SignalNewFriendRequest(targetId);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationEventType.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationEventType.cs
new file mode 100644
index 00000000..5136ae8a
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationEventType.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService
+{
+ enum NotificationEventType : uint
+ {
+ Invalid = 0x0,
+ FriendListUpdate = 0x1,
+ NewFriendRequest = 0x65
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs
new file mode 100644
index 00000000..e710bf06
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x10)]
+ struct NotificationInfo
+ {
+ public NotificationEventType Type;
+ private Array4<byte> _padding;
+ public long NetworkUserIdPlaceholder;
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/FriendServicePermissionLevel.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/FriendServicePermissionLevel.cs
new file mode 100644
index 00000000..12a3d42f
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/FriendServicePermissionLevel.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
+{
+ [Flags]
+ enum FriendServicePermissionLevel
+ {
+ UserMask = 1,
+ ViewerMask = 2,
+ ManagerMask = 4,
+ SystemMask = 8,
+
+ Administrator = -1,
+ User = UserMask,
+ Viewer = UserMask | ViewerMask,
+ Manager = UserMask | ViewerMask | ManagerMask,
+ System = UserMask | SystemMask
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/PlayHistoryRegistrationKey.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/PlayHistoryRegistrationKey.cs
new file mode 100644
index 00000000..32d962c1
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/PlayHistoryRegistrationKey.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x20)]
+ struct PlayHistoryRegistrationKey
+ {
+ public ushort Type;
+ public byte KeyIndex;
+ public byte UserIdBool;
+ public byte UnknownBool;
+ public Array11<byte> Reserved;
+ public Array16<byte> Uuid;
+ }
+}