diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.HLE/HOS/Services/Account | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Services/Account')
27 files changed, 1951 insertions, 0 deletions
diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs new file mode 100644 index 00000000..f5364329 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs @@ -0,0 +1,241 @@ +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + public class AccountManager + { + public static readonly UserId DefaultUserId = new UserId("00000000000000010000000000000000"); + + private readonly AccountSaveDataManager _accountSaveDataManager; + + // Todo: The account service doesn't have the permissions to delete save data. Qlaunch takes care of deleting + // save data, so we're currently passing a client with full permissions. Consider moving save data deletion + // outside of the AccountManager. + private readonly HorizonClient _horizonClient; + + private readonly ConcurrentDictionary<string, UserProfile> _profiles; + private UserProfile[] _storedOpenedUsers; + + public UserProfile LastOpenedUser { get; private set; } + + public AccountManager(HorizonClient horizonClient, string initialProfileName = null) + { + _horizonClient = horizonClient; + + _profiles = new ConcurrentDictionary<string, UserProfile>(); + _storedOpenedUsers = Array.Empty<UserProfile>(); + + _accountSaveDataManager = new AccountSaveDataManager(_profiles); + + if (!_profiles.TryGetValue(DefaultUserId.ToString(), out _)) + { + byte[] defaultUserImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/DefaultUserImage.jpg"); + + AddUser("RyuPlayer", defaultUserImage, DefaultUserId); + + OpenUser(DefaultUserId); + } + else + { + UserId commandLineUserProfileOverride = default; + if (!string.IsNullOrEmpty(initialProfileName)) + { + commandLineUserProfileOverride = _profiles.Values.FirstOrDefault(x => x.Name == initialProfileName)?.UserId ?? default; + if (commandLineUserProfileOverride.IsNull) + Logger.Warning?.Print(LogClass.Application, $"The command line specified profile named '{initialProfileName}' was not found"); + } + OpenUser(commandLineUserProfileOverride.IsNull ? _accountSaveDataManager.LastOpened : commandLineUserProfileOverride); + } + } + + public void AddUser(string name, byte[] image, UserId userId = new UserId()) + { + if (userId.IsNull) + { + userId = new UserId(Guid.NewGuid().ToString().Replace("-", "")); + } + + UserProfile profile = new UserProfile(userId, name, image); + + _profiles.AddOrUpdate(userId.ToString(), profile, (key, old) => profile); + + _accountSaveDataManager.Save(_profiles); + } + + public void OpenUser(UserId userId) + { + if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile)) + { + // TODO: Support multiple open users ? + foreach (UserProfile userProfile in GetAllUsers()) + { + if (userProfile == LastOpenedUser) + { + userProfile.AccountState = AccountState.Closed; + + break; + } + } + + (LastOpenedUser = profile).AccountState = AccountState.Open; + + _accountSaveDataManager.LastOpened = userId; + } + + _accountSaveDataManager.Save(_profiles); + } + + public void CloseUser(UserId userId) + { + if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile)) + { + profile.AccountState = AccountState.Closed; + } + + _accountSaveDataManager.Save(_profiles); + } + + public void OpenUserOnlinePlay(UserId userId) + { + if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile)) + { + // TODO: Support multiple open online users ? + foreach (UserProfile userProfile in GetAllUsers()) + { + if (userProfile == LastOpenedUser) + { + userProfile.OnlinePlayState = AccountState.Closed; + + break; + } + } + + profile.OnlinePlayState = AccountState.Open; + } + + _accountSaveDataManager.Save(_profiles); + } + + public void CloseUserOnlinePlay(UserId userId) + { + if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile)) + { + profile.OnlinePlayState = AccountState.Closed; + } + + _accountSaveDataManager.Save(_profiles); + } + + public void SetUserImage(UserId userId, byte[] image) + { + foreach (UserProfile userProfile in GetAllUsers()) + { + if (userProfile.UserId == userId) + { + userProfile.Image = image; + + break; + } + } + + _accountSaveDataManager.Save(_profiles); + } + + public void SetUserName(UserId userId, string name) + { + foreach (UserProfile userProfile in GetAllUsers()) + { + if (userProfile.UserId == userId) + { + userProfile.Name = name; + + break; + } + } + + _accountSaveDataManager.Save(_profiles); + } + + public void DeleteUser(UserId userId) + { + DeleteSaveData(userId); + + _profiles.Remove(userId.ToString(), out _); + + OpenUser(DefaultUserId); + + _accountSaveDataManager.Save(_profiles); + } + + private void DeleteSaveData(UserId userId) + { + var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: default, + new LibHac.Fs.UserId((ulong)userId.High, (ulong)userId.Low), saveDataId: default, index: default); + + using var saveDataIterator = new UniqueRef<SaveDataIterator>(); + + _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref, SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10]; + + while (true) + { + saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure(); + + if (readCount == 0) + { + break; + } + + for (int i = 0; i < readCount; i++) + { + _horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, saveDataInfo[i].SaveDataId).ThrowIfFailure(); + } + } + } + + internal int GetUserCount() + { + return _profiles.Count; + } + + internal bool TryGetUser(UserId userId, out UserProfile profile) + { + return _profiles.TryGetValue(userId.ToString(), out profile); + } + + public IEnumerable<UserProfile> GetAllUsers() + { + return _profiles.Values; + } + + internal IEnumerable<UserProfile> GetOpenedUsers() + { + return _profiles.Values.Where(x => x.AccountState == AccountState.Open); + } + + internal IEnumerable<UserProfile> GetStoredOpenedUsers() + { + return _storedOpenedUsers; + } + + internal void StoreOpenedUsers() + { + _storedOpenedUsers = _profiles.Values.Where(x => x.AccountState == AccountState.Open).ToArray(); + } + + internal UserProfile GetFirst() + { + return _profiles.First().Value; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs new file mode 100644 index 00000000..535779d2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs @@ -0,0 +1,76 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Account.Acc.Types; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + class AccountSaveDataManager + { + private readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json"); + + private static readonly ProfilesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public UserId LastOpened { get; set; } + + public AccountSaveDataManager(ConcurrentDictionary<string, UserProfile> profiles) + { + // TODO: Use 0x8000000000000010 system savedata instead of a JSON file if needed. + + if (File.Exists(_profilesJsonPath)) + { + try + { + ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, SerializerContext.ProfilesJson); + + foreach (var profile in profilesJson.Profiles) + { + UserProfile addedProfile = new UserProfile(new UserId(profile.UserId), profile.Name, profile.Image, profile.LastModifiedTimestamp); + + profiles.AddOrUpdate(profile.UserId, addedProfile, (key, old) => addedProfile); + } + + LastOpened = new UserId(profilesJson.LastOpened); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.Application, $"Failed to parse {_profilesJsonPath}: {e.Message} Loading default profile!"); + + LastOpened = AccountManager.DefaultUserId; + } + } + else + { + LastOpened = AccountManager.DefaultUserId; + } + } + + public void Save(ConcurrentDictionary<string, UserProfile> profiles) + { + ProfilesJson profilesJson = new ProfilesJson() + { + Profiles = new List<UserProfileJson>(), + LastOpened = LastOpened.ToString() + }; + + foreach (var profile in profiles) + { + profilesJson.Profiles.Add(new UserProfileJson() + { + UserId = profile.Value.UserId.ToString(), + Name = profile.Value.Name, + AccountState = profile.Value.AccountState, + OnlinePlayState = profile.Value.OnlinePlayState, + LastModifiedTimestamp = profile.Value.LastModifiedTimestamp, + Image = profile.Value.Image, + }); + } + + JsonHelper.SerializeToFile(_profilesJsonPath, profilesJson, SerializerContext.ProfilesJson); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IManagerForApplication.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IManagerForApplication.cs new file mode 100644 index 00000000..9c058cb5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IManagerForApplication.cs @@ -0,0 +1,75 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService +{ + class IManagerForApplication : IpcService + { + private ManagerServer _managerServer; + + public IManagerForApplication(UserId userId) + { + _managerServer = new ManagerServer(userId); + } + + [CommandCmif(0)] + // CheckAvailability() + public ResultCode CheckAvailability(ServiceCtx context) + { + return _managerServer.CheckAvailability(context); + } + + [CommandCmif(1)] + // GetAccountId() -> nn::account::NetworkServiceAccountId + public ResultCode GetAccountId(ServiceCtx context) + { + return _managerServer.GetAccountId(context); + } + + [CommandCmif(2)] + // EnsureIdTokenCacheAsync() -> object<nn::account::detail::IAsyncContext> + public ResultCode EnsureIdTokenCacheAsync(ServiceCtx context) + { + ResultCode resultCode = _managerServer.EnsureIdTokenCacheAsync(context, out IAsyncContext asyncContext); + + if (resultCode == ResultCode.Success) + { + MakeObject(context, asyncContext); + } + + return resultCode; + } + + [CommandCmif(3)] + // LoadIdTokenCache() -> (u32 id_token_cache_size, buffer<bytes, 6>) + public ResultCode LoadIdTokenCache(ServiceCtx context) + { + return _managerServer.LoadIdTokenCache(context); + } + + [CommandCmif(130)] + // GetNintendoAccountUserResourceCacheForApplication() -> (nn::account::NintendoAccountId, nn::account::nas::NasUserBaseForApplication, buffer<bytes, 6>) + public ResultCode GetNintendoAccountUserResourceCacheForApplication(ServiceCtx context) + { + return _managerServer.GetNintendoAccountUserResourceCacheForApplication(context); + } + + [CommandCmif(160)] // 5.0.0+ + // StoreOpenContext() + public ResultCode StoreOpenContext(ServiceCtx context) + { + return _managerServer.StoreOpenContext(context); + } + + [CommandCmif(170)] // 6.0.0+ + // LoadNetworkServiceLicenseKindAsync() -> object<nn::account::detail::IAsyncNetworkServiceLicenseKindContext> + public ResultCode LoadNetworkServiceLicenseKindAsync(ServiceCtx context) + { + ResultCode resultCode = _managerServer.LoadNetworkServiceLicenseKindAsync(context, out IAsyncNetworkServiceLicenseKindContext asyncContext); + + if (resultCode == ResultCode.Success) + { + MakeObject(context, asyncContext); + } + + return resultCode; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IManagerForSystemService.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IManagerForSystemService.cs new file mode 100644 index 00000000..ecd51687 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IManagerForSystemService.cs @@ -0,0 +1,47 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService +{ + class IManagerForSystemService : IpcService + { + private ManagerServer _managerServer; + + public IManagerForSystemService(UserId userId) + { + _managerServer = new ManagerServer(userId); + } + + [CommandCmif(0)] + // CheckAvailability() + public ResultCode CheckAvailability(ServiceCtx context) + { + return _managerServer.CheckAvailability(context); + } + + [CommandCmif(1)] + // GetAccountId() -> nn::account::NetworkServiceAccountId + public ResultCode GetAccountId(ServiceCtx context) + { + return _managerServer.GetAccountId(context); + } + + [CommandCmif(2)] + // EnsureIdTokenCacheAsync() -> object<nn::account::detail::IAsyncContext> + public ResultCode EnsureIdTokenCacheAsync(ServiceCtx context) + { + ResultCode resultCode = _managerServer.EnsureIdTokenCacheAsync(context, out IAsyncContext asyncContext); + + if (resultCode == ResultCode.Success) + { + MakeObject(context, asyncContext); + } + + return resultCode; + } + + [CommandCmif(3)] + // LoadIdTokenCache() -> (u32 id_token_cache_size, buffer<bytes, 6>) + public ResultCode LoadIdTokenCache(ServiceCtx context) + { + return _managerServer.LoadIdTokenCache(context); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IProfile.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IProfile.cs new file mode 100644 index 00000000..14911dfb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IProfile.cs @@ -0,0 +1,40 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService +{ + class IProfile : IpcService + { + private ProfileServer _profileServer; + + public IProfile(UserProfile profile) + { + _profileServer = new ProfileServer(profile); + } + + [CommandCmif(0)] + // Get() -> (nn::account::profile::ProfileBase, buffer<nn::account::profile::UserData, 0x1a>) + public ResultCode Get(ServiceCtx context) + { + return _profileServer.Get(context); + } + + [CommandCmif(1)] + // GetBase() -> nn::account::profile::ProfileBase + public ResultCode GetBase(ServiceCtx context) + { + return _profileServer.GetBase(context); + } + + [CommandCmif(10)] + // GetImageSize() -> u32 + public ResultCode GetImageSize(ServiceCtx context) + { + return _profileServer.GetImageSize(context); + } + + [CommandCmif(11)] + // LoadImage() -> (u32, buffer<bytes, 6>) + public ResultCode LoadImage(ServiceCtx context) + { + return _profileServer.LoadImage(context); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IProfileEditor.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IProfileEditor.cs new file mode 100644 index 00000000..64b6070f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/IProfileEditor.cs @@ -0,0 +1,54 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService +{ + class IProfileEditor : IpcService + { + private ProfileServer _profileServer; + + public IProfileEditor(UserProfile profile) + { + _profileServer = new ProfileServer(profile); + } + + [CommandCmif(0)] + // Get() -> (nn::account::profile::ProfileBase, buffer<nn::account::profile::UserData, 0x1a>) + public ResultCode Get(ServiceCtx context) + { + return _profileServer.Get(context); + } + + [CommandCmif(1)] + // GetBase() -> nn::account::profile::ProfileBase + public ResultCode GetBase(ServiceCtx context) + { + return _profileServer.GetBase(context); + } + + [CommandCmif(10)] + // GetImageSize() -> u32 + public ResultCode GetImageSize(ServiceCtx context) + { + return _profileServer.GetImageSize(context); + } + + [CommandCmif(11)] + // LoadImage() -> (u32, buffer<bytes, 6>) + public ResultCode LoadImage(ServiceCtx context) + { + return _profileServer.LoadImage(context); + } + + [CommandCmif(100)] + // Store(nn::account::profile::ProfileBase, buffer<nn::account::profile::UserData, 0x19>) + public ResultCode Store(ServiceCtx context) + { + return _profileServer.Store(context); + } + + [CommandCmif(101)] + // StoreWithImage(nn::account::profile::ProfileBase, buffer<nn::account::profile::UserData, 0x19>, buffer<bytes, 5>) + public ResultCode StoreWithImage(ServiceCtx context) + { + return _profileServer.StoreWithImage(context); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ManagerServer.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ManagerServer.cs new file mode 100644 index 00000000..97240311 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ManagerServer.cs @@ -0,0 +1,187 @@ +using Microsoft.IdentityModel.Tokens; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.HLE.HOS.Services.Account.Acc.AsyncContext; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService +{ + class ManagerServer + { + // TODO: Determine where and how NetworkServiceAccountId is set. + private const long NetworkServiceAccountId = 0xcafe; + + private UserId _userId; + + public ManagerServer(UserId userId) + { + _userId = userId; + } + + private static string GenerateIdToken() + { + using RSA provider = RSA.Create(2048); + + RSAParameters parameters = provider.ExportParameters(true); + + RsaSecurityKey secKey = new RsaSecurityKey(parameters); + + SigningCredentials credentials = new SigningCredentials(secKey, "RS256"); + + credentials.Key.KeyId = parameters.ToString(); + + var header = new JwtHeader(credentials) + { + { "jku", "https://e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com/1.0.0/certificates" } + }; + + byte[] rawUserId = new byte[0x10]; + RandomNumberGenerator.Fill(rawUserId); + + byte[] deviceId = new byte[0x10]; + RandomNumberGenerator.Fill(deviceId); + + byte[] deviceAccountId = new byte[0x10]; + RandomNumberGenerator.Fill(deviceId); + + var payload = new JwtPayload + { + { "sub", Convert.ToHexString(rawUserId).ToLower() }, + { "aud", "ed9e2f05d286f7b8" }, + { "di", Convert.ToHexString(deviceId).ToLower() }, + { "sn", "XAW10000000000" }, + { "bs:did", Convert.ToHexString(deviceAccountId).ToLower() }, + { "iss", "https://e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com" }, + { "typ", "id_token" }, + { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, + { "jti", Guid.NewGuid().ToString() }, + { "exp", (DateTimeOffset.UtcNow + TimeSpan.FromHours(3)).ToUnixTimeSeconds() } + }; + + JwtSecurityToken securityToken = new JwtSecurityToken(header, payload); + + return new JwtSecurityTokenHandler().WriteToken(securityToken); + } + + public ResultCode CheckAvailability(ServiceCtx context) + { + // NOTE: This opens the file at "su/baas/USERID_IN_UUID_STRING.dat" where USERID_IN_UUID_STRING is formatted as "%08x-%04x-%04x-%02x%02x-%08x%04x". + // Then it searches the Availability of Online Services related to the UserId in this file and returns it. + + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + // NOTE: Even if we try to return different error codes here, the guest still needs other calls. + return ResultCode.Success; + } + + public ResultCode GetAccountId(ServiceCtx context) + { + // NOTE: This opens the file at "su/baas/USERID_IN_UUID_STRING.dat" (where USERID_IN_UUID_STRING is formatted + // as "%08x-%04x-%04x-%02x%02x-%08x%04x") in the account:/ savedata. + // Then it searches the NetworkServiceAccountId related to the UserId in this file and returns it. + + Logger.Stub?.PrintStub(LogClass.ServiceAcc, new { NetworkServiceAccountId }); + + context.ResponseData.Write(NetworkServiceAccountId); + + return ResultCode.Success; + } + + public ResultCode EnsureIdTokenCacheAsync(ServiceCtx context, out IAsyncContext asyncContext) + { + KEvent asyncEvent = new KEvent(context.Device.System.KernelContext); + AsyncExecution asyncExecution = new AsyncExecution(asyncEvent); + + asyncExecution.Initialize(1000, EnsureIdTokenCacheAsyncImpl); + + asyncContext = new IAsyncContext(asyncExecution); + + // return ResultCode.NullObject if the IAsyncContext pointer is null. Doesn't occur in our case. + + return ResultCode.Success; + } + + private async Task EnsureIdTokenCacheAsyncImpl(CancellationToken token) + { + // NOTE: This open the file at "su/baas/USERID_IN_UUID_STRING.dat" (where USERID_IN_UUID_STRING is formatted as "%08x-%04x-%04x-%02x%02x-%08x%04x") + // in the "account:/" savedata. + // Then its read data, use dauth API with this data to get the Token Id and probably store the dauth response + // in "su/cache/USERID_IN_UUID_STRING.dat" (where USERID_IN_UUID_STRING is formatted as "%08x-%04x-%04x-%02x%02x-%08x%04x") in the "account:/" savedata. + // Since we don't support online services, we can stub it. + + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + // TODO: Use a real function instead, with the CancellationToken. + await Task.CompletedTask; + } + + public ResultCode LoadIdTokenCache(ServiceCtx context) + { + ulong bufferPosition = context.Request.ReceiveBuff[0].Position; + ulong bufferSize = context.Request.ReceiveBuff[0].Size; + + // NOTE: This opens the file at "su/cache/USERID_IN_UUID_STRING.dat" (where USERID_IN_UUID_STRING is formatted as "%08x-%04x-%04x-%02x%02x-%08x%04x") + // in the "account:/" savedata and writes some data in the buffer. + // Since we don't support online services, we can stub it. + + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + /* + if (internal_object != null) + { + if (bufferSize > 0xC00) + { + return ResultCode.InvalidIdTokenCacheBufferSize; + } + } + */ + + byte[] tokenData = Encoding.ASCII.GetBytes(GenerateIdToken()); + + context.Memory.Write(bufferPosition, tokenData); + context.ResponseData.Write(tokenData.Length); + + return ResultCode.Success; + } + + public ResultCode GetNintendoAccountUserResourceCacheForApplication(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceAcc, new { NetworkServiceAccountId }); + + context.ResponseData.Write(NetworkServiceAccountId); + + // TODO: determine and fill the output IPC buffer. + + return ResultCode.Success; + } + + public ResultCode StoreOpenContext(ServiceCtx context) + { + context.Device.System.AccountManager.StoreOpenedUsers(); + + return ResultCode.Success; + } + + public ResultCode LoadNetworkServiceLicenseKindAsync(ServiceCtx context, out IAsyncNetworkServiceLicenseKindContext asyncContext) + { + KEvent asyncEvent = new KEvent(context.Device.System.KernelContext); + AsyncExecution asyncExecution = new AsyncExecution(asyncEvent); + + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + // NOTE: This is an extension of the data retrieved from the id token cache. + asyncExecution.Initialize(1000, EnsureIdTokenCacheAsyncImpl); + + asyncContext = new IAsyncNetworkServiceLicenseKindContext(asyncExecution, NetworkServiceLicenseKind.Subscribed); + + // return ResultCode.NullObject if the IAsyncNetworkServiceLicenseKindContext pointer is null. Doesn't occur in our case. + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ProfileServer.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ProfileServer.cs new file mode 100644 index 00000000..8e29f94b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ProfileServer.cs @@ -0,0 +1,114 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Cpu; +using Ryujinx.HLE.Utilities; +using System.Text; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService +{ + class ProfileServer + { + private UserProfile _profile; + + public ProfileServer(UserProfile profile) + { + _profile = profile; + } + + public ResultCode Get(ServiceCtx context) + { + context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(0x80UL); + + ulong bufferPosition = context.Request.RecvListBuff[0].Position; + + MemoryHelper.FillWithZeros(context.Memory, bufferPosition, 0x80); + + // TODO: Determine the struct. + context.Memory.Write(bufferPosition, 0); // Unknown + context.Memory.Write(bufferPosition + 4, 1); // Icon ID. 0 = Mii, the rest are character icon IDs. + context.Memory.Write(bufferPosition + 8, (byte)1); // Profile icon background color ID + // 0x07 bytes - Unknown + // 0x10 bytes - Some ID related to the Mii? All zeros when a character icon is used. + // 0x60 bytes - Usually zeros? + + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + return GetBase(context); + } + + public ResultCode GetBase(ServiceCtx context) + { + _profile.UserId.Write(context.ResponseData); + + context.ResponseData.Write(_profile.LastModifiedTimestamp); + + byte[] username = StringUtils.GetFixedLengthBytes(_profile.Name, 0x20, Encoding.UTF8); + + context.ResponseData.Write(username); + + return ResultCode.Success; + } + + public ResultCode GetImageSize(ServiceCtx context) + { + context.ResponseData.Write(_profile.Image.Length); + + return ResultCode.Success; + } + + public ResultCode LoadImage(ServiceCtx context) + { + ulong bufferPosition = context.Request.ReceiveBuff[0].Position; + ulong bufferLen = context.Request.ReceiveBuff[0].Size; + + if ((ulong)_profile.Image.Length > bufferLen) + { + return ResultCode.InvalidBufferSize; + } + + context.Memory.Write(bufferPosition, _profile.Image); + + context.ResponseData.Write(_profile.Image.Length); + + return ResultCode.Success; + } + + public ResultCode Store(ServiceCtx context) + { + ulong userDataPosition = context.Request.PtrBuff[0].Position; + ulong userDataSize = context.Request.PtrBuff[0].Size; + + byte[] userData = new byte[userDataSize]; + + context.Memory.Read(userDataPosition, userData); + + // TODO: Read the nn::account::profile::ProfileBase and store everything in the savedata. + + Logger.Stub?.PrintStub(LogClass.ServiceAcc, new { userDataSize }); + + return ResultCode.Success; + } + + public ResultCode StoreWithImage(ServiceCtx context) + { + ulong userDataPosition = context.Request.PtrBuff[0].Position; + ulong userDataSize = context.Request.PtrBuff[0].Size; + + byte[] userData = new byte[userDataSize]; + + context.Memory.Read(userDataPosition, userData); + + ulong profileImagePosition = context.Request.SendBuff[0].Position; + ulong profileImageSize = context.Request.SendBuff[0].Size; + + byte[] profileImageData = new byte[profileImageSize]; + + context.Memory.Read(profileImagePosition, profileImageData); + + // TODO: Read the nn::account::profile::ProfileBase and store everything in the savedata. + + Logger.Stub?.PrintStub(LogClass.ServiceAcc, new { userDataSize, profileImageSize }); + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/ApplicationServiceServer.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/ApplicationServiceServer.cs new file mode 100644 index 00000000..d9f9864a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/ApplicationServiceServer.cs @@ -0,0 +1,254 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.HLE.HOS.Services.Account.Acc.AccountService; +using Ryujinx.HLE.HOS.Services.Account.Acc.AsyncContext; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + class ApplicationServiceServer + { + readonly AccountServiceFlag _serviceFlag; + + public ApplicationServiceServer(AccountServiceFlag serviceFlag) + { + _serviceFlag = serviceFlag; + } + + public ResultCode GetUserCountImpl(ServiceCtx context) + { + context.ResponseData.Write(context.Device.System.AccountManager.GetUserCount()); + + return ResultCode.Success; + } + + public ResultCode GetUserExistenceImpl(ServiceCtx context) + { + ResultCode resultCode = CheckUserId(context, out UserId userId); + + if (resultCode != ResultCode.Success) + { + return resultCode; + } + + context.ResponseData.Write(context.Device.System.AccountManager.TryGetUser(userId, out _)); + + return ResultCode.Success; + } + + public ResultCode ListAllUsers(ServiceCtx context) + { + return WriteUserList(context, context.Device.System.AccountManager.GetAllUsers()); + } + + public ResultCode ListOpenUsers(ServiceCtx context) + { + return WriteUserList(context, context.Device.System.AccountManager.GetOpenedUsers()); + } + + private ResultCode WriteUserList(ServiceCtx context, IEnumerable<UserProfile> profiles) + { + if (context.Request.RecvListBuff.Count == 0) + { + return ResultCode.InvalidBuffer; + } + + ulong outputPosition = context.Request.RecvListBuff[0].Position; + ulong outputSize = context.Request.RecvListBuff[0].Size; + + MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize); + + ulong offset = 0; + + foreach (UserProfile userProfile in profiles) + { + if (offset + 0x10 > outputSize) + { + break; + } + + context.Memory.Write(outputPosition + offset, userProfile.UserId.High); + context.Memory.Write(outputPosition + offset + 8, userProfile.UserId.Low); + + offset += 0x10; + } + + return ResultCode.Success; + } + + public ResultCode GetLastOpenedUser(ServiceCtx context) + { + context.Device.System.AccountManager.LastOpenedUser.UserId.Write(context.ResponseData); + + return ResultCode.Success; + } + + public ResultCode GetProfile(ServiceCtx context, out IProfile profile) + { + profile = default; + + ResultCode resultCode = CheckUserId(context, out UserId userId); + + if (resultCode != ResultCode.Success) + { + return resultCode; + } + + if (!context.Device.System.AccountManager.TryGetUser(userId, out UserProfile userProfile)) + { + Logger.Warning?.Print(LogClass.ServiceAcc, $"User 0x{userId} not found!"); + + return ResultCode.UserNotFound; + } + + profile = new IProfile(userProfile); + + // Doesn't occur in our case. + // return ResultCode.NullObject; + + return ResultCode.Success; + } + + public ResultCode IsUserRegistrationRequestPermitted(ServiceCtx context) + { + context.ResponseData.Write(_serviceFlag != AccountServiceFlag.Application); + + return ResultCode.Success; + } + + public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context) + { + if (context.Device.System.AccountManager.GetUserCount() < 1) + { + // Invalid UserId. + UserId.Null.Write(context.ResponseData); + + return ResultCode.UserNotFound; + } + + bool isNetworkServiceAccountRequired = context.RequestData.ReadBoolean(); + + if (isNetworkServiceAccountRequired) + { + // NOTE: This checks something related to baas (online), and then return an invalid UserId if the check in baas returns an error code. + // In our case, we can just log it for now. + + Logger.Stub?.PrintStub(LogClass.ServiceAcc, new { isNetworkServiceAccountRequired }); + } + + // NOTE: As we returned an invalid UserId if there is more than one user earlier, now we can return only the first one. + context.Device.System.AccountManager.GetFirst().UserId.Write(context.ResponseData); + + return ResultCode.Success; + } + + public ResultCode CheckNetworkServiceAvailabilityAsync(ServiceCtx context, out IAsyncContext asyncContext) + { + KEvent asyncEvent = new(context.Device.System.KernelContext); + AsyncExecution asyncExecution = new(asyncEvent); + + asyncExecution.Initialize(1000, CheckNetworkServiceAvailabilityAsyncImpl); + + asyncContext = new IAsyncContext(asyncExecution); + + // return ResultCode.NullObject if the IAsyncContext pointer is null. Doesn't occur in our case. + + return ResultCode.Success; + } + + private async Task CheckNetworkServiceAvailabilityAsyncImpl(CancellationToken token) + { + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + // TODO: Use a real function instead, with the CancellationToken. + await Task.CompletedTask; + } + + public ResultCode StoreSaveDataThumbnail(ServiceCtx context) + { + ResultCode resultCode = CheckUserId(context, out UserId _); + + if (resultCode != ResultCode.Success) + { + return resultCode; + } + + if (context.Request.SendBuff.Count == 0) + { + return ResultCode.InvalidBuffer; + } + + ulong inputPosition = context.Request.SendBuff[0].Position; + ulong inputSize = context.Request.SendBuff[0].Size; + + if (inputSize != 0x24000) + { + return ResultCode.InvalidBufferSize; + } + + byte[] thumbnailBuffer = new byte[inputSize]; + + context.Memory.Read(inputPosition, thumbnailBuffer); + + // NOTE: Account service call nn::fs::WriteSaveDataThumbnailFile(). + // TODO: Store thumbnailBuffer somewhere, in save data 0x8000000000000010 ? + + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + return ResultCode.Success; + } + + public ResultCode ClearSaveDataThumbnail(ServiceCtx context) + { + ResultCode resultCode = CheckUserId(context, out UserId _); + + if (resultCode != ResultCode.Success) + { + return resultCode; + } + + /* + // NOTE: Doesn't occur in our case. + if (userId == null) + { + return ResultCode.InvalidArgument; + } + */ + + // NOTE: Account service call nn::fs::WriteSaveDataThumbnailFileHeader(); + // TODO: Clear the Thumbnail somewhere, in save data 0x8000000000000010 ? + + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + return ResultCode.Success; + } + + public ResultCode ListOpenContextStoredUsers(ServiceCtx context) + { + return WriteUserList(context, context.Device.System.AccountManager.GetStoredOpenedUsers()); + } + + public ResultCode ListQualifiedUsers(ServiceCtx context) + { + // TODO: Determine how users are "qualified". We assume all users are "qualified" for now. + + return WriteUserList(context, context.Device.System.AccountManager.GetAllUsers()); + } + + public ResultCode CheckUserId(ServiceCtx context, out UserId userId) + { + userId = context.RequestData.ReadStruct<UserId>(); + + if (userId.IsNull) + { + return ResultCode.NullArgument; + } + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AsyncContext/AsyncExecution.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AsyncContext/AsyncExecution.cs new file mode 100644 index 00000000..2ea92b11 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AsyncContext/AsyncExecution.cs @@ -0,0 +1,56 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel.Threading; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc.AsyncContext +{ + class AsyncExecution + { + private readonly CancellationTokenSource _tokenSource; + private readonly CancellationToken _token; + + public KEvent SystemEvent { get; } + public bool IsInitialized { get; private set; } + public bool IsRunning { get; private set; } + + public AsyncExecution(KEvent asyncEvent) + { + SystemEvent = asyncEvent; + + _tokenSource = new CancellationTokenSource(); + _token = _tokenSource.Token; + } + + public void Initialize(int timeout, Func<CancellationToken, Task> taskAsync) + { + Task.Run(async () => + { + IsRunning = true; + + _tokenSource.CancelAfter(timeout); + + try + { + await taskAsync(_token); + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.ServiceAcc, $"Exception: {ex.Message}"); + } + + SystemEvent.ReadableEvent.Signal(); + + IsRunning = false; + }, _token); + + IsInitialized = true; + } + + public void Cancel() + { + _tokenSource.Cancel(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/DefaultUserImage.jpg b/src/Ryujinx.HLE/HOS/Services/Account/Acc/DefaultUserImage.jpg Binary files differnew file mode 100644 index 00000000..64c4e8ec --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/DefaultUserImage.jpg diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForAdministrator.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForAdministrator.cs new file mode 100644 index 00000000..6a457f04 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForAdministrator.cs @@ -0,0 +1,129 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Account.Acc.AccountService; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + [Service("acc:su", AccountServiceFlag.Administrator)] // Max Sessions: 8 + class IAccountServiceForAdministrator : IpcService + { + private ApplicationServiceServer _applicationServiceServer; + + public IAccountServiceForAdministrator(ServiceCtx context, AccountServiceFlag serviceFlag) + { + _applicationServiceServer = new ApplicationServiceServer(serviceFlag); + } + + [CommandCmif(0)] + // GetUserCount() -> i32 + public ResultCode GetUserCount(ServiceCtx context) + { + return _applicationServiceServer.GetUserCountImpl(context); + } + + [CommandCmif(1)] + // GetUserExistence(nn::account::Uid) -> bool + public ResultCode GetUserExistence(ServiceCtx context) + { + return _applicationServiceServer.GetUserExistenceImpl(context); + } + + [CommandCmif(2)] + // ListAllUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListAllUsers(ServiceCtx context) + { + return _applicationServiceServer.ListAllUsers(context); + } + + [CommandCmif(3)] + // ListOpenUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListOpenUsers(ServiceCtx context) + { + return _applicationServiceServer.ListOpenUsers(context); + } + + [CommandCmif(4)] + // GetLastOpenedUser() -> nn::account::Uid + public ResultCode GetLastOpenedUser(ServiceCtx context) + { + return _applicationServiceServer.GetLastOpenedUser(context); + } + + [CommandCmif(5)] + // GetProfile(nn::account::Uid) -> object<nn::account::profile::IProfile> + public ResultCode GetProfile(ServiceCtx context) + { + ResultCode resultCode = _applicationServiceServer.GetProfile(context, out IProfile iProfile); + + if (resultCode == ResultCode.Success) + { + MakeObject(context, iProfile); + } + + return resultCode; + } + + [CommandCmif(50)] + // IsUserRegistrationRequestPermitted(pid) -> bool + public ResultCode IsUserRegistrationRequestPermitted(ServiceCtx context) + { + // NOTE: pid is unused. + + return _applicationServiceServer.IsUserRegistrationRequestPermitted(context); + } + + [CommandCmif(51)] + // TrySelectUserWithoutInteraction(bool) -> nn::account::Uid + public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context) + { + return _applicationServiceServer.TrySelectUserWithoutInteraction(context); + } + + [CommandCmif(102)] + // GetBaasAccountManagerForSystemService(nn::account::Uid) -> object<nn::account::baas::IManagerForApplication> + public ResultCode GetBaasAccountManagerForSystemService(ServiceCtx context) + { + ResultCode resultCode = _applicationServiceServer.CheckUserId(context, out UserId userId); + + if (resultCode != ResultCode.Success) + { + return resultCode; + } + + MakeObject(context, new IManagerForSystemService(userId)); + + // Doesn't occur in our case. + // return ResultCode.NullObject; + + return ResultCode.Success; + } + + [CommandCmif(140)] // 6.0.0+ + // ListQualifiedUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListQualifiedUsers(ServiceCtx context) + { + return _applicationServiceServer.ListQualifiedUsers(context); + } + + [CommandCmif(205)] + // GetProfileEditor(nn::account::Uid) -> object<nn::account::profile::IProfileEditor> + public ResultCode GetProfileEditor(ServiceCtx context) + { + UserId userId = context.RequestData.ReadStruct<UserId>(); + + if (!context.Device.System.AccountManager.TryGetUser(userId, out UserProfile userProfile)) + { + Logger.Warning?.Print(LogClass.ServiceAcc, $"User 0x{userId} not found!"); + + return ResultCode.UserNotFound; + } + + MakeObject(context, new IProfileEditor(userProfile)); + + // Doesn't occur in our case. + // return ResultCode.NullObject; + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForApplication.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForApplication.cs new file mode 100644 index 00000000..8ec83e5c --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForApplication.cs @@ -0,0 +1,200 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Account.Acc.AccountService; +using Ryujinx.HLE.HOS.Services.Arp; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + [Service("acc:u0", AccountServiceFlag.Application)] // Max Sessions: 4 + class IAccountServiceForApplication : IpcService + { + private ApplicationServiceServer _applicationServiceServer; + + public IAccountServiceForApplication(ServiceCtx context, AccountServiceFlag serviceFlag) + { + _applicationServiceServer = new ApplicationServiceServer(serviceFlag); + } + + [CommandCmif(0)] + // GetUserCount() -> i32 + public ResultCode GetUserCount(ServiceCtx context) + { + return _applicationServiceServer.GetUserCountImpl(context); + } + + [CommandCmif(1)] + // GetUserExistence(nn::account::Uid) -> bool + public ResultCode GetUserExistence(ServiceCtx context) + { + return _applicationServiceServer.GetUserExistenceImpl(context); + } + + [CommandCmif(2)] + // ListAllUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListAllUsers(ServiceCtx context) + { + return _applicationServiceServer.ListAllUsers(context); + } + + [CommandCmif(3)] + // ListOpenUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListOpenUsers(ServiceCtx context) + { + return _applicationServiceServer.ListOpenUsers(context); + } + + [CommandCmif(4)] + // GetLastOpenedUser() -> nn::account::Uid + public ResultCode GetLastOpenedUser(ServiceCtx context) + { + return _applicationServiceServer.GetLastOpenedUser(context); + } + + [CommandCmif(5)] + // GetProfile(nn::account::Uid) -> object<nn::account::profile::IProfile> + public ResultCode GetProfile(ServiceCtx context) + { + ResultCode resultCode = _applicationServiceServer.GetProfile(context, out IProfile iProfile); + + if (resultCode == ResultCode.Success) + { + MakeObject(context, iProfile); + } + + return resultCode; + } + + [CommandCmif(50)] + // IsUserRegistrationRequestPermitted(pid) -> bool + public ResultCode IsUserRegistrationRequestPermitted(ServiceCtx context) + { + // NOTE: pid is unused. + return _applicationServiceServer.IsUserRegistrationRequestPermitted(context); + } + + [CommandCmif(51)] + // TrySelectUserWithoutInteraction(bool) -> nn::account::Uid + public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context) + { + return _applicationServiceServer.TrySelectUserWithoutInteraction(context); + } + + [CommandCmif(100)] + [CommandCmif(140)] // 6.0.0+ + [CommandCmif(160)] // 13.0.0+ + // InitializeApplicationInfo(u64 pid_placeholder, pid) + public ResultCode InitializeApplicationInfo(ServiceCtx context) + { + // NOTE: In call 100, account service use the pid_placeholder instead of the real pid, which is wrong, call 140 fix that. + + /* + + // TODO: Account actually calls nn::arp::detail::IReader::GetApplicationLaunchProperty() with the current PID and store the result (ApplicationLaunchProperty) internally. + // For now we can hardcode values, and fix it after GetApplicationLaunchProperty is implemented. + if (nn::arp::detail::IReader::GetApplicationLaunchProperty() == 0xCC9D) // ResultCode.InvalidProcessId + { + return ResultCode.InvalidArgument; + } + + */ + + // TODO: Determine where ApplicationLaunchProperty is used. + ApplicationLaunchProperty applicationLaunchProperty = ApplicationLaunchProperty.GetByPid(context); + + Logger.Stub?.PrintStub(LogClass.ServiceAcc, new { applicationLaunchProperty.TitleId }); + + return ResultCode.Success; + } + + [CommandCmif(101)] + // GetBaasAccountManagerForApplication(nn::account::Uid) -> object<nn::account::baas::IManagerForApplication> + public ResultCode GetBaasAccountManagerForApplication(ServiceCtx context) + { + ResultCode resultCode = _applicationServiceServer.CheckUserId(context, out UserId userId); + + if (resultCode != ResultCode.Success) + { + return resultCode; + } + + MakeObject(context, new IManagerForApplication(userId)); + + // Doesn't occur in our case. + // return ResultCode.NullObject; + + return ResultCode.Success; + } + + [CommandCmif(103)] // 4.0.0+ + // CheckNetworkServiceAvailabilityAsync() -> object<nn::account::detail::IAsyncContext> + public ResultCode CheckNetworkServiceAvailabilityAsync(ServiceCtx context) + { + ResultCode resultCode = _applicationServiceServer.CheckNetworkServiceAvailabilityAsync(context, out IAsyncContext asyncContext); + + if (resultCode == ResultCode.Success) + { + MakeObject(context, asyncContext); + } + + return resultCode; + } + + [CommandCmif(110)] + // StoreSaveDataThumbnail(nn::account::Uid, buffer<bytes, 5>) + public ResultCode StoreSaveDataThumbnail(ServiceCtx context) + { + return _applicationServiceServer.StoreSaveDataThumbnail(context); + } + + [CommandCmif(111)] + // ClearSaveDataThumbnail(nn::account::Uid) + public ResultCode ClearSaveDataThumbnail(ServiceCtx context) + { + return _applicationServiceServer.ClearSaveDataThumbnail(context); + } + + [CommandCmif(130)] // 5.0.0+ + // LoadOpenContext(nn::account::Uid) -> object<nn::account::baas::IManagerForApplication> + public ResultCode LoadOpenContext(ServiceCtx context) + { + ResultCode resultCode = _applicationServiceServer.CheckUserId(context, out UserId userId); + + if (resultCode != ResultCode.Success) + { + return resultCode; + } + + MakeObject(context, new IManagerForApplication(userId)); + + return ResultCode.Success; + } + + [CommandCmif(60)] // 5.0.0-5.1.0 + [CommandCmif(131)] // 6.0.0+ + // ListOpenContextStoredUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListOpenContextStoredUsers(ServiceCtx context) + { + return _applicationServiceServer.ListOpenContextStoredUsers(context); + } + + [CommandCmif(141)] // 6.0.0+ + // ListQualifiedUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListQualifiedUsers(ServiceCtx context) + { + return _applicationServiceServer.ListQualifiedUsers(context); + } + + [CommandCmif(150)] // 6.0.0+ + // IsUserAccountSwitchLocked() -> bool + public ResultCode IsUserAccountSwitchLocked(ServiceCtx context) + { + // TODO: Account actually calls nn::arp::detail::IReader::GetApplicationControlProperty() with the current Pid and store the result (NACP file) internally. + // But since we use LibHac and we load one Application at a time, it's not necessary. + + context.ResponseData.Write((byte)context.Device.Processes.ActiveApplication.ApplicationControlProperties.UserAccountSwitchLock); + + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForSystemService.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForSystemService.cs new file mode 100644 index 00000000..3b5f3b03 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForSystemService.cs @@ -0,0 +1,107 @@ +using Ryujinx.Common; +using Ryujinx.HLE.HOS.Services.Account.Acc.AccountService; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + [Service("acc:u1", AccountServiceFlag.SystemService)] // Max Sessions: 16 + class IAccountServiceForSystemService : IpcService + { + private ApplicationServiceServer _applicationServiceServer; + + public IAccountServiceForSystemService(ServiceCtx context, AccountServiceFlag serviceFlag) + { + _applicationServiceServer = new ApplicationServiceServer(serviceFlag); + } + + [CommandCmif(0)] + // GetUserCount() -> i32 + public ResultCode GetUserCount(ServiceCtx context) + { + return _applicationServiceServer.GetUserCountImpl(context); + } + + [CommandCmif(1)] + // GetUserExistence(nn::account::Uid) -> bool + public ResultCode GetUserExistence(ServiceCtx context) + { + return _applicationServiceServer.GetUserExistenceImpl(context); + } + + [CommandCmif(2)] + // ListAllUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListAllUsers(ServiceCtx context) + { + return _applicationServiceServer.ListAllUsers(context); + } + + [CommandCmif(3)] + // ListOpenUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListOpenUsers(ServiceCtx context) + { + return _applicationServiceServer.ListOpenUsers(context); + } + + [CommandCmif(4)] + // GetLastOpenedUser() -> nn::account::Uid + public ResultCode GetLastOpenedUser(ServiceCtx context) + { + return _applicationServiceServer.GetLastOpenedUser(context); + } + + [CommandCmif(5)] + // GetProfile(nn::account::Uid) -> object<nn::account::profile::IProfile> + public ResultCode GetProfile(ServiceCtx context) + { + ResultCode resultCode = _applicationServiceServer.GetProfile(context, out IProfile iProfile); + + if (resultCode == ResultCode.Success) + { + MakeObject(context, iProfile); + } + + return resultCode; + } + + [CommandCmif(50)] + // IsUserRegistrationRequestPermitted(pid) -> bool + public ResultCode IsUserRegistrationRequestPermitted(ServiceCtx context) + { + // NOTE: pid is unused. + + return _applicationServiceServer.IsUserRegistrationRequestPermitted(context); + } + + [CommandCmif(51)] + // TrySelectUserWithoutInteraction(bool) -> nn::account::Uid + public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context) + { + return _applicationServiceServer.TrySelectUserWithoutInteraction(context); + } + + [CommandCmif(102)] + // GetBaasAccountManagerForSystemService(nn::account::Uid) -> object<nn::account::baas::IManagerForApplication> + public ResultCode GetBaasAccountManagerForSystemService(ServiceCtx context) + { + UserId userId = context.RequestData.ReadStruct<UserId>(); + + if (userId.IsNull) + { + return ResultCode.NullArgument; + } + + MakeObject(context, new IManagerForSystemService(userId)); + + // Doesn't occur in our case. + // return ResultCode.NullObject; + + return ResultCode.Success; + } + + [CommandCmif(140)] // 6.0.0+ + // ListQualifiedUsers() -> array<nn::account::Uid, 0xa> + public ResultCode ListQualifiedUsers(ServiceCtx context) + { + return _applicationServiceServer.ListQualifiedUsers(context); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAsyncContext.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAsyncContext.cs new file mode 100644 index 00000000..c9af0727 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAsyncContext.cs @@ -0,0 +1,79 @@ +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Services.Account.Acc.AsyncContext; +using Ryujinx.Horizon.Common; +using System; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + class IAsyncContext : IpcService + { + protected AsyncExecution AsyncExecution; + + public IAsyncContext(AsyncExecution asyncExecution) + { + AsyncExecution = asyncExecution; + } + + [CommandCmif(0)] + // GetSystemEvent() -> handle<copy> + public ResultCode GetSystemEvent(ServiceCtx context) + { + if (context.Process.HandleTable.GenerateHandle(AsyncExecution.SystemEvent.ReadableEvent, out int _systemEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_systemEventHandle); + + return ResultCode.Success; + } + + [CommandCmif(1)] + // Cancel() + public ResultCode Cancel(ServiceCtx context) + { + if (!AsyncExecution.IsInitialized) + { + return ResultCode.AsyncExecutionNotInitialized; + } + + if (AsyncExecution.IsRunning) + { + AsyncExecution.Cancel(); + } + + return ResultCode.Success; + } + + [CommandCmif(2)] + // HasDone() -> b8 + public ResultCode HasDone(ServiceCtx context) + { + if (!AsyncExecution.IsInitialized) + { + return ResultCode.AsyncExecutionNotInitialized; + } + + context.ResponseData.Write(AsyncExecution.SystemEvent.ReadableEvent.IsSignaled()); + + return ResultCode.Success; + } + + [CommandCmif(3)] + // GetResult() + public ResultCode GetResult(ServiceCtx context) + { + if (!AsyncExecution.IsInitialized) + { + return ResultCode.AsyncExecutionNotInitialized; + } + + if (!AsyncExecution.SystemEvent.ReadableEvent.IsSignaled()) + { + return ResultCode.Unknown41; + } + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAsyncNetworkServiceLicenseKindContext.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAsyncNetworkServiceLicenseKindContext.cs new file mode 100644 index 00000000..1fa5cf2a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IAsyncNetworkServiceLicenseKindContext.cs @@ -0,0 +1,38 @@ +using Ryujinx.HLE.HOS.Services.Account.Acc.AsyncContext; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + class IAsyncNetworkServiceLicenseKindContext : IAsyncContext + { + private NetworkServiceLicenseKind? _serviceLicenseKind; + + public IAsyncNetworkServiceLicenseKindContext(AsyncExecution asyncExecution, NetworkServiceLicenseKind? serviceLicenseKind) : base(asyncExecution) + { + _serviceLicenseKind = serviceLicenseKind; + } + + [CommandCmif(100)] + // GetNetworkServiceLicenseKind() -> nn::account::NetworkServiceLicenseKind + public ResultCode GetNetworkServiceLicenseKind(ServiceCtx context) + { + if (!AsyncExecution.IsInitialized) + { + return ResultCode.AsyncExecutionNotInitialized; + } + + if (!AsyncExecution.SystemEvent.ReadableEvent.IsSignaled()) + { + return ResultCode.Unknown41; + } + + if (!_serviceLicenseKind.HasValue) + { + return ResultCode.MissingNetworkServiceLicenseKind; + } + + context.ResponseData.Write((uint)_serviceLicenseKind.Value); + + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/IBaasAccessTokenAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IBaasAccessTokenAccessor.cs new file mode 100644 index 00000000..223be2f5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/IBaasAccessTokenAccessor.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + [Service("acc:aa", AccountServiceFlag.BaasAccessTokenAccessor)] // Max Sessions: 4 + class IBaasAccessTokenAccessor : IpcService + { + public IBaasAccessTokenAccessor(ServiceCtx context) { } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs new file mode 100644 index 00000000..6b54898e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs @@ -0,0 +1,11 @@ +using Ryujinx.HLE.HOS.Services.Account.Acc.Types; +using System.Text.Json.Serialization; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ProfilesJson))] + internal partial class ProfilesJsonSerializerContext : JsonSerializerContext + { + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountServiceFlag.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountServiceFlag.cs new file mode 100644 index 00000000..a991f977 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountServiceFlag.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + enum AccountServiceFlag + { + Administrator = 100, + SystemService = 101, + Application = 102, + BaasAccessTokenAccessor = 200 + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs new file mode 100644 index 00000000..1699abfb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs @@ -0,0 +1,12 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + [JsonConverter(typeof(TypedStringEnumConverter<AccountState>))] + public enum AccountState + { + Closed, + Open + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/NetworkServiceLicenseKind.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/NetworkServiceLicenseKind.cs new file mode 100644 index 00000000..a33e2670 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/NetworkServiceLicenseKind.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + enum NetworkServiceLicenseKind : uint + { + NoSubscription, + Subscribed + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs new file mode 100644 index 00000000..09f9d142 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc.Types +{ + internal struct ProfilesJson + { + public List<UserProfileJson> Profiles { get; set; } + public string LastOpened { get; set; } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserId.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserId.cs new file mode 100644 index 00000000..e5577a94 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserId.cs @@ -0,0 +1,64 @@ +using LibHac.Account; +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + [StructLayout(LayoutKind.Sequential)] + public readonly record struct UserId + { + public readonly long High; + public readonly long Low; + + public bool IsNull => (Low | High) == 0; + + public static UserId Null => new UserId(0, 0); + + public UserId(long low, long high) + { + Low = low; + High = high; + } + + public UserId(byte[] bytes) + { + High = BitConverter.ToInt64(bytes, 0); + Low = BitConverter.ToInt64(bytes, 8); + } + + public UserId(string hex) + { + if (hex == null || hex.Length != 32 || !hex.All("0123456789abcdefABCDEF".Contains)) + { + throw new ArgumentException("Invalid Hex value!", nameof(hex)); + } + + Low = long.Parse(hex.AsSpan(16), NumberStyles.HexNumber); + High = long.Parse(hex.AsSpan(0, 16), NumberStyles.HexNumber); + } + + public void Write(BinaryWriter binaryWriter) + { + binaryWriter.Write(High); + binaryWriter.Write(Low); + } + + public override string ToString() + { + return High.ToString("x16") + Low.ToString("x16"); + } + + public Uid ToLibHacUid() + { + return new Uid((ulong)High, (ulong)Low); + } + + public UInt128 ToUInt128() + { + return new UInt128((ulong)High, (ulong)Low); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfile.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfile.cs new file mode 100644 index 00000000..210b369c --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfile.cs @@ -0,0 +1,87 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + public class UserProfile + { + public UserId UserId { get; } + + public long LastModifiedTimestamp { get; set; } + + private string _name; + + public string Name + { + get => _name; + set + { + _name = value; + + UpdateLastModifiedTimestamp(); + } + } + + private byte[] _image; + + public byte[] Image + { + get => _image; + set + { + _image = value; + + UpdateLastModifiedTimestamp(); + } + } + + private AccountState _accountState; + + public AccountState AccountState + { + get => _accountState; + set + { + _accountState = value; + + UpdateLastModifiedTimestamp(); + } + } + + public AccountState _onlinePlayState; + + public AccountState OnlinePlayState + { + get => _onlinePlayState; + set + { + _onlinePlayState = value; + + UpdateLastModifiedTimestamp(); + } + } + + public UserProfile(UserId userId, string name, byte[] image, long lastModifiedTimestamp = 0) + { + UserId = userId; + Name = name; + Image = image; + + AccountState = AccountState.Closed; + OnlinePlayState = AccountState.Closed; + + if (lastModifiedTimestamp != 0) + { + LastModifiedTimestamp = lastModifiedTimestamp; + } + else + { + UpdateLastModifiedTimestamp(); + } + } + + private void UpdateLastModifiedTimestamp() + { + LastModifiedTimestamp = (long)(DateTime.Now - DateTime.UnixEpoch).TotalSeconds; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs new file mode 100644 index 00000000..06ff4833 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc.Types +{ + internal struct UserProfileJson + { + public string UserId { get; set; } + public string Name { get; set; } + public AccountState AccountState { get; set; } + public AccountState OnlinePlayState { get; set; } + public long LastModifiedTimestamp { get; set; } + public byte[] Image { get; set; } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Dauth/IService.cs b/src/Ryujinx.HLE/HOS/Services/Account/Dauth/IService.cs new file mode 100644 index 00000000..72301349 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/Dauth/IService.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Dauth +{ + [Service("dauth:0")] // 5.0.0+ + class IService : IpcService + { + public IService(ServiceCtx context) { } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Account/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Account/ResultCode.cs new file mode 100644 index 00000000..34114ec9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Account/ResultCode.cs @@ -0,0 +1,24 @@ +namespace Ryujinx.HLE.HOS.Services.Account +{ + enum ResultCode + { + ModuleId = 124, + ErrorCodeShift = 9, + + Success = 0, + + NullArgument = (20 << ErrorCodeShift) | ModuleId, + InvalidArgument = (22 << ErrorCodeShift) | ModuleId, + NullInputBuffer = (30 << ErrorCodeShift) | ModuleId, + InvalidBufferSize = (31 << ErrorCodeShift) | ModuleId, + InvalidBuffer = (32 << ErrorCodeShift) | ModuleId, + AsyncExecutionNotInitialized = (40 << ErrorCodeShift) | ModuleId, + Unknown41 = (41 << ErrorCodeShift) | ModuleId, + InternetRequestDenied = (59 << ErrorCodeShift) | ModuleId, + UserNotFound = (100 << ErrorCodeShift) | ModuleId, + NullObject = (302 << ErrorCodeShift) | ModuleId, + Unknown341 = (341 << ErrorCodeShift) | ModuleId, + MissingNetworkServiceLicenseKind = (400 << ErrorCodeShift) | ModuleId, + InvalidIdTokenCacheBufferSize = (451 << ErrorCodeShift) | ModuleId + } +} |
