Просмотр исходного кода

Migrate friends service to new IPC (#6174)

* Migrate friends service to new IPC

* Add a note that the pointer buffer size and domain counts are wrong

* Wrong length

* Format whitespace

* PR feedback

* Fill in structs from PR feedback

* Missed that one

* Somehow forgot to save that one

* Fill in enums from PR review

* Language enum, NotificationTime

* Format whitespace

* Fix the warning
gdkchan 2 лет назад
Родитель
Сommit
4117c13377
100 измененных файлов с 2348 добавлено и 832 удалено
  1. 1 1
      src/Ryujinx.HLE/HOS/Horizon.cs
  2. 12 1
      src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs
  3. 0 55
      src/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs
  4. 0 14
      src/Ryujinx.HLE/HOS/Services/Friend/ResultCode.cs
  5. 0 29
      src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs
  6. 0 24
      src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs
  7. 0 34
      src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs
  8. 0 14
      src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs
  9. 0 374
      src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
  10. 0 178
      src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs
  11. 0 74
      src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs
  12. 0 13
      src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs
  13. 49 0
      src/Ryujinx.Horizon/Friends/FriendsIpcServer.cs
  14. 17 0
      src/Ryujinx.Horizon/Friends/FriendsMain.cs
  15. 11 0
      src/Ryujinx.Horizon/Friends/FriendsPortIndex.cs
  16. 36 0
      src/Ryujinx.Horizon/Friends/FriendsServerManager.cs
  17. 4 1
      src/Ryujinx.Horizon/HorizonOptions.cs
  18. 8 0
      src/Ryujinx.Horizon/Sdk/Account/IEmulatorAccountManager.cs
  19. 20 0
      src/Ryujinx.Horizon/Sdk/Account/NetworkServiceAccountId.cs
  20. 29 0
      src/Ryujinx.Horizon/Sdk/Account/Nickname.cs
  21. 8 8
      src/Ryujinx.Horizon/Sdk/Account/Uid.cs
  22. 12 0
      src/Ryujinx.Horizon/Sdk/Friends/ApplicationInfo.cs
  23. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs
  24. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs
  25. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendDetailedInfoImpl.cs
  26. 19 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendImpl.cs
  27. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs
  28. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationGroupImpl.cs
  29. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs
  30. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendSettingImpl.cs
  31. 7 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/DaemonSuspendSessionService.cs
  32. 1015 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendService.cs
  33. 3 6
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs
  34. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IDaemonSuspendSessionService.cs
  35. 97 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IFriendService.cs
  36. 12 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/INotificationService.cs
  37. 13 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IServiceCreator.cs
  38. 58 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventHandler.cs
  39. 1 1
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs
  40. 172 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationService.cs
  41. 1 1
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs
  42. 51 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/ServiceCreator.cs
  43. 25 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedFriendFilter.cs
  44. 13 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedNotificationInfo.cs
  45. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs
  46. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs
  47. 1 1
      src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs
  48. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileExtraImpl.cs
  49. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs
  50. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs
  51. 29 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceImpl.cs
  52. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceViewImpl.cs
  53. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/Detail/UserSettingImpl.cs
  54. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalog.cs
  55. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalogId.cs
  56. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/FacedFriendRequestRegistrationKey.cs
  57. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/FriendCode.cs
  58. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGameModeDescription.cs
  59. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGroupId.cs
  60. 8 0
      src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs
  61. 13 0
      src/Ryujinx.Horizon/Sdk/Friends/FriendResult.cs
  62. 26 0
      src/Ryujinx.Horizon/Sdk/Friends/InAppScreenName.cs
  63. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/MiiImageUrlParam.cs
  64. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/MiiName.cs
  65. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/NintendoNetworkIdUserInfo.cs
  66. 5 3
      src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs
  67. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryStatistics.cs
  68. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/Relationship.cs
  69. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/RequestId.cs
  70. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/SnsAccountLinkage.cs
  71. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/SnsAccountProfile.cs
  72. 30 0
      src/Ryujinx.Horizon/Sdk/Friends/Url.cs
  73. 9 0
      src/Ryujinx.Horizon/Sdk/Friends/WebPageUrl.cs
  74. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/BatteryLot.cs
  75. 12 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerOffset.cs
  76. 12 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerScale.cs
  77. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcdsaCertificate.cs
  78. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsCertificate.cs
  79. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsKey.cs
  80. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsRootCertificate.cs
  81. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvCertificate.cs
  82. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboKey.cs
  83. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickFactoryCalibration.cs
  84. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickModelParameter.cs
  85. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/BdAddress.cs
  86. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/ConfigurationId1.cs
  87. 12 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/ConsoleSixAxisSensorHorizontalOffset.cs
  88. 8 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/CountryCode.cs
  89. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceCertificate.cs
  90. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceKey.cs
  91. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardCertificate.cs
  92. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardKey.cs
  93. 12 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeOffset.cs
  94. 12 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeScale.cs
  95. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/MacAddress.cs
  96. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceCertificate.cs
  97. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceKey.cs
  98. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/SerialNumber.cs
  99. 32 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/SpeakerParameter.cs
  100. 9 0
      src/Ryujinx.Horizon/Sdk/Settings/Factory/SslCertificate.cs

+ 1 - 1
src/Ryujinx.HLE/HOS/Horizon.cs

@@ -330,7 +330,7 @@ namespace Ryujinx.HLE.HOS
             HorizonFsClient fsClient = new(this);
 
             ServiceTable = new ServiceTable();
-            var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices, LibHacHorizonManager.BcatClient, fsClient));
+            var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices, LibHacHorizonManager.BcatClient, fsClient, AccountManager));
 
             foreach (var service in services)
             {

+ 12 - 1
src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs

@@ -4,6 +4,7 @@ using LibHac.Fs;
 using LibHac.Fs.Shim;
 using Ryujinx.Common;
 using Ryujinx.Common.Logging;
+using Ryujinx.Horizon.Sdk.Account;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -11,7 +12,7 @@ using System.Linq;
 
 namespace Ryujinx.HLE.HOS.Services.Account.Acc
 {
-    public class AccountManager
+    public class AccountManager : IEmulatorAccountManager
     {
         public static readonly UserId DefaultUserId = new("00000000000000010000000000000000");
 
@@ -106,6 +107,11 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
             _accountSaveDataManager.Save(_profiles);
         }
 
+        public void OpenUserOnlinePlay(Uid userId)
+        {
+            OpenUserOnlinePlay(new UserId((long)userId.Low, (long)userId.High));
+        }
+
         public void OpenUserOnlinePlay(UserId userId)
         {
             if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile))
@@ -127,6 +133,11 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
             _accountSaveDataManager.Save(_profiles);
         }
 
+        public void CloseUserOnlinePlay(Uid userId)
+        {
+            CloseUserOnlinePlay(new UserId((long)userId.Low, (long)userId.High));
+        }
+
         public void CloseUserOnlinePlay(UserId userId)
         {
             if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile))

+ 0 - 55
src/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs

@@ -1,55 +0,0 @@
-using Ryujinx.Common;
-using Ryujinx.HLE.HOS.Services.Account.Acc;
-using Ryujinx.HLE.HOS.Services.Friend.ServiceCreator;
-
-namespace Ryujinx.HLE.HOS.Services.Friend
-{
-    [Service("friend:a", FriendServicePermissionLevel.Administrator)]
-    [Service("friend:m", FriendServicePermissionLevel.Manager)]
-    [Service("friend:s", FriendServicePermissionLevel.System)]
-    [Service("friend:u", FriendServicePermissionLevel.User)]
-    [Service("friend:v", FriendServicePermissionLevel.Viewer)]
-    class IServiceCreator : IpcService
-    {
-        private readonly FriendServicePermissionLevel _permissionLevel;
-
-        public IServiceCreator(ServiceCtx context, FriendServicePermissionLevel permissionLevel)
-        {
-            _permissionLevel = permissionLevel;
-        }
-
-        [CommandCmif(0)]
-        // CreateFriendService() -> object<nn::friends::detail::ipc::IFriendService>
-        public ResultCode CreateFriendService(ServiceCtx context)
-        {
-            MakeObject(context, new IFriendService(_permissionLevel));
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(1)] // 2.0.0+
-        // CreateNotificationService(nn::account::Uid userId) -> object<nn::friends::detail::ipc::INotificationService>
-        public ResultCode CreateNotificationService(ServiceCtx context)
-        {
-            UserId userId = context.RequestData.ReadStruct<UserId>();
-
-            if (userId.IsNull)
-            {
-                return ResultCode.InvalidArgument;
-            }
-
-            MakeObject(context, new INotificationService(context, userId, _permissionLevel));
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(2)] // 4.0.0+
-        // CreateDaemonSuspendSessionService() -> object<nn::friends::detail::ipc::IDaemonSuspendSessionService>
-        public ResultCode CreateDaemonSuspendSessionService(ServiceCtx context)
-        {
-            MakeObject(context, new IDaemonSuspendSessionService(_permissionLevel));
-
-            return ResultCode.Success;
-        }
-    }
-}

