Make dlc/updates records; use ApplicationLibrary for loading logic

This commit is contained in:
Jimmy Reichley 2024-08-15 23:41:12 -04:00
parent 8073b8d189
commit 48b7517284
No known key found for this signature in database
GPG key ID: 67715DC5A329803C
9 changed files with 77 additions and 125 deletions

View file

@ -514,7 +514,7 @@ namespace Ryujinx.UI.App.Common
if (nca.Header.ContentType == NcaContentType.PublicData) if (nca.Header.ContentType == NcaContentType.PublicData)
{ {
titleUpdates.Add(new DownloadableContentModel(nca.Header.TitleId, filePath, fileEntry.FullPath, false)); titleUpdates.Add(new DownloadableContentModel(nca.Header.TitleId, filePath, fileEntry.FullPath));
} }
} }
@ -1294,7 +1294,7 @@ namespace Ryujinx.UI.App.Common
{ {
return new Nca(_virtualFileSystem.KeySet, ncaStorage); return new Nca(_virtualFileSystem.KeySet, ncaStorage);
} }
catch (Exception ex) { } catch (Exception) { }
return null; return null;
} }

View file

@ -1,24 +1,11 @@
namespace Ryujinx.UI.Common.Models namespace Ryujinx.UI.Common.Models
{ {
public class DownloadableContentModel public record DownloadableContentModel(ulong TitleId, string ContainerPath, string FullPath)
{ {
public ulong TitleId { get; } public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci";
public string ContainerPath { get; }
public string FullPath { get; }
public bool Enabled { get; }
public bool IsBundled { get; }
public string FileName => System.IO.Path.GetFileName(ContainerPath); public string FileName => System.IO.Path.GetFileName(ContainerPath);
public string TitleIdStr => TitleId.ToString("X16"); public string TitleIdStr => TitleId.ToString("X16");
public ulong TitleIdBase => TitleId & ~0x1FFFUL; public ulong TitleIdBase => TitleId & ~0x1FFFUL;
public DownloadableContentModel(ulong titleId, string containerPath, string fullPath, bool enabled)
{
TitleId = titleId;
ContainerPath = containerPath;
FullPath = fullPath;
Enabled = enabled;
IsBundled = System.IO.Path.GetExtension(containerPath)?.ToLower() == ".xci";
}
} }
} }

View file

@ -1,23 +1,10 @@
namespace Ryujinx.UI.Common.Models namespace Ryujinx.UI.Common.Models
{ {
public class TitleUpdateModel public record TitleUpdateModel(ulong TitleId, ulong Version, string DisplayVersion, string Path)
{ {
public ulong TitleId { get; } public bool IsBundled { get; } = System.IO.Path.GetExtension(Path)?.ToLower() == ".xci";
public ulong Version { get; }
public string DisplayVersion { get; }
public string Path { get; }
public bool IsBundled { get; }
public string TitleIdStr => TitleId.ToString("X16"); public string TitleIdStr => TitleId.ToString("X16");
public ulong TitleIdBase => TitleId & ~0x1FFFUL; public ulong TitleIdBase => TitleId & ~0x1FFFUL;
public TitleUpdateModel(ulong titleId, ulong version, string displayVersion, string path)
{
TitleId = titleId;
Version = version;
DisplayVersion = displayVersion;
Path = path;
IsBundled = System.IO.Path.GetExtension(path)?.ToLower() == ".xci";
}
} }
} }

View file

@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.ApplicationLibrary, viewModel.SelectedApplication);
} }
} }
@ -96,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.ApplicationLibrary, viewModel.SelectedApplication);
} }
} }

View file

