Add hooks to ApplicationLibrary for loading DLC/updates
This commit is contained in:
parent
0137c9e635
commit
5e0b1ccd6e
3 changed files with 373 additions and 7 deletions
|
@ -16,6 +16,7 @@ using Ryujinx.HLE.FileSystem;
|
|||
using Ryujinx.HLE.HOS.SystemState;
|
||||
using Ryujinx.HLE.Loaders.Npdm;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.HLE.Utilities;
|
||||
using Ryujinx.UI.Common.Configuration;
|
||||
using Ryujinx.UI.Common.Configuration.System;
|
||||
using System;
|
||||
|
@ -37,6 +38,8 @@ namespace Ryujinx.UI.App.Common
|
|||
public Language DesiredLanguage { get; set; }
|
||||
public event EventHandler<ApplicationAddedEventArgs> ApplicationAdded;
|
||||
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
|
||||
public event EventHandler<TitleUpdateAddedEventArgs> TitleUpdateAdded;
|
||||
public event EventHandler<DownloadableContentAddedEventArgs> DownloadableContentAdded;
|
||||
|
||||
private readonly byte[] _nspIcon;
|
||||
private readonly byte[] _xciIcon;
|
||||
|
@ -474,6 +477,125 @@ namespace Ryujinx.UI.App.Common
|
|||
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()
|
||||
{
|
||||
_cancellationToken?.Cancel();
|
||||
|
@ -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)
|
||||
{
|
||||
ApplicationAdded?.Invoke(null, e);
|
||||
|
@ -620,6 +944,16 @@ namespace Ryujinx.UI.App.Common
|
|||
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)
|
||||
{
|
||||
string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui");
|
||||
|
@ -936,5 +1270,16 @@ namespace Ryujinx.UI.App.Common
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Nca TryOpenNca(IStorage ncaStorage)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
|
||||
}
|
||||
catch (Exception ex) { }
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
10
src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs
Normal file
10
src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs
Normal 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; }
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue