diff --git a/Ryujinx.HLE/HOS/ApplicationLoader.cs b/Ryujinx.HLE/HOS/ApplicationLoader.cs index 16c9664db..46b621783 100644 --- a/Ryujinx.HLE/HOS/ApplicationLoader.cs +++ b/Ryujinx.HLE/HOS/ApplicationLoader.cs @@ -15,6 +15,7 @@ using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Npdm; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -28,32 +29,33 @@ namespace Ryujinx.HLE.HOS public class ApplicationLoader { - private readonly Switch _device; - private readonly ContentManager _contentManager; - private readonly VirtualFileSystem _fileSystem; - - public BlitStruct ControlData { get; set; } - - public string TitleName { get; private set; } - public string DisplayVersion { get; private set; } - - public ulong TitleId { get; private set; } - public string TitleIdText => TitleId.ToString("x16"); - - public bool TitleIs64Bit { get; private set; } - - public bool EnablePtc => _device.System.EnablePtc; - // Binaries from exefs are loaded into mem in this order. Do not change. private static readonly string[] ExeFsPrefixes = { "rtld", "main", "subsdk*", "sdk" }; + private readonly Switch _device; + private readonly ContentManager _contentManager; + private readonly VirtualFileSystem _fileSystem; + + private string _titleName; + private string _displayVersion; + private BlitStruct _controlData; + + public BlitStruct ControlData => _controlData; + public string TitleName => _titleName; + public string DisplayVersion => _displayVersion; + + public ulong TitleId { get; private set; } + public bool TitleIs64Bit { get; private set; } + + public string TitleIdText => TitleId.ToString("x16"); + public ApplicationLoader(Switch device, VirtualFileSystem fileSystem, ContentManager contentManager) { - _device = device; + _device = device; _contentManager = contentManager; - _fileSystem = fileSystem; + _fileSystem = fileSystem; - ControlData = new BlitStruct(1); + _controlData = new BlitStruct(1); // Clear Mods cache _fileSystem.ModLoader.Clear(); @@ -80,19 +82,26 @@ namespace Ryujinx.HLE.HOS LoadExeFs(codeFs, metaData); } - private (Nca main, Nca patch, Nca control) GetGameData(PartitionFileSystem pfs) + public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex) { - Nca mainNca = null; - Nca patchNca = null; + Nca mainNca = null; + Nca patchNca = null; Nca controlNca = null; - _fileSystem.ImportTickets(pfs); + fileSystem.ImportTickets(pfs); foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) { pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - Nca nca = new Nca(_fileSystem.KeySet, ncaFile.AsStorage()); + Nca nca = new Nca(fileSystem.KeySet, ncaFile.AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } if (nca.Header.ContentType == NcaContentType.Program) { @@ -116,11 +125,77 @@ namespace Ryujinx.HLE.HOS return (mainNca, patchNca, controlNca); } + public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) + { + Nca patchNca = null; + Nca controlNca = null; + + fileSystem.ImportTickets(pfs); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new Nca(fileSystem.KeySet, ncaFile.AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) + { + break; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + patchNca = nca; + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (patchNca, controlNca); + } + + public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath) + { + updatePath = null; + + if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) + { + // Clear the program index part. + titleIdBase &= 0xFFFFFFFFFFFFFFF0; + + // Load update informations if existing. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + + if (File.Exists(titleUpdateMetadataPath)) + { + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected; + + if (File.Exists(updatePath)) + { + FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read); + PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); + + return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); + } + } + } + + return (null, null); + } + public void LoadXci(string xciFile) { FileStream file = new FileStream(xciFile, FileMode.Open, FileAccess.Read); - - Xci xci = new Xci(_fileSystem.KeySet, file.AsStorage()); + Xci xci = new Xci(_fileSystem.KeySet, file.AsStorage()); if (!xci.HasPartition(XciPartitionType.Secure)) { @@ -131,13 +206,13 @@ namespace Ryujinx.HLE.HOS PartitionFileSystem securePartition = xci.OpenPartition(XciPartitionType.Secure); - Nca mainNca = null; - Nca patchNca = null; - Nca controlNca = null; + Nca mainNca; + Nca patchNca; + Nca controlNca; try { - (mainNca, patchNca, controlNca) = GetGameData(securePartition); + (mainNca, patchNca, controlNca) = GetGameData(_fileSystem, securePartition, _device.UserChannelPersistence.Index); } catch (Exception e) { @@ -154,7 +229,6 @@ namespace Ryujinx.HLE.HOS } _contentManager.LoadEntries(_device); - _contentManager.ClearAocData(); _contentManager.AddAocData(securePartition, xciFile, mainNca.Header.TitleId); @@ -163,17 +237,16 @@ namespace Ryujinx.HLE.HOS public void LoadNsp(string nspFile) { - FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read); + FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read); + PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); - PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); - - Nca mainNca = null; - Nca patchNca = null; - Nca controlNca = null; + Nca mainNca; + Nca patchNca; + Nca controlNca; try { - (mainNca, patchNca, controlNca) = GetGameData(nsp); + (mainNca, patchNca, controlNca) = GetGameData(_fileSystem, nsp, _device.UserChannelPersistence.Index); } catch (Exception e) { @@ -206,8 +279,7 @@ namespace Ryujinx.HLE.HOS public void LoadNca(string ncaFile) { FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read); - - Nca nca = new Nca(_fileSystem.KeySet, file.AsStorage(false)); + Nca nca = new Nca(_fileSystem.KeySet, file.AsStorage(false)); LoadNca(nca, null, null); } @@ -221,46 +293,24 @@ namespace Ryujinx.HLE.HOS return; } - IStorage dataStorage = null; - IFileSystem codeFs = null; + IStorage dataStorage = null; + IFileSystem codeFs = null; - // Load Update - string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json"); + (Nca updatePatchNca, Nca updateControlNca) = GetGameUpdateData(_fileSystem, mainNca.Header.TitleId.ToString("x16"), _device.UserChannelPersistence.Index, out _); - if (File.Exists(titleUpdateMetadataPath)) + if (updatePatchNca != null) { - string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected; - - if (File.Exists(updatePath)) - { - FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); - - _fileSystem.ImportTickets(nsp); - - foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca")) - { - nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new Nca(_fileSystem.KeySet, ncaFile.AsStorage()); - - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != mainNca.Header.TitleId.ToString("x16")) - { - break; - } - - if (nca.Header.ContentType == NcaContentType.Program) - { - patchNca = nca; - } - else if (nca.Header.ContentType == NcaContentType.Control) - { - controlNca = nca; - } - } - } + patchNca = updatePatchNca; } + if (updateControlNca != null) + { + controlNca = updateControlNca; + } + + // Load program 0 control NCA as we are going to need it for display version. + (_, Nca updateProgram0ControlNca) = GetGameUpdateData(_fileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); + // Load Aoc string titleAocMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json"); @@ -315,13 +365,23 @@ namespace Ryujinx.HLE.HOS if (controlNca != null) { - ReadControlData(controlNca); + ReadControlData(_device, controlNca, ref _controlData, ref _titleName, ref _displayVersion); } else { ControlData.ByteSpan.Clear(); } + // NOTE: Nintendo doesn't guarantee that the display version will be updated on sub programs when updating a multi program application. + // BODY: As such, to avoid PTC cache confusion, we only trust the the program 0 display version when launching a sub program. + if (updateProgram0ControlNca != null && _device.UserChannelPersistence.Index != 0) + { + string dummyTitleName = ""; + BlitStruct dummyControl = new BlitStruct(1); + + ReadControlData(_device, updateProgram0ControlNca, ref dummyControl, ref dummyTitleName, ref _displayVersion); + } + if (dataStorage == null) { Logger.Warning?.Print(LogClass.Loader, "No RomFS found in NCA"); @@ -329,6 +389,7 @@ namespace Ryujinx.HLE.HOS else { IStorage newStorage = _fileSystem.ModLoader.ApplyRomFsMods(TitleId, dataStorage); + _fileSystem.SetRomFs(newStorage.AsStream(FileAccess.Read)); } @@ -346,6 +407,7 @@ namespace Ryujinx.HLE.HOS private Npdm ReadNpdm(IFileSystem fs) { Result result = fs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read); + Npdm metaData; if (ResultFs.PathNotFound.Includes(result)) @@ -359,39 +421,36 @@ namespace Ryujinx.HLE.HOS metaData = new Npdm(npdmFile.AsStream()); } - TitleId = metaData.Aci0.TitleId; + TitleId = metaData.Aci0.TitleId; TitleIs64Bit = metaData.Is64Bit; return metaData; } - private void ReadControlData(Nca controlNca) + private static void ReadControlData(Switch device, Nca controlNca, ref BlitStruct controlData, ref string titleName, ref string displayVersion) { - IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); - - Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read); + IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, device.System.FsIntegrityCheckLevel); + Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read); if (result.IsSuccess()) { - result = controlFile.Read(out long bytesRead, 0, ControlData.ByteSpan, ReadOption.None); + result = controlFile.Read(out long bytesRead, 0, controlData.ByteSpan, ReadOption.None); - if (result.IsSuccess() && bytesRead == ControlData.ByteSpan.Length) + if (result.IsSuccess() && bytesRead == controlData.ByteSpan.Length) { - TitleName = ControlData.Value - .Titles[(int)_device.System.State.DesiredTitleLanguage].Name.ToString(); + titleName = controlData.Value.Titles[(int)device.System.State.DesiredTitleLanguage].Name.ToString(); - if (string.IsNullOrWhiteSpace(TitleName)) + if (string.IsNullOrWhiteSpace(titleName)) { - TitleName = ControlData.Value.Titles.ToArray() - .FirstOrDefault(x => x.Name[0] != 0).Name.ToString(); + titleName = controlData.Value.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString(); } - DisplayVersion = ControlData.Value.DisplayVersion.ToString(); + displayVersion = controlData.Value.DisplayVersion.ToString(); } } else { - ControlData.ByteSpan.Clear(); + controlData.ByteSpan.Clear(); } } @@ -428,18 +487,18 @@ namespace Ryujinx.HLE.HOS // ExeFs file replacements bool modified = _fileSystem.ModLoader.ApplyExefsMods(TitleId, nsos); - var programs = nsos.ToArray(); + NsoExecutable[] programs = nsos.ToArray(); modified |= _fileSystem.ModLoader.ApplyNsoPatches(TitleId, programs); _contentManager.LoadEntries(_device); - if (EnablePtc && modified) + if (_device.System.EnablePtc && modified) { Logger.Warning?.Print(LogClass.Ptc, $"Detected exefs modifications. PPTC disabled."); } - Ptc.Initialize(TitleIdText, DisplayVersion, EnablePtc && !modified); + Ptc.Initialize(TitleIdText, DisplayVersion, _device.System.EnablePtc && !modified); ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: programs); } @@ -447,15 +506,15 @@ namespace Ryujinx.HLE.HOS public void LoadProgram(string filePath) { Npdm metaData = GetDefaultNpdm(); - - bool isNro = Path.GetExtension(filePath).ToLower() == ".nro"; + bool isNro = Path.GetExtension(filePath).ToLower() == ".nro"; IExecutable executable; if (isNro) { - FileStream input = new FileStream(filePath, FileMode.Open); - NroExecutable obj = new NroExecutable(input.AsStorage()); + FileStream input = new FileStream(filePath, FileMode.Open); + NroExecutable obj = new NroExecutable(input.AsStorage()); + executable = obj; // homebrew NRO can actually have some data after the actual NRO @@ -466,20 +525,19 @@ namespace Ryujinx.HLE.HOS BinaryReader reader = new BinaryReader(input); uint asetMagic = reader.ReadUInt32(); - if (asetMagic == 0x54455341) { uint asetVersion = reader.ReadUInt32(); if (asetVersion == 0) { ulong iconOffset = reader.ReadUInt64(); - ulong iconSize = reader.ReadUInt64(); + ulong iconSize = reader.ReadUInt64(); ulong nacpOffset = reader.ReadUInt64(); - ulong nacpSize = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); ulong romfsOffset = reader.ReadUInt64(); - ulong romfsSize = reader.ReadUInt64(); + ulong romfsSize = reader.ReadUInt64(); if (romfsSize != 0) { @@ -533,8 +591,8 @@ namespace Ryujinx.HLE.HOS _contentManager.LoadEntries(_device); - TitleName = metaData.TitleName; - TitleId = metaData.Aci0.TitleId; + _titleName = metaData.TitleName; + TitleId = metaData.Aci0.TitleId; TitleIs64Bit = metaData.Is64Bit; ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: executable); @@ -572,25 +630,24 @@ namespace Ryujinx.HLE.HOS "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games."); } - FileSystemClient fs = _fileSystem.FsClient; + FileSystemClient fileSystem = _fileSystem.FsClient; + Result resultCode = fileSystem.EnsureApplicationCacheStorage(out _, applicationId, ref control); - Result rc = fs.EnsureApplicationCacheStorage(out _, applicationId, ref control); - - if (rc.IsFailure()) + if (resultCode.IsFailure()) { - Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {rc.ToStringWithName()}"); + Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {resultCode.ToStringWithName()}"); - return rc; + return resultCode; } - rc = EnsureApplicationSaveData(fs, out _, applicationId, ref control, ref user); + resultCode = EnsureApplicationSaveData(fileSystem, out _, applicationId, ref control, ref user); - if (rc.IsFailure()) + if (resultCode.IsFailure()) { - Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {rc.ToStringWithName()}"); + Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {resultCode.ToStringWithName()}"); } - return rc; + return resultCode; } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/IManagerForApplication.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/IManagerForApplication.cs index 54565cb1a..7e2b5d4fc 100644 --- a/Ryujinx.HLE/HOS/Services/Account/Acc/IManagerForApplication.cs +++ b/Ryujinx.HLE/HOS/Services/Account/Acc/IManagerForApplication.cs @@ -126,5 +126,14 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc return ResultCode.Success; } + + [Command(160)] // 5.0.0+ + // StoreOpenContext() + public ResultCode StoreOpenContext(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceAcc); + + return ResultCode.Success; + } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs index a310a094f..82d7c7fa7 100644 --- a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs +++ b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs @@ -5,11 +5,13 @@ using LibHac.Fs; using LibHac.Ns; using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService; using Ryujinx.HLE.HOS.SystemState; using System; @@ -35,11 +37,27 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati } [Command(1)] - // PopLaunchParameter(u32) -> object + // PopLaunchParameter(LaunchParameterKind kind) -> object public ResultCode PopLaunchParameter(ServiceCtx context) { - // Only the first 0x18 bytes of the Data seems to be actually used. - MakeObject(context, new AppletAE.IStorage(StorageHelper.MakeLaunchParams(context.Device.System.State.Account.LastOpenedUser))); + LaunchParameterKind kind = (LaunchParameterKind)context.RequestData.ReadUInt32(); + + byte[] storageData; + + switch (kind) + { + case LaunchParameterKind.UserChannel: + storageData = context.Device.UserChannelPersistence.Pop(); + break; + case LaunchParameterKind.PreselectedUser: + // Only the first 0x18 bytes of the Data seems to be actually used. + storageData = StorageHelper.MakeLaunchParams(context.Device.System.State.Account.LastOpenedUser); + break; + default: + throw new NotImplementedException($"Unknown LaunchParameterKind {kind}"); + } + + MakeObject(context, new AppletAE.IStorage(storageData)); return ResultCode.Success; } @@ -376,14 +394,49 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati return (ResultCode)QueryPlayStatisticsManager.GetPlayStatistics(context, true); } + [Command(120)] // 5.0.0+ + // ExecuteProgram(ProgramSpecifyKind kind, u64 value) + public ResultCode ExecuteProgram(ServiceCtx context) + { + ProgramSpecifyKind kind = (ProgramSpecifyKind)context.RequestData.ReadUInt32(); + + // padding + context.RequestData.ReadUInt32(); + + ulong value = context.RequestData.ReadUInt64(); + + Logger.Stub?.PrintStub(LogClass.ServiceAm, new { kind, value }); + + context.Device.UiHandler.ExecuteProgram(context.Device, kind, value); + + return ResultCode.Success; + } + + [Command(121)] // 5.0.0+ + // ClearUserChannel() + public ResultCode ClearUserChannel(ServiceCtx context) + { + context.Device.UserChannelPersistence.Clear(); + + return ResultCode.Success; + } + + [Command(122)] // 5.0.0+ + // UnpopToUserChannel(object input_storage) + public ResultCode UnpopToUserChannel(ServiceCtx context) + { + AppletAE.IStorage data = GetObject(context, 0); + + context.Device.UserChannelPersistence.Push(data.Data); + + return ResultCode.Success; + } + [Command(123)] // 5.0.0+ // GetPreviousProgramIndex() -> s32 program_index public ResultCode GetPreviousProgramIndex(ServiceCtx context) { - // TODO: The output PreviousProgramIndex is -1 when there was no previous title. - // When multi-process will be supported, return the last program index. - - int previousProgramIndex = -1; + int previousProgramIndex = context.Device.UserChannelPersistence.PreviousIndex; context.ResponseData.Write(previousProgramIndex); diff --git a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/Types/LaunchParameterKind.cs b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/Types/LaunchParameterKind.cs new file mode 100644 index 000000000..40432074d --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/Types/LaunchParameterKind.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types +{ + public enum LaunchParameterKind : uint + { + UserChannel = 1, + PreselectedUser, + Unknown + } +} diff --git a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/Types/ProgramSpecifyKind.cs b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/Types/ProgramSpecifyKind.cs new file mode 100644 index 000000000..efc284a5c --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/Types/ProgramSpecifyKind.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types +{ + public enum ProgramSpecifyKind : uint + { + ExecuteProgram, + SubApplicationProgram, + RestartProgram + } +} diff --git a/Ryujinx.HLE/HOS/UserChannelPersistence.cs b/Ryujinx.HLE/HOS/UserChannelPersistence.cs new file mode 100644 index 000000000..62f28feef --- /dev/null +++ b/Ryujinx.HLE/HOS/UserChannelPersistence.cs @@ -0,0 +1,56 @@ +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; +using System; +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS +{ + public class UserChannelPersistence + { + private Stack _userChannelStorages; + public int PreviousIndex { get; private set; } + public int Index { get; private set; } + public ProgramSpecifyKind Kind { get; private set; } + + public UserChannelPersistence() + { + _userChannelStorages = new Stack(); + Kind = ProgramSpecifyKind.ExecuteProgram; + PreviousIndex = -1; + Index = 0; + } + + public void Clear() + { + _userChannelStorages.Clear(); + } + + public void Push(byte[] data) + { + _userChannelStorages.Push(data); + } + + public byte[] Pop() + { + return _userChannelStorages.Pop(); + } + + public bool IsEmpty => _userChannelStorages.Count == 0; + + public void ExecuteProgram(ProgramSpecifyKind kind, ulong value) + { + Kind = kind; + PreviousIndex = Index; + + switch (kind) + { + case ProgramSpecifyKind.ExecuteProgram: + Index = (int)value; + break; + case ProgramSpecifyKind.RestartProgram: + break; + default: + throw new NotImplementedException($"{kind} not implemented"); + } + } + } +} diff --git a/Ryujinx.HLE/IHostUiHandler.cs b/Ryujinx.HLE/IHostUiHandler.cs index bd64da87f..8e9bfc773 100644 --- a/Ryujinx.HLE/IHostUiHandler.cs +++ b/Ryujinx.HLE/IHostUiHandler.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; namespace Ryujinx.HLE { @@ -22,5 +23,13 @@ namespace Ryujinx.HLE /// /// True when OK is pressed, False otherwise. bool DisplayMessageDialog(ControllerAppletUiArgs args); + + /// + /// Tell the UI that we need to transisition to another program. + /// + /// The device instance. + /// The program kind. + /// The value associated to the . + void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value); } } \ No newline at end of file diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index 5401f1ccd..f689d2e07 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -35,13 +35,15 @@ namespace Ryujinx.HLE public PerformanceStatistics Statistics { get; private set; } + public UserChannelPersistence UserChannelPersistence { get; } + public Hid Hid { get; private set; } public IHostUiHandler UiHandler { get; set; } public bool EnableDeviceVsync { get; set; } = true; - public Switch(VirtualFileSystem fileSystem, ContentManager contentManager, IRenderer renderer, IAalOutput audioOut) + public Switch(VirtualFileSystem fileSystem, ContentManager contentManager, UserChannelPersistence userChannelPersistence, IRenderer renderer, IAalOutput audioOut) { if (renderer == null) { @@ -53,6 +55,13 @@ namespace Ryujinx.HLE throw new ArgumentNullException(nameof(audioOut)); } + if (userChannelPersistence == null) + { + throw new ArgumentNullException(nameof(userChannelPersistence)); + } + + UserChannelPersistence = userChannelPersistence; + AudioOut = audioOut; Memory = new MemoryBlock(1UL << 32); diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs index 09062c860..fbf14e0eb 100644 --- a/Ryujinx/Ui/ApplicationLibrary.cs +++ b/Ryujinx/Ui/ApplicationLibrary.cs @@ -5,10 +5,11 @@ using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; using LibHac.Ns; -using Ryujinx.Common.Logging; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; using Ryujinx.Configuration.System; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; using Ryujinx.HLE.Loaders.Npdm; using System; using System.Collections.Generic; @@ -39,10 +40,12 @@ namespace Ryujinx.Ui public static IEnumerable GetFilesInDirectory(string directory) { Stack stack = new Stack(); + stack.Push(directory); + while (stack.Count > 0) { - string dir = stack.Pop(); + string dir = stack.Pop(); string[] content = { }; try @@ -57,7 +60,9 @@ namespace Ryujinx.Ui if (content.Length > 0) { foreach (string file in content) + { yield return file; + } } try @@ -72,7 +77,9 @@ namespace Ryujinx.Ui if (content.Length > 0) { foreach (string subdir in content) + { stack.Push(subdir); + } } } } @@ -94,6 +101,7 @@ namespace Ryujinx.Ui // Builds the applications list with paths to found applications List applications = new List(); + foreach (string appDir in appDirs) { @@ -128,6 +136,7 @@ namespace Ryujinx.Ui string developer = "Unknown"; string version = "0"; byte[] applicationIcon = null; + BlitStruct controlHolder = new BlitStruct(1); try @@ -448,23 +457,7 @@ namespace Ryujinx.Ui private static void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId) { - Nca controlNca = null; - - // Add keys to key set if needed - _virtualFileSystem.ImportTickets(pfs); - - // Find the Control NCA and store it in variable called controlNca - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); - - if (nca.Header.ContentType == NcaContentType.Control) - { - controlNca = nca; - } - } + (_, _, Nca controlNca) = ApplicationLoader.GetGameData(_virtualFileSystem, pfs, 0); // Return the ControlFS controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); @@ -574,6 +567,7 @@ namespace Ryujinx.Ui if (!((U8Span)controlTitle.Name).IsEmpty()) { titleName = controlTitle.Name.ToString(); + break; } } @@ -586,6 +580,7 @@ namespace Ryujinx.Ui if (!((U8Span)controlTitle.Publisher).IsEmpty()) { publisher = controlTitle.Publisher.ToString(); + break; } } @@ -611,68 +606,34 @@ namespace Ryujinx.Ui private static bool IsUpdateApplied(string titleId, out string version) { - string jsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json"); + string updatePath = "(unknown)"; - if (File.Exists(jsonPath)) + try { - string updatePath = JsonHelper.DeserializeFromFile(jsonPath).Selected; + (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); - if (!File.Exists(updatePath)) + if (patchNca != null && controlNca != null) { - version = ""; + ApplicationControlProperty controlData = new ApplicationControlProperty(); - return false; - } - - using (FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read)) - { - PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); - - _virtualFileSystem.ImportTickets(nsp); - - foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca")) - { - nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - try - { - Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); - - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) - { - break; - } - - if (nca.Header.ContentType == NcaContentType.Control) - { - ApplicationControlProperty controlData = new ApplicationControlProperty(); - - nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - - version = controlData.DisplayVersion.ToString(); - - return true; - } - } - catch (InvalidDataException) - { - Logger.Warning?.Print(LogClass.Application, - $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}"); - - break; - } - catch (MissingKeyException exception) - { - Logger.Warning?.Print(LogClass.Application, - $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}"); - - break; - } - } + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + version = controlData.DisplayVersion.ToString(); + + return true; } } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, + $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}"); + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}"); + } version = ""; diff --git a/Ryujinx/Ui/GameTableContextMenu.cs b/Ryujinx/Ui/GameTableContextMenu.cs index a654b385d..61e6a80c9 100644 --- a/Ryujinx/Ui/GameTableContextMenu.cs +++ b/Ryujinx/Ui/GameTableContextMenu.cs @@ -13,6 +13,7 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; using System; using System.Buffers; using System.Collections.Generic; @@ -246,7 +247,7 @@ namespace Ryujinx.Ui return workingPath; } - private void ExtractSection(NcaSectionType ncaSectionType) + private void ExtractSection(NcaSectionType ncaSectionType, int programIndex = 0) { FileChooserDialog fileChooser = new FileChooserDialog("Choose the folder to extract into", null, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Extract", ResponseType.Accept); fileChooser.SetPosition(WindowPosition.Center); @@ -340,36 +341,12 @@ namespace Ryujinx.Ui return; } - string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json"); - if (File.Exists(titleUpdateMetadataPath)) + (Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); + + if (updatePatchNca != null) { - string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected; - - if (File.Exists(updatePath)) - { - FileStream updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new PartitionFileSystem(updateFile.AsStorage()); - - _virtualFileSystem.ImportTickets(nsp); - - foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca")) - { - nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); - - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != mainNca.Header.TitleId.ToString("x16")) - { - break; - } - - if (nca.Header.ContentType == NcaContentType.Program) - { - patchNca = nca; - } - } - } + patchNca = updatePatchNca; } int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType); diff --git a/Ryujinx/Ui/GtkHostUiHandler.cs b/Ryujinx/Ui/GtkHostUiHandler.cs index 90830056d..fd193dd7c 100644 --- a/Ryujinx/Ui/GtkHostUiHandler.cs +++ b/Ryujinx/Ui/GtkHostUiHandler.cs @@ -2,6 +2,7 @@ using Gtk; using Ryujinx.Common.Logging; using Ryujinx.HLE; using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using System; using System.Threading; @@ -121,5 +122,11 @@ namespace Ryujinx.Ui return error || okPressed; } + + public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value) + { + device.UserChannelPersistence.ExecuteProgram(kind, value); + MainWindow.GlWidget?.Exit(); + } } } diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 4e6895139..6ce069854 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -11,6 +11,7 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.OpenGL; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem.Content; +using Ryujinx.HLE.HOS; using Ryujinx.Ui.Diagnostic; using System; using System.Diagnostics; @@ -27,6 +28,7 @@ namespace Ryujinx.Ui { private static VirtualFileSystem _virtualFileSystem; private static ContentManager _contentManager; + private static UserChannelPersistence _userChannelPersistence; private static WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution; private static HLE.Switch _emulationContext; @@ -34,12 +36,15 @@ namespace Ryujinx.Ui private static GlRenderer _glWidget; private static GtkHostUiHandler _uiHandler; + public static GlRenderer GlWidget => _glWidget; + private static AutoResetEvent _deviceExitStatus = new AutoResetEvent(false); private static ListStore _tableStore; private static bool _updatingGameTable; private static bool _gameLoaded; + private static string _gamePath; private static bool _ending; #pragma warning disable CS0169, CS0649, IDE0044 @@ -110,6 +115,7 @@ namespace Ryujinx.Ui } _virtualFileSystem = VirtualFileSystem.CreateInstance(); + _userChannelPersistence = new UserChannelPersistence(); _contentManager = new ContentManager(_virtualFileSystem); if (migrationNeeded) @@ -181,6 +187,7 @@ namespace Ryujinx.Ui _statusBar.Hide(); _uiHandler = new GtkHostUiHandler(this); + _gamePath = null; } private void MainWindow_WindowStateEvent(object o, WindowStateEventArgs args) @@ -278,7 +285,7 @@ namespace Ryujinx.Ui { _virtualFileSystem.Reload(); - HLE.Switch instance = new HLE.Switch(_virtualFileSystem, _contentManager, InitializeRenderer(), InitializeAudioEngine()) + HLE.Switch instance = new HLE.Switch(_virtualFileSystem, _contentManager, _userChannelPersistence, InitializeRenderer(), InitializeAudioEngine()) { UiHandler = _uiHandler }; @@ -485,6 +492,7 @@ namespace Ryujinx.Ui } _emulationContext = device; + _gamePath = path; _deviceExitStatus.Reset(); @@ -589,6 +597,7 @@ namespace Ryujinx.Ui UpdateGameTable(); Task.Run(RefreshFirmwareLabel); + Task.Run(HandleRelaunch); _stopEmulation.Sensitive = false; _firmwareInstallFile.Sensitive = true; @@ -962,6 +971,21 @@ namespace Ryujinx.Ui })); } + private void HandleRelaunch() + { + // If the previous index isn't -1, that mean we are relaunching. + if (_userChannelPersistence.PreviousIndex != -1) + { + LoadApplication(_gamePath); + } + else + { + // otherwise, clear state. + _userChannelPersistence = new UserChannelPersistence(); + _gamePath = null; + } + } + private void HandleInstallerDialog(FileChooserDialog fileChooser) { if (fileChooser.Run() == (int)ResponseType.Accept) diff --git a/Ryujinx/Ui/TitleUpdateWindow.cs b/Ryujinx/Ui/TitleUpdateWindow.cs index d332b5477..c3345271a 100644 --- a/Ryujinx/Ui/TitleUpdateWindow.cs +++ b/Ryujinx/Ui/TitleUpdateWindow.cs @@ -9,6 +9,7 @@ using LibHac.Ns; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; using System; using System.Collections.Generic; using System.IO; @@ -83,63 +84,47 @@ namespace Ryujinx.Ui { PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); - _virtualFileSystem.ImportTickets(nsp); - - foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca")) + try { - nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); - try + if (controlNca != null && patchNca != null) { - Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); + ApplicationControlProperty controlData = new ApplicationControlProperty(); - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" == _titleId) - { - if (nca.Header.ContentType == NcaContentType.Control) - { - ApplicationControlProperty controlData = new ApplicationControlProperty(); + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersion.ToString()} - {path}"); + radioButton.JoinGroup(_noUpdateRadioButton); - RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersion.ToString()} - {path}"); - radioButton.JoinGroup(_noUpdateRadioButton); + _availableUpdatesBox.Add(radioButton); + _radioButtonToPathDictionary.Add(radioButton, path); - _availableUpdatesBox.Add(radioButton); - _radioButtonToPathDictionary.Add(radioButton, path); - - radioButton.Show(); - radioButton.Active = true; - } - } - else - { - GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); - - break; - } + radioButton.Show(); + radioButton.Active = true; } - catch (InvalidDataException exception) + else { - Logger.Error?.Print(LogClass.Application, $"{exception.Message}. Errored File: {path}"); - - if (showErrorDialog) - { - GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", "The NCA header content type check has failed. This is usually because the header key is incorrect or missing."); - } - - break; + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); } - catch (MissingKeyException exception) + } + catch (InvalidDataException exception) + { + Logger.Error?.Print(LogClass.Application, $"{exception.Message}. Errored File: {path}"); + + if (showErrorDialog) { - Logger.Error?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {path}"); + GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", "The NCA header content type check has failed. This is usually because the header key is incorrect or missing."); + } + } + catch (MissingKeyException exception) + { + Logger.Error?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {path}"); - if (showErrorDialog) - { - GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", $"Your key set is missing a key with the name: {exception.Name}"); - } - - break; + if (showErrorDialog) + { + GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", $"Your key set is missing a key with the name: {exception.Name}"); } } }