@ -35,6 +35,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private readonly string _downloadableContentJsonPath; private readonly string _downloadableContentJsonPath;
private readonly VirtualFileSystem _virtualFileSystem; private readonly VirtualFileSystem _virtualFileSystem;
private readonly ApplicationLibrary _applicationLibrary;
private AvaloniaList<DownloadableContentModel> _downloadableContents = new(); private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
private AvaloniaList<DownloadableContentModel> _views = new(); private AvaloniaList<DownloadableContentModel> _views = new();
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new(); private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
@ -93,9 +94,10 @@ namespace Ryujinx.Ava.UI.ViewModels
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count); get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
} }
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{ {
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
_applicationLibrary = applicationLibrary;
_applicationData = applicationData; _applicationData = applicationData;
@ -145,12 +147,11 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
var content = new DownloadableContentModel(nca.Header.TitleId, var content = new DownloadableContentModel(nca.Header.TitleId,
downloadableContentContainer.ContainerPath, downloadableContentContainer.ContainerPath,
downloadableContentNca.FullPath, downloadableContentNca.FullPath);
downloadableContentNca.Enabled);
DownloadableContents.Add(content); DownloadableContents.Add(content);
if (content.Enabled) if (downloadableContentNca.Enabled)
{ {
SelectedDownloadableContents.Add(content); SelectedDownloadableContents.Add(content);
} }
@ -240,35 +241,24 @@ namespace Ryujinx.Ava.UI.ViewModels
return true; return true;
} }
using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem); if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs))
{
return false;
}
bool success = false; bool success = false;
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) foreach (var dlc in dlcs)
{ {
using var ncaFile = new UniqueRef<IFile>(); if (dlc.TitleIdBase != _applicationData.IdBase)
partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
if (nca == null)
{ {
continue; continue;
} }
if (nca.Header.ContentType == NcaContentType.PublicData) DownloadableContents.Add(dlc);
{ Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(dlc));
if (nca.GetProgramIdBase() != _applicationData.IdBase)
{
continue;
}
var content = new DownloadableContentModel(nca.Header.TitleId, path, fileEntry.FullPath, true);
DownloadableContents.Add(content);
Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(content));
success = true; success = true;
} }
}
if (success) if (success)
{ {
@ -282,6 +272,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public void Remove(DownloadableContentModel model) public void Remove(DownloadableContentModel model)
{ {
DownloadableContents.Remove(model); DownloadableContents.Remove(model);
SelectedDownloadableContents.Remove(model);
OnPropertyChanged(nameof(UpdateCount)); OnPropertyChanged(nameof(UpdateCount));
Sort(); Sort();
} }
@ -289,13 +280,15 @@ namespace Ryujinx.Ava.UI.ViewModels
public void RemoveAll() public void RemoveAll()
{ {
DownloadableContents.Clear(); DownloadableContents.Clear();
SelectedDownloadableContents.Clear();
OnPropertyChanged(nameof(UpdateCount)); OnPropertyChanged(nameof(UpdateCount));
Sort(); Sort();
} }
public void EnableAll() public void EnableAll()
{ {
SelectedDownloadableContents = new(DownloadableContents); SelectedDownloadableContents.Clear();
SelectedDownloadableContents.AddRange(DownloadableContents);
} }
public void DisableAll() public void DisableAll()
@ -303,6 +296,16 @@ namespace Ryujinx.Ava.UI.ViewModels
SelectedDownloadableContents.Clear(); SelectedDownloadableContents.Clear();
} }
public void Enable(DownloadableContentModel model)
{
SelectedDownloadableContents.ReplaceOrAdd(model, model);
}
public void Disable(DownloadableContentModel model)
{
SelectedDownloadableContents.Remove(model);
}
public void Save() public void Save()
{ {
_downloadableContentContainerList.Clear(); _downloadableContentContainerList.Clear();
@ -327,7 +330,7 @@ namespace Ryujinx.Ava.UI.ViewModels
container.DownloadableContentNcaList.Add(new DownloadableContentNca container.DownloadableContentNcaList.Add(new DownloadableContentNca
{ {
Enabled = downloadableContent.Enabled, Enabled = SelectedDownloadableContents.Contains(downloadableContent),
TitleId = downloadableContent.TitleId, TitleId = downloadableContent.TitleId,
FullPath = downloadableContent.FullPath, FullPath = downloadableContent.FullPath,
}); });

View file

@ -38,6 +38,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 ApplicationLibrary ApplicationLibrary { get; }
private ApplicationData ApplicationData { get; } private ApplicationData ApplicationData { get; }
private AvaloniaList<TitleUpdateModel> _titleUpdates = new(); private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
@ -78,9 +79,10 @@ namespace Ryujinx.Ava.UI.ViewModels
public IStorageProvider StorageProvider; public IStorageProvider StorageProvider;
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{ {
VirtualFileSystem = virtualFileSystem; VirtualFileSystem = virtualFileSystem;
ApplicationLibrary = applicationLibrary;
ApplicationData = applicationData; ApplicationData = applicationData;
@ -162,53 +164,35 @@ namespace Ryujinx.Ava.UI.ViewModels
return; return;
} }
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
try try
{ {
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, VirtualFileSystem); if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var titleUpdates))
Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.Patch, VirtualFileSystem, checkLevel);
Nca patchNca = null;
Nca controlNca = null;
if (updates.TryGetValue(ApplicationData.Id, out ContentMetaData content))
{
patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program);
controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control);
}
if (controlNca != null && patchNca != null)
{
ApplicationControlProperty controlData = new();
using UniqueRef<IFile> nacpFile = new();
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
var displayVersion = controlData.DisplayVersionString.ToString();
var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version, displayVersion, path);
TitleUpdates.Add(update);
if (selected)
{
Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = update);
}
}
else
{ {
if (!ignoreNotFound) if (!ignoreNotFound)
{ {
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); Dispatcher.UIThread.InvokeAsync(() =>
ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
}
return;
}
foreach (var titleUpdate in titleUpdates)
{
if (titleUpdate.TitleIdBase != ApplicationData.Id)
{
continue;
}
TitleUpdates.Add(titleUpdate);
if (selected)
{
Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = titleUpdate);
} }
} }
} } catch (Exception ex)
catch (Exception ex)
{ {
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path))); Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
} }
@ -254,7 +238,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
TitleUpdateWindowData.Paths.Add(update.Path); TitleUpdateWindowData.Paths.Add(update.Path);
if (update == SelectedUpdate) if (update == SelectedUpdate as TitleUpdateModel)
{ {
TitleUpdateWindowData.Selected = update.Path; TitleUpdateWindowData.Selected = update.Path;
} }

