aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Horizon/Sdk/Friends
diff options
context:
space:
mode:
authorgdkchan <gab.dark.100@gmail.com>2024-01-29 18:45:40 -0300
committerGitHub <noreply@github.com>2024-01-29 22:45:40 +0100
commit4117c13377b51b83ff87b1d00393be1a5ab5bfff (patch)
treec59181e229947070b76c20d88e7b9cbba314a4e9 /src/Ryujinx.Horizon/Sdk/Friends
parent20a392ad552ce5cdbff1cb74f1d26d2f797cca31 (diff)
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
Diffstat (limited to 'src/Ryujinx.Horizon/Sdk/Friends')
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/ApplicationInfo.cs12
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendDetailedInfoImpl.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendImpl.cs19
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationGroupImpl.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendSettingImpl.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/DaemonSuspendSessionService.cs7
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendService.cs1015
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs16
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IDaemonSuspendSessionService.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IFriendService.cs97
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/INotificationService.cs12
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IServiceCreator.cs13
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventHandler.cs58
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationService.cs172
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs10
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/ServiceCreator.cs51
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedFriendFilter.cs25
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedNotificationInfo.cs13
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileExtraImpl.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceImpl.cs29
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceViewImpl.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Detail/UserSettingImpl.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalog.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalogId.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/FacedFriendRequestRegistrationKey.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/FriendCode.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGameModeDescription.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGroupId.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/FriendResult.cs13
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/InAppScreenName.cs26
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/MiiImageUrlParam.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/MiiName.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/NintendoNetworkIdUserInfo.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs18
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryStatistics.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Relationship.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/RequestId.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/SnsAccountLinkage.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/SnsAccountProfile.cs9
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/Url.cs30
-rw-r--r--src/Ryujinx.Horizon/Sdk/Friends/WebPageUrl.cs9
52 files changed, 1924 insertions, 0 deletions
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/ApplicationInfo.cs b/src/Ryujinx.Horizon/Sdk/Friends/ApplicationInfo.cs
new file mode 100644
index 00000000..23bad3d1
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs
new file mode 100644
index 00000000..d5f8a031
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ struct BlockedUserImpl
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs
new file mode 100644
index 00000000..21e99c75
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ struct FriendCandidateImpl
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendDetailedInfoImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendDetailedInfoImpl.cs
new file mode 100644
index 00000000..1b46dccd
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendImpl.cs
new file mode 100644
index 00000000..d22ca4b9
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs
new file mode 100644
index 00000000..416ba365
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ struct FriendInvitationForViewerImpl
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationGroupImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationGroupImpl.cs
new file mode 100644
index 00000000..ef923834
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs
new file mode 100644
index 00000000..ba567169
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ struct FriendRequestImpl
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendSettingImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendSettingImpl.cs
new file mode 100644
index 00000000..f711d31f
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/DaemonSuspendSessionService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/DaemonSuspendSessionService.cs
new file mode 100644
index 00000000..aaf88ed0
--- /dev/null
+++ b/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.
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendService.cs
new file mode 100644
index 00000000..1b4c8c30
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs
new file mode 100644
index 00000000..f4bbe100
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs
@@ -0,0 +1,16 @@
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+ enum FriendsServicePermissionLevel
+ {
+ UserMask = 1,
+ ViewerMask = 2,
+ ManagerMask = 4,
+ SystemMask = 8,
+
+ Admin = -1,
+ User = UserMask,
+ Viewer = UserMask | ViewerMask,
+ Manager = UserMask | ViewerMask | ManagerMask,
+ System = UserMask | SystemMask,
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IDaemonSuspendSessionService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IDaemonSuspendSessionService.cs
new file mode 100644
index 00000000..2bb0434e
--- /dev/null
+++ b/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.
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IFriendService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IFriendService.cs
new file mode 100644
index 00000000..c19d0b78
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/INotificationService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/INotificationService.cs
new file mode 100644
index 00000000..a3a28e8c
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IServiceCreator.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IServiceCreator.cs
new file mode 100644
index 00000000..58e2569b
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventHandler.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventHandler.cs
new file mode 100644
index 00000000..61c692a6
--- /dev/null
+++ b/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);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs
new file mode 100644
index 00000000..e46fc9b7
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+ enum NotificationEventType : uint
+ {
+ Invalid = 0x0,
+ FriendListUpdate = 0x1,
+ NewFriendRequest = 0x65,
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationService.cs
new file mode 100644
index 00000000..534bf63e
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs
new file mode 100644
index 00000000..3ea10587
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc
+{
+ enum PresenceStatusFilter : uint
+ {
+ None,
+ Online,
+ OnlinePlay,
+ OnlineOrOnlinePlay,
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/ServiceCreator.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/ServiceCreator.cs
new file mode 100644
index 00000000..1be804df
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedFriendFilter.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedFriendFilter.cs
new file mode 100644
index 00000000..d93a2ae2
--- /dev/null
+++ b/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} }}";
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedNotificationInfo.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedNotificationInfo.cs
new file mode 100644
index 00000000..0da26a1a
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs
new file mode 100644
index 00000000..66d61e4c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ struct NintendoNetworkIdFriendImpl
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs
new file mode 100644
index 00000000..9f90f0c8
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ struct PlayHistoryImpl
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs
new file mode 100644
index 00000000..5ddbe14e
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ enum PresenceStatus : uint
+ {
+ Offline,
+ Online,
+ OnlinePlay,
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileExtraImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileExtraImpl.cs
new file mode 100644
index 00000000..1548d725
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs
new file mode 100644
index 00000000..f779d93c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ struct ProfileImpl
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs
new file mode 100644
index 00000000..dc6adf03
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends.Detail
+{
+ struct SnsAccountFriendImpl
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceImpl.cs
new file mode 100644
index 00000000..cf4520cf
--- /dev/null
+++ b/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} }}";
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceViewImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceViewImpl.cs
new file mode 100644
index 00000000..04c09260
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserSettingImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserSettingImpl.cs
new file mode 100644
index 00000000..9d057fb1
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalog.cs b/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalog.cs
new file mode 100644
index 00000000..0d9c157d
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalogId.cs b/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalogId.cs
new file mode 100644
index 00000000..7ed36cd9
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FacedFriendRequestRegistrationKey.cs b/src/Ryujinx.Horizon/Sdk/Friends/FacedFriendRequestRegistrationKey.cs
new file mode 100644
index 00000000..6b5812f6
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendCode.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendCode.cs
new file mode 100644
index 00000000..d78497a1
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGameModeDescription.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGameModeDescription.cs
new file mode 100644
index 00000000..29b4a097
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGroupId.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGroupId.cs
new file mode 100644
index 00000000..ef53882b
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs
new file mode 100644
index 00000000..7be19d57
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs
@@ -0,0 +1,8 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+ struct FriendInvitationId
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendResult.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendResult.cs
new file mode 100644
index 00000000..5965d508
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/InAppScreenName.cs b/src/Ryujinx.Horizon/Sdk/Friends/InAppScreenName.cs
new file mode 100644
index 00000000..22574a5c
--- /dev/null
+++ b/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]);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/MiiImageUrlParam.cs b/src/Ryujinx.Horizon/Sdk/Friends/MiiImageUrlParam.cs
new file mode 100644
index 00000000..8790bb93
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/MiiName.cs b/src/Ryujinx.Horizon/Sdk/Friends/MiiName.cs
new file mode 100644
index 00000000..e73c0d83
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/NintendoNetworkIdUserInfo.cs b/src/Ryujinx.Horizon/Sdk/Friends/NintendoNetworkIdUserInfo.cs
new file mode 100644
index 00000000..a2a9e046
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs b/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs
new file mode 100644
index 00000000..bb672a79
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs
@@ -0,0 +1,18 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Horizon.Sdk.Account;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Friends
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x40)]
+ struct PlayHistoryRegistrationKey
+ {
+ public ushort Type;
+ public byte KeyIndex;
+ public byte UserIdBool;
+ public byte UnknownBool;
+ public Array11<byte> Reserved;
+ public Uid Uuid;
+ public Array32<byte> HmacHash;
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryStatistics.cs b/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryStatistics.cs
new file mode 100644
index 00000000..ea3e3d99
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Relationship.cs b/src/Ryujinx.Horizon/Sdk/Friends/Relationship.cs
new file mode 100644
index 00000000..efba09a8
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/RequestId.cs b/src/Ryujinx.Horizon/Sdk/Friends/RequestId.cs
new file mode 100644
index 00000000..3236a1d7
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountLinkage.cs b/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountLinkage.cs
new file mode 100644
index 00000000..b4660d9e
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountProfile.cs b/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountProfile.cs
new file mode 100644
index 00000000..d872b3da
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Url.cs b/src/Ryujinx.Horizon/Sdk/Friends/Url.cs
new file mode 100644
index 00000000..833ee123
--- /dev/null
+++ b/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]);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Friends/WebPageUrl.cs b/src/Ryujinx.Horizon/Sdk/Friends/WebPageUrl.cs
new file mode 100644
index 00000000..85488af6
--- /dev/null
+++ b/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
+ {
+ }
+}