forked from Mirror/Ryujinx
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:
parent
d5670aff77
commit
5423daea56
5 changed files with 121 additions and 33 deletions
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++;
|
||||||
|
|
|
@ -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))
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue