0
0
Fork 0
mirror of https://github.com/GreemDev/Ryujinx.git synced 2024-12-23 08:05:47 +00:00

Add support for multi game XCIs (#5638)

* 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

* Fix formatting issues

* 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

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
This commit is contained in:
TSRBerry 2023-11-11 21:56:57 +01:00 committed by GitHub
parent 55557525b1
commit 5c3cfb84c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1168 additions and 816 deletions

View file

@ -54,8 +54,6 @@ using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;
using InputManager = Ryujinx.Input.HLE.InputManager;
using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
using Key = Ryujinx.Input.Key; using Key = Ryujinx.Input.Key;
using MouseButton = Ryujinx.Input.MouseButton; using MouseButton = Ryujinx.Input.MouseButton;
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
@ -123,12 +121,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,
@ -152,6 +152,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;
@ -641,7 +642,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();
@ -668,7 +669,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();

View file

@ -539,6 +539,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",

View file

@ -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;

View file

@ -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.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId); string titleModsPath = ModLoader.GetTitleDir(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.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@ -160,15 +148,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();
@ -208,14 +196,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();
@ -263,7 +251,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");
@ -284,7 +272,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))
{ {
@ -305,7 +293,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);
} }
} }
@ -319,7 +307,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);
} }
} }
@ -333,7 +321,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);
} }
} }
@ -344,7 +332,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);
} }
} }
@ -354,7 +342,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);
} }
} }
} }

View file

@ -82,7 +82,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>

View file

@ -85,7 +85,7 @@
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
FontWeight="Bold" FontWeight="Bold"
Text="{Binding TitleName}" Text="{Binding Name}"
TextAlignment="Left" TextAlignment="Left"
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="Left" TextAlignment="Left"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock

View file

@ -1,4 +1,5 @@
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using System.IO; using System.IO;
namespace Ryujinx.Ava.UI.Models namespace Ryujinx.Ava.UI.Models
@ -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;

View file

@ -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
{ {

View file

@ -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)
{ {

View file

@ -17,11 +17,12 @@ 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.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 static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@ -92,18 +93,25 @@ namespace Ryujinx.Ava.UI.ViewModels
public IStorageProvider StorageProvider; public IStorageProvider StorageProvider;
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
{ {
@ -120,6 +128,9 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadDownloadableContents() private void LoadDownloadableContents()
{ {
// NOTE: Try to load downloadable contents from PFS first.
AddDownloadableContent(_applicationData.Path);
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
{ {
if (File.Exists(downloadableContentContainer.ContainerPath)) if (File.Exists(downloadableContentContainer.ContainerPath))
@ -127,7 +138,11 @@ namespace Ryujinx.Ava.UI.ViewModels
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
PartitionFileSystem partitionFileSystem = new(); PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure())
{
continue;
}
_virtualFileSystem.ImportTickets(partitionFileSystem); _virtualFileSystem.ImportTickets(partitionFileSystem);
@ -220,22 +235,34 @@ 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))
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
}
} }
} }
private async Task AddDownloadableContent(string path) private bool AddDownloadableContent(string path)
{ {
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null) if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
{ {
return; return true;
} }
using FileStream containerFile = File.OpenRead(path); using FileStream containerFile = File.OpenRead(path);
PartitionFileSystem partitionFileSystem = new(); IFileSystem partitionFileSystem;
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDownloadableContent = false; if (Path.GetExtension(path).ToLower() == ".xci")
{
partitionFileSystem = new Xci(_virtualFileSystem.KeySet, containerFile.AsStorage()).OpenPartition(XciPartitionType.Secure);
}
else
{
var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(containerFile.AsStorage()).ThrowIfFailure();
partitionFileSystem = pfsTemp;
}
_virtualFileSystem.ImportTickets(partitionFileSystem); _virtualFileSystem.ImportTickets(partitionFileSystem);
@ -253,7 +280,7 @@ 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; break;
} }
@ -265,14 +292,11 @@ namespace Ryujinx.Ava.UI.ViewModels
OnPropertyChanged(nameof(UpdateCount)); OnPropertyChanged(nameof(UpdateCount));
Sort(); Sort();
containsDownloadableContent = true; return true;
} }
} }
if (!containsDownloadableContent) return false;
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
}
} }
public void Remove(DownloadableContentModel model) public void Remove(DownloadableContentModel model)

View file

