Просмотр исходного кода

Implement dlc management window (#1313)

* Implement dlc management window

* reduce repetition

* Implement per NCA toggling of DLC rather than per container
Xpl0itR 5 лет назад
Родитель
Сommit
2ed9db1fcd

+ 10 - 0
Ryujinx.Common/Configuration/DlcContainer.cs

@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.Common.Configuration
+{
+    public struct DlcContainer
+    {
+        public string Path { get; set; }
+        public List<DlcNca> DlcNcaList { get; set; }
+    }
+}

+ 9 - 0
Ryujinx.Common/Configuration/DlcNca.cs

@@ -0,0 +1,9 @@
+namespace Ryujinx.Common.Configuration
+{
+    public struct DlcNca
+    {
+        public string Path    { get; set; }
+        public ulong  TitleId { get; set; }
+        public bool   Enabled { get; set; }
+    }
+}

+ 12 - 1
Ryujinx.HLE/FileSystem/Content/ContentManager.cs

@@ -4,7 +4,6 @@ using LibHac.Fs;
 using LibHac.FsSystem;
 using LibHac.FsSystem.NcaUtils;
 using LibHac.Ncm;
-using LibHac.Spl;
 using Ryujinx.Common.Logging;
 using Ryujinx.HLE.Exceptions;
 using Ryujinx.HLE.HOS.Services.Time;
@@ -241,6 +240,18 @@ namespace Ryujinx.HLE.FileSystem.Content
             }
         }
 
+        public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool enabled)
+        {
+            if (!_aocData.TryAdd(titleId, new AocItem(containerPath, ncaPath, enabled)))
+            {
+                Logger.PrintWarning(LogClass.Application, $"Duplicate AddOnContent detected. TitleId {titleId:X16}");
+            }
+            else
+            {
+                Logger.PrintInfo(LogClass.Application, $"Found AddOnContent with TitleId {titleId:X16}");
+            }
+        }
+
         public void ClearAocData() => _aocData.Clear();
 
         public int GetAocCount() => _aocData.Where(e => e.Value.Enabled).Count();

+ 18 - 24
Ryujinx.HLE/HOS/ApplicationLoader.cs

@@ -149,17 +149,6 @@ namespace Ryujinx.HLE.HOS
             _contentManager.ClearAocData();
             _contentManager.AddAocData(securePartition, xciFile, mainNca.Header.TitleId);
 
-            // Check all nsp's in the base directory for AOC
-            foreach (var fn in new FileInfo(xciFile).Directory.EnumerateFiles("*.nsp"))
-            {
-                using (FileStream fs = fn.OpenRead())
-                using (IStorage storage = fs.AsStorage())
-                using (PartitionFileSystem pfs = new PartitionFileSystem(storage))
-                {
-                    _contentManager.AddAocData(pfs, fn.FullName, mainNca.Header.TitleId);
-                }
-            }
-
             LoadNca(mainNca, patchNca, controlNca);
         }
 
@@ -196,18 +185,6 @@ namespace Ryujinx.HLE.HOS
                 _contentManager.ClearAocData();
                 _contentManager.AddAocData(nsp, nspFile, mainNca.Header.TitleId);
 
-                // Check all nsp's in the base directory for AOC
-                foreach (var fn in new FileInfo(nspFile).Directory.EnumerateFiles("*.nsp"))
-                {
-                    if (fn.FullName == nspFile) continue;
-                    using (FileStream fs = fn.OpenRead())
-                    using (IStorage storage = fs.AsStorage())
-                    using (PartitionFileSystem pfs = new PartitionFileSystem(storage))
-                    {
-                        _contentManager.AddAocData(pfs, fn.FullName, mainNca.Header.TitleId);
-                    }
-                }
-
                 LoadNca(mainNca, patchNca, controlNca);
 
                 return;
@@ -238,7 +215,8 @@ namespace Ryujinx.HLE.HOS
             IStorage dataStorage = null;
             IFileSystem codeFs = null;
 