+ 0 - 14
src/Ryujinx.HLE/HOS/Services/Friend/ResultCode.cs

@@ -1,14 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Friend
-{
-    enum ResultCode
-    {
-        ModuleId = 121,
-        ErrorCodeShift = 9,
-
-        Success = 0,
-
-        InvalidArgument = (2 << ErrorCodeShift) | ModuleId,
-        InternetRequestDenied = (6 << ErrorCodeShift) | ModuleId,
-        NotificationQueueEmpty = (15 << ErrorCodeShift) | ModuleId,
-    }
-}

+ 0 - 29
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs

@@ -1,29 +0,0 @@
-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)]
-        readonly char[] Unknown;
-
-        [MarshalAs(UnmanagedType.I1)]
-        public bool IsValid;
-    }
-}

+ 0 - 24
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs

@@ -1,24 +0,0 @@
-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;
-    }
-}

+ 0 - 34
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs

@@ -1,34 +0,0 @@
-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 readonly override string ToString()
-        {
-            return $"UserPresence {{ UserId: {UserId}, LastTimeOnlineTimestamp: {LastTimeOnlineTimestamp}, Status: {Status} }}";
-        }
-    }
-}

+ 0 - 14
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs

@@ -1,14 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
-{
-    class IDaemonSuspendSessionService : IpcService
-    {
-#pragma warning disable IDE0052 // Remove unread private member
-        private readonly FriendServicePermissionLevel _permissionLevel;
-#pragma warning restore IDE0052
-
-        public IDaemonSuspendSessionService(FriendServicePermissionLevel permissionLevel)
-        {
-            _permissionLevel = permissionLevel;
-        }
-    }
-}

+ 0 - 374
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs

