Bladeren bron

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 <thog@protonmail.com>
Alex Barney 6 jaren geleden
bovenliggende
commit
63b24b4af2

+ 2 - 2
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

+ 2 - 2
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**
 

+ 3 - 3
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");

+ 40 - 34
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<ApplicationControlProperty> 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<ApplicationControlProperty>(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")

+ 1 - 1
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);
 

+ 33 - 7
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<Uid>();
+            TitleId titleId = new TitleId(context.Process.TitleId);
 
-            context.ResponseData.Write(0L);
+            BlitStruct<ApplicationControlProperty> 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<ApplicationControlProperty>(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)]

+ 10 - 42
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<UInt128>();
-            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);
+        }
     }
 }

+ 184 - 38
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<bytes<0x301>, 0x19, 0x301>) -> object<nn::fssrv::sf::IFileSystem> 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<nn::fssrv::sf::IFileSystem>
         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<SaveDataAttribute>();
+            SaveDataCreateInfo createInfo     = context.RequestData.ReadStruct<SaveDataCreateInfo>();
+            SaveMetaCreateInfo metaCreateInfo = context.RequestData.ReadStruct<SaveMetaCreateInfo>();
+
+            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<SaveDataAttribute>();
+            SaveDataCreateInfo createInfo = context.RequestData.ReadStruct<SaveDataCreateInfo>();
+
+            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<SaveDataAttribute>();
+
+            Result result = _baseFileSystemProxy.DeleteSaveDataFileSystemBySaveDataAttribute(spaceId, ref attribute);
+
+            return (ResultCode)result.Value;
+        }
+
         [Command(30)]
         // OpenGameCardStorage(u32, u32) -> object<nn::fssrv::sf::IStorage>
         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<SaveDataAttribute>();
+            SaveDataCreateInfo createInfo     = context.RequestData.ReadStruct<SaveDataCreateInfo>();
+            SaveMetaCreateInfo metaCreateInfo = context.RequestData.ReadStruct<SaveMetaCreateInfo>();
+            HashSalt           hashSalt       = context.RequestData.ReadStruct<HashSalt>();
+
+            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<nn::fssrv::sf::IFileSystem> 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<SaveDataAttribute>();
+
+            if (attribute.TitleId == TitleId.Zero)
+            {
+                attribute.TitleId = new TitleId(context.Process.TitleId);
+            }
 
-            if (result == ResultCode.Success)
+            Result result = _baseFileSystemProxy.OpenSaveDataFileSystem(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(52)]
         // OpenSaveDataFileSystemBySystemSaveDataId(u8 save_data_space_id, nn::fssrv::sf::SaveStruct saveStruct) -> object<nn::fssrv::sf::IFileSystem> 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<SaveDataAttribute>();
+
+            Result result = _baseFileSystemProxy.OpenSaveDataFileSystemBySystemSaveDataId(out LibHac.Fs.IFileSystem fileSystem, spaceId, ref attribute);
 
-            if (result == ResultCode.Success)
+            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<nn::fssrv::sf::IFileSystem>
         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<SaveDataAttribute>();
+
+            if (attribute.TitleId == TitleId.Zero)
+            {
+                attribute.TitleId = new TitleId(context.Process.TitleId);
+            }
+
+            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 == ResultCode.Success)
+            if (result.IsSuccess())
             {
-                MakeObject(context, fileSystem);
+                MakeObject(context, new ISaveDataInfoReader(infoReader));
             }
 
-            return result;
+            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<SaveDataFilter>();
+
+            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<SaveDataFilter>();
+
+            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;
+        }
     }
 }

+ 31 - 0
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<unknown, 6>)
+        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;
+        }
+    }
+}

+ 7 - 195
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<unknown, 6>)
+        // GetApplicationControlData(u8, u64) -> (unknown<4>, buffer<unknown, 6>)
         public ResultCode GetApplicationControlData(ServiceCtx context)
         {
-            long position = context.Request.ReceiveBuff[0].Position;
-
-            Nacp nacp = context.Device.System.ControlData;
-
-            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;
+            byte  source  = (byte)context.RequestData.ReadInt64();
+            ulong titleId = (byte)context.RequestData.ReadUInt64();
 
-            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];
-                }
+            long position = context.Request.ReceiveBuff[0].Position;
 
-                context.Memory.WriteUInt64(position, value);
-                position += 8;
-            }
+            byte[] nacpData = context.Device.System.ControlData.ByteSpan.ToArray();
 
-            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;
         }

+ 2 - 2
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<ulong> titleIds = new List<ulong>();
 
@@ -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;
                     }

+ 2 - 2
Ryujinx.HLE/Ryujinx.HLE.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>netcoreapp3.0</TargetFramework>
@@ -51,7 +51,7 @@
 
   <ItemGroup>
     <PackageReference Include="Concentus" Version="1.1.7" />
-    <PackageReference Include="LibHac" Version="0.6.0" />
+    <PackageReference Include="LibHac" Version="0.7.0" />
     <PackageReference Include="TimeZoneConverter.Posix" Version="2.1.0" />
   </ItemGroup>
 

+ 1 - 1
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; }
 

+ 2 - 2
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();

+ 2 - 15
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<AboutInfo>(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)

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

+ 21 - 1
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<string> appDirs, Keyset keySet, TitleLanguage desiredTitleLanguage)
+        public static void LoadApplications(List<string> 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++;

+ 95 - 18
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,39 +45,108 @@ 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)
+                return;
+            }
+
+            string saveDir = GetSaveDataDirectory(saveDataId);
+
+            Process.Start(new ProcessStartInfo()
+            {
+                FileName        = saveDir,
+                UseShellExecute = true,
+                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           = $"Could not find save directory for {titleName} [{titleId}]",
-                    SecondaryText  = "Would you like to create the directory?",
+                    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)
+                if (messageDialog.Run() != (int)ResponseType.Yes)
                 {
-                    Directory.CreateDirectory(saveDir);
+                    return false;
                 }
-                else
+
+                result = _fsClient.CreateSaveData(new TitleId(titleId), new UserId(1, 0), new TitleId(titleId), 0, 0, 0);
+
+                if (result.IsFailure())
                 {
-                    messageDialog.Dispose();
+                    GtkDialog.CreateErrorDialog($"There was an error creating the specified savedata: {result.ToStringWithName()}");
 
-                    return;
+                    return false;
                 }
 
-                messageDialog.Dispose();
+                // Try to find the savedata again after creating it
+                result = _fsClient.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, ref filter);
             }
 
-            Process.Start(new ProcessStartInfo()
+            if (result.IsSuccess())
             {
-                FileName        = saveDir,
-                UseShellExecute = true,
-                Verb            = "open"
-            });
+                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;
         }
     }
 }

+ 26 - 9
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);
         }

+ 184 - 0
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);
+        }
+    }
+}

+ 218 - 0
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<SaveToImport> Saves { get; } = new List<SaveToImport>();
+
+            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<UInt128, UserId>(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;
+            }
+        }
+    }
+}

+ 3 - 3
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;