-            string titleUpdateMetadataPath = System.IO.Path.Combine(_fileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json");
+            // Load Update
+            string titleUpdateMetadataPath = Path.Combine(_fileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json");
 
             if (File.Exists(titleUpdateMetadataPath))
             {
@@ -274,6 +252,22 @@ namespace Ryujinx.HLE.HOS
                 }
             }
 
+            // Load Aoc
+            string titleAocMetadataPath = Path.Combine(_fileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "dlc.json");
+
+            if (File.Exists(titleAocMetadataPath))
+            {
+                List<DlcContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(titleAocMetadataPath);
+
+                foreach (DlcContainer dlcContainer in dlcContainerList)
+                {
+                    foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
+                    {
+                        _contentManager.AddAocItem(dlcNca.TitleId, dlcContainer.Path, dlcNca.Path, dlcNca.Enabled);
+                    }
+                }
+            }
+
             if (patchNca == null)
             {
                 if (mainNca.CanOpenSection(NcaSectionType.Data))

+ 2 - 2
Ryujinx/Ryujinx.csproj

@@ -47,7 +47,7 @@
     <None Remove="Ui\assets\Icon.png" />
     <None Remove="Ui\assets\TwitterLogo.png" />
     <None Remove="Ui\ControllerWindow.glade" />
-    <None Remove="Ui\GameTableContextMenu.glade" />
+    <None Remove="Ui\DlcWindow.glade" />
     <None Remove="Ui\MainWindow.glade" />
     <None Remove="Ui\ProfileDialog.glade" />
     <None Remove="Ui\SettingsWindow.glade" />
@@ -71,10 +71,10 @@
     <EmbeddedResource Include="Ui\assets\Icon.png" />
     <EmbeddedResource Include="Ui\assets\TwitterLogo.png" />
     <EmbeddedResource Include="Ui\ControllerWindow.glade" />
-    <EmbeddedResource Include="Ui\GameTableContextMenu.glade" />
     <EmbeddedResource Include="Ui\MainWindow.glade" />
     <EmbeddedResource Include="Ui\ProfileDialog.glade" />
     <EmbeddedResource Include="Ui\SettingsWindow.glade" />
+    <EmbeddedResource Include="Ui\DlcWindow.glade" />
     <EmbeddedResource Include="Ui\TitleUpdateWindow.glade" />
   </ItemGroup>
 

+ 244 - 0
Ryujinx/Ui/DlcWindow.cs

@@ -0,0 +1,244 @@
+using Gtk;
+using LibHac;
+using LibHac.Common;
+using LibHac.FsSystem.NcaUtils;
+using LibHac.Fs;
+using LibHac.FsSystem;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.FileSystem;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using GUI        = Gtk.Builder.ObjectAttribute;
+using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
+
+namespace Ryujinx.Ui
+{
+    public class DlcWindow : Window
+    {
+        private readonly VirtualFileSystem  _virtualFileSystem;
+        private readonly string             _titleId;
+        private readonly string             _dlcJsonPath;
+        private readonly List<DlcContainer> _dlcContainerList;
+
+#pragma warning disable CS0649, IDE0044
+        [GUI] Label         _baseTitleInfoLabel;
+        [GUI] TreeView      _dlcTreeView;
+        [GUI] TreeSelection _dlcTreeSelection;
+#pragma warning restore CS0649, IDE0044
+
+        public DlcWindow(string titleId, string titleName, VirtualFileSystem virtualFileSystem) : this(new Builder("Ryujinx.Ui.DlcWindow.glade"), titleId, titleName, virtualFileSystem) { }
+
+        private DlcWindow(Builder builder, string titleId, string titleName, VirtualFileSystem virtualFileSystem) : base(builder.GetObject("_dlcWindow").Handle)
+        {
+            builder.Autoconnect(this);
+
+            _titleId                 = titleId;
+            _virtualFileSystem       = virtualFileSystem;
+            _dlcJsonPath             = System.IO.Path.Combine(virtualFileSystem.GetBasePath(), "games", _titleId, "dlc.json");
+            _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
+
+            try
+            {
+                _dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
+            }
+            catch
+            {
+                _dlcContainerList = new List<DlcContainer>();
+            }
+            
+            _dlcTreeView.Model = new TreeStore(
+                typeof(bool),
+                typeof(string),
+                typeof(string));
+
+            CellRendererToggle enableToggle = new CellRendererToggle();
+            enableToggle.Toggled += (sender, args) =>
+            {
+                _dlcTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path));
+                bool newValue = !(bool)_dlcTreeView.Model.GetValue(treeIter, 0);
+                _dlcTreeView.Model.SetValue(treeIter, 0, newValue);
+
+                if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, treeIter))
+                {
+                    do
+                    {
+                        _dlcTreeView.Model.SetValue(childIter, 0, newValue);
+                    }
+                    while (_dlcTreeView.Model.IterNext(ref childIter));
+                }
+            };
+
+            _dlcTreeView.AppendColumn("Enabled", enableToggle,           "active", 0);
+            _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text",   1);
+            _dlcTreeView.AppendColumn("Path",    new CellRendererText(), "text",   2);
+
+            foreach (DlcContainer dlcContainer in _dlcContainerList)
+            {
+                TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", dlcContainer.Path);
+
+                using FileStream containerFile = File.OpenRead(dlcContainer.Path);
+                PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
+                _virtualFileSystem.ImportTickets(pfs);
+
+                foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
+                {
+                    pfs.OpenFile(out IFile ncaFile, dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+                    Nca nca = TryCreateNca(ncaFile.AsStorage(), dlcContainer.Path);
+                    
+                    if (nca != null)
+                    {
+                        ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.Path);
+                    }
+                }
+            }
+        }
+
+        private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
+        {
+            try
+            {
+                return new Nca(_virtualFileSystem.KeySet, ncaStorage);
+            }
+            catch (InvalidDataException exception)
+            {
+                Logger.PrintError(LogClass.Application, $"{exception.Message}. Errored File: {containerPath}");
+
+                GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add DLC Failed!", "The NCA header content type check has failed. This is usually because the header key is incorrect or missing.");
+            }
+            catch (MissingKeyException exception)
+            {
+                Logger.PrintError(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {containerPath}");
+
+                GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add DLC Failed!", $"Your key set is missing a key with the name: {exception.Name}");
+            }
+
+            return null;
+        }
+
+        private void AddButton_Clicked(object sender, EventArgs args)
+        {
+            FileChooserDialog fileChooser = new FileChooserDialog("Select DLC 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 containerPath in fileChooser.Filenames)
+                {
+                    if (!File.Exists(containerPath))
+                    {
+                        return;
+                    }
+
+                    using (FileStream containerFile = File.OpenRead(containerPath))
+                    {
+                        PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
+                        bool containsDlc = false;
+
+                        _virtualFileSystem.ImportTickets(pfs);
+
+                        TreeIter? parentIter = null;
+
+                        foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
+                        {
+                            pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+                            Nca nca = TryCreateNca(ncaFile.AsStorage(), containerPath);
+
+                            if (nca == null) continue;
+
+                            if (nca.Header.ContentType == NcaContentType.PublicData)
+                            {
+                                if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId)
+                                {
+                                    break;
+                                }
+
+                                parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
+
+                                ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
+                                containsDlc = true;
+                            }
+                        }
+
+                        if (!containsDlc)
+                        {
+                            GtkDialog.CreateErrorDialog("The specified file does not contain a DLC for the selected title!");
+                        }
+                    }
+                }
+            }
+
+            fileChooser.Dispose();
+        }
+
+        private void RemoveButton_Clicked(object sender, EventArgs args)
+        {
+            if (_dlcTreeSelection.GetSelected(out ITreeModel treeModel, out TreeIter treeIter))
+            {
+                if (_dlcTreeView.Model.IterParent(out TreeIter parentIter, treeIter) && _dlcTreeView.Model.IterNChildren(parentIter) <= 1)
+                {
+                    ((TreeStore)treeModel).Remove(ref parentIter);
+                }
+                else
+                {
+                    ((TreeStore)treeModel).Remove(ref treeIter);
+                }
+            }
+        }
+
+        private void SaveButton_Clicked(object sender, EventArgs args)
+        {
+            _dlcContainerList.Clear();
+
+            if (_dlcTreeView.Model.GetIterFirst(out TreeIter parentIter))
+            {
+                do
+                {
+                    if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter))
+                    {
+                        DlcContainer dlcContainer = new DlcContainer
+                        {
+                            Path       = (string)_dlcTreeView.Model.GetValue(parentIter, 2),
+                            DlcNcaList = new List<DlcNca>()
+                        };
+
+                        do
+                        {
+                            dlcContainer.DlcNcaList.Add(new DlcNca
+                            {
+                                Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0),
+                                TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16),
+                                Path    = (string)_dlcTreeView.Model.GetValue(childIter, 2)
+                            });
+                        }
+                        while (_dlcTreeView.Model.IterNext(ref childIter));
+
+                        _dlcContainerList.Add(dlcContainer);
+                    }
+                }
+                while (_dlcTreeView.Model.IterNext(ref parentIter));
+            }
+
+            using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
+            {
+                dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
+            }
+
+            Dispose();
+        }
+
+        private void CancelButton_Clicked(object sender, EventArgs args)
+        {
+            Dispose();
+        }
+    }
+}

