Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#7205 auto add dlc and updates #4

Merged
merged 35 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5e0b1cc
Add hooks to ApplicationLibrary for loading DLC/updates
J-Swift Aug 15, 2024
2e93c96
Trigger DLC/update load on games refresh
J-Swift Aug 15, 2024
e117108
Initial moving of DLC/updates to UI.Common
J-Swift Aug 16, 2024
8073b8d
Use new models in ApplicationLibrary
J-Swift Aug 16, 2024
48b7517
Make dlc/updates records; use ApplicationLibrary for loading logic
J-Swift Aug 16, 2024
472feb9
Fix a bug with DLC window; rework some logic
J-Swift Aug 17, 2024
a90a6b2
Extract DLC json load logic
J-Swift Aug 17, 2024
7850a2b
Move more DLC logic out of view model
J-Swift Aug 17, 2024
47e2cc6
Run formatter
J-Swift Aug 17, 2024
1eb7146
Run formatter for real
J-Swift Aug 17, 2024
57de6a7
Refactor more logic out of DLC manager VM
J-Swift Aug 17, 2024
867bc70
Auto-load bundled DLC on startup
J-Swift Aug 17, 2024
a381cea
Autoload DLC
J-Swift Aug 17, 2024
bc60126
Add setting for autoloading dlc/updates
J-Swift Aug 17, 2024
20e0dbe
Remove dead code; bind to AppLibrary apps directly in mainwindow
J-Swift Aug 18, 2024
50cd3ad
Stub out bulk dlc menu item
J-Swift Aug 18, 2024
1301356
Add localization; stub out bulk load updates
J-Swift Aug 19, 2024
eb54872
Set autoload dirs explicitly
J-Swift Aug 19, 2024
665d1d4
Remove autoload content checkbox
J-Swift Aug 19, 2024
7f854c3
Formatter
J-Swift Aug 19, 2024
3d7ede5
Begin extracting updates to match DLC refactors
J-Swift Aug 19, 2024
6bdf194
Add title update autoloading
J-Swift Aug 19, 2024
4bf4371
Reduce size of settings sections
J-Swift Aug 19, 2024
2b0e121
Better cache lookup for apps
J-Swift Aug 19, 2024
14c90d1
Dont reload entire library on game version change
J-Swift Aug 19, 2024
cd062ee
Format
J-Swift Aug 19, 2024
13bac28
Fix list enumeration
J-Swift Aug 20, 2024
aa26421
Remove ApplicationAdded event; always enumerate nsp when autoloading
J-Swift Aug 20, 2024
e4399b4
Remove stale todo
J-Swift Aug 20, 2024
e7f3f9c
Fix locale string bug
J-Swift Aug 20, 2024
bb24688
Some comments
J-Swift Aug 20, 2024
b3f3b19
Merge remote-tracking branch 'origin/master' into feature/auto-load-e…
J-Swift Aug 24, 2024
401e60e
Merge remote-tracking branch 'origin/master' into feature/auto-load-e…
J-Swift Aug 31, 2024
ecbea21
Merge remote-tracking branch 'origin/master' into feature/auto-load-e…
J-Swift Sep 7, 2024
16511aa
Merge remote-tracking branch 'origin/master' into feature/auto-load-e…
J-Swift Sep 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions src/Ryujinx.UI.Common/App/ApplicationAddedEventArgs.cs

This file was deleted.

549 changes: 532 additions & 17 deletions src/Ryujinx.UI.Common/App/ApplicationLibrary.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class ConfigurationFileFormat
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 51;
public const int CurrentVersion = 52;

/// <summary>
/// Version of the configuration file format
Expand Down Expand Up @@ -262,6 +262,11 @@ public class ConfigurationFileFormat
/// </summary>
public List<string> GameDirs { get; set; }

/// <summary>
/// A list of directories containing DLC/updates the user wants to autoload during library refreshes
/// </summary>
public List<string> AutoloadDirs { get; set; }