@@ -1,374 +0,0 @@
-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
-    {
-#pragma warning disable IDE0052 // Remove unread private member
-        private readonly FriendServicePermissionLevel _permissionLevel;
-#pragma warning restore IDE0052
-        private KEvent _completionEvent;
-
-        public IFriendService(FriendServicePermissionLevel permissionLevel)
-        {
-            _permissionLevel = permissionLevel;
-        }
-
-        [CommandCmif(0)]
-        // GetCompletionEvent() -> handle<copy>
-        public ResultCode GetCompletionEvent(ServiceCtx context)
-        {
-            _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!");
-            }
-
-            _completionEvent.WritableEvent.Signal();
-
-            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(10420)]
-        // nn::friends::CheckBlockedUserListAvailability(nn::account::Uid userId) -> bool
-        public ResultCode CheckBlockedUserListAvailability(ServiceCtx context)
-        {
-            UserId userId = context.RequestData.ReadStruct<UserId>();
-
-            // Yes, it is available.
-            context.ResponseData.Write(true);
-
-            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { 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();
-
-            Guid.NewGuid().ToByteArray().AsSpan().CopyTo(randomGuid.AsSpan());
-
-            PlayHistoryRegistrationKey playHistoryRegistrationKey = new()
-            {
-                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();
-#pragma warning disable IDE0059 // Remove unnecessary value assignment
-            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;
-#pragma warning restore IDE0059
-            ulong inAppScreenName1Size = context.Request.PtrBuff[1].Size;
-
-#pragma warning disable IDE0059 // Remove unnecessary value assignment
-            ulong inAppScreenName2Position = context.Request.PtrBuff[2].Position;
-#pragma warning restore IDE0059
-            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.
-#pragma warning disable IDE0059 // Remove unnecessary value assignment
-            ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
-#pragma warning restore IDE0059
-
-            /*
-
-            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;
-        }
-    }
-}

+ 0 - 178
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs

@@ -1,178 +0,0 @@
-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();
-
-        private readonly KEvent _notificationEvent;
-        private int _notificationEventHandle = 0;
-
-        private readonly 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();
-
-                        if (_notifications.Count != 0)
-                        {
-                            friendListNotification = _notifications.First.Value;
-                            _notifications.RemoveFirst();
-                        }
-
-                        friendListNotification.Type = NotificationEventType.FriendListUpdate;
-                        _hasFriendListUpdate = true;
-
-                        if (_hasNewFriendRequest)
-                        {
-                            NotificationInfo newFriendRequestNotification = new();
-
-                            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()
-                        {
-                            Type = NotificationEventType.NewFriendRequest,
-                        };
-
-                        _notifications.AddLast(newFriendRequestNotification);
-                        _hasNewFriendRequest = true;
-                    }
-
-                    _notificationEvent.ReadableEvent.Signal();
-                }
-            }
-        }
-
-        protected override void Dispose(bool isDisposing)
-        {
-            if (isDisposing)
-            {
-                NotificationEventHandler.Instance.UnregisterNotificationService(this);
-            }
-        }
-    }
-}

+ 0 - 74
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs

@@ -1,74 +0,0 @@
-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 readonly object _instanceLock = new();
-
-        private readonly INotificationService[] _registry;
-
-        public static NotificationEventHandler Instance
-        {
-            get
-            {
-                lock (_instanceLock)
-                {
-                    _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++)
-            {
-                _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++)
-            {
-                _registry[i]?.SignalNewFriendRequest(targetId);
-            }
-        }
-    }
-}

+ 0 - 13
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs

@@ -1,13 +0,0 @@
-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;
-    }
-}

+ 49 - 0
src/Ryujinx.Horizon/Friends/FriendsIpcServer.cs

@@ -0,0 +1,49 @@
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+
+namespace Ryujinx.Horizon.Friends
+{
+    class FriendsIpcServer
+    {
+        private const int MaxSessionsCount = 8;
+        private const int TotalMaxSessionsCount = MaxSessionsCount * 5;
+
+        private const int PointerBufferSize = 0xA00;
+        private const int MaxDomains = 64;
+        private const int MaxDomainObjects = 16;
+        private const int MaxPortsCount = 5;
+
+        private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private FriendsServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new FriendsServerManager(allocator, _sm, MaxPortsCount, _managerOptions, TotalMaxSessionsCount);
+
+#pragma warning disable IDE0055 // Disable formatting
+            _serverManager.RegisterServer((int)FriendsPortIndex.Admin,   ServiceName.Encode("friend:a"), MaxSessionsCount);
+            _serverManager.RegisterServer((int)FriendsPortIndex.User,    ServiceName.Encode("friend:u"), MaxSessionsCount);
+            _serverManager.RegisterServer((int)FriendsPortIndex.Viewer,  ServiceName.Encode("friend:v"), MaxSessionsCount);
+            _serverManager.RegisterServer((int)FriendsPortIndex.Manager, ServiceName.Encode("friend:m"), MaxSessionsCount);
+            _serverManager.RegisterServer((int)FriendsPortIndex.System,  ServiceName.Encode("friend:s"), MaxSessionsCount);
+#pragma warning restore IDE0055
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}

+ 17 - 0
src/Ryujinx.Horizon/Friends/FriendsMain.cs

@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Friends
+{
+    class FriendsMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            FriendsIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}

+ 11 - 0
src/Ryujinx.Horizon/Friends/FriendsPortIndex.cs

@@ -0,0 +1,11 @@
+namespace Ryujinx.Horizon.Friends
+{
+    enum FriendsPortIndex
+    {
+        Admin,
+        User,
+        Viewer,
+        Manager,
+        System,
+    }
+}

+ 36 - 0
src/Ryujinx.Horizon/Friends/FriendsServerManager.cs

@@ -0,0 +1,36 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Account;
+using Ryujinx.Horizon.Sdk.Friends.Detail.Ipc;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+using System;
+
+namespace Ryujinx.Horizon.Friends
+{
+    class FriendsServerManager : ServerManager
+    {
+        private readonly IEmulatorAccountManager _accountManager;
+        private readonly NotificationEventHandler _notificationEventHandler;
+
+        public FriendsServerManager(HeapAllocator allocator, SmApi sm, int maxPorts, ManagerOptions options, int maxSessions) : base(allocator, sm, maxPorts, options, maxSessions)
+        {
+            _accountManager = HorizonStatic.Options.AccountManager;
+            _notificationEventHandler = new();
+        }
+
+        protected override Result OnNeedsToAccept(int portIndex, Server server)
+        {
+            return (FriendsPortIndex)portIndex switch
+            {
+#pragma warning disable IDE0055 // Disable formatting
+                FriendsPortIndex.Admin   => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.Admin)),
+                FriendsPortIndex.User    => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.User)),
+                FriendsPortIndex.Viewer  => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.Viewer)),
+                FriendsPortIndex.Manager => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.Manager)),
+                FriendsPortIndex.System  => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.System)),
+                _                        => throw new ArgumentOutOfRangeException(nameof(portIndex)),
+#pragma warning restore IDE0055
+            };
+        }
+    }
+}

+ 4 - 1
src/Ryujinx.Horizon/HorizonOptions.cs

@@ -1,4 +1,5 @@
 using LibHac;
+using Ryujinx.Horizon.Sdk.Account;
 using Ryujinx.Horizon.Sdk.Fs;
 
 namespace Ryujinx.Horizon
@@ -10,13 +11,15 @@ namespace Ryujinx.Horizon
 
         public HorizonClient BcatClient { get; }
         public IFsClient FsClient { get; }
+        public IEmulatorAccountManager AccountManager { get; }
 
-        public HorizonOptions(bool ignoreMissingServices, HorizonClient bcatClient, IFsClient fsClient)
+        public HorizonOptions(bool ignoreMissingServices, HorizonClient bcatClient, IFsClient fsClient, IEmulatorAccountManager accountManager)
         {
             IgnoreMissingServices = ignoreMissingServices;
             ThrowOnInvalidCommandIds = true;
             BcatClient = bcatClient;
             FsClient = fsClient;
+            AccountManager = accountManager;
         }
     }
 }

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Account/IEmulatorAccountManager.cs

@@ -0,0 +1,8 @@
+namespace Ryujinx.Horizon.Sdk.Account
+{
+    public interface IEmulatorAccountManager
+    {
+        void OpenUserOnlinePlay(Uid userId);
+        void CloseUserOnlinePlay(Uid userId);
+    }
+}

+ 20 - 0
src/Ryujinx.Horizon/Sdk/Account/NetworkServiceAccountId.cs

@@ -0,0 +1,20 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Account
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)]
+    readonly record struct NetworkServiceAccountId
+    {
+        public readonly ulong Id;
+
+        public NetworkServiceAccountId(ulong id)
+        {
+            Id = id;
+        }
+
+        public override readonly string ToString()
+        {
+            return Id.ToString("x16");
+        }
+    }
+}

+ 29 - 0
src/Ryujinx.Horizon/Sdk/Account/Nickname.cs

@@ -0,0 +1,29 @@
+using Ryujinx.Common.Memory;
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Account
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x21, Pack = 0x1)]
+    readonly struct Nickname
+    {
+        public readonly Array33<byte> Name;
+
+        public Nickname(in Array33<byte> name)
+        {
+            Name = name;
+        }
+
+        public override string ToString()
+        {
+            int length = ((ReadOnlySpan<byte>)Name.AsSpan()).IndexOf((byte)0);
+            if (length < 0)
+            {
+                length = 33;
+            }
+
+            return Encoding.UTF8.GetString(Name.AsSpan()[..length]);
+        }
+    }
+}

+ 8 - 8
src/Ryujinx.Horizon/Sdk/Account/Uid.cs

@@ -6,16 +6,16 @@ using System.Runtime.InteropServices;
 namespace Ryujinx.Horizon.Sdk.Account
 {
     [StructLayout(LayoutKind.Sequential)]
-    readonly record struct Uid
+    public readonly record struct Uid
     {
-        public readonly long High;
-        public readonly long Low;
+        public readonly ulong High;
+        public readonly ulong Low;
 
         public bool IsNull => (Low | High) == 0;
 
         public static Uid Null => new(0, 0);
 
-        public Uid(long low, long high)
+        public Uid(ulong low, ulong high)
         {
             Low = low;
             High = high;
@@ -23,8 +23,8 @@ namespace Ryujinx.Horizon.Sdk.Account
 
         public Uid(byte[] bytes)
         {
-            High = BitConverter.ToInt64(bytes, 0);
-            Low = BitConverter.ToInt64(bytes, 8);
+            High = BitConverter.ToUInt64(bytes, 0);
+            Low = BitConverter.ToUInt64(bytes, 8);
         }
 
         public Uid(string hex)
@@ -34,8 +34,8 @@ namespace Ryujinx.Horizon.Sdk.Account
                 throw new ArgumentException("Invalid Hex value!", nameof(hex));
             }
 
-            Low = Convert.ToInt64(hex[16..], 16);
-            High = Convert.ToInt64(hex[..16], 16);
+            Low = Convert.ToUInt64(hex[16..], 16);
+            High = Convert.ToUInt64(hex[..16], 16);
         }
 
         public void Write(BinaryWriter binaryWriter)

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Friends/ApplicationInfo.cs

@@ -0,0 +1,12 @@
+using Ryujinx.Horizon.Sdk.Ncm;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)]
+    struct ApplicationInfo
+    {
+        public ApplicationId ApplicationId;
+        public ulong PresenceGroupId;
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    struct BlockedUserImpl
+    {
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    struct FriendCandidateImpl
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendDetailedInfoImpl.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x800)]
+    struct FriendDetailedInfoImpl
+    {
+    }
+}

+ 19 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendImpl.cs

@@ -0,0 +1,19 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Horizon.Sdk.Account;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x200, Pack = 0x8)]
+    struct FriendImpl
+    {
+        public Uid UserId;
+        public NetworkServiceAccountId NetworkUserId;
+        public Nickname Nickname;
+        public UserPresenceImpl Presence;
+        public bool IsFavourite;
+        public bool IsNew;
+        public Array6<byte> Unknown;
+        public bool IsValid;
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    struct FriendInvitationForViewerImpl
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationGroupImpl.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x1400)]
+    struct FriendInvitationGroupImpl
+    {
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    struct FriendRequestImpl
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendSettingImpl.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x40)]
+    struct FriendSettingImpl
+    {
+    }
+}

+ 7 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/DaemonSuspendSessionService.cs

@@ -0,0 +1,7 @@
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    partial class DaemonSuspendSessionService : IDaemonSuspendSessionService
+    {
+        // NOTE: This service has no commands.
+    }
+}

+ 1015 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendService.cs

@@ -0,0 +1,1015 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Account;
+using Ryujinx.Horizon.Sdk.OsTypes;
+using Ryujinx.Horizon.Sdk.Settings;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    partial class FriendService : IFriendService, IDisposable
+    {
+        private readonly IEmulatorAccountManager _accountManager;
+        private SystemEventType _completionEvent;
+
+        public FriendService(IEmulatorAccountManager accountManager, FriendsServicePermissionLevel permissionLevel)
+        {
+            _accountManager = accountManager;
+
+            Os.CreateSystemEvent(out _completionEvent, EventClearMode.ManualClear, interProcess: true).AbortOnFailure();
+            Os.SignalSystemEvent(ref _completionEvent); // TODO: Figure out where we are supposed to signal this.
+        }
+
+        [CmifCommand(0)]
+        public Result GetCompletionEvent([CopyHandle] out int completionEventHandle)
+        {
+            completionEventHandle = Os.GetReadableHandleOfSystemEvent(ref _completionEvent);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(1)]
+        public Result Cancel()
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10100)]
+        public Result GetFriendListIds(
+            out int count,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer)] Span<NetworkServiceAccountId> friendIds,
+            Uid userId,
+            int offset,
+            SizedFriendFilter filter,
+            ulong pidPlaceholder,
+            [ClientProcessId] ulong pid)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, offset, filter, pidPlaceholder, pid });
+
+            if (userId.IsNull)
+            {
+                return FriendResult.InvalidArgument;
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10101)]
+        public Result GetFriendList(
+            out int count,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<FriendImpl> friendList,
+            Uid userId,
+            int offset,
+            SizedFriendFilter filter,
+            ulong pidPlaceholder,
+            [ClientProcessId] ulong pid)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, offset, filter, pidPlaceholder, pid });
+
+            if (userId.IsNull)
+            {
+                return FriendResult.InvalidArgument;
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10102)]
+        public Result UpdateFriendInfo(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<FriendImpl> info,
+            Uid userId,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<NetworkServiceAccountId> friendIds,
+            ulong pidPlaceholder,
+            [ClientProcessId] ulong pid)
+        {
+            string friendIdList = string.Join(", ", friendIds.ToArray());
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendIdList, pidPlaceholder, pid });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10110)]
+        public Result GetFriendProfileImage(
+            out int size,
+            Uid userId,
+            NetworkServiceAccountId friendId,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> profileImage)
+        {
+            size = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10120)]
+        public Result CheckFriendListAvailability(out bool listAvailable, Uid userId)
+        {
+            listAvailable = true;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10121)]
+        public Result EnsureFriendListAvailable(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10200)]
+        public Result SendFriendRequestForApplication(
+            Uid userId,
+            NetworkServiceAccountId friendId,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg2,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg3,
+            ulong pidPlaceholder,
+            [ClientProcessId] ulong pid)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, arg3, pidPlaceholder, pid });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10211)]
+        public Result AddFacedFriendRequestForApplication(
+            Uid userId,
+            FacedFriendRequestRegistrationKey key,
+            Nickname nickname,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> arg3,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg4,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg5,
+            ulong pidPlaceholder,
+            [ClientProcessId] ulong pid)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, key, nickname, arg4, arg5, pidPlaceholder, pid });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10400)]
+        public Result GetBlockedUserListIds(
+            out int count,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer)] Span<NetworkServiceAccountId> blockedIds,
+            Uid userId,
+            int offset)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, offset });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10420)]
+        public Result CheckBlockedUserListAvailability(out bool listAvailable, Uid userId)
+        {
+            listAvailable = true;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10421)]
+        public Result EnsureBlockedUserListAvailable(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10500)]
+        public Result GetProfileList(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<ProfileImpl> profileList,
+            Uid userId,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<NetworkServiceAccountId> friendIds)
+        {
+            string friendIdList = string.Join(", ", friendIds.ToArray());
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendIdList });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10600)]
+        public Result DeclareOpenOnlinePlaySession(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            if (userId.IsNull)
+            {
+                return FriendResult.InvalidArgument;
+            }
+
+            _accountManager.OpenUserOnlinePlay(userId);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10601)]
+        public Result DeclareCloseOnlinePlaySession(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            if (userId.IsNull)
+            {
+                return FriendResult.InvalidArgument;
+            }
+
+            _accountManager.CloseUserOnlinePlay(userId);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10610)]
+        public Result UpdateUserPresence(
+            Uid userId,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0xE0)] in UserPresenceImpl userPresence,
+            ulong pidPlaceholder,
+            [ClientProcessId] ulong pid)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, userPresence, pidPlaceholder, pid });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10700)]
+        public Result GetPlayHistoryRegistrationKey(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x40)] out PlayHistoryRegistrationKey registrationKey,
+            Uid userId,
+            bool arg2)
+        {
+            if (userId.IsNull)
+            {
+                registrationKey = default;
+
+                return FriendResult.InvalidArgument;
+            }
+
+            // NOTE: Calls nn::friends::detail::service::core::PlayHistoryManager::GetInstance and stores the instance.
+
+            // NOTE: Calls nn::friends::detail::service::core::UuidManager::GetInstance and stores the instance.
+            //       Then calls nn::friends::detail::service::core::AccountStorageManager::GetInstance and stores the instance.
+            //       Then it checks if an Uuid is already stored for the UserId, if not it generates a random Uuid,
+            //       and stores it in the savedata 8000000000000080 in the friends:/uid.bin file.
+
+            /*
+
+            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);
+
+            */
+
+            Uid randomGuid = new();
+
+            Guid.NewGuid().TryWriteBytes(MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref randomGuid, 1)));
+
+            registrationKey = new()
+            {
+                Type = 0x101,
+                KeyIndex = (byte)(Random.Shared.Next() & 7),
+                UserIdBool = 0, // TODO: Find it.
+                UnknownBool = (byte)(arg2 ? 1 : 0), // TODO: Find it.
+                Reserved = new(),
+                Uuid = randomGuid,
+                HmacHash = new(),
+            };
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10701)]
+        public Result GetPlayHistoryRegistrationKeyWithNetworkServiceAccountId(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x40)] out PlayHistoryRegistrationKey registrationKey,
+            NetworkServiceAccountId friendId,
+            bool arg2)
+        {
+            registrationKey = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { friendId, arg2 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(10702)]
+        public Result AddPlayHistory(
+            Uid userId,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x40)] in PlayHistoryRegistrationKey registrationKey,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg2,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg3,
+            ulong pidPlaceholder,
+            [ClientProcessId] ulong pid)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, registrationKey, arg2, arg3, pidPlaceholder, pid });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(11000)]
+        public Result GetProfileImageUrl(out Url imageUrl, Url url, int arg2)
+        {
+            imageUrl = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { url, arg2 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20100)]
+        public Result GetFriendCount(out int count, Uid userId, SizedFriendFilter filter, ulong pidPlaceholder, [ClientProcessId] ulong pid)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, filter, pidPlaceholder, pid });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20101)]
+        public Result GetNewlyFriendCount(out int count, Uid userId)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20102)]
+        public Result GetFriendDetailedInfo(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x800)] out FriendDetailedInfoImpl detailedInfo,
+            Uid userId,
+            NetworkServiceAccountId friendId)
+        {
+            detailedInfo = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20103)]
+        public Result SyncFriendList(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20104)]
+        public Result RequestSyncFriendList(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20110)]
+        public Result LoadFriendSetting(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x40)] out FriendSettingImpl friendSetting,
+            Uid userId,
+            NetworkServiceAccountId friendId)
+        {
+            friendSetting = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20200)]
+        public Result GetReceivedFriendRequestCount(out int count, out int count2, Uid userId)
+        {
+            count = 0;
+            count2 = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20201)]
+        public Result GetFriendRequestList(
+            out int count,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<FriendRequestImpl> requestList,
+            Uid userId,
+            int arg3,
+            int arg4)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3, arg4 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20300)]
+        public Result GetFriendCandidateList(
+            out int count,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<FriendCandidateImpl> candidateList,
+            Uid userId,
+            int arg3)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20301)]
+        public Result GetNintendoNetworkIdInfo(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x38)] out NintendoNetworkIdUserInfo networkIdInfo,
+            out int arg1,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<NintendoNetworkIdFriendImpl> friendInfo,
+            Uid userId,
+            int arg4)
+        {
+            networkIdInfo = default;
+            arg1 = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg4 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20302)]
+        public Result GetSnsAccountLinkage(out SnsAccountLinkage accountLinkage, Uid userId)
+        {
+            accountLinkage = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20303)]
+        public Result GetSnsAccountProfile(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x380)] out SnsAccountProfile accountProfile,
+            Uid userId,
+            NetworkServiceAccountId friendId,
+            int arg3)
+        {
+            accountProfile = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg3 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20304)]
+        public Result GetSnsAccountFriendList(
+            out int count,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<SnsAccountFriendImpl> friendList,
+            Uid userId,
+            int arg3)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20400)]
+        public Result GetBlockedUserList(
+            out int count,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<BlockedUserImpl> blockedUsers,
+            Uid userId,
+            int arg3)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20401)]
+        public Result SyncBlockedUserList(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20500)]
+        public Result GetProfileExtraList(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<ProfileExtraImpl> extraList,
+            Uid userId,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<NetworkServiceAccountId> friendIds)
+        {
+            string friendIdList = string.Join(", ", friendIds.ToArray());
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendIdList });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20501)]
+        public Result GetRelationship(out Relationship relationship, Uid userId, NetworkServiceAccountId friendId)
+        {
+            relationship = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20600)]
+        public Result GetUserPresenceView([Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0xE0)] out UserPresenceViewImpl userPresenceView, Uid userId)
+        {
+            userPresenceView = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20700)]
+        public Result GetPlayHistoryList(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<PlayHistoryImpl> playHistoryList, Uid userId, int arg3)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20701)]
+        public Result GetPlayHistoryStatistics(out PlayHistoryStatistics statistics, Uid userId)
+        {
+            statistics = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20800)]
+        public Result LoadUserSetting([Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x800)] out UserSettingImpl userSetting, Uid userId)
+        {
+            userSetting = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20801)]
+        public Result SyncUserSetting(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20900)]
+        public Result RequestListSummaryOverlayNotification()
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(21000)]
+        public Result GetExternalApplicationCatalog(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x4B8)] out ExternalApplicationCatalog catalog,
+            ExternalApplicationCatalogId catalogId,
+            LanguageCode language)
+        {
+            catalog = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { catalogId, language });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(22000)]
+        public Result GetReceivedFriendInvitationList(
+            out int count,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<FriendInvitationForViewerImpl> invitationList,
+            Uid userId)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(22001)]
+        public Result GetReceivedFriendInvitationDetailedInfo(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias, 0x1400)] out FriendInvitationGroupImpl invicationGroup,
+            Uid userId,
+            FriendInvitationGroupId groupId)
+        {
+            invicationGroup = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, groupId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(22010)]
+        public Result GetReceivedFriendInvitationCountCache(out int count, Uid userId)
+        {
+            count = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30100)]
+        public Result DropFriendNewlyFlags(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30101)]
+        public Result DeleteFriend(Uid userId, NetworkServiceAccountId friendId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30110)]
+        public Result DropFriendNewlyFlag(Uid userId, NetworkServiceAccountId friendId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30120)]
+        public Result ChangeFriendFavoriteFlag(Uid userId, NetworkServiceAccountId friendId, bool favoriteFlag)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, favoriteFlag });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30121)]
+        public Result ChangeFriendOnlineNotificationFlag(Uid userId, NetworkServiceAccountId friendId, bool onlineNotificationFlag)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, onlineNotificationFlag });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30200)]
+        public Result SendFriendRequest(Uid userId, NetworkServiceAccountId friendId, int arg2)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30201)]
+        public Result SendFriendRequestWithApplicationInfo(
+            Uid userId,
+            NetworkServiceAccountId friendId,
+            int arg2,
+            ApplicationInfo applicationInfo,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg4,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg5)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, applicationInfo, arg4, arg5 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30202)]
+        public Result CancelFriendRequest(Uid userId, RequestId requestId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, requestId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30203)]
+        public Result AcceptFriendRequest(Uid userId, RequestId requestId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, requestId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30204)]
+        public Result RejectFriendRequest(Uid userId, RequestId requestId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, requestId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30205)]
+        public Result ReadFriendRequest(Uid userId, RequestId requestId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, requestId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30210)]
+        public Result GetFacedFriendRequestRegistrationKey(out FacedFriendRequestRegistrationKey registrationKey, Uid userId)
+        {
+            registrationKey = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30211)]
+        public Result AddFacedFriendRequest(
+            Uid userId,
+            FacedFriendRequestRegistrationKey registrationKey,
+            Nickname nickname,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> arg3)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, registrationKey, nickname });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30212)]
+        public Result CancelFacedFriendRequest(Uid userId, NetworkServiceAccountId friendId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30213)]
+        public Result GetFacedFriendRequestProfileImage(
+            out int size,
+            Uid userId,
+            NetworkServiceAccountId friendId,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> profileImage)
+        {
+            size = 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30214)]
+        public Result GetFacedFriendRequestProfileImageFromPath(
+            out int size,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> path,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> profileImage)
+        {
+            size = 0;
+
+            string pathString = Encoding.UTF8.GetString(path);
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { pathString });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30215)]
+        public Result SendFriendRequestWithExternalApplicationCatalogId(
+            Uid userId,
+            NetworkServiceAccountId friendId,
+            int arg2,
+            ExternalApplicationCatalogId catalogId,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg4,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg5)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, catalogId, arg4, arg5 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30216)]
+        public Result ResendFacedFriendRequest(Uid userId, NetworkServiceAccountId friendId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30217)]
+        public Result SendFriendRequestWithNintendoNetworkIdInfo(
+            Uid userId,
+            NetworkServiceAccountId friendId,
+            int arg2,
+            MiiName arg3,
+            MiiImageUrlParam arg4,
+            MiiName arg5,
+            MiiImageUrlParam arg6)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, arg3, arg4, arg5, arg6 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30300)]
+        public Result GetSnsAccountLinkPageUrl([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias, 0x1000)] out WebPageUrl url, Uid userId, int arg2)
+        {
+            url = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg2 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30301)]
+        public Result UnlinkSnsAccount(Uid userId, int arg1)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg1 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30400)]
+        public Result BlockUser(Uid userId, NetworkServiceAccountId friendId, int arg2)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30401)]
+        public Result BlockUserWithApplicationInfo(
+            Uid userId,
+            NetworkServiceAccountId friendId,
+            int arg2,
+            ApplicationInfo applicationInfo,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg4)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, applicationInfo, arg4 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30402)]
+        public Result UnblockUser(Uid userId, NetworkServiceAccountId friendId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30500)]
+        public Result GetProfileExtraFromFriendCode(
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x400)] out ProfileExtraImpl profileExtra,
+            Uid userId,
+            FriendCode friendCode)
+        {
+            profileExtra = default;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendCode });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30700)]
+        public Result DeletePlayHistory(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30810)]
+        public Result ChangePresencePermission(Uid userId, int permission)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, permission });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30811)]
+        public Result ChangeFriendRequestReception(Uid userId, bool reception)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, reception });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30812)]
+        public Result ChangePlayLogPermission(Uid userId, int permission)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, permission });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30820)]
+        public Result IssueFriendCode(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30830)]
+        public Result ClearPlayLog(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30900)]
+        public Result SendFriendInvitation(
+            Uid userId,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<NetworkServiceAccountId> friendIds,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias, 0xC00)] in FriendInvitationGameModeDescription description,
+            ApplicationInfo applicationInfo,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> arg4,
+            bool arg5)
+        {
+            string friendIdList = string.Join(", ", friendIds.ToArray());
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendIdList, description, applicationInfo, arg5 });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30910)]
+        public Result ReadFriendInvitation(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<FriendInvitationId> invitationIds)
+        {
+            string invitationIdList = string.Join(", ", invitationIds.ToArray());
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, invitationIdList });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(30911)]
+        public Result ReadAllFriendInvitations(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(40100)]
+        public Result DeleteFriendListCache(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(40400)]
+        public Result DeleteBlockedUserListCache(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        [CmifCommand(49900)]
+        public Result DeleteNetworkServiceAccountCache(Uid userId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId });
+
+            return Result.Success;
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                Os.DestroySystemEvent(ref _completionEvent);
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(disposing: true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}