+ 186 - 0
Ryujinx/Ui/DlcWindow.glade

@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkWindow" id="_dlcWindow">
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes">Ryujinx - DLC Manager</property>
+    <property name="modal">True</property>
+    <property name="window_position">center</property>
+    <property name="default_width">550</property>
+    <property name="default_height">350</property>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <object class="GtkBox" id="MainBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkBox" id="DlcBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkLabel" id="_baseTitleInfoLabel">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_left">10</property>
+                <property name="margin_right">10</property>
+                <property name="margin_top">10</property>
+                <property name="margin_bottom">10</property>
+                <property name="label" translatable="yes">Available DLC</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="margin_left">10</property>
+                <property name="margin_right">10</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <object class="GtkViewport">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkTreeView" id="_dlcTreeView">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="headers_clickable">False</property>
+                        <child internal-child="selection">
+                          <object class="GtkTreeSelection" id="_dlcTreeSelection"/>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkButtonBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_top">10</property>
+                <property name="margin_bottom">10</property>
+                <property name="layout_style">start</property>
+                <child>
+                  <object class="GtkButton" id="_addUpdate">
+                    <property name="label" translatable="yes">Add</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="tooltip_text" translatable="yes">Adds an update to this list</property>
+                    <property name="margin_left">10</property>
+                    <signal name="clicked" handler="AddButton_Clicked" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="_removeUpdate">
+                    <property name="label" translatable="yes">Remove</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="tooltip_text" translatable="yes">Removes the selected update</property>
+                    <property name="margin_left">10</property>
+                    <signal name="clicked" handler="RemoveButton_Clicked" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButtonBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_top">10</property>
+                <property name="margin_bottom">10</property>
+                <property name="layout_style">end</property>
+                <child>
+                  <object class="GtkButton" id="_saveButton">
+                    <property name="label" translatable="yes">Save</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="margin_right">10</property>
+                    <property name="margin_top">2</property>
+                    <property name="margin_bottom">2</property>
+                    <signal name="clicked" handler="SaveButton_Clicked" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="_cancelButton">
+                    <property name="label" translatable="yes">Cancel</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="margin_right">10</property>
+                    <property name="margin_top">2</property>
+                    <property name="margin_bottom">2</property>
+                    <signal name="clicked" handler="CancelButton_Clicked" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>

