From ad3d2fb5a9326577a9ea1b67e06a34b09236dd8d Mon Sep 17 00:00:00 2001 From: Xpl0itR Date: Sun, 12 Apr 2020 22:02:37 +0100 Subject: [PATCH] Implement update loader and log loaded application info (#1023) * Implement update loader * Add title version to titlebar and log loaded application info * nits * requested changes --- .../Configuration/ConfigurationFileFormat.cs | 2 +- .../Configuration/TitleUpdateMetadata.cs | 10 + Ryujinx.HLE/HOS/Horizon.cs | 70 +++++- Ryujinx.HLE/HOS/ProgramLoader.cs | 7 +- Ryujinx.HLE/Loaders/Npdm/Npdm.cs | 4 +- Ryujinx/Ryujinx.csproj | 2 + Ryujinx/Ui/ApplicationLibrary.cs | 76 ++++++- Ryujinx/Ui/GLRenderer.cs | 11 +- Ryujinx/Ui/GameTableContextMenu.cs | 46 ++-- Ryujinx/Ui/GameTableContextMenu.glade | 14 ++ Ryujinx/Ui/TitleUpdateWindow.cs | 204 ++++++++++++++++++ Ryujinx/Ui/TitleUpdateWindow.glade | 198 +++++++++++++++++ 12 files changed, 605 insertions(+), 39 deletions(-) create mode 100644 Ryujinx.Common/Configuration/TitleUpdateMetadata.cs create mode 100644 Ryujinx/Ui/TitleUpdateWindow.cs create mode 100644 Ryujinx/Ui/TitleUpdateWindow.glade diff --git a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs index 9123f0443..f47dc4b3b 100644 --- a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs +++ b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs @@ -200,7 +200,7 @@ namespace Ryujinx.Configuration File.WriteAllText(path, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson()); } - private class ConfigurationEnumFormatter : IJsonFormatter + public class ConfigurationEnumFormatter : IJsonFormatter where T : struct { public void Serialize(ref JsonWriter writer, T value, IJsonFormatterResolver formatterResolver) diff --git a/Ryujinx.Common/Configuration/TitleUpdateMetadata.cs b/Ryujinx.Common/Configuration/TitleUpdateMetadata.cs new file mode 100644 index 000000000..ea208e9c2 --- /dev/null +++ b/Ryujinx.Common/Configuration/TitleUpdateMetadata.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ryujinx.Common.Configuration +{ + public struct TitleUpdateMetadata + { + public string Selected { get; set; } + public List Paths { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 302ee100c..30c8098e9 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -7,6 +7,7 @@ using LibHac.FsSystem.NcaUtils; using LibHac.Ncm; using LibHac.Ns; using LibHac.Spl; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.HOS.Font; @@ -30,6 +31,8 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading; +using Utf8Json; +using Utf8Json.Resolvers; using TimeServiceManager = Ryujinx.HLE.HOS.Services.Time.TimeManager; using NsoExecutable = Ryujinx.HLE.Loaders.Executables.NsoExecutable; @@ -117,6 +120,10 @@ namespace Ryujinx.HLE.HOS public ulong TitleId { get; private set; } public string TitleIdText => TitleId.ToString("x16"); + public string TitleVersionString { get; private set; } + + public bool TitleIs64Bit { get; private set; } + public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; } public int GlobalAccessLogMode { get; set; } @@ -368,6 +375,8 @@ namespace Ryujinx.HLE.HOS TitleName = ControlData.Value.Titles.ToArray() .FirstOrDefault(x => x.Name[0] != 0).Name.ToString(); } + + TitleVersionString = ControlData.Value.DisplayVersion.ToString(); } } else @@ -455,6 +464,54 @@ namespace Ryujinx.HLE.HOS IStorage dataStorage = null; IFileSystem codeFs = null; + if (File.Exists(Path.Combine(Device.FileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json"))) + { + using (Stream stream = File.OpenRead(Path.Combine(Device.FileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json"))) + { + IJsonFormatterResolver resolver = CompositeResolver.Create(StandardResolver.AllowPrivateSnakeCase); + string updatePath = JsonSerializer.Deserialize(stream, resolver).Selected; + + if (File.Exists(updatePath)) + { + FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read); + PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); + + foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik")) + { + Result result = nsp.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read); + + if (result.IsSuccess()) + { + Ticket ticket = new Ticket(ticketFile.AsStream()); + + KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet))); + } + } + + foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca")) + { + nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new Nca(KeySet, ncaFile.AsStorage()); + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != mainNca.Header.TitleId.ToString("x16")) + { + break; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + patchNca = nca; + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + } + } + } + if (patchNca == null) { if (mainNca.CanOpenSection(NcaSectionType.Data)) @@ -498,7 +555,8 @@ namespace Ryujinx.HLE.HOS LoadExeFs(codeFs, out Npdm metaData); - TitleId = metaData.Aci0.TitleId; + TitleId = metaData.Aci0.TitleId; + TitleIs64Bit = metaData.Is64Bit; if (controlNca != null) { @@ -513,6 +571,8 @@ namespace Ryujinx.HLE.HOS { EnsureSaveData(new TitleId(TitleId)); } + + Logger.PrintInfo(LogClass.Loader, $"Application Loaded: {TitleName} v{TitleVersionString} [{TitleIdText}] [{(TitleIs64Bit ? "64-bit" : "32-bit")}]"); } private void LoadExeFs(IFileSystem codeFs, out Npdm metaData) @@ -551,7 +611,8 @@ namespace Ryujinx.HLE.HOS } } - TitleId = metaData.Aci0.TitleId; + TitleId = metaData.Aci0.TitleId; + TitleIs64Bit = metaData.Is64Bit; LoadNso("rtld"); LoadNso("main"); @@ -653,8 +714,9 @@ namespace Ryujinx.HLE.HOS ContentManager.LoadEntries(Device); - TitleName = metaData.TitleName; - TitleId = metaData.Aci0.TitleId; + TitleName = metaData.TitleName; + TitleId = metaData.Aci0.TitleId; + TitleIs64Bit = metaData.Is64Bit; ProgramLoader.LoadStaticObjects(this, metaData, new IExecutable[] { staticObject }); } diff --git a/Ryujinx.HLE/HOS/ProgramLoader.cs b/Ryujinx.HLE/HOS/ProgramLoader.cs index d6f3d1d3e..044bc9c6d 100644 --- a/Ryujinx.HLE/HOS/ProgramLoader.cs +++ b/Ryujinx.HLE/HOS/ProgramLoader.cs @@ -126,14 +126,9 @@ namespace Ryujinx.HLE.HOS IExecutable[] staticObjects, byte[] arguments = null) { - if (!metaData.Is64Bits) - { - Logger.PrintWarning(LogClass.Loader, "32-bits application detected."); - } - ulong argsStart = 0; int argsSize = 0; - ulong codeStart = metaData.Is64Bits ? 0x8000000UL : 0x200000UL; + ulong codeStart = metaData.Is64Bit ? 0x8000000UL : 0x200000UL; int codeSize = 0; ulong[] nsoBase = new ulong[staticObjects.Length]; diff --git a/Ryujinx.HLE/Loaders/Npdm/Npdm.cs b/Ryujinx.HLE/Loaders/Npdm/Npdm.cs index 4400793f1..345721c7a 100644 --- a/Ryujinx.HLE/Loaders/Npdm/Npdm.cs +++ b/Ryujinx.HLE/Loaders/Npdm/Npdm.cs @@ -12,7 +12,7 @@ namespace Ryujinx.HLE.Loaders.Npdm private const int MetaMagic = 'M' << 0 | 'E' << 8 | 'T' << 16 | 'A' << 24; public byte MmuFlags { get; private set; } - public bool Is64Bits { get; private set; } + public bool Is64Bit { get; private set; } public byte MainThreadPriority { get; private set; } public byte DefaultCpuId { get; private set; } public int PersonalMmHeapSize { get; private set; } @@ -37,7 +37,7 @@ namespace Ryujinx.HLE.Loaders.Npdm MmuFlags = reader.ReadByte(); - Is64Bits = (MmuFlags & 1) != 0; + Is64Bit = (MmuFlags & 1) != 0; reader.ReadByte(); diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj index d82c62f70..9bb79b093 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -48,6 +48,7 @@ + @@ -69,6 +70,7 @@ + diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs index 27a0f0ce2..82401b698 100644 --- a/Ryujinx/Ui/ApplicationLibrary.cs +++ b/Ryujinx/Ui/ApplicationLibrary.cs @@ -9,6 +9,7 @@ using LibHac.Ncm; using LibHac.Ns; using LibHac.Spl; using Ryujinx.Common.Logging; +using Ryujinx.Common.Configuration; using Ryujinx.Configuration.System; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.Loaders.Npdm; @@ -218,7 +219,7 @@ namespace Ryujinx.Ui controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); // Get the title name, title ID, developer name and version number from the NACP - version = controlHolder.Value.DisplayVersion.ToString(); + version = IsUpdateApplied(titleId, out string updateVersion) ? updateVersion : controlHolder.Value.DisplayVersion.ToString(); GetNameIdDeveloper(ref controlHolder.Value, out titleName, out _, out developer); @@ -400,11 +401,11 @@ namespace Ryujinx.Ui if (result.IsSuccess()) { - saveDataPath = Path.Combine(virtualFileSystem.GetNandPath(), $"user/save/{saveDataInfo.SaveDataId:x16}"); + saveDataPath = Path.Combine(virtualFileSystem.GetNandPath(), "user", "save", saveDataInfo.SaveDataId.ToString("x16")); } } - ApplicationData data = new ApplicationData() + ApplicationData data = new ApplicationData { Favorite = appMetadata.Favorite, Icon = applicationIcon, @@ -629,5 +630,72 @@ namespace Ryujinx.Ui titleId = "0000000000000000"; } } + + private static bool IsUpdateApplied(string titleId, out string version) + { + string jsonPath = Path.Combine(_virtualFileSystem.GetBasePath(), "games", titleId, "updates.json"); + + if (File.Exists(jsonPath)) + { + using (Stream stream = File.OpenRead(jsonPath)) + { + IJsonFormatterResolver resolver = CompositeResolver.Create(StandardResolver.AllowPrivateSnakeCase); + string updatePath = JsonSerializer.Deserialize(stream, resolver).Selected; + + if (!File.Exists(updatePath)) + { + version = ""; + + return false; + } + + using (FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read)) + { + PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); + + foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik")) + { + Result result = nsp.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read); + + if (result.IsSuccess()) + { + Ticket ticket = new Ticket(ticketFile.AsStream()); + + _virtualFileSystem.KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_virtualFileSystem.KeySet))); + } + } + + foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca")) + { + nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) + { + break; + } + + if (nca.Header.ContentType == NcaContentType.Control) + { + ApplicationControlProperty controlData = new ApplicationControlProperty(); + + nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + nacpFile.Read(out long _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + version = controlData.DisplayVersion.ToString(); + + return true; + } + } + } + } + } + + version = ""; + + return false; + } } -} +} \ No newline at end of file diff --git a/Ryujinx/Ui/GLRenderer.cs b/Ryujinx/Ui/GLRenderer.cs index f69d88ce0..335b06a84 100644 --- a/Ryujinx/Ui/GLRenderer.cs +++ b/Ryujinx/Ui/GLRenderer.cs @@ -192,12 +192,17 @@ namespace Ryujinx.Ui parent.Present(); string titleNameSection = string.IsNullOrWhiteSpace(_device.System.TitleName) ? string.Empty - : " | " + _device.System.TitleName; + : $" - {_device.System.TitleName}"; + + string titleVersionSection = string.IsNullOrWhiteSpace(_device.System.TitleVersionString) ? string.Empty + : $" v{_device.System.TitleVersionString}"; string titleIdSection = string.IsNullOrWhiteSpace(_device.System.TitleIdText) ? string.Empty - : " | " + _device.System.TitleIdText.ToUpper(); + : $" ({_device.System.TitleIdText.ToUpper()})"; - parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleIdSection}"; + string titleArchSection = _device.System.TitleIs64Bit ? " (64-bit)" : " (32-bit)"; + + parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}"; }); Thread renderLoopThread = new Thread(Render) diff --git a/Ryujinx/Ui/GameTableContextMenu.cs b/Ryujinx/Ui/GameTableContextMenu.cs index 5b3b9df42..0796d95d7 100644 --- a/Ryujinx/Ui/GameTableContextMenu.cs +++ b/Ryujinx/Ui/GameTableContextMenu.cs @@ -12,7 +12,6 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using System; using System.Buffers; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; @@ -38,6 +37,7 @@ namespace Ryujinx.Ui #pragma warning disable IDE0044 [GUI] MenuItem _openSaveUserDir; [GUI] MenuItem _openSaveDeviceDir; + [GUI] MenuItem _manageTitleUpdates; [GUI] MenuItem _extractRomFs; [GUI] MenuItem _extractExeFs; [GUI] MenuItem _extractLogo; @@ -51,21 +51,21 @@ namespace Ryujinx.Ui { builder.Autoconnect(this); - _openSaveUserDir.Activated += OpenSaveUserDir_Clicked; - _openSaveDeviceDir.Activated += OpenSaveDeviceDir_Clicked; - - _openSaveUserDir.Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0; - _openSaveDeviceDir.Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0; - - _extractRomFs.Activated += ExtractRomFs_Clicked; - _extractExeFs.Activated += ExtractExeFs_Clicked; - _extractLogo.Activated += ExtractLogo_Clicked; - _gameTableStore = gameTableStore; _rowIter = rowIter; _virtualFileSystem = virtualFileSystem; _controlData = controlData; + _openSaveUserDir.Activated += OpenSaveUserDir_Clicked; + _openSaveDeviceDir.Activated += OpenSaveDeviceDir_Clicked; + _manageTitleUpdates.Activated += ManageTitleUpdates_Clicked; + _extractRomFs.Activated += ExtractRomFs_Clicked; + _extractExeFs.Activated += ExtractExeFs_Clicked; + _extractLogo.Activated += ExtractLogo_Clicked; + + _openSaveUserDir.Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0; + _openSaveDeviceDir.Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0; + string ext = System.IO.Path.GetExtension(_gameTableStore.GetValue(_rowIter, 9).ToString()).ToLower(); if (ext != ".nca" && ext != ".nsp" && ext != ".pfs0" && ext != ".xci") { @@ -86,10 +86,10 @@ namespace Ryujinx.Ui // Savedata was not found. Ask the user if they want to create it using MessageDialog messageDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, null) { - Title = "Ryujinx", - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), - Text = $"There is no savedata for {titleName} [{titleId:x16}]", - SecondaryText = "Would you like to create savedata for this game?", + Title = "Ryujinx", + Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), + Text = $"There is no savedata for {titleName} [{titleId:x16}]", + SecondaryText = "Would you like to create savedata for this game?", WindowPosition = WindowPosition.Center }; @@ -107,7 +107,7 @@ namespace Ryujinx.Ui control = ref new BlitStruct(1).Value; // The set sizes don't actually matter as long as they're non-zero because we use directory savedata. - control.UserAccountSaveDataSize = 0x4000; + control.UserAccountSaveDataSize = 0x4000; control.UserAccountSaveDataJournalSize = 0x4000; Logger.PrintWarning(LogClass.Application, @@ -415,7 +415,7 @@ namespace Ryujinx.Ui private void OpenSaveUserDir_Clicked(object sender, EventArgs args) { string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); + string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); if (!ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) { @@ -449,11 +449,10 @@ namespace Ryujinx.Ui }); } - // Events private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) { string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); + string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); if (!ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) { @@ -468,6 +467,15 @@ namespace Ryujinx.Ui OpenSaveDir(titleName, titleIdNumber, filter); } + private void ManageTitleUpdates_Clicked(object sender, EventArgs args) + { + string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; + string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); + + TitleUpdateWindow titleUpdateWindow = new TitleUpdateWindow(titleId, titleName, _virtualFileSystem); + titleUpdateWindow.Show(); + } + private void ExtractRomFs_Clicked(object sender, EventArgs args) { ExtractSection(NcaSectionType.Data); diff --git a/Ryujinx/Ui/GameTableContextMenu.glade b/Ryujinx/Ui/GameTableContextMenu.glade index 96b493399..8f71ecf73 100644 --- a/Ryujinx/Ui/GameTableContextMenu.glade +++ b/Ryujinx/Ui/GameTableContextMenu.glade @@ -29,6 +29,20 @@ False + + + True + False + Manage Title Updates + True + + + + + True + False + + True diff --git a/Ryujinx/Ui/TitleUpdateWindow.cs b/Ryujinx/Ui/TitleUpdateWindow.cs new file mode 100644 index 000000000..01025d6dd --- /dev/null +++ b/Ryujinx/Ui/TitleUpdateWindow.cs @@ -0,0 +1,204 @@ +using Gtk; +using JsonPrettyPrinterPlus; +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; +using LibHac.Ns; +using LibHac.Spl; +using Ryujinx.Common.Configuration; +using Ryujinx.HLE.FileSystem; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Utf8Json; +using Utf8Json.Resolvers; + +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.Ui +{ + public class TitleUpdateWindow : Window + { + private readonly string _titleId; + private readonly VirtualFileSystem _virtualFileSystem; + + private TitleUpdateMetadata _titleUpdateWindowData; + private Dictionary _radioButtonToPathDictionary = new Dictionary(); + +#pragma warning disable CS0649 +#pragma warning disable IDE0044 + [GUI] Label _baseTitleInfoLabel; + [GUI] Box _availableUpdatesBox; + [GUI] RadioButton _noUpdateRadioButton; +#pragma warning restore CS0649 +#pragma warning restore IDE0044 + + public TitleUpdateWindow(string titleId, string titleName, VirtualFileSystem virtualFileSystem) : this(new Builder("Ryujinx.Ui.TitleUpdateWindow.glade"), titleId, titleName, virtualFileSystem) { } + + private TitleUpdateWindow(Builder builder, string titleId, string titleName, VirtualFileSystem virtualFileSystem) : base(builder.GetObject("_titleUpdateWindow").Handle) + { + builder.Autoconnect(this); + + _titleId = titleId; + _virtualFileSystem = virtualFileSystem; + + try + { + using (Stream stream = File.OpenRead(System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", _titleId, "updates.json"))) + { + IJsonFormatterResolver resolver = CompositeResolver.Create(StandardResolver.AllowPrivateSnakeCase); + + _titleUpdateWindowData = JsonSerializer.Deserialize(stream, resolver); + } + } + catch + { + _titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = "", + Paths = new List() + }; + } + + _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId}]"; + + foreach (string path in _titleUpdateWindowData.Paths) + { + AddUpdate(path); + } + + _noUpdateRadioButton.Active = true; + foreach (KeyValuePair keyValuePair in _radioButtonToPathDictionary) + { + if (keyValuePair.Value == _titleUpdateWindowData.Selected) + { + keyValuePair.Key.Active = true; + } + } + } + + private void AddUpdate(string path) + { + if (File.Exists(path)) + { + using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); + + foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik")) + { + Result result = nsp.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read); + + if (result.IsSuccess()) + { + Ticket ticket = new Ticket(ticketFile.AsStream()); + + _virtualFileSystem.KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_virtualFileSystem.KeySet))); + } + } + + foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca")) + { + nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" == _titleId) + { + if (nca.Header.ContentType == NcaContentType.Control) + { + ApplicationControlProperty controlData = new ApplicationControlProperty(); + + nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Read(out long _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersion.ToString()} - {path}"); + radioButton.JoinGroup(_noUpdateRadioButton); + + _availableUpdatesBox.Add(radioButton); + _radioButtonToPathDictionary.Add(radioButton, path); + + radioButton.Show(); + radioButton.Active = true; + } + } + else + { + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); + break; + } + } + } + } + } + + private void AddButton_Clicked(object sender, EventArgs args) + { + FileChooserDialog fileChooser = new FileChooserDialog("Select update files", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept) + { + SelectMultiple = true, + Filter = new FileFilter() + }; + fileChooser.SetPosition(WindowPosition.Center); + fileChooser.Filter.AddPattern("*.nsp"); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + foreach (string path in fileChooser.Filenames) + { + AddUpdate(path); + } + } + + fileChooser.Dispose(); + } + + private void RemoveButton_Clicked(object sender, EventArgs args) + { + foreach (RadioButton radioButton in _noUpdateRadioButton.Group) + { + if (radioButton.Label != "No Update" && radioButton.Active) + { + _availableUpdatesBox.Remove(radioButton); + _radioButtonToPathDictionary.Remove(radioButton); + radioButton.Dispose(); + } + } + } + + private void SaveButton_Clicked(object sender, EventArgs args) + { + _titleUpdateWindowData.Paths.Clear(); + foreach (string paths in _radioButtonToPathDictionary.Values) + { + _titleUpdateWindowData.Paths.Add(paths); + } + + foreach (RadioButton radioButton in _noUpdateRadioButton.Group) + { + if (radioButton.Active) + { + _titleUpdateWindowData.Selected = _radioButtonToPathDictionary.TryGetValue(radioButton, out string updatePath) ? updatePath : ""; + } + } + + IJsonFormatterResolver resolver = CompositeResolver.Create(StandardResolver.AllowPrivateSnakeCase); + + string path = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", _titleId, "updates.json"); + byte[] data = JsonSerializer.Serialize(_titleUpdateWindowData, resolver); + + File.WriteAllText(path, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson()); + + MainWindow.UpdateGameTable(); + Dispose(); + } + + private void CancelButton_Clicked(object sender, EventArgs args) + { + Dispose(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/TitleUpdateWindow.glade b/Ryujinx/Ui/TitleUpdateWindow.glade new file mode 100644 index 000000000..081dc3eaa --- /dev/null +++ b/Ryujinx/Ui/TitleUpdateWindow.glade @@ -0,0 +1,198 @@ + + + + + + False + Ryujinx - Title Update Manager + True + center + 440 + 250 + + + + + + True + False + vertical + + + True + False + vertical + + + True + False + 10 + 10 + 10 + 10 + Available Updates + + + False + True + 0 + + + + + True + True + 10 + 10 + in + + + True + False + + + True + False + vertical + + + No Update + True + True + False + True + True + + + False + True + 0 + + + + + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + + + True + False + 10 + 10 + start + + + Add + True + True + True + Adds an update to this list + 10 + + + + True + True + 0 + + + + + Remove + True + True + True + Removes the selected update + 10 + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + 10 + 10 + end + + + Save + True + True + True + 10 + 2 + 2 + + + + True + True + 0 + + + + + Cancel + True + True + True + 10 + 2 + 2 + + + + True + True + 1 + + + + + True + True + 1 + + + + + False + True + 1 + + + + + +