using Gtk; using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; using System.IO; using System.Linq; using GUI = Gtk.Builder.ObjectAttribute; using SpanHelpers = LibHac.Common.SpanHelpers; namespace Ryujinx.Ui.Windows { public class TitleUpdateWindow : Window { private readonly MainWindow _parent; private readonly VirtualFileSystem _virtualFileSystem; private readonly string _titleId; private readonly string _updateJsonPath; private TitleUpdateMetadata _titleUpdateWindowData; private readonly Dictionary _radioButtonToPathDictionary; private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); #pragma warning disable CS0649, IDE0044 [GUI] Label _baseTitleInfoLabel; [GUI] Box _availableUpdatesBox; [GUI] RadioButton _noUpdateRadioButton; #pragma warning restore CS0649, IDE0044 public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) { _parent = parent; builder.Autoconnect(this); _titleId = titleId; _virtualFileSystem = virtualFileSystem; _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); _radioButtonToPathDictionary = new Dictionary(); try { _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, SerializerContext.TitleUpdateMetadata); } catch { _titleUpdateWindowData = new TitleUpdateMetadata { Selected = "", Paths = new List() }; } _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; foreach (string path in _titleUpdateWindowData.Paths) { AddUpdate(path); } if (_titleUpdateWindowData.Selected == "") { _noUpdateRadioButton.Active = true; } else { foreach ((RadioButton update, var _) in _radioButtonToPathDictionary.Where(keyValuePair => keyValuePair.Value == _titleUpdateWindowData.Selected)) { update.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()); try { (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); if (controlNca != null && patchNca != null) { ApplicationControlProperty controlData = new ApplicationControlProperty(); using var nacpFile = new UniqueRef(); 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(); RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersionString.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!"); } } catch (Exception exception) { GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}"); } } } } private void RemoveUpdates(bool removeSelectedOnly = false) { foreach (RadioButton radioButton in _noUpdateRadioButton.Group) { if (radioButton.Label != "No Update" && (!removeSelectedOnly || radioButton.Active)) { _availableUpdatesBox.Remove(radioButton); _radioButtonToPathDictionary.Remove(radioButton); radioButton.Dispose(); } } } private void AddButton_Clicked(object sender, EventArgs args) { using (FileChooserNative fileChooser = new FileChooserNative("Select update files", this, FileChooserAction.Open, "Add", "Cancel")) { fileChooser.SelectMultiple = true; FileFilter filter = new FileFilter() { Name = "Switch Game Updates" }; filter.AddPattern("*.nsp"); fileChooser.AddFilter(filter); if (fileChooser.Run() == (int)ResponseType.Accept) { foreach (string path in fileChooser.Filenames) { AddUpdate(path); } } } } private void RemoveButton_Clicked(object sender, EventArgs args) { RemoveUpdates(true); } private void RemoveAllButton_Clicked(object sender, EventArgs args) { RemoveUpdates(); } private void SaveButton_Clicked(object sender, EventArgs args) { _titleUpdateWindowData.Paths.Clear(); _titleUpdateWindowData.Selected = ""; 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 : ""; } } JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, SerializerContext.TitleUpdateMetadata); _parent.UpdateGameTable(); Dispose(); } private void CancelButton_Clicked(object sender, EventArgs args) { Dispose(); } } }