diff options
| author | TSRBerry <20988865+TSRBerry@users.noreply.github.com> | 2023-11-11 21:56:57 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-11 21:56:57 +0100 |
| commit | 5c3cfb84c09b0566da677425915afa0b2d76da55 (patch) | |
| tree | d53c683c3ed3e685bec5b16ca661755d8815f66e /src/Ryujinx.Ava/UI | |
| parent | 55557525b16f8256d91f769e026874b5c70c3b2d (diff) | |
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>
Diffstat (limited to 'src/Ryujinx.Ava/UI')
16 files changed, 194 insertions, 110 deletions
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index 0f007106..69465c7c 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -1,7 +1,6 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.Threading; using LibHac.Fs; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common; @@ -15,7 +14,6 @@ using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using Path = System.IO.Path; @@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls { viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; - ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata => + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => { appMetadata.Favorite = viewModel.SelectedApplication.Favorite; }); @@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls { if (viewModel?.SelectedApplication != null) { - if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - Dispatcher.UIThread.InvokeAsync(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); - - return; - } - - var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default); - ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName); + ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name); } } @@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); } } @@ -108,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); } } @@ -120,8 +108,8 @@ namespace Ryujinx.Ava.UI.Controls { await new CheatWindow( viewModel.VirtualFileSystem, - viewModel.SelectedApplication.TitleId, - viewModel.SelectedApplication.TitleName, + viewModel.SelectedApplication.IdString, + viewModel.SelectedApplication.Name, viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window); } } @@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -160,15 +148,15 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); List<FileInfo> cacheFiles = new(); @@ -208,14 +196,14 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader")); List<DirectoryInfo> oldCacheDirectories = new(); List<FileInfo> newCacheFiles = new(); @@ -263,7 +251,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu"); + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); string mainDir = Path.Combine(ptcDir, "0"); string backupDir = Path.Combine(ptcDir, "1"); @@ -284,7 +272,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"); + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -305,7 +293,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Code, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -319,7 +307,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Data, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -333,7 +321,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Logo, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -344,7 +332,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { ApplicationData selectedApplication = viewModel.SelectedApplication; - ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon); + ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon); } } @@ -354,7 +342,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await viewModel.LoadApplication(viewModel.SelectedApplication.Path); + await viewModel.LoadApplication(viewModel.SelectedApplication); } } } diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml index bbdb4c4a..5919652e 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml @@ -82,7 +82,7 @@ <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" - Text="{Binding TitleName}" + Text="{Binding Name}" TextAlignment="Center" TextWrapping="Wrap" /> </Panel> diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index 9004f751..24ec2b35 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -85,7 +85,7 @@ <TextBlock HorizontalAlignment="Stretch" FontWeight="Bold" - Text="{Binding TitleName}" + Text="{Binding Name}" TextAlignment="Left" TextWrapping="Wrap" /> <TextBlock @@ -109,7 +109,7 @@ Spacing="5"> <TextBlock HorizontalAlignment="Stretch" - Text="{Binding TitleId}" + Text="{Binding Id, StringFormat=X16}" TextAlignment="Left" TextWrapping="Wrap" /> <TextBlock diff --git a/src/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs b/src/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs index fedb3527..e39ffead 100644 --- a/src/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs +++ b/src/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs @@ -1,4 +1,5 @@ -using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels; using System.IO; namespace Ryujinx.Ava.UI.Models @@ -24,6 +25,9 @@ namespace Ryujinx.Ava.UI.Models public string FileName => Path.GetFileName(ContainerPath); + public string Label => + Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName; + public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled) { TitleId = titleId; diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs index 7b476932..2e3ed3ba 100644 --- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs +++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs @@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models TitleId = info.ProgramId; UserId = info.UserId; - var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString); + var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString); InGameList = appData != null; if (InGameList) { Icon = appData.Icon; - Title = appData.TitleName; + Title = appData.Name; } else { diff --git a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs index 3b44e8ee..fae2a08d 100644 --- a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs @@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models public ApplicationControlProperty Control { get; } public string Path { get; } - public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString()); + public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue( + System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel, + Control.DisplayVersionString.ToString() + ); public TitleUpdateModel(ApplicationControlProperty control, string path) { diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index cdecae77..9f3a0045 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -17,11 +17,12 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.Ui.App.Common; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using Application = Avalonia.Application; using Path = System.IO.Path; @@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new(); private string _search; - private readonly ulong _titleId; + private readonly ApplicationData _applicationData; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -92,18 +93,25 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) + public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { _virtualFileSystem = virtualFileSystem; - _titleId = titleId; + _applicationData = applicationData; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); + _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json"); + + if (!File.Exists(_downloadableContentJsonPath)) + { + _downloadableContentContainerList = new List<DownloadableContentContainer>(); + + Save(); + } try { @@ -120,6 +128,9 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { + // NOTE: Try to load downloadable contents from PFS first. + AddDownloadableContent(_applicationData.Path); + foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) { if (File.Exists(downloadableContentContainer.ContainerPath)) @@ -127,7 +138,11 @@ namespace Ryujinx.Ava.UI.ViewModels using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); PartitionFileSystem partitionFileSystem = new(); - partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure()) + { + continue; + } _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -220,22 +235,34 @@ namespace Ryujinx.Ava.UI.ViewModels foreach (var file in result) { - await AddDownloadableContent(file.Path.LocalPath); + if (!AddDownloadableContent(file.Path.LocalPath)) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); + } } } - private async Task AddDownloadableContent(string path) + private bool AddDownloadableContent(string path) { if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null) { - return; + return true; } using FileStream containerFile = File.OpenRead(path); - PartitionFileSystem partitionFileSystem = new(); - partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - bool containsDownloadableContent = false; + IFileSystem partitionFileSystem; + + 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); @@ -253,7 +280,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (nca.Header.ContentType == NcaContentType.PublicData) { - if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId) + if (nca.GetProgramIdBase() != _applicationData.IdBase) { break; } @@ -265,14 +292,11 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(nameof(UpdateCount)); Sort(); - containsDownloadableContent = true; + return true; } } - if (!containsDownloadableContent) - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); - } + return false; } public void Remove(DownloadableContentModel model) diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 80df5d39..692df483 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _canUpdate = true; private Cursor _cursor; private string _title; - private string _currentEmulatedGamePath; + private ApplicationData _currentApplicationData; private readonly AutoResetEvent _rendererWaitEvent; private WindowState _windowState; private double _windowWidth; @@ -106,7 +106,6 @@ namespace Ryujinx.Ava.UI.ViewModels public ApplicationData ListSelectedApplication; public ApplicationData GridSelectedApplication; - private string TitleName { get; set; } internal AppHost AppHost { get; set; } public MainWindowViewModel() @@ -930,8 +929,8 @@ namespace Ryujinx.Ava.UI.ViewModels return SortMode switch { #pragma warning disable IDE0055 // Disable formatting - ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName) - : SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName), + ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Name) + : SortExpressionComparer<ApplicationData>.Descending(app => app.Name), ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer) : SortExpressionComparer<ApplicationData>.Descending(app => app.Developer), ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), @@ -968,7 +967,7 @@ namespace Ryujinx.Ava.UI.ViewModels { 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; @@ -1097,7 +1096,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case LoadState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1117,7 +1116,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case ShaderCacheLoadingState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1168,13 +1167,13 @@ namespace Ryujinx.Ava.UI.ViewModels { UserChannelPersistence.ShouldRestart = false; - await LoadApplication(_currentEmulatedGamePath); + await LoadApplication(_currentApplicationData); } else { // Otherwise, clear state. UserChannelPersistence = new UserChannelPersistence(); - _currentEmulatedGamePath = null; + _currentApplicationData = null; } } @@ -1451,7 +1450,12 @@ namespace Ryujinx.Ava.UI.ViewModels 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) { - 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) { @@ -1489,7 +1499,7 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language); + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); PrepareLoadScreen(); @@ -1498,7 +1508,8 @@ namespace Ryujinx.Ava.UI.ViewModels AppHost = new AppHost( RendererHostControl, InputManager, - path, + application.Path, + application.Id, VirtualFileSystem, ContentManager, AccountManager, @@ -1516,17 +1527,17 @@ namespace Ryujinx.Ava.UI.ViewModels CanUpdate = false; - LoadHeading = TitleName = titleName; + LoadHeading = application.Name; - if (string.IsNullOrWhiteSpace(titleName)) + if (string.IsNullOrWhiteSpace(application.Name)) { LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name); - TitleName = AppHost.Device.Processes.ActiveApplication.Name; + application.Name = AppHost.Device.Processes.ActiveApplication.Name; } SwitchToRenderer(startFullscreen); - _currentEmulatedGamePath = path; + _currentApplicationData = application; Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; gameThread.Start(); diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs index 5090a8c7..7bb96131 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -1,4 +1,3 @@ -using Avalonia; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; @@ -8,6 +7,7 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; +using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Locale; @@ -17,12 +17,16 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Application = Avalonia.Application; +using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using SpanHelpers = LibHac.Common.SpanHelpers; @@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels public TitleUpdateMetadata TitleUpdateWindowData; public readonly string TitleUpdateJsonPath; private VirtualFileSystem VirtualFileSystem { get; } - private ulong TitleId { get; } + private ApplicationData ApplicationData { get; } private AvaloniaList<TitleUpdateModel> _titleUpdates = new(); private AvaloniaList<object> _views = new(); @@ -73,18 +77,18 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) + public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { VirtualFileSystem = virtualFileSystem; - TitleId = titleId; + ApplicationData = applicationData; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); + TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json"); try { @@ -92,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels } catch { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}"); + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}"); TitleUpdateWindowData = new TitleUpdateMetadata { @@ -108,6 +112,9 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadUpdates() { + // Try to load updates from PFS first + AddUpdate(ApplicationData.Path, true); + foreach (string path in TitleUpdateWindowData.Paths) { AddUpdate(path); @@ -162,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)) { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + IFileSystem pfs; + try { - var pfs = new PartitionFileSystem(); - pfs.Initialize(file.AsStorage()).ThrowIfFailure(); - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); + if (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(ApplicationData.Id, out ContentCollection content)) + { + patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program); + controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control); + } if (controlNca != null && patchNca != null) { @@ -187,7 +218,10 @@ namespace Ryujinx.Ava.UI.ViewModels } else { - Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); + if (!ignoreNotFound) + { + Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); + } } } catch (Exception ex) diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs index 4f2d262d..af3c5deb 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Utilities; using Ryujinx.Modules; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -131,7 +132,14 @@ namespace Ryujinx.Ava.UI.Views.Main if (!string.IsNullOrEmpty(contentPath)) { - await ViewModel.LoadApplication(contentPath, false, "Mii Applet"); + ApplicationData applicationData = new() + { + Name = "miiEdit", + Id = 0x0100000000001009, + Path = contentPath, + }; + + await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen); } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml index cc21b5c6..7a716fb2 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -104,7 +104,7 @@ Content="{locale:Locale GameListHeaderApplication}" GroupName="Sort" IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" - Tag="Title" /> + Tag="Application" /> <RadioButton Checked="Sort_Checked" Content="{locale:Locale GameListHeaderDeveloper}" diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs index fde249a0..76f1a991 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs @@ -1,9 +1,11 @@ using Avalonia.Collections; +using LibHac.Tools.FsSystem; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Models; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.Globalization; @@ -34,9 +36,12 @@ namespace Ryujinx.Ava.UI.Windows public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath) { LoadedCheats = new AvaloniaList<CheatsList>(); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); - BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath); + BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath); InitializeComponent(); diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml index 99cf28e7..98aac09c 100644 --- a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml @@ -97,7 +97,7 @@ MaxLines="2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" - Text="{Binding FileName}" /> + Text="{Binding Label}" /> <TextBlock Grid.Column="1" Margin="10 0" diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs index dfe8807b..c871ae19 100644 --- a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs @@ -7,9 +7,9 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System.Threading.Tasks; -using Button = Avalonia.Controls.Button; namespace Ryujinx.Ava.UI.Windows { @@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId) + public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { - DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId); + DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId), - Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")), + Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData), + Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdString), }; Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>()); diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index c78f4160..352ac4e5 100644 --- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Controls; +using LibHac.Tools.FsSystem; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; @@ -23,7 +24,6 @@ using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; using System; -using System.IO; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -139,9 +139,7 @@ namespace Ryujinx.Ava.UI.Windows { ViewModel.SelectedIcon = args.Application.Icon; - string path = new FileInfo(args.Application.Path).FullName; - - ViewModel.LoadApplication(path).Wait(); + ViewModel.LoadApplication(args.Application).Wait(); } args.Handled = true; @@ -190,7 +188,11 @@ namespace Ryujinx.Ava.UI.Windows LibHacHorizonManager.InitializeBcatServer(); LibHacHorizonManager.InitializeSystemClients(); - ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel); // Save data created before we supported extra data in directory save data will not work properly if // given empty extra data. Luckily some of that extra data can be created using the data from the @@ -297,7 +299,12 @@ namespace Ryujinx.Ava.UI.Windows { _deferLoad = false; - ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); + ApplicationData applicationData = new() + { + Path = _launchPath, + }; + + ViewModel.LoadApplication(applicationData, _startFullscreen).Wait(); } } else diff --git a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs index 7ece6335..8ecf165c 100644 --- a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System.Threading.Tasks; -using Button = Avalonia.Controls.Button; namespace Ryujinx.Ava.UI.Windows { public partial class TitleUpdateWindow : UserControl { - public TitleUpdateViewModel ViewModel; + public readonly TitleUpdateViewModel ViewModel; public TitleUpdateWindow() { @@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId) + public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { - DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId); + DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new TitleUpdateWindow(virtualFileSystem, titleId), - Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")), + Content = new TitleUpdateWindow(virtualFileSystem, applicationData), + Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString), }; Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>()); |
