From 63b24b4af2804f173764c98586a19c39db04ad4d Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 5 Jan 2020 04:49:44 -0700 Subject: [PATCH] Rename "RyuFs" directory to "Ryujinx" and use the same savedata system the Switch uses (#801) * Use savedata FS commands from LibHac * Add EnsureSaveData. Use ApplicationControlProperty struct * Add a function to migrate to the new directory layout * LibHac update * Change backup structure * Don't create UI files in the save path * Update RyuFs paths * Add GetProgramIndexForAccessLog Ryujinx only runs one program at a time, so always return values reflecting that * Load control NCA when loading from an NSP * Skip over UI stats when exiting * Set TitleName and TitleId in more cases. Fix TitleID naming style * Completely comment out GUI play stats code * rebase * Update LibHac * Update LibHac * Revert UI changes * Do migration automatically at startup * Rename RyuFs directory to Ryujinx * Update RyuFs text * Store savedata paths in the GUI * Make "Open Save Directory" work * Use a dummy NACP in EnsureSaveData if one is not loaded * Remove manual migration button * Respond to feedback * Don't read the installer config to get a version string * Delete nuget.config * Exclude 'sdcard' and 'bis' during migration Co-authored-by: Thog --- KEYS.md | 4 +- README.md | 4 +- Ryujinx.HLE/FileSystem/VirtualFileSystem.cs | 6 +- Ryujinx.HLE/HOS/Horizon.cs | 74 +++--- .../Acc/IAccountServiceForApplication.cs | 2 +- .../ApplicationProxy/IApplicationFunctions.cs | 40 +++- .../FileSystemProxy/FileSystemProxyHelper.cs | 52 +--- .../HOS/Services/Fs/IFileSystemProxy.cs | 222 +++++++++++++++--- .../HOS/Services/Fs/ISaveDataInfoReader.cs | 31 +++ .../Ns/IApplicationManagerInterface.cs | 202 +--------------- .../QueryPlayStatisticsManager.cs | 4 +- Ryujinx.HLE/Ryujinx.HLE.csproj | 4 +- Ryujinx.HLE/Switch.cs | 2 +- Ryujinx/Program.cs | 4 +- Ryujinx/Ui/AboutWindow.cs | 17 +- Ryujinx/Ui/ApplicationData.cs | 1 + Ryujinx/Ui/ApplicationLibrary.cs | 22 +- Ryujinx/Ui/GameTableContextMenu.cs | 127 ++++++++-- Ryujinx/Ui/MainWindow.cs | 35 ++- Ryujinx/Ui/Migration.cs | 184 +++++++++++++++ Ryujinx/Ui/SaveImporter.cs | 218 +++++++++++++++++ Ryujinx/Ui/SwitchSettings.cs | 6 +- 22 files changed, 877 insertions(+), 384 deletions(-) create mode 100644 Ryujinx.HLE/HOS/Services/Fs/ISaveDataInfoReader.cs create mode 100644 Ryujinx/Ui/Migration.cs create mode 100644 Ryujinx/Ui/SaveImporter.cs diff --git a/KEYS.md b/KEYS.md index 2250cd3e2..868e1f06a 100644 --- a/KEYS.md +++ b/KEYS.md @@ -6,7 +6,7 @@ Keys are required for decrypting most of the file formats used by the Nintendo S * `prod.keys` - Contains common keys used by all Nintendo Switch devices. * `title.keys` - Contains game-specific keys. -Ryujinx will first look for keys in `RyuFS/system`, and if it doesn't find any there it will look in `$HOME/.switch`. +Ryujinx will first look for keys in `Ryujinx/system`, and if it doesn't find any there it will look in `$HOME/.switch`. To dump your `prod.keys` and `title.keys` please follow these following steps. 1. First off learn how to boot into RCM mode and inject payloads if you haven't already. This can be done [here](https://nh-server.github.io/switch-guide/). 2. Make sure you have an SD card with the latest release of [Atmosphere](https://github.com/Atmosphere-NX/Atmosphere/releases) inserted into your Nintendo Switch. @@ -18,7 +18,7 @@ To dump your `prod.keys` and `title.keys` please follow these following steps. 8. After its completion press any button to return to the main menu of Lockpick_RCM. 9. Navigate to and select `Power off` if you have an SD card reader. Or you could Navigate and select `Reboot (RCM)` if you want to mount your SD card using `TegraRCMGUI > Tools > Memloader V3 > MMC - SD Card`. 10. You can find your keys in `sd:/switch/prod.keys` and `sd:/switch/title.keys` respectively. -11. Copy these files and paste them in `RyuFS/system`. +11. Copy these files and paste them in `Ryujinx/system`. And you're done! ## Title keys diff --git a/README.md b/README.md index f14b7d522..80bfb6fd9 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ If you build it yourself you will need to: Run `dotnet run -c Release -- path\to\homebrew.nro` inside the Ryujinx project folder to run homebrew apps. Run `dotnet run -c Release -- path\to\game.nsp/xci` to run official games. -Every file related to Ryujinx is stored in the `RyuFs` folder. Located in `C:\Users\USERNAME\AppData\Roaming\` for Windows, `/home/USERNAME/.config` for Linux or `/Users/USERNAME/Library/Application Support/` for macOS. It can also be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI. +Every file related to Ryujinx is stored in the `Ryujinx` folder. Located in `C:\Users\USERNAME\AppData\Roaming\` for Windows, `/home/USERNAME/.config` for Linux or `/Users/USERNAME/Library/Application Support/` for macOS. It can also be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI. ## Latest build @@ -49,7 +49,7 @@ The latest automatic build for Windows, macOS, and Linux can be found on the [Of - **System Titles** - Some of our System Module implementations, like `time`, require [System Data Archives](https://switchbrew.org/wiki/Title_list#System_Data_Archives). You can install them by mounting your nand partition using [HacDiskMount](https://switchtools.sshnuke.net/) and copying the content to `RyuFs/nand/system`. + Some of our System Module implementations, like `time`, require [System Data Archives](https://switchbrew.org/wiki/Title_list#System_Data_Archives). You can install them by mounting your nand partition using [HacDiskMount](https://switchtools.sshnuke.net/) and copying the content to `Ryujinx/nand/system`. - **Executables** diff --git a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index 5511ebcc9..257a55a22 100644 --- a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -7,9 +7,9 @@ namespace Ryujinx.HLE.FileSystem { public class VirtualFileSystem : IDisposable { - public const string BasePath = "RyuFs"; - public const string NandPath = "nand"; - public const string SdCardPath = "sdmc"; + public const string BasePath = "Ryujinx"; + public const string NandPath = "bis"; + public const string SdCardPath = "sdcard"; public const string SystemPath = "system"; public static string SafeNandPath = Path.Combine(NandPath, "safe"); diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 2e5b7a704..164a49a00 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -1,8 +1,10 @@ using LibHac; +using LibHac.Common; using LibHac.Fs; using LibHac.FsService; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; +using LibHac.Ns; using LibHac.Spl; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem.Content; @@ -103,7 +105,7 @@ namespace Ryujinx.HLE.HOS private bool _hasStarted; - public Nacp ControlData { get; set; } + public BlitStruct ControlData { get; set; } public string TitleName { get; private set; } @@ -116,11 +118,13 @@ namespace Ryujinx.HLE.HOS internal long HidBaseAddress { get; private set; } internal FileSystemServer FsServer { get; private set; } + public FileSystemClient FsClient { get; private set; } + internal EmulatedGameCard GameCard { get; private set; } public Horizon(Switch device) { - ControlData = new Nacp(); + ControlData = new BlitStruct(1); Device = device; @@ -245,6 +249,7 @@ namespace Ryujinx.HLE.HOS }; FsServer = new FileSystemServer(fsServerConfig); + FsClient = FsServer.CreateFileSystemClient(); } public void LoadCart(string exeFsDir, string romFsFile = null) @@ -350,6 +355,10 @@ namespace Ryujinx.HLE.HOS { ReadControlData(controlNca); } + else + { + ControlData.ByteSpan.Clear(); + } return (mainNca, patchNca, controlNca); } @@ -362,9 +371,23 @@ namespace Ryujinx.HLE.HOS if (result.IsSuccess()) { - ControlData = new Nacp(controlFile.AsStream()); + result = controlFile.Read(out long bytesRead, 0, ControlData.ByteSpan, ReadOption.None); - TitleName = ControlData.Descriptions[(int)State.DesiredTitleLanguage].Title; + if (result.IsSuccess() && bytesRead == ControlData.ByteSpan.Length) + { + TitleName = ControlData.Value + .Titles[(int) State.DesiredTitleLanguage].Name.ToString(); + + if (string.IsNullOrWhiteSpace(TitleName)) + { + TitleName = ControlData.Value.Titles.ToArray() + .FirstOrDefault(x => x.Name[0] != 0).Name.ToString(); + } + } + } + else + { + ControlData.ByteSpan.Clear(); } } @@ -489,33 +512,16 @@ namespace Ryujinx.HLE.HOS } LoadExeFs(codeFs, out Npdm metaData); - - Nacp ReadControlData() - { - IFileSystem controlRomfs = controlNca.OpenFileSystem(NcaSectionType.Data, FsIntegrityCheckLevel); - - controlRomfs.OpenFile(out IFile controlFile, "/control.nacp", OpenMode.Read).ThrowIfFailure(); - - Nacp controlData = new Nacp(controlFile.AsStream()); - - TitleName = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title; - TitleId = metaData.Aci0.TitleId.ToString("x16"); - - if (string.IsNullOrWhiteSpace(TitleName)) - { - TitleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; - } - - return controlData; - } + + TitleId = metaData.Aci0.TitleId.ToString("x16"); if (controlNca != null) { - ReadControlData(); + ReadControlData(controlNca); } else { - TitleId = metaData.Aci0.TitleId.ToString("x16"); + ControlData.ByteSpan.Clear(); } } @@ -613,28 +619,28 @@ namespace Ryujinx.HLE.HOS if (nacpSize != 0) { input.Seek(obj.FileSize + (long)nacpOffset, SeekOrigin.Begin); - using (MemoryStream stream = new MemoryStream(reader.ReadBytes((int)nacpSize))) - { - ControlData = new Nacp(stream); - } - metaData.TitleName = ControlData.Descriptions[(int)State.DesiredTitleLanguage].Title; + reader.Read(ControlData.ByteSpan); + + ref ApplicationControlProperty nacp = ref ControlData.Value; + + metaData.TitleName = nacp.Titles[(int)State.DesiredTitleLanguage].Name.ToString(); if (string.IsNullOrWhiteSpace(metaData.TitleName)) { - metaData.TitleName = ControlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; + metaData.TitleName = nacp.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString(); } - metaData.Aci0.TitleId = ControlData.PresenceGroupId; + metaData.Aci0.TitleId = nacp.PresenceGroupId; if (metaData.Aci0.TitleId == 0) { - metaData.Aci0.TitleId = ControlData.SaveDataOwnerId; + metaData.Aci0.TitleId = nacp.SaveDataOwnerId.Value; } if (metaData.Aci0.TitleId == 0) { - metaData.Aci0.TitleId = ControlData.AddOnContentBaseId - 0x1000; + metaData.Aci0.TitleId = nacp.AddOnContentBaseId - 0x1000; } if (metaData.Aci0.TitleId.ToString("x16") == "fffffffffffff000") diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForApplication.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForApplication.cs index f8e6b22a5..ec26d11fa 100644 --- a/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForApplication.cs +++ b/Ryujinx.HLE/HOS/Services/Account/Acc/IAccountServiceForApplication.cs @@ -287,7 +287,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc // 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(context.Device.System.ControlData.UserAccountSwitchLock); + context.ResponseData.Write(context.Device.System.ControlData.Value.UserAccountSwitchLock); Logger.PrintStub(LogClass.ServiceAcc); 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 464d0b47b..904264aa8 100644 --- a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs +++ b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs @@ -1,13 +1,19 @@ +using LibHac; +using LibHac.Account; +using LibHac.Common; +using LibHac.Ncm; +using LibHac.Ns; +using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.HLE.HOS.Services.Am.AppletAE; using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage; using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService; -using Ryujinx.HLE.Utilities; using System; +using static LibHac.Fs.ApplicationSaveDataManagement; + namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy { class IApplicationFunctions : IpcService @@ -24,7 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati public ResultCode PopLaunchParameter(ServiceCtx context) { // Only the first 0x18 bytes of the Data seems to be actually used. - MakeObject(context, new IStorage(StorageHelper.MakeLaunchParams())); + MakeObject(context, new AppletAE.IStorage(StorageHelper.MakeLaunchParams())); return ResultCode.Success; } @@ -33,13 +39,33 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati // EnsureSaveData(nn::account::Uid) -> u64 public ResultCode EnsureSaveData(ServiceCtx context) { - UInt128 userId = new UInt128(context.RequestData.ReadBytes(0x10)); + Uid userId = context.RequestData.ReadStruct(); + TitleId titleId = new TitleId(context.Process.TitleId); - context.ResponseData.Write(0L); + BlitStruct controlHolder = context.Device.System.ControlData; - Logger.PrintStub(LogClass.ServiceAm, new { userId }); + ref ApplicationControlProperty control = ref controlHolder.Value; - return ResultCode.Success; + if (Util.IsEmpty(controlHolder.ByteSpan)) + { + // If the current application doesn't have a loaded control property, create a dummy one + // and set the savedata sizes so a user savedata will be created. + control = ref new BlitStruct(1).Value; + + // The set sizes don't actually matter as long as they're non-zero because we use directory savedata. + control.UserAccountSaveDataSize = 0x4000; + control.UserAccountSaveDataJournalSize = 0x4000; + + Logger.PrintWarning(LogClass.ServiceAm, + "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games."); + } + + Result result = EnsureApplicationSaveData(context.Device.System.FsClient, out long requiredSize, titleId, + ref context.Device.System.ControlData.Value, ref userId); + + context.ResponseData.Write(requiredSize); + + return (ResultCode)result.Value; } [Command(21)] diff --git a/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs b/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs index 2b0f06dda..1dd5fb86e 100644 --- a/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs +++ b/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs @@ -3,54 +3,12 @@ using LibHac.Fs; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; using LibHac.Spl; -using Ryujinx.Common; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Utilities; using System.IO; namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy { static class FileSystemProxyHelper { - public static ResultCode LoadSaveDataFileSystem(ServiceCtx context, bool readOnly, out IFileSystem loadedFileSystem) - { - loadedFileSystem = null; - - SaveSpaceId saveSpaceId = (SaveSpaceId)context.RequestData.ReadInt64(); - ulong titleId = context.RequestData.ReadUInt64(); - UInt128 userId = context.RequestData.ReadStruct(); - long saveId = context.RequestData.ReadInt64(); - SaveDataType saveDataType = (SaveDataType)context.RequestData.ReadByte(); - SaveInfo saveInfo = new SaveInfo(titleId, saveId, saveDataType, saveSpaceId, userId); - string savePath = context.Device.FileSystem.GetSavePath(context, saveInfo); - - try - { - LocalFileSystem fileSystem = new LocalFileSystem(savePath); - - Result result = DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem dirFileSystem, fileSystem); - if (result.IsFailure()) - { - return (ResultCode)result.Value; - } - - LibHac.Fs.IFileSystem saveFileSystem = dirFileSystem; - - if (readOnly) - { - saveFileSystem = new ReadOnlyFileSystem(saveFileSystem); - } - - loadedFileSystem = new IFileSystem(saveFileSystem); - } - catch (HorizonResultException ex) - { - return (ResultCode)ex.ResultValue.Value; - } - - return ResultCode.Success; - } - public static ResultCode OpenNsp(ServiceCtx context, string pfsPath, out IFileSystem openedFileSystem) { openedFileSystem = null; @@ -154,5 +112,15 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy } } } + + public static Result ReadFsPath(out FsPath path, ServiceCtx context, int index = 0) + { + long position = context.Request.SendBuff[index].Position; + long size = context.Request.SendBuff[index].Size; + + byte[] pathBytes = context.Memory.ReadBytes(position, size); + + return FsPath.FromSpan(out path, pathBytes); + } } } diff --git a/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs b/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs index 381110199..60f4a3f43 100644 --- a/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs +++ b/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs @@ -3,13 +3,14 @@ using LibHac.Fs; using LibHac.FsService; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; +using LibHac.Ncm; +using Ryujinx.Common; using Ryujinx.Common.Logging; -using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy; using System.IO; -using static Ryujinx.HLE.FileSystem.VirtualFileSystem; using static Ryujinx.HLE.Utilities.StringUtils; +using StorageId = Ryujinx.HLE.FileSystem.StorageId; namespace Ryujinx.HLE.HOS.Services.Fs { @@ -90,29 +91,13 @@ namespace Ryujinx.HLE.HOS.Services.Fs // OpenBisFileSystem(nn::fssrv::sf::Partition partitionID, buffer, 0x19, 0x301>) -> object Bis public ResultCode OpenBisFileSystem(ServiceCtx context) { - int bisPartitionId = context.RequestData.ReadInt32(); - string partitionString = ReadUtf8String(context); - string bisPartitionPath = string.Empty; + BisPartitionId bisPartitionId = (BisPartitionId)context.RequestData.ReadInt32(); - switch (bisPartitionId) - { - case 29: - bisPartitionPath = SafeNandPath; - break; - case 30: - case 31: - bisPartitionPath = SystemNandPath; - break; - case 32: - bisPartitionPath = UserNandPath; - break; - default: - return ResultCode.InvalidInput; - } + Result rc = FileSystemProxyHelper.ReadFsPath(out FsPath path, context); + if (rc.IsFailure()) return (ResultCode)rc.Value; - string fullPath = context.Device.FileSystem.GetFullPartitionPath(bisPartitionPath); - - LocalFileSystem fileSystem = new LocalFileSystem(fullPath); + rc = _baseFileSystemProxy.OpenBisFileSystem(out LibHac.Fs.IFileSystem fileSystem, ref path, bisPartitionId); + if (rc.IsFailure()) return (ResultCode)rc.Value; MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem)); @@ -123,15 +108,69 @@ namespace Ryujinx.HLE.HOS.Services.Fs // OpenSdCardFileSystem() -> object public ResultCode OpenSdCardFileSystem(ServiceCtx context) { - string sdCardPath = context.Device.FileSystem.GetSdCardPath(); - - LocalFileSystem fileSystem = new LocalFileSystem(sdCardPath); + Result rc = _baseFileSystemProxy.OpenSdCardFileSystem(out LibHac.Fs.IFileSystem fileSystem); + if (rc.IsFailure()) return (ResultCode)rc.Value; MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem)); return ResultCode.Success; } + [Command(21)] + public ResultCode DeleteSaveDataFileSystem(ServiceCtx context) + { + ulong saveDataId = context.RequestData.ReadUInt64(); + + Result result = _baseFileSystemProxy.DeleteSaveDataFileSystem(saveDataId); + + return (ResultCode)result.Value; + } + + [Command(22)] + public ResultCode CreateSaveDataFileSystem(ServiceCtx context) + { + SaveDataAttribute attribute = context.RequestData.ReadStruct(); + SaveDataCreateInfo createInfo = context.RequestData.ReadStruct(); + SaveMetaCreateInfo metaCreateInfo = context.RequestData.ReadStruct(); + + Result result = _baseFileSystemProxy.CreateSaveDataFileSystem(ref attribute, ref createInfo, ref metaCreateInfo); + + return (ResultCode)result.Value; + } + + [Command(23)] + public ResultCode CreateSaveDataFileSystemBySystemSaveDataId(ServiceCtx context) + { + SaveDataAttribute attribute = context.RequestData.ReadStruct(); + SaveDataCreateInfo createInfo = context.RequestData.ReadStruct(); + + Result result = _baseFileSystemProxy.CreateSaveDataFileSystemBySystemSaveDataId(ref attribute, ref createInfo); + + return (ResultCode)result.Value; + } + + [Command(25)] + public ResultCode DeleteSaveDataFileSystemBySaveDataSpaceId(ServiceCtx context) + { + SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64(); + ulong saveDataId = context.RequestData.ReadUInt64(); + + Result result = _baseFileSystemProxy.DeleteSaveDataFileSystemBySaveDataSpaceId(spaceId, saveDataId); + + return (ResultCode)result.Value; + } + + [Command(28)] + public ResultCode DeleteSaveDataFileSystemBySaveDataAttribute(ServiceCtx context) + { + SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64(); + SaveDataAttribute attribute = context.RequestData.ReadStruct(); + + Result result = _baseFileSystemProxy.DeleteSaveDataFileSystemBySaveDataAttribute(spaceId, ref attribute); + + return (ResultCode)result.Value; + } + [Command(30)] // OpenGameCardStorage(u32, u32) -> object public ResultCode OpenGameCardStorage(ServiceCtx context) @@ -149,46 +188,141 @@ namespace Ryujinx.HLE.HOS.Services.Fs return (ResultCode)result.Value; } + [Command(35)] + public ResultCode CreateSaveDataFileSystemWithHashSalt(ServiceCtx context) + { + SaveDataAttribute attribute = context.RequestData.ReadStruct(); + SaveDataCreateInfo createInfo = context.RequestData.ReadStruct(); + SaveMetaCreateInfo metaCreateInfo = context.RequestData.ReadStruct(); + HashSalt hashSalt = context.RequestData.ReadStruct(); + + Result result = _baseFileSystemProxy.CreateSaveDataFileSystemWithHashSalt(ref attribute, ref createInfo, ref metaCreateInfo, ref hashSalt); + + return (ResultCode)result.Value; + } + [Command(51)] // OpenSaveDataFileSystem(u8 save_data_space_id, nn::fssrv::sf::SaveStruct saveStruct) -> object saveDataFs public ResultCode OpenSaveDataFileSystem(ServiceCtx context) { - ResultCode result = FileSystemProxyHelper.LoadSaveDataFileSystem(context, false, out FileSystemProxy.IFileSystem fileSystem); + SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64(); + SaveDataAttribute attribute = context.RequestData.ReadStruct(); - if (result == ResultCode.Success) + if (attribute.TitleId == TitleId.Zero) { - MakeObject(context, fileSystem); + attribute.TitleId = new TitleId(context.Process.TitleId); } - return result; + Result result = _baseFileSystemProxy.OpenSaveDataFileSystem(out LibHac.Fs.IFileSystem fileSystem, spaceId, ref attribute); + + if (result.IsSuccess()) + { + MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem)); + } + + return (ResultCode)result.Value; } [Command(52)] // OpenSaveDataFileSystemBySystemSaveDataId(u8 save_data_space_id, nn::fssrv::sf::SaveStruct saveStruct) -> object systemSaveDataFs public ResultCode OpenSaveDataFileSystemBySystemSaveDataId(ServiceCtx context) { - ResultCode result = FileSystemProxyHelper.LoadSaveDataFileSystem(context, false, out FileSystemProxy.IFileSystem fileSystem); + SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64(); + SaveDataAttribute attribute = context.RequestData.ReadStruct(); - if (result == ResultCode.Success) + Result result = _baseFileSystemProxy.OpenSaveDataFileSystemBySystemSaveDataId(out LibHac.Fs.IFileSystem fileSystem, spaceId, ref attribute); + + if (result.IsSuccess()) { - MakeObject(context, fileSystem); + MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem)); } - return result; + return (ResultCode)result.Value; } [Command(53)] // OpenReadOnlySaveDataFileSystem(u8 save_data_space_id, nn::fssrv::sf::SaveStruct save_struct) -> object public ResultCode OpenReadOnlySaveDataFileSystem(ServiceCtx context) { - ResultCode result = FileSystemProxyHelper.LoadSaveDataFileSystem(context, true, out FileSystemProxy.IFileSystem fileSystem); + SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64(); + SaveDataAttribute attribute = context.RequestData.ReadStruct(); - if (result == ResultCode.Success) + if (attribute.TitleId == TitleId.Zero) { - MakeObject(context, fileSystem); + attribute.TitleId = new TitleId(context.Process.TitleId); } - return result; + Result result = _baseFileSystemProxy.OpenReadOnlySaveDataFileSystem(out LibHac.Fs.IFileSystem fileSystem, spaceId, ref attribute); + + if (result.IsSuccess()) + { + MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem)); + } + + return (ResultCode)result.Value; + } + + [Command(60)] + public ResultCode OpenSaveDataInfoReader(ServiceCtx context) + { + Result result = _baseFileSystemProxy.OpenSaveDataInfoReader(out LibHac.FsService.ISaveDataInfoReader infoReader); + + if (result.IsSuccess()) + { + MakeObject(context, new ISaveDataInfoReader(infoReader)); + } + + return (ResultCode)result.Value; + } + + [Command(61)] + public ResultCode OpenSaveDataInfoReaderBySaveDataSpaceId(ServiceCtx context) + { + SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadByte(); + + Result result = _baseFileSystemProxy.OpenSaveDataInfoReaderBySaveDataSpaceId(out LibHac.FsService.ISaveDataInfoReader infoReader, spaceId); + + if (result.IsSuccess()) + { + MakeObject(context, new ISaveDataInfoReader(infoReader)); + } + + return (ResultCode)result.Value; + } + + [Command(67)] + public ResultCode FindSaveDataWithFilter(ServiceCtx context) + { + SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64(); + SaveDataFilter filter = context.RequestData.ReadStruct(); + + long bufferPosition = context.Request.ReceiveBuff[0].Position; + long bufferLen = context.Request.ReceiveBuff[0].Size; + + byte[] infoBuffer = new byte[bufferLen]; + + Result result = _baseFileSystemProxy.FindSaveDataWithFilter(out long count, infoBuffer, spaceId, ref filter); + + context.Memory.WriteBytes(bufferPosition, infoBuffer); + context.ResponseData.Write(count); + + return (ResultCode)result.Value; + } + + [Command(68)] + public ResultCode OpenSaveDataInfoReaderWithFilter(ServiceCtx context) + { + SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64(); + SaveDataFilter filter = context.RequestData.ReadStruct(); + + Result result = _baseFileSystemProxy.OpenSaveDataInfoReaderWithFilter(out LibHac.FsService.ISaveDataInfoReader infoReader, spaceId, ref filter); + + if (result.IsSuccess()) + { + MakeObject(context, new ISaveDataInfoReader(infoReader)); + } + + return (ResultCode)result.Value; } [Command(200)] @@ -306,5 +440,17 @@ namespace Ryujinx.HLE.HOS.Services.Fs return ResultCode.Success; } + + [Command(1011)] + public ResultCode GetProgramIndexForAccessLog(ServiceCtx context) + { + int programIndex = 0; + int programCount = 1; + + context.ResponseData.Write(programIndex); + context.ResponseData.Write(programCount); + + return ResultCode.Success; + } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Fs/ISaveDataInfoReader.cs b/Ryujinx.HLE/HOS/Services/Fs/ISaveDataInfoReader.cs new file mode 100644 index 000000000..3d5ae8e2c --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Fs/ISaveDataInfoReader.cs @@ -0,0 +1,31 @@ +using LibHac; + +namespace Ryujinx.HLE.HOS.Services.Fs +{ + class ISaveDataInfoReader : IpcService + { + private LibHac.FsService.ISaveDataInfoReader _baseReader; + + public ISaveDataInfoReader(LibHac.FsService.ISaveDataInfoReader baseReader) + { + _baseReader = baseReader; + } + + [Command(0)] + // ReadSaveDataInfo() -> (u64, buffer) + public ResultCode ReadSaveDataInfo(ServiceCtx context) + { + long bufferPosition = context.Request.ReceiveBuff[0].Position; + long bufferLen = context.Request.ReceiveBuff[0].Size; + + byte[] infoBuffer = new byte[bufferLen]; + + Result result = _baseReader.ReadSaveDataInfo(out long readCount, infoBuffer); + + context.Memory.WriteBytes(bufferPosition, infoBuffer); + context.ResponseData.Write(readCount); + + return (ResultCode)result.Value; + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/Ns/IApplicationManagerInterface.cs b/Ryujinx.HLE/HOS/Services/Ns/IApplicationManagerInterface.cs index d09403f9d..e185233be 100644 --- a/Ryujinx.HLE/HOS/Services/Ns/IApplicationManagerInterface.cs +++ b/Ryujinx.HLE/HOS/Services/Ns/IApplicationManagerInterface.cs @@ -1,8 +1,4 @@ -using LibHac; -using System; -using System.Text; - -namespace Ryujinx.HLE.HOS.Services.Ns +namespace Ryujinx.HLE.HOS.Services.Ns { [Service("ns:am")] class IApplicationManagerInterface : IpcService @@ -10,201 +6,17 @@ namespace Ryujinx.HLE.HOS.Services.Ns public IApplicationManagerInterface(ServiceCtx context) { } [Command(400)] - // GetApplicationControlData(unknown<0x10>) -> (unknown<4>, buffer) + // GetApplicationControlData(u8, u64) -> (unknown<4>, buffer) public ResultCode GetApplicationControlData(ServiceCtx context) { + byte source = (byte)context.RequestData.ReadInt64(); + ulong titleId = (byte)context.RequestData.ReadUInt64(); + long position = context.Request.ReceiveBuff[0].Position; - Nacp nacp = context.Device.System.ControlData; + byte[] nacpData = context.Device.System.ControlData.ByteSpan.ToArray(); - for (int i = 0; i < 0x10; i++) - { - NacpDescription description = nacp.Descriptions[i]; - - byte[] titleData = new byte[0x200]; - byte[] developerData = new byte[0x100]; - - if (description !=null && description.Title != null) - { - byte[] titleDescriptionData = Encoding.ASCII.GetBytes(description.Title); - Buffer.BlockCopy(titleDescriptionData, 0, titleData, 0, titleDescriptionData.Length); - - } - - if (description != null && description.Developer != null) - { - byte[] developerDescriptionData = Encoding.ASCII.GetBytes(description.Developer); - Buffer.BlockCopy(developerDescriptionData, 0, developerData, 0, developerDescriptionData.Length); - } - - context.Memory.WriteBytes(position, titleData); - context.Memory.WriteBytes(position + 0x200, developerData); - - position += i * 0x300; - } - - byte[] isbn = new byte[0x25]; - - if (nacp.Isbn != null) - { - byte[] isbnData = Encoding.ASCII.GetBytes(nacp.Isbn); - Buffer.BlockCopy(isbnData, 0, isbn, 0, isbnData.Length); - } - - context.Memory.WriteBytes(position, isbn); - position += isbn.Length; - - context.Memory.WriteByte(position++, nacp.StartupUserAccount); - context.Memory.WriteByte(position++, nacp.UserAccountSwitchLock); - context.Memory.WriteByte(position++, nacp.AocRegistrationType); - - context.Memory.WriteInt32(position, nacp.AttributeFlag); - position += 4; - - context.Memory.WriteUInt32(position, nacp.SupportedLanguageFlag); - position += 4; - - context.Memory.WriteUInt32(position, nacp.ParentalControlFlag); - position += 4; - - context.Memory.WriteByte(position++, nacp.Screenshot); - context.Memory.WriteByte(position++, nacp.VideoCapture); - context.Memory.WriteByte(position++, nacp.DataLossConfirmation); - context.Memory.WriteByte(position++, nacp.PlayLogPolicy); - - context.Memory.WriteUInt64(position, nacp.PresenceGroupId); - position += 8; - - for (int i = 0; i < nacp.RatingAge.Length; i++) - { - context.Memory.WriteSByte(position++, nacp.RatingAge[i]); - } - - byte[] displayVersion = new byte[0x10]; - - if (nacp.DisplayVersion != null) - { - byte[] displayVersionData = Encoding.ASCII.GetBytes(nacp.DisplayVersion); - Buffer.BlockCopy(displayVersionData, 0, displayVersion, 0, displayVersionData.Length); - } - - context.Memory.WriteBytes(position, displayVersion); - position += displayVersion.Length; - - context.Memory.WriteUInt64(position, nacp.AddOnContentBaseId); - position += 8; - - context.Memory.WriteUInt64(position, nacp.SaveDataOwnerId); - position += 8; - - context.Memory.WriteInt64(position, nacp.UserAccountSaveDataSize); - position += 8; - - context.Memory.WriteInt64(position, nacp.UserAccountSaveDataJournalSize); - position += 8; - - context.Memory.WriteInt64(position, nacp.DeviceSaveDataSize); - position += 8; - - context.Memory.WriteInt64(position, nacp.DeviceSaveDataJournalSize); - position += 8; - - context.Memory.WriteInt64(position, nacp.BcatDeliveryCacheStorageSize); - position += 8; - - byte[] applicationErrorCodeCategory = new byte[0x8]; - - if (nacp.ApplicationErrorCodeCategory != null) - { - byte[] applicationErrorCodeCategoryData = Encoding.ASCII.GetBytes(nacp.ApplicationErrorCodeCategory); - Buffer.BlockCopy(applicationErrorCodeCategoryData, 0, applicationErrorCodeCategoryData, 0, applicationErrorCodeCategoryData.Length); - } - - context.Memory.WriteBytes(position, applicationErrorCodeCategory); - position += applicationErrorCodeCategory.Length; - - for (int i = 0; i < nacp.LocalCommunicationId.Length; i++) - { - context.Memory.WriteUInt64(position, nacp.LocalCommunicationId[i]); - position += 8; - } - - context.Memory.WriteByte(position++, nacp.LogoType); - context.Memory.WriteByte(position++, nacp.LogoHandling); - context.Memory.WriteByte(position++, nacp.RuntimeAddOnContentInstall); - - byte[] reserved000 = new byte[0x3]; - context.Memory.WriteBytes(position, reserved000); - position += reserved000.Length; - - context.Memory.WriteByte(position++, nacp.CrashReport); - context.Memory.WriteByte(position++, nacp.Hdcp); - context.Memory.WriteUInt64(position, nacp.SeedForPseudoDeviceId); - position += 8; - - byte[] bcatPassphrase = new byte[65]; - if (nacp.BcatPassphrase != null) - { - byte[] bcatPassphraseData = Encoding.ASCII.GetBytes(nacp.BcatPassphrase); - Buffer.BlockCopy(bcatPassphraseData, 0, bcatPassphrase, 0, bcatPassphraseData.Length); - } - - context.Memory.WriteBytes(position, bcatPassphrase); - position += bcatPassphrase.Length; - - context.Memory.WriteByte(position++, nacp.Reserved01); - - byte[] reserved02 = new byte[0x6]; - context.Memory.WriteBytes(position, reserved02); - position += reserved02.Length; - - context.Memory.WriteInt64(position, nacp.UserAccountSaveDataSizeMax); - position += 8; - - context.Memory.WriteInt64(position, nacp.UserAccountSaveDataJournalSizeMax); - position += 8; - - context.Memory.WriteInt64(position, nacp.DeviceSaveDataSizeMax); - position += 8; - - context.Memory.WriteInt64(position, nacp.DeviceSaveDataJournalSizeMax); - position += 8; - - context.Memory.WriteInt64(position, nacp.TemporaryStorageSize); - position += 8; - - context.Memory.WriteInt64(position, nacp.CacheStorageSize); - position += 8; - - context.Memory.WriteInt64(position, nacp.CacheStorageJournalSize); - position += 8; - - context.Memory.WriteInt64(position, nacp.CacheStorageDataAndJournalSizeMax); - position += 8; - - context.Memory.WriteInt16(position, nacp.CacheStorageIndex); - position += 2; - - byte[] reserved03 = new byte[0x6]; - context.Memory.WriteBytes(position, reserved03); - position += reserved03.Length; - - for (int i = 0; i < 16; i++) - { - ulong value = 0; - - if (nacp.PlayLogQueryableApplicationId.Count > i) - { - value = nacp.PlayLogQueryableApplicationId[i]; - } - - context.Memory.WriteUInt64(position, value); - position += 8; - } - - context.Memory.WriteByte(position++, nacp.PlayLogQueryCapability); - context.Memory.WriteByte(position++, nacp.RepairFlag); - context.Memory.WriteByte(position++, nacp.ProgramIndex); + context.Memory.WriteBytes(position, nacpData); return ResultCode.Success; } diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/QueryPlayStatisticsManager.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/QueryPlayStatisticsManager.cs index b3646925f..925a2593b 100644 --- a/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/QueryPlayStatisticsManager.cs +++ b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/QueryPlayStatisticsManager.cs @@ -30,7 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService } } - PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.System.ControlData.PlayLogQueryCapability; + PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.System.ControlData.Value.PlayLogQueryCapability; List titleIds = new List(); @@ -44,7 +44,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService // Check if input title ids are in the whitelist. foreach (ulong titleId in titleIds) { - if (!context.Device.System.ControlData.PlayLogQueryableApplicationId.Contains(titleId)) + if (!context.Device.System.ControlData.Value.PlayLogQueryableApplicationId.Contains(titleId)) { return (ResultCode)Am.ResultCode.ObjectInvalid; } diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj index 42bc4ddc0..683714654 100644 --- a/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -51,7 +51,7 @@ - + diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index a4d07f6ac..d1ca3d1f4 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -21,7 +21,7 @@ namespace Ryujinx.HLE internal NvGpu Gpu { get; private set; } - internal VirtualFileSystem FileSystem { get; private set; } + public VirtualFileSystem FileSystem { get; private set; } public Horizon System { get; private set; } diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs index 98b8d692d..b2b6cc735 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -48,11 +48,11 @@ namespace Ryujinx Application.Init(); - string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "system", "prod.keys"); + string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ryujinx", "system", "prod.keys"); string userProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch", "prod.keys"); if (!File.Exists(appDataPath) && !File.Exists(userProfilePath)) { - GtkDialog.CreateErrorDialog($"Key file was not found. Please refer to `KEYS.md` for more info"); + GtkDialog.CreateErrorDialog("Key file was not found. Please refer to `KEYS.md` for more info"); } MainWindow mainWindow = new MainWindow(); diff --git a/Ryujinx/Ui/AboutWindow.cs b/Ryujinx/Ui/AboutWindow.cs index b95342437..0332d7a48 100644 --- a/Ryujinx/Ui/AboutWindow.cs +++ b/Ryujinx/Ui/AboutWindow.cs @@ -40,21 +40,8 @@ namespace Ryujinx.Ui _discordLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.DiscordLogo.png", 30 , 30 ); _twitterLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.TwitterLogo.png", 30 , 30 ); - try - { - IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); - - using (Stream stream = File.OpenRead(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "Installer", "Config", "Config.json"))) - { - AboutInformation = JsonSerializer.Deserialize(stream, resolver); - } - - _versionText.Text = $"Version {AboutInformation.InstallVersion} - {AboutInformation.InstallBranch} ({AboutInformation.InstallCommit})"; - } - catch - { - _versionText.Text = "Unknown Version"; - } + // todo: Get version string + _versionText.Text = "Unknown Version"; } private static void OpenUrl(string url) diff --git a/Ryujinx/Ui/ApplicationData.cs b/Ryujinx/Ui/ApplicationData.cs index f43099c1c..defc5e983 100644 --- a/Ryujinx/Ui/ApplicationData.cs +++ b/Ryujinx/Ui/ApplicationData.cs @@ -13,5 +13,6 @@ public string FileExtension { get; set; } public string FileSize { get; set; } public string Path { get; set; } + public string SaveDataPath { get; set; } } } diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs index fecbf27b4..90d333a25 100644 --- a/Ryujinx/Ui/ApplicationLibrary.cs +++ b/Ryujinx/Ui/ApplicationLibrary.cs @@ -1,14 +1,17 @@ using JsonPrettyPrinterPlus; using LibHac; using LibHac.Fs; +using LibHac.Fs.Shim; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; +using LibHac.Ncm; using LibHac.Spl; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.Loaders.Npdm; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -16,6 +19,7 @@ using System.Text; using Utf8Json; using Utf8Json.Resolvers; +using RightsId = LibHac.Fs.RightsId; using TitleLanguage = Ryujinx.HLE.HOS.SystemState.TitleLanguage; namespace Ryujinx.Ui @@ -34,7 +38,7 @@ namespace Ryujinx.Ui private static TitleLanguage _desiredTitleLanguage; private static ApplicationMetadata _appMetadata; - public static void LoadApplications(List appDirs, Keyset keySet, TitleLanguage desiredTitleLanguage) + public static void LoadApplications(List appDirs, Keyset keySet, TitleLanguage desiredTitleLanguage, FileSystemClient fsClient = null, VirtualFileSystem vfs = null) { int numApplicationsFound = 0; int numApplicationsLoaded = 0; @@ -127,6 +131,7 @@ namespace Ryujinx.Ui string titleId = "0000000000000000"; string developer = "Unknown"; string version = "0"; + string saveDataPath = null; byte[] applicationIcon = null; using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read)) @@ -336,6 +341,20 @@ namespace Ryujinx.Ui (bool favorite, string timePlayed, string lastPlayed) = GetMetadata(titleId); + if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNum)) + { + SaveDataFilter filter = new SaveDataFilter(); + filter.SetUserId(new UserId(1, 0)); + filter.SetTitleId(new TitleId(titleIdNum)); + + Result result = fsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter); + + if (result.IsSuccess()) + { + saveDataPath = Path.Combine(vfs.GetNandPath(), $"user/save/{saveDataInfo.SaveDataId:x16}"); + } + } + ApplicationData data = new ApplicationData() { Favorite = favorite, @@ -349,6 +368,7 @@ namespace Ryujinx.Ui FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1), FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB", Path = applicationPath, + SaveDataPath = saveDataPath }; numApplicationsLoaded++; diff --git a/Ryujinx/Ui/GameTableContextMenu.cs b/Ryujinx/Ui/GameTableContextMenu.cs index f8d1d6815..e74d18285 100644 --- a/Ryujinx/Ui/GameTableContextMenu.cs +++ b/Ryujinx/Ui/GameTableContextMenu.cs @@ -1,7 +1,12 @@ using Gtk; +using LibHac; +using LibHac.Fs; +using LibHac.Fs.Shim; +using LibHac.Ncm; using Ryujinx.HLE.FileSystem; using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Reflection; @@ -13,6 +18,7 @@ namespace Ryujinx.Ui { private static ListStore _gameTableStore; private static TreeIter _rowIter; + private FileSystemClient _fsClient; #pragma warning disable CS0649 #pragma warning disable IDE0044 @@ -20,9 +26,10 @@ namespace Ryujinx.Ui #pragma warning restore CS0649 #pragma warning restore IDE0044 - public GameTableContextMenu(ListStore gameTableStore, TreeIter rowIter) : this(new Builder("Ryujinx.Ui.GameTableContextMenu.glade"), gameTableStore, rowIter) { } + public GameTableContextMenu(ListStore gameTableStore, TreeIter rowIter, FileSystemClient fsClient) + : this(new Builder("Ryujinx.Ui.GameTableContextMenu.glade"), gameTableStore, rowIter, fsClient) { } - private GameTableContextMenu(Builder builder, ListStore gameTableStore, TreeIter rowIter) : base(builder.GetObject("_contextMenu").Handle) + private GameTableContextMenu(Builder builder, ListStore gameTableStore, TreeIter rowIter, FileSystemClient fsClient) : base(builder.GetObject("_contextMenu").Handle) { builder.Autoconnect(this); @@ -30,6 +37,7 @@ namespace Ryujinx.Ui _gameTableStore = gameTableStore; _rowIter = rowIter; + _fsClient = fsClient; } //Events @@ -37,33 +45,14 @@ namespace Ryujinx.Ui { string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - string saveDir = System.IO.Path.Combine(new VirtualFileSystem().GetNandPath(), "user", "save", "0000000000000000", "00000000000000000000000000000001", titleId, "0"); - if (!Directory.Exists(saveDir)) + if (!TryFindSaveData(titleName, titleId, out ulong saveDataId)) { - MessageDialog messageDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, null) - { - Title = "Ryujinx", - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), - Text = $"Could not find save directory for {titleName} [{titleId}]", - SecondaryText = "Would you like to create the directory?", - WindowPosition = WindowPosition.Center - }; - - if (messageDialog.Run() == (int)ResponseType.Yes) - { - Directory.CreateDirectory(saveDir); - } - else - { - messageDialog.Dispose(); - - return; - } - - messageDialog.Dispose(); + return; } + string saveDir = GetSaveDataDirectory(saveDataId); + Process.Start(new ProcessStartInfo() { FileName = saveDir, @@ -71,5 +60,93 @@ namespace Ryujinx.Ui Verb = "open" }); } + + private bool TryFindSaveData(string titleName, string titleIdText, out ulong saveDataId) + { + saveDataId = default; + + if (!ulong.TryParse(titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleId)) + { + GtkDialog.CreateErrorDialog("UI error: The selected game did not have a valid title ID"); + + return false; + } + + SaveDataFilter filter = new SaveDataFilter(); + filter.SetUserId(new UserId(1, 0)); + filter.SetTitleId(new TitleId(titleId)); + + Result result = _fsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter); + + if (result == ResultFs.TargetNotFound) + { + // Savedata was not found. Ask the user if they want to create it + using MessageDialog messageDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, null) + { + Title = "Ryujinx", + Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), + Text = $"There is no savedata for {titleName} [{titleId:x16}]", + SecondaryText = "Would you like to create savedata for this game?", + WindowPosition = WindowPosition.Center + }; + + if (messageDialog.Run() != (int)ResponseType.Yes) + { + return false; + } + + result = _fsClient.CreateSaveData(new TitleId(titleId), new UserId(1, 0), new TitleId(titleId), 0, 0, 0); + + if (result.IsFailure()) + { + GtkDialog.CreateErrorDialog($"There was an error creating the specified savedata: {result.ToStringWithName()}"); + + return false; + } + + // Try to find the savedata again after creating it + result = _fsClient.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, ref filter); + } + + if (result.IsSuccess()) + { + saveDataId = saveDataInfo.SaveDataId; + + return true; + } + + GtkDialog.CreateErrorDialog($"There was an error finding the specified savedata: {result.ToStringWithName()}"); + + return false; + } + + private string GetSaveDataDirectory(ulong saveDataId) + { + string saveRootPath = System.IO.Path.Combine(new VirtualFileSystem().GetNandPath(), $"user/save/{saveDataId:x16}"); + + if (!Directory.Exists(saveRootPath)) + { + // Inconsistent state. Create the directory + Directory.CreateDirectory(saveRootPath); + } + + string committedPath = System.IO.Path.Combine(saveRootPath, "0"); + string workingPath = System.IO.Path.Combine(saveRootPath, "1"); + + // If the committed directory exists, that path will be loaded the next time the savedata is mounted + if (Directory.Exists(committedPath)) + { + return committedPath; + } + + // If the working directory exists and the committed directory doesn't, + // the working directory will be loaded the next time the savedata is mounted + if (!Directory.Exists(workingPath)) + { + Directory.CreateDirectory(workingPath); + } + + return workingPath; + } } } diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index dc3315e97..e65e56ffd 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -1,22 +1,21 @@ using Gtk; +using JsonPrettyPrinterPlus; using Ryujinx.Audio; using Ryujinx.Common.Logging; +using Ryujinx.Configuration; using Ryujinx.Graphics.Gal; using Ryujinx.Graphics.Gal.OpenGL; +using Ryujinx.HLE.FileSystem; using Ryujinx.Profiler; using System; +using System.Diagnostics; using System.IO; using System.Reflection; using System.Text; using System.Threading; -using Ryujinx.Configuration; -using System.Diagnostics; using System.Threading.Tasks; using Utf8Json; -using JsonPrettyPrinterPlus; using Utf8Json.Resolvers; -using Ryujinx.HLE.FileSystem; - using GUI = Gtk.Builder.ObjectAttribute; @@ -74,6 +73,12 @@ namespace Ryujinx.Ui _gameTable.ButtonReleaseEvent += Row_Clicked; + bool continueWithStartup = Migration.PromptIfMigrationNeededForStartup(this, out bool migrationNeeded); + if (!continueWithStartup) + { + End(); + } + _renderer = new OglRenderer(); _audioOut = InitializeAudioEngine(); @@ -81,6 +86,16 @@ namespace Ryujinx.Ui // TODO: Initialization and dispose of HLE.Switch when starting/stoping emulation. _device = InitializeSwitchInstance(); + if (migrationNeeded) + { + bool migrationSuccessful = Migration.DoMigrationForStartup(this, _device); + + if (!migrationSuccessful) + { + End(); + } + } + _treeView = _gameTable; ApplyTheme(); @@ -198,7 +213,9 @@ namespace Ryujinx.Ui _tableStore.Clear(); - await Task.Run(() => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage)); + await Task.Run(() => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, + _device.System.KeySet, _device.System.State.DesiredTitleLanguage, _device.System.FsClient, + _device.FileSystem)); _updatingGameTable = false; } @@ -377,8 +394,8 @@ namespace Ryujinx.Ui } Profile.FinishProfiling(); - _device.Dispose(); - _audioOut.Dispose(); + _device?.Dispose(); + _audioOut?.Dispose(); Logger.Shutdown(); Environment.Exit(0); } @@ -474,7 +491,7 @@ namespace Ryujinx.Ui if (treeIter.UserData == IntPtr.Zero) return; - GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter); + GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter, _device.System.FsClient); contextMenu.ShowAll(); contextMenu.PopupAtPointer(null); } diff --git a/Ryujinx/Ui/Migration.cs b/Ryujinx/Ui/Migration.cs new file mode 100644 index 000000000..c508878d6 --- /dev/null +++ b/Ryujinx/Ui/Migration.cs @@ -0,0 +1,184 @@ +using Gtk; +using LibHac; +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +using Switch = Ryujinx.HLE.Switch; + +namespace Ryujinx.Ui +{ + internal class Migration + { + private Switch Device { get; } + + public Migration(Switch device) + { + Device = device; + } + + public static bool PromptIfMigrationNeededForStartup(Window parentWindow, out bool isMigrationNeeded) + { + if (!IsMigrationNeeded()) + { + isMigrationNeeded = false; + + return true; + } + + isMigrationNeeded = true; + + int dialogResponse; + + using (MessageDialog dialog = new MessageDialog(parentWindow, DialogFlags.Modal, MessageType.Question, + ButtonsType.YesNo, "What's this?")) + { + dialog.Title = "Data Migration Needed"; + dialog.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); + dialog.Text = + "The folder structure of Ryujinx's RyuFs folder has been updated and renamed to \"Ryujinx\". " + + "Your RyuFs folder must be copied and migrated to the new \"Ryujinx\" structure. Would you like to do the migration now?\n\n" + + "Select \"Yes\" to automatically perform the migration. Your old RyuFs folder will remain as it is.\n\n" + + "Selecting \"No\" will exit Ryujinx without changing anything."; + + dialogResponse = dialog.Run(); + } + + return dialogResponse == (int)ResponseType.Yes; + } + + public static bool DoMigrationForStartup(Window parentWindow, Switch device) + { + try + { + Migration migration = new Migration(device); + int saveCount = migration.Migrate(); + + using MessageDialog dialogSuccess = new MessageDialog(parentWindow, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, null) + { + Title = "Migration Success", + Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), + Text = $"Data migration was successful. {saveCount} saves were migrated.", + }; + + dialogSuccess.Run(); + + return true; + } + catch (HorizonResultException ex) + { + GtkDialog.CreateErrorDialog(ex.Message); + + return false; + } + } + + // Returns the number of saves migrated + public int Migrate() + { + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + + string oldBasePath = Path.Combine(appDataPath, "RyuFs"); + string newBasePath = Path.Combine(appDataPath, "Ryujinx"); + + string oldSaveDir = Path.Combine(oldBasePath, "nand/user/save"); + + CopyRyuFs(oldBasePath, newBasePath); + + SaveImporter importer = new SaveImporter(oldSaveDir, Device.System.FsClient); + + return importer.Import(); + } + + private static void CopyRyuFs(string oldPath, string newPath) + { + Directory.CreateDirectory(newPath); + + CopyExcept(oldPath, newPath, "nand", "bis", "sdmc", "sdcard"); + + string oldNandPath = Path.Combine(oldPath, "nand"); + string newNandPath = Path.Combine(newPath, "bis"); + + CopyExcept(oldNandPath, newNandPath, "system", "user"); + + string oldSdPath = Path.Combine(oldPath, "sdmc"); + string newSdPath = Path.Combine(newPath, "sdcard"); + + CopyDirectory(oldSdPath, newSdPath); + + string oldSystemPath = Path.Combine(oldNandPath, "system"); + string newSystemPath = Path.Combine(newNandPath, "system"); + + CopyExcept(oldSystemPath, newSystemPath, "save"); + + string oldUserPath = Path.Combine(oldNandPath, "user"); + string newUserPath = Path.Combine(newNandPath, "user"); + + CopyExcept(oldUserPath, newUserPath, "save"); + } + + private static void CopyExcept(string srcPath, string dstPath, params string[] exclude) + { + exclude = exclude.Select(x => x.ToLowerInvariant()).ToArray(); + + DirectoryInfo srcDir = new DirectoryInfo(srcPath); + + if (!srcDir.Exists) + { + return; + } + + Directory.CreateDirectory(dstPath); + + foreach (DirectoryInfo subDir in srcDir.EnumerateDirectories()) + { + if (exclude.Contains(subDir.Name.ToLowerInvariant())) + { + continue; + } + + CopyDirectory(subDir.FullName, Path.Combine(dstPath, subDir.Name)); + } + + foreach (FileInfo file in srcDir.EnumerateFiles()) + { + file.CopyTo(Path.Combine(dstPath, file.Name)); + } + } + + private static void CopyDirectory(string srcPath, string dstPath) + { + Directory.CreateDirectory(dstPath); + + DirectoryInfo srcDir = new DirectoryInfo(srcPath); + + if (!srcDir.Exists) + { + return; + } + + Directory.CreateDirectory(dstPath); + + foreach (DirectoryInfo subDir in srcDir.EnumerateDirectories()) + { + CopyDirectory(subDir.FullName, Path.Combine(dstPath, subDir.Name)); + } + + foreach (FileInfo file in srcDir.EnumerateFiles()) + { + file.CopyTo(Path.Combine(dstPath, file.Name)); + } + } + + private static bool IsMigrationNeeded() + { + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + + string oldBasePath = Path.Combine(appDataPath, "RyuFs"); + string newBasePath = Path.Combine(appDataPath, "Ryujinx"); + + return Directory.Exists(oldBasePath) && !Directory.Exists(newBasePath); + } + } +} diff --git a/Ryujinx/Ui/SaveImporter.cs b/Ryujinx/Ui/SaveImporter.cs new file mode 100644 index 000000000..b0a5f6433 --- /dev/null +++ b/Ryujinx/Ui/SaveImporter.cs @@ -0,0 +1,218 @@ +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using LibHac.FsSystem; +using LibHac.FsSystem.Save; +using LibHac.Ncm; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Ui +{ + internal class SaveImporter + { + private FileSystemClient FsClient { get; } + private string ImportPath { get; } + + public SaveImporter(string importPath, FileSystemClient destFsClient) + { + ImportPath = importPath; + FsClient = destFsClient; + } + + // Returns the number of saves imported + public int Import() + { + return ImportSaves(FsClient, ImportPath); + } + + private static int ImportSaves(FileSystemClient fsClient, string rootSaveDir) + { + if (!Directory.Exists(rootSaveDir)) + { + return 0; + } + + SaveFinder finder = new SaveFinder(); + finder.FindSaves(rootSaveDir); + + foreach (SaveToImport save in finder.Saves) + { + Result importResult = ImportSave(fsClient, save); + + if (importResult.IsFailure()) + { + throw new HorizonResultException(importResult, $"Error importing save {save.Path}"); + } + } + + return finder.Saves.Count; + } + + private static Result ImportSave(FileSystemClient fs, SaveToImport save) + { + SaveDataAttribute key = save.Attribute; + + Result result = fs.CreateSaveData(key.TitleId, key.UserId, key.TitleId, 0, 0, 0); + if (result.IsFailure()) return result; + + bool isOldMounted = false; + bool isNewMounted = false; + + try + { + result = fs.Register("OldSave".ToU8Span(), new LocalFileSystem(save.Path)); + if (result.IsFailure()) return result; + + isOldMounted = true; + + result = fs.MountSaveData("NewSave".ToU8Span(), key.TitleId, key.UserId); + if (result.IsFailure()) return result; + + isNewMounted = true; + + result = fs.CopyDirectory("OldSave:/", "NewSave:/"); + if (result.IsFailure()) return result; + + result = fs.Commit("NewSave"); + } + finally + { + if (isOldMounted) + { + fs.Unmount("OldSave"); + } + + if (isNewMounted) + { + fs.Unmount("NewSave"); + } + } + + return result; + } + + private class SaveFinder + { + public List Saves { get; } = new List(); + + public void FindSaves(string rootPath) + { + foreach (string subDir in Directory.EnumerateDirectories(rootPath)) + { + if (TryGetUInt64(subDir, out ulong saveDataId)) + { + SearchSaveId(subDir, saveDataId); + } + } + } + + private void SearchSaveId(string path, ulong saveDataId) + { + foreach (string subDir in Directory.EnumerateDirectories(path)) + { + if (TryGetUserId(subDir, out UserId userId)) + { + SearchUser(subDir, saveDataId, userId); + } + } + } + + private void SearchUser(string path, ulong saveDataId, UserId userId) + { + foreach (string subDir in Directory.EnumerateDirectories(path)) + { + if (TryGetUInt64(subDir, out ulong titleId) && TryGetDataPath(subDir, out string dataPath)) + { + SaveDataAttribute attribute = new SaveDataAttribute + { + Type = SaveDataType.SaveData, + UserId = userId, + TitleId = new TitleId(titleId) + }; + + SaveToImport save = new SaveToImport(dataPath, attribute); + + Saves.Add(save); + } + } + } + + private static bool TryGetDataPath(string path, out string dataPath) + { + string committedPath = Path.Combine(path, "0"); + string workingPath = Path.Combine(path, "1"); + + if (Directory.Exists(committedPath) && Directory.EnumerateFileSystemEntries(committedPath).Any()) + { + dataPath = committedPath; + return true; + } + + if (Directory.Exists(workingPath) && Directory.EnumerateFileSystemEntries(workingPath).Any()) + { + dataPath = workingPath; + return true; + } + + dataPath = default; + return false; + } + + private static bool TryGetUInt64(string path, out ulong converted) + { + string name = Path.GetFileName(path); + + if (name.Length == 16) + { + try + { + converted = Convert.ToUInt64(name, 16); + return true; + } + catch { } + } + + converted = default; + return false; + } + + private static bool TryGetUserId(string path, out UserId userId) + { + string name = Path.GetFileName(path); + + if (name.Length == 32) + { + try + { + UInt128 id = new UInt128(name); + + userId = Unsafe.As(ref id); + return true; + } + catch { } + } + + userId = default; + return false; + } + } + + private class SaveToImport + { + public string Path { get; } + public SaveDataAttribute Attribute { get; } + + public SaveToImport(string path, SaveDataAttribute attribute) + { + Path = path; + Attribute = attribute; + } + } + } +} diff --git a/Ryujinx/Ui/SwitchSettings.cs b/Ryujinx/Ui/SwitchSettings.cs index 5c56cf7ea..8bd164d81 100644 --- a/Ryujinx/Ui/SwitchSettings.cs +++ b/Ryujinx/Ui/SwitchSettings.cs @@ -1,12 +1,12 @@ using Gtk; +using Ryujinx.Configuration; +using Ryujinx.Configuration.Hid; +using Ryujinx.Configuration.System; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using Ryujinx.Configuration; -using Ryujinx.Configuration.System; -using Ryujinx.Configuration.Hid; using GUI = Gtk.Builder.ObjectAttribute;