From 5423daea56345237cd10e02870139143d5922aa2 Mon Sep 17 00:00:00 2001
From: Thog <me@thog.eu>
Date: Wed, 25 Mar 2020 18:09:38 +0100
Subject: [PATCH] ui: Make it possible to open the device save directory
 (#1040)

* Add an open device folder option

* Simplify logic from previous commit

* Address Xpl0itR's comments

* Address Ac_K comment
---
 Ryujinx/Ui/ApplicationData.cs         |   7 +-
 Ryujinx/Ui/ApplicationLibrary.cs      |  13 +++-
 Ryujinx/Ui/GameTableContextMenu.cs    | 106 ++++++++++++++++++++------
 Ryujinx/Ui/GameTableContextMenu.glade |  15 +++-
 Ryujinx/Ui/MainWindow.cs              |  13 +++-
 5 files changed, 121 insertions(+), 33 deletions(-)

diff --git a/Ryujinx/Ui/ApplicationData.cs b/Ryujinx/Ui/ApplicationData.cs
index defc5e9837..49d7dfaad2 100644
--- a/Ryujinx/Ui/ApplicationData.cs
+++ b/Ryujinx/Ui/ApplicationData.cs
@@ -1,4 +1,8 @@
-namespace Ryujinx.Ui
+using LibHac;
+using LibHac.Common;
+using LibHac.Ns;
+
+namespace Ryujinx.Ui
 {
     public struct ApplicationData
     {
@@ -14,5 +18,6 @@
         public string FileSize      { get; set; }
         public string Path          { get; set; }
         public string SaveDataPath  { get; set; }
+        public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
     }
 }
diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs
index 3ff7a54c3a..02deb7cae0 100644
--- a/Ryujinx/Ui/ApplicationLibrary.cs
+++ b/Ryujinx/Ui/ApplicationLibrary.cs
@@ -6,6 +6,7 @@ using LibHac.Fs.Shim;
 using LibHac.FsSystem;
 using LibHac.FsSystem.NcaUtils;
 using LibHac.Ncm;
+using LibHac.Ns;
 using LibHac.Spl;
 using Ryujinx.Common.Logging;
 using Ryujinx.Configuration.System;
@@ -81,6 +82,12 @@ namespace Ryujinx.Ui
             }
         }
 
+        public static void ReadControlData(IFileSystem controlFs, Span<byte> outProperty)
+        {
+            controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+            controlFile.Read(out long _, 0, outProperty, ReadOption.None).ThrowIfFailure();
+        }
+
         public static void LoadApplications(List<string> appDirs, VirtualFileSystem virtualFileSystem, Language desiredTitleLanguage)
         {
             int numApplicationsFound  = 0;
@@ -127,6 +134,7 @@ namespace Ryujinx.Ui
                 string version         = "0";
                 string saveDataPath    = null;
                 byte[] applicationIcon = null;
+                BlitStruct<ApplicationControlProperty> controlHolder = new BlitStruct<ApplicationControlProperty>(1);
 
                 try
                 {
@@ -204,6 +212,8 @@ namespace Ryujinx.Ui
                                     // Store the ControlFS in variable called controlFs
                                     GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId);
 
+                                    ReadControlData(controlFs, controlHolder.ByteSpan);
+
                                     // Creates NACP class from the NACP file
                                     controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
 
@@ -413,7 +423,8 @@ 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
+                    SaveDataPath  = saveDataPath,
+                    ControlHolder = controlHolder
                 };
 
                 numApplicationsLoaded++;
diff --git a/Ryujinx/Ui/GameTableContextMenu.cs b/Ryujinx/Ui/GameTableContextMenu.cs
index 8c0bd0bc64..5b3b9df423 100644
--- a/Ryujinx/Ui/GameTableContextMenu.cs
+++ b/Ryujinx/Ui/GameTableContextMenu.cs
@@ -1,21 +1,25 @@
 using Gtk;
 using LibHac;
+using LibHac.Account;
 using LibHac.Common;
 using LibHac.Fs;
 using LibHac.Fs.Shim;
 using LibHac.FsSystem;
 using LibHac.FsSystem.NcaUtils;
 using LibHac.Ncm;
+using LibHac.Ns;
 using Ryujinx.Common.Logging;
 using Ryujinx.HLE.FileSystem;
 using System;
 using System.Buffers;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Reflection;
 using System.Threading;
 
+using static LibHac.Fs.ApplicationSaveDataManagement;
 using GUI = Gtk.Builder.ObjectAttribute;
 
 namespace Ryujinx.Ui
@@ -28,23 +32,31 @@ namespace Ryujinx.Ui
         private MessageDialog     _dialog;
         private bool              _cancel;
 
+        private BlitStruct<ApplicationControlProperty> _controlData;
+
 #pragma warning disable CS0649
 #pragma warning disable IDE0044
-        [GUI] MenuItem _openSaveDir;
+        [GUI] MenuItem _openSaveUserDir;
+        [GUI] MenuItem _openSaveDeviceDir;
         [GUI] MenuItem _extractRomFs;
         [GUI] MenuItem _extractExeFs;
         [GUI] MenuItem _extractLogo;
 #pragma warning restore CS0649
 #pragma warning restore IDE0044
 
-        public GameTableContextMenu(ListStore gameTableStore, TreeIter rowIter, VirtualFileSystem virtualFileSystem)
-            : this(new Builder("Ryujinx.Ui.GameTableContextMenu.glade"), gameTableStore, rowIter, virtualFileSystem) { }
+        public GameTableContextMenu(ListStore gameTableStore, BlitStruct<ApplicationControlProperty> controlData, TreeIter rowIter, VirtualFileSystem virtualFileSystem)
+            : this(new Builder("Ryujinx.Ui.GameTableContextMenu.glade"), gameTableStore, controlData, rowIter, virtualFileSystem) { }
 
-        private GameTableContextMenu(Builder builder, ListStore gameTableStore, TreeIter rowIter, VirtualFileSystem virtualFileSystem) : base(builder.GetObject("_contextMenu").Handle)
+        private GameTableContextMenu(Builder builder, ListStore gameTableStore, BlitStruct<ApplicationControlProperty> controlData, TreeIter rowIter, VirtualFileSystem virtualFileSystem) : base(builder.GetObject("_contextMenu").Handle)
         {
             builder.Autoconnect(this);
 
-            _openSaveDir.Activated  += OpenSaveDir_Clicked;
+            _openSaveUserDir.Activated   += OpenSaveUserDir_Clicked;
+            _openSaveDeviceDir.Activated += OpenSaveDeviceDir_Clicked;
+
+            _openSaveUserDir.Sensitive   = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
+            _openSaveDeviceDir.Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
+
             _extractRomFs.Activated += ExtractRomFs_Clicked;
             _extractExeFs.Activated += ExtractExeFs_Clicked;
             _extractLogo.Activated  += ExtractLogo_Clicked;
@@ -52,6 +64,7 @@ namespace Ryujinx.Ui
             _gameTableStore    = gameTableStore;
             _rowIter           = rowIter;
             _virtualFileSystem = virtualFileSystem;
+            _controlData       = controlData;
 
             string ext = System.IO.Path.GetExtension(_gameTableStore.GetValue(_rowIter, 9).ToString()).ToLower();
             if (ext != ".nca" && ext != ".nsp" && ext != ".pfs0" && ext != ".xci")
@@ -62,21 +75,10 @@ namespace Ryujinx.Ui
             }
         }
 
-        private bool TryFindSaveData(string titleName, string titleIdText, out ulong saveDataId)
+        private bool TryFindSaveData(string titleName, ulong titleId, BlitStruct<ApplicationControlProperty> controlHolder, SaveDataFilter filter, 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.SetProgramId(new TitleId(titleId));
-
             Result result = _virtualFileSystem.FsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter);
 
             if (ResultFs.TargetNotFound.Includes(result))
@@ -84,10 +86,10 @@ namespace Ryujinx.Ui
                 // 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?",
+                    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
                 };
 
@@ -96,7 +98,25 @@ namespace Ryujinx.Ui
                     return false;
                 }
 
-                result = _virtualFileSystem.FsClient.CreateSaveData(new TitleId(titleId), new UserId(1, 0), new TitleId(titleId), 0, 0, 0);
+                ref ApplicationControlProperty control = ref controlHolder.Value;
+
+                if (LibHac.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.Application,
+                        "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
+                }
+
+                Uid user = new Uid(1, 0);
+
+                result = EnsureApplicationSaveData(_virtualFileSystem.FsClient, out _, new TitleId(titleId), ref control, ref user);
 
                 if (result.IsFailure())
                 {
@@ -392,12 +412,29 @@ namespace Ryujinx.Ui
         }
 
         // Events