View file

@ -77,8 +77,9 @@
SelectionMode="Multiple, Toggle" SelectionMode="Multiple, Toggle"
Background="Transparent" Background="Transparent"
SelectionChanged="OnSelectionChanged" SelectionChanged="OnSelectionChanged"
SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}" SelectedItems="{Binding SelectedDownloadableContents}"
ItemsSource="{Binding Views}"> ItemsSource="{Binding Views}">
<!-- SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}" -->
<ListBox.DataTemplates> <ListBox.DataTemplates>
<DataTemplate <DataTemplate
DataType="models:DownloadableContentModel"> DataType="models:DownloadableContentModel">

View file

@ -24,21 +24,21 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent(); InitializeComponent();
} }
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{ {
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData); DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationLibrary, applicationData);
InitializeComponent(); InitializeComponent();
} }
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{ {
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
PrimaryButtonText = "", PrimaryButtonText = "",
SecondaryButtonText = "", SecondaryButtonText = "",
CloseButtonText = "", CloseButtonText = "",
Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData), Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationLibrary, applicationData),
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdBaseString), Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdBaseString),
}; };
@ -89,12 +89,7 @@ namespace Ryujinx.Ava.UI.Windows
{ {
if (content is DownloadableContentModel model) if (content is DownloadableContentModel model)
{ {
var index = ViewModel.DownloadableContents.IndexOf(model); ViewModel.Enable(model);
if (index != -1)
{
// ViewModel.DownloadableContents[index].Enabled = true;
}
} }
} }
@ -102,12 +97,7 @@ namespace Ryujinx.Ava.UI.Windows
{ {
if (content is DownloadableContentModel model) if (content is DownloadableContentModel model)
{ {
var index = ViewModel.DownloadableContents.IndexOf(model); ViewModel.Disable(model);
if (index != -1)
{
// ViewModel.DownloadableContents[index].Enabled = false;
}
} }
} }
} }

View file

@ -26,21 +26,21 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent(); InitializeComponent();
} }
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{ {
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData); DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationLibrary, applicationData);
InitializeComponent(); InitializeComponent();
} }
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData)
{ {
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
PrimaryButtonText = "", PrimaryButtonText = "",
SecondaryButtonText = "", SecondaryButtonText = "",
CloseButtonText = "", CloseButtonText = "",
Content = new TitleUpdateWindow(virtualFileSystem, applicationData), Content = new TitleUpdateWindow(virtualFileSystem, applicationLibrary, applicationData),
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdBaseString), Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdBaseString),
}; };