0
0
Fork 0

Refactor out Application details from Horizon (#1236)

* Initial Application refactor

* Misc typo and access modifier fixes

* Clean unused namespaces

* Address gdkchan's comments

* Move ticket reading to common method

* Change IParentalControlService to use ApplicationLoader.ControlData
This commit is contained in:
mageven 2020-05-15 11:46:46 +05:30 committed by GitHub
parent 0ff00bd6d3
commit ba4830293e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 609 additions and 641 deletions

View file

@ -580,7 +580,7 @@ namespace Ryujinx.HLE.FileSystem.Content
SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
{
IntegrityCheckLevel integrityCheckLevel = Switch.GetIntigrityCheckLevel();
IntegrityCheckLevel integrityCheckLevel = Switch.GetIntegrityCheckLevel();
SystemVersion systemVersion = null;
@ -743,7 +743,7 @@ namespace Ryujinx.HLE.FileSystem.Content
SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
{
IntegrityCheckLevel integrityCheckLevel = Switch.GetIntigrityCheckLevel();
IntegrityCheckLevel integrityCheckLevel = Switch.GetIntegrityCheckLevel();
SystemVersion systemVersion = null;
@ -874,7 +874,7 @@ namespace Ryujinx.HLE.FileSystem.Content
public SystemVersion GetCurrentFirmwareVersion()
{
IntegrityCheckLevel integrityCheckLevel = Switch.GetIntigrityCheckLevel();
IntegrityCheckLevel integrityCheckLevel = Switch.GetIntegrityCheckLevel();
LoadEntries();

View file

@ -1,7 +1,9 @@
using LibHac;
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsService;
using LibHac.FsSystem;
using LibHac.Spl;
using Ryujinx.HLE.FileSystem.Content;
using Ryujinx.HLE.HOS;
using System;
@ -261,6 +263,21 @@ namespace Ryujinx.HLE.FileSystem
KeySet = ExternalKeyReader.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile);
}
public void ImportTickets(IFileSystem fs)
{
foreach (DirectoryEntryEx ticketEntry in fs.EnumerateEntries("/", "*.tik"))
{
Result result = fs.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet)));
}
}
}
public void Unload()
{
RomFs?.Dispose();
@ -283,7 +300,7 @@ namespace Ryujinx.HLE.FileSystem
{
if (_isInitialized)
{
throw new InvalidOperationException($"VirtualFileSystem can only be instanciated once!");
throw new InvalidOperationException($"VirtualFileSystem can only be instantiated once!");
}
_isInitialized = true;

View file

@ -0,0 +1,537 @@
using LibHac;
using LibHac.Account;
using LibHac.Common;
using LibHac.Fs;
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.HLE.FileSystem;
using Ryujinx.HLE.FileSystem.Content;
using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Npdm;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using static LibHac.Fs.ApplicationSaveDataManagement;
namespace Ryujinx.HLE.HOS
{
using JsonHelper = Common.Utilities.JsonHelper;
public class ApplicationLoader
{
private readonly Switch _device;
private readonly ContentManager _contentManager;
private readonly VirtualFileSystem _fileSystem;
public IntegrityCheckLevel FsIntegrityCheckLevel => _device.System.FsIntegrityCheckLevel;
public ulong TitleId { get; private set; }
public string TitleIdText => TitleId.ToString("x16");
public string TitleName { get; private set; }
public string TitleVersionString { get; private set; }
public bool TitleIs64Bit { get; private set; }
public BlitStruct<ApplicationControlProperty> ControlData { get; set; }
public ApplicationLoader(Switch device, VirtualFileSystem fileSystem, ContentManager contentManager)
{
_device = device;
_contentManager = contentManager;
_fileSystem = fileSystem;
ControlData = new BlitStruct<ApplicationControlProperty>(1);
}
public void LoadCart(string exeFsDir, string romFsFile = null)
{
if (romFsFile != null)
{
_fileSystem.LoadRomFs(romFsFile);
}
LocalFileSystem codeFs = new LocalFileSystem(exeFsDir);
LoadExeFs(codeFs, out _);
if (TitleId != 0)
{
EnsureSaveData(new TitleId(TitleId));
}
}
private (Nca Main, Nca Patch, Nca Control) GetGameData(PartitionFileSystem pfs)
{
Nca mainNca = null;
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());
if (nca.Header.ContentType == NcaContentType.Program)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
patchNca = nca;
}
else
{
mainNca = nca;
}
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
return (mainNca, patchNca, controlNca);
}
public void LoadXci(string xciFile)
{
FileStream file = new FileStream(xciFile, FileMode.Open, FileAccess.Read);
Xci xci = new Xci(_fileSystem.KeySet, file.AsStorage());
if (!xci.HasPartition(XciPartitionType.Secure))
{
Logger.PrintError(LogClass.Loader, "Unable to load XCI: Could not find XCI secure partition");
return;
}
PartitionFileSystem securePartition = xci.OpenPartition(XciPartitionType.Secure);
Nca mainNca = null;
Nca patchNca = null;
Nca controlNca = null;
try
{
(mainNca, patchNca, controlNca) = GetGameData(securePartition);
}
catch (Exception e)
{
Logger.PrintError(LogClass.Loader, $"Unable to load XCI: {e.Message}");
return;
}
if (mainNca == null)
{
Logger.PrintError(LogClass.Loader, "Unable to load XCI: Could not find Main NCA");
return;
}
_contentManager.LoadEntries(_device);
LoadNca(mainNca, patchNca, controlNca);
}
public void LoadNsp(string nspFile)
{
FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
Nca mainNca = null;
Nca patchNca = null;
Nca controlNca = null;
try
{
(mainNca, patchNca, controlNca) = GetGameData(nsp);
}
catch (Exception e)
{
Logger.PrintError(LogClass.Loader, $"Unable to load NSP: {e.Message}");
return;
}
if (mainNca == null)
{
Logger.PrintError(LogClass.Loader, "Unable to load NSP: Could not find Main NCA");
return;
}
if (mainNca != null)
{
LoadNca(mainNca, patchNca, controlNca);
return;
}
// This is not a normal NSP, it's actually a ExeFS as a NSP
LoadExeFs(nsp, out _);
}
public void LoadNca(string ncaFile)
{
FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
Nca nca = new Nca(_fileSystem.KeySet, file.AsStorage(false));
LoadNca(nca, null, null);
}
private void LoadNca(Nca mainNca, Nca patchNca, Nca controlNca)
{
if (mainNca.Header.ContentType != NcaContentType.Program)
{
Logger.PrintError(LogClass.Loader, "Selected NCA is not a \"Program\" NCA");
return;
}
IStorage dataStorage = null;
IFileSystem codeFs = null;
string titleUpdateMetadataPath = System.IO.Path.Combine(_fileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json");
if (File.Exists(titleUpdateMetadataPath))
{
string 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());
_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 (patchNca == null)
{
if (mainNca.CanOpenSection(NcaSectionType.Data))
{
dataStorage = mainNca.OpenStorage(NcaSectionType.Data, FsIntegrityCheckLevel);
}
if (mainNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, FsIntegrityCheckLevel);
}
}
else
{
if (patchNca.CanOpenSection(NcaSectionType.Data))
{
dataStorage = mainNca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, FsIntegrityCheckLevel);
}
if (patchNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, FsIntegrityCheckLevel);
}
}
if (codeFs == null)
{
Logger.PrintError(LogClass.Loader, "No ExeFS found in NCA");
return;
}
if (dataStorage == null)
{
Logger.PrintWarning(LogClass.Loader, "No RomFS found in NCA");
}
else
{
_fileSystem.SetRomFs(dataStorage.AsStream(FileAccess.Read));
}
LoadExeFs(codeFs, out Npdm metaData);
TitleId = metaData.Aci0.TitleId;
TitleIs64Bit = metaData.Is64Bit;
if (controlNca != null)
{
ReadControlData(controlNca);
}
else
{
ControlData.ByteSpan.Clear();
}
if (TitleId != 0)
{
EnsureSaveData(new TitleId(TitleId));
}
Logger.PrintInfo(LogClass.Loader, $"Application Loaded: {TitleName} v{TitleVersionString} [{TitleIdText}] [{(TitleIs64Bit ? "64-bit" : "32-bit")}]");
}
public void ReadControlData(Nca controlNca)
{
IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, FsIntegrityCheckLevel);
Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
result = controlFile.Read(out long bytesRead, 0, ControlData.ByteSpan, ReadOption.None);
if (result.IsSuccess() && bytesRead == ControlData.ByteSpan.Length)
{
TitleName = ControlData.Value
.Titles[(int)_device.System.State.DesiredTitleLanguage].Name.ToString();
if (string.IsNullOrWhiteSpace(TitleName))
{
TitleName = ControlData.Value.Titles.ToArray()
.FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
}
TitleVersionString = ControlData.Value.DisplayVersion.ToString();
}
}
else
{
ControlData.ByteSpan.Clear();
}
}
private void LoadExeFs(IFileSystem codeFs, out Npdm metaData)
{
Result result = codeFs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read);
if (ResultFs.PathNotFound.Includes(result))
{
Logger.PrintWarning(LogClass.Loader, "NPDM file not found, using default values!");
metaData = GetDefaultNpdm();
}
else
{
metaData = new Npdm(npdmFile.AsStream());
}
List<IExecutable> nsos = new List<IExecutable>();
void LoadNso(string filename)
{
foreach (DirectoryEntryEx file in codeFs.EnumerateEntries("/", $"{filename}*"))
{
if (Path.GetExtension(file.Name) != string.Empty)
{
continue;
}
Logger.PrintInfo(LogClass.Loader, $"Loading {file.Name}...");
codeFs.OpenFile(out IFile nsoFile, file.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
NsoExecutable nso = new NsoExecutable(nsoFile.AsStorage());
nsos.Add(nso);
}
}
TitleId = metaData.Aci0.TitleId;
TitleIs64Bit = metaData.Is64Bit;
LoadNso("rtld");
LoadNso("main");
LoadNso("subsdk");
LoadNso("sdk");
_contentManager.LoadEntries(_device);
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: nsos.ToArray());
}
public void LoadProgram(string filePath)
{
Npdm metaData = GetDefaultNpdm();
bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
IExecutable nro;
if (isNro)
{
FileStream input = new FileStream(filePath, FileMode.Open);
NroExecutable obj = new NroExecutable(input);
nro = obj;
// homebrew NRO can actually have some data after the actual NRO
if (input.Length > obj.FileSize)
{
input.Position = obj.FileSize;
BinaryReader reader = new BinaryReader(input);
uint asetMagic = reader.ReadUInt32();
if (asetMagic == 0x54455341)
{
uint asetVersion = reader.ReadUInt32();
if (asetVersion == 0)
{
ulong iconOffset = reader.ReadUInt64();
ulong iconSize = reader.ReadUInt64();
ulong nacpOffset = reader.ReadUInt64();
ulong nacpSize = reader.ReadUInt64();
ulong romfsOffset = reader.ReadUInt64();
ulong romfsSize = reader.ReadUInt64();
if (romfsSize != 0)
{
_fileSystem.SetRomFs(new HomebrewRomFsStream(input, obj.FileSize + (long)romfsOffset));
}
if (nacpSize != 0)
{
input.Seek(obj.FileSize + (long)nacpOffset, SeekOrigin.Begin);
reader.Read(ControlData.ByteSpan);
ref ApplicationControlProperty nacp = ref ControlData.Value;
metaData.TitleName = nacp.Titles[(int)_device.System.State.DesiredTitleLanguage].Name.ToString();
if (string.IsNullOrWhiteSpace(metaData.TitleName))
{
metaData.TitleName = nacp.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
}
if (nacp.PresenceGroupId != 0)
{
metaData.Aci0.TitleId = nacp.PresenceGroupId;
}
else if (nacp.SaveDataOwnerId.Value != 0)
{
metaData.Aci0.TitleId = nacp.SaveDataOwnerId.Value;
}
else if (nacp.AddOnContentBaseId != 0)
{
metaData.Aci0.TitleId = nacp.AddOnContentBaseId - 0x1000;
}
else
{
metaData.Aci0.TitleId = 0000000000000000;
}
}
}
else
{
Logger.PrintWarning(LogClass.Loader, $"Unsupported ASET header version found \"{asetVersion}\"");
}
}
}
}
else
{
nro = new NsoExecutable(new LocalStorage(filePath, FileAccess.Read));
}
_contentManager.LoadEntries(_device);
TitleName = metaData.TitleName;
TitleId = metaData.Aci0.TitleId;
TitleIs64Bit = metaData.Is64Bit;
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: nro);
}
private Npdm GetDefaultNpdm()
{
Assembly asm = Assembly.GetCallingAssembly();
using (Stream npdmStream = asm.GetManifestResourceStream("Ryujinx.HLE.Homebrew.npdm"))
{
return new Npdm(npdmStream);
}
}
private Result EnsureSaveData(TitleId titleId)
{
Logger.PrintInfo(LogClass.Application, "Ensuring required savedata exists.");
Uid user = _device.System.State.Account.LastOpenedUser.UserId.ToLibHacUid();
ref ApplicationControlProperty control = ref ControlData.Value;
if (Util.IsEmpty(ControlData.ByteSpan))
{
// If the current application doesn't have a loaded control property, create a dummy one
// and set the savedata sizes so a user savedata will be created.
control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
// The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
control.UserAccountSaveDataSize = 0x4000;
control.UserAccountSaveDataJournalSize = 0x4000;
Logger.PrintWarning(LogClass.Application,
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
}
FileSystemClient fs = _fileSystem.FsClient;
Result rc = fs.EnsureApplicationCacheStorage(out _, titleId, ref control);
if (rc.IsFailure())
{
Logger.PrintError(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {rc.ToStringWithName()}");
return rc;
}
rc = EnsureApplicationSaveData(fs, out _, titleId, ref control, ref user);
if (rc.IsFailure())
{
Logger.PrintError(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {rc.ToStringWithName()}");
}
return rc;
}
}
}

View file

@ -1,16 +1,8 @@
using LibHac;
using LibHac.Account;
using LibHac.Bcat;
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsSystem;
using LibHac.FsSystem.NcaUtils;
using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Spl;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Configuration;
using Ryujinx.HLE.FileSystem.Content;
using Ryujinx.HLE.HOS.Font;
@ -30,20 +22,14 @@ using Ryujinx.HLE.HOS.Services.SurfaceFlinger;
using Ryujinx.HLE.HOS.Services.Time.Clock;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.HLE.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using static LibHac.Fs.ApplicationSaveDataManagement;
namespace Ryujinx.HLE.HOS
{
using TimeServiceManager = Services.Time.TimeManager;
using JsonHelper = Common.Utilities.JsonHelper;
public class Horizon : IDisposable
{
@ -80,16 +66,6 @@ namespace Ryujinx.HLE.HOS
#pragma warning restore CS0649
private bool _isDisposed;
public BlitStruct<ApplicationControlProperty> ControlData { get; set; }
public string TitleName { get; private set; }
public ulong TitleId { get; private set; }
public string TitleIdText => TitleId.ToString("x16");
public string TitleVersionString { get; private set; }
public bool TitleIs64Bit { get; private set; }
public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; }
@ -104,8 +80,6 @@ namespace Ryujinx.HLE.HOS
public Horizon(Switch device, ContentManager contentManager)
{
ControlData = new BlitStruct<ApplicationControlProperty>(1);
KernelContext = new KernelContext(device, device.Memory);
Device = device;
@ -207,6 +181,13 @@ namespace Ryujinx.HLE.HOS
InitLibHacHorizon();
}
public void LoadKip(string kipFile)
{
using IStorage fs = new LocalStorage(kipFile, FileAccess.Read);
ProgramLoader.LoadKip(KernelContext, new KipExecutable(fs));
}
private void InitLibHacHorizon()
{
LibHac.Horizon horizon = new LibHac.Horizon(null, Device.FileSystem.FsServer);
@ -233,537 +214,6 @@ namespace Ryujinx.HLE.HOS
}
}
public void LoadCart(string exeFsDir, string romFsFile = null)
{
if (romFsFile != null)
{
Device.FileSystem.LoadRomFs(romFsFile);
}
LocalFileSystem codeFs = new LocalFileSystem(exeFsDir);
LoadExeFs(codeFs, out _);
if (TitleId != 0)
{
EnsureSaveData(new TitleId(TitleId));
}
}
public void LoadXci(string xciFile)
{
FileStream file = new FileStream(xciFile, FileMode.Open, FileAccess.Read);
Xci xci = new Xci(KeySet, file.AsStorage());
(Nca mainNca, Nca patchNca, Nca controlNca) = GetXciGameData(xci);
if (mainNca == null)
{
Logger.PrintError(LogClass.Loader, "Unable to load XCI");
return;
}
ContentManager.LoadEntries(Device);
LoadNca(mainNca, patchNca, controlNca);
}
public void LoadKip(string kipFile)
{
using (IStorage fs = new LocalStorage(kipFile, FileAccess.Read))
{
ProgramLoader.LoadKip(KernelContext, new KipExecutable(fs));
}
}
private (Nca Main, Nca patch, Nca Control) GetXciGameData(Xci xci)
{
if (!xci.HasPartition(XciPartitionType.Secure))
{
throw new InvalidDataException("Could not find XCI secure partition");
}
Nca mainNca = null;
Nca patchNca = null;
Nca controlNca = null;
XciPartition securePartition = xci.OpenPartition(XciPartitionType.Secure);
foreach (DirectoryEntryEx ticketEntry in securePartition.EnumerateEntries("/", "*.tik"))
{
Result result = securePartition.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet)));
}
}
foreach (DirectoryEntryEx fileEntry in securePartition.EnumerateEntries("/", "*.nca"))
{
Result result = securePartition.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsFailure())
{
continue;
}
Nca nca = new Nca(KeySet, ncaFile.AsStorage());
if (nca.Header.ContentType == NcaContentType.Program)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
patchNca = nca;
}
else
{
mainNca = nca;
}
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
if (mainNca == null)
{
Logger.PrintError(LogClass.Loader, "Could not find an Application NCA in the provided XCI file");
}
if (controlNca != null)
{
ReadControlData(controlNca);
}
else
{
ControlData.ByteSpan.Clear();
}
return (mainNca, patchNca, controlNca);
}
public void ReadControlData(Nca controlNca)
{
IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, FsIntegrityCheckLevel);
Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
result = controlFile.Read(out long bytesRead, 0, ControlData.ByteSpan, ReadOption.None);
if (result.IsSuccess() && bytesRead == ControlData.ByteSpan.Length)
{
TitleName = ControlData.Value
.Titles[(int) State.DesiredTitleLanguage].Name.ToString();
if (string.IsNullOrWhiteSpace(TitleName))
{
TitleName = ControlData.Value.Titles.ToArray()
.FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
}
TitleVersionString = ControlData.Value.DisplayVersion.ToString();
}
}
else
{
ControlData.ByteSpan.Clear();
}
}
public void LoadNca(string ncaFile)
{
FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
Nca nca = new Nca(KeySet, file.AsStorage(false));
LoadNca(nca, null, null);
}
public void LoadNsp(string nspFile)
{
FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik"))
{
Result result = nsp.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet)));
}
}
Nca mainNca = null;
Nca patchNca = null;
Nca controlNca = null;
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
{
nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(KeySet, ncaFile.AsStorage());
if (nca.Header.ContentType == NcaContentType.Program)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
patchNca = nca;
}
else
{
mainNca = nca;
}
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
if (mainNca != null)
{
LoadNca(mainNca, patchNca, controlNca);
return;
}
// This is not a normal NSP, it's actually a ExeFS as a NSP
LoadExeFs(nsp, out _);
}
public void LoadNca(Nca mainNca, Nca patchNca, Nca controlNca)
{
if (mainNca.Header.ContentType != NcaContentType.Program)
{
Logger.PrintError(LogClass.Loader, "Selected NCA is not a \"Program\" NCA");
return;
}
IStorage dataStorage = null;
IFileSystem codeFs = null;
string titleUpdateMetadataPath = System.IO.Path.Combine(Device.FileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json");
if (File.Exists(titleUpdateMetadataPath))
{
string 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());
foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik"))
{
Result result = nsp.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet)));
}
}
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
{
nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(KeySet, ncaFile.AsStorage());
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != mainNca.Header.TitleId.ToString("x16"))
{
break;
}
if (nca.Header.ContentType == NcaContentType.Program)
{
patchNca = nca;
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
}
}
if (patchNca == null)
{
if (mainNca.CanOpenSection(NcaSectionType.Data))
{
dataStorage = mainNca.OpenStorage(NcaSectionType.Data, FsIntegrityCheckLevel);
}
if (mainNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, FsIntegrityCheckLevel);
}
}
else
{
if (patchNca.CanOpenSection(NcaSectionType.Data))
{
dataStorage = mainNca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, FsIntegrityCheckLevel);
}
if (patchNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, FsIntegrityCheckLevel);
}
}
if (codeFs == null)
{
Logger.PrintError(LogClass.Loader, "No ExeFS found in NCA");
return;
}
if (dataStorage == null)
{
Logger.PrintWarning(LogClass.Loader, "No RomFS found in NCA");
}
else
{
Device.FileSystem.SetRomFs(dataStorage.AsStream(FileAccess.Read));
}
LoadExeFs(codeFs, out Npdm metaData);
TitleId = metaData.Aci0.TitleId;
TitleIs64Bit = metaData.Is64Bit;
if (controlNca != null)
{
ReadControlData(controlNca);
}
else
{
ControlData.ByteSpan.Clear();
}
if (TitleId != 0)
{
EnsureSaveData(new TitleId(TitleId));
}
Logger.PrintInfo(LogClass.Loader, $"Application Loaded: {TitleName} v{TitleVersionString} [{TitleIdText}] [{(TitleIs64Bit ? "64-bit" : "32-bit")}]");
}
private void LoadExeFs(IFileSystem codeFs, out Npdm metaData)
{
Result result = codeFs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read);
if (ResultFs.PathNotFound.Includes(result))
{
Logger.PrintWarning(LogClass.Loader, "NPDM file not found, using default values!");
metaData = GetDefaultNpdm();
}
else
{
metaData = new Npdm(npdmFile.AsStream());
}
List<IExecutable> staticObjects = new List<IExecutable>();
void LoadNso(string filename)
{
foreach (DirectoryEntryEx file in codeFs.EnumerateEntries("/", $"{filename}*"))
{
if (Path.GetExtension(file.Name) != string.Empty)
{
continue;
}
Logger.PrintInfo(LogClass.Loader, $"Loading {file.Name}...");
codeFs.OpenFile(out IFile nsoFile, file.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
NsoExecutable staticObject = new NsoExecutable(nsoFile.AsStorage());
staticObjects.Add(staticObject);
}
}
TitleId = metaData.Aci0.TitleId;
TitleIs64Bit = metaData.Is64Bit;
LoadNso("rtld");
LoadNso("main");
LoadNso("subsdk");
LoadNso("sdk");
ContentManager.LoadEntries(Device);
ProgramLoader.LoadNsos(KernelContext, metaData, staticObjects.ToArray());
}
public void LoadProgram(string filePath)
{
Npdm metaData = GetDefaultNpdm();
bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
IExecutable staticObject;
if (isNro)
{
FileStream input = new FileStream(filePath, FileMode.Open);
NroExecutable obj = new NroExecutable(input);
staticObject = obj;
// homebrew NRO can actually have some data after the actual NRO
if (input.Length > obj.FileSize)
{
input.Position = obj.FileSize;
BinaryReader reader = new BinaryReader(input);
uint asetMagic = reader.ReadUInt32();
if (asetMagic == 0x54455341)
{
uint asetVersion = reader.ReadUInt32();
if (asetVersion == 0)
{
ulong iconOffset = reader.ReadUInt64();
ulong iconSize = reader.ReadUInt64();
ulong nacpOffset = reader.ReadUInt64();
ulong nacpSize = reader.ReadUInt64();
ulong romfsOffset = reader.ReadUInt64();
ulong romfsSize = reader.ReadUInt64();
if (romfsSize != 0)
{
Device.FileSystem.SetRomFs(new HomebrewRomFsStream(input, obj.FileSize + (long)romfsOffset));
}
if (nacpSize != 0)
{
input.Seek(obj.FileSize + (long)nacpOffset, SeekOrigin.Begin);
reader.Read(ControlData.ByteSpan);
ref ApplicationControlProperty nacp = ref ControlData.Value;
metaData.TitleName = nacp.Titles[(int)State.DesiredTitleLanguage].Name.ToString();
if (string.IsNullOrWhiteSpace(metaData.TitleName))
{
metaData.TitleName = nacp.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
}
if (nacp.PresenceGroupId != 0)
{
metaData.Aci0.TitleId = nacp.PresenceGroupId;
}
else if (nacp.SaveDataOwnerId.Value != 0)
{
metaData.Aci0.TitleId = nacp.SaveDataOwnerId.Value;
}
else if (nacp.AddOnContentBaseId != 0)
{
metaData.Aci0.TitleId = nacp.AddOnContentBaseId - 0x1000;
}
else
{
metaData.Aci0.TitleId = 0000000000000000;
}
}
}
else
{
Logger.PrintWarning(LogClass.Loader, $"Unsupported ASET header version found \"{asetVersion}\"");
}
}
}
}
else
{
staticObject = new NsoExecutable(new LocalStorage(filePath, FileAccess.Read));
}
ContentManager.LoadEntries(Device);
TitleName = metaData.TitleName;
TitleId = metaData.Aci0.TitleId;
TitleIs64Bit = metaData.Is64Bit;
ProgramLoader.LoadNsos(KernelContext, metaData, new IExecutable[] { staticObject });
}
private Npdm GetDefaultNpdm()
{
Assembly asm = Assembly.GetCallingAssembly();
using (Stream npdmStream = asm.GetManifestResourceStream("Ryujinx.HLE.Homebrew.npdm"))
{
return new Npdm(npdmStream);
}
}
private Result EnsureSaveData(TitleId titleId)
{
Logger.PrintInfo(LogClass.Application, "Ensuring required savedata exists.");
Uid user = State.Account.LastOpenedUser.UserId.ToLibHacUid();
ref ApplicationControlProperty control = ref ControlData.Value;
if (LibHac.Util.IsEmpty(ControlData.ByteSpan))
{
// If the current application doesn't have a loaded control property, create a dummy one
// and set the savedata sizes so a user savedata will be created.
control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
// The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
control.UserAccountSaveDataSize = 0x4000;
control.UserAccountSaveDataJournalSize = 0x4000;
Logger.PrintWarning(LogClass.Application,
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
}
FileSystemClient fs = Device.FileSystem.FsClient;
Result rc = fs.EnsureApplicationCacheStorage(out _, titleId, ref control);
if (rc.IsFailure())
{
Logger.PrintError(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {rc.ToStringWithName()}");
}
rc = EnsureApplicationSaveData(fs, out _, titleId, ref control, ref user);
if (rc.IsFailure())
{
Logger.PrintError(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {rc.ToStringWithName()}");
}
return rc;
}
public void SignalDisplayResolutionChange()
{
DisplayResolutionChangeEvent.ReadableEvent.Signal();

View file

@ -123,19 +123,19 @@ namespace Ryujinx.HLE.HOS
public static bool LoadNsos(
KernelContext context,
Npdm metaData,
IExecutable[] nsos,
byte[] arguments = null)
byte[] arguments = null,
params IExecutable[] executables)
{
ulong argsStart = 0;
int argsSize = 0;
ulong codeStart = metaData.Is64Bit ? 0x8000000UL : 0x200000UL;
int codeSize = 0;
ulong[] nsoBase = new ulong[nsos.Length];
ulong[] nsoBase = new ulong[executables.Length];
for (int index = 0; index < nsos.Length; index++)
for (int index = 0; index < executables.Length; index++)
{
IExecutable staticObject = nsos[index];
IExecutable staticObject = executables[index];
int textEnd = staticObject.TextOffset + staticObject.Text.Length;
int roEnd = staticObject.RoOffset + staticObject.Ro.Length;
@ -226,11 +226,11 @@ namespace Ryujinx.HLE.HOS
return false;
}
for (int index = 0; index < nsos.Length; index++)
for (int index = 0; index < executables.Length; index++)
{
Logger.PrintInfo(LogClass.Loader, $"Loading image {index} at 0x{nsoBase[index]:x16}...");
result = LoadIntoMemory(process, nsos[index], nsoBase[index]);
result = LoadIntoMemory(process, executables[index], nsoBase[index]);
if (result != KernelResult.Success)
{

View file

@ -289,7 +289,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
// Account actually calls nn::arp::detail::IReader::GetApplicationControlProperty() with the current PID and store the result (NACP File) internally.
// But since we use LibHac and we load one Application at a time, it's not necessary.
context.ResponseData.Write(context.Device.System.ControlData.Value.UserAccountSwitchLock);
context.ResponseData.Write(context.Device.Application.ControlData.Value.UserAccountSwitchLock);
Logger.PrintStub(LogClass.ServiceAcc);

View file

@ -49,7 +49,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
TitleId titleId = new TitleId(context.Process.TitleId);
BlitStruct<ApplicationControlProperty> controlHolder = context.Device.System.ControlData;
BlitStruct<ApplicationControlProperty> controlHolder = context.Device.Application.ControlData;
ref ApplicationControlProperty control = ref controlHolder.Value;

View file

@ -33,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Services.Arp
return new ApplicationLaunchProperty
{
TitleId = context.Device.System.TitleId,
TitleId = context.Device.Application.TitleId,
Version = 0x00,
BaseGameStorageId = (byte)StorageId.NandSystem,
UpdateGameStorageId = (byte)StorageId.None

View file

@ -22,7 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Arp
launchProperty = new LibHac.Arp.ApplicationLaunchProperty();
launchProperty.BaseStorageId = StorageId.BuiltInUser;
launchProperty.ApplicationId = new ApplicationId(System.TitleId);
launchProperty.ApplicationId = new ApplicationId(System.Device.Application.TitleId);
return Result.Success;
}

View file

@ -14,7 +14,7 @@
long position = context.Request.ReceiveBuff[0].Position;
byte[] nacpData = context.Device.System.ControlData.ByteSpan.ToArray();
byte[] nacpData = context.Device.Application.ControlData.ByteSpan.ToArray();
context.Memory.Write((ulong)position, nacpData);

View file

@ -13,7 +13,7 @@
long position = context.Request.ReceiveBuff[0].Position;
byte[] nacpData = context.Device.System.ControlData.ByteSpan.ToArray();
byte[] nacpData = context.Device.Application.ControlData.ByteSpan.ToArray();
context.Memory.Write((ulong)position, nacpData);

View file

@ -43,8 +43,8 @@ namespace Ryujinx.HLE.HOS.Services.Pctl.ParentalControlServiceFactory
_titleId = titleId;
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented, if it return ResultCode.Success we assign fields.
_ratingAge = Array.ConvertAll(context.Device.System.ControlData.Value.RatingAge.ToArray(), Convert.ToInt32);
_freeCommunicationEnabled = context.Device.System.ControlData.Value.ParentalControl == LibHac.Ns.ParentalControlFlagValue.FreeCommunication;
_ratingAge = Array.ConvertAll(context.Device.Application.ControlData.Value.RatingAge.ToArray(), Convert.ToInt32);
_freeCommunicationEnabled = context.Device.Application.ControlData.Value.ParentalControl == LibHac.Ns.ParentalControlFlagValue.FreeCommunication;
}
}

View file

@ -31,7 +31,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
}
}
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.System.ControlData.Value.PlayLogQueryCapability;
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.Application.ControlData.Value.PlayLogQueryCapability;
List<ulong> titleIds = new List<ulong>();
@ -45,7 +45,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
// Check if input title ids are in the whitelist.
foreach (ulong titleId in titleIds)
{
if (!context.Device.System.ControlData.Value.PlayLogQueryableApplicationId.Contains(titleId))
if (!context.Device.Application.ControlData.Value.PlayLogQueryableApplicationId.Contains(titleId))
{
return (ResultCode)Am.ResultCode.ObjectInvalid;
}

View file

@ -27,6 +27,8 @@ namespace Ryujinx.HLE
public Horizon System { get; private set; }
public ApplicationLoader Application { get; }
public PerformanceStatistics Statistics { get; private set; }
public Hid Hid { get; private set; }
@ -59,6 +61,8 @@ namespace Ryujinx.HLE
Hid = new Hid(this, System.HidBaseAddress);
Hid.InitDevices();
Application = new ApplicationLoader(this, fileSystem, contentManager);
}
public void Initialize()
@ -76,14 +80,14 @@ namespace Ryujinx.HLE
System.EnableMultiCoreScheduling();
}
System.FsIntegrityCheckLevel = GetIntigrityCheckLevel();
System.FsIntegrityCheckLevel = GetIntegrityCheckLevel();
System.GlobalAccessLogMode = ConfigurationState.Instance.System.FsGlobalAccessLogMode;
ServiceConfiguration.IgnoreMissingServices = ConfigurationState.Instance.System.IgnoreMissingServices;
}
public static IntegrityCheckLevel GetIntigrityCheckLevel()
public static IntegrityCheckLevel GetIntegrityCheckLevel()
{
return ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
@ -92,27 +96,27 @@ namespace Ryujinx.HLE
public void LoadCart(string exeFsDir, string romFsFile = null)
{
System.LoadCart(exeFsDir, romFsFile);
Application.LoadCart(exeFsDir, romFsFile);
}
public void LoadXci(string xciFile)
{
System.LoadXci(xciFile);
Application.LoadXci(xciFile);
}
public void LoadNca(string ncaFile)
{
System.LoadNca(ncaFile);
Application.LoadNca(ncaFile);
}
public void LoadNsp(string nspFile)
{
System.LoadNsp(nspFile);
Application.LoadNsp(nspFile);
}
public void LoadProgram(string fileName)
{
System.LoadProgram(fileName);
Application.LoadProgram(fileName);
}
public bool WaitFifo()

View file

@ -474,17 +474,7 @@ namespace Ryujinx.Ui
Nca controlNca = null;
// Add keys to key set if needed
foreach (DirectoryEntryEx ticketEntry in pfs.EnumerateEntries("/", "*.tik"))
{
Result result = pfs.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
_virtualFileSystem.KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_virtualFileSystem.KeySet)));
}
}
_virtualFileSystem.ImportTickets(pfs);
// Find the Control NCA and store it in variable called controlNca
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
@ -661,17 +651,7 @@ namespace Ryujinx.Ui
{
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik"))
{
Result result = nsp.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
_virtualFileSystem.KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_virtualFileSystem.KeySet)));
}
}
_virtualFileSystem.ImportTickets(nsp);
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
{

View file

@ -180,16 +180,16 @@ namespace Ryujinx.Ui
{
parent.Present();
string titleNameSection = string.IsNullOrWhiteSpace(_device.System.TitleName) ? string.Empty
: $" - {_device.System.TitleName}";
string titleNameSection = string.IsNullOrWhiteSpace(_device.Application.TitleName) ? string.Empty
: $" - {_device.Application.TitleName}";
string titleVersionSection = string.IsNullOrWhiteSpace(_device.System.TitleVersionString) ? string.Empty
: $" v{_device.System.TitleVersionString}";
string titleVersionSection = string.IsNullOrWhiteSpace(_device.Application.TitleVersionString) ? string.Empty
: $" v{_device.Application.TitleVersionString}";
string titleIdSection = string.IsNullOrWhiteSpace(_device.System.TitleIdText) ? string.Empty
: $" ({_device.System.TitleIdText.ToUpper()})";
string titleIdSection = string.IsNullOrWhiteSpace(_device.Application.TitleIdText) ? string.Empty
: $" ({_device.Application.TitleIdText.ToUpper()})";
string titleArchSection = _device.System.TitleIs64Bit ? " (64-bit)" : " (32-bit)";
string titleArchSection = _device.Application.TitleIs64Bit ? " (64-bit)" : " (32-bit)";
parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
});

