aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Ryujinx.Common/Configuration/DlcContainer.cs10
-rw-r--r--Ryujinx.Common/Configuration/DlcNca.cs9
-rw-r--r--Ryujinx.HLE/FileSystem/Content/ContentManager.cs13
-rw-r--r--Ryujinx.HLE/HOS/ApplicationLoader.cs42
-rw-r--r--Ryujinx/Ryujinx.csproj4
-rw-r--r--Ryujinx/Ui/DlcWindow.cs244
-rw-r--r--Ryujinx/Ui/DlcWindow.glade186
-rw-r--r--Ryujinx/Ui/GameTableContextMenu.cs127
-rw-r--r--Ryujinx/Ui/GameTableContextMenu.glade80
-rw-r--r--Ryujinx/Ui/TitleUpdateWindow.cs30
10 files changed, 581 insertions, 164 deletions
diff --git a/Ryujinx.Common/Configuration/DlcContainer.cs b/Ryujinx.Common/Configuration/DlcContainer.cs
new file mode 100644
index 00000000..c056ac1e
--- /dev/null
+++ b/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; }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Common/Configuration/DlcNca.cs b/Ryujinx.Common/Configuration/DlcNca.cs
new file mode 100644
index 00000000..df6a4532
--- /dev/null
+++ b/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; }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
index 839078e8..1b9ea143 100644
--- a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
+++ b/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();
diff --git a/Ryujinx.HLE/HOS/ApplicationLoader.cs b/Ryujinx.HLE/HOS/ApplicationLoader.cs
index 5ca67445..f6ab5ba9 100644
--- a/Ryujinx.HLE/HOS/ApplicationLoader.cs
+++ b/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))
diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj
index 30b368ab..cf672131 100644
--- a/Ryujinx/Ryujinx.csproj
+++ b/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>
diff --git a/Ryujinx/Ui/DlcWindow.cs b/Ryujinx/Ui/DlcWindow.cs
new file mode 100644
index 00000000..a81698fa
--- /dev/null
+++ b/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();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx/Ui/DlcWindow.glade b/Ryujinx/Ui/DlcWindow.glade
new file mode 100644
index 00000000..08d7ca0a
--- /dev/null
+++ b/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>
diff --git a/Ryujinx/Ui/GameTableContextMenu.cs b/Ryujinx/Ui/GameTableContextMenu.cs
index 2aee44d5..90ea44bd 100644
--- a/Ryujinx/Ui/GameTableContextMenu.cs
+++ b/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);
diff --git a/Ryujinx/Ui/GameTableContextMenu.glade b/Ryujinx/Ui/GameTableContextMenu.glade
deleted file mode 100644
index e648b745..00000000
--- a/Ryujinx/Ui/GameTableContextMenu.glade
+++ /dev/null
@@ -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>
diff --git a/Ryujinx/Ui/TitleUpdateWindow.cs b/Ryujinx/Ui/TitleUpdateWindow.cs
index acf0c2bd..b45b5df0 100644
--- a/Ryujinx/Ui/TitleUpdateWindow.cs
+++ b/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();