forked from MeloNX/MeloNX
* Add default values to ApplicationData directly * Refactor application loading It should now be possible to load multi game XCIs. Included updates won't be detected for now. Opening a game from the command line currently only opens the first one. * Only include program NCAs where at least one tuple item is not null * Get application data by title id and add programIndex check back * Refactor application loading again and remove duplicate code * Actually use patch ncas for updates * Fix number of applications found with multi game xcis * Don't load bundled updates from multi game xcis * Change ApplicationData.TitleId type to ulong & Add TitleIdString property * Use cnmt files and ContentCollection to load programs * Ava: Add updates and DLCs from gamecarts * Get the cnmt file from its NCA * Ava: Identify bundled updates in updater window * Fix the (hopefully) last few bugs * Add idOffset parameter to GetNcaByType * Handle missing file for dlc.json * Ava: Shorten error message for invalid files * Gtk: Add additional string for bundled updates in TitleUpdateWindow * Hopefully fix DLC issues * Apply formatting * Finally fix DLC issues * Adjust property names and fileSize field * Read the correct update file * Fix wrong casing for application id strings * Rename TitleId to ApplicationId * Address review comments * Fix formatting issues * Apply suggestions from code review Co-authored-by: gdkchan <gab.dark.100@gmail.com> * Gracefully fail when loading pfs for update and dlc window * Fix applications with multiple programs * Fix DLCWindow crash on GTK * Fix some GUI issues * Remove IsXci again --------- Co-authored-by: gdkchan <gab.dark.100@gmail.com>
273 lines
10 KiB
C#
273 lines
10 KiB
C#
using LibHac;
|
|
using LibHac.Common;
|
|
using LibHac.Fs;
|
|
using LibHac.Fs.Fsa;
|
|
using LibHac.FsSystem;
|
|
using LibHac.Loader;
|
|
using LibHac.Ncm;
|
|
using LibHac.Ns;
|
|
using LibHac.Tools.Fs;
|
|
using LibHac.Tools.FsSystem;
|
|
using LibHac.Tools.FsSystem.NcaUtils;
|
|
using LibHac.Tools.Ncm;
|
|
using Ryujinx.Common.Configuration;
|
|
using Ryujinx.Common.Logging;
|
|
using Ryujinx.Common.Utilities;
|
|
using Ryujinx.HLE.FileSystem;
|
|
using Ryujinx.HLE.HOS;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using ApplicationId = LibHac.Ncm.ApplicationId;
|
|
using ContentType = LibHac.Ncm.ContentType;
|
|
using Path = System.IO.Path;
|
|
|
|
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|
{
|
|
public static class NcaExtensions
|
|
{
|
|
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
|
|
|
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
|
|
{
|
|
// Extract RomFs and ExeFs from NCA.
|
|
IStorage romFs = nca.GetRomFs(device, patchNca);
|
|
IFileSystem exeFs = nca.GetExeFs(device, patchNca);
|
|
|
|
if (exeFs == null)
|
|
{
|
|
Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA");
|
|
|
|
return ProcessResult.Failed;
|
|
}
|
|
|
|
// Load Npdm file.
|
|
MetaLoader metaLoader = exeFs.GetNpdm();
|
|
|
|
// Collecting mods related to AocTitleIds and ProgramId.
|
|
device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
|
|
device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()),
|
|
ModLoader.GetModsBasePath(),
|
|
ModLoader.GetSdModsBasePath());
|
|
|
|
// Load Nacp file.
|
|
var nacpData = new BlitStruct<ApplicationControlProperty>(1);
|
|
|
|
if (controlNca != null)
|
|
{
|
|
nacpData = controlNca.GetNacp(device);
|
|
}
|
|
|
|
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update.
|
|
|
|
// Load program 0 control NCA as we are going to need it for display version.
|
|
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
|
|
|
// NOTE: Nintendo doesn't guarantee that the display version will be updated on sub programs when updating a multi program application.
|
|
// As such, to avoid PTC cache confusion, we only trust the program 0 display version when launching a sub program.
|
|
if (updateProgram0ControlNca != null && _device.Configuration.UserChannelPersistence.Index != 0)
|
|
{
|
|
nacpData.Value.DisplayVersion = updateProgram0ControlNca.GetNacp(_device).Value.DisplayVersion;
|
|
}
|
|
|
|
*/
|
|
|
|
ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader);
|
|
|
|
// Load RomFS.
|
|
if (romFs == null)
|
|
{
|
|
Logger.Warning?.Print(LogClass.Loader, "No RomFS found in NCA");
|
|
}
|
|
else
|
|
{
|
|
romFs = device.Configuration.VirtualFileSystem.ModLoader.ApplyRomFsMods(processResult.ProgramId, romFs);
|
|
|
|
device.Configuration.VirtualFileSystem.SetRomFs(processResult.ProcessId, romFs.AsStream(FileAccess.Read));
|
|
}
|
|
|
|
// Don't create save data for system programs.
|
|
if (processResult.ProgramId != 0 && (processResult.ProgramId < SystemProgramId.Start.Value || processResult.ProgramId > SystemAppletId.End.Value))
|
|
{
|
|
// Multi-program applications can technically use any program ID for the main program, but in practice they always use 0 in the low nibble.
|
|
// We'll know if this changes in the future because applications will get errors when trying to mount the correct save.
|
|
ProcessLoaderHelper.EnsureSaveData(device, new ApplicationId(processResult.ProgramId & ~0xFul), nacpData);
|
|
}
|
|
|
|
return processResult;
|
|
}
|
|
|
|
public static ulong GetProgramIdBase(this Nca nca)
|
|
{
|
|
return nca.Header.TitleId & ~0x1FFFUL;
|
|
}
|
|
|
|
public static int GetProgramIndex(this Nca nca)
|
|
{
|
|
return (int)(nca.Header.TitleId & 0xF);
|
|
}
|
|
|
|
public static bool IsProgram(this Nca nca)
|
|
{
|
|
return nca.Header.ContentType == NcaContentType.Program;
|
|
}
|
|
|
|
public static bool IsMain(this Nca nca)
|
|
{
|
|
return nca.IsProgram() && !nca.IsPatch();
|
|
}
|
|
|
|
public static bool IsPatch(this Nca nca)
|
|
{
|
|
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
|
|
|
return nca.IsProgram() && nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection();
|
|
}
|
|
|
|
public static bool IsControl(this Nca nca)
|
|
{
|
|
return nca.Header.ContentType == NcaContentType.Control;
|
|
}
|
|
|
|
public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath)
|
|
{
|
|
updatePath = "(unknown)";
|
|
|
|
// Load Update NCAs.
|
|
Nca updatePatchNca = null;
|
|
Nca updateControlNca = null;
|
|
|
|
// Clear the program index part.
|
|
ulong titleIdBase = mainNca.GetProgramIdBase();
|
|
|
|
// Load update information if exists.
|
|
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
|
|
if (File.Exists(titleUpdateMetadataPath))
|
|
{
|
|
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
|
if (File.Exists(updatePath))
|
|
{
|
|
var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
|
|
|
|
IFileSystem updatePartitionFileSystem;
|
|
|
|
if (Path.GetExtension(updatePath).ToLower() == ".xci")
|
|
{
|
|
updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
|
}
|
|
else
|
|
{
|
|
PartitionFileSystem pfsTemp = new();
|
|
pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure();
|
|
updatePartitionFileSystem = pfsTemp;
|
|
}
|
|
|
|
foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel))
|
|
{
|
|
if ((updateTitleId & ~0x1FFFUL) != titleIdBase)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex);
|
|
updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (updatePatchNca, updateControlNca);
|
|
}
|
|
|
|
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
|
|
{
|
|
IFileSystem exeFs = null;
|
|
|
|
if (patchNca == null)
|
|
{
|
|
if (nca.CanOpenSection(NcaSectionType.Code))
|
|
{
|
|
exeFs = nca.OpenFileSystem(NcaSectionType.Code, device.System.FsIntegrityCheckLevel);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (patchNca.CanOpenSection(NcaSectionType.Code))
|
|
{
|
|
exeFs = nca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, device.System.FsIntegrityCheckLevel);
|
|
}
|
|
}
|
|
|
|
return exeFs;
|
|
}
|
|
|
|
public static IStorage GetRomFs(this Nca nca, Switch device, Nca patchNca = null)
|
|
{
|
|
IStorage romFs = null;
|
|
|
|
if (patchNca == null)
|
|
{
|
|
if (nca.CanOpenSection(NcaSectionType.Data))
|
|
{
|
|
romFs = nca.OpenStorage(NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (patchNca.CanOpenSection(NcaSectionType.Data))
|
|
{
|
|
romFs = nca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
|
|
}
|
|
}
|
|
|
|
return romFs;
|
|
}
|
|
|
|
public static BlitStruct<ApplicationControlProperty> GetNacp(this Nca controlNca, Switch device)
|
|
{
|
|
var nacpData = new BlitStruct<ApplicationControlProperty>(1);
|
|
|
|
using var controlFile = new UniqueRef<IFile>();
|
|
|
|
Result result = controlNca.OpenFileSystem(NcaSectionType.Data, device.System.FsIntegrityCheckLevel)
|
|
.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read);
|
|
|
|
if (result.IsSuccess())
|
|
{
|
|
result = controlFile.Get.Read(out long bytesRead, 0, nacpData.ByteSpan, ReadOption.None);
|
|
}
|
|
else
|
|
{
|
|
nacpData.ByteSpan.Clear();
|
|
}
|
|
|
|
return nacpData;
|
|
}
|
|
|
|
public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType)
|
|
{
|
|
string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt";
|
|
using var cnmtFile = new UniqueRef<IFile>();
|
|
|
|
try
|
|
{
|
|
Result result = cnmtNca.OpenFileSystem(0, checkLevel)
|
|
.OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read);
|
|
|
|
if (result.IsSuccess())
|
|
{
|
|
return new Cnmt(cnmtFile.Release().AsStream());
|
|
}
|
|
}
|
|
catch (HorizonResultException ex)
|
|
{
|
|
if (!ResultFs.PathNotFound.Includes(ex.ResultValue))
|
|
{
|
|
Logger.Warning?.Print(LogClass.Application, $"Failed get cnmt for '{cnmtNca.Header.TitleId:x16}' from nca: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|