Add hooks to ApplicationLibrary for loading DLC/updates

This commit is contained in:
Jimmy Reichley 2024-08-14 21:54:32 -04:00
parent 0137c9e635
commit 5e0b1ccd6e
No known key found for this signature in database
GPG key ID: 67715DC5A329803C
3 changed files with 373 additions and 7 deletions

View file

@ -16,6 +16,7 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.HLE.Utilities;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Configuration.System; using Ryujinx.UI.Common.Configuration.System;
using System; using System;
@ -37,6 +38,8 @@ namespace Ryujinx.UI.App.Common
public Language DesiredLanguage { get; set; } public Language DesiredLanguage { get; set; }
public event EventHandler<ApplicationAddedEventArgs> ApplicationAdded; public event EventHandler<ApplicationAddedEventArgs> ApplicationAdded;
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated; public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
public event EventHandler<TitleUpdateAddedEventArgs> TitleUpdateAdded;
public event EventHandler<DownloadableContentAddedEventArgs> DownloadableContentAdded;
private readonly byte[] _nspIcon; private readonly byte[] _nspIcon;
private readonly byte[] _xciIcon; private readonly byte[] _xciIcon;
@ -275,7 +278,7 @@ namespace Ryujinx.UI.App.Common
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
Logger.Warning?.Print(LogClass.Application, $"The file was not found: '{applicationPath}'"); Logger.Warning?.Print(LogClass.Application, $"The file was not found: '{applicationPath}'");
return false; return false;
} }
@ -473,6 +476,125 @@ namespace Ryujinx.UI.App.Common
return true; return true;
} }
public bool TryGetDownloadableContentFromFile(string filePath, out List<(ulong TitleId, string ContainerPath, string FullPath)> titleUpdates)
{
titleUpdates = [];
try
{
string extension = Path.GetExtension(filePath).ToLower();
using FileStream file = new(filePath, FileMode.Open, FileAccess.Read);
switch (extension)
{
case ".xci":
case ".nsp":
{
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem);
// Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.AddOnContent, _virtualFileSystem, checkLevel);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryOpenNca(ncaFile.Get.AsStorage());
if (nca == null)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.PublicData)
{
titleUpdates.Add((nca.Header.TitleId, filePath, fileEntry.FullPath));
}
}
if (titleUpdates.Count == 0)
{
return false;
}
return true;
}
}
}
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: {filePath}");
}
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: '{filePath}' Error: {exception}");
}
return false;
}
public bool TryGetTitleUpdatesFromFile(string filePath, out List<(ulong, string)> titleUpdates)
{
titleUpdates = [];
try
{
string extension = Path.GetExtension(filePath).ToLower();
using FileStream file = new(filePath, FileMode.Open, FileAccess.Read);
switch (extension)
{
case ".xci":
case ".nsp":
{
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem);
Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel);
if (updates.Count == 0)
{
return false;
}
titleUpdates.AddRange(updates.Select(it => (it.Key, filePath)));
return true;
}
}
}
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: {filePath}");
}
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: '{filePath}' Error: {exception}");
}
return false;
}
public void CancelLoading() public void CancelLoading()
{ {
@ -524,12 +646,12 @@ namespace Ryujinx.UI.App.Common
IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", options).Where(file => IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", options).Where(file =>
{ {
return return
(Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) ||
(Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value) || (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value) ||
(Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value) || (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value) ||
(Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value) || (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value) ||
(Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value) || (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value) ||
(Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value); (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value);
}); });
foreach (string app in files) foreach (string app in files)
@ -610,6 +732,208 @@ namespace Ryujinx.UI.App.Common
} }
} }
public void LoadDownloadableContents(List<string> appDirs)
{
// Logger.Warning?.Print(LogClass.Application, "JIMMY load DLC");
_cancellationToken = new CancellationTokenSource();
// Builds the applications list with paths to found applications
List<string> applicationPaths = new();
try
{
foreach (string appDir in appDirs)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
if (!Directory.Exists(appDir))
{
Logger.Warning?.Print(LogClass.Application,
$"The specified game directory \"{appDir}\" does not exist.");
continue;
}
try
{
EnumerationOptions options = new()
{
RecurseSubdirectories = true, IgnoreInaccessible = false,
};
IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", options).Where(
file =>
{
return
(Path.GetExtension(file).ToLower() is ".nsp" &&
ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) ||
(Path.GetExtension(file).ToLower() is ".xci" &&
ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value);
});
foreach (string app in files)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
var fileInfo = new FileInfo(app);
try
{
var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName;
applicationPaths.Add(fullPath);
}
catch (IOException exception)
{
Logger.Warning?.Print(LogClass.Application,
$"Failed to resolve the full path to file: \"{app}\" Error: {exception}");
}
}
}
catch (UnauthorizedAccessException)
{
Logger.Warning?.Print(LogClass.Application,
$"Failed to get access to directory: \"{appDir}\"");
}
}
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
foreach (string applicationPath in applicationPaths)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
if (TryGetDownloadableContentFromFile(applicationPath, out List<(ulong, string, string)> applications))
{
foreach (var application in applications)
{
OnDownloadableContentAdded(new DownloadableContentAddedEventArgs
{
TitleId = application.Item1,
ContainerFilePath = application.Item2,
NcaPath = application.Item3
});
}
}
}
}
finally
{
_cancellationToken.Dispose();
_cancellationToken = null;
}
}
public void LoadTitleUpdates(List<string> appDirs)
{
// Logger.Warning?.Print(LogClass.Application, "JIMMY title updates");
_cancellationToken = new CancellationTokenSource();
// Builds the applications list with paths to found applications
List<string> applicationPaths = new();
try
{
foreach (string appDir in appDirs)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
if (!Directory.Exists(appDir))
{
Logger.Warning?.Print(LogClass.Application,
$"The specified game directory \"{appDir}\" does not exist.");
continue;
}
try
{
EnumerationOptions options = new()
{
RecurseSubdirectories = true, IgnoreInaccessible = false,
};
IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", options)
.Where(file =>
{
return
(Path.GetExtension(file).ToLower() is ".nsp" &&
ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) ||
(Path.GetExtension(file).ToLower() is ".xci" &&
ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value);
});
foreach (string app in files)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
var fileInfo = new FileInfo(app);
try
{
var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ??
fileInfo.FullName;
applicationPaths.Add(fullPath);
}
catch (IOException exception)
{
Logger.Warning?.Print(LogClass.Application,
$"Failed to resolve the full path to file: \"{app}\" Error: {exception}");
}
}
}
catch (UnauthorizedAccessException)
{
Logger.Warning?.Print(LogClass.Application,
$"Failed to get access to directory: \"{appDir}\"");
}
}
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
foreach (string applicationPath in applicationPaths)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
if (TryGetTitleUpdatesFromFile(applicationPath,
out List<(ulong, string)> titleUpdates))
{
foreach (var application in titleUpdates)
{
OnTitleUpdateAdded(new TitleUpdateAddedEventArgs()
{
TitleId = application.Item1,
FilePath = application.Item2,
});
}
}
}
}
finally
{
_cancellationToken.Dispose();
_cancellationToken = null;
}
}
protected void OnApplicationAdded(ApplicationAddedEventArgs e) protected void OnApplicationAdded(ApplicationAddedEventArgs e)
{ {
ApplicationAdded?.Invoke(null, e); ApplicationAdded?.Invoke(null, e);
@ -619,6 +943,16 @@ namespace Ryujinx.UI.App.Common
{ {
ApplicationCountUpdated?.Invoke(null, e); ApplicationCountUpdated?.Invoke(null, e);
} }
protected void OnTitleUpdateAdded(TitleUpdateAddedEventArgs e)
{
TitleUpdateAdded?.Invoke(null, e);
}
protected void OnDownloadableContentAdded(DownloadableContentAddedEventArgs e)
{
DownloadableContentAdded?.Invoke(null, e);
}
public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null) public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
{ {
@ -936,5 +1270,16 @@ namespace Ryujinx.UI.App.Common
return false; return false;
} }
private Nca TryOpenNca(IStorage ncaStorage)
{
try
{
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
}
catch (Exception ex) { }
return null;
}
} }
} }

View file

@ -0,0 +1,11 @@
using System;
namespace Ryujinx.UI.App.Common
{
public class DownloadableContentAddedEventArgs : EventArgs
{
public ulong TitleId { get; set; }
public string ContainerFilePath { get; set; }
public string NcaPath { get; set; }
}
}

View file

@ -0,0 +1,10 @@
using System;
namespace Ryujinx.UI.App.Common
{
public class TitleUpdateAddedEventArgs : EventArgs
{
public ulong TitleId { get; set; }
public string FilePath { get; set; }
}
}