Skip to content

Commit

Permalink
Merge pull request #4 from Ryujinx-NX/#7205-Auto-add-DLC-and-updates
Browse files Browse the repository at this point in the history
#7205 auto add dlc and updates
  • Loading branch information
ryzendew authored Oct 5, 2024
2 parents 563c342 + 16511aa commit 6ecfd2f
Show file tree
Hide file tree
Showing 30 changed files with 1,505 additions and 455 deletions.
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

0 comments on commit 6ecfd2f

Please sign in to comment.