aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Ryujinx.Gtk3/Program.cs31
-rw-r--r--src/Ryujinx.Gtk3/UI/MainWindow.cs107
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs108
-rw-r--r--src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs9
-rw-r--r--src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs128
-rw-r--r--src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs92
-rw-r--r--src/Ryujinx.HLE/FileSystem/ContentManager.cs42
-rw-r--r--src/Ryujinx.HLE/FileSystem/ContentMetaData.cs61
-rw-r--r--src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs1
-rw-r--r--src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs86
-rw-r--r--src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs133
-rw-r--r--src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs8
-rw-r--r--src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs9
-rw-r--r--src/Ryujinx.HLE/Switch.cs8
-rw-r--r--src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs45
-rw-r--r--src/Ryujinx.UI.Common/App/ApplicationData.cs18
-rw-r--r--src/Ryujinx.UI.Common/App/ApplicationLibrary.cs854
-rw-r--r--src/Ryujinx.UI.Common/Helper/CommandLineState.cs5
-rw-r--r--src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs26
-rw-r--r--src/Ryujinx/AppHost.cs7
-rw-r--r--src/Ryujinx/Assets/Locales/en_US.json3
-rw-r--r--src/Ryujinx/Common/ApplicationHelper.cs9
-rw-r--r--src/Ryujinx/Program.cs2
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs56
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationGridView.axaml2
-rw-r--r--src/Ryujinx/UI/Controls/ApplicationListView.axaml4
-rw-r--r--src/Ryujinx/UI/Models/DownloadableContentModel.cs4
-rw-r--r--src/Ryujinx/UI/Models/SaveModel.cs4
-rw-r--r--src/Ryujinx/UI/Models/TitleUpdateModel.cs5
-rw-r--r--src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs66
-rw-r--r--src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs50
-rw-r--r--src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs77
-rw-r--r--src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs10
-rw-r--r--src/Ryujinx/UI/Windows/CheatWindow.axaml.cs7
-rw-r--r--src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml2
-rw-r--r--src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs13
-rw-r--r--src/Ryujinx/UI/Windows/MainWindow.axaml.cs47
-rw-r--r--src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs15
38 files changed, 1246 insertions, 908 deletions
diff --git a/src/Ryujinx.Gtk3/Program.cs b/src/Ryujinx.Gtk3/Program.cs
index 749cb697..0fb71288 100644
--- a/src/Ryujinx.Gtk3/Program.cs
+++ b/src/Ryujinx.Gtk3/Program.cs
@@ -7,6 +7,7 @@ using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules;
using Ryujinx.SDL2.Common;
using Ryujinx.UI;
+using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
@@ -322,7 +323,35 @@ namespace Ryujinx
if (CommandLineState.LaunchPathArg != null)
{
- mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
+ if (mainWindow.ApplicationLibrary.TryGetApplicationsFromFile(CommandLineState.LaunchPathArg, out List<ApplicationData> applications))
+ {
+ ApplicationData applicationData;
+
+ if (CommandLineState.LaunchApplicationId != null)
+ {
+ applicationData = applications.Find(application => application.IdString == CommandLineState.LaunchApplicationId);
+
+ if (applicationData != null)
+ {
+ mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Couldn't find requested application id '{CommandLineState.LaunchApplicationId}' in '{CommandLineState.LaunchPathArg}'.");
+ UserErrorDialog.CreateUserErrorDialog(UserError.ApplicationNotFound);
+ }
+ }
+ else
+ {
+ applicationData = applications[0];
+ mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
+ }
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Couldn't find any application in '{CommandLineState.LaunchPathArg}'.");
+ UserErrorDialog.CreateUserErrorDialog(UserError.ApplicationNotFound);
+ }
}
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
diff --git a/src/Ryujinx.Gtk3/UI/MainWindow.cs b/src/Ryujinx.Gtk3/UI/MainWindow.cs
index d1ca6ce6..7f9eceb3 100644
--- a/src/Ryujinx.Gtk3/UI/MainWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/MainWindow.cs
@@ -37,7 +37,9 @@ using Ryujinx.UI.Windows;
using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading;
@@ -60,7 +62,6 @@ namespace Ryujinx.UI
private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
- private readonly ApplicationLibrary _applicationLibrary;
private readonly GtkHostUIHandler _uiHandler;
private readonly AutoResetEvent _deviceExitStatus;
private readonly ListStore _tableStore;
@@ -69,11 +70,12 @@ namespace Ryujinx.UI
private bool _gameLoaded;
private bool _ending;
- private string _currentEmulatedGamePath = null;
+ private ApplicationData _currentApplicationData = null;
private string _lastScannedAmiiboId = "";
private bool _lastScannedAmiiboShowAll = false;
+ public readonly ApplicationLibrary ApplicationLibrary;
public RendererWidgetBase RendererWidget;
public InputManager InputManager;
@@ -180,8 +182,12 @@ namespace Ryujinx.UI
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
_userChannelPersistence = new UserChannelPersistence();
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
// Instantiate GUI objects.
- _applicationLibrary = new ApplicationLibrary(_virtualFileSystem);
+ ApplicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel);
_uiHandler = new GtkHostUIHandler(this);
_deviceExitStatus = new AutoResetEvent(false);
@@ -190,8 +196,8 @@ namespace Ryujinx.UI
FocusInEvent += MainWindow_FocusInEvent;
FocusOutEvent += MainWindow_FocusOutEvent;
- _applicationLibrary.ApplicationAdded += Application_Added;
- _applicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
+ ApplicationLibrary.ApplicationAdded += Application_Added;
+ ApplicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
_fileMenu.StateChanged += FileMenu_StateChanged;
_actionMenu.StateChanged += ActionMenu_StateChanged;
@@ -732,7 +738,7 @@ namespace Ryujinx.UI
Thread applicationLibraryThread = new(() =>
{
- _applicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language);
+ ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language);
_updatingGameTable = false;
})
@@ -783,7 +789,7 @@ namespace Ryujinx.UI
}
}
- private bool LoadApplication(string path, bool isFirmwareTitle)
+ private bool LoadApplication(string path, ulong applicationId, bool isFirmwareTitle)
{
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
@@ -857,7 +863,7 @@ namespace Ryujinx.UI
case ".xci":
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
- return _emulationContext.LoadXci(path);
+ return _emulationContext.LoadXci(path, applicationId);
case ".nca":
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
@@ -866,7 +872,7 @@ namespace Ryujinx.UI
case ".pfs0":
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
- return _emulationContext.LoadNsp(path);
+ return _emulationContext.LoadNsp(path, applicationId);
default:
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
try
@@ -887,7 +893,7 @@ namespace Ryujinx.UI
return false;
}
- public void RunApplication(string path, bool startFullscreen = false)
+ public void RunApplication(ApplicationData application, bool startFullscreen = false)
{
if (_gameLoaded)
{
@@ -909,14 +915,14 @@ namespace Ryujinx.UI
bool isFirmwareTitle = false;
- if (path.StartsWith("@SystemContent"))
+ if (application.Path.StartsWith("@SystemContent"))
{
- path = VirtualFileSystem.SwitchPathToSystemPath(path);
+ application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path);
isFirmwareTitle = true;
}
- if (!LoadApplication(path, isFirmwareTitle))
+ if (!LoadApplication(application.Path, application.Id, isFirmwareTitle))
{
_emulationContext.Dispose();
SwitchToGameTable();
@@ -926,7 +932,7 @@ namespace Ryujinx.UI
SetupProgressUIHandlers();
- _currentEmulatedGamePath = path;
+ _currentApplicationData = application;
_deviceExitStatus.Reset();
@@ -1165,7 +1171,7 @@ namespace Ryujinx.UI
_tableStore.AppendValues(
args.AppData.Favorite,
new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
- $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
+ $"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}",
args.AppData.Developer,
args.AppData.Version,
args.AppData.TimePlayedString,
@@ -1253,9 +1259,22 @@ namespace Ryujinx.UI
{
_gameTableSelection.GetSelected(out TreeIter treeIter);
- string path = (string)_tableStore.GetValue(treeIter, 9);
+ ApplicationData application = new()
+ {
+ Favorite = (bool)_tableStore.GetValue(treeIter, 0),
+ Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0],
+ Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
+ Developer = (string)_tableStore.GetValue(treeIter, 3),
+ Version = (string)_tableStore.GetValue(treeIter, 4),
+ TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
+ LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
+ FileExtension = (string)_tableStore.GetValue(treeIter, 7),
+ FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
+ Path = (string)_tableStore.GetValue(treeIter, 9),
+ ControlHolder = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10),
+ };
- RunApplication(path);
+ RunApplication(application);
}
private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
@@ -1313,13 +1332,22 @@ namespace Ryujinx.UI
return;
}
- string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString();
- string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0];
- string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
-
- BlitStruct<ApplicationControlProperty> controlData = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10);
+ ApplicationData application = new()
+ {
+ Favorite = (bool)_tableStore.GetValue(treeIter, 0),
+ Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0],
+ Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
+ Developer = (string)_tableStore.GetValue(treeIter, 3),
+ Version = (string)_tableStore.GetValue(treeIter, 4),
+ TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
+ LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
+ FileExtension = (string)_tableStore.GetValue(treeIter, 7),
+ FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
+ Path = (string)_tableStore.GetValue(treeIter, 9),
+ ControlHolder = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10),
+ };
- _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
+ _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application);
}
private void Load_Application_File(object sender, EventArgs args)
@@ -1341,7 +1369,15 @@ namespace Ryujinx.UI
if (fileChooser.Run() == (int)ResponseType.Accept)
{
- RunApplication(fileChooser.Filename);
+ if (ApplicationLibrary.TryGetApplicationsFromFile(fileChooser.Filename,
+ out List<ApplicationData> applications))
+ {
+ RunApplication(applications[0]);
+ }
+ else
+ {
+ GtkDialog.CreateErrorDialog("No applications found in selected file.");
+ }
}
}
@@ -1351,7 +1387,13 @@ namespace Ryujinx.UI
if (fileChooser.Run() == (int)ResponseType.Accept)
{
- RunApplication(fileChooser.Filename);
+ ApplicationData applicationData = new()
+ {
+ Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename),
+ Path = fileChooser.Filename,
+ };
+
+ RunApplication(applicationData);
}
}
@@ -1366,7 +1408,14 @@ namespace Ryujinx.UI
{
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
- RunApplication(contentPath);
+ ApplicationData applicationData = new()
+ {
+ Name = "miiEdit",
+ Id = 0x0100000000001009ul,
+ Path = contentPath,
+ };
+
+ RunApplication(applicationData);
}
private void Open_Ryu_Folder(object sender, EventArgs args)
@@ -1646,13 +1695,13 @@ namespace Ryujinx.UI
{
_userChannelPersistence.ShouldRestart = false;
- RunApplication(_currentEmulatedGamePath);
+ RunApplication(_currentApplicationData);
}
else
{
// otherwise, clear state.
_userChannelPersistence = new UserChannelPersistence();
- _currentEmulatedGamePath = null;
+ _currentApplicationData = null;
_actionMenu.Sensitive = false;
_firmwareInstallFile.Sensitive = true;
_firmwareInstallDirectory.Sensitive = true;
@@ -1714,7 +1763,7 @@ namespace Ryujinx.UI
_emulationContext.Processes.ActiveApplication.ProgramId,
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
- _currentEmulatedGamePath);
+ _currentApplicationData.Path);
window.Destroyed += CheatWindow_Destroyed;
window.Show();
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs b/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
index c8236223..e37906d5 100644
--- a/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
+++ b/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
@@ -16,6 +16,8 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
@@ -23,7 +25,6 @@ using Ryujinx.UI.Windows;
using System;
using System.Buffers;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading;
@@ -36,17 +37,13 @@ namespace Ryujinx.UI.Widgets
private readonly VirtualFileSystem _virtualFileSystem;
private readonly AccountManager _accountManager;
private readonly HorizonClient _horizonClient;
- private readonly BlitStruct<ApplicationControlProperty> _controlData;
- private readonly string _titleFilePath;
- private readonly string _titleName;
- private readonly string _titleIdText;
- private readonly ulong _titleId;
+ private readonly ApplicationData _applicationData;
private MessageDialog _dialog;
private bool _cancel;
- public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct<ApplicationControlProperty> controlData)
+ public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData)
{
_parent = parent;
@@ -55,23 +52,22 @@ namespace Ryujinx.UI.Widgets
_virtualFileSystem = virtualFileSystem;
_accountManager = accountManager;
_horizonClient = horizonClient;
- _titleFilePath = titleFilePath;
- _titleName = titleName;
- _titleIdText = titleId;
- _controlData = controlData;
+ _applicationData = applicationData;
- if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId))
+ if (!_applicationData.ControlHolder.ByteSpan.IsZeros())
{
- GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id");
-
- return;
+ _openSaveUserDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.UserAccountSaveDataSize > 0;
+ _openSaveDeviceDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.DeviceSaveDataSize > 0;
+ _openSaveBcatDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
+ }
+ else
+ {
+ _openSaveUserDirMenuItem.Sensitive = false;
+ _openSaveDeviceDirMenuItem.Sensitive = false;
+ _openSaveBcatDirMenuItem.Sensitive = false;
}
- _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
- _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
- _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
-
- string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower();
+ string fileExt = System.IO.Path.GetExtension(_applicationData.Path).ToLower();
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
_extractRomFsMenuItem.Sensitive = hasNca;
@@ -137,7 +133,7 @@ namespace Ryujinx.UI.Widgets
private void OpenSaveDir(in SaveDataFilter saveDataFilter)
{
- if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId))
+ if (!TryFindSaveData(_applicationData.Name, _applicationData.Id, _applicationData.ControlHolder, in saveDataFilter, out ulong saveDataId))
{
return;
}
@@ -190,7 +186,7 @@ namespace Ryujinx.UI.Widgets
{
Title = "Ryujinx - NCA Section Extractor",
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Gtk3.UI.Common.Resources.Logo_Ryujinx.png"),
- SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...",
+ SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_applicationData.Path)}...",
WindowPosition = WindowPosition.Center,
};
@@ -202,29 +198,16 @@ namespace Ryujinx.UI.Widgets
}
});
- using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read);
+ using FileStream file = new(_applicationData.Path, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
- if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") ||
- (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
- (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
+ if ((System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".nsp") ||
+ (System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".pfs0") ||
+ (System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".xci"))
{
- IFileSystem pfs;
-
- if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
- {
- Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
-
- pfs = xci.OpenPartition(XciPartitionType.Secure);
- }
- else
- {
- var pfsTemp = new PartitionFileSystem();
- pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
- pfs = pfsTemp;
- }
+ IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(_applicationData.Path, _virtualFileSystem);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
@@ -249,7 +232,7 @@ namespace Ryujinx.UI.Widgets
}
}
}
- else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca")
+ else if (System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".nca")
{
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
}
@@ -266,7 +249,11 @@ namespace Ryujinx.UI.Widgets
return;
}
- (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
+ (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
if (updatePatchNca != null)
{
@@ -460,44 +447,44 @@ namespace Ryujinx.UI.Widgets
private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
{
var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
- var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default);
+ var saveDataFilter = SaveDataFilter.Make(_applicationData.Id, saveType: default, userId, saveDataId: default, index: default);
OpenSaveDir(in saveDataFilter);
}
private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
{
- var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default);
+ var saveDataFilter = SaveDataFilter.Make(_applicationData.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default);
OpenSaveDir(in saveDataFilter);
}
private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
{
- var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
+ var saveDataFilter = SaveDataFilter.Make(_applicationData.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
OpenSaveDir(in saveDataFilter);
}
private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
{
- new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show();
+ new TitleUpdateWindow(_parent, _virtualFileSystem, _applicationData).Show();
}
private void ManageDlc_Clicked(object sender, EventArgs args)
{
- new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show();
+ new DlcWindow(_virtualFileSystem, _applicationData.IdString, _applicationData).Show();
}
private void ManageCheats_Clicked(object sender, EventArgs args)
{
- new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show();
+ new CheatWindow(_virtualFileSystem, _applicationData.Id, _applicationData.Name, _applicationData.Path).Show();
}
private void OpenTitleModDir_Clicked(object sender, EventArgs args)
{
string modsBasePath = ModLoader.GetModsBasePath();
- string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, _titleIdText);
+ string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, _applicationData.IdString);
OpenHelper.OpenFolder(titleModsPath);
}
@@ -505,7 +492,7 @@ namespace Ryujinx.UI.Widgets
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
{
string sdModsBasePath = ModLoader.GetSdModsBasePath();
- string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, _titleIdText);
+ string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, _applicationData.IdString);
OpenHelper.OpenFolder(titleModsPath);
}
@@ -527,7 +514,7 @@ namespace Ryujinx.UI.Widgets
private void OpenPtcDir_Clicked(object sender, EventArgs args)
{
- string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu");
+ string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu");
string mainPath = System.IO.Path.Combine(ptcDir, "0");
string backupPath = System.IO.Path.Combine(ptcDir, "1");
@@ -544,7 +531,7 @@ namespace Ryujinx.UI.Widgets
private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
{
- string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader");
+ string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "shader");
if (!Directory.Exists(shaderCacheDir))
{
@@ -556,10 +543,10 @@ namespace Ryujinx.UI.Widgets
private void PurgePtcCache_Clicked(object sender, EventArgs args)
{
- DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0"));
- DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1"));
+ DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu", "0"));
+ DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu", "1"));
- MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
+ MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n<b>{_applicationData.Name}</b>\n\nAre you sure you want to proceed?");
List<FileInfo> cacheFiles = new();
@@ -593,9 +580,9 @@ namespace Ryujinx.UI.Widgets
private void PurgeShaderCache_Clicked(object sender, EventArgs args)
{
- DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"));
+ DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "shader"));
- using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
+ using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_applicationData.Name}</b>\n\nAre you sure you want to proceed?");
List<DirectoryInfo> oldCacheDirectories = new();
List<FileInfo> newCacheFiles = new();
@@ -637,8 +624,11 @@ namespace Ryujinx.UI.Widgets
private void CreateShortcut_Clicked(object sender, EventArgs args)
{
- byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
- ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+ byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_applicationData.Path, ConfigurationState.Instance.System.Language, _applicationData.Id);
+ ShortcutHelper.CreateAppShortcut(_applicationData.Path, _applicationData.Name, _applicationData.IdString, appIcon);
}
}
}
diff --git a/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs
index 96ed0723..d9f01918 100644
--- a/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs
@@ -1,7 +1,9 @@
using Gtk;
+using LibHac.Tools.FsSystem;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
@@ -27,8 +29,13 @@ namespace Ryujinx.UI.Windows
private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
{
builder.Autoconnect(this);
+
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
- _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}";
+ _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}";
string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, titleId.ToString("X16"));
diff --git a/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs
index 388f1108..b69cc003 100644
--- a/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs
@@ -2,17 +2,21 @@ using Gtk;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
-using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
+using Ryujinx.UI.App.Common;
using Ryujinx.UI.Widgets;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
+using System.Linq;
using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.UI.Windows
@@ -20,7 +24,7 @@ namespace Ryujinx.UI.Windows
public class DlcWindow : Window
{
private readonly VirtualFileSystem _virtualFileSystem;
- private readonly string _titleId;
+ private readonly string _applicationId;
private readonly string _dlcJsonPath;
private readonly List<DownloadableContentContainer> _dlcContainerList;
@@ -32,16 +36,16 @@ namespace Ryujinx.UI.Windows
[GUI] TreeSelection _dlcTreeSelection;
#pragma warning restore CS0649, IDE0044
- public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { }
+ public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData applicationData) : this(new Builder("Ryujinx.Gtk3.UI.Windows.DlcWindow.glade"), virtualFileSystem, titleId, applicationData) { }
- private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow"))
+ private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_dlcWindow"))
{
builder.Autoconnect(this);
- _titleId = titleId;
+ _applicationId = applicationId;
_virtualFileSystem = virtualFileSystem;
- _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json");
- _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
+ _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json");
+ _baseTitleInfoLabel.Text = $"DLC Available for {applicationData.Name} [{applicationId.ToUpper()}]";
try
{
@@ -72,7 +76,7 @@ namespace Ryujinx.UI.Windows
};
_dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
- _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
+ _dlcTreeView.AppendColumn("ApplicationId", new CellRendererText(), "text", 1);
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
@@ -86,18 +90,18 @@ namespace Ryujinx.UI.Windows
bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled);
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath);
- using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
+ using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(dlcContainer.ContainerPath, _virtualFileSystem, false);
- PartitionFileSystem pfs = new();
- pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
-
- _virtualFileSystem.ImportTickets(pfs);
+ if (partitionFileSystem == null)
+ {
+ continue;
+ }
foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList)
{
using var ncaFile = new UniqueRef<IFile>();
- pfs.OpenFile(ref ncaFile.Ref, dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ partitionFileSystem.OpenFile(ref ncaFile.Ref, dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath);
if (nca != null)
@@ -112,6 +116,9 @@ namespace Ryujinx.UI.Windows
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}");
}
}
+
+ // NOTE: Try to load downloadable contents from PFS last to preserve enabled state.
+ AddDlc(applicationData.Path, true);
}
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
@@ -128,6 +135,52 @@ namespace Ryujinx.UI.Windows
return null;
}
+ private void AddDlc(string path, bool ignoreNotFound = false)
+ {
+ if (!File.Exists(path) || _dlcContainerList.Any(x => x.ContainerPath == path))
+ {
+ return;
+ }
+
+ using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
+
+ bool containsDlc = false;
+
+ TreeIter? parentIter = null;
+
+ foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
+ {
+ using var ncaFile = new UniqueRef<IFile>();
+
+ partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
+
+ if (nca == null)
+ {
+ continue;
+ }
+
+ if (nca.Header.ContentType == NcaContentType.PublicData)
+ {
+ if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL))
+ {
+ continue;
+ }
+
+ parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path);
+
+ ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
+ containsDlc = true;
+ }
+ }
+
+ if (!containsDlc && !ignoreNotFound)
+ {
+ GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
+ }
+ }
+
private void AddButton_Clicked(object sender, EventArgs args)
{
FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
@@ -147,52 +200,7 @@ namespace Ryujinx.UI.Windows
{
foreach (string containerPath in fileChooser.Filenames)
{
- if (!File.Exists(containerPath))
- {
- return;
- }
-
- using FileStream containerFile = File.OpenRead(containerPath);
-
- PartitionFileSystem pfs = new();
- pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
- bool containsDlc = false;
-
- _virtualFileSystem.ImportTickets(pfs);
-
- TreeIter? parentIter = null;
-
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
- {
- using var ncaFile = new UniqueRef<IFile>();
-
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath);
-
- if (nca == null)
- {
- continue;
- }
-
- if (nca.Header.ContentType == NcaContentType.PublicData)
- {
- if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId)
- {
- break;
- }
-
- parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
-
- ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
- containsDlc = true;
- }
- }
-
- if (!containsDlc)
- {
- GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
- }
+ AddDlc(containerPath);
}
}
diff --git a/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs
index 74b2330e..3ac972ea 100644
--- a/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs
@@ -2,14 +2,17 @@ using Gtk;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
-using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Widgets;
using System;
using System.Collections.Generic;
@@ -24,7 +27,7 @@ namespace Ryujinx.UI.Windows
{
private readonly MainWindow _parent;
private readonly VirtualFileSystem _virtualFileSystem;
- private readonly string _titleId;
+ private readonly ApplicationData _applicationData;
private readonly string _updateJsonPath;
private TitleUpdateMetadata _titleUpdateWindowData;
@@ -38,17 +41,17 @@ namespace Ryujinx.UI.Windows
[GUI] RadioButton _noUpdateRadioButton;
#pragma warning restore CS0649, IDE0044
- public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { }
+ public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Gtk3.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { }
- private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
+ private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
{
_parent = parent;
builder.Autoconnect(this);
- _titleId = titleId;
+ _applicationData = applicationData;
_virtualFileSystem = virtualFileSystem;
- _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json");
+ _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "updates.json");
_radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
try
@@ -64,7 +67,10 @@ namespace Ryujinx.UI.Windows
};
}
- _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]";
+ _baseTitleInfoLabel.Text = $"Updates Available for {applicationData.Name} [{applicationData.IdString}]";
+
+ // Try to get updates from PFS first
+ AddUpdate(_applicationData.Path, true);
foreach (string path in _titleUpdateWindowData.Paths)
{
@@ -84,47 +90,69 @@ namespace Ryujinx.UI.Windows
}
}
- private void AddUpdate(string path)
+ private void AddUpdate(string path, bool ignoreNotFound = false)
{
- if (File.Exists(path))
+ if (!File.Exists(path) || _radioButtonToPathDictionary.ContainsValue(path))
{
- using FileStream file = new(path, FileMode.Open, FileAccess.Read);
+ return;
+ }
- PartitionFileSystem nsp = new();
- nsp.Initialize(file.AsStorage()).ThrowIfFailure();
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
- try
- {
- (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
+ try
+ {
+ using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
- if (controlNca != null && patchNca != null)
- {
- ApplicationControlProperty controlData = new();
+ Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel);
- using var nacpFile = new UniqueRef<IFile>();
+ Nca patchNca = null;
+ Nca controlNca = null;
- controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
- nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
+ if (updates.TryGetValue(_applicationData.Id, out ContentMetaData update))
+ {
+ patchNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Program);
+ controlNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Control);
+ }
- RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}");
- radioButton.JoinGroup(_noUpdateRadioButton);
+ if (controlNca != null && patchNca != null)
+ {
+ ApplicationControlProperty controlData = new();
- _availableUpdatesBox.Add(radioButton);
- _radioButtonToPathDictionary.Add(radioButton, path);
+ using var nacpFile = new UniqueRef<IFile>();
- radioButton.Show();
- radioButton.Active = true;
- }
- else
+ controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
+
+ string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}";
+
+ if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
{
- GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
+ radioLabel = "Bundled: " + radioLabel;
}
+
+ RadioButton radioButton = new(radioLabel);
+ radioButton.JoinGroup(_noUpdateRadioButton);
+
+ _availableUpdatesBox.Add(radioButton);
+ _radioButtonToPathDictionary.Add(radioButton, path);
+
+ radioButton.Show();
+ radioButton.Active = true;
}
- catch (Exception exception)
+ else
{
- GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");
+ if (!ignoreNotFound)
+ {
+ GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
+ }
}
}
+ catch (Exception exception)
+ {
+ GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");
+ }
}
private void RemoveUpdates(bool removeSelectedOnly = false)
diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
index 3c34a886..e6c0fce0 100644
--- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs
+++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
@@ -14,6 +14,7 @@ using Ryujinx.Common.Utilities;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Services.Ssl;
using Ryujinx.HLE.HOS.Services.Time;
+using Ryujinx.HLE.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
@@ -184,41 +185,6 @@ namespace Ryujinx.HLE.FileSystem
}
}
- // fs must contain AOC nca files in its root
- public void AddAocData(IFileSystem fs, string containerPath, ulong aocBaseId, IntegrityCheckLevel integrityCheckLevel)
- {
- _virtualFileSystem.ImportTickets(fs);
-
- foreach (var ncaPath in fs.EnumerateEntries("*.cnmt.nca", SearchOptions.Default))
- {
- using var ncaFile = new UniqueRef<IFile>();
-
- fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
- if (nca.Header.ContentType != NcaContentType.Meta)
- {
- Logger.Warning?.Print(LogClass.Application, $"{ncaPath} is not a valid metadata file");
-
- continue;
- }
-
- using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel);
- using var cnmtFile = new UniqueRef<IFile>();
-
- pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- var cnmt = new Cnmt(cnmtFile.Get.AsStream());
- if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId)
- {
- continue;
- }
-
- string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower();
-
- AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true);
- }
- }
-
public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool mergedToContainer = false)
{
// TODO: Check Aoc version.
@@ -232,11 +198,7 @@ namespace Ryujinx.HLE.FileSystem
if (!mergedToContainer)
{
- using FileStream fileStream = File.OpenRead(containerPath);
- using PartitionFileSystem partitionFileSystem = new();
- partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure();
-
- _virtualFileSystem.ImportTickets(partitionFileSystem);
+ using var pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(containerPath, _virtualFileSystem);
}
}
}
diff --git a/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs b/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs
new file mode 100644
index 00000000..aebcf798
--- /dev/null
+++ b/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs
@@ -0,0 +1,61 @@
+using LibHac.Common.Keys;
+using LibHac.Fs.Fsa;
+using LibHac.Ncm;
+using LibHac.Tools.FsSystem.NcaUtils;
+using LibHac.Tools.Ncm;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using System;
+
+namespace Ryujinx.HLE.FileSystem
+{
+ /// <summary>
+ /// Thin wrapper around <see cref="Cnmt"/>
+ /// </summary>
+ public class ContentMetaData
+ {
+ private readonly IFileSystem _pfs;
+ private readonly Cnmt _cnmt;
+
+ public ulong Id => _cnmt.TitleId;
+ public TitleVersion Version => _cnmt.TitleVersion;
+ public ContentMetaType Type => _cnmt.Type;
+ public ulong ApplicationId => _cnmt.ApplicationTitleId;
+ public ulong PatchId => _cnmt.PatchTitleId;
+ public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion;
+ public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion;
+ public byte[] Digest => _cnmt.Hash;
+
+ public ulong ProgramBaseId => Id & ~0x1FFFUL;
+ public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application;
+
+ public ContentMetaData(IFileSystem pfs, Cnmt cnmt)
+ {
+ _pfs = pfs;
+ _cnmt = cnmt;
+ }
+
+ public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0)
+ {
+ // TODO: Replace this with a check for IdOffset as soon as LibHac supports it:
+ // && entry.IdOffset == programIndex
+
+ foreach (var entry in _cnmt.ContentEntries)
+ {
+ if (entry.Type != type)
+ {
+ continue;
+ }
+
+ string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower();
+ Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca");
+
+ if (nca.GetProgramIndex() == programIndex)
+ {
+ return nca;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
index 6c2415e4..6c2a1989 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
@@ -3,7 +3,6 @@ using LibHac.FsSystem;
using LibHac.Loader;
using LibHac.Ncm;
using LibHac.Ns;
-using Ryujinx.HLE.HOS;
using Ryujinx.HLE.Loaders.Processes.Extensions;
namespace Ryujinx.HLE.Loaders.Processes
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
index e70fcb6f..da563720 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
@@ -7,16 +7,25 @@ using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
+using LibHac.Tools.Ncm;
+using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.Utilities;
using System.IO;
using System.Linq;
using ApplicationId = LibHac.Ncm.ApplicationId;
+using ContentType = LibHac.Ncm.ContentType;
+using Path = System.IO.Path;
namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
- static class NcaExtensions
+ public static class NcaExtensions
{
+ private static readonly TitleUpdateMetadataJsonSerializerContext _applicationSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
{
// Extract RomFs and ExeFs from NCA.
@@ -47,7 +56,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
nacpData = controlNca.GetNacp(device);
}
- /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update.
+ /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update.
// Load program 0 control NCA as we are going to need it for display version.
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
@@ -86,6 +95,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return processResult;
}
+ public static ulong GetProgramIdBase(this Nca nca)
+ {
+ return nca.Header.TitleId & ~0x1FFFUL;
+ }
+
public static int GetProgramIndex(this Nca nca)
{
return (int)(nca.Header.TitleId & 0xF);
@@ -96,6 +110,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nca.Header.ContentType == NcaContentType.Program;
}
+ public static bool IsMain(this Nca nca)
+ {
+ return nca.IsProgram() && !nca.IsPatch();
+ }
+
public static bool IsPatch(this Nca nca)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
@@ -108,6 +127,43 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nca.Header.ContentType == NcaContentType.Control;
}
+ public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath)
+ {
+ updatePath = null;
+
+ // Load Update NCAs.
+ Nca updatePatchNca = null;
+ Nca updateControlNca = null;
+
+ // Clear the program index part.
+ ulong titleIdBase = mainNca.GetProgramIdBase();
+
+ // Load update information if exists.
+ string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
+ if (File.Exists(titleUpdateMetadataPath))
+ {
+ updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _applicationSerializerContext.TitleUpdateMetadata).Selected;
+ if (File.Exists(updatePath))
+ {
+ IFileSystem updatePartitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(updatePath, fileSystem);
+
+ foreach ((ulong applicationTitleId, ContentMetaData content) in updatePartitionFileSystem.GetContentData(ContentMetaType.Patch, fileSystem, checkLevel))
+ {
+ if ((applicationTitleId & ~0x1FFFUL) != titleIdBase)
+ {
+ continue;
+ }
+
+ updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex);
+ updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex);
+ break;
+ }
+ }
+ }
+
+ return (updatePatchNca, updateControlNca);
+ }
+
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
{
IFileSystem exeFs = null;
@@ -172,5 +228,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nacpData;
}
+
+ public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType)
+ {
+ string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt";
+ using var cnmtFile = new UniqueRef<IFile>();
+
+ try
+ {
+ Result result = cnmtNca.OpenFileSystem(0, checkLevel)
+ .OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read);
+
+ if (result.IsSuccess())
+ {
+ return new Cnmt(cnmtFile.Release().AsStream());
+ }
+ }
+ catch (HorizonResultException ex)
+ {
+ if (!ResultFs.PathNotFound.Includes(ex.ResultValue))
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Failed get CNMT for '{cnmtNca.Header.TitleId:x16}' from NCA: {ex.Message}");
+ }
+ }
+
+ return null;
+ }
}
}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
index e95b1b05..bee2572a 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
@@ -1,26 +1,58 @@
using LibHac.Common;
+using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
+using LibHac.Tools.Ncm;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
+using ContentType = LibHac.Ncm.ContentType;
namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
public static class PartitionFileSystemExtensions
{
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, string path, out string errorMessage)
+ public static Dictionary<ulong, ContentMetaData> GetContentData(this IFileSystem partitionFileSystem,
+ ContentMetaType contentType, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel)
+ {
+ fileSystem.ImportTickets(partitionFileSystem);
+
+ var programs = new Dictionary<ulong, ContentMetaData>();
+
+ foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca"))
+ {
+ Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, contentType);
+
+ if (cnmt == null)
+ {
+ continue;
+ }
+
+ ContentMetaData content = new(partitionFileSystem, cnmt);
+
+ if (content.Type != contentType)
+ {
+ continue;
+ }
+
+ programs.TryAdd(content.ApplicationId, content);
+ }
+
+ return programs;
+ }
+
+ internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, string path, ulong applicationId, out string errorMessage)
where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
where TFormat : IPartitionFileSystemFormat
where THeader : unmanaged, IPartitionFileSystemHeader
@@ -35,31 +67,22 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
try
{
- device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem);
+ Dictionary<ulong, ContentMetaData> applications = partitionFileSystem.GetContentData(ContentMetaType.Application, device.FileSystem, device.System.FsIntegrityCheckLevel);
- // TODO: To support multi-games container, this should use CNMT NCA instead.
- foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
+ if (applicationId == 0)
{
- Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
-
- if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
- {
- continue;
- }
-
- if (nca.IsPatch())
+ foreach ((ulong _, ContentMetaData content) in applications)
{
- patchNca = nca;
- }
- else if (nca.IsProgram())
- {
- mainNca = nca;
- }
- else if (nca.IsControl())
- {
- controlNca = nca;
+ mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
+ controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
+ break;
}
}
+ else if (applications.TryGetValue(applicationId, out ContentMetaData content))
+ {
+ mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
+ controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
+ }
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
}
@@ -79,54 +102,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return (false, ProcessResult.Failed);
}
- // Load Update NCAs.
- Nca updatePatchNca = null;
- Nca updateControlNca = null;
-
- if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
- {
- // Clear the program index part.
- titleIdBase &= ~0xFUL;
-
- // Load update information if exists.
- string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
- if (File.Exists(titleUpdateMetadataPath))
- {
- string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
- if (File.Exists(updatePath))
- {
- PartitionFileSystem updatePartitionFileSystem = new();
- updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
-
- device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
-
- // TODO: This should use CNMT NCA instead.
- foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
- {
- Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
-
- if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
- {
- continue;
- }
-
- if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
- {
- break;
- }
-
- if (nca.IsProgram())
- {
- updatePatchNca = nca;
- }
- else if (nca.IsControl())
- {
- updateControlNca = nca;
- }
- }
- }
- }
- }
+ (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _);
if (updatePatchNca != null)
{
@@ -138,10 +114,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
controlNca = updateControlNca;
}
- // Load contained DownloadableContents.
// TODO: If we want to support multi-processes in future, we shouldn't clear AddOnContent data here.
device.Configuration.ContentManager.ClearAocData();
- device.Configuration.ContentManager.AddAocData(partitionFileSystem, path, mainNca.Header.TitleId, device.Configuration.FsIntegrityCheckLevel);
// Load DownloadableContents.
string addOnContentMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
@@ -153,9 +127,12 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
- if (File.Exists(downloadableContentContainer.ContainerPath) && downloadableContentNca.Enabled)
+ if (File.Exists(downloadableContentContainer.ContainerPath))
{
- device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath);
+ if (downloadableContentNca.Enabled)
+ {
+ device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath);
+ }
}
else
{
@@ -168,18 +145,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return (true, mainNca.Load(device, patchNca, controlNca));
}
- errorMessage = "Unable to load: Could not find Main NCA";
+ errorMessage = $"Unable to load: Could not find Main NCA for title \"{applicationId:X16}\"";
return (false, ProcessResult.Failed);
}
- public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path)
+ public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path)
{
using var ncaFile = new UniqueRef<IFile>();
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage());
+ return new Nca(keySet, ncaFile.Release().AsStorage());
}
}
}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
index e5056c89..12d9c8bd 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
@@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
}
- public bool LoadXci(string path)
+ public bool LoadXci(string path, ulong applicationId)
{
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
@@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes
return false;
}
- (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage);
+ (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, applicationId, out string errorMessage);
if (!success)
{
@@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes
return false;
}
- public bool LoadNsp(string path)
+ public bool LoadNsp(string path, ulong applicationId)
{
FileStream file = new(path, FileMode.Open, FileAccess.Read);
PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
- (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);
+ (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, applicationId, out string errorMessage);
if (processResult.ProcessId == 0)
{
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
index fe2aaac6..cf4eb416 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
@@ -43,15 +43,14 @@ namespace Ryujinx.HLE.Loaders.Processes
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
{
- Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
+ Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath);
- if (!nca.IsProgram() && nca.IsPatch())
+ if (!nca.IsProgram())
{
continue;
}
- ulong currentProgramId = nca.Header.TitleId;
- ulong currentMainProgramId = currentProgramId & ~0xFFFul;
+ ulong currentMainProgramId = nca.GetProgramIdBase();
if (applicationId == 0 && currentMainProgramId != 0)
{
@@ -68,7 +67,7 @@ namespace Ryujinx.HLE.Loaders.Processes
break;
}
- hasIndex[(int)(currentProgramId & 0xF)] = true;
+ hasIndex[nca.GetProgramIndex()] = true;
}
if (programCount == 0)
diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs
index 81c3ab47..9dfc6989 100644
--- a/src/Ryujinx.HLE/Switch.cs
+++ b/src/Ryujinx.HLE/Switch.cs
@@ -73,9 +73,9 @@ namespace Ryujinx.HLE
return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
}
- public bool LoadXci(string xciFile)
+ public bool LoadXci(string xciFile, ulong applicationId = 0)
{
- return Processes.LoadXci(xciFile);
+ return Processes.LoadXci(xciFile, applicationId);
}
public bool LoadNca(string ncaFile)
@@ -83,9 +83,9 @@ namespace Ryujinx.HLE
return Processes.LoadNca(ncaFile);
}
- public bool LoadNsp(string nspFile)
+ public bool LoadNsp(string nspFile, ulong applicationId = 0)
{
- return Processes.LoadNsp(nspFile);
+ return Processes.LoadNsp(nspFile, applicationId);
}
public bool LoadProgram(string fileName)
diff --git a/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs b/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs
new file mode 100644
index 00000000..3c4ce085
--- /dev/null
+++ b/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs
@@ -0,0 +1,45 @@
+using LibHac;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Tools.Fs;
+using LibHac.Tools.FsSystem;
+using Ryujinx.HLE.FileSystem;
+using System.IO;
+
+namespace Ryujinx.HLE.Utilities
+{
+ public static class PartitionFileSystemUtils
+ {
+ public static IFileSystem OpenApplicationFileSystem(string path, VirtualFileSystem fileSystem, bool throwOnFailure = true)
+ {
+ FileStream file = File.OpenRead(path);
+
+ IFileSystem partitionFileSystem;
+
+ if (Path.GetExtension(path).ToLower() == ".xci")
+ {
+ partitionFileSystem = new Xci(fileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
+ }
+ else
+ {
+ var pfsTemp = new PartitionFileSystem();
+ Result initResult = pfsTemp.Initialize(file.AsStorage());
+
+ if (throwOnFailure)
+ {
+ initResult.ThrowIfFailure();
+ }
+ else if (initResult.IsFailure())
+ {
+ return null;
+ }
+
+ partitionFileSystem = pfsTemp;
+ }
+
+ fileSystem.ImportTickets(partitionFileSystem);
+
+ return partitionFileSystem;
+ }
+ }
+}
diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs
index 13c05655..7108defc 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationData.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs
@@ -9,9 +9,11 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.UI.Common.Helper;
using System;
using System.IO;
+using System.Text.Json.Serialization;
namespace Ryujinx.UI.App.Common
{
@@ -19,10 +21,10 @@ namespace Ryujinx.UI.App.Common
{
public bool Favorite { get; set; }
public byte[] Icon { get; set; }
- public string TitleName { get; set; }
- public string TitleId { get; set; }
- public string Developer { get; set; }
- public string Version { get; set; }
+ public string Name { get; set; } = "Unknown";
+ public ulong Id { get; set; }
+ public string Developer { get; set; } = "Unknown";
+ public string Version { get; set; } = "0";
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }
@@ -36,7 +38,11 @@ namespace Ryujinx.UI.App.Common
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
- public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
+ [JsonIgnore] public string IdString => Id.ToString("x16");
+
+ [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL;
+
+ public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
{
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
@@ -105,7 +111,7 @@ namespace Ryujinx.UI.App.Common
return string.Empty;
}
- (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
+ (Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _);
if (updatePatchNca != null)
{
diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
index 176011dd..ef3826cf 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
@@ -4,28 +4,29 @@ using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
-using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Npdm;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Configuration.System;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
+using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using TimeSpan = System.TimeSpan;
@@ -43,15 +44,16 @@ namespace Ryujinx.UI.App.Common
private readonly byte[] _nsoIcon;
private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly IntegrityCheckLevel _checkLevel;
private Language _desiredTitleLanguage;
private CancellationTokenSource _cancellationToken;
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- public ApplicationLibrary(VirtualFileSystem virtualFileSystem)
+ public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
{
_virtualFileSystem = virtualFileSystem;
+ _checkLevel = checkLevel;
_nspIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSP.png");
_xciIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_XCI.png");
@@ -70,272 +72,251 @@ namespace Ryujinx.UI.App.Common
return resourceByteArray;
}
- public void CancelLoading()
+ private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath)
{
- _cancellationToken?.Cancel();
- }
+ ApplicationData data = new()
+ {
+ Icon = _nspIcon,
+ };
- public static void ReadControlData(IFileSystem controlFs, Span<byte> outProperty)
- {
- using UniqueRef<IFile> controlFile = new();
+ using UniqueRef<IFile> npdmFile = new();
- controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
- controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
- }
+ try
+ {
+ Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
- public void LoadApplications(List<string> appDirs, Language desiredTitleLanguage)
- {
- int numApplicationsFound = 0;
- int numApplicationsLoaded = 0;
+ if (ResultFs.PathNotFound.Includes(result))
+ {
+ Npdm npdm = new(npdmFile.Get.AsStream());
- _desiredTitleLanguage = desiredTitleLanguage;
+ data.Name = npdm.TitleName;
+ data.Id = npdm.Aci0.TitleId;
+ }
- _cancellationToken = new CancellationTokenSource();
+ return data;
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception.Message}");
- // Builds the applications list with paths to found applications
- List<string> applications = new();
+ return null;
+ }
+ }
- try
- {
- foreach (string appDir in appDirs)
- {
- if (_cancellationToken.Token.IsCancellationRequested)
- {
- return;
- }
+ private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath)
+ {
+ bool isExeFs = false;
- if (!Directory.Exists(appDir))
- {
- Logger.Warning?.Print(LogClass.Application, $"The specified game directory \"{appDir}\" does not exist.");
+ // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
+ bool hasMainNca = false;
- continue;
- }
+ foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
+ {
+ if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca")
+ {
+ using UniqueRef<IFile> ncaFile = new();
try
{
- IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", SearchOption.AllDirectories).Where(file =>
- {
- return
- (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) ||
- (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value) ||
- (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value) ||
- (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value) ||
- (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value) ||
- (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value);
- });
+ pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- foreach (string app in files)
- {
- if (_cancellationToken.Token.IsCancellationRequested)
- {
- return;
- }
-
- var fileInfo = new FileInfo(app);
- string extension = fileInfo.Extension.ToLower();
-
- if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso")
- {
- var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName;
+ Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
+ int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
- if (!File.Exists(fullPath))
- {
- Logger.Warning?.Print(LogClass.Application, $"Skipping invalid symlink: {fileInfo.FullName}");
- continue;
- }
+ // Some main NCAs don't have a data partition, so check if the partition exists before opening it
+ if (nca.Header.ContentType == NcaContentType.Program &&
+ !(nca.SectionExists(NcaSectionType.Data) &&
+ nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
+ {
+ hasMainNca = true;
- applications.Add(fullPath);
- numApplicationsFound++;
- }
+ break;
}
}
- catch (UnauthorizedAccessException)
+ catch (Exception exception)
{
- Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\"");
+ Logger.Warning?.Print(LogClass.Application, $"Encountered an error while trying to load applications from file '{filePath}': {exception}");
+
+ return null;
}
}
-
- // Loops through applications list, creating a struct and then firing an event containing the struct for each application
- foreach (string applicationPath in applications)
+ else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
{
- if (_cancellationToken.Token.IsCancellationRequested)
- {
- return;
- }
+ isExeFs = true;
+ }
+ }
- long fileSize = new FileInfo(applicationPath).Length;
- string titleName = "Unknown";
- string titleId = "0000000000000000";
- string developer = "Unknown";
- string version = "0";
- byte[] applicationIcon = null;
+ if (hasMainNca)
+ {
+ List<ApplicationData> applications = GetApplicationsFromPfs(pfs, filePath);
- BlitStruct<ApplicationControlProperty> controlHolder = new(1);
+ switch (applications.Count)
+ {
+ case 1:
+ return applications[0];
+ case >= 1:
+ Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}");
+ return applications[0];
+ default:
+ return null;
+ }
+ }
- try
- {
- string extension = Path.GetExtension(applicationPath).ToLower();
+ if (isExeFs)
+ {
+ return GetApplicationFromExeFs(pfs, filePath);
+ }
- using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
+ return null;
+ }
- if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
- {
- try
- {
- IFileSystem pfs;
+ private List<ApplicationData> GetApplicationsFromPfs(IFileSystem pfs, string filePath)
+ {
+ var applications = new List<ApplicationData>();
+ string extension = Path.GetExtension(filePath).ToLower();
- bool isExeFs = false;
+ foreach ((ulong titleId, ContentMetaData content) in pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel))
+ {
+ ApplicationData applicationData = new()
+ {
+ Id = titleId,
+ Path = filePath,
+ };
- if (extension == ".xci")
- {
- Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
+ try
+ {
+ Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program);
+ Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control);
- pfs = xci.OpenPartition(XciPartitionType.Secure);
- }
- else
- {
- var pfsTemp = new PartitionFileSystem();
- pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
- pfs = pfsTemp;
+ BlitStruct<ApplicationControlProperty> controlHolder = new(1);
- // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
- bool hasMainNca = false;
+ IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
- {
- if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
- {
- using UniqueRef<IFile> ncaFile = new();
+ // Check if there is an update available.
+ if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs))
+ {
+ // Replace the original ControlFs by the updated one.
+ controlFs = updatedControlFs;
+ }
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ ReadControlData(controlFs, controlHolder.ByteSpan);
- Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
- int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
+ GetApplicationInformation(ref controlHolder.Value, ref applicationData);
- // Some main NCAs don't have a data partition, so check if the partition exists before opening it
- if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
- {
- hasMainNca = true;
+ // Read the icon from the ControlFS and store it as a byte array
+ try
+ {
+ using UniqueRef<IFile> icon = new();
- break;
- }
- }
- else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
- {
- isExeFs = true;
- }
- }
+ controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
- if (!hasMainNca && !isExeFs)
- {
- numApplicationsFound--;
+ using MemoryStream stream = new();
- continue;
- }
- }
+ icon.Get.AsStream().CopyTo(stream);
+ applicationData.Icon = stream.ToArray();
+ }
+ catch (HorizonResultException)
+ {
+ foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
+ {
+ if (entry.Name == "control.nacp")
+ {
+ continue;
+ }
- if (isExeFs)
- {
- applicationIcon = _nspIcon;
+ using var icon = new UniqueRef<IFile>();
- using UniqueRef<IFile> npdmFile = new();
+ controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
+ using MemoryStream stream = new();
- if (ResultFs.PathNotFound.Includes(result))
- {
- Npdm npdm = new(npdmFile.Get.AsStream());
+ icon.Get.AsStream().CopyTo(stream);
+ applicationData.Icon = stream.ToArray();
- titleName = npdm.TitleName;
- titleId = npdm.Aci0.TitleId.ToString("x16");
- }
- }
- else
- {
- GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId);
+ if (applicationData.Icon != null)
+ {
+ break;
+ }
+ }
- // Check if there is an update available.
- if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs))
- {
- // Replace the original ControlFs by the updated one.
- controlFs = updatedControlFs;
- }
+ applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon;
+ }
- ReadControlData(controlFs, controlHolder.ByteSpan);
+ applicationData.ControlHolder = controlHolder;
- GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version);
+ applications.Add(applicationData);
+ }
+ catch (MissingKeyException exception)
+ {
+ applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon;
- // Read the icon from the ControlFS and store it as a byte array
- try
- {
- using UniqueRef<IFile> icon = new();
+ Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
+ }
+ catch (InvalidDataException)
+ {
+ applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon;
- controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}");
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}");
+ }
+ }
- using MemoryStream stream = new();
+ return applications;
+ }
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
- }
- catch (HorizonResultException)
- {
- foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
- {
- if (entry.Name == "control.nacp")
- {
- continue;
- }
+ public bool TryGetApplicationsFromFile(string applicationPath, out List<ApplicationData> applications)
+ {
+ applications = [];
+
+ long fileSize = new FileInfo(applicationPath).Length;
- using var icon = new UniqueRef<IFile>();
+ BlitStruct<ApplicationControlProperty> controlHolder = new(1);
- controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ try
+ {
+ string extension = Path.GetExtension(applicationPath).ToLower();
- using MemoryStream stream = new();
+ using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
+ switch (extension)
+ {
+ case ".xci":
+ {
+ Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
- if (applicationIcon != null)
- {
- break;
- }
- }
+ applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath);
- applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
- }
- }
- }
- catch (MissingKeyException exception)
+ if (applications.Count == 0)
{
- applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
-
- Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
+ return false;
}
- catch (InvalidDataException)
- {
- applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
- Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}");
- }
- catch (Exception exception)
- {
- Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}");
+ break;
+ }
+ case ".nsp":
+ case ".pfs0":
+ {
+ var pfs = new PartitionFileSystem();
+ pfs.Initialize(file.AsStorage()).ThrowIfFailure();
- numApplicationsFound--;
+ ApplicationData result = GetApplicationFromNsp(pfs, applicationPath);
- continue;
+ if (result == null)
+ {
+ return false;
}
+
+ applications.Add(result);
+
+ break;
}
- else if (extension == ".nro")
+ case ".nro":
{
BinaryReader reader = new(file);
-
- byte[] Read(long position, int size)
- {
- file.Seek(position, SeekOrigin.Begin);
-
- return reader.ReadBytes(size);
- }
+ ApplicationData application = new();
try
{
@@ -356,46 +337,61 @@ namespace Ryujinx.UI.App.Common
// Reads and stores game icon as byte array
if (iconSize > 0)
{
- applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
+ application.Icon = Read(assetOffset + iconOffset, (int)iconSize);
}
else
{
- applicationIcon = _nroIcon;
+ application.Icon = _nroIcon;
}
// Read the NACP data
Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
- GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version);
+ GetApplicationInformation(ref controlHolder.Value, ref application);
}
else
{
- applicationIcon = _nroIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
+ application.Icon = _nroIcon;
+ application.Name = Path.GetFileNameWithoutExtension(applicationPath);
}
+
+ application.ControlHolder = controlHolder;
+ applications.Add(application);
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
- numApplicationsFound--;
+ return false;
+ }
- continue;
+ break;
+
+ byte[] Read(long position, int size)
+ {
+ file.Seek(position, SeekOrigin.Begin);
+
+ return reader.ReadBytes(size);
}
}
- else if (extension == ".nca")
+ case ".nca":
{
try
{
+ ApplicationData application = new();
+
Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage());
- int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
- if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
+ if (!nca.IsProgram() || nca.IsPatch())
{
- numApplicationsFound--;
-
- continue;
+ return false;
}
+
+ application.Icon = _ncaIcon;
+ application.Name = Path.GetFileNameWithoutExtension(applicationPath);
+ application.ControlHolder = controlHolder;
+
+ applications.Add(application);
}
catch (InvalidDataException)
{
@@ -405,78 +401,178 @@ namespace Ryujinx.UI.App.Common
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
- numApplicationsFound--;
-
- continue;
+ return false;
}
- applicationIcon = _ncaIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
+ break;
+ }
+ // If its an NSO we just set defaults
+ case ".nso":
+ {
+ ApplicationData application = new()
+ {
+ Icon = _nsoIcon,
+ Name = Path.GetFileNameWithoutExtension(applicationPath),
+ };
+
+ applications.Add(application);
+ break;
}
- // If its an NSO we just set defaults
- else if (extension == ".nso")
+ }
+ }
+ catch (IOException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, exception.Message);
+
+ return false;
+ }
+
+ foreach (var data in applications)
+ {
+ ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata =>
+ {
+ appMetadata.Title = data.Name;
+
+ // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
+ if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
+ {
+ appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
+ appMetadata.TimePlayedOld = default;
+ }
+
+ // Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
+ if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
+ {
+ // Migrate from string-based last_played to DateTime-based last_played_utc.
+ if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
{
- applicationIcon = _nsoIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
+ appMetadata.LastPlayed = lastPlayedOldParsed;
+
+ // Migration successful: deleting last_played from the metadata file.
+ appMetadata.LastPlayedOld = default;
}
+
}
- catch (IOException exception)
+ });
+
+ data.Favorite = appMetadata.Favorite;
+ data.TimePlayed = appMetadata.TimePlayed;
+ data.LastPlayed = appMetadata.LastPlayed;
+ data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper();
+ data.FileSize = fileSize;
+ data.Path = applicationPath;
+ }
+
+ return true;
+ }
+
+ public void CancelLoading()
+ {
+ _cancellationToken?.Cancel();
+ }
+
+ public static void ReadControlData(IFileSystem controlFs, Span<byte> outProperty)
+ {
+ using UniqueRef<IFile> controlFile = new();
+
+ controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
+ }
+
+ public void LoadApplications(List<string> appDirs, Language desiredTitleLanguage)
+ {
+ int numApplicationsFound = 0;
+ int numApplicationsLoaded = 0;
+
+ _desiredTitleLanguage = desiredTitleLanguage;
+
+ _cancellationToken = new CancellationTokenSource();
+
+ // Builds the applications list with paths to found applications
+ List<string> applicationPaths = new();
+
+ try
+ {
+ foreach (string appDir in appDirs)
+ {
+ if (_cancellationToken.Token.IsCancellationRequested)
{
- Logger.Warning?.Print(LogClass.Application, exception.Message);
+ return;
+ }
- numApplicationsFound--;
+ if (!Directory.Exists(appDir))
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The specified game directory \"{appDir}\" does not exist.");
continue;
}
- ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata =>
+ try
{
- appMetadata.Title = titleName;
-
- // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
- if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
+ IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", SearchOption.AllDirectories).Where(file =>
{
- appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
- appMetadata.TimePlayedOld = default;
- }
+ return
+ (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) ||
+ (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value) ||
+ (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value) ||
+ (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value) ||
+ (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value) ||
+ (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value);
+ });
- // Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
- if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
+ foreach (string app in files)
{
- // Migrate from string-based last_played to DateTime-based last_played_utc.
- if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+ if (_cancellationToken.Token.IsCancellationRequested)
{
- appMetadata.LastPlayed = lastPlayedOldParsed;
-
- // Migration successful: deleting last_played from the metadata file.
- appMetadata.LastPlayedOld = default;
+ return;
}
+ var fileInfo = new FileInfo(app);
+ string extension = fileInfo.Extension.ToLower();
+
+ if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso")
+ {
+ var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName;
+ applicationPaths.Add(fullPath);
+ numApplicationsFound++;
+ }
}
- });
+ }
+ catch (UnauthorizedAccessException)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\"");
+ }
+ }
- ApplicationData data = new()
+ // Loops through applications list, creating a struct and then firing an event containing the struct for each application
+ foreach (string applicationPath in applicationPaths)
+ {
+ if (_cancellationToken.Token.IsCancellationRequested)
{
- Favorite = appMetadata.Favorite,
- Icon = applicationIcon,
- TitleName = titleName,
- TitleId = titleId,
- Developer = developer,
- Version = version,
- TimePlayed = appMetadata.TimePlayed,
- LastPlayed = appMetadata.LastPlayed,
- FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(),
- FileSize = fileSize,
- Path = applicationPath,
- ControlHolder = controlHolder,
- };
-
- numApplicationsLoaded++;
-
- OnApplicationAdded(new ApplicationAddedEventArgs
+ return;
+ }
+
+ if (TryGetApplicationsFromFile(applicationPath, out List<ApplicationData> applications))
{
- AppData = data,
- });
+ foreach (var application in applications)
+ {
+ OnApplicationAdded(new ApplicationAddedEventArgs
+ {
+ AppData = application,
+ });
+ }
+
+ if (applications.Count > 1)
+ {
+ numApplicationsFound += applications.Count - 1;
+ }
+
+ numApplicationsLoaded += applications.Count;
+ }
+ else
+ {
+ numApplicationsFound--;
+ }
OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs
{
@@ -508,15 +604,6 @@ namespace Ryujinx.UI.App.Common
ApplicationCountUpdated?.Invoke(null, e);
}
- private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId)
- {
- (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0);
-
- // Return the ControlFS
- controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
- titleId = controlNca?.Header.TitleId.ToString("x16");
- }
-
public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
{
string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui");
@@ -554,10 +641,29 @@ namespace Ryujinx.UI.App.Common
return appMetadata;
}
- public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage)
+ public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong applicationId)
{
byte[] applicationIcon = null;
+ if (applicationId == 0)
+ {
+ if (Directory.Exists(applicationPath))
+ {
+ return _ncaIcon;
+ }
+
+ return Path.GetExtension(applicationPath).ToLower() switch
+ {
+ ".nsp" => _nspIcon,
+ ".pfs0" => _nspIcon,
+ ".xci" => _xciIcon,
+ ".nso" => _nsoIcon,
+ ".nro" => _nroIcon,
+ ".nca" => _ncaIcon,
+ _ => _ncaIcon,
+ };
+ }
+
try
{
// Look for icon only if applicationPath is not a directory
@@ -603,7 +709,16 @@ namespace Ryujinx.UI.App.Common
else
{
// Store the ControlFS in variable called controlFs
- GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _);
+ Dictionary<ulong, ContentMetaData> programs = pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel);
+ IFileSystem controlFs = null;
+
+ if (programs.TryGetValue(applicationId, out ContentMetaData value))
+ {
+ if (value.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca)
+ {
+ controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
+ }
+ }
// Read the icon from the ControlFS and store it as a byte array
try
@@ -630,16 +745,11 @@ namespace Ryujinx.UI.App.Common
controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- using (MemoryStream stream = new())
- {
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
- }
+ using MemoryStream stream = new();
+ icon.Get.AsStream().CopyTo(stream);
+ applicationIcon = stream.ToArray();
- if (applicationIcon != null)
- {
- break;
- }
+ break;
}
applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
@@ -722,80 +832,79 @@ namespace Ryujinx.UI.App.Common
return applicationIcon ?? _ncaIcon;
}
- private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
+ private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data)
{
_ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage)
{
- titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString();
- publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString();
+ data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString();
+ data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString();
}
else
{
- titleName = null;
- publisher = null;
+ data.Name = null;
+ data.Developer = null;
}
- if (string.IsNullOrWhiteSpace(titleName))
+ if (string.IsNullOrWhiteSpace(data.Name))
{
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
{
if (!controlTitle.NameString.IsEmpty())
{
- titleName = controlTitle.NameString.ToString();
+ data.Name = controlTitle.NameString.ToString();
break;
}
}
}
- if (string.IsNullOrWhiteSpace(publisher))
+ if (string.IsNullOrWhiteSpace(data.Developer))
{
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
{
if (!controlTitle.PublisherString.IsEmpty())
{
- publisher = controlTitle.PublisherString.ToString();
+ data.Developer = controlTitle.PublisherString.ToString();
break;
}
}
}
- if (controlData.PresenceGroupId != 0)
+ if (data.Id == 0)
{
- titleId = controlData.PresenceGroupId.ToString("x16");
- }
- else if (controlData.SaveDataOwnerId != 0)
- {
- titleId = controlData.SaveDataOwnerId.ToString();
- }
- else if (controlData.AddOnContentBaseId != 0)
- {
- titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
- }
- else
- {
- titleId = "0000000000000000";
+ if (controlData.SaveDataOwnerId != 0)
+ {
+ data.Id = controlData.SaveDataOwnerId;
+ }
+ else if (controlData.PresenceGroupId != 0)
+ {
+ data.Id = controlData.PresenceGroupId;
+ }
+ else if (controlData.AddOnContentBaseId != 0)
+ {
+ data.Id = (controlData.AddOnContentBaseId - 0x1000);
+ }
}
- version = controlData.DisplayVersionString.ToString();
+ data.Version = controlData.DisplayVersionString.ToString();
}
- private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs)
+ private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs)
{
updatedControlFs = null;
- string updatePath = "(unknown)";
+ string updatePath = null;
try
{
- (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath);
+ (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath);
if (patchNca != null && controlNca != null)
{
- updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
+ updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
return true;
}
@@ -811,120 +920,5 @@ namespace Ryujinx.UI.App.Common
return false;
}
-
- public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex)
- {
- Nca mainNca = null;
- Nca patchNca = null;
- Nca controlNca = null;
-
- fileSystem.ImportTickets(pfs);
-
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
- {
- using var ncaFile = new UniqueRef<IFile>();
-
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage());
-
- int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
-
- if (ncaProgramIndex != programIndex)
- {
- continue;
- }
-
- if (nca.Header.ContentType == NcaContentType.Program)
- {
- int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
-
- if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
- {
- patchNca = nca;
- }
- else
- {
- mainNca = nca;
- }
- }
- else if (nca.Header.ContentType == NcaContentType.Control)
- {
- controlNca = nca;
- }
- }
-
- return (mainNca, patchNca, controlNca);
- }
-
- public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex)
- {
- Nca patchNca = null;
- Nca controlNca = null;
-
- fileSystem.ImportTickets(pfs);
-
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
- {
- using var ncaFile = new UniqueRef<IFile>();
-
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage());
-
- int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
-
- if (ncaProgramIndex != programIndex)
- {
- continue;
- }
-
- if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId)
- {
- break;
- }
-
- if (nca.Header.ContentType == NcaContentType.Program)
- {
- patchNca = nca;
- }
- else if (nca.Header.ContentType == NcaContentType.Control)
- {
- controlNca = nca;
- }
- }
-
- return (patchNca, controlNca);
- }
-
- public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath)
- {
- updatePath = null;
-
- if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
- {
- // Clear the program index part.
- titleIdBase &= ~0xFUL;
-
- // Load update information if exists.
- string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
-
- if (File.Exists(titleUpdateMetadataPath))
- {
- updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
-
- if (File.Exists(updatePath))
- {
- FileStream file = new(updatePath, FileMode.Open, FileAccess.Read);
- PartitionFileSystem nsp = new();
- nsp.Initialize(file.AsStorage()).ThrowIfFailure();
-
- return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
- }
- }
- }
-
- return (null, null);
- }
}
}
diff --git a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
index bbacd5fe..ae0e4d90 100644
--- a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
+++ b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
@@ -14,6 +14,7 @@ namespace Ryujinx.UI.Common.Helper
public static string BaseDirPathArg { get; private set; }
public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; }
+ public static string LaunchApplicationId { get; private set; }
public static bool StartFullscreenArg { get; private set; }
public static void ParseArguments(string[] args)
@@ -72,6 +73,10 @@ namespace Ryujinx.UI.Common.Helper
OverrideGraphicsBackend = args[++i];
break;
+ case "-i":
+ case "--application-id":
+ LaunchApplicationId = args[++i];
+ break;
case "--docked-mode":
OverrideDockedMode = true;
break;
diff --git a/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
index c2085b28..58bdc90e 100644
--- a/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
+++ b/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
@@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Helper
public static class ShortcutHelper
{
[SupportedOSPlatform("windows")]
- private static void CreateShortcutWindows(string applicationFilePath, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
+ private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
iconPath += ".ico";
@@ -25,13 +25,13 @@ namespace Ryujinx.UI.Common.Helper
image.Mutate(x => x.Resize(128, 128));
SaveBitmapAsIcon(image, iconPath);
- var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0);
+ var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId), iconPath, 0);
shortcut.StringData.NameString = cleanedAppName;
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
}
[SupportedOSPlatform("linux")]
- private static void CreateShortcutLinux(string applicationFilePath, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName)
+ private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh");
var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.desktop");
@@ -41,11 +41,11 @@ namespace Ryujinx.UI.Common.Helper
image.SaveAsPng(iconPath);
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
- outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}");
+ outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId)}");
}
[SupportedOSPlatform("macos")]
- private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName)
+ private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
var plistFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.plist");
@@ -64,7 +64,7 @@ namespace Ryujinx.UI.Common.Helper
string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
using StreamWriter scriptFile = new(scriptPath);
- scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath));
+ scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId));
// Set execute permission
FileInfo fileInfo = new(scriptPath);
@@ -95,7 +95,7 @@ namespace Ryujinx.UI.Common.Helper
{
string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
- CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath);
+ CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath);
return;
}
@@ -105,14 +105,14 @@ namespace Ryujinx.UI.Common.Helper
string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
Directory.CreateDirectory(iconPath);
- CreateShortcutLinux(applicationFilePath, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
+ CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
return;
}
if (OperatingSystem.IsMacOS())
{
- CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName);
+ CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName);
return;
}
@@ -120,7 +120,7 @@ namespace Ryujinx.UI.Common.Helper
throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
}
- private static string GetArgsString(string appFilePath)
+ private static string GetArgsString(string appFilePath, string applicationId)
{
// args are first defined as a list, for easier adjustments in the future
var argsList = new List<string>();
@@ -131,6 +131,12 @@ namespace Ryujinx.UI.Common.Helper
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
}
+ if (appFilePath.ToLower().EndsWith(".xci"))
+ {
+ argsList.Add("--application-id");
+ argsList.Add($"\"{applicationId}\"");
+ }
+
argsList.Add($"\"{appFilePath}\"");
return String.Join(" ", argsList);
diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs
index 7004908a..8c643f34 100644
--- a/src/Ryujinx/AppHost.cs
+++ b/src/Ryujinx/AppHost.cs
@@ -132,12 +132,14 @@ namespace Ryujinx.Ava
public int Width { get; private set; }
public int Height { get; private set; }
public string ApplicationPath { get; private set; }
+ public ulong ApplicationId { get; private set; }
public bool ScreenshotRequested { get; set; }
public AppHost(
RendererHost renderer,
InputManager inputManager,
string applicationPath,
+ ulong applicationId,
VirtualFileSystem virtualFileSystem,
ContentManager contentManager,
AccountManager accountManager,
@@ -161,6 +163,7 @@ namespace Ryujinx.Ava
NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager();
ApplicationPath = applicationPath;
+ ApplicationId = applicationId;
VirtualFileSystem = virtualFileSystem;
ContentManager = contentManager;
@@ -719,7 +722,7 @@ namespace Ryujinx.Ava
{
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
- if (!Device.LoadXci(ApplicationPath))
+ if (!Device.LoadXci(ApplicationPath, ApplicationId))
{
Device.Dispose();
@@ -746,7 +749,7 @@ namespace Ryujinx.Ava
{
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
- if (!Device.LoadNsp(ApplicationPath))
+ if (!Device.LoadNsp(ApplicationPath, ApplicationId))
{
Device.Dispose();
diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json
index 8df0f96a..74e18056 100644
--- a/src/Ryujinx/Assets/Locales/en_US.json
+++ b/src/Ryujinx/Assets/Locales/en_US.json
@@ -10,6 +10,7 @@
"SettingsTabSystemUseHypervisor": "Use Hypervisor",
"MenuBarFile": "_File",
"MenuBarFileOpenFromFile": "_Load Application From File",
+ "MenuBarFileOpenFromFileError": "No applications found in selected file.",
"MenuBarFileOpenUnpacked": "Load _Unpacked Game",
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
"MenuBarFileOpenLogsFolder": "Open Logs Folder",
@@ -649,6 +650,8 @@
"OpenSetupGuideMessage": "Open the Setup Guide",
"NoUpdate": "No Update",
"TitleUpdateVersionLabel": "Version {0}",
+ "TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
+ "TitleBundledDlcLabel": "Bundled:",
"RyujinxInfo": "Ryujinx - Info",
"RyujinxConfirm": "Ryujinx - Confirmation",
"FileDialogAllTypes": "All types",
diff --git a/src/Ryujinx/Common/ApplicationHelper.cs b/src/Ryujinx/Common/ApplicationHelper.cs
index 622a6a02..14773114 100644
--- a/src/Ryujinx/Common/ApplicationHelper.cs
+++ b/src/Ryujinx/Common/ApplicationHelper.cs
@@ -18,7 +18,8 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Account.Acc;
-using Ryujinx.UI.App.Common;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using System;
using System.Buffers;
@@ -226,7 +227,11 @@ namespace Ryujinx.Ava.Common
return;
}
- (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
+ (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
if (updatePatchNca != null)
{
patchNca = updatePatchNca;
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 4f68ca24..97696342 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -125,7 +125,7 @@ namespace Ryujinx.Ava
if (CommandLineState.LaunchPathArg != null)
{
- MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
+ MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.LaunchApplicationId, CommandLineState.StartFullscreenArg);
}
}
diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs
index 894ac6c1..5edd0230 100644
--- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs
+++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs
@@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
-using Avalonia.Threading;
using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common;
@@ -15,7 +14,6 @@ using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common.Helper;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using Path = System.IO.Path;
@@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls
{
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
- ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata =>
+ ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata =>
{
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
});
@@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls
{
if (viewModel?.SelectedApplication != null)
{
- if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
- {
- Dispatcher.UIThread.InvokeAsync(async () =>
- {
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
- });
-
- return;
- }
-
- var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
+ var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default);
- ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName);
+ ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name);
}
}
@@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
+ await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
}
}
@@ -108,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
+ await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
}
}
@@ -120,8 +108,8 @@ namespace Ryujinx.Ava.UI.Controls
{
await new CheatWindow(
viewModel.VirtualFileSystem,
- viewModel.SelectedApplication.TitleId,
- viewModel.SelectedApplication.TitleName,
+ viewModel.SelectedApplication.IdString,
+ viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
}
}
@@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
string modsBasePath = ModLoader.GetModsBasePath();
- string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.TitleId);
+ string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.IdString);
OpenHelper.OpenFolder(titleModsPath);
}
@@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
string sdModsBasePath = ModLoader.GetSdModsBasePath();
- string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.TitleId);
+ string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.IdString);
OpenHelper.OpenFolder(titleModsPath);
}
@@ -158,7 +146,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- await ModManagerWindow.Show(ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
+ await ModManagerWindow.Show(viewModel.SelectedApplication.Id, viewModel.SelectedApplication.Name);
}
}
@@ -170,15 +158,15 @@ namespace Ryujinx.Ava.UI.Controls
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogWarning],
- LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes)
{
- DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0"));
- DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1"));
+ DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
+ DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
List<FileInfo> cacheFiles = new();
@@ -218,14 +206,14 @@ namespace Ryujinx.Ava.UI.Controls
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogWarning],
- LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes)
{
- DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"));
+ DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"));
List<DirectoryInfo> oldCacheDirectories = new();
List<FileInfo> newCacheFiles = new();
@@ -273,7 +261,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu");
+ string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu");
string mainDir = Path.Combine(ptcDir, "0");
string backupDir = Path.Combine(ptcDir, "1");
@@ -294,7 +282,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader");
+ string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader");
if (!Directory.Exists(shaderCacheDir))
{
@@ -315,7 +303,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider,
NcaSectionType.Code,
viewModel.SelectedApplication.Path,
- viewModel.SelectedApplication.TitleName);
+ viewModel.SelectedApplication.Name);
}
}
@@ -329,7 +317,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider,
NcaSectionType.Data,
viewModel.SelectedApplication.Path,
- viewModel.SelectedApplication.TitleName);
+ viewModel.SelectedApplication.Name);
}
}
@@ -343,7 +331,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider,
NcaSectionType.Logo,
viewModel.SelectedApplication.Path,
- viewModel.SelectedApplication.TitleName);
+ viewModel.SelectedApplication.Name);
}
}
@@ -354,7 +342,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
ApplicationData selectedApplication = viewModel.SelectedApplication;
- ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon);
+ ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon);
}
}
@@ -364,7 +352,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
+ await viewModel.LoadApplication(viewModel.SelectedApplication);
}
}
}
diff --git a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml
index 2dc95662..98a1c004 100644
--- a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml
@@ -80,7 +80,7 @@
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
- Text="{Binding TitleName}"
+ Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap" />
</Panel>
diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml
index fecf0888..f99cf316 100644
--- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml
@@ -85,7 +85,7 @@
<TextBlock
HorizontalAlignment="Stretch"
FontWeight="Bold"
- Text="{Binding TitleName}"
+ Text="{Binding Name}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
@@ -109,7 +109,7 @@
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
- Text="{Binding TitleId}"
+ Text="{Binding Id, StringFormat=X16}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
diff --git a/src/Ryujinx/UI/Models/DownloadableContentModel.cs b/src/Ryujinx/UI/Models/DownloadableContentModel.cs
index 9e400441..1409d971 100644
--- a/src/Ryujinx/UI/Models/DownloadableContentModel.cs
+++ b/src/Ryujinx/UI/Models/DownloadableContentModel.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using System.IO;
@@ -24,6 +25,9 @@ namespace Ryujinx.Ava.UI.Models
public string FileName => Path.GetFileName(ContainerPath);
+ public string Label =>
+ Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName;
+
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
{
TitleId = titleId;
diff --git a/src/Ryujinx/UI/Models/SaveModel.cs b/src/Ryujinx/UI/Models/SaveModel.cs
index d6dea2f6..181295b0 100644
--- a/src/Ryujinx/UI/Models/SaveModel.cs
+++ b/src/Ryujinx/UI/Models/SaveModel.cs
@@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models
TitleId = info.ProgramId;
UserId = info.UserId;
- var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString);
+ var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString);
InGameList = appData != null;
if (InGameList)
{
Icon = appData.Icon;
- Title = appData.TitleName;
+ Title = appData.Name;
}
else
{
diff --git a/src/Ryujinx/UI/Models/TitleUpdateModel.cs b/src/Ryujinx/UI/Models/TitleUpdateModel.cs
index c270c9ed..cde37bf9 100644
--- a/src/Ryujinx/UI/Models/TitleUpdateModel.cs
+++ b/src/Ryujinx/UI/Models/TitleUpdateModel.cs
@@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models
public ApplicationControlProperty Control { get; }
public string Path { get; }
- public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString());
+ public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(
+ System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel,
+ Control.DisplayVersionString.ToString()
+ );
public TitleUpdateModel(ApplicationControlProperty control, string path)
{
diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs
index 2cd714f4..0f500513 100644
--- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs
@@ -6,7 +6,6 @@ using DynamicData;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
-using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
@@ -17,11 +16,13 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
+using Ryujinx.UI.App.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Threading.Tasks;
using Application = Avalonia.Application;
using Path = System.IO.Path;
@@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
private string _search;
- private readonly ulong _titleId;
+ private readonly ApplicationData _applicationData;
private readonly IStorageProvider _storageProvider;
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@@ -91,18 +92,25 @@ namespace Ryujinx.Ava.UI.ViewModels
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
}
- public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
+ public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{
_virtualFileSystem = virtualFileSystem;
- _titleId = titleId;
+ _applicationData = applicationData;
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
_storageProvider = desktop.MainWindow.StorageProvider;
}
- _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
+ _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json");
+
+ if (!File.Exists(_downloadableContentJsonPath))
+ {
+ _downloadableContentContainerList = new List<DownloadableContentContainer>();
+
+ Save();
+ }
try
{
@@ -123,12 +131,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
if (File.Exists(downloadableContentContainer.ContainerPath))
{
- using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
-
- PartitionFileSystem partitionFileSystem = new();
- partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
-
- _virtualFileSystem.ImportTickets(partitionFileSystem);
+ using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, _virtualFileSystem);
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
@@ -157,6 +160,9 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
+ // NOTE: Try to load downloadable contents from PFS last to preserve enabled state.
+ AddDownloadableContent(_applicationData.Path);
+
// NOTE: Save the list again to remove leftovers.
Save();
Sort();
@@ -219,25 +225,23 @@ namespace Ryujinx.Ava.UI.ViewModels
foreach (var file in result)
{
- await AddDownloadableContent(file.Path.LocalPath);
+ if (!AddDownloadableContent(file.Path.LocalPath))
+ {
+ await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
+ }
}
}
- private async Task AddDownloadableContent(string path)
+ private bool AddDownloadableContent(string path)
{
- if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
+ if (!File.Exists(path) || _downloadableContentContainerList.Any(x => x.ContainerPath == path))
{
- return;
+ return true;
}
- using FileStream containerFile = File.OpenRead(path);
-
- PartitionFileSystem partitionFileSystem = new();
- partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
- bool containsDownloadableContent = false;
-
- _virtualFileSystem.ImportTickets(partitionFileSystem);
+ using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
+ bool success = false;
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
@@ -252,26 +256,26 @@ namespace Ryujinx.Ava.UI.ViewModels
if (nca.Header.ContentType == NcaContentType.PublicData)
{
- if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
+ if (nca.GetProgramIdBase() != _applicationData.IdBase)
{
- break;
+ continue;
}
var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true);
DownloadableContents.Add(content);
SelectedDownloadableContents.Add(content);
- OnPropertyChanged(nameof(UpdateCount));
- Sort();
-
- containsDownloadableContent = true;
+ success = true;
}
}
- if (!containsDownloadableContent)
+ if (success)
{
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
+ OnPropertyChanged(nameof(UpdateCount));
+ Sort();
}
+
+ return success;
}
public void Remove(DownloadableContentModel model)
diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
index b47cc4b7..134e9030 100644
--- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
@@ -96,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private bool _canUpdate = true;
private Cursor _cursor;
private string _title;
- private string _currentEmulatedGamePath;
+ private ApplicationData _currentApplicationData;
private readonly AutoResetEvent _rendererWaitEvent;
private WindowState _windowState;
private double _windowWidth;
@@ -108,7 +108,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public ApplicationData ListSelectedApplication;
public ApplicationData GridSelectedApplication;
- private string TitleName { get; set; }
internal AppHost AppHost { get; set; }
public MainWindowViewModel()
@@ -954,8 +953,8 @@ namespace Ryujinx.Ava.UI.ViewModels
return SortMode switch
{
#pragma warning disable IDE0055 // Disable formatting
- ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName)
- : SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName),
+ ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Name)
+ : SortExpressionComparer<ApplicationData>.Descending(app => app.Name),
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
@@ -999,7 +998,7 @@ namespace Ryujinx.Ava.UI.ViewModels
CompareInfo compareInfo = CultureInfo.CurrentCulture.CompareInfo;
- return compareInfo.IndexOf(app.TitleName, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0;
+ return compareInfo.IndexOf(app.Name, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0;
}
return false;
@@ -1128,7 +1127,7 @@ namespace Ryujinx.Ava.UI.ViewModels
IsLoadingIndeterminate = false;
break;
case LoadState.Loaded:
- LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
+ LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
IsLoadingIndeterminate = true;
CacheLoadStatus = "";
break;
@@ -1148,7 +1147,7 @@ namespace Ryujinx.Ava.UI.ViewModels
IsLoadingIndeterminate = false;
break;
case ShaderCacheLoadingState.Loaded:
- LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
+ LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
IsLoadingIndeterminate = true;
CacheLoadStatus = "";
break;
@@ -1200,13 +1199,13 @@ namespace Ryujinx.Ava.UI.ViewModels
{
UserChannelPersistence.ShouldRestart = false;
- await LoadApplication(_currentEmulatedGamePath);
+ await LoadApplication(_currentApplicationData);
}
else
{
// Otherwise, clear state.
UserChannelPersistence = new UserChannelPersistence();
- _currentEmulatedGamePath = null;
+ _currentApplicationData = null;
}
}
@@ -1493,7 +1492,15 @@ namespace Ryujinx.Ava.UI.ViewModels
if (result.Count > 0)
{
- await LoadApplication(result[0].Path.LocalPath);
+ if (ApplicationLibrary.TryGetApplicationsFromFile(result[0].Path.LocalPath,
+ out List<ApplicationData> applications))
+ {
+ await LoadApplication(applications[0]);
+ }
+ else
+ {
+ await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.MenuBarFileOpenFromFileError]);
+ }
}
}
@@ -1507,11 +1514,17 @@ namespace Ryujinx.Ava.UI.ViewModels
if (result.Count > 0)
{
- await LoadApplication(result[0].Path.LocalPath);
+ ApplicationData applicationData = new()
+ {
+ Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath),
+ Path = result[0].Path.LocalPath,
+ };
+
+ await LoadApplication(applicationData);
}
}
- public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "")
+ public async Task LoadApplication(ApplicationData application, bool startFullscreen = false)
{
if (AppHost != null)
{
@@ -1531,7 +1544,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.RestartTime();
- SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language);
+ SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);
PrepareLoadScreen();
@@ -1540,7 +1553,8 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost = new AppHost(
RendererHostControl,
InputManager,
- path,
+ application.Path,
+ application.Id,
VirtualFileSystem,
ContentManager,
AccountManager,
@@ -1558,17 +1572,17 @@ namespace Ryujinx.Ava.UI.ViewModels
CanUpdate = false;
- LoadHeading = TitleName = titleName;
+ LoadHeading = application.Name;
- if (string.IsNullOrWhiteSpace(titleName))
+ if (string.IsNullOrWhiteSpace(application.Name))
{
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
- TitleName = AppHost.Device.Processes.ActiveApplication.Name;
+ application.Name = AppHost.Device.Processes.ActiveApplication.Name;
}
SwitchToRenderer(startFullscreen);
- _currentEmulatedGamePath = path;
+ _currentApplicationData = application;
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
gameThread.Start();
diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs
index 5989ce09..6c38edb3 100644
--- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs
@@ -1,4 +1,3 @@
-using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage;
@@ -6,7 +5,7 @@ using Avalonia.Threading;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
-using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
@@ -17,12 +16,17 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Application = Avalonia.Application;
+using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers;
@@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public TitleUpdateMetadata TitleUpdateWindowData;
public readonly string TitleUpdateJsonPath;
private VirtualFileSystem VirtualFileSystem { get; }
- private ulong TitleId { get; }
+ private ApplicationData ApplicationData { get; }
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
private AvaloniaList<object> _views = new();
@@ -73,18 +77,18 @@ namespace Ryujinx.Ava.UI.ViewModels
public IStorageProvider StorageProvider;
- public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
+ public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{
VirtualFileSystem = virtualFileSystem;
- TitleId = titleId;
+ ApplicationData = applicationData;
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
StorageProvider = desktop.MainWindow.StorageProvider;
}
- TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
+ TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json");
try
{
@@ -92,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
catch
{
- Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}");
+ Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}");
TitleUpdateWindowData = new TitleUpdateMetadata
{
@@ -108,6 +112,9 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadUpdates()
{
+ // Try to load updates from PFS first
+ AddUpdate(ApplicationData.Path, true);
+
foreach (string path in TitleUpdateWindowData.Paths)
{
AddUpdate(path);
@@ -162,38 +169,54 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
- private void AddUpdate(string path)
+ private void AddUpdate(string path, bool ignoreNotFound = false)
{
- if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
+ if (!File.Exists(path) || TitleUpdates.Any(x => x.Path == path))
+ {
+ return;
+ }
+
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
+ try
{
- using FileStream file = new(path, FileMode.Open, FileAccess.Read);
+ using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, VirtualFileSystem);
+
+ Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.Patch, VirtualFileSystem, checkLevel);
+
+ Nca patchNca = null;
+ Nca controlNca = null;
- try
+ if (updates.TryGetValue(ApplicationData.Id, out ContentMetaData content))
{
- var pfs = new PartitionFileSystem();
- pfs.Initialize(file.AsStorage()).ThrowIfFailure();
- (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0);
+ patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program);
+ controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control);
+ }
- if (controlNca != null && patchNca != null)
- {
- ApplicationControlProperty controlData = new();
+ if (controlNca != null && patchNca != null)
+ {
+ ApplicationControlProperty controlData = new();
- using UniqueRef<IFile> nacpFile = new();
+ using UniqueRef<IFile> nacpFile = new();
- controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
- nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
+ controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
- TitleUpdates.Add(new TitleUpdateModel(controlData, path));
- }
- else
+ TitleUpdates.Add(new TitleUpdateModel(controlData, path));
+ }
+ else
+ {
+ if (!ignoreNotFound)
{
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
}
}
- catch (Exception ex)
- {
- Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
- }
+ }
+ catch (Exception ex)
+ {
+ Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
}
}
diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
index 522ac19b..73ae0df1 100644
--- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
+++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
@@ -11,6 +11,7 @@ using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Utilities;
using Ryujinx.Modules;
+using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
@@ -134,7 +135,14 @@ namespace Ryujinx.Ava.UI.Views.Main
if (!string.IsNullOrEmpty(contentPath))
{
- await ViewModel.LoadApplication(contentPath, false, "Mii Applet");
+ ApplicationData applicationData = new()
+ {
+ Name = "miiEdit",
+ Id = 0x0100000000001009,
+ Path = contentPath,
+ };
+
+ await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen);
}
}
diff --git a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs
index d78e48a4..8f4c3ceb 100644
--- a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs
@@ -1,9 +1,11 @@
using Avalonia.Collections;
+using LibHac.Tools.FsSystem;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Configuration;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -34,9 +36,12 @@ namespace Ryujinx.Ava.UI.Windows
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
{
LoadedCheats = new AvaloniaList<CheatNode>();
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
- BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath);
+ BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath);
InitializeComponent();
diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml
index 99cf28e7..98aac09c 100644
--- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml
+++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml
@@ -97,7 +97,7 @@
MaxLines="2"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
- Text="{Binding FileName}" />
+ Text="{Binding Label}" />
<TextBlock
Grid.Column="1"
Margin="10 0"
diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs
index b9e2c7be..72cd9631 100644
--- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs
@@ -3,13 +3,12 @@ using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common.Helper;
using System.Threading.Tasks;
-using Button = Avalonia.Controls.Button;
namespace Ryujinx.Ava.UI.Windows
{
@@ -24,22 +23,22 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent();
}
- public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId)
+ public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{
- DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId);
+ DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData);
InitializeComponent();
}
- public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
+ public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = "",
SecondaryButtonText = "",
CloseButtonText = "",
- Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId),
- Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")),
+ Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData),
+ Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdString),
};
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
index 7de8a49a..dc5336ab 100644
--- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
@@ -5,6 +5,7 @@ using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
+using LibHac.Tools.FsSystem;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
@@ -24,7 +25,7 @@ using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using System;
-using System.IO;
+using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
@@ -40,6 +41,7 @@ namespace Ryujinx.Ava.UI.Windows
private UserChannelPersistence _userChannelPersistence;
private static bool _deferLoad;
private static string _launchPath;
+ private static string _launchApplicationId;
private static bool _startFullscreen;
internal readonly AvaHostUIHandler UiHandler;
@@ -168,18 +170,17 @@ namespace Ryujinx.Ava.UI.Windows
{
ViewModel.SelectedIcon = args.Application.Icon;
- string path = new FileInfo(args.Application.Path).FullName;
-
- ViewModel.LoadApplication(path).Wait();
+ ViewModel.LoadApplication(args.Application).Wait();
}
args.Handled = true;
}
- internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg)
+ internal static void DeferLoadApplication(string launchPathArg, string launchApplicationId, bool startFullscreenArg)
{
_deferLoad = true;
_launchPath = launchPathArg;
+ _launchApplicationId = launchApplicationId;
_startFullscreen = startFullscreenArg;
}
@@ -219,7 +220,11 @@ namespace Ryujinx.Ava.UI.Windows
LibHacHorizonManager.InitializeBcatServer();
LibHacHorizonManager.InitializeSystemClients();
- ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem);
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
+ ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel);
// Save data created before we supported extra data in directory save data will not work properly if
// given empty extra data. Luckily some of that extra data can be created using the data from the
@@ -314,7 +319,35 @@ namespace Ryujinx.Ava.UI.Windows
{
_deferLoad = false;
- await ViewModel.LoadApplication(_launchPath, _startFullscreen);
+ if (ApplicationLibrary.TryGetApplicationsFromFile(_launchPath, out List<ApplicationData> applications))
+ {
+ ApplicationData applicationData;
+
+ if (_launchApplicationId != null)
+ {
+ applicationData = applications.Find(application => application.IdString == _launchApplicationId);
+
+ if (applicationData != null)
+ {
+ await ViewModel.LoadApplication(applicationData, _startFullscreen);
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Couldn't find requested application id '{_launchApplicationId}' in '{_launchPath}'.");
+ await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.ApplicationNotFound));
+ }
+ }
+ else
+ {
+ applicationData = applications[0];
+ await ViewModel.LoadApplication(applicationData, _startFullscreen);
+ }
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Couldn't find any application in '{_launchPath}'.");
+ await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.ApplicationNotFound));
+ }
}
}
else
diff --git a/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs
index f5e25032..8de5cb14 100644
--- a/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs
@@ -5,19 +5,18 @@ using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common.Helper;
using System.Threading.Tasks;
-using Button = Avalonia.Controls.Button;
namespace Ryujinx.Ava.UI.Windows
{
public partial class TitleUpdateWindow : UserControl
{
- public TitleUpdateViewModel ViewModel;
+ public readonly TitleUpdateViewModel ViewModel;
public TitleUpdateWindow()
{
@@ -26,22 +25,22 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent();
}
- public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId)
+ public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{
- DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId);
+ DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData);
InitializeComponent();
}
- public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
+ public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = "",
SecondaryButtonText = "",
CloseButtonText = "",
- Content = new TitleUpdateWindow(virtualFileSystem, titleId),
- Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")),
+ Content = new TitleUpdateWindow(virtualFileSystem, applicationData),
+ Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString),
};
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());