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
This commit is contained in:
Thog 2020-03-25 18:09:38 +01:00 committed by GitHub
parent d5670aff77
commit 5423daea56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 33 deletions

View file

@ -1,4 +1,8 @@
namespace Ryujinx.Ui using LibHac;
using LibHac.Common;
using LibHac.Ns;
namespace Ryujinx.Ui
{ {
public struct ApplicationData public struct ApplicationData
{ {
@ -14,5 +18,6 @@
public string FileSize { get; set; } public string FileSize { get; set; }
public string Path { get; set; } public string Path { get; set; }
public string SaveDataPath { get; set; } public string SaveDataPath { get; set; }
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
} }
} }

View file

@ -6,6 +6,7 @@ using LibHac.Fs.Shim;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.FsSystem.NcaUtils; using LibHac.FsSystem.NcaUtils;
using LibHac.Ncm; using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Spl; using LibHac.Spl;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Configuration.System; 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) public static void LoadApplications(List<string> appDirs, VirtualFileSystem virtualFileSystem, Language desiredTitleLanguage)
{ {
int numApplicationsFound = 0; int numApplicationsFound = 0;
@ -127,6 +134,7 @@ namespace Ryujinx.Ui
string version = "0"; string version = "0";
string saveDataPath = null; string saveDataPath = null;
byte[] applicationIcon = null; byte[] applicationIcon = null;
BlitStruct<ApplicationControlProperty> controlHolder = new BlitStruct<ApplicationControlProperty>(1);
try try
{ {
@ -204,6 +212,8 @@ namespace Ryujinx.Ui
// Store the ControlFS in variable called controlFs // Store the ControlFS in variable called controlFs
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId);
ReadControlData(controlFs, controlHolder.ByteSpan);
// Creates NACP class from the NACP file // Creates NACP class from the NACP file
controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); 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), FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB", FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB",
Path = applicationPath, Path = applicationPath,
SaveDataPath = saveDataPath SaveDataPath = saveDataPath,
ControlHolder = controlHolder
}; };
numApplicationsLoaded++; numApplicationsLoaded++;

View file

@ -1,21 +1,25 @@
using Gtk; using Gtk;
using LibHac; using LibHac;
using LibHac.Account;
using LibHac.Common; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Shim; using LibHac.Fs.Shim;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.FsSystem.NcaUtils; using LibHac.FsSystem.NcaUtils;
using LibHac.Ncm; using LibHac.Ncm;
using LibHac.Ns;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using static LibHac.Fs.ApplicationSaveDataManagement;
using GUI = Gtk.Builder.ObjectAttribute; using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.Ui namespace Ryujinx.Ui
@ -28,23 +32,31 @@ namespace Ryujinx.Ui
private MessageDialog _dialog; private MessageDialog _dialog;
private bool _cancel; private bool _cancel;
private BlitStruct<ApplicationControlProperty> _controlData;
#pragma warning disable CS0649 #pragma warning disable CS0649
#pragma warning disable IDE0044 #pragma warning disable IDE0044
[GUI] MenuItem _openSaveDir; [GUI] MenuItem _openSaveUserDir;
[GUI] MenuItem _openSaveDeviceDir;
[GUI] MenuItem _extractRomFs; [GUI] MenuItem _extractRomFs;
[GUI] MenuItem _extractExeFs; [GUI] MenuItem _extractExeFs;
[GUI] MenuItem _extractLogo; [GUI] MenuItem _extractLogo;
#pragma warning restore CS0649 #pragma warning restore CS0649
#pragma warning restore IDE0044 #pragma warning restore IDE0044
public GameTableContextMenu(ListStore gameTableStore, TreeIter rowIter, VirtualFileSystem virtualFileSystem) public GameTableContextMenu(ListStore gameTableStore, BlitStruct<ApplicationControlProperty> controlData, TreeIter rowIter, VirtualFileSystem virtualFileSystem)
: this(new Builder("Ryujinx.Ui.GameTableContextMenu.glade"), gameTableStore, rowIter, 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); 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; _extractRomFs.Activated += ExtractRomFs_Clicked;
_extractExeFs.Activated += ExtractExeFs_Clicked; _extractExeFs.Activated += ExtractExeFs_Clicked;
_extractLogo.Activated += ExtractLogo_Clicked; _extractLogo.Activated += ExtractLogo_Clicked;
@ -52,6 +64,7 @@ namespace Ryujinx.Ui
_gameTableStore = gameTableStore; _gameTableStore = gameTableStore;
_rowIter = rowIter; _rowIter = rowIter;
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
_controlData = controlData;
string ext = System.IO.Path.GetExtension(_gameTableStore.GetValue(_rowIter, 9).ToString()).ToLower(); string ext = System.IO.Path.GetExtension(_gameTableStore.GetValue(_rowIter, 9).ToString()).ToLower();
if (ext != ".nca" && ext != ".nsp" && ext != ".pfs0" && ext != ".xci") 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; 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); Result result = _virtualFileSystem.FsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter);
if (ResultFs.TargetNotFound.Includes(result)) 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 // 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) using MessageDialog messageDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, null)
{ {
Title = "Ryujinx", Title = "Ryujinx",
Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"),
Text = $"There is no savedata for {titleName} [{titleId:x16}]", Text = $"There is no savedata for {titleName} [{titleId:x16}]",
SecondaryText = "Would you like to create savedata for this game?", SecondaryText = "Would you like to create savedata for this game?",
WindowPosition = WindowPosition.Center WindowPosition = WindowPosition.Center
}; };
@ -96,7 +98,25 @@ namespace Ryujinx.Ui
return false; 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()) if (result.IsFailure())
{ {
@ -392,12 +412,29 @@ namespace Ryujinx.Ui
} }
// Events // 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 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; 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) private void ExtractRomFs_Clicked(object sender, EventArgs args)
{ {
ExtractSection(NcaSectionType.Data); ExtractSection(NcaSectionType.Data);

View file

@ -6,11 +6,20 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child> <child>
<object class="GtkMenuItem" id="_openSaveDir"> <object class="GtkMenuItem" id="_openSaveUserDir">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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="tooltip_text" translatable="yes">Open the folder where the User save for the application is loaded</property>
<property name="label" translatable="yes">Open Save Directory</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> <property name="use_underline">True</property>
</object> </object>
</child> </child>

View file

@ -1,5 +1,7 @@
using Gtk; using Gtk;
using JsonPrettyPrinterPlus; using JsonPrettyPrinterPlus;
using LibHac.Common;
using LibHac.Ns;
using Ryujinx.Audio; using Ryujinx.Audio;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Configuration; using Ryujinx.Configuration;
@ -9,6 +11,7 @@ using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.FileSystem.Content;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
@ -156,7 +159,8 @@ namespace Ryujinx.Ui
typeof(string), typeof(string),
typeof(string), typeof(string),
typeof(string), typeof(string),
typeof(string)); typeof(string),
typeof(BlitStruct<ApplicationControlProperty>));
_tableStore.SetSortFunc(5, TimePlayedSort); _tableStore.SetSortFunc(5, TimePlayedSort);
_tableStore.SetSortFunc(6, LastPlayedSort); _tableStore.SetSortFunc(6, LastPlayedSort);
@ -580,7 +584,8 @@ namespace Ryujinx.Ui
args.AppData.LastPlayed, args.AppData.LastPlayed,
args.AppData.FileExtension, args.AppData.FileExtension,
args.AppData.FileSize, 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; 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.ShowAll();
contextMenu.PopupAtPointer(null); contextMenu.PopupAtPointer(null);
} }