+ 3 - 6
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/FriendServicePermissionLevel.cs → src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs

@@ -1,16 +1,13 @@
-using System;
-
-namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
 {
-    [Flags]
-    enum FriendServicePermissionLevel
+    enum FriendsServicePermissionLevel
     {
         UserMask = 1,
         ViewerMask = 2,
         ManagerMask = 4,
         SystemMask = 8,
 
-        Administrator = -1,
+        Admin = -1,
         User = UserMask,
         Viewer = UserMask | ViewerMask,
         Manager = UserMask | ViewerMask | ManagerMask,

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IDaemonSuspendSessionService.cs

@@ -0,0 +1,9 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    interface IDaemonSuspendSessionService : IServiceObject
+    {
+        // NOTE: This service has no commands.
+    }
+}

+ 97 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IFriendService.cs

@@ -0,0 +1,97 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Account;
+using Ryujinx.Horizon.Sdk.Settings;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    interface IFriendService : IServiceObject
+    {
+        Result GetCompletionEvent(out int completionEventHandle);
+        Result Cancel();
+        Result GetFriendListIds(out int count, Span<NetworkServiceAccountId> friendIds, Uid userId, int offset, SizedFriendFilter filter, ulong pidPlaceholder, ulong pid);
+        Result GetFriendList(out int count, Span<FriendImpl> friendList, Uid userId, int offset, SizedFriendFilter filter, ulong pidPlaceholder, ulong pid);
+        Result UpdateFriendInfo(Span<FriendImpl> info, Uid userId, ReadOnlySpan<NetworkServiceAccountId> friendIds, ulong pidPlaceholder, ulong pid);
+        Result GetFriendProfileImage(out int size, Uid userId, NetworkServiceAccountId friendId, Span<byte> profileImage);
+        Result CheckFriendListAvailability(out bool listAvailable, Uid userId);
+        Result EnsureFriendListAvailable(Uid userId);
+        Result SendFriendRequestForApplication(Uid userId, NetworkServiceAccountId friendId, in InAppScreenName arg2, in InAppScreenName arg3, ulong pidPlaceholder, ulong pid);
+        Result AddFacedFriendRequestForApplication(Uid userId, FacedFriendRequestRegistrationKey key, Nickname nickname, ReadOnlySpan<byte> arg3, in InAppScreenName arg4, in InAppScreenName arg5, ulong pidPlaceholder, ulong pid);
+        Result GetBlockedUserListIds(out int count, Span<NetworkServiceAccountId> blockedIds, Uid userId, int offset);
+        Result CheckBlockedUserListAvailability(out bool listAvailable, Uid userId);
+        Result EnsureBlockedUserListAvailable(Uid userId);
+        Result GetProfileList(Span<ProfileImpl> profileList, Uid userId, ReadOnlySpan<NetworkServiceAccountId> friendIds);
+        Result DeclareOpenOnlinePlaySession(Uid userId);
+        Result DeclareCloseOnlinePlaySession(Uid userId);
+        Result UpdateUserPresence(Uid userId, in UserPresenceImpl userPresence, ulong pidPlaceholder, ulong pid);
+        Result GetPlayHistoryRegistrationKey(out PlayHistoryRegistrationKey registrationKey, Uid userId, bool arg2);
+        Result GetPlayHistoryRegistrationKeyWithNetworkServiceAccountId(out PlayHistoryRegistrationKey registrationKey, NetworkServiceAccountId friendId, bool arg2);
+        Result AddPlayHistory(Uid userId, in PlayHistoryRegistrationKey registrationKey, in InAppScreenName arg2, in InAppScreenName arg3, ulong pidPlaceholder, ulong pid);
+        Result GetProfileImageUrl(out Url imageUrl, Url url, int arg2);
+        Result GetFriendCount(out int count, Uid userId, SizedFriendFilter filter, ulong pidPlaceholder, ulong pid);
+        Result GetNewlyFriendCount(out int count, Uid userId);
+        Result GetFriendDetailedInfo(out FriendDetailedInfoImpl detailedInfo, Uid userId, NetworkServiceAccountId friendId);
+        Result SyncFriendList(Uid userId);
+        Result RequestSyncFriendList(Uid userId);
+        Result LoadFriendSetting(out FriendSettingImpl friendSetting, Uid userId, NetworkServiceAccountId friendId);
+        Result GetReceivedFriendRequestCount(out int count, out int count2, Uid userId);
+        Result GetFriendRequestList(out int count, Span<FriendRequestImpl> requestList, Uid userId, int arg3, int arg4);
+        Result GetFriendCandidateList(out int count, Span<FriendCandidateImpl> candidateList, Uid userId, int arg3);
+        Result GetNintendoNetworkIdInfo(out NintendoNetworkIdUserInfo networkIdInfo, out int arg1, Span<NintendoNetworkIdFriendImpl> friendInfo, Uid userId, int arg4);
+        Result GetSnsAccountLinkage(out SnsAccountLinkage accountLinkage, Uid userId);
+        Result GetSnsAccountProfile(out SnsAccountProfile accountProfile, Uid userId, NetworkServiceAccountId friendId, int arg3);
+        Result GetSnsAccountFriendList(out int count, Span<SnsAccountFriendImpl> friendList, Uid userId, int arg3);
+        Result GetBlockedUserList(out int count, Span<BlockedUserImpl> blockedUsers, Uid userId, int arg3);
+        Result SyncBlockedUserList(Uid userId);
+        Result GetProfileExtraList(Span<ProfileExtraImpl> extraList, Uid userId, ReadOnlySpan<NetworkServiceAccountId> friendIds);
+        Result GetRelationship(out Relationship relationship, Uid userId, NetworkServiceAccountId friendId);
+        Result GetUserPresenceView(out UserPresenceViewImpl userPresenceView, Uid userId);
+        Result GetPlayHistoryList(out int count, Span<PlayHistoryImpl> playHistoryList, Uid userId, int arg3);
+        Result GetPlayHistoryStatistics(out PlayHistoryStatistics statistics, Uid userId);
+        Result LoadUserSetting(out UserSettingImpl userSetting, Uid userId);
+        Result SyncUserSetting(Uid userId);
+        Result RequestListSummaryOverlayNotification();
+        Result GetExternalApplicationCatalog(out ExternalApplicationCatalog catalog, ExternalApplicationCatalogId catalogId, LanguageCode language);
+        Result GetReceivedFriendInvitationList(out int count, Span<FriendInvitationForViewerImpl> invitationList, Uid userId);
+        Result GetReceivedFriendInvitationDetailedInfo(out FriendInvitationGroupImpl invicationGroup, Uid userId, FriendInvitationGroupId groupId);
+        Result GetReceivedFriendInvitationCountCache(out int count, Uid userId);
+        Result DropFriendNewlyFlags(Uid userId);
+        Result DeleteFriend(Uid userId, NetworkServiceAccountId friendId);
+        Result DropFriendNewlyFlag(Uid userId, NetworkServiceAccountId friendId);
+        Result ChangeFriendFavoriteFlag(Uid userId, NetworkServiceAccountId friendId, bool favoriteFlag);
+        Result ChangeFriendOnlineNotificationFlag(Uid userId, NetworkServiceAccountId friendId, bool onlineNotificationFlag);
+        Result SendFriendRequest(Uid userId, NetworkServiceAccountId friendId, int arg2);
+        Result SendFriendRequestWithApplicationInfo(Uid userId, NetworkServiceAccountId friendId, int arg2, ApplicationInfo applicationInfo, in InAppScreenName arg4, in InAppScreenName arg5);
+        Result CancelFriendRequest(Uid userId, RequestId requestId);
+        Result AcceptFriendRequest(Uid userId, RequestId requestId);
+        Result RejectFriendRequest(Uid userId, RequestId requestId);
+        Result ReadFriendRequest(Uid userId, RequestId requestId);
+        Result GetFacedFriendRequestRegistrationKey(out FacedFriendRequestRegistrationKey registrationKey, Uid userId);
+        Result AddFacedFriendRequest(Uid userId, FacedFriendRequestRegistrationKey registrationKey, Nickname nickname, ReadOnlySpan<byte> arg3);
+        Result CancelFacedFriendRequest(Uid userId, NetworkServiceAccountId friendId);
+        Result GetFacedFriendRequestProfileImage(out int size, Uid userId, NetworkServiceAccountId friendId, Span<byte> profileImage);
+        Result GetFacedFriendRequestProfileImageFromPath(out int size, ReadOnlySpan<byte> path, Span<byte> profileImage);
+        Result SendFriendRequestWithExternalApplicationCatalogId(Uid userId, NetworkServiceAccountId friendId, int arg2, ExternalApplicationCatalogId catalogId, in InAppScreenName arg4, in InAppScreenName arg5);
+        Result ResendFacedFriendRequest(Uid userId, NetworkServiceAccountId friendId);
+        Result SendFriendRequestWithNintendoNetworkIdInfo(Uid userId, NetworkServiceAccountId friendId, int arg2, MiiName arg3, MiiImageUrlParam arg4, MiiName arg5, MiiImageUrlParam arg6);
+        Result GetSnsAccountLinkPageUrl(out WebPageUrl url, Uid userId, int arg2);
+        Result UnlinkSnsAccount(Uid userId, int arg1);
+        Result BlockUser(Uid userId, NetworkServiceAccountId friendId, int arg2);
+        Result BlockUserWithApplicationInfo(Uid userId, NetworkServiceAccountId friendId, int arg2, ApplicationInfo applicationInfo, in InAppScreenName arg4);
+        Result UnblockUser(Uid userId, NetworkServiceAccountId friendId);
+        Result GetProfileExtraFromFriendCode(out ProfileExtraImpl profileExtra, Uid userId, FriendCode friendCode);
+        Result DeletePlayHistory(Uid userId);
+        Result ChangePresencePermission(Uid userId, int permission);
+        Result ChangeFriendRequestReception(Uid userId, bool reception);
+        Result ChangePlayLogPermission(Uid userId, int permission);
+        Result IssueFriendCode(Uid userId);
+        Result ClearPlayLog(Uid userId);
+        Result SendFriendInvitation(Uid userId, ReadOnlySpan<NetworkServiceAccountId> friendIds, in FriendInvitationGameModeDescription description, ApplicationInfo applicationInfo, ReadOnlySpan<byte> arg4, bool arg5);
+        Result ReadFriendInvitation(Uid userId, ReadOnlySpan<FriendInvitationId> invitationIds);
+        Result ReadAllFriendInvitations(Uid userId);
+        Result DeleteFriendListCache(Uid userId);
+        Result DeleteBlockedUserListCache(Uid userId);
+        Result DeleteNetworkServiceAccountCache(Uid userId);
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/INotificationService.cs

@@ -0,0 +1,12 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    interface INotificationService : IServiceObject
+    {
+        Result GetEvent(out int eventHandle);
+        Result Clear();
+        Result Pop(out SizedNotificationInfo sizedNotificationInfo);
+    }
+}

+ 13 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IServiceCreator.cs

@@ -0,0 +1,13 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Account;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    interface IServiceCreator : IServiceObject
+    {
+        Result CreateFriendService(out IFriendService friendService);
+        Result CreateNotificationService(out INotificationService notificationService, Uid userId);
+        Result CreateDaemonSuspendSessionService(out IDaemonSuspendSessionService daemonSuspendSessionService);
+    }
+}

+ 58 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventHandler.cs

@@ -0,0 +1,58 @@
+using Ryujinx.Horizon.Sdk.Account;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    sealed class NotificationEventHandler
+    {
+        private readonly NotificationService[] _registry;
+
+        public NotificationEventHandler()
+        {
+            _registry = new NotificationService[0x20];
+        }
+
+        public void RegisterNotificationService(NotificationService service)
+        {
+            // NOTE: When there's no enough space 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;
+                }
+            }
+        }
+
+        public void UnregisterNotificationService(NotificationService service)
+        {
+            // NOTE: When there's no enough space 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 have enough things to go online.
+        public void SignalFriendListUpdate(Uid targetId)
+        {
+            for (int i = 0; i < _registry.Length; i++)
+            {
+                _registry[i]?.SignalFriendListUpdate(targetId);
+            }
+        }
+
+        // TODO: Use this when we have enough things to go online.
+        public void SignalNewFriendRequest(Uid targetId)
+        {
+            for (int i = 0; i < _registry.Length; i++)
+            {
+                _registry[i]?.SignalNewFriendRequest(targetId);
+            }
+        }
+    }
+}

+ 1 - 1
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationEventType.cs → src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs

@@ -1,4 +1,4 @@
-namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
 {
     enum NotificationEventType : uint
     {

+ 172 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationService.cs

@@ -0,0 +1,172 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Account;
+using Ryujinx.Horizon.Sdk.OsTypes;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    partial class NotificationService : INotificationService, IDisposable
+    {
+        private readonly NotificationEventHandler _notificationEventHandler;
+        private readonly Uid _userId;
+        private readonly FriendsServicePermissionLevel _permissionLevel;
+
+        private readonly object _lock = new();
+
+        private SystemEventType _notificationEvent;
+
+        private readonly LinkedList<SizedNotificationInfo> _notifications;
+
+        private bool _hasNewFriendRequest;
+        private bool _hasFriendListUpdate;
+
+        public NotificationService(NotificationEventHandler notificationEventHandler, Uid userId, FriendsServicePermissionLevel permissionLevel)
+        {
+            _notificationEventHandler = notificationEventHandler;
+            _userId = userId;
+            _permissionLevel = permissionLevel;
+            _notifications = new LinkedList<SizedNotificationInfo>();
+            Os.CreateSystemEvent(out _notificationEvent, EventClearMode.AutoClear, interProcess: true).AbortOnFailure();
+
+            _hasNewFriendRequest = false;
+            _hasFriendListUpdate = false;
+
+            notificationEventHandler.RegisterNotificationService(this);
+        }
+
+        [CmifCommand(0)]
+        public Result GetEvent([CopyHandle] out int eventHandle)
+        {
+            eventHandle = Os.GetReadableHandleOfSystemEvent(ref _notificationEvent);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(1)]
+        public Result Clear()
+        {
+            lock (_lock)
+            {
+                _hasNewFriendRequest = false;
+                _hasFriendListUpdate = false;
+
+                _notifications.Clear();
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(2)]
+        public Result Pop(out SizedNotificationInfo sizedNotificationInfo)
+        {
+            lock (_lock)
+            {
+                if (_notifications.Count >= 1)
+                {
+                    sizedNotificationInfo = _notifications.First.Value;
+                    _notifications.RemoveFirst();
+
+                    if (sizedNotificationInfo.Type == NotificationEventType.FriendListUpdate)
+                    {
+                        _hasFriendListUpdate = false;
+                    }
+                    else if (sizedNotificationInfo.Type == NotificationEventType.NewFriendRequest)
+                    {
+                        _hasNewFriendRequest = false;
+                    }
+
+                    return Result.Success;
+                }
+            }
+
+            sizedNotificationInfo = default;
+
+            return FriendResult.NotificationQueueEmpty;
+        }
+
+        public void SignalFriendListUpdate(Uid targetId)
+        {
+            lock (_lock)
+            {
+                if (_userId == targetId)
+                {
+                    if (!_hasFriendListUpdate)
+                    {
+                        SizedNotificationInfo friendListNotification = new();
+
+                        if (_notifications.Count != 0)
+                        {
+                            friendListNotification = _notifications.First.Value;
+                            _notifications.RemoveFirst();
+                        }
+
+                        friendListNotification.Type = NotificationEventType.FriendListUpdate;
+                        _hasFriendListUpdate = true;
+
+                        if (_hasNewFriendRequest)
+                        {
+                            SizedNotificationInfo newFriendRequestNotification = new();
+
+                            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);
+                    }
+
+                    Os.SignalSystemEvent(ref _notificationEvent);
+                }
+            }
+        }
+
+        public void SignalNewFriendRequest(Uid targetId)
+        {
+            lock (_lock)
+            {
+                if (_permissionLevel.HasFlag(FriendsServicePermissionLevel.ViewerMask) && _userId == targetId)
+                {
+                    if (!_hasNewFriendRequest)
+                    {
+                        if (_notifications.Count == 100)
+                        {
+                            SignalFriendListUpdate(targetId);
+                        }
+
+                        SizedNotificationInfo newFriendRequestNotification = new()
+                        {
+                            Type = NotificationEventType.NewFriendRequest,
+                        };
+
+                        _notifications.AddLast(newFriendRequestNotification);
+                        _hasNewFriendRequest = true;
+                    }
+
+                    Os.SignalSystemEvent(ref _notificationEvent);
+                }
+            }
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _notificationEventHandler.UnregisterNotificationService(this);
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(disposing: true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}

+ 1 - 1
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatusFilter.cs → src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs

@@ -1,4 +1,4 @@
-namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
 {
     enum PresenceStatusFilter : uint
     {

+ 51 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/ServiceCreator.cs

@@ -0,0 +1,51 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Account;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    partial class ServiceCreator : IServiceCreator
+    {
+        private readonly IEmulatorAccountManager _accountManager;
+        private readonly NotificationEventHandler _notificationEventHandler;
+        private readonly FriendsServicePermissionLevel _permissionLevel;
+
+        public ServiceCreator(IEmulatorAccountManager accountManager, NotificationEventHandler notificationEventHandler, FriendsServicePermissionLevel permissionLevel)
+        {
+            _accountManager = accountManager;
+            _notificationEventHandler = notificationEventHandler;
+            _permissionLevel = permissionLevel;
+        }
+
+        [CmifCommand(0)]
+        public Result CreateFriendService(out IFriendService friendService)
+        {
+            friendService = new FriendService(_accountManager, _permissionLevel);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(1)] // 2.0.0+
+        public Result CreateNotificationService(out INotificationService notificationService, Uid userId)
+        {
+            if (userId.IsNull)
+            {
+                notificationService = null;
+
+                return FriendResult.InvalidArgument;
+            }
+
+            notificationService = new NotificationService(_notificationEventHandler, userId, _permissionLevel);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(2)] // 4.0.0+
+        public Result CreateDaemonSuspendSessionService(out IDaemonSuspendSessionService daemonSuspendSessionService)
+        {
+            daemonSuspendSessionService = new DaemonSuspendSessionService();
+
+            return Result.Success;
+        }
+    }
+}

+ 25 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedFriendFilter.cs

@@ -0,0 +1,25 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)]
+    struct SizedFriendFilter
+    {
+        public PresenceStatusFilter PresenceStatus;
+        public bool IsFavoriteOnly;
+        public bool IsSameAppPresenceOnly;
+        public bool IsSameAppPlayedOnly;
+        public bool IsArbitraryAppPlayedOnly;
+        public ulong PresenceGroupId;
+
+        public readonly override string ToString()
+        {
+            return $"{{ PresenceStatus: {PresenceStatus}, " +
+                $"IsFavoriteOnly: {IsFavoriteOnly}, " +
+                $"IsSameAppPresenceOnly: {IsSameAppPresenceOnly}, " +
+                $"IsSameAppPlayedOnly: {IsSameAppPlayedOnly}, " +
+                $"IsArbitraryAppPlayedOnly: {IsArbitraryAppPlayedOnly}, " +
+                $"PresenceGroupId: {PresenceGroupId} }}";
+        }
+    }
+}

+ 13 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedNotificationInfo.cs

@@ -0,0 +1,13 @@
+using Ryujinx.Horizon.Sdk.Account;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)]
+    struct SizedNotificationInfo
+    {
+        public NotificationEventType Type;
+        public uint Padding;
+        public NetworkServiceAccountId NetworkUserIdPlaceholder;
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    struct NintendoNetworkIdFriendImpl
+    {
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    struct PlayHistoryImpl
+    {
+    }
+}

+ 1 - 1
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatus.cs → src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs

@@ -1,4 +1,4 @@
-namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
 {
     enum PresenceStatus : uint
     {

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileExtraImpl.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x400)]
+    struct ProfileExtraImpl
+    {
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    struct ProfileImpl
+    {
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    struct SnsAccountFriendImpl
+    {
+    }
+}

+ 29 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceImpl.cs

@@ -0,0 +1,29 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Horizon.Sdk.Account;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0xE0)]
+    struct UserPresenceImpl
+    {
+        public Uid UserId;
+        public long LastTimeOnlineTimestamp;
+        public PresenceStatus Status;
+        public bool SamePresenceGroupApplication;
+        public Array3<byte> Unknown;
+        public AppKeyValueStorageHolder AppKeyValueStorage;
+
+        [InlineArray(0xC0)]
+        public struct AppKeyValueStorageHolder
+        {
+            public byte Value;
+        }
+
+        public readonly override string ToString()
+        {
+            return $"{{ UserId: {UserId}, LastTimeOnlineTimestamp: {LastTimeOnlineTimestamp}, Status: {Status} }}";
+        }
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceViewImpl.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0xE0)]
+    struct UserPresenceViewImpl
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/Detail/UserSettingImpl.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x800)]
+    struct UserSettingImpl
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalog.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x4B8)]
+    struct ExternalApplicationCatalog
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalogId.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)]
+    struct ExternalApplicationCatalogId
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/FacedFriendRequestRegistrationKey.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x40, Pack = 0x1)]
+    struct FacedFriendRequestRegistrationKey
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/FriendCode.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 0x1)]
+    struct FriendCode
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGameModeDescription.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0xC00)]
+    struct FriendInvitationGameModeDescription
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGroupId.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)]
+    struct FriendInvitationGroupId
+    {
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    struct FriendInvitationId
+    {
+    }
+}

