Add support for multi game XCIs (second try) (#6515)
* Add default values to ApplicationData directly * Refactor application loading It should now be possible to load multi game XCIs. Included updates won't be detected for now. Opening a game from the command line currently only opens the first one. * Only include program NCAs where at least one tuple item is not null * Get application data by title id and add programIndex check back * Refactor application loading again and remove duplicate code * Actually use patch ncas for updates * Fix number of applications found with multi game xcis * Don't load bundled updates from multi game xcis * Change ApplicationData.TitleId type to ulong & Add TitleIdString property * Use cnmt files and ContentCollection to load programs * Ava: Add updates and DLCs from gamecarts * Get the cnmt file from its NCA * Ava: Identify bundled updates in updater window * Fix the (hopefully) last few bugs * Add idOffset parameter to GetNcaByType * Handle missing file for dlc.json * Ava: Shorten error message for invalid files * Gtk: Add additional string for bundled updates in TitleUpdateWindow * Hopefully fix DLC issues * Apply formatting * Finally fix DLC issues * Adjust property names and fileSize field * Read the correct update file * Fix wrong casing for application id strings * Rename TitleId to ApplicationId * Address review comments * Apply suggestions from code review Co-authored-by: gdkchan <gab.dark.100@gmail.com> * Gracefully fail when loading pfs for update and dlc window * Fix applications with multiple programs * Fix DLCWindow crash on GTK * Fix some GUI issues * Remove IsXci again * Don't add duplicates to update/dlc windows * Avoid double lookup * Preserve DLC enabled state for bundled DLCs * Fix DLCWindow not opening using GTK * Fix missing information when loading applications from file * Address review feedback Rename ContentCollection to ContentMetaData Fix casing issues in log messages Use null as the default value for updatePath * Fix re-adding bundled DLCs every time * Fix bundled DLCs disappearing * Abstract common code to open application pfs * Remove unused imports * Fix file exists check when loading DLCs * Load bundled DLCs only using dlc.json * Load AoC items correctly * Add all DLCs from a PFS * Add argument to launch a specific application id * Use application-id argument for shortcuts if necessary * Return the application id from the control NCA if possible * GetApplicationInformation: Don't overwrite application ids Move SaveDataOwnerId check to the top, since it seems to be more reliable. * Get application ids from CNMT again This commit reverts some parts of 61615b8f0d6f90ae86778958ddc38eaf6dc280ab. Since the issue wasn't actually related to the application id in CMNTs, we can remove the wrong assumptions. * Revert erroneous axaml change from adca8900 * Rename title to application * Wrap nsp/pfs0 case with curly braces * Check if _applicationData.ControlHolder.ByteSpan is zeros only once * Catch exceptions while loading applications from nsps --------- Co-authored-by: gdkchan <gab.dark.100@gmail.com>
This commit is contained in:
parent
344f4f52c1
commit
6fbf279fac
38 changed files with 1299 additions and 961 deletions
|
@ -7,6 +7,7 @@ using Ryujinx.Common.SystemInterop;
|
||||||
using Ryujinx.Modules;
|
using Ryujinx.Modules;
|
||||||
using Ryujinx.SDL2.Common;
|
using Ryujinx.SDL2.Common;
|
||||||
using Ryujinx.UI;
|
using Ryujinx.UI;
|
||||||
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common;
|
using Ryujinx.UI.Common;
|
||||||
using Ryujinx.UI.Common.Configuration;
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
|
@ -322,7 +323,35 @@ namespace Ryujinx
|
||||||
|
|
||||||
if (CommandLineState.LaunchPathArg != null)
|
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))
|
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
|
||||||
|
|
|
@ -37,7 +37,9 @@ using Ryujinx.UI.Windows;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
using SPB.Graphics.Vulkan;
|
using SPB.Graphics.Vulkan;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -60,7 +62,6 @@ namespace Ryujinx.UI
|
||||||
|
|
||||||
private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
|
private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
|
||||||
|
|
||||||
private readonly ApplicationLibrary _applicationLibrary;
|
|
||||||
private readonly GtkHostUIHandler _uiHandler;
|
private readonly GtkHostUIHandler _uiHandler;
|
||||||
private readonly AutoResetEvent _deviceExitStatus;
|
private readonly AutoResetEvent _deviceExitStatus;
|
||||||
private readonly ListStore _tableStore;
|
private readonly ListStore _tableStore;
|
||||||
|
@ -69,11 +70,12 @@ namespace Ryujinx.UI
|
||||||
private bool _gameLoaded;
|
private bool _gameLoaded;
|
||||||
private bool _ending;
|
private bool _ending;
|
||||||
|
|
||||||
private string _currentEmulatedGamePath = null;
|
private ApplicationData _currentApplicationData = null;
|
||||||
|
|
||||||
private string _lastScannedAmiiboId = "";
|
private string _lastScannedAmiiboId = "";
|
||||||
private bool _lastScannedAmiiboShowAll = false;
|
private bool _lastScannedAmiiboShowAll = false;
|
||||||
|
|
||||||
|
public readonly ApplicationLibrary ApplicationLibrary;
|
||||||
public RendererWidgetBase RendererWidget;
|
public RendererWidgetBase RendererWidget;
|
||||||
public InputManager InputManager;
|
public InputManager InputManager;
|
||||||
|
|
||||||
|
@ -180,8 +182,12 @@ namespace Ryujinx.UI
|
||||||
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
|
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
|
||||||
_userChannelPersistence = new UserChannelPersistence();
|
_userChannelPersistence = new UserChannelPersistence();
|
||||||
|
|
||||||
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
// Instantiate GUI objects.
|
// Instantiate GUI objects.
|
||||||
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem);
|
ApplicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel);
|
||||||
_uiHandler = new GtkHostUIHandler(this);
|
_uiHandler = new GtkHostUIHandler(this);
|
||||||
_deviceExitStatus = new AutoResetEvent(false);
|
_deviceExitStatus = new AutoResetEvent(false);
|
||||||
|
|
||||||
|
@ -190,8 +196,8 @@ namespace Ryujinx.UI
|
||||||
FocusInEvent += MainWindow_FocusInEvent;
|
FocusInEvent += MainWindow_FocusInEvent;
|
||||||
FocusOutEvent += MainWindow_FocusOutEvent;
|
FocusOutEvent += MainWindow_FocusOutEvent;
|
||||||
|
|
||||||
_applicationLibrary.ApplicationAdded += Application_Added;
|
ApplicationLibrary.ApplicationAdded += Application_Added;
|
||||||
_applicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
|
ApplicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
|
||||||
|
|
||||||
_fileMenu.StateChanged += FileMenu_StateChanged;
|
_fileMenu.StateChanged += FileMenu_StateChanged;
|
||||||
_actionMenu.StateChanged += ActionMenu_StateChanged;
|
_actionMenu.StateChanged += ActionMenu_StateChanged;
|
||||||
|
@ -732,7 +738,7 @@ namespace Ryujinx.UI
|
||||||
|
|
||||||
Thread applicationLibraryThread = new(() =>
|
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;
|
_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();
|
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
|
||||||
|
|
||||||
|
@ -857,7 +863,7 @@ namespace Ryujinx.UI
|
||||||
case ".xci":
|
case ".xci":
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
||||||
|
|
||||||
return _emulationContext.LoadXci(path);
|
return _emulationContext.LoadXci(path, applicationId);
|
||||||
case ".nca":
|
case ".nca":
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
|
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
|
||||||
|
|
||||||
|
@ -866,7 +872,7 @@ namespace Ryujinx.UI
|
||||||
case ".pfs0":
|
case ".pfs0":
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
||||||
|
|
||||||
return _emulationContext.LoadNsp(path);
|
return _emulationContext.LoadNsp(path, applicationId);
|
||||||
default:
|
default:
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
|
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
|
||||||
try
|
try
|
||||||
|
@ -887,7 +893,7 @@ namespace Ryujinx.UI
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RunApplication(string path, bool startFullscreen = false)
|
public void RunApplication(ApplicationData application, bool startFullscreen = false)
|
||||||
{
|
{
|
||||||
if (_gameLoaded)
|
if (_gameLoaded)
|
||||||
{
|
{
|
||||||
|
@ -909,14 +915,14 @@ namespace Ryujinx.UI
|
||||||
|
|
||||||
bool isFirmwareTitle = false;
|
bool isFirmwareTitle = false;
|
||||||
|
|
||||||
if (path.StartsWith("@SystemContent"))
|
if (application.Path.StartsWith("@SystemContent"))
|
||||||
{
|
{
|
||||||
path = VirtualFileSystem.SwitchPathToSystemPath(path);
|
application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path);
|
||||||
|
|
||||||
isFirmwareTitle = true;
|
isFirmwareTitle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LoadApplication(path, isFirmwareTitle))
|
if (!LoadApplication(application.Path, application.Id, isFirmwareTitle))
|
||||||
{
|
{
|
||||||
_emulationContext.Dispose();
|
_emulationContext.Dispose();
|
||||||
SwitchToGameTable();
|
SwitchToGameTable();
|
||||||
|
@ -926,7 +932,7 @@ namespace Ryujinx.UI
|
||||||
|
|
||||||
SetupProgressUIHandlers();
|
SetupProgressUIHandlers();
|
||||||
|
|
||||||
_currentEmulatedGamePath = path;
|
_currentApplicationData = application;
|
||||||
|
|
||||||
_deviceExitStatus.Reset();
|
_deviceExitStatus.Reset();
|
||||||
|
|
||||||
|
@ -1165,7 +1171,7 @@ namespace Ryujinx.UI
|
||||||
_tableStore.AppendValues(
|
_tableStore.AppendValues(
|
||||||
args.AppData.Favorite,
|
args.AppData.Favorite,
|
||||||
new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
|
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.Developer,
|
||||||
args.AppData.Version,
|
args.AppData.Version,
|
||||||
args.AppData.TimePlayedString,
|
args.AppData.TimePlayedString,
|
||||||
|
@ -1253,9 +1259,22 @@ namespace Ryujinx.UI
|
||||||
{
|
{
|
||||||
_gameTableSelection.GetSelected(out TreeIter treeIter);
|
_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)
|
private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
|
||||||
|
@ -1313,13 +1332,22 @@ namespace Ryujinx.UI
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString();
|
ApplicationData application = new()
|
||||||
string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0];
|
{
|
||||||
string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
|
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),
|
||||||
|
};
|
||||||
|
|
||||||
BlitStruct<ApplicationControlProperty> controlData = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10);
|
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application);
|
||||||
|
|
||||||
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Load_Application_File(object sender, EventArgs args)
|
private void Load_Application_File(object sender, EventArgs args)
|
||||||
|
@ -1341,7 +1369,15 @@ namespace Ryujinx.UI
|
||||||
|
|
||||||
if (fileChooser.Run() == (int)ResponseType.Accept)
|
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)
|
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);
|
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)
|
private void Open_Ryu_Folder(object sender, EventArgs args)
|
||||||
|
@ -1646,13 +1695,13 @@ namespace Ryujinx.UI
|
||||||
{
|
{
|
||||||
_userChannelPersistence.ShouldRestart = false;
|
_userChannelPersistence.ShouldRestart = false;
|
||||||
|
|
||||||
RunApplication(_currentEmulatedGamePath);
|
RunApplication(_currentApplicationData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// otherwise, clear state.
|
// otherwise, clear state.
|
||||||
_userChannelPersistence = new UserChannelPersistence();
|
_userChannelPersistence = new UserChannelPersistence();
|
||||||
_currentEmulatedGamePath = null;
|
_currentApplicationData = null;
|
||||||
_actionMenu.Sensitive = false;
|
_actionMenu.Sensitive = false;
|
||||||
_firmwareInstallFile.Sensitive = true;
|
_firmwareInstallFile.Sensitive = true;
|
||||||
_firmwareInstallDirectory.Sensitive = true;
|
_firmwareInstallDirectory.Sensitive = true;
|
||||||
|
@ -1714,7 +1763,7 @@ namespace Ryujinx.UI
|
||||||
_emulationContext.Processes.ActiveApplication.ProgramId,
|
_emulationContext.Processes.ActiveApplication.ProgramId,
|
||||||
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties
|
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties
|
||||||
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
|
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
|
||||||
_currentEmulatedGamePath);
|
_currentApplicationData.Path);
|
||||||
|
|
||||||
window.Destroyed += CheatWindow_Destroyed;
|
window.Destroyed += CheatWindow_Destroyed;
|
||||||
window.Show();
|
window.Show();
|
||||||
|
|
|
@ -16,6 +16,8 @@ using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
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.App.Common;
|
||||||
using Ryujinx.UI.Common.Configuration;
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
|
@ -23,7 +25,6 @@ using Ryujinx.UI.Windows;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -36,17 +37,13 @@ namespace Ryujinx.UI.Widgets
|
||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
private readonly AccountManager _accountManager;
|
private readonly AccountManager _accountManager;
|
||||||
private readonly HorizonClient _horizonClient;
|
private readonly HorizonClient _horizonClient;
|
||||||
private readonly BlitStruct<ApplicationControlProperty> _controlData;
|
|
||||||
|
|
||||||
private readonly string _titleFilePath;
|
private readonly ApplicationData _applicationData;
|
||||||
private readonly string _titleName;
|
|
||||||
private readonly string _titleIdText;
|
|
||||||
private readonly ulong _titleId;
|
|
||||||
|
|
||||||
private MessageDialog _dialog;
|
private MessageDialog _dialog;
|
||||||
private bool _cancel;
|
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;
|
_parent = parent;
|
||||||
|
|
||||||
|
@ -55,23 +52,22 @@ namespace Ryujinx.UI.Widgets
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_virtualFileSystem = virtualFileSystem;
|
||||||
_accountManager = accountManager;
|
_accountManager = accountManager;
|
||||||
_horizonClient = horizonClient;
|
_horizonClient = horizonClient;
|
||||||
_titleFilePath = titleFilePath;
|
_applicationData = applicationData;
|
||||||
_titleName = titleName;
|
|
||||||
_titleIdText = titleId;
|
|
||||||
_controlData = controlData;
|
|
||||||
|
|
||||||
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");
|
_openSaveUserDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||||
|
_openSaveDeviceDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||||
return;
|
_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;
|
string fileExt = System.IO.Path.GetExtension(_applicationData.Path).ToLower();
|
||||||
_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();
|
|
||||||
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
|
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
|
||||||
|
|
||||||
_extractRomFsMenuItem.Sensitive = hasNca;
|
_extractRomFsMenuItem.Sensitive = hasNca;
|
||||||
|
@ -137,7 +133,7 @@ namespace Ryujinx.UI.Widgets
|
||||||
|
|
||||||
private void OpenSaveDir(in SaveDataFilter saveDataFilter)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -190,7 +186,7 @@ namespace Ryujinx.UI.Widgets
|
||||||
{
|
{
|
||||||
Title = "Ryujinx - NCA Section Extractor",
|
Title = "Ryujinx - NCA Section Extractor",
|
||||||
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Gtk3.UI.Common.Resources.Logo_Ryujinx.png"),
|
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,
|
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 mainNca = null;
|
||||||
Nca patchNca = null;
|
Nca patchNca = null;
|
||||||
|
|
||||||
if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") ||
|
if ((System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".nsp") ||
|
||||||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
|
(System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".pfs0") ||
|
||||||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
|
(System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".xci"))
|
||||||
{
|
{
|
||||||
IFileSystem pfs;
|
IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(_applicationData.Path, _virtualFileSystem);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
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());
|
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
|
||||||
}
|
}
|
||||||
|
@ -266,7 +249,11 @@ namespace Ryujinx.UI.Widgets
|
||||||
return;
|
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)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
|
@ -460,44 +447,44 @@ namespace Ryujinx.UI.Widgets
|
||||||
private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
|
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 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);
|
OpenSaveDir(in saveDataFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
|
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);
|
OpenSaveDir(in saveDataFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
|
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);
|
OpenSaveDir(in saveDataFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
|
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)
|
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)
|
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)
|
private void OpenTitleModDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
string modsBasePath = ModLoader.GetModsBasePath();
|
string modsBasePath = ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, _titleIdText);
|
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, _applicationData.IdString);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
|
@ -505,7 +492,7 @@ namespace Ryujinx.UI.Widgets
|
||||||
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
|
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, _titleIdText);
|
string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, _applicationData.IdString);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
|
@ -527,7 +514,7 @@ namespace Ryujinx.UI.Widgets
|
||||||
|
|
||||||
private void OpenPtcDir_Clicked(object sender, EventArgs args)
|
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 mainPath = System.IO.Path.Combine(ptcDir, "0");
|
||||||
string backupPath = System.IO.Path.Combine(ptcDir, "1");
|
string backupPath = System.IO.Path.Combine(ptcDir, "1");
|
||||||
|
@ -544,7 +531,7 @@ namespace Ryujinx.UI.Widgets
|
||||||
|
|
||||||
private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
|
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))
|
if (!Directory.Exists(shaderCacheDir))
|
||||||
{
|
{
|
||||||
|
@ -556,10 +543,10 @@ namespace Ryujinx.UI.Widgets
|
||||||
|
|
||||||
private void PurgePtcCache_Clicked(object sender, EventArgs args)
|
private void PurgePtcCache_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0"));
|
DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu", "0"));
|
||||||
DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1"));
|
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();
|
List<FileInfo> cacheFiles = new();
|
||||||
|
|
||||||
|
@ -593,9 +580,9 @@ namespace Ryujinx.UI.Widgets
|
||||||
|
|
||||||
private void PurgeShaderCache_Clicked(object sender, EventArgs args)
|
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<DirectoryInfo> oldCacheDirectories = new();
|
||||||
List<FileInfo> newCacheFiles = new();
|
List<FileInfo> newCacheFiles = new();
|
||||||
|
@ -637,8 +624,11 @@ namespace Ryujinx.UI.Widgets
|
||||||
|
|
||||||
private void CreateShortcut_Clicked(object sender, EventArgs args)
|
private void CreateShortcut_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
|
? 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using Gtk;
|
using Gtk;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
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"))
|
private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
|
||||||
{
|
{
|
||||||
builder.Autoconnect(this);
|
builder.Autoconnect(this);
|
||||||
|
|
||||||
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
|
_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 modsBasePath = ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, titleId.ToString("X16"));
|
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, titleId.ToString("X16"));
|
||||||
|
|
|
@ -2,17 +2,21 @@ using Gtk;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
|
||||||
using LibHac.Tools.Fs;
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
using Ryujinx.HLE.Utilities;
|
||||||
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Widgets;
|
using Ryujinx.UI.Widgets;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using GUI = Gtk.Builder.ObjectAttribute;
|
using GUI = Gtk.Builder.ObjectAttribute;
|
||||||
|
|
||||||
namespace Ryujinx.UI.Windows
|
namespace Ryujinx.UI.Windows
|
||||||
|
@ -20,7 +24,7 @@ namespace Ryujinx.UI.Windows
|
||||||
public class DlcWindow : Window
|
public class DlcWindow : Window
|
||||||
{
|
{
|
||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
private readonly string _titleId;
|
private readonly string _applicationId;
|
||||||
private readonly string _dlcJsonPath;
|
private readonly string _dlcJsonPath;
|
||||||
private readonly List<DownloadableContentContainer> _dlcContainerList;
|
private readonly List<DownloadableContentContainer> _dlcContainerList;
|
||||||
|
|
||||||
|
@ -32,16 +36,16 @@ namespace Ryujinx.UI.Windows
|
||||||
[GUI] TreeSelection _dlcTreeSelection;
|
[GUI] TreeSelection _dlcTreeSelection;
|
||||||
#pragma warning restore CS0649, IDE0044
|
#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);
|
builder.Autoconnect(this);
|
||||||
|
|
||||||
_titleId = titleId;
|
_applicationId = applicationId;
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_virtualFileSystem = virtualFileSystem;
|
||||||
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json");
|
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json");
|
||||||
_baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
|
_baseTitleInfoLabel.Text = $"DLC Available for {applicationData.Name} [{applicationId.ToUpper()}]";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -72,7 +76,7 @@ namespace Ryujinx.UI.Windows
|
||||||
};
|
};
|
||||||
|
|
||||||
_dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
|
_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);
|
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
|
||||||
|
|
||||||
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
|
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
|
||||||
|
@ -86,18 +90,18 @@ namespace Ryujinx.UI.Windows
|
||||||
bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled);
|
bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled);
|
||||||
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath);
|
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();
|
if (partitionFileSystem == null)
|
||||||
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
{
|
||||||
|
continue;
|
||||||
_virtualFileSystem.ImportTickets(pfs);
|
}
|
||||||
|
|
||||||
foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList)
|
foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList)
|
||||||
{
|
{
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
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);
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath);
|
||||||
|
|
||||||
if (nca != null)
|
if (nca != null)
|
||||||
|
@ -112,6 +116,9 @@ namespace Ryujinx.UI.Windows
|
||||||
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}");
|
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)
|
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
||||||
|
@ -128,6 +135,52 @@ namespace Ryujinx.UI.Windows
|
||||||
return null;
|
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)
|
private void AddButton_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
|
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)
|
foreach (string containerPath in fileChooser.Filenames)
|
||||||
{
|
{
|
||||||
if (!File.Exists(containerPath))
|
AddDlc(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!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,17 @@ using Gtk;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.Ncm;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
using Ryujinx.HLE.Utilities;
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using Ryujinx.UI.Widgets;
|
using Ryujinx.UI.Widgets;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -24,7 +27,7 @@ namespace Ryujinx.UI.Windows
|
||||||
{
|
{
|
||||||
private readonly MainWindow _parent;
|
private readonly MainWindow _parent;
|
||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
private readonly string _titleId;
|
private readonly ApplicationData _applicationData;
|
||||||
private readonly string _updateJsonPath;
|
private readonly string _updateJsonPath;
|
||||||
|
|
||||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||||
|
@ -38,17 +41,17 @@ namespace Ryujinx.UI.Windows
|
||||||
[GUI] RadioButton _noUpdateRadioButton;
|
[GUI] RadioButton _noUpdateRadioButton;
|
||||||
#pragma warning restore CS0649, IDE0044
|
#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;
|
_parent = parent;
|
||||||
|
|
||||||
builder.Autoconnect(this);
|
builder.Autoconnect(this);
|
||||||
|
|
||||||
_titleId = titleId;
|
_applicationData = applicationData;
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_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>();
|
_radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
|
||||||
|
|
||||||
try
|
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)
|
foreach (string path in _titleUpdateWindowData.Paths)
|
||||||
{
|
{
|
||||||
|
@ -84,18 +90,31 @@ 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();
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
nsp.Initialize(file.AsStorage()).ThrowIfFailure();
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
|
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
|
||||||
|
|
||||||
|
Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel);
|
||||||
|
|
||||||
|
Nca patchNca = null;
|
||||||
|
Nca controlNca = null;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
if (controlNca != null && patchNca != null)
|
if (controlNca != null && patchNca != null)
|
||||||
{
|
{
|
||||||
|
@ -106,7 +125,14 @@ namespace Ryujinx.UI.Windows
|
||||||
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).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();
|
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||||
|
|
||||||
RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}");
|
string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}";
|
||||||
|
|
||||||
|
if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
|
||||||
|
{
|
||||||
|
radioLabel = "Bundled: " + radioLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadioButton radioButton = new(radioLabel);
|
||||||
radioButton.JoinGroup(_noUpdateRadioButton);
|
radioButton.JoinGroup(_noUpdateRadioButton);
|
||||||
|
|
||||||
_availableUpdatesBox.Add(radioButton);
|
_availableUpdatesBox.Add(radioButton);
|
||||||
|
@ -116,16 +142,18 @@ namespace Ryujinx.UI.Windows
|
||||||
radioButton.Active = true;
|
radioButton.Active = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
if (!ignoreNotFound)
|
||||||
{
|
{
|
||||||
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
|
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");
|
GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveUpdates(bool removeSelectedOnly = false)
|
private void RemoveUpdates(bool removeSelectedOnly = false)
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,6 +14,7 @@ using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.Exceptions;
|
using Ryujinx.HLE.Exceptions;
|
||||||
using Ryujinx.HLE.HOS.Services.Ssl;
|
using Ryujinx.HLE.HOS.Services.Ssl;
|
||||||
using Ryujinx.HLE.HOS.Services.Time;
|
using Ryujinx.HLE.HOS.Services.Time;
|
||||||
|
using Ryujinx.HLE.Utilities;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
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)
|
public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool mergedToContainer = false)
|
||||||
{
|
{
|
||||||
// TODO: Check Aoc version.
|
// TODO: Check Aoc version.
|
||||||
|
@ -232,11 +198,7 @@ namespace Ryujinx.HLE.FileSystem
|
||||||
|
|
||||||
if (!mergedToContainer)
|
if (!mergedToContainer)
|
||||||
{
|
{
|
||||||
using FileStream fileStream = File.OpenRead(containerPath);
|
using var pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(containerPath, _virtualFileSystem);
|
||||||
using PartitionFileSystem partitionFileSystem = new();
|
|
||||||
partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure();
|
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
61
src/Ryujinx.HLE/FileSystem/ContentMetaData.cs
Normal file
61
src/Ryujinx.HLE/FileSystem/ContentMetaData.cs
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ using LibHac.FsSystem;
|
||||||
using LibHac.Loader;
|
using LibHac.Loader;
|
||||||
using LibHac.Ncm;
|
using LibHac.Ncm;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using Ryujinx.HLE.HOS;
|
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.Loaders.Processes
|
namespace Ryujinx.HLE.Loaders.Processes
|
||||||
|
|
|
@ -7,16 +7,25 @@ using LibHac.Ncm;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Tools.Ncm;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
|
using Ryujinx.HLE.Utilities;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ApplicationId = LibHac.Ncm.ApplicationId;
|
using ApplicationId = LibHac.Ncm.ApplicationId;
|
||||||
|
using ContentType = LibHac.Ncm.ContentType;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
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)
|
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
|
||||||
{
|
{
|
||||||
// Extract RomFs and ExeFs from NCA.
|
// Extract RomFs and ExeFs from NCA.
|
||||||
|
@ -47,7 +56,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
nacpData = controlNca.GetNacp(device);
|
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.
|
// 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 _);
|
(_, 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;
|
return processResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ulong GetProgramIdBase(this Nca nca)
|
||||||
|
{
|
||||||
|
return nca.Header.TitleId & ~0x1FFFUL;
|
||||||
|
}
|
||||||
|
|
||||||
public static int GetProgramIndex(this Nca nca)
|
public static int GetProgramIndex(this Nca nca)
|
||||||
{
|
{
|
||||||
return (int)(nca.Header.TitleId & 0xF);
|
return (int)(nca.Header.TitleId & 0xF);
|
||||||
|
@ -96,6 +110,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return nca.Header.ContentType == NcaContentType.Program;
|
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)
|
public static bool IsPatch(this Nca nca)
|
||||||
{
|
{
|
||||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||||
|
@ -108,6 +127,43 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return nca.Header.ContentType == NcaContentType.Control;
|
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)
|
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
|
||||||
{
|
{
|
||||||
IFileSystem exeFs = null;
|
IFileSystem exeFs = null;
|
||||||
|
@ -172,5 +228,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
|
|
||||||
return nacpData;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,58 @@
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
|
using LibHac.Common.Keys;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Ncm;
|
||||||
using LibHac.Tools.Fs;
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Tools.Ncm;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using ContentType = LibHac.Ncm.ContentType;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
{
|
{
|
||||||
public static class PartitionFileSystemExtensions
|
public static class PartitionFileSystemExtensions
|
||||||
{
|
{
|
||||||
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
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 TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
|
||||||
where TFormat : IPartitionFileSystemFormat
|
where TFormat : IPartitionFileSystemFormat
|
||||||
where THeader : unmanaged, IPartitionFileSystemHeader
|
where THeader : unmanaged, IPartitionFileSystemHeader
|
||||||
|
@ -35,30 +67,21 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
|
|
||||||
try
|
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.
|
if (applicationId == 0)
|
||||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
{
|
||||||
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
|
foreach ((ulong _, ContentMetaData content) in applications)
|
||||||
|
|
||||||
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
|
|
||||||
{
|
{
|
||||||
continue;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nca.IsPatch())
|
|
||||||
{
|
|
||||||
patchNca = nca;
|
|
||||||
}
|
}
|
||||||
else if (nca.IsProgram())
|
else if (applications.TryGetValue(applicationId, out ContentMetaData content))
|
||||||
{
|
{
|
||||||
mainNca = 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);
|
||||||
else if (nca.IsControl())
|
|
||||||
{
|
|
||||||
controlNca = nca;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
|
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
|
||||||
|
@ -79,54 +102,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return (false, ProcessResult.Failed);
|
return (false, ProcessResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Update NCAs.
|
(Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatePatchNca != null)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
|
@ -138,10 +114,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
controlNca = updateControlNca;
|
controlNca = updateControlNca;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load contained DownloadableContents.
|
|
||||||
// TODO: If we want to support multi-processes in future, we shouldn't clear AddOnContent data here.
|
// TODO: If we want to support multi-processes in future, we shouldn't clear AddOnContent data here.
|
||||||
device.Configuration.ContentManager.ClearAocData();
|
device.Configuration.ContentManager.ClearAocData();
|
||||||
device.Configuration.ContentManager.AddAocData(partitionFileSystem, path, mainNca.Header.TitleId, device.Configuration.FsIntegrityCheckLevel);
|
|
||||||
|
|
||||||
// Load DownloadableContents.
|
// Load DownloadableContents.
|
||||||
string addOnContentMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
|
string addOnContentMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
|
||||||
|
@ -153,10 +127,13 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
{
|
{
|
||||||
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||||
{
|
{
|
||||||
if (File.Exists(downloadableContentContainer.ContainerPath) && downloadableContentNca.Enabled)
|
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||||
|
{
|
||||||
|
if (downloadableContentNca.Enabled)
|
||||||
{
|
{
|
||||||
device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath);
|
device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {downloadableContentContainer.ContainerPath}. It may have been moved or renamed.");
|
Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {downloadableContentContainer.ContainerPath}. It may have been moved or renamed.");
|
||||||
|
@ -168,18 +145,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return (true, mainNca.Load(device, patchNca, controlNca));
|
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);
|
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>();
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
|
_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);
|
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
|
||||||
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
|
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
|
||||||
|
@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
return false;
|
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)
|
if (!success)
|
||||||
{
|
{
|
||||||
|
@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadNsp(string path)
|
public bool LoadNsp(string path, ulong applicationId)
|
||||||
{
|
{
|
||||||
FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||||
PartitionFileSystem partitionFileSystem = new();
|
PartitionFileSystem partitionFileSystem = new();
|
||||||
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
|
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)
|
if (processResult.ProcessId == 0)
|
||||||
{
|
{
|
||||||
|
|
|
@ -43,15 +43,14 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong currentProgramId = nca.Header.TitleId;
|
ulong currentMainProgramId = nca.GetProgramIdBase();
|
||||||
ulong currentMainProgramId = currentProgramId & ~0xFFFul;
|
|
||||||
|
|
||||||
if (applicationId == 0 && currentMainProgramId != 0)
|
if (applicationId == 0 && currentMainProgramId != 0)
|
||||||
{
|
{
|
||||||
|
@ -68,7 +67,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasIndex[(int)(currentProgramId & 0xF)] = true;
|
hasIndex[nca.GetProgramIndex()] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (programCount == 0)
|
if (programCount == 0)
|
||||||
|
|
|
@ -73,9 +73,9 @@ namespace Ryujinx.HLE
|
||||||
return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
|
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)
|
public bool LoadNca(string ncaFile)
|
||||||
|
@ -83,9 +83,9 @@ namespace Ryujinx.HLE
|
||||||
return Processes.LoadNca(ncaFile);
|
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)
|
public bool LoadProgram(string fileName)
|
||||||
|
|
45
src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs
Normal file
45
src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,9 +9,11 @@ using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Ryujinx.UI.App.Common
|
namespace Ryujinx.UI.App.Common
|
||||||
{
|
{
|
||||||
|
@ -19,10 +21,10 @@ namespace Ryujinx.UI.App.Common
|
||||||
{
|
{
|
||||||
public bool Favorite { get; set; }
|
public bool Favorite { get; set; }
|
||||||
public byte[] Icon { get; set; }
|
public byte[] Icon { get; set; }
|
||||||
public string TitleName { get; set; }
|
public string Name { get; set; } = "Unknown";
|
||||||
public string TitleId { get; set; }
|
public ulong Id { get; set; }
|
||||||
public string Developer { get; set; }
|
public string Developer { get; set; } = "Unknown";
|
||||||
public string Version { get; set; }
|
public string Version { get; set; } = "0";
|
||||||
public TimeSpan TimePlayed { get; set; }
|
public TimeSpan TimePlayed { get; set; }
|
||||||
public DateTime? LastPlayed { get; set; }
|
public DateTime? LastPlayed { get; set; }
|
||||||
public string FileExtension { get; set; }
|
public string FileExtension { get; set; }
|
||||||
|
@ -36,7 +38,11 @@ namespace Ryujinx.UI.App.Common
|
||||||
|
|
||||||
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
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);
|
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
|
@ -105,7 +111,7 @@ namespace Ryujinx.UI.App.Common
|
||||||
return string.Empty;
|
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)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,6 +14,7 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
public static string BaseDirPathArg { get; private set; }
|
public static string BaseDirPathArg { get; private set; }
|
||||||
public static string Profile { get; private set; }
|
public static string Profile { get; private set; }
|
||||||
public static string LaunchPathArg { 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 bool StartFullscreenArg { get; private set; }
|
||||||
|
|
||||||
public static void ParseArguments(string[] args)
|
public static void ParseArguments(string[] args)
|
||||||
|
@ -72,6 +73,10 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
|
|
||||||
OverrideGraphicsBackend = args[++i];
|
OverrideGraphicsBackend = args[++i];
|
||||||
break;
|
break;
|
||||||
|
case "-i":
|
||||||
|
case "--application-id":
|
||||||
|
LaunchApplicationId = args[++i];
|
||||||
|
break;
|
||||||
case "--docked-mode":
|
case "--docked-mode":
|
||||||
OverrideDockedMode = true;
|
OverrideDockedMode = true;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
public static class ShortcutHelper
|
public static class ShortcutHelper
|
||||||
{
|
{
|
||||||
[SupportedOSPlatform("windows")]
|
[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");
|
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
|
||||||
iconPath += ".ico";
|
iconPath += ".ico";
|
||||||
|
@ -25,13 +25,13 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
image.Mutate(x => x.Resize(128, 128));
|
image.Mutate(x => x.Resize(128, 128));
|
||||||
SaveBitmapAsIcon(image, iconPath);
|
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.StringData.NameString = cleanedAppName;
|
||||||
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
|
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("linux")]
|
[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");
|
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh");
|
||||||
var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.desktop");
|
var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.desktop");
|
||||||
|
@ -41,11 +41,11 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
image.SaveAsPng(iconPath);
|
image.SaveAsPng(iconPath);
|
||||||
|
|
||||||
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
|
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")]
|
[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");
|
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
|
||||||
var plistFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.plist");
|
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);
|
string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
|
||||||
using StreamWriter scriptFile = new(scriptPath);
|
using StreamWriter scriptFile = new(scriptPath);
|
||||||
|
|
||||||
scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath));
|
scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId));
|
||||||
|
|
||||||
// Set execute permission
|
// Set execute permission
|
||||||
FileInfo fileInfo = new(scriptPath);
|
FileInfo fileInfo = new(scriptPath);
|
||||||
|
@ -95,7 +95,7 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
{
|
{
|
||||||
string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
|
string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
|
||||||
|
|
||||||
CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath);
|
CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -105,14 +105,14 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
|
string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
|
||||||
|
|
||||||
Directory.CreateDirectory(iconPath);
|
Directory.CreateDirectory(iconPath);
|
||||||
CreateShortcutLinux(applicationFilePath, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
|
CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName);
|
CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
|
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
|
// args are first defined as a list, for easier adjustments in the future
|
||||||
var argsList = new List<string>();
|
var argsList = new List<string>();
|
||||||
|
@ -131,6 +131,12 @@ namespace Ryujinx.UI.Common.Helper
|
||||||
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
|
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appFilePath.ToLower().EndsWith(".xci"))
|
||||||
|
{
|
||||||
|
argsList.Add("--application-id");
|
||||||
|
argsList.Add($"\"{applicationId}\"");
|
||||||
|
}
|
||||||
|
|
||||||
argsList.Add($"\"{appFilePath}\"");
|
argsList.Add($"\"{appFilePath}\"");
|
||||||
|
|
||||||
return String.Join(" ", argsList);
|
return String.Join(" ", argsList);
|
||||||
|
|
|
@ -132,12 +132,14 @@ namespace Ryujinx.Ava
|
||||||
public int Width { get; private set; }
|
public int Width { get; private set; }
|
||||||
public int Height { get; private set; }
|
public int Height { get; private set; }
|
||||||
public string ApplicationPath { get; private set; }
|
public string ApplicationPath { get; private set; }
|
||||||
|
public ulong ApplicationId { get; private set; }
|
||||||
public bool ScreenshotRequested { get; set; }
|
public bool ScreenshotRequested { get; set; }
|
||||||
|
|
||||||
public AppHost(
|
public AppHost(
|
||||||
RendererHost renderer,
|
RendererHost renderer,
|
||||||
InputManager inputManager,
|
InputManager inputManager,
|
||||||
string applicationPath,
|
string applicationPath,
|
||||||
|
ulong applicationId,
|
||||||
VirtualFileSystem virtualFileSystem,
|
VirtualFileSystem virtualFileSystem,
|
||||||
ContentManager contentManager,
|
ContentManager contentManager,
|
||||||
AccountManager accountManager,
|
AccountManager accountManager,
|
||||||
|
@ -161,6 +163,7 @@ namespace Ryujinx.Ava
|
||||||
NpadManager = _inputManager.CreateNpadManager();
|
NpadManager = _inputManager.CreateNpadManager();
|
||||||
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
||||||
ApplicationPath = applicationPath;
|
ApplicationPath = applicationPath;
|
||||||
|
ApplicationId = applicationId;
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
ContentManager = contentManager;
|
ContentManager = contentManager;
|
||||||
|
|
||||||
|
@ -719,7 +722,7 @@ namespace Ryujinx.Ava
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
||||||
|
|
||||||
if (!Device.LoadXci(ApplicationPath))
|
if (!Device.LoadXci(ApplicationPath, ApplicationId))
|
||||||
{
|
{
|
||||||
Device.Dispose();
|
Device.Dispose();
|
||||||
|
|
||||||
|
@ -746,7 +749,7 @@ namespace Ryujinx.Ava
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
||||||
|
|
||||||
if (!Device.LoadNsp(ApplicationPath))
|
if (!Device.LoadNsp(ApplicationPath, ApplicationId))
|
||||||
{
|
{
|
||||||
Device.Dispose();
|
Device.Dispose();
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"SettingsTabSystemUseHypervisor": "Use Hypervisor",
|
"SettingsTabSystemUseHypervisor": "Use Hypervisor",
|
||||||
"MenuBarFile": "_File",
|
"MenuBarFile": "_File",
|
||||||
"MenuBarFileOpenFromFile": "_Load Application From File",
|
"MenuBarFileOpenFromFile": "_Load Application From File",
|
||||||
|
"MenuBarFileOpenFromFileError": "No applications found in selected file.",
|
||||||
"MenuBarFileOpenUnpacked": "Load _Unpacked Game",
|
"MenuBarFileOpenUnpacked": "Load _Unpacked Game",
|
||||||
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
|
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
|
||||||
"MenuBarFileOpenLogsFolder": "Open Logs Folder",
|
"MenuBarFileOpenLogsFolder": "Open Logs Folder",
|
||||||
|
@ -649,6 +650,8 @@
|
||||||
"OpenSetupGuideMessage": "Open the Setup Guide",
|
"OpenSetupGuideMessage": "Open the Setup Guide",
|
||||||
"NoUpdate": "No Update",
|
"NoUpdate": "No Update",
|
||||||
"TitleUpdateVersionLabel": "Version {0}",
|
"TitleUpdateVersionLabel": "Version {0}",
|
||||||
|
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
|
||||||
|
"TitleBundledDlcLabel": "Bundled:",
|
||||||
"RyujinxInfo": "Ryujinx - Info",
|
"RyujinxInfo": "Ryujinx - Info",
|
||||||
"RyujinxConfirm": "Ryujinx - Confirmation",
|
"RyujinxConfirm": "Ryujinx - Confirmation",
|
||||||
"FileDialogAllTypes": "All types",
|
"FileDialogAllTypes": "All types",
|
||||||
|
|
|
@ -18,7 +18,8 @@ using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
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 Ryujinx.UI.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
@ -226,7 +227,11 @@ namespace Ryujinx.Ava.Common
|
||||||
return;
|
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)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
patchNca = updatePatchNca;
|
patchNca = updatePatchNca;
|
||||||
|
|
|
@ -125,7 +125,7 @@ namespace Ryujinx.Ava
|
||||||
|
|
||||||
if (CommandLineState.LaunchPathArg != null)
|
if (CommandLineState.LaunchPathArg != null)
|
||||||
{
|
{
|
||||||
MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
|
MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.LaunchApplicationId, CommandLineState.StartFullscreenArg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Threading;
|
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
|
@ -15,7 +14,6 @@ using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
|
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
|
||||||
|
|
||||||
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata =>
|
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata =>
|
||||||
{
|
{
|
||||||
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
|
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
|
||||||
});
|
});
|
||||||
|
@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default);
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name);
|
||||||
}
|
|
||||||
|
|
||||||
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
|
|
||||||
|
|
||||||
ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
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)
|
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(
|
await new CheatWindow(
|
||||||
viewModel.VirtualFileSystem,
|
viewModel.VirtualFileSystem,
|
||||||
viewModel.SelectedApplication.TitleId,
|
viewModel.SelectedApplication.IdString,
|
||||||
viewModel.SelectedApplication.TitleName,
|
viewModel.SelectedApplication.Name,
|
||||||
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
|
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string modsBasePath = ModLoader.GetModsBasePath();
|
string modsBasePath = ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.TitleId);
|
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.IdString);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
|
@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.TitleId);
|
string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.IdString);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
|
@ -158,7 +146,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
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(
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
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.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
|
|
||||||
if (result == UserResult.Yes)
|
if (result == UserResult.Yes)
|
||||||
{
|
{
|
||||||
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0"));
|
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
|
||||||
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1"));
|
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
|
||||||
|
|
||||||
List<FileInfo> cacheFiles = new();
|
List<FileInfo> cacheFiles = new();
|
||||||
|
|
||||||
|
@ -218,14 +206,14 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
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.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
|
|
||||||
if (result == UserResult.Yes)
|
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<DirectoryInfo> oldCacheDirectories = new();
|
||||||
List<FileInfo> newCacheFiles = new();
|
List<FileInfo> newCacheFiles = new();
|
||||||
|
@ -273,7 +261,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
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 mainDir = Path.Combine(ptcDir, "0");
|
||||||
string backupDir = Path.Combine(ptcDir, "1");
|
string backupDir = Path.Combine(ptcDir, "1");
|
||||||
|
|
||||||
|
@ -294,7 +282,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
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))
|
if (!Directory.Exists(shaderCacheDir))
|
||||||
{
|
{
|
||||||
|
@ -315,7 +303,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Code,
|
NcaSectionType.Code,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.TitleName);
|
viewModel.SelectedApplication.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +317,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Data,
|
NcaSectionType.Data,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.TitleName);
|
viewModel.SelectedApplication.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,7 +331,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Logo,
|
NcaSectionType.Logo,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.TitleName);
|
viewModel.SelectedApplication.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +342,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
ApplicationData selectedApplication = viewModel.SelectedApplication;
|
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)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
|
await viewModel.LoadApplication(viewModel.SelectedApplication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{Binding TitleName}"
|
Text="{Binding Name}"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Text="{Binding TitleName}"
|
Text="{Binding Name}"
|
||||||
TextAlignment="Start"
|
TextAlignment="Start"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
Spacing="5">
|
Spacing="5">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{Binding TitleId}"
|
Text="{Binding Id, StringFormat=X16}"
|
||||||
TextAlignment="Start"
|
TextAlignment="Start"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
|
@ -24,6 +25,9 @@ namespace Ryujinx.Ava.UI.Models
|
||||||
|
|
||||||
public string FileName => Path.GetFileName(ContainerPath);
|
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)
|
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||||
{
|
{
|
||||||
TitleId = titleId;
|
TitleId = titleId;
|
||||||
|
|
|
@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models
|
||||||
TitleId = info.ProgramId;
|
TitleId = info.ProgramId;
|
||||||
UserId = info.UserId;
|
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;
|
InGameList = appData != null;
|
||||||
|
|
||||||
if (InGameList)
|
if (InGameList)
|
||||||
{
|
{
|
||||||
Icon = appData.Icon;
|
Icon = appData.Icon;
|
||||||
Title = appData.TitleName;
|
Title = appData.Name;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models
|
||||||
public ApplicationControlProperty Control { get; }
|
public ApplicationControlProperty Control { get; }
|
||||||
public string Path { 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)
|
public TitleUpdateModel(ApplicationControlProperty control, string path)
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,7 +6,6 @@ using DynamicData;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
|
||||||
using LibHac.Tools.Fs;
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
@ -17,11 +16,13 @@ using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
using Ryujinx.HLE.Utilities;
|
||||||
|
using Ryujinx.UI.App.Common;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Application = Avalonia.Application;
|
using Application = Avalonia.Application;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
||||||
|
|
||||||
private string _search;
|
private string _search;
|
||||||
private readonly ulong _titleId;
|
private readonly ApplicationData _applicationData;
|
||||||
private readonly IStorageProvider _storageProvider;
|
private readonly IStorageProvider _storageProvider;
|
||||||
|
|
||||||
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
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);
|
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
|
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_virtualFileSystem = virtualFileSystem;
|
||||||
|
|
||||||
_titleId = titleId;
|
_applicationData = applicationData;
|
||||||
|
|
||||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
_storageProvider = desktop.MainWindow.StorageProvider;
|
_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
|
try
|
||||||
{
|
{
|
||||||
|
@ -123,12 +131,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||||
{
|
{
|
||||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, _virtualFileSystem);
|
||||||
|
|
||||||
PartitionFileSystem partitionFileSystem = new();
|
|
||||||
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
|
||||||
|
|
||||||
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
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.
|
// NOTE: Save the list again to remove leftovers.
|
||||||
Save();
|
Save();
|
||||||
Sort();
|
Sort();
|
||||||
|
@ -219,25 +225,23 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
foreach (var file in result)
|
foreach (var file in result)
|
||||||
{
|
{
|
||||||
await AddDownloadableContent(file.Path.LocalPath);
|
if (!AddDownloadableContent(file.Path.LocalPath))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddDownloadableContent(string path)
|
|
||||||
{
|
{
|
||||||
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||||
{
|
}
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using FileStream containerFile = File.OpenRead(path);
|
private bool AddDownloadableContent(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path) || _downloadableContentContainerList.Any(x => x.ContainerPath == path))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
PartitionFileSystem partitionFileSystem = new();
|
using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
|
||||||
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
|
||||||
bool containsDownloadableContent = false;
|
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||||
{
|
{
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
@ -252,26 +256,26 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
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);
|
var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true);
|
||||||
DownloadableContents.Add(content);
|
DownloadableContents.Add(content);
|
||||||
SelectedDownloadableContents.Add(content);
|
SelectedDownloadableContents.Add(content);
|
||||||
|
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
OnPropertyChanged(nameof(UpdateCount));
|
OnPropertyChanged(nameof(UpdateCount));
|
||||||
Sort();
|
Sort();
|
||||||
|
|
||||||
containsDownloadableContent = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!containsDownloadableContent)
|
return success;
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(DownloadableContentModel model)
|
public void Remove(DownloadableContentModel model)
|
||||||
|
|
|
@ -96,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
private bool _canUpdate = true;
|
private bool _canUpdate = true;
|
||||||
private Cursor _cursor;
|
private Cursor _cursor;
|
||||||
private string _title;
|
private string _title;
|
||||||
private string _currentEmulatedGamePath;
|
private ApplicationData _currentApplicationData;
|
||||||
private readonly AutoResetEvent _rendererWaitEvent;
|
private readonly AutoResetEvent _rendererWaitEvent;
|
||||||
private WindowState _windowState;
|
private WindowState _windowState;
|
||||||
private double _windowWidth;
|
private double _windowWidth;
|
||||||
|
@ -108,7 +108,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
public ApplicationData ListSelectedApplication;
|
public ApplicationData ListSelectedApplication;
|
||||||
public ApplicationData GridSelectedApplication;
|
public ApplicationData GridSelectedApplication;
|
||||||
|
|
||||||
private string TitleName { get; set; }
|
|
||||||
internal AppHost AppHost { get; set; }
|
internal AppHost AppHost { get; set; }
|
||||||
|
|
||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
|
@ -954,8 +953,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
return SortMode switch
|
return SortMode switch
|
||||||
{
|
{
|
||||||
#pragma warning disable IDE0055 // Disable formatting
|
#pragma warning disable IDE0055 // Disable formatting
|
||||||
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName)
|
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Name)
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName),
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.Name),
|
||||||
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
||||||
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
|
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
|
||||||
|
@ -999,7 +998,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
CompareInfo compareInfo = CultureInfo.CurrentCulture.CompareInfo;
|
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;
|
return false;
|
||||||
|
@ -1128,7 +1127,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
IsLoadingIndeterminate = false;
|
IsLoadingIndeterminate = false;
|
||||||
break;
|
break;
|
||||||
case LoadState.Loaded:
|
case LoadState.Loaded:
|
||||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
|
||||||
IsLoadingIndeterminate = true;
|
IsLoadingIndeterminate = true;
|
||||||
CacheLoadStatus = "";
|
CacheLoadStatus = "";
|
||||||
break;
|
break;
|
||||||
|
@ -1148,7 +1147,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
IsLoadingIndeterminate = false;
|
IsLoadingIndeterminate = false;
|
||||||
break;
|
break;
|
||||||
case ShaderCacheLoadingState.Loaded:
|
case ShaderCacheLoadingState.Loaded:
|
||||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
|
||||||
IsLoadingIndeterminate = true;
|
IsLoadingIndeterminate = true;
|
||||||
CacheLoadStatus = "";
|
CacheLoadStatus = "";
|
||||||
break;
|
break;
|
||||||
|
@ -1200,13 +1199,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
UserChannelPersistence.ShouldRestart = false;
|
UserChannelPersistence.ShouldRestart = false;
|
||||||
|
|
||||||
await LoadApplication(_currentEmulatedGamePath);
|
await LoadApplication(_currentApplicationData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Otherwise, clear state.
|
// Otherwise, clear state.
|
||||||
UserChannelPersistence = new UserChannelPersistence();
|
UserChannelPersistence = new UserChannelPersistence();
|
||||||
_currentEmulatedGamePath = null;
|
_currentApplicationData = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1493,7 +1492,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
if (result.Count > 0)
|
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)
|
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)
|
if (AppHost != null)
|
||||||
{
|
{
|
||||||
|
@ -1531,7 +1544,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
Logger.RestartTime();
|
Logger.RestartTime();
|
||||||
|
|
||||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language);
|
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);
|
||||||
|
|
||||||
PrepareLoadScreen();
|
PrepareLoadScreen();
|
||||||
|
|
||||||
|
@ -1540,7 +1553,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
AppHost = new AppHost(
|
AppHost = new AppHost(
|
||||||
RendererHostControl,
|
RendererHostControl,
|
||||||
InputManager,
|
InputManager,
|
||||||
path,
|
application.Path,
|
||||||
|
application.Id,
|
||||||
VirtualFileSystem,
|
VirtualFileSystem,
|
||||||
ContentManager,
|
ContentManager,
|
||||||
AccountManager,
|
AccountManager,
|
||||||
|
@ -1558,17 +1572,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
CanUpdate = false;
|
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);
|
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);
|
SwitchToRenderer(startFullscreen);
|
||||||
|
|
||||||
_currentEmulatedGamePath = path;
|
_currentApplicationData = application;
|
||||||
|
|
||||||
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
|
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
|
||||||
gameThread.Start();
|
gameThread.Start();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
@ -6,7 +5,7 @@ using Avalonia.Threading;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.Ncm;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
@ -17,12 +16,17 @@ using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
using Ryujinx.HLE.Utilities;
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Application = Avalonia.Application;
|
||||||
|
using ContentType = LibHac.Ncm.ContentType;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||||
|
|
||||||
|
@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
public TitleUpdateMetadata TitleUpdateWindowData;
|
public TitleUpdateMetadata TitleUpdateWindowData;
|
||||||
public readonly string TitleUpdateJsonPath;
|
public readonly string TitleUpdateJsonPath;
|
||||||
private VirtualFileSystem VirtualFileSystem { get; }
|
private VirtualFileSystem VirtualFileSystem { get; }
|
||||||
private ulong TitleId { get; }
|
private ApplicationData ApplicationData { get; }
|
||||||
|
|
||||||
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
||||||
private AvaloniaList<object> _views = new();
|
private AvaloniaList<object> _views = new();
|
||||||
|
@ -73,18 +77,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
public IStorageProvider StorageProvider;
|
public IStorageProvider StorageProvider;
|
||||||
|
|
||||||
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
|
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
|
|
||||||
TitleId = titleId;
|
ApplicationData = applicationData;
|
||||||
|
|
||||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
StorageProvider = desktop.MainWindow.StorageProvider;
|
StorageProvider = desktop.MainWindow.StorageProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
|
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -92,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
}
|
}
|
||||||
catch
|
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
|
TitleUpdateWindowData = new TitleUpdateMetadata
|
||||||
{
|
{
|
||||||
|
@ -108,6 +112,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
private void LoadUpdates()
|
private void LoadUpdates()
|
||||||
{
|
{
|
||||||
|
// Try to load updates from PFS first
|
||||||
|
AddUpdate(ApplicationData.Path, true);
|
||||||
|
|
||||||
foreach (string path in TitleUpdateWindowData.Paths)
|
foreach (string path in TitleUpdateWindowData.Paths)
|
||||||
{
|
{
|
||||||
AddUpdate(path);
|
AddUpdate(path);
|
||||||
|
@ -162,17 +169,31 @@ 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))
|
||||||
{
|
{
|
||||||
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pfs = new PartitionFileSystem();
|
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, VirtualFileSystem);
|
||||||
pfs.Initialize(file.AsStorage()).ThrowIfFailure();
|
|
||||||
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0);
|
Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.Patch, VirtualFileSystem, checkLevel);
|
||||||
|
|
||||||
|
Nca patchNca = null;
|
||||||
|
Nca controlNca = null;
|
||||||
|
|
||||||
|
if (updates.TryGetValue(ApplicationData.Id, out ContentMetaData content))
|
||||||
|
{
|
||||||
|
patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program);
|
||||||
|
controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control);
|
||||||
|
}
|
||||||
|
|
||||||
if (controlNca != null && patchNca != null)
|
if (controlNca != null && patchNca != null)
|
||||||
{
|
{
|
||||||
|
@ -186,16 +207,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
|
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
if (!ignoreNotFound)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
|
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveUpdate(TitleUpdateModel update)
|
public void RemoveUpdate(TitleUpdateModel update)
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,6 +11,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.Modules;
|
using Ryujinx.Modules;
|
||||||
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common;
|
using Ryujinx.UI.Common;
|
||||||
using Ryujinx.UI.Common.Configuration;
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
|
@ -134,7 +135,14 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(contentPath))
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
@ -34,9 +36,12 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
|
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
|
||||||
{
|
{
|
||||||
LoadedCheats = new AvaloniaList<CheatNode>();
|
LoadedCheats = new AvaloniaList<CheatNode>();
|
||||||
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
|
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
|
||||||
BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath);
|
BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
MaxLines="2"
|
MaxLines="2"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
Text="{Binding FileName}" />
|
Text="{Binding Label}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="10 0"
|
Margin="10 0"
|
||||||
|
|
|
@ -3,13 +3,12 @@ using Avalonia.Interactivity;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Button = Avalonia.Controls.Button;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Windows
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
|
@ -24,22 +23,22 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
InitializeComponent();
|
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();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = new()
|
ContentDialog contentDialog = new()
|
||||||
{
|
{
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = "",
|
CloseButtonText = "",
|
||||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId),
|
Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData),
|
||||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")),
|
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdString),
|
||||||
};
|
};
|
||||||
|
|
||||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||||
|
|
|
@ -5,6 +5,7 @@ using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Input;
|
using Ryujinx.Ava.Input;
|
||||||
|
@ -24,7 +25,7 @@ using Ryujinx.UI.Common;
|
||||||
using Ryujinx.UI.Common.Configuration;
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -40,6 +41,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
private UserChannelPersistence _userChannelPersistence;
|
private UserChannelPersistence _userChannelPersistence;
|
||||||
private static bool _deferLoad;
|
private static bool _deferLoad;
|
||||||
private static string _launchPath;
|
private static string _launchPath;
|
||||||
|
private static string _launchApplicationId;
|
||||||
private static bool _startFullscreen;
|
private static bool _startFullscreen;
|
||||||
internal readonly AvaHostUIHandler UiHandler;
|
internal readonly AvaHostUIHandler UiHandler;
|
||||||
|
|
||||||
|
@ -168,18 +170,17 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
ViewModel.SelectedIcon = args.Application.Icon;
|
ViewModel.SelectedIcon = args.Application.Icon;
|
||||||
|
|
||||||
string path = new FileInfo(args.Application.Path).FullName;
|
ViewModel.LoadApplication(args.Application).Wait();
|
||||||
|
|
||||||
ViewModel.LoadApplication(path).Wait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg)
|
internal static void DeferLoadApplication(string launchPathArg, string launchApplicationId, bool startFullscreenArg)
|
||||||
{
|
{
|
||||||
_deferLoad = true;
|
_deferLoad = true;
|
||||||
_launchPath = launchPathArg;
|
_launchPath = launchPathArg;
|
||||||
|
_launchApplicationId = launchApplicationId;
|
||||||
_startFullscreen = startFullscreenArg;
|
_startFullscreen = startFullscreenArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +220,11 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
LibHacHorizonManager.InitializeBcatServer();
|
LibHacHorizonManager.InitializeBcatServer();
|
||||||
LibHacHorizonManager.InitializeSystemClients();
|
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
|
// 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
|
// 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;
|
_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
|
else
|
||||||
|
|
|
@ -5,19 +5,18 @@ using Avalonia.Interactivity;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Button = Avalonia.Controls.Button;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Windows
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
public partial class TitleUpdateWindow : UserControl
|
public partial class TitleUpdateWindow : UserControl
|
||||||
{
|
{
|
||||||
public TitleUpdateViewModel ViewModel;
|
public readonly TitleUpdateViewModel ViewModel;
|
||||||
|
|
||||||
public TitleUpdateWindow()
|
public TitleUpdateWindow()
|
||||||
{
|
{
|
||||||
|
@ -26,22 +25,22 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
InitializeComponent();
|
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();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = new()
|
ContentDialog contentDialog = new()
|
||||||
{
|
{
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = "",
|
CloseButtonText = "",
|
||||||
Content = new TitleUpdateWindow(virtualFileSystem, titleId),
|
Content = new TitleUpdateWindow(virtualFileSystem, applicationData),
|
||||||
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")),
|
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString),
|
||||||
};
|
};
|
||||||
|
|
||||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||||
|
|
Loading…
Reference in a new issue