diff --git a/Ryujinx/Ui/ApplicationData.cs b/Ryujinx/Ui/ApplicationData.cs index defc5e983..49d7dfaad 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 ControlHolder { get; set; } } } diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs index 3ff7a54c3..02deb7cae 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 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 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 controlHolder = new BlitStruct(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 8c0bd0bc6..5b3b9df42 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 _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 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 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 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(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 13bade4e5..96b493399 100644 --- a/Ryujinx/Ui/GameTableContextMenu.glade +++ b/Ryujinx/Ui/GameTableContextMenu.glade @@ -6,11 +6,20 @@ True False - + True False - Open the folder where saves for the application is loaded - Open Save Directory + Open the folder where the User save for the application is loaded + Open User Save Directory + True + + + + + True + False + Open the folder where the Device save for the application is loaded + Open Device Save Directory True diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index f658bea46..05c9686db 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)); _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 controlData = (BlitStruct)_tableStore.GetValue(treeIter, 10); + + GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, controlData, treeIter, _virtualFileSystem); contextMenu.ShowAll(); contextMenu.PopupAtPointer(null); }