+ 13 - 0
src/Ryujinx.Horizon/Sdk/Friends/FriendResult.cs

@@ -0,0 +1,13 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    static class FriendResult
+    {
+        private const int ModuleId = 121;
+
+        public static Result InvalidArgument => new(ModuleId, 2);
+        public static Result InternetRequestDenied => new(ModuleId, 6);
+        public static Result NotificationQueueEmpty => new(ModuleId, 15);
+    }
+}

+ 26 - 0
src/Ryujinx.Horizon/Sdk/Friends/InAppScreenName.cs

@@ -0,0 +1,26 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Horizon.Sdk.Settings;
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x48)]
+    struct InAppScreenName
+    {
+        public Array64<byte> Name;
+        public LanguageCode LanguageCode;
+
+        public override readonly string ToString()
+        {
+            int length = Name.AsSpan().IndexOf((byte)0);
+            if (length < 0)
+            {
+                length = 64;
+            }
+
+            return Encoding.UTF8.GetString(Name.AsSpan()[..length]);
+        }
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/MiiImageUrlParam.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x1)]
+    struct MiiImageUrlParam
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/MiiName.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 0x1)]
+    struct MiiName
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/NintendoNetworkIdUserInfo.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x38)]
+    struct NintendoNetworkIdUserInfo
+    {
+    }
+}