@ -95,7 +95,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;
@ -106,7 +106,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()
@ -930,8 +929,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),
@ -968,7 +967,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
if (arg is ApplicationData app) if (arg is ApplicationData app)
{ {
return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower()); return string.IsNullOrWhiteSpace(_searchText) || app.Name.ToLower().Contains(_searchText.ToLower());
} }
return false; return false;
@ -1097,7 +1096,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;
@ -1117,7 +1116,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;
@ -1168,13 +1167,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;
} }
} }
@ -1451,7 +1450,12 @@ namespace Ryujinx.Ava.UI.ViewModels
if (result.Count > 0) if (result.Count > 0)
{ {
await LoadApplication(result[0].Path.LocalPath); ApplicationData applicationData = new()
{
Path = result[0].Path.LocalPath,
};
await LoadApplication(applicationData);
} }
} }
@ -1465,11 +1469,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)
{ {
@ -1489,7 +1499,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();
@ -1498,7 +1508,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,
@ -1516,17 +1527,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();

View file

@ -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;
@ -8,6 +7,7 @@ using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Ns; using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
@ -17,12 +17,16 @@ 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.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,41 @@ 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.All(x => x.Path != path))
{ {
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
using FileStream file = new(path, FileMode.Open, FileAccess.Read); using FileStream file = new(path, FileMode.Open, FileAccess.Read);
IFileSystem pfs;
try try
{ {
var pfs = new PartitionFileSystem(); if (Path.GetExtension(path).ToLower() == ".xci")
pfs.Initialize(file.AsStorage()).ThrowIfFailure(); {
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); pfs = new Xci(VirtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
}
else
{
var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
}
Dictionary<ulong, ContentCollection> updates = pfs.GetUpdateData(VirtualFileSystem, checkLevel);
Nca patchNca = null;
Nca controlNca = null;
if (updates.TryGetValue(ApplicationData.Id, out ContentCollection 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)
{ {
@ -187,7 +218,10 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
else else
{ {
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); if (!ignoreNotFound)
{
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
}
} }
} }
catch (Exception ex) catch (Exception ex)

View file

@ -10,6 +10,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;
@ -131,7 +132,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);
} }
} }

View file

@ -104,7 +104,7 @@
Content="{locale:Locale GameListHeaderApplication}" Content="{locale:Locale GameListHeaderApplication}"
GroupName="Sort" GroupName="Sort"
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
Tag="Title" /> Tag="Application" />
<RadioButton <RadioButton
Checked="Sort_Checked" Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderDeveloper}" Content="{locale:Locale GameListHeaderDeveloper}"

View file

@ -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<CheatsList>(); LoadedCheats = new AvaloniaList<CheatsList>();
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();

View file

@ -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"

View file

@ -7,9 +7,9 @@ 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 +24,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>());

View file

@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
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;
@ -23,7 +24,6 @@ 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.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -139,9 +139,7 @@ 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;
@ -190,7 +188,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
@ -297,7 +299,12 @@ namespace Ryujinx.Ava.UI.Windows
{ {
_deferLoad = false; _deferLoad = false;
ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); ApplicationData applicationData = new()
{
Path = _launchPath,
};
ViewModel.LoadApplication(applicationData, _startFullscreen).Wait();
} }
} }
else else

View file

@ -7,15 +7,15 @@ 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()
{ {
@ -24,22 +24,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>());

View 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 ContentCollection
{
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 ContentCollection(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;
}
}
}

View file

@ -2,21 +2,31 @@
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.Loader; using LibHac.Loader;
using LibHac.Ncm; using LibHac.Ncm;
using LibHac.Ns; using LibHac.Ns;
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.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
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 _titleSerializerContext = 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 +57,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 +96,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 +111,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 +128,56 @@ 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 = "(unknown)";
// 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, _titleSerializerContext.TitleUpdateMetadata).Selected;
if (File.Exists(updatePath))
{
var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
IFileSystem updatePartitionFileSystem;
if (Path.GetExtension(updatePath).ToLower() == ".xci")
{
updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure);
}
else
{
PartitionFileSystem pfsTemp = new();
pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure();
updatePartitionFileSystem = pfsTemp;
}
foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel))
{
if ((updateTitleId & ~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 +242,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;
}
} }
} }

View file

@ -1,26 +1,87 @@
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, ContentCollection> GetApplicationData(this IFileSystem partitionFileSystem,
VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel)
{
fileSystem.ImportTickets(partitionFileSystem);
var programs = new Dictionary<ulong, ContentCollection>();
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca"))
{
Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Application);
if (cnmt == null)
{
continue;
}
ContentCollection content = new(partitionFileSystem, cnmt);
if (content.Type != ContentMetaType.Application)
{
continue;
}
programs.TryAdd(content.ApplicationId, content);
}
return programs;
}
public static Dictionary<ulong, ContentCollection> GetUpdateData(this IFileSystem partitionFileSystem,
VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel)
{
fileSystem.ImportTickets(partitionFileSystem);
var programs = new Dictionary<ulong, ContentCollection>();
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca"))
{
Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Patch);
if (cnmt == null)
{
continue;
}
ContentCollection content = new(partitionFileSystem, cnmt);
if (content.Type != ContentMetaType.Patch)
{
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 titleId, 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,31 +96,22 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
try try
{ {
device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem); Dictionary<ulong, ContentCollection> applications = partitionFileSystem.GetApplicationData(device.FileSystem, device.System.FsIntegrityCheckLevel);
// TODO: To support multi-games container, this should use CNMT NCA instead. if (titleId == 0)
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
{ {
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); foreach ((ulong _, ContentCollection 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())
{
mainNca = nca;
}
else if (nca.IsControl())
{
controlNca = nca;
} }
} }
else if (applications.TryGetValue(titleId, out ContentCollection content))
{
mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
}
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure(); ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
} }
@ -79,54 +131,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)
{ {
@ -168,18 +173,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 \"{titleId: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());
} }
} }
} }