-        private void OpenSaveDir_Clicked(object sender, EventArgs args)
+        private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
         {
             string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0];
-            string titleId   = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower();
+            string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower();
 
-            if (!TryFindSaveData(titleName, titleId, out ulong saveDataId))
+            if (!ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
+            {
+                GtkDialog.CreateErrorDialog("UI error: The selected game did not have a valid title ID");
+
+                return;
+            }
+
+            SaveDataFilter filter = new SaveDataFilter();
+            filter.SetUserId(new UserId(1, 0));
+
+            OpenSaveDir(titleName, titleIdNumber, filter);
+        }
+
+        private void OpenSaveDir(string titleName, ulong titleId, SaveDataFilter filter)
+        {
+            filter.SetProgramId(new TitleId(titleId));
+
+            if (!TryFindSaveData(titleName, titleId, _controlData, filter, out ulong saveDataId))
             {
                 return;
             }
@@ -412,6 +449,25 @@ namespace Ryujinx.Ui
             });
         }
 
+        // Events
+        private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
+        {
+            string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0];
+            string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower();
+
+            if (!ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
+            {
+                GtkDialog.CreateErrorDialog("UI error: The selected game did not have a valid title ID");
+
+                return;
+            }
+
+            SaveDataFilter filter = new SaveDataFilter();
+            filter.SetSaveDataType(SaveDataType.Device);
+
+            OpenSaveDir(titleName, titleIdNumber, filter);
+        }
+
         private void ExtractRomFs_Clicked(object sender, EventArgs args)
         {
             ExtractSection(NcaSectionType.Data);
diff --git a/Ryujinx/Ui/GameTableContextMenu.glade b/Ryujinx/Ui/GameTableContextMenu.glade
index 13bade4e50..96b493399c 100644
--- a/Ryujinx/Ui/GameTableContextMenu.glade
+++ b/Ryujinx/Ui/GameTableContextMenu.glade
@@ -6,11 +6,20 @@
     <property name="visible">True</property>
     <property name="can_focus">False</property>
     <child>
-      <object class="GtkMenuItem" id="_openSaveDir">
+      <object class="GtkMenuItem" id="_openSaveUserDir">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="tooltip_text" translatable="yes">Open the folder where saves for the application is loaded</property>
-        <property name="label" translatable="yes">Open Save Directory</property>
+        <property name="tooltip_text" translatable="yes">Open the folder where the User save for the application is loaded</property>
+        <property name="label" translatable="yes">Open User Save Directory</property>
+        <property name="use_underline">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkMenuItem" id="_openSaveDeviceDir">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="tooltip_text" translatable="yes">Open the folder where the Device save for the application is loaded</property>
+        <property name="label" translatable="yes">Open Device Save Directory</property>
         <property name="use_underline">True</property>
       </object>
     </child>
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index f658bea467..05c9686db0 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -1,5 +1,7 @@
 using Gtk;
 using JsonPrettyPrinterPlus;
+using LibHac.Common;
+using LibHac.Ns;
 using Ryujinx.Audio;
 using Ryujinx.Common.Logging;
 using Ryujinx.Configuration;
@@ -9,6 +11,7 @@ using Ryujinx.Graphics.OpenGL;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.FileSystem.Content;
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Reflection;
@@ -156,7 +159,8 @@ namespace Ryujinx.Ui
                 typeof(string),
                 typeof(string),
                 typeof(string),
-                typeof(string));
+                typeof(string),
+                typeof(BlitStruct<ApplicationControlProperty>));
 
             _tableStore.SetSortFunc(5, TimePlayedSort);
             _tableStore.SetSortFunc(6, LastPlayedSort);
@@ -580,7 +584,8 @@ namespace Ryujinx.Ui
                     args.AppData.LastPlayed,
                     args.AppData.FileExtension,
                     args.AppData.FileSize,
-                    args.AppData.Path);
+                    args.AppData.Path,
+                    args.AppData.ControlHolder);
             });
         }
 
@@ -653,7 +658,9 @@ namespace Ryujinx.Ui
 
             if (treeIter.UserData == IntPtr.Zero) return;
 
-            GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter, _virtualFileSystem);
+            BlitStruct<ApplicationControlProperty> controlData = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10);
+
+            GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, controlData, treeIter, _virtualFileSystem);
             contextMenu.ShowAll();
             contextMenu.PopupAtPointer(null);
         }