+ 5 - 3
src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/PlayHistoryRegistrationKey.cs → src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs

@@ -1,9 +1,10 @@
 using Ryujinx.Common.Memory;
+using Ryujinx.Horizon.Sdk.Account;
 using System.Runtime.InteropServices;
 
-namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
+namespace Ryujinx.Horizon.Sdk.Friends
 {
-    [StructLayout(LayoutKind.Sequential, Size = 0x20)]
+    [StructLayout(LayoutKind.Sequential, Size = 0x40)]
     struct PlayHistoryRegistrationKey
     {
         public ushort Type;
@@ -11,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
         public byte UserIdBool;
         public byte UnknownBool;
         public Array11<byte> Reserved;
-        public Array16<byte> Uuid;
+        public Uid Uuid;
+        public Array32<byte> HmacHash;
     }
 }

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryStatistics.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)]
+    struct PlayHistoryStatistics
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/Relationship.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x1)]
+    struct Relationship
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/RequestId.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)]
+    struct RequestId
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/SnsAccountLinkage.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x1)]
+    struct SnsAccountLinkage
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/SnsAccountProfile.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x380)]
+    struct SnsAccountProfile
+    {
+    }
+}

+ 30 - 0
src/Ryujinx.Horizon/Sdk/Friends/Url.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0xA0, Pack = 0x1)]
+    struct Url
+    {
+        public UrlStorage Path;
+
+        [InlineArray(0xA0)]
+        public struct UrlStorage
+        {
+            public byte Value;
+        }
+
+        public override readonly string ToString()
+        {
+            int length = ((ReadOnlySpan<byte>)Path).IndexOf((byte)0);
+            if (length < 0)
+            {
+                length = 33;
+            }
+
+            return Encoding.UTF8.GetString(((ReadOnlySpan<byte>)Path)[..length]);
+        }
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Friends/WebPageUrl.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x1000)]
+    struct WebPageUrl
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/BatteryLot.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x1)]
+    struct BatteryLot
+    {
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerOffset.cs

@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)]
+    struct AccelerometerOffset
+    {
+        public ushort X;
+        public ushort Y;
+        public ushort Z;
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerScale.cs

@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)]
+    struct AccelerometerScale
+    {
+        public ushort X;
+        public ushort Y;
+        public ushort Z;
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcdsaCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x74, Pack = 0x4)]
+    struct AmiiboEcdsaCertificate
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x24, Pack = 0x4)]
+    struct AmiiboEcqvBlsCertificate
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsKey.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x48, Pack = 0x4)]
+    struct AmiiboEcqvBlsKey
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsRootCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x94, Pack = 0x4)]
+    struct AmiiboEcqvBlsRootCertificate
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x4)]
+    struct AmiiboEcqvCertificate
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboKey.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x58, Pack = 0x4)]
+    struct AmiiboKey
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickFactoryCalibration.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x9, Pack = 0x1)]
+    struct AnalogStickFactoryCalibration
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickModelParameter.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x12, Pack = 0x1)]
+    struct AnalogStickModelParameter
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/BdAddress.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x1)]
+    struct BdAddress
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/ConfigurationId1.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x1E, Pack = 0x1)]
+    struct ConfigurationId1
+    {
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/ConsoleSixAxisSensorHorizontalOffset.cs

@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)]
+    struct ConsoleSixAxisSensorHorizontalOffset
+    {
+        public ushort X;
+        public ushort Y;
+        public ushort Z;
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/CountryCode.cs

@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    struct CountryCode
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x180)]
+    struct EccB233DeviceCertificate
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceKey.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x58, Pack = 0x4)]
+    struct EccB233DeviceKey
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x400)]
+    struct GameCardCertificate
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardKey.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x138)]
+    struct GameCardKey
+    {
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeOffset.cs

@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)]
+    struct GyroscopeOffset
+    {
+        public ushort X;
+        public ushort Y;
+        public ushort Z;
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeScale.cs

@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)]
+    struct GyroscopeScale
+    {
+        public ushort X;
+        public ushort Y;
+        public ushort Z;
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/MacAddress.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x1)]
+    struct MacAddress
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x240)]
+    struct Rsa2048DeviceCertificate
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceKey.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x248)]
+    struct Rsa2048DeviceKey
+    {
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/SerialNumber.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x1)]
+    struct SerialNumber
+    {
+    }
+}