/// <summary>
/// A list of file types to be hidden in the games List
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ public WindowStartupSettings()
/// </summary>
public ReactiveObject<List<string>> GameDirs { get; private set; }

/// <summary>
/// A list of directories containing DLC/updates the user wants to autoload during library refreshes
/// </summary>
public ReactiveObject<List<string>> AutoloadDirs { get; private set; }

/// <summary>
/// A list of file types to be hidden in the games List
/// </summary>
Expand Down Expand Up @@ -192,6 +197,7 @@ public UISection()
GuiColumns = new Columns();
ColumnSort = new ColumnSortSettings();
GameDirs = new ReactiveObject<List<string>>();
AutoloadDirs = new ReactiveObject<List<string>>();
ShownFileTypes = new ShownFileTypeSettings();
WindowStartup = new WindowStartupSettings();
EnableCustomTheme = new ReactiveObject<bool>();
Expand Down Expand Up @@ -728,6 +734,7 @@ public ConfigurationFileFormat ToFileFormat()
SortAscending = UI.ColumnSort.SortAscending,
},
GameDirs = UI.GameDirs,
AutoloadDirs = UI.AutoloadDirs,
ShownFileTypes = new ShownFileTypes
{
NSP = UI.ShownFileTypes.NSP,
Expand Down Expand Up @@ -836,6 +843,7 @@ public void LoadDefault()
UI.ColumnSort.SortColumnId.Value = 0;
UI.ColumnSort.SortAscending.Value = false;
UI.GameDirs.Value = new List<string>();
UI.AutoloadDirs.Value = new List<string>();
UI.ShownFileTypes.NSP.Value = true;
UI.ShownFileTypes.PFS0.Value = true;
UI.ShownFileTypes.XCI.Value = true;
Expand Down Expand Up @@ -1477,6 +1485,15 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu
configurationFileUpdated = true;
}

if (configurationFileFormat.Version < 52)
{
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52.");

configurationFileFormat.AutoloadDirs = new();

configurationFileUpdated = true;
}

Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
Expand Down Expand Up @@ -1538,6 +1555,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu
UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId;
UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending;
UI.GameDirs.Value = configurationFileFormat.GameDirs;
UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs;
UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP;
UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0;
UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI;
Expand Down
135 changes: 135 additions & 0 deletions src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Utilities;
using Ryujinx.UI.Common.Models;
using System;
using System.Collections.Generic;
using System.IO;
using Path = System.IO.Path;

namespace Ryujinx.UI.Common.Helper
{
public static class DownloadableContentsHelper
{
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());

public static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase)
{
var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase);

if (!File.Exists(downloadableContentJsonPath))
{
return [];
}

try
{
var downloadableContentContainerList = JsonHelper.DeserializeFromFile(downloadableContentJsonPath,
_serializerContext.ListDownloadableContentContainer);
return LoadDownloadableContents(vfs, downloadableContentContainerList);
}
catch
{
Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize.");
return [];
}
}

public static void SaveDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
{
DownloadableContentContainer container = default;
List<DownloadableContentContainer> downloadableContentContainerList = new();

foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
{
if (container.ContainerPath != dlc.ContainerPath)
{
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
{
downloadableContentContainerList.Add(container);
}

container = new DownloadableContentContainer
{
ContainerPath = dlc.ContainerPath,
DownloadableContentNcaList = [],
};
}

container.DownloadableContentNcaList.Add(new DownloadableContentNca
{
Enabled = isEnabled,
TitleId = dlc.TitleId,
FullPath = dlc.FullPath,
});
}

if (!string.IsNullOrWhiteSpace(container.ContainerPath))
{
downloadableContentContainerList.Add(container);
}

var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase);
JsonHelper.SerializeToFile(downloadableContentJsonPath, downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer);
}