+ 84 - 43
Ryujinx/Ui/GameTableContextMenu.cs

@@ -8,7 +8,6 @@ using LibHac.FsSystem;
 using LibHac.FsSystem.NcaUtils;
 using LibHac.Ncm;
 using LibHac.Ns;
-using LibHac.Spl;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.Utilities;
@@ -20,64 +19,97 @@ using System.Globalization;
 using System.IO;
 using System.Reflection;
 using System.Threading;
+
 using static LibHac.Fs.ApplicationSaveDataManagement;
-using GUI = Gtk.Builder.ObjectAttribute;
 
 namespace Ryujinx.Ui
 {
     public class GameTableContextMenu : Menu
     {
-        private ListStore         _gameTableStore;
-        private TreeIter          _rowIter;
-        private VirtualFileSystem _virtualFileSystem;
-        private MessageDialog     _dialog;
-        private bool              _cancel;
-
-        private BlitStruct<ApplicationControlProperty> _controlData;
-
-#pragma warning disable CS0649
-#pragma warning disable IDE0044
-        [GUI] MenuItem _openSaveUserDir;
-        [GUI] MenuItem _openSaveDeviceDir;
-        [GUI] MenuItem _openSaveBcatDir;
-        [GUI] MenuItem _manageTitleUpdates;
-        [GUI] MenuItem _extractRomFs;
-        [GUI] MenuItem _extractExeFs;
-        [GUI] MenuItem _extractLogo;
-#pragma warning restore CS0649
-#pragma warning restore IDE0044
+        private readonly ListStore         _gameTableStore;
+        private readonly TreeIter          _rowIter;
+        private readonly VirtualFileSystem _virtualFileSystem;
 
-        public GameTableContextMenu(ListStore gameTableStore, BlitStruct<ApplicationControlProperty> controlData, TreeIter rowIter, VirtualFileSystem virtualFileSystem)
-            : this(new Builder("Ryujinx.Ui.GameTableContextMenu.glade"), gameTableStore, controlData, rowIter, virtualFileSystem) { }
+        private readonly BlitStruct<ApplicationControlProperty> _controlData;
 
-        private GameTableContextMenu(Builder builder, ListStore gameTableStore, BlitStruct<ApplicationControlProperty> controlData, TreeIter rowIter, VirtualFileSystem virtualFileSystem) : base(builder.GetObject("_contextMenu").Handle)
-        {
-            builder.Autoconnect(this);
+        private MessageDialog _dialog;
+        private bool          _cancel;
 
+        public GameTableContextMenu(ListStore gameTableStore, BlitStruct<ApplicationControlProperty> controlData, TreeIter rowIter, VirtualFileSystem virtualFileSystem)
+        {
             _gameTableStore    = gameTableStore;
             _rowIter           = rowIter;
             _virtualFileSystem = virtualFileSystem;
             _controlData       = controlData;
 
-            _openSaveUserDir.Activated    += OpenSaveUserDir_Clicked;
-            _openSaveDeviceDir.Activated  += OpenSaveDeviceDir_Clicked;
-            _openSaveBcatDir.Activated    += OpenSaveBcatDir_Clicked;
-            _manageTitleUpdates.Activated += ManageTitleUpdates_Clicked;
-            _extractRomFs.Activated       += ExtractRomFs_Clicked;
-            _extractExeFs.Activated       += ExtractExeFs_Clicked;
-            _extractLogo.Activated        += ExtractLogo_Clicked;
+            MenuItem openSaveUserDir = new MenuItem("Open User Save Directory")
+            {
+                Sensitive   = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0,
+                TooltipText = "Open the folder where the User save for the application is loaded"
+            };
 
-            _openSaveUserDir.Sensitive   = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
-            _openSaveDeviceDir.Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
-            _openSaveBcatDir.Sensitive   = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
+            MenuItem openSaveDeviceDir = new MenuItem("Open Device Save Directory")
+            {
+                Sensitive   = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0,
+                TooltipText = "Open the folder where the Device save for the application is loaded"
+            };
 
-            string ext = System.IO.Path.GetExtension(_gameTableStore.GetValue(_rowIter, 9).ToString()).ToLower();
-            if (ext != ".nca" && ext != ".nsp" && ext != ".pfs0" && ext != ".xci")
+            MenuItem openSaveBcatDir = new MenuItem("Open BCAT Save Directory")
             {
-                _extractRomFs.Sensitive = false;
-                _extractExeFs.Sensitive = false;
-                _extractLogo.Sensitive  = false;
-            }
+                Sensitive   = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0,
+                TooltipText = "Open the folder where the BCAT save for the application is loaded"
+            };
+
+            MenuItem manageTitleUpdates = new MenuItem("Manage Title Updates")
+            {
+                TooltipText = "Open the title update management window"
+            };
+
+            MenuItem manageDlc = new MenuItem("Manage DLC")
+            {
+                TooltipText = "Open the DLC management window"
+            };
+
+            string ext    = System.IO.Path.GetExtension(_gameTableStore.GetValue(_rowIter, 9).ToString()).ToLower();
+            bool   hasNca = ext == ".nca" || ext == ".nsp" || ext == ".pfs0" || ext == ".xci";
+
+            MenuItem extractRomFs = new MenuItem("Extract RomFS Section")
+            {
+                Sensitive   = hasNca,
+                TooltipText = "Exctact the RomFs section present in the main NCA"
+            };
+
+            MenuItem extractExeFs = new MenuItem("Extract ExeFS Section")
+            {
+                Sensitive   = hasNca,
+                TooltipText = "Exctact the ExeFs section present in the main NCA"
+            };
+
+            MenuItem extractLogo = new MenuItem("Extract Logo Section")
+            {
+                Sensitive   = hasNca,
+                TooltipText = "Exctact the Logo section present in the main NCA"
+            };
+
+            openSaveUserDir.Activated    += OpenSaveUserDir_Clicked;
+            openSaveDeviceDir.Activated  += OpenSaveDeviceDir_Clicked;
+            openSaveBcatDir.Activated    += OpenSaveBcatDir_Clicked;
+            manageTitleUpdates.Activated += ManageTitleUpdates_Clicked;
+            manageDlc.Activated          += ManageDlc_Clicked;
+            extractRomFs.Activated       += ExtractRomFs_Clicked;
+            extractExeFs.Activated       += ExtractExeFs_Clicked;
+            extractLogo.Activated        += ExtractLogo_Clicked;
+            
+            this.Add(openSaveUserDir);
+            this.Add(openSaveDeviceDir);
+            this.Add(openSaveBcatDir);
+            this.Add(new SeparatorMenuItem());
+            this.Add(manageTitleUpdates);
+            this.Add(manageDlc);
+            this.Add(new SeparatorMenuItem());
+            this.Add(extractRomFs);
+            this.Add(extractExeFs);
+            this.Add(extractLogo);
         }
 
         private bool TryFindSaveData(string titleName, ulong titleId, BlitStruct<ApplicationControlProperty> controlHolder, SaveDataFilter filter, out ulong saveDataId)
@@ -478,7 +510,7 @@ namespace Ryujinx.Ui
 
             string saveDir = GetSaveDataDirectory(saveDataId);
 
-            Process.Start(new ProcessStartInfo()
+            Process.Start(new ProcessStartInfo
             {
                 FileName        = saveDir,
                 UseShellExecute = true,
@@ -531,6 +563,15 @@ namespace Ryujinx.Ui
             titleUpdateWindow.Show();
         }
 
+        private void ManageDlc_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();
+
+            DlcWindow dlcWindow = new DlcWindow(titleId, titleName, _virtualFileSystem);
+            dlcWindow.Show();
+        }
+
         private void ExtractRomFs_Clicked(object sender, EventArgs args)
         {
             ExtractSection(NcaSectionType.Data);

+ 0 - 80
Ryujinx/Ui/GameTableContextMenu.glade

@@ -1,80 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.1 -->
-<interface>
-  <requires lib="gtk+" version="3.20"/>
-  <object class="GtkMenu" id="_contextMenu">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <child>
-      <object class="GtkMenuItem" id="_openSaveUserDir">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="tooltip_text" translatable="yes">Open the folder where the User save for the application is loaded</property>
-        <property name="label" translatable="yes">Open User Save Directory</property>
-        <property name="use_underline">True</property>
-      </object>
-    </child>
-    <child>
-      <object class="GtkMenuItem" id="_openSaveDeviceDir">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="tooltip_text" translatable="yes">Open the folder where the Device save for the application is loaded</property>
-        <property name="label" translatable="yes">Open Device Save Directory</property>
-        <property name="use_underline">True</property>
-      </object>
-    </child>
-    <child>
-      <object class="GtkMenuItem" id="_openSaveBcatDir">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="tooltip_text" translatable="yes">Open the folder where the BCAT save for the application is loaded</property>
-        <property name="label" translatable="yes">Open BCAT Save Directory</property>
-        <property name="use_underline">True</property>
-      </object>
-    </child>
-    <child>
-      <object class="GtkSeparatorMenuItem">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-      </object>
-    </child>
-    <child>
-      <object class="GtkMenuItem" id="_manageTitleUpdates">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="label" translatable="yes">Manage Title Updates</property>
-        <property name="use_underline">True</property>
-      </object>
-    </child>
-    <child>
-      <object class="GtkSeparatorMenuItem">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-      </object>
-    </child>
-    <child>
-      <object class="GtkMenuItem" id="_extractRomFs">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="label" translatable="yes">Extract RomFS Section</property>
-        <property name="use_underline">True</property>
-      </object>
-    </child>
-    <child>
-      <object class="GtkMenuItem" id="_extractExeFs">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="label" translatable="yes">Extract ExeFS Section</property>
-        <property name="use_underline">True</property>
-      </object>
-    </child>
-    <child>
-      <object class="GtkMenuItem" id="_extractLogo">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="label" translatable="yes">Extract Logo Section</property>
-        <property name="use_underline">True</property>
-      </object>
-    </child>
-  </object>
-</interface>

+ 16 - 14
Ryujinx/Ui/TitleUpdateWindow.cs

@@ -5,26 +5,27 @@ using LibHac.Fs;
 using LibHac.FsSystem;
 using LibHac.FsSystem.NcaUtils;
 using LibHac.Ns;
-using LibHac.Spl;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.HLE.FileSystem;
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Text;
 
-using GUI = Gtk.Builder.ObjectAttribute;
+using GUI        = Gtk.Builder.ObjectAttribute;
 using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
 
 namespace Ryujinx.Ui
 {
     public class TitleUpdateWindow : Window
     {
-        private readonly string            _titleId;
         private readonly VirtualFileSystem _virtualFileSystem;
+        private readonly string            _titleId;
+        private readonly string            _updateJsonPath;
 
-        private TitleUpdateMetadata _titleUpdateWindowData;
-        private Dictionary<RadioButton, string> _radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
+        private TitleUpdateMetadata             _titleUpdateWindowData;
+        private Dictionary<RadioButton, string> _radioButtonToPathDictionary;
 
 #pragma warning disable CS0649, IDE0044
         [GUI] Label       _baseTitleInfoLabel;
@@ -38,14 +39,14 @@ namespace Ryujinx.Ui
         {
             builder.Autoconnect(this);
 
-            _titleId           = titleId;
-            _virtualFileSystem = virtualFileSystem;
+            _titleId                     = titleId;
+            _virtualFileSystem           = virtualFileSystem;
+            _updateJsonPath              = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", _titleId, "updates.json");
+            _radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
 
             try
             {
-                string path = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", _titleId, "updates.json");
-
-                _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(path);
+                _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
             }
             catch
             {
@@ -56,7 +57,7 @@ namespace Ryujinx.Ui
                 };
             }
 
-            _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId}]";
+            _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]";
 
             foreach (string path in _titleUpdateWindowData.Paths)
             {
@@ -194,9 +195,10 @@ namespace Ryujinx.Ui
                 }
             }
 
-            string path = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", _titleId, "updates.json");
-
-            File.WriteAllText(path, JsonHelper.Serialize(_titleUpdateWindowData, true));
+            using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
+            {
+                dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
+            }
 
             MainWindow.UpdateGameTable();
             Dispose();