View file

@ -280,17 +280,7 @@ namespace Ryujinx.Ui
FileStream updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new PartitionFileSystem(updateFile.AsStorage());
foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik"))
{
Result result = nsp.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
_virtualFileSystem.KeySet.ExternalKeySet.Add(new LibHac.Fs.RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_virtualFileSystem.KeySet)));
}
}
_virtualFileSystem.ImportTickets(nsp);
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
{

View file

@ -435,9 +435,9 @@ namespace Ryujinx.Ui
_firmwareInstallFile.Sensitive = false;
_firmwareInstallDirectory.Sensitive = false;
DiscordIntegrationModule.SwitchToPlayingState(device.System.TitleIdText, device.System.TitleName);
DiscordIntegrationModule.SwitchToPlayingState(device.Application.TitleIdText, device.Application.TitleName);
ApplicationLibrary.LoadAndSaveMetaData(device.System.TitleIdText, appMetadata =>
ApplicationLibrary.LoadAndSaveMetaData(device.Application.TitleIdText, appMetadata =>
{
appMetadata.LastPlayed = DateTime.UtcNow.ToString();
});
@ -580,7 +580,7 @@ namespace Ryujinx.Ui
if (device != null)
{
UpdateGameMetadata(device.System.TitleIdText);
UpdateGameMetadata(device.Application.TitleIdText);
if (_glWidget != null)
{

View file

@ -81,17 +81,7 @@ namespace Ryujinx.Ui
{
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik"))
{
Result result = nsp.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
_virtualFileSystem.KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_virtualFileSystem.KeySet)));
}
}
_virtualFileSystem.ImportTickets(nsp);
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
{