aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Ava/UI
diff options
context:
space:
mode:
authorTSRBerry <20988865+TSRBerry@users.noreply.github.com>2023-11-11 21:56:57 +0100
committerGitHub <noreply@github.com>2023-11-11 21:56:57 +0100
commit5c3cfb84c09b0566da677425915afa0b2d76da55 (patch)
treed53c683c3ed3e685bec5b16ca661755d8815f66e /src/Ryujinx.Ava/UI
parent55557525b16f8256d91f769e026874b5c70c3b2d (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')
-rw-r--r--src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs54
-rw-r--r--src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml2
-rw-r--r--src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml4
-rw-r--r--src/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs6
-rw-r--r--src/Ryujinx.Ava/UI/Models/SaveModel.cs4
-rw-r--r--src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs5
-rw-r--r--src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs60
-rw-r--r--src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs47
-rw-r--r--src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs56
-rw-r--r--src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs10
-rw-r--r--src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml2
-rw-r--r--src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs7
-rw-r--r--src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml2
-rw-r--r--src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs12
-rw-r--r--src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs19
-rw-r--r--src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs14
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>());