View file

@ -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 titleId)
{ {
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, titleId, 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 titleId)
{ {
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, titleId, out string errorMessage);
if (processResult.ProcessId == 0) if (processResult.ProcessId == 0)
{ {

View file

@ -42,15 +42,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)
{ {
@ -67,7 +66,7 @@ namespace Ryujinx.HLE.Loaders.Processes
break; break;
} }
hasIndex[(int)(currentProgramId & 0xF)] = true; hasIndex[nca.GetProgramIndex()] = true;
} }
if (programCount == 0) if (programCount == 0)

View file

@ -72,9 +72,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 titleId = 0)
{ {
return Processes.LoadXci(xciFile); return Processes.LoadXci(xciFile, titleId);
} }
public bool LoadNca(string ncaFile) public bool LoadNca(string ncaFile)
@ -82,9 +82,9 @@ namespace Ryujinx.HLE
return Processes.LoadNca(ncaFile); return Processes.LoadNca(ncaFile);
} }
public bool LoadNsp(string nspFile) public bool LoadNsp(string nspFile, ulong titleId = 0)
{ {
return Processes.LoadNsp(nspFile); return Processes.LoadNsp(nspFile, titleId);
} }
public bool LoadProgram(string fileName) public bool LoadProgram(string fileName)

View file

@ -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

View file

@ -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;
@ -332,7 +333,12 @@ namespace Ryujinx
if (CommandLineState.LaunchPathArg != null) if (CommandLineState.LaunchPathArg != null)
{ {
mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg); ApplicationData applicationData = new()
{
Path = CommandLineState.LaunchPathArg,
};
mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
} }
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))

View file

@ -39,6 +39,7 @@ using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan; using SPB.Graphics.Vulkan;
using System; using System;
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;
@ -70,7 +71,7 @@ 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;
@ -181,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);
@ -784,7 +789,7 @@ namespace Ryujinx.Ui
} }
} }
private bool LoadApplication(string path, bool isFirmwareTitle) private bool LoadApplication(string path, ulong titleId, bool isFirmwareTitle)
{ {
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
@ -858,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, titleId);
case ".nca": case ".nca":
Logger.Info?.Print(LogClass.Application, "Loading as NCA."); Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
@ -867,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, titleId);
default: default:
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
try try
@ -888,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)
{ {
@ -910,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();
@ -927,7 +932,7 @@ namespace Ryujinx.Ui
SetupProgressUiHandlers(); SetupProgressUiHandlers();
_currentEmulatedGamePath = path; _currentApplicationData = application;
_deviceExitStatus.Reset(); _deviceExitStatus.Reset();
@ -1168,7 +1173,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,
@ -1256,9 +1261,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)
@ -1316,13 +1334,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)
@ -1344,7 +1371,12 @@ namespace Ryujinx.Ui
if (fileChooser.Run() == (int)ResponseType.Accept) if (fileChooser.Run() == (int)ResponseType.Accept)
{ {
RunApplication(fileChooser.Filename); ApplicationData applicationData = new()
{
Path = fileChooser.Filename,
};
RunApplication(applicationData);
} }
} }
@ -1354,7 +1386,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);
} }
} }
@ -1369,7 +1407,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)
@ -1645,13 +1690,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;
@ -1713,7 +1758,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();

View file

@ -16,6 +16,7 @@ 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.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 +24,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 +36,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 _title;
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 +51,13 @@ namespace Ryujinx.Ui.Widgets
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
_accountManager = accountManager; _accountManager = accountManager;
_horizonClient = horizonClient; _horizonClient = horizonClient;
_titleFilePath = titleFilePath; _title = applicationData;
_titleName = titleName;
_titleIdText = titleId;
_controlData = controlData;
if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId)) _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.UserAccountSaveDataSize > 0;
{ _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.DeviceSaveDataSize > 0;
GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id"); _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
return; string fileExt = System.IO.Path.GetExtension(_title.Path).ToLower();
}
_openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
_openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
_openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower();
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 +123,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(_title.Name, _title.Id, _title.ControlHolder, in saveDataFilter, out ulong saveDataId))
{ {
return; return;
} }
@ -190,7 +176,7 @@ namespace Ryujinx.Ui.Widgets
{ {
Title = "Ryujinx - NCA Section Extractor", Title = "Ryujinx - NCA Section Extractor",
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"), Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.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(_title.Path)}...",
WindowPosition = WindowPosition.Center, WindowPosition = WindowPosition.Center,
}; };
@ -202,18 +188,18 @@ namespace Ryujinx.Ui.Widgets
} }
}); });
using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read); using FileStream file = new(_title.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(_title.Path).ToLower() == ".nsp") ||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || (System.IO.Path.GetExtension(_title.Path).ToLower() == ".pfs0") ||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci"))
{ {
IFileSystem pfs; IFileSystem pfs;
if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci")
{ {
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
@ -249,7 +235,7 @@ namespace Ryujinx.Ui.Widgets
} }
} }
} }
else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca") else if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".nca")
{ {
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
} }
@ -266,7 +252,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 +450,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(_title.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(_title.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(_title.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, _title).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, _title.IdString, _title).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, _title.Id, _title.Name, _title.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.GetTitleDir(modsBasePath, _titleIdText); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _title.IdString);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@ -505,7 +495,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.GetTitleDir(sdModsBasePath, _titleIdText); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _title.IdString);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@ -527,7 +517,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, _title.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 +534,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, _title.IdString, "cache", "shader");
if (!Directory.Exists(shaderCacheDir)) if (!Directory.Exists(shaderCacheDir))
{ {
@ -556,10 +546,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, _title.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, _title.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>{_title.Name}</b>\n\nAre you sure you want to proceed?");
List<FileInfo> cacheFiles = new(); List<FileInfo> cacheFiles = new();
@ -593,9 +583,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, _title.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>{_title.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 +627,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(_title.Path, ConfigurationState.Instance.System.Language, _title.Id);
ShortcutHelper.CreateAppShortcut(_title.Path, _title.Name, _title.IdString, appIcon);
} }
} }
} }

View file

@ -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.GetTitleDir(modsBasePath, titleId.ToString("X16")); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));

View file

@ -9,9 +9,12 @@ 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.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 GUI = Gtk.Builder.ObjectAttribute; using GUI = Gtk.Builder.ObjectAttribute;
@ -20,7 +23,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 +35,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.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData title) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, title) { }
private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData title) : 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 {title.Name} [{applicationId.ToUpper()}]";
try try
{ {
@ -72,9 +75,12 @@ 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);
// NOTE: Try to load downloadable contents from PFS first.
AddDlc(title.Path, true);
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
{ {
if (File.Exists(dlcContainer.ContainerPath)) if (File.Exists(dlcContainer.ContainerPath))
@ -89,7 +95,10 @@ namespace Ryujinx.Ui.Windows
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
PartitionFileSystem pfs = new(); PartitionFileSystem pfs = new();
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); if (pfs.Initialize(containerFile.AsStorage()).IsFailure())
{
continue;
}
_virtualFileSystem.ImportTickets(pfs); _virtualFileSystem.ImportTickets(pfs);
@ -128,6 +137,57 @@ namespace Ryujinx.Ui.Windows
return null; return null;
} }
private void AddDlc(string path, bool ignoreNotFound = false)
{
if (!File.Exists(path))
{
return;
}
using FileStream containerFile = File.OpenRead(path);
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(), path);
if (nca == null)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.PublicData)
{
if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL))
{
break;
}
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 +207,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!");
}
} }
} }

View file

@ -4,12 +4,15 @@ using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Ns; using LibHac.Ns;
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.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 _title;
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.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.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; _title = 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(_title.Path, true);
foreach (string path in _titleUpdateWindowData.Paths) foreach (string path in _titleUpdateWindowData.Paths)
{ {
@ -84,18 +90,41 @@ 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))
{ {
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
using FileStream file = new(path, FileMode.Open, FileAccess.Read); using FileStream file = new(path, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new(); IFileSystem pfs;
nsp.Initialize(file.AsStorage()).ThrowIfFailure();
try try
{ {
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
{
pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
}
else
{
var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
}
Dictionary<ulong, ContentCollection> updates = pfs.GetUpdateData(_virtualFileSystem, checkLevel);
Nca patchNca = null;
Nca controlNca = null;
if (updates.TryGetValue(_title.Id, out ContentCollection 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 +135,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);
@ -117,7 +153,10 @@ namespace Ryujinx.Ui.Windows
} }
else else
{ {
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); if (!ignoreNotFound)
{
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
}
} }
} }
catch (Exception exception) catch (Exception exception)