private static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContents(VirtualFileSystem vfs, List<DownloadableContentContainer> downloadableContentContainers)
{
var result = new List<(DownloadableContentModel, bool IsEnabled)>();

foreach (DownloadableContentContainer downloadableContentContainer in downloadableContentContainers)
{
if (!File.Exists(downloadableContentContainer.ContainerPath))
{
continue;
}

using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, vfs);

foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
using UniqueRef<IFile> ncaFile = new();

partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();

Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage());
if (nca == null)
{
continue;
}

var content = new DownloadableContentModel(nca.Header.TitleId,
downloadableContentContainer.ContainerPath,
downloadableContentNca.FullPath);

result.Add((content, downloadableContentNca.Enabled));
}
}

return result;
}

private static Nca TryOpenNca(VirtualFileSystem vfs, IStorage ncaStorage)
{
try
{
return new Nca(vfs.KeySet, ncaStorage);
}
catch (Exception) { }

return null;
}

private static string PathToGameDLCJson(ulong applicationIdBase)
{
return Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
}
}
}
162 changes: 162 additions & 0 deletions src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using LibHac.Common;
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.HLE.Utilities;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Models;
using System;
using System.Collections.Generic;
using System.IO;
using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers;
using TitleUpdateMetadata = Ryujinx.Common.Configuration.TitleUpdateMetadata;

namespace Ryujinx.UI.Common.Helper
{
public static class TitleUpdatesHelper
{
private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());

public static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase)
{
var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);

if (!File.Exists(titleUpdatesJsonPath))
{
return [];
}

try
{
var titleUpdateWindowData = JsonHelper.DeserializeFromFile(titleUpdatesJsonPath, _serializerContext.TitleUpdateMetadata);
return LoadTitleUpdates(vfs, titleUpdateWindowData, applicationIdBase);
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {applicationIdBase:x16} at {titleUpdatesJsonPath}");
return [];
}
}

public static void SaveTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates)
{
var titleUpdateWindowData = new TitleUpdateMetadata
{
Selected = "",
Paths = [],
};

foreach ((TitleUpdateModel update, bool isSelected) in updates)
{
titleUpdateWindowData.Paths.Add(update.Path);
if (isSelected)
{
if (!string.IsNullOrEmpty(titleUpdateWindowData.Selected))
{
Logger.Error?.Print(LogClass.Application,
$"Tried to save two updates as 'IsSelected' for {applicationIdBase:x16}");
return;
}

titleUpdateWindowData.Selected = update.Path;
}
}

var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
}

private static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase)
{
var result = new List<(TitleUpdateModel, bool IsSelected)>();

IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;

foreach (string path in titleUpdateMetadata.Paths)
{
if (!File.Exists(path))
{
continue;
}

try
{
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, vfs);

Dictionary<ulong, ContentMetaData> updates =
pfs.GetContentData(ContentMetaType.Patch, vfs, checkLevel);

Nca patchNca = null;
Nca controlNca = null;

if (!updates.TryGetValue(applicationIdBase, out ContentMetaData content))
{
continue;
}

patchNca = content.GetNcaByType(vfs.KeySet, ContentType.Program);
controlNca = content.GetNcaByType(vfs.KeySet, ContentType.Control);

if (controlNca == null || patchNca == null)
{
continue;
}

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

result.Add((update, path == titleUpdateMetadata.Selected));
}
catch (MissingKeyException exception)
{
Logger.Warning?.Print(LogClass.Application,
$"Your key set is missing a key with the name: {exception.Name}");
}
catch (InvalidDataException)
{
Logger.Warning?.Print(LogClass.Application,
$"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {path}");
}
catch (IOException exception)
{
Logger.Warning?.Print(LogClass.Application, exception.Message);
}
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application,
$"The file encountered was not of a valid type. File: '{path}' Error: {exception}");
}
}

return result;
}

private static string PathToGameUpdatesJson(ulong applicationIdBase)
{
return Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "updates.json");
}
}
}
Loading
Loading