+ 32 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/SpeakerParameter.cs

@@ -0,0 +1,32 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x5A, Pack = 0x2)]
+    struct SpeakerParameter
+    {
+        public ushort Version;
+        public Array34<byte> Reserved;
+        public ushort SpeakerHpf2A1;
+        public ushort SpeakerHpf2A2;
+        public ushort SpeakerHpf2H0;
+        public ushort SpeakerEqInputVolume;
+        public ushort SpeakerEqOutputVolume;
+        public ushort SpeakerEqCtrl1;
+        public ushort SpeakerEqCtrl2;
+        public ushort SpeakerDrcAgcCtrl2;
+        public ushort SpeakerDrcAgcCtrl3;
+        public ushort SpeakerDrcAgcCtrl1;
+        public ushort SpeakerAnalogVolume;
+        public ushort HeadphoneAnalogVolume;
+        public ushort SpeakerDigitalVolumeMin;
+        public ushort SpeakerDigitalVolumeMax;
+        public ushort HeadphoneDigitalVolumeMin;
+        public ushort HeadphoneDigitalVolumeMax;
+        public ushort MicFixedGain;
+        public ushort MicVariableVolumeMin;
+        public ushort MicVariableVolumeMax;
+        public Array16<byte> Reserved2;
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Settings/Factory/SslCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Settings.Factory
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x804)]
+    struct SslCertificate
+    {
+    }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов