forked from Mirror/Ryujinx
hle/ui: Basic multi programs support (#1560)
* hos/gui: Add a check of NCA program index in titleid This add a check to `ApplicationLoader` for the last 2 digits of the game TitleId who seems to be the NCA program index. We currently return the last index, instead of the lower one. Same check is added to ApplicationLibrary in the UI. I've cleaned up both file too. * hle: implement partial relaunch logic TODO: make the emulator auto relauch. * Handle auto relaunch * hle: Unify update usage system * hle: Implement support of multi programs in update system * Add some documentation * Address rip's comment Co-authored-by: Ac_K <Acoustik666@gmail.com>
This commit is contained in:
parent
90ab28d1c6
commit
33f8284bc0
13 changed files with 435 additions and 270 deletions
|
@ -15,6 +15,7 @@ using Ryujinx.HLE.Loaders.Executables;
|
||||||
using Ryujinx.HLE.Loaders.Npdm;
|
using Ryujinx.HLE.Loaders.Npdm;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
@ -28,32 +29,33 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
public class ApplicationLoader
|
public class ApplicationLoader
|
||||||
{
|
{
|
||||||
private readonly Switch _device;
|
|
||||||
private readonly ContentManager _contentManager;
|
|
||||||
private readonly VirtualFileSystem _fileSystem;
|
|
||||||
|
|
||||||
public BlitStruct<ApplicationControlProperty> ControlData { get; set; }
|
|
||||||
|
|
||||||
public string TitleName { get; private set; }
|
|
||||||
public string DisplayVersion { get; private set; }
|
|
||||||
|
|
||||||
public ulong TitleId { get; private set; }
|
|
||||||
public string TitleIdText => TitleId.ToString("x16");
|
|
||||||
|
|
||||||
public bool TitleIs64Bit { get; private set; }
|
|
||||||
|
|
||||||
public bool EnablePtc => _device.System.EnablePtc;
|
|
||||||
|
|
||||||
// Binaries from exefs are loaded into mem in this order. Do not change.
|
// Binaries from exefs are loaded into mem in this order. Do not change.
|
||||||
private static readonly string[] ExeFsPrefixes = { "rtld", "main", "subsdk*", "sdk" };
|
private static readonly string[] ExeFsPrefixes = { "rtld", "main", "subsdk*", "sdk" };
|
||||||
|
|
||||||
|
private readonly Switch _device;
|
||||||
|
private readonly ContentManager _contentManager;
|
||||||
|
private readonly VirtualFileSystem _fileSystem;
|
||||||
|
|
||||||
|
private string _titleName;
|
||||||
|
private string _displayVersion;
|
||||||
|
private BlitStruct<ApplicationControlProperty> _controlData;
|
||||||
|
|
||||||
|
public BlitStruct<ApplicationControlProperty> ControlData => _controlData;
|
||||||
|
public string TitleName => _titleName;
|
||||||
|
public string DisplayVersion => _displayVersion;
|
||||||
|
|
||||||
|
public ulong TitleId { get; private set; }
|
||||||
|
public bool TitleIs64Bit { get; private set; }
|
||||||
|
|
||||||
|
public string TitleIdText => TitleId.ToString("x16");
|
||||||
|
|
||||||
public ApplicationLoader(Switch device, VirtualFileSystem fileSystem, ContentManager contentManager)
|
public ApplicationLoader(Switch device, VirtualFileSystem fileSystem, ContentManager contentManager)
|
||||||
{
|
{
|
||||||
_device = device;
|
_device = device;
|
||||||
_contentManager = contentManager;
|
_contentManager = contentManager;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
|
||||||
ControlData = new BlitStruct<ApplicationControlProperty>(1);
|
_controlData = new BlitStruct<ApplicationControlProperty>(1);
|
||||||
|
|
||||||
// Clear Mods cache
|
// Clear Mods cache
|
||||||
_fileSystem.ModLoader.Clear();
|
_fileSystem.ModLoader.Clear();
|
||||||
|
@ -80,19 +82,26 @@ namespace Ryujinx.HLE.HOS
|
||||||
LoadExeFs(codeFs, metaData);
|
LoadExeFs(codeFs, metaData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Nca main, Nca patch, Nca control) GetGameData(PartitionFileSystem pfs)
|
public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
|
||||||
{
|
{
|
||||||
Nca mainNca = null;
|
Nca mainNca = null;
|
||||||
Nca patchNca = null;
|
Nca patchNca = null;
|
||||||
Nca controlNca = null;
|
Nca controlNca = null;
|
||||||
|
|
||||||
_fileSystem.ImportTickets(pfs);
|
fileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
{
|
{
|
||||||
pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
Nca nca = new Nca(_fileSystem.KeySet, ncaFile.AsStorage());
|
Nca nca = new Nca(fileSystem.KeySet, ncaFile.AsStorage());
|
||||||
|
|
||||||
|
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
|
||||||
|
|
||||||
|
if (ncaProgramIndex != programIndex)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.Program)
|
if (nca.Header.ContentType == NcaContentType.Program)
|
||||||
{
|
{
|
||||||
|
@ -116,11 +125,77 @@ namespace Ryujinx.HLE.HOS
|
||||||
return (mainNca, patchNca, controlNca);
|
return (mainNca, patchNca, controlNca);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex)
|
||||||
|
{
|
||||||
|
Nca patchNca = null;
|
||||||
|
Nca controlNca = null;
|
||||||
|
|
||||||
|
fileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
|
{
|
||||||
|
pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = new Nca(fileSystem.KeySet, ncaFile.AsStorage());
|
||||||
|
|
||||||
|
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
|
||||||
|
|
||||||
|
if (ncaProgramIndex != programIndex)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nca.Header.ContentType == NcaContentType.Program)
|
||||||
|
{
|
||||||
|
patchNca = nca;
|
||||||
|
}
|
||||||
|
else if (nca.Header.ContentType == NcaContentType.Control)
|
||||||
|
{
|
||||||
|
controlNca = nca;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (patchNca, controlNca);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath)
|
||||||
|
{
|
||||||
|
updatePath = null;
|
||||||
|
|
||||||
|
if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
|
||||||
|
{
|
||||||
|
// Clear the program index part.
|
||||||
|
titleIdBase &= 0xFFFFFFFFFFFFFFF0;
|
||||||
|
|
||||||
|
// Load update informations if existing.
|
||||||
|
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
|
||||||
|
|
||||||
|
if (File.Exists(titleUpdateMetadataPath))
|
||||||
|
{
|
||||||
|
updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected;
|
||||||
|
|
||||||
|
if (File.Exists(updatePath))
|
||||||
|
{
|
||||||
|
FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
|
||||||
|
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
||||||
|
|
||||||
|
return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
public void LoadXci(string xciFile)
|
public void LoadXci(string xciFile)
|
||||||
{
|
{
|
||||||
FileStream file = new FileStream(xciFile, FileMode.Open, FileAccess.Read);
|
FileStream file = new FileStream(xciFile, FileMode.Open, FileAccess.Read);
|
||||||
|
Xci xci = new Xci(_fileSystem.KeySet, file.AsStorage());
|
||||||
Xci xci = new Xci(_fileSystem.KeySet, file.AsStorage());
|
|
||||||
|
|
||||||
if (!xci.HasPartition(XciPartitionType.Secure))
|
if (!xci.HasPartition(XciPartitionType.Secure))
|
||||||
{
|
{
|
||||||
|
@ -131,13 +206,13 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
PartitionFileSystem securePartition = xci.OpenPartition(XciPartitionType.Secure);
|
PartitionFileSystem securePartition = xci.OpenPartition(XciPartitionType.Secure);
|
||||||
|
|
||||||
Nca mainNca = null;
|
Nca mainNca;
|
||||||
Nca patchNca = null;
|
Nca patchNca;
|
||||||
Nca controlNca = null;
|
Nca controlNca;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(mainNca, patchNca, controlNca) = GetGameData(securePartition);
|
(mainNca, patchNca, controlNca) = GetGameData(_fileSystem, securePartition, _device.UserChannelPersistence.Index);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -154,7 +229,6 @@ namespace Ryujinx.HLE.HOS
|
||||||
}
|
}
|
||||||
|
|
||||||
_contentManager.LoadEntries(_device);
|
_contentManager.LoadEntries(_device);
|
||||||
|
|
||||||
_contentManager.ClearAocData();
|
_contentManager.ClearAocData();
|
||||||
_contentManager.AddAocData(securePartition, xciFile, mainNca.Header.TitleId);
|
_contentManager.AddAocData(securePartition, xciFile, mainNca.Header.TitleId);
|
||||||
|
|
||||||
|
@ -163,17 +237,16 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
public void LoadNsp(string nspFile)
|
public void LoadNsp(string nspFile)
|
||||||
{
|
{
|
||||||
FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read);
|
FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read);
|
||||||
|
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
||||||
|
|
||||||
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
Nca mainNca;
|
||||||
|
Nca patchNca;
|
||||||
Nca mainNca = null;
|
Nca controlNca;
|
||||||
Nca patchNca = null;
|
|
||||||
Nca controlNca = null;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(mainNca, patchNca, controlNca) = GetGameData(nsp);
|
(mainNca, patchNca, controlNca) = GetGameData(_fileSystem, nsp, _device.UserChannelPersistence.Index);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -206,8 +279,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
public void LoadNca(string ncaFile)
|
public void LoadNca(string ncaFile)
|
||||||
{
|
{
|
||||||
FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
|
FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
|
||||||
|
Nca nca = new Nca(_fileSystem.KeySet, file.AsStorage(false));
|
||||||
Nca nca = new Nca(_fileSystem.KeySet, file.AsStorage(false));
|
|
||||||
|
|
||||||
LoadNca(nca, null, null);
|
LoadNca(nca, null, null);
|
||||||
}
|
}
|
||||||
|
@ -221,46 +293,24 @@ namespace Ryujinx.HLE.HOS
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IStorage dataStorage = null;
|
IStorage dataStorage = null;
|
||||||
IFileSystem codeFs = null;
|
IFileSystem codeFs = null;
|
||||||
|
|
||||||
// Load Update
|
(Nca updatePatchNca, Nca updateControlNca) = GetGameUpdateData(_fileSystem, mainNca.Header.TitleId.ToString("x16"), _device.UserChannelPersistence.Index, out _);
|
||||||
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
|
|
||||||
|
|
||||||
if (File.Exists(titleUpdateMetadataPath))
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
string updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected;
|
patchNca = updatePatchNca;
|
||||||
|
|
||||||
if (File.Exists(updatePath))
|
|
||||||
{
|
|
||||||
FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
|
|
||||||
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
|
||||||
|
|
||||||
_fileSystem.ImportTickets(nsp);
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
|
||||||
nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = new Nca(_fileSystem.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 (updateControlNca != null)
|
||||||
|
{
|
||||||
|
controlNca = updateControlNca;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load program 0 control NCA as we are going to need it for display version.
|
||||||
|
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_fileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
||||||
|
|
||||||
// Load Aoc
|
// Load Aoc
|
||||||
string titleAocMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
|
string titleAocMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
|
||||||
|
|
||||||
|
@ -315,13 +365,23 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
if (controlNca != null)
|
if (controlNca != null)
|
||||||
{
|
{
|
||||||
ReadControlData(controlNca);
|
ReadControlData(_device, controlNca, ref _controlData, ref _titleName, ref _displayVersion);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ControlData.ByteSpan.Clear();
|
ControlData.ByteSpan.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Nintendo doesn't guarantee that the display version will be updated on sub programs when updating a multi program application.
|
||||||
|
// BODY: As such, to avoid PTC cache confusion, we only trust the the program 0 display version when launching a sub program.
|
||||||
|
if (updateProgram0ControlNca != null && _device.UserChannelPersistence.Index != 0)
|
||||||
|
{
|
||||||
|
string dummyTitleName = "";
|
||||||
|
BlitStruct<ApplicationControlProperty> dummyControl = new BlitStruct<ApplicationControlProperty>(1);
|
||||||
|
|
||||||
|
ReadControlData(_device, updateProgram0ControlNca, ref dummyControl, ref dummyTitleName, ref _displayVersion);
|
||||||
|
}
|
||||||
|
|
||||||
if (dataStorage == null)
|
if (dataStorage == null)
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Loader, "No RomFS found in NCA");
|
Logger.Warning?.Print(LogClass.Loader, "No RomFS found in NCA");
|
||||||
|
@ -329,6 +389,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
IStorage newStorage = _fileSystem.ModLoader.ApplyRomFsMods(TitleId, dataStorage);
|
IStorage newStorage = _fileSystem.ModLoader.ApplyRomFsMods(TitleId, dataStorage);
|
||||||
|
|
||||||
_fileSystem.SetRomFs(newStorage.AsStream(FileAccess.Read));
|
_fileSystem.SetRomFs(newStorage.AsStream(FileAccess.Read));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,6 +407,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
private Npdm ReadNpdm(IFileSystem fs)
|
private Npdm ReadNpdm(IFileSystem fs)
|
||||||
{
|
{
|
||||||
Result result = fs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read);
|
Result result = fs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read);
|
||||||
|
|
||||||
Npdm metaData;
|
Npdm metaData;
|
||||||
|
|
||||||
if (ResultFs.PathNotFound.Includes(result))
|
if (ResultFs.PathNotFound.Includes(result))
|
||||||
|
@ -359,39 +421,36 @@ namespace Ryujinx.HLE.HOS
|
||||||
metaData = new Npdm(npdmFile.AsStream());
|
metaData = new Npdm(npdmFile.AsStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
TitleId = metaData.Aci0.TitleId;
|
TitleId = metaData.Aci0.TitleId;
|
||||||
TitleIs64Bit = metaData.Is64Bit;
|
TitleIs64Bit = metaData.Is64Bit;
|
||||||
|
|
||||||
return metaData;
|
return metaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReadControlData(Nca controlNca)
|
private static void ReadControlData(Switch device, Nca controlNca, ref BlitStruct<ApplicationControlProperty> controlData, ref string titleName, ref string displayVersion)
|
||||||
{
|
{
|
||||||
IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
|
IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
|
||||||
|
Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read);
|
||||||
Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read);
|
|
||||||
|
|
||||||
if (result.IsSuccess())
|
if (result.IsSuccess())
|
||||||
{
|
{
|
||||||
result = controlFile.Read(out long bytesRead, 0, ControlData.ByteSpan, ReadOption.None);
|
result = controlFile.Read(out long bytesRead, 0, controlData.ByteSpan, ReadOption.None);
|
||||||
|
|
||||||
if (result.IsSuccess() && bytesRead == ControlData.ByteSpan.Length)
|
if (result.IsSuccess() && bytesRead == controlData.ByteSpan.Length)
|
||||||
{
|
{
|
||||||
TitleName = ControlData.Value
|
titleName = controlData.Value.Titles[(int)device.System.State.DesiredTitleLanguage].Name.ToString();
|
||||||
.Titles[(int)_device.System.State.DesiredTitleLanguage].Name.ToString();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(TitleName))
|
if (string.IsNullOrWhiteSpace(titleName))
|
||||||
{
|
{
|
||||||
TitleName = ControlData.Value.Titles.ToArray()
|
titleName = controlData.Value.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
|
||||||
.FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayVersion = ControlData.Value.DisplayVersion.ToString();
|
displayVersion = controlData.Value.DisplayVersion.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ControlData.ByteSpan.Clear();
|
controlData.ByteSpan.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,18 +487,18 @@ namespace Ryujinx.HLE.HOS
|
||||||
// ExeFs file replacements
|
// ExeFs file replacements
|
||||||
bool modified = _fileSystem.ModLoader.ApplyExefsMods(TitleId, nsos);
|
bool modified = _fileSystem.ModLoader.ApplyExefsMods(TitleId, nsos);
|
||||||
|
|
||||||
var programs = nsos.ToArray();
|
NsoExecutable[] programs = nsos.ToArray();
|
||||||
|
|
||||||
modified |= _fileSystem.ModLoader.ApplyNsoPatches(TitleId, programs);
|
modified |= _fileSystem.ModLoader.ApplyNsoPatches(TitleId, programs);
|
||||||
|
|
||||||
_contentManager.LoadEntries(_device);
|
_contentManager.LoadEntries(_device);
|
||||||
|
|
||||||
if (EnablePtc && modified)
|
if (_device.System.EnablePtc && modified)
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Ptc, $"Detected exefs modifications. PPTC disabled.");
|
Logger.Warning?.Print(LogClass.Ptc, $"Detected exefs modifications. PPTC disabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ptc.Initialize(TitleIdText, DisplayVersion, EnablePtc && !modified);
|
Ptc.Initialize(TitleIdText, DisplayVersion, _device.System.EnablePtc && !modified);
|
||||||
|
|
||||||
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: programs);
|
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: programs);
|
||||||
}
|
}
|
||||||
|
@ -447,15 +506,15 @@ namespace Ryujinx.HLE.HOS
|
||||||
public void LoadProgram(string filePath)
|
public void LoadProgram(string filePath)
|
||||||
{
|
{
|
||||||
Npdm metaData = GetDefaultNpdm();
|
Npdm metaData = GetDefaultNpdm();
|
||||||
|
bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
|
||||||
bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
|
|
||||||
|
|
||||||
IExecutable executable;
|
IExecutable executable;
|
||||||
|
|
||||||
if (isNro)
|
if (isNro)
|
||||||
{
|
{
|
||||||
FileStream input = new FileStream(filePath, FileMode.Open);
|
FileStream input = new FileStream(filePath, FileMode.Open);
|
||||||
NroExecutable obj = new NroExecutable(input.AsStorage());
|
NroExecutable obj = new NroExecutable(input.AsStorage());
|
||||||
|
|
||||||
executable = obj;
|
executable = obj;
|
||||||
|
|
||||||
// homebrew NRO can actually have some data after the actual NRO
|
// homebrew NRO can actually have some data after the actual NRO
|
||||||
|
@ -466,20 +525,19 @@ namespace Ryujinx.HLE.HOS
|
||||||
BinaryReader reader = new BinaryReader(input);
|
BinaryReader reader = new BinaryReader(input);
|
||||||
|
|
||||||
uint asetMagic = reader.ReadUInt32();
|
uint asetMagic = reader.ReadUInt32();
|
||||||
|
|
||||||
if (asetMagic == 0x54455341)
|
if (asetMagic == 0x54455341)
|
||||||
{
|
{
|
||||||
uint asetVersion = reader.ReadUInt32();
|
uint asetVersion = reader.ReadUInt32();
|
||||||
if (asetVersion == 0)
|
if (asetVersion == 0)
|
||||||
{
|
{
|
||||||
ulong iconOffset = reader.ReadUInt64();
|
ulong iconOffset = reader.ReadUInt64();
|
||||||
ulong iconSize = reader.ReadUInt64();
|
ulong iconSize = reader.ReadUInt64();
|
||||||
|
|
||||||
ulong nacpOffset = reader.ReadUInt64();
|
ulong nacpOffset = reader.ReadUInt64();
|
||||||
ulong nacpSize = reader.ReadUInt64();
|
ulong nacpSize = reader.ReadUInt64();
|
||||||
|
|
||||||
ulong romfsOffset = reader.ReadUInt64();
|
ulong romfsOffset = reader.ReadUInt64();
|
||||||
ulong romfsSize = reader.ReadUInt64();
|
ulong romfsSize = reader.ReadUInt64();
|
||||||
|
|
||||||
if (romfsSize != 0)
|
if (romfsSize != 0)
|
||||||
{
|
{
|
||||||
|
@ -533,8 +591,8 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
_contentManager.LoadEntries(_device);
|
_contentManager.LoadEntries(_device);
|
||||||
|
|
||||||
TitleName = metaData.TitleName;
|
_titleName = metaData.TitleName;
|
||||||
TitleId = metaData.Aci0.TitleId;
|
TitleId = metaData.Aci0.TitleId;
|
||||||
TitleIs64Bit = metaData.Is64Bit;
|
TitleIs64Bit = metaData.Is64Bit;
|
||||||
|
|
||||||
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: executable);
|
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: executable);
|
||||||
|
@ -572,25 +630,24 @@ namespace Ryujinx.HLE.HOS
|
||||||
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
|
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemClient fs = _fileSystem.FsClient;
|
FileSystemClient fileSystem = _fileSystem.FsClient;
|
||||||
|
Result resultCode = fileSystem.EnsureApplicationCacheStorage(out _, applicationId, ref control);
|
||||||
|
|
||||||
Result rc = fs.EnsureApplicationCacheStorage(out _, applicationId, ref control);
|
if (resultCode.IsFailure())
|
||||||
|
|
||||||
if (rc.IsFailure())
|
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {rc.ToStringWithName()}");
|
Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {resultCode.ToStringWithName()}");
|
||||||
|
|
||||||
return rc;
|
return resultCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
rc = EnsureApplicationSaveData(fs, out _, applicationId, ref control, ref user);
|
resultCode = EnsureApplicationSaveData(fileSystem, out _, applicationId, ref control, ref user);
|
||||||
|
|
||||||
if (rc.IsFailure())
|
if (resultCode.IsFailure())
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {rc.ToStringWithName()}");
|
Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {resultCode.ToStringWithName()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return rc;
|
return resultCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -126,5 +126,14 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command(160)] // 5.0.0+
|
||||||
|
// StoreOpenContext()
|
||||||
|
public ResultCode StoreOpenContext(ServiceCtx context)
|
||||||
|
{
|
||||||
|
Logger.Stub?.PrintStub(LogClass.ServiceAcc);
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,11 +5,13 @@ using LibHac.Fs;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.Exceptions;
|
||||||
using Ryujinx.HLE.HOS.Ipc;
|
using Ryujinx.HLE.HOS.Ipc;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Common;
|
using Ryujinx.HLE.HOS.Kernel.Common;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Memory;
|
using Ryujinx.HLE.HOS.Kernel.Memory;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
|
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
|
||||||
using Ryujinx.HLE.HOS.SystemState;
|
using Ryujinx.HLE.HOS.SystemState;
|
||||||
using System;
|
using System;
|
||||||
|
@ -35,11 +37,27 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(1)]
|
[Command(1)]
|
||||||
// PopLaunchParameter(u32) -> object<nn::am::service::IStorage>
|
// PopLaunchParameter(LaunchParameterKind kind) -> object<nn::am::service::IStorage>
|
||||||
public ResultCode PopLaunchParameter(ServiceCtx context)
|
public ResultCode PopLaunchParameter(ServiceCtx context)
|
||||||
{
|
{
|
||||||
// Only the first 0x18 bytes of the Data seems to be actually used.
|
LaunchParameterKind kind = (LaunchParameterKind)context.RequestData.ReadUInt32();
|
||||||
MakeObject(context, new AppletAE.IStorage(StorageHelper.MakeLaunchParams(context.Device.System.State.Account.LastOpenedUser)));
|
|
||||||
|
byte[] storageData;
|
||||||
|
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case LaunchParameterKind.UserChannel:
|
||||||
|
storageData = context.Device.UserChannelPersistence.Pop();
|
||||||
|
break;
|
||||||
|
case LaunchParameterKind.PreselectedUser:
|
||||||
|
// Only the first 0x18 bytes of the Data seems to be actually used.
|
||||||
|
storageData = StorageHelper.MakeLaunchParams(context.Device.System.State.Account.LastOpenedUser);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException($"Unknown LaunchParameterKind {kind}");
|
||||||
|
}
|
||||||
|
|
||||||
|
MakeObject(context, new AppletAE.IStorage(storageData));
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
@ -376,14 +394,49 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||||
return (ResultCode)QueryPlayStatisticsManager.GetPlayStatistics(context, true);
|
return (ResultCode)QueryPlayStatisticsManager.GetPlayStatistics(context, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command(120)] // 5.0.0+
|
||||||
|
// ExecuteProgram(ProgramSpecifyKind kind, u64 value)
|
||||||
|
public ResultCode ExecuteProgram(ServiceCtx context)
|
||||||
|
{
|
||||||
|
ProgramSpecifyKind kind = (ProgramSpecifyKind)context.RequestData.ReadUInt32();
|
||||||
|
|
||||||
|
// padding
|
||||||
|
context.RequestData.ReadUInt32();
|
||||||
|
|
||||||
|
ulong value = context.RequestData.ReadUInt64();
|
||||||
|
|
||||||
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { kind, value });
|
||||||
|
|
||||||
|
context.Device.UiHandler.ExecuteProgram(context.Device, kind, value);
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(121)] // 5.0.0+
|
||||||
|
// ClearUserChannel()
|
||||||
|
public ResultCode ClearUserChannel(ServiceCtx context)
|
||||||
|
{
|
||||||
|
context.Device.UserChannelPersistence.Clear();
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(122)] // 5.0.0+
|
||||||
|
// UnpopToUserChannel(object<nn::am::service::IStorage> input_storage)
|
||||||
|
public ResultCode UnpopToUserChannel(ServiceCtx context)
|
||||||
|
{
|
||||||
|
AppletAE.IStorage data = GetObject<AppletAE.IStorage>(context, 0);
|
||||||
|
|
||||||
|
context.Device.UserChannelPersistence.Push(data.Data);
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
[Command(123)] // 5.0.0+
|
[Command(123)] // 5.0.0+
|
||||||
// GetPreviousProgramIndex() -> s32 program_index
|
// GetPreviousProgramIndex() -> s32 program_index
|
||||||
public ResultCode GetPreviousProgramIndex(ServiceCtx context)
|
public ResultCode GetPreviousProgramIndex(ServiceCtx context)
|
||||||
{
|
{
|
||||||
// TODO: The output PreviousProgramIndex is -1 when there was no previous title.
|
int previousProgramIndex = context.Device.UserChannelPersistence.PreviousIndex;
|
||||||
// When multi-process will be supported, return the last program index.
|
|
||||||
|
|
||||||
int previousProgramIndex = -1;
|
|
||||||
|
|
||||||
context.ResponseData.Write(previousProgramIndex);
|
context.ResponseData.Write(previousProgramIndex);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types
|
||||||
|
{
|
||||||
|
public enum LaunchParameterKind : uint
|
||||||
|
{
|
||||||
|
UserChannel = 1,
|
||||||
|
PreselectedUser,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types
|
||||||
|
{
|
||||||
|
public enum ProgramSpecifyKind : uint
|
||||||
|
{
|
||||||
|
ExecuteProgram,
|
||||||
|
SubApplicationProgram,
|
||||||
|
RestartProgram
|
||||||
|
}
|
||||||
|
}
|
56
Ryujinx.HLE/HOS/UserChannelPersistence.cs
Normal file
56
Ryujinx.HLE/HOS/UserChannelPersistence.cs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS
|
||||||
|
{
|
||||||
|
public class UserChannelPersistence
|
||||||
|
{
|
||||||
|
private Stack<byte[]> _userChannelStorages;
|
||||||
|
public int PreviousIndex { get; private set; }
|
||||||
|
public int Index { get; private set; }
|
||||||
|
public ProgramSpecifyKind Kind { get; private set; }
|
||||||
|
|
||||||
|
public UserChannelPersistence()
|
||||||
|
{
|
||||||
|
_userChannelStorages = new Stack<byte[]>();
|
||||||
|
Kind = ProgramSpecifyKind.ExecuteProgram;
|
||||||
|
PreviousIndex = -1;
|
||||||
|
Index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_userChannelStorages.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Push(byte[] data)
|
||||||
|
{
|
||||||
|
_userChannelStorages.Push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Pop()
|
||||||
|
{
|
||||||
|
return _userChannelStorages.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEmpty => _userChannelStorages.Count == 0;
|
||||||
|
|
||||||
|
public void ExecuteProgram(ProgramSpecifyKind kind, ulong value)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
PreviousIndex = Index;
|
||||||
|
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case ProgramSpecifyKind.ExecuteProgram:
|
||||||
|
Index = (int)value;
|
||||||
|
break;
|
||||||
|
case ProgramSpecifyKind.RestartProgram:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException($"{kind} not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.HLE.HOS.Applets;
|
using Ryujinx.HLE.HOS.Applets;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||||
|
|
||||||
namespace Ryujinx.HLE
|
namespace Ryujinx.HLE
|
||||||
{
|
{
|
||||||
|
@ -22,5 +23,13 @@ namespace Ryujinx.HLE
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>True when OK is pressed, False otherwise.</returns>
|
/// <returns>True when OK is pressed, False otherwise.</returns>
|
||||||
bool DisplayMessageDialog(ControllerAppletUiArgs args);
|
bool DisplayMessageDialog(ControllerAppletUiArgs args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tell the UI that we need to transisition to another program.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="device">The device instance.</param>
|
||||||
|
/// <param name="kind">The program kind.</param>
|
||||||
|
/// <param name="value">The value associated to the <paramref name="kind"/>.</param>
|
||||||
|
void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -35,13 +35,15 @@ namespace Ryujinx.HLE
|
||||||
|
|
||||||
public PerformanceStatistics Statistics { get; private set; }
|
public PerformanceStatistics Statistics { get; private set; }
|
||||||
|
|
||||||
|
public UserChannelPersistence UserChannelPersistence { get; }
|
||||||
|
|
||||||
public Hid Hid { get; private set; }
|
public Hid Hid { get; private set; }
|
||||||
|
|
||||||
public IHostUiHandler UiHandler { get; set; }
|
public IHostUiHandler UiHandler { get; set; }
|
||||||
|
|
||||||
public bool EnableDeviceVsync { get; set; } = true;
|
public bool EnableDeviceVsync { get; set; } = true;
|
||||||
|
|
||||||
public Switch(VirtualFileSystem fileSystem, ContentManager contentManager, IRenderer renderer, IAalOutput audioOut)
|
public Switch(VirtualFileSystem fileSystem, ContentManager contentManager, UserChannelPersistence userChannelPersistence, IRenderer renderer, IAalOutput audioOut)
|
||||||
{
|
{
|
||||||
if (renderer == null)
|
if (renderer == null)
|
||||||
{
|
{
|
||||||
|
@ -53,6 +55,13 @@ namespace Ryujinx.HLE
|
||||||
throw new ArgumentNullException(nameof(audioOut));
|
throw new ArgumentNullException(nameof(audioOut));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userChannelPersistence == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(userChannelPersistence));
|
||||||
|
}
|
||||||
|
|
||||||
|
UserChannelPersistence = userChannelPersistence;
|
||||||
|
|
||||||
AudioOut = audioOut;
|
AudioOut = audioOut;
|
||||||
|
|
||||||
Memory = new MemoryBlock(1UL << 32);
|
Memory = new MemoryBlock(1UL << 32);
|
||||||
|
|
|
@ -5,10 +5,11 @@ using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.FsSystem.NcaUtils;
|
using LibHac.FsSystem.NcaUtils;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using Ryujinx.Common.Logging;
|
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Configuration.System;
|
using Ryujinx.Configuration.System;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.HLE.Loaders.Npdm;
|
using Ryujinx.HLE.Loaders.Npdm;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -39,10 +40,12 @@ namespace Ryujinx.Ui
|
||||||
public static IEnumerable<string> GetFilesInDirectory(string directory)
|
public static IEnumerable<string> GetFilesInDirectory(string directory)
|
||||||
{
|
{
|
||||||
Stack<string> stack = new Stack<string>();
|
Stack<string> stack = new Stack<string>();
|
||||||
|
|
||||||
stack.Push(directory);
|
stack.Push(directory);
|
||||||
|
|
||||||
while (stack.Count > 0)
|
while (stack.Count > 0)
|
||||||
{
|
{
|
||||||
string dir = stack.Pop();
|
string dir = stack.Pop();
|
||||||
string[] content = { };
|
string[] content = { };
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -57,7 +60,9 @@ namespace Ryujinx.Ui
|
||||||
if (content.Length > 0)
|
if (content.Length > 0)
|
||||||
{
|
{
|
||||||
foreach (string file in content)
|
foreach (string file in content)
|
||||||
|
{
|
||||||
yield return file;
|
yield return file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -72,7 +77,9 @@ namespace Ryujinx.Ui
|
||||||
if (content.Length > 0)
|
if (content.Length > 0)
|
||||||
{
|
{
|
||||||
foreach (string subdir in content)
|
foreach (string subdir in content)
|
||||||
|
{
|
||||||
stack.Push(subdir);
|
stack.Push(subdir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,6 +101,7 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
// Builds the applications list with paths to found applications
|
// Builds the applications list with paths to found applications
|
||||||
List<string> applications = new List<string>();
|
List<string> applications = new List<string>();
|
||||||
|
|
||||||
foreach (string appDir in appDirs)
|
foreach (string appDir in appDirs)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -128,6 +136,7 @@ namespace Ryujinx.Ui
|
||||||
string developer = "Unknown";
|
string developer = "Unknown";
|
||||||
string version = "0";
|
string version = "0";
|
||||||
byte[] applicationIcon = null;
|
byte[] applicationIcon = null;
|
||||||
|
|
||||||
BlitStruct<ApplicationControlProperty> controlHolder = new BlitStruct<ApplicationControlProperty>(1);
|
BlitStruct<ApplicationControlProperty> controlHolder = new BlitStruct<ApplicationControlProperty>(1);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -448,23 +457,7 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
private static void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId)
|
private static void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId)
|
||||||
{
|
{
|
||||||
Nca controlNca = null;
|
(_, _, Nca controlNca) = ApplicationLoader.GetGameData(_virtualFileSystem, pfs, 0);
|
||||||
|
|
||||||
// Add keys to key set if needed
|
|
||||||
_virtualFileSystem.ImportTickets(pfs);
|
|
||||||
|
|
||||||
// Find the Control NCA and store it in variable called controlNca
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
|
||||||
pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage());
|
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.Control)
|
|
||||||
{
|
|
||||||
controlNca = nca;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the ControlFS
|
// Return the ControlFS
|
||||||
controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
|
controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
|
||||||
|
@ -574,6 +567,7 @@ namespace Ryujinx.Ui
|
||||||
if (!((U8Span)controlTitle.Name).IsEmpty())
|
if (!((U8Span)controlTitle.Name).IsEmpty())
|
||||||
{
|
{
|
||||||
titleName = controlTitle.Name.ToString();
|
titleName = controlTitle.Name.ToString();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -586,6 +580,7 @@ namespace Ryujinx.Ui
|
||||||
if (!((U8Span)controlTitle.Publisher).IsEmpty())
|
if (!((U8Span)controlTitle.Publisher).IsEmpty())
|
||||||
{
|
{
|
||||||
publisher = controlTitle.Publisher.ToString();
|
publisher = controlTitle.Publisher.ToString();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -611,68 +606,34 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
private static bool IsUpdateApplied(string titleId, out string version)
|
private static bool IsUpdateApplied(string titleId, out string version)
|
||||||
{
|
{
|
||||||
string jsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
string updatePath = "(unknown)";
|
||||||
|
|
||||||
if (File.Exists(jsonPath))
|
try
|
||||||
{
|
{
|
||||||
string updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(jsonPath).Selected;
|
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath);
|
||||||
|
|
||||||
if (!File.Exists(updatePath))
|
if (patchNca != null && controlNca != null)
|
||||||
{
|
{
|
||||||
version = "";
|
ApplicationControlProperty controlData = new ApplicationControlProperty();
|
||||||
|
|
||||||
return false;
|
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
}
|
|
||||||
|
nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||||
using (FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read))
|
|
||||||
{
|
version = controlData.DisplayVersion.ToString();
|
||||||
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
|
||||||
|
return true;
|
||||||
_virtualFileSystem.ImportTickets(nsp);
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
|
||||||
nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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 _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
|
||||||
|
|
||||||
version = controlData.DisplayVersion.ToString();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (InvalidDataException)
|
|
||||||
{
|
|
||||||
Logger.Warning?.Print(LogClass.Application,
|
|
||||||
$"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}");
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (MissingKeyException exception)
|
|
||||||
{
|
|
||||||
Logger.Warning?.Print(LogClass.Application,
|
|
||||||
$"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}");
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (InvalidDataException)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application,
|
||||||
|
$"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}");
|
||||||
|
}
|
||||||
|
catch (MissingKeyException exception)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}");
|
||||||
|
}
|
||||||
|
|
||||||
version = "";
|
version = "";
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.HOS;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -246,7 +247,7 @@ namespace Ryujinx.Ui
|
||||||
return workingPath;
|
return workingPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExtractSection(NcaSectionType ncaSectionType)
|
private void ExtractSection(NcaSectionType ncaSectionType, int programIndex = 0)
|
||||||
{
|
{
|
||||||
FileChooserDialog fileChooser = new FileChooserDialog("Choose the folder to extract into", null, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Extract", ResponseType.Accept);
|
FileChooserDialog fileChooser = new FileChooserDialog("Choose the folder to extract into", null, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Extract", ResponseType.Accept);
|
||||||
fileChooser.SetPosition(WindowPosition.Center);
|
fileChooser.SetPosition(WindowPosition.Center);
|
||||||
|
@ -340,36 +341,12 @@ namespace Ryujinx.Ui
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
|
|
||||||
|
|
||||||
if (File.Exists(titleUpdateMetadataPath))
|
(Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
|
||||||
|
|
||||||
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
string updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected;
|
patchNca = updatePatchNca;
|
||||||
|
|
||||||
if (File.Exists(updatePath))
|
|
||||||
{
|
|
||||||
FileStream updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
|
|
||||||
PartitionFileSystem nsp = new PartitionFileSystem(updateFile.AsStorage());
|
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(nsp);
|
|
||||||
|
|
||||||
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" != mainNca.Header.TitleId.ToString("x16"))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.Program)
|
|
||||||
{
|
|
||||||
patchNca = nca;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType);
|
int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType);
|
||||||
|
|
|
@ -2,6 +2,7 @@ using Gtk;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using Ryujinx.HLE.HOS.Applets;
|
using Ryujinx.HLE.HOS.Applets;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
|
@ -121,5 +122,11 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
return error || okPressed;
|
return error || okPressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value)
|
||||||
|
{
|
||||||
|
device.UserChannelPersistence.ExecuteProgram(kind, value);
|
||||||
|
MainWindow.GlWidget?.Exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ using Ryujinx.Graphics.GAL;
|
||||||
using Ryujinx.Graphics.OpenGL;
|
using Ryujinx.Graphics.OpenGL;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.FileSystem.Content;
|
using Ryujinx.HLE.FileSystem.Content;
|
||||||
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.Ui.Diagnostic;
|
using Ryujinx.Ui.Diagnostic;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
@ -27,6 +28,7 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
private static VirtualFileSystem _virtualFileSystem;
|
private static VirtualFileSystem _virtualFileSystem;
|
||||||
private static ContentManager _contentManager;
|
private static ContentManager _contentManager;
|
||||||
|
private static UserChannelPersistence _userChannelPersistence;
|
||||||
|
|
||||||
private static WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
|
private static WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
|
||||||
private static HLE.Switch _emulationContext;
|
private static HLE.Switch _emulationContext;
|
||||||
|
@ -34,12 +36,15 @@ namespace Ryujinx.Ui
|
||||||
private static GlRenderer _glWidget;
|
private static GlRenderer _glWidget;
|
||||||
private static GtkHostUiHandler _uiHandler;
|
private static GtkHostUiHandler _uiHandler;
|
||||||
|
|
||||||
|
public static GlRenderer GlWidget => _glWidget;
|
||||||
|
|
||||||
private static AutoResetEvent _deviceExitStatus = new AutoResetEvent(false);
|
private static AutoResetEvent _deviceExitStatus = new AutoResetEvent(false);
|
||||||
|
|
||||||
private static ListStore _tableStore;
|
private static ListStore _tableStore;
|
||||||
|
|
||||||
private static bool _updatingGameTable;
|
private static bool _updatingGameTable;
|
||||||
private static bool _gameLoaded;
|
private static bool _gameLoaded;
|
||||||
|
private static string _gamePath;
|
||||||
private static bool _ending;
|
private static bool _ending;
|
||||||
|
|
||||||
#pragma warning disable CS0169, CS0649, IDE0044
|
#pragma warning disable CS0169, CS0649, IDE0044
|
||||||
|
@ -110,6 +115,7 @@ namespace Ryujinx.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
_virtualFileSystem = VirtualFileSystem.CreateInstance();
|
_virtualFileSystem = VirtualFileSystem.CreateInstance();
|
||||||
|
_userChannelPersistence = new UserChannelPersistence();
|
||||||
_contentManager = new ContentManager(_virtualFileSystem);
|
_contentManager = new ContentManager(_virtualFileSystem);
|
||||||
|
|
||||||
if (migrationNeeded)
|
if (migrationNeeded)
|
||||||
|
@ -181,6 +187,7 @@ namespace Ryujinx.Ui
|
||||||
_statusBar.Hide();
|
_statusBar.Hide();
|
||||||
|
|
||||||
_uiHandler = new GtkHostUiHandler(this);
|
_uiHandler = new GtkHostUiHandler(this);
|
||||||
|
_gamePath = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MainWindow_WindowStateEvent(object o, WindowStateEventArgs args)
|
private void MainWindow_WindowStateEvent(object o, WindowStateEventArgs args)
|
||||||
|
@ -278,7 +285,7 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
_virtualFileSystem.Reload();
|
_virtualFileSystem.Reload();
|
||||||
|
|
||||||
HLE.Switch instance = new HLE.Switch(_virtualFileSystem, _contentManager, InitializeRenderer(), InitializeAudioEngine())
|
HLE.Switch instance = new HLE.Switch(_virtualFileSystem, _contentManager, _userChannelPersistence, InitializeRenderer(), InitializeAudioEngine())
|
||||||
{
|
{
|
||||||
UiHandler = _uiHandler
|
UiHandler = _uiHandler
|
||||||
};
|
};
|
||||||
|
@ -485,6 +492,7 @@ namespace Ryujinx.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
_emulationContext = device;
|
_emulationContext = device;
|
||||||
|
_gamePath = path;
|
||||||
|
|
||||||
_deviceExitStatus.Reset();
|
_deviceExitStatus.Reset();
|
||||||
|
|
||||||
|
@ -589,6 +597,7 @@ namespace Ryujinx.Ui
|
||||||
UpdateGameTable();
|
UpdateGameTable();
|
||||||
|
|
||||||
Task.Run(RefreshFirmwareLabel);
|
Task.Run(RefreshFirmwareLabel);
|
||||||
|
Task.Run(HandleRelaunch);
|
||||||
|
|
||||||
_stopEmulation.Sensitive = false;
|
_stopEmulation.Sensitive = false;
|
||||||
_firmwareInstallFile.Sensitive = true;
|
_firmwareInstallFile.Sensitive = true;
|
||||||
|
@ -962,6 +971,21 @@ namespace Ryujinx.Ui
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleRelaunch()
|
||||||
|
{
|
||||||
|
// If the previous index isn't -1, that mean we are relaunching.
|
||||||
|
if (_userChannelPersistence.PreviousIndex != -1)
|
||||||
|
{
|
||||||
|
LoadApplication(_gamePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// otherwise, clear state.
|
||||||
|
_userChannelPersistence = new UserChannelPersistence();
|
||||||
|
_gamePath = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleInstallerDialog(FileChooserDialog fileChooser)
|
private void HandleInstallerDialog(FileChooserDialog fileChooser)
|
||||||
{
|
{
|
||||||
if (fileChooser.Run() == (int)ResponseType.Accept)
|
if (fileChooser.Run() == (int)ResponseType.Accept)
|
||||||
|
|
|
@ -9,6 +9,7 @@ using LibHac.Ns;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.HOS;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -83,63 +84,47 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(nsp);
|
try
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
{
|
||||||
nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
|
||||||
|
|
||||||
try
|
if (controlNca != null && patchNca != null)
|
||||||
{
|
{
|
||||||
Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage());
|
ApplicationControlProperty controlData = new ApplicationControlProperty();
|
||||||
|
|
||||||
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" == _titleId)
|
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
{
|
nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||||
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();
|
RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersion.ToString()} - {path}");
|
||||||
nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
radioButton.JoinGroup(_noUpdateRadioButton);
|
||||||
|
|
||||||
RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersion.ToString()} - {path}");
|
_availableUpdatesBox.Add(radioButton);
|
||||||
radioButton.JoinGroup(_noUpdateRadioButton);
|
_radioButtonToPathDictionary.Add(radioButton, path);
|
||||||
|
|
||||||
_availableUpdatesBox.Add(radioButton);
|
radioButton.Show();
|
||||||
_radioButtonToPathDictionary.Add(radioButton, path);
|
radioButton.Active = true;
|
||||||
|
|
||||||
radioButton.Show();
|
|
||||||
radioButton.Active = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (InvalidDataException exception)
|
else
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, $"{exception.Message}. Errored File: {path}");
|
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
|
||||||
|
|
||||||
if (showErrorDialog)
|
|
||||||
{
|
|
||||||
GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", "The NCA header content type check has failed. This is usually because the header key is incorrect or missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
catch (MissingKeyException exception)
|
}
|
||||||
|
catch (InvalidDataException exception)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application, $"{exception.Message}. Errored File: {path}");
|
||||||
|
|
||||||
|
if (showErrorDialog)
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {path}");
|
GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", "The NCA header content type check has failed. This is usually because the header key is incorrect or missing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (MissingKeyException exception)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {path}");
|
||||||
|
|
||||||
if (showErrorDialog)
|
if (showErrorDialog)
|
||||||
{
|
{
|
||||||
GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", $"Your key set is missing a key with the name: {exception.Name}");
|
GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", $"Your key set is missing a key with the name: {exception.Name}");
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue