forked from MeloNX/MeloNX
add interface fore loading games from storage
This commit is contained in:
parent
145cd0b2b1
commit
56a6ac2a65
@ -6,7 +6,7 @@ namespace LibRyujinx.Jni.References
|
||||
public readonly struct JClassLocalRef : IEquatable<JClassLocalRef>
|
||||
{
|
||||
#pragma warning disable 0649
|
||||
private readonly JObjectLocalRef _value;
|
||||
public readonly JObjectLocalRef _value;
|
||||
#pragma warning restore 0649
|
||||
|
||||
#region Public Methods
|
||||
|
@ -6,7 +6,7 @@ namespace LibRyujinx.Jni.References
|
||||
public readonly struct JStringLocalRef : IEquatable<JStringLocalRef>
|
||||
{
|
||||
#pragma warning disable 0649
|
||||
private readonly JObjectLocalRef _value;
|
||||
public readonly JObjectLocalRef _value;
|
||||
#pragma warning restore 0649
|
||||
|
||||
#region Public Methods
|
||||
|
@ -16,6 +16,10 @@ using Silk.NET.Vulkan;
|
||||
using Silk.NET.Vulkan.Extensions.KHR;
|
||||
using LibRyujinx.Shared.Audio.Oboe;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace LibRyujinx
|
||||
{
|
||||
@ -67,6 +71,12 @@ namespace LibRyujinx
|
||||
return s;
|
||||
}
|
||||
|
||||
private static JStringLocalRef CreateString(string str, JEnvRef jEnv)
|
||||
{
|
||||
return str.AsSpan().WithSafeFixed(jEnv, CreateString);
|
||||
}
|
||||
|
||||
|
||||
private static JStringLocalRef CreateString(in IReadOnlyFixedContext<Char> ctx, JEnvRef jEnv)
|
||||
{
|
||||
JEnvValue value = jEnv.Environment;
|
||||
@ -97,6 +107,19 @@ namespace LibRyujinx
|
||||
return LoadApplication(path);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
|
||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
||||
{
|
||||
if (SwitchDevice?.EmulationContext == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stream = OpenFile(descriptor);
|
||||
|
||||
return LoadApplication(stream, isXci);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
|
||||
public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject)
|
||||
{
|
||||
@ -163,7 +186,6 @@ namespace LibRyujinx
|
||||
IntPtr getArrayLengthPtr = jInterface.GetArrayLengthPointer;
|
||||
IntPtr getObjectArrayElementPtr = jInterface.GetObjectArrayElementPointer;
|
||||
IntPtr getObjectFieldPtr = jInterface.GetObjectFieldPointer;
|
||||
IntPtr getJvmPtr = jInterface.GetJavaVMPointer;
|
||||
|
||||
var getObjectClass = getObjectClassPtr.GetUnsafeDelegate<GetObjectClassDelegate>();
|
||||
var getFieldId = getFieldIdPtr.GetUnsafeDelegate<GetFieldIdDelegate>();
|
||||
@ -171,7 +193,6 @@ namespace LibRyujinx
|
||||
var getObjectArrayElement = getObjectArrayElementPtr.GetUnsafeDelegate<GetObjectArrayElementDelegate>();
|
||||
var getLongField = getLongFieldPtr.GetUnsafeDelegate<GetLongFieldDelegate>();
|
||||
var getObjectField = getObjectFieldPtr.GetUnsafeDelegate<GetObjectFieldDelegate>();
|
||||
var getJvm = getJvmPtr.GetUnsafeDelegate<GetJavaVMDelegate>();
|
||||
|
||||
List<string> extensions = new List<string>();
|
||||
|
||||
@ -226,6 +247,63 @@ namespace LibRyujinx
|
||||
RunLoop();
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
|
||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JBoolean isXci)
|
||||
{
|
||||
using var stream = OpenFile(fileDescriptor);
|
||||
|
||||
var info = GetGameInfo(stream, isXci) ?? new GameInfo();
|
||||
|
||||
var javaClassName = GetCCharSequence("org/ryujinx/android/viewmodels/GameInfo");
|
||||
|
||||
JEnvValue value = jEnv.Environment;
|
||||
ref JNativeInterface jInterface = ref value.Functions;
|
||||
IntPtr findClassPtr = jInterface.FindClassPointer;
|
||||
IntPtr newGlobalRefPtr = jInterface.NewGlobalRefPointer;
|
||||
IntPtr getFieldIdPtr = jInterface.GetFieldIdPointer;
|
||||
IntPtr getMethodPtr = jInterface.GetMethodIdPointer;
|
||||
IntPtr newObjectPtr = jInterface.NewObjectPointer;
|
||||
IntPtr setObjectFieldPtr = jInterface.SetObjectFieldPointer;
|
||||
IntPtr setDoubleFieldPtr = jInterface.SetDoubleFieldPointer;
|
||||
|
||||
|
||||
var findClass = findClassPtr.GetUnsafeDelegate<FindClassDelegate>();
|
||||
var newGlobalRef = newGlobalRefPtr.GetUnsafeDelegate <NewGlobalRefDelegate>();
|
||||
var getFieldId = getFieldIdPtr.GetUnsafeDelegate<GetFieldIdDelegate>();
|
||||
var getMethod = getMethodPtr.GetUnsafeDelegate<GetMethodIdDelegate>();
|
||||
var newObject = newObjectPtr.GetUnsafeDelegate<NewObjectDelegate>();
|
||||
var setObjectField = setObjectFieldPtr.GetUnsafeDelegate<SetObjectFieldDelegate>();
|
||||
var setDoubleField = setDoubleFieldPtr.GetUnsafeDelegate<SetDoubleFieldDelegate>();
|
||||
|
||||
var javaClass = findClass(jEnv, javaClassName);
|
||||
var newGlobal = newGlobalRef(jEnv, javaClass._value);
|
||||
var constructor = getMethod(jEnv, javaClass, GetCCharSequence("<init>"), GetCCharSequence("()V"));
|
||||
var newObj = newObject(jEnv, javaClass, constructor, 0);
|
||||
|
||||
using var sha = SHA256.Create();
|
||||
|
||||
var iconCacheByte = sha.ComputeHash(info.Icon ?? new byte[0]);
|
||||
var iconCache = BitConverter.ToString(iconCacheByte).Replace("-", "");
|
||||
|
||||
var cacheDirectory = Path.Combine(AppDataManager.BaseDirPath, "iconCache");
|
||||
Directory.CreateDirectory(cacheDirectory);
|
||||
|
||||
var cachePath = Path.Combine(cacheDirectory, iconCache);
|
||||
if (!File.Exists(cachePath))
|
||||
{
|
||||
File.WriteAllBytes(cachePath, info.Icon ?? new byte[0]);
|
||||
}
|
||||
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("TitleName"), GetCCharSequence("Ljava/lang/String;")), CreateString(info.TitleName, jEnv)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("TitleId"), GetCCharSequence("Ljava/lang/String;")), CreateString(info.TitleId, jEnv)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Developer"), GetCCharSequence("Ljava/lang/String;")), CreateString(info.Developer, jEnv)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Version"), GetCCharSequence("Ljava/lang/String;")), CreateString(info.Version, jEnv)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("IconCache"), GetCCharSequence("Ljava/lang/String;")), CreateString(iconCache, jEnv)._value);
|
||||
setDoubleField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("FileSize"), GetCCharSequence("D")), info.FileSize);
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetVsync")]
|
||||
public static void JniSetVsyncStateNative(JEnvRef jEnv, JObjectLocalRef jObj, JBoolean enabled)
|
||||
{
|
||||
@ -293,6 +371,13 @@ namespace LibRyujinx
|
||||
|
||||
return (id ?? "").AsSpan().WithSafeFixed(jEnv, CreateString);
|
||||
}
|
||||
|
||||
private static Stream OpenFile(int descriptor)
|
||||
{
|
||||
var safeHandle = new SafeFileHandle(descriptor, false);
|
||||
|
||||
return new FileStream(safeHandle, FileAccess.Read);
|
||||
}
|
||||
}
|
||||
|
||||
internal static partial class Logcat
|
||||
|
@ -41,6 +41,12 @@ namespace LibRyujinx
|
||||
return LoadApplication(path);
|
||||
}
|
||||
|
||||
public static bool LoadApplication(Stream stream, bool isXci)
|
||||
{
|
||||
var emulationContext = SwitchDevice.EmulationContext;
|
||||
return (isXci ? emulationContext?.LoadXci(stream) : emulationContext.LoadNsp(stream)) ?? false;
|
||||
}
|
||||
|
||||
public static bool LoadApplication(string path)
|
||||
{
|
||||
var emulationContext = SwitchDevice.EmulationContext;
|
||||
|
@ -15,12 +15,32 @@ using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Audio.Integration;
|
||||
using Ryujinx.Audio.Backends.SDL2;
|
||||
using System.IO;
|
||||
using LibHac.Common.Keys;
|
||||
using LibHac.Common;
|
||||
using LibHac.Ns;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Fs;
|
||||
using Path = System.IO.Path;
|
||||
using LibHac;
|
||||
using Ryujinx.HLE.Loaders.Npdm;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Globalization;
|
||||
using Ryujinx.Ui.Common.Configuration.System;
|
||||
|
||||
namespace LibRyujinx
|
||||
{
|
||||
public static partial class LibRyujinx
|
||||
{
|
||||
internal static IHardwareDeviceDriver AudioDriver { get; set; } = new DummyHardwareDeviceDriver();
|
||||
|
||||
private static readonly TitleUpdateMetadataJsonSerializerContext TitleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
public static SwitchDevice? SwitchDevice { get; set; }
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "initialize")]
|
||||
@ -64,7 +84,6 @@ namespace LibRyujinx
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -72,6 +91,388 @@ namespace LibRyujinx
|
||||
{
|
||||
AudioDriver = new SDL2HardwareDeviceDriver();
|
||||
}
|
||||
|
||||
public static GameInfo GetGameInfo(Stream gameStream, bool isXci)
|
||||
{
|
||||
var gameInfo = new GameInfo();
|
||||
gameInfo.FileSize = gameStream.Length * 0.000000000931;
|
||||
gameInfo.TitleName = "Unknown";
|
||||
gameInfo.TitleId = "0000000000000000";
|
||||
gameInfo.Developer = "Unknown";
|
||||
gameInfo.Version = "0";
|
||||
gameInfo.Icon = null;
|
||||
|
||||
Language titleLanguage = Language.AmericanEnglish;
|
||||
|
||||
BlitStruct<ApplicationControlProperty> controlHolder = new(1);
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
PartitionFileSystem pfs;
|
||||
|
||||
bool isExeFs = false;
|
||||
|
||||
if (isXci)
|
||||
{
|
||||
Xci xci = new(SwitchDevice.VirtualFileSystem.KeySet, gameStream.AsStorage());
|
||||
|
||||
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
||||
}
|
||||
else
|
||||
{
|
||||
pfs = new PartitionFileSystem(gameStream.AsStorage());
|
||||
|
||||
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
|
||||
bool hasMainNca = false;
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
|
||||
{
|
||||
if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
|
||||
{
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = new(SwitchDevice.VirtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||
|
||||
// Some main NCAs don't have a data partition, so check if the partition exists before opening it
|
||||
if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
|
||||
{
|
||||
hasMainNca = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
|
||||
{
|
||||
isExeFs = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMainNca && !isExeFs)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (isExeFs)
|
||||
{
|
||||
using UniqueRef<IFile> npdmFile = new();
|
||||
|
||||
Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
|
||||
|
||||
if (ResultFs.PathNotFound.Includes(result))
|
||||
{
|
||||
Npdm npdm = new(npdmFile.Get.AsStream());
|
||||
|
||||
gameInfo.TitleName = npdm.TitleName;
|
||||
gameInfo.TitleId = npdm.Aci0.TitleId.ToString("x16");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var id = gameInfo.TitleId;
|
||||
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out id);
|
||||
|
||||
gameInfo.TitleId = id;
|
||||
|
||||
// Check if there is an update available.
|
||||
if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem updatedControlFs))
|
||||
{
|
||||
// Replace the original ControlFs by the updated one.
|
||||
controlFs = updatedControlFs;
|
||||
}
|
||||
|
||||
ReadControlData(controlFs, controlHolder.ByteSpan);
|
||||
|
||||
GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version);
|
||||
|
||||
// Read the icon from the ControlFS and store it as a byte array
|
||||
try
|
||||
{
|
||||
using UniqueRef<IFile> icon = new();
|
||||
|
||||
controlFs.OpenFile(ref icon.Ref, $"/icon_{titleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
using MemoryStream stream = new();
|
||||
|
||||
icon.Get.AsStream().CopyTo(stream);
|
||||
gameInfo.Icon = stream.ToArray();
|
||||
}
|
||||
catch (HorizonResultException)
|
||||
{
|
||||
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
|
||||
{
|
||||
if (entry.Name == "control.nacp")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var icon = new UniqueRef<IFile>();
|
||||
|
||||
controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
using MemoryStream stream = new();
|
||||
|
||||
icon.Get.AsStream().CopyTo(stream);
|
||||
gameInfo.Icon = stream.ToArray();
|
||||
|
||||
if (gameInfo.Icon != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (MissingKeyException exception)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
|
||||
Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. ");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"The gameStream encountered was not of a valid type. Error: {exception}");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, exception.Message);
|
||||
}
|
||||
|
||||
void ReadControlData(IFileSystem controlFs, Span<byte> outProperty)
|
||||
{
|
||||
using UniqueRef<IFile> controlFile = new();
|
||||
|
||||
controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
|
||||
}
|
||||
|
||||
void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
|
||||
{
|
||||
_ = Enum.TryParse(titleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
|
||||
|
||||
if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage)
|
||||
{
|
||||
titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString();
|
||||
publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
titleName = null;
|
||||
publisher = null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(titleName))
|
||||
{
|
||||
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
|
||||
{
|
||||
if (!controlTitle.NameString.IsEmpty())
|
||||
{
|
||||
titleName = controlTitle.NameString.ToString();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(publisher))
|
||||
{
|
||||
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
|
||||
{
|
||||
if (!controlTitle.PublisherString.IsEmpty())
|
||||
{
|
||||
publisher = controlTitle.PublisherString.ToString();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (controlData.PresenceGroupId != 0)
|
||||
{
|
||||
titleId = controlData.PresenceGroupId.ToString("x16");
|
||||
}
|
||||
else if (controlData.SaveDataOwnerId != 0)
|
||||
{
|
||||
titleId = controlData.SaveDataOwnerId.ToString();
|
||||
}
|
||||
else if (controlData.AddOnContentBaseId != 0)
|
||||
{
|
||||
titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
|
||||
}
|
||||
else
|
||||
{
|
||||
titleId = "0000000000000000";
|
||||
}
|
||||
|
||||
version = controlData.DisplayVersionString.ToString();
|
||||
}
|
||||
|
||||
void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId)
|
||||
{
|
||||
(_, _, Nca controlNca) = GetGameData(SwitchDevice.VirtualFileSystem, pfs, 0);
|
||||
|
||||
// Return the ControlFS
|
||||
controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
|
||||
titleId = controlNca?.Header.TitleId.ToString("x16");
|
||||
}
|
||||
|
||||
(Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
|
||||
{
|
||||
Nca mainNca = null;
|
||||
Nca patchNca = null;
|
||||
Nca controlNca = null;
|
||||
|
||||
fileSystem.ImportTickets(pfs);
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().AsStorage());
|
||||
|
||||
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
|
||||
|
||||
if (ncaProgramIndex != programIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.Program)
|
||||
{
|
||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||
|
||||
if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
|
||||
{
|
||||
patchNca = nca;
|
||||
}
|
||||
else
|
||||
{
|
||||
mainNca = nca;
|
||||
}
|
||||
}
|
||||
else if (nca.Header.ContentType == NcaContentType.Control)
|
||||
{
|
||||
controlNca = nca;
|
||||
}
|
||||
}
|
||||
|
||||
return (mainNca, patchNca, controlNca);
|
||||
}
|
||||
|
||||
bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs)
|
||||
{
|
||||
updatedControlFs = null;
|
||||
|
||||
string updatePath = "(unknown)";
|
||||
|
||||
try
|
||||
{
|
||||
(Nca patchNca, Nca controlNca) = GetGameUpdateData(SwitchDevice.VirtualFileSystem, titleId, 0, out updatePath);
|
||||
|
||||
if (patchNca != null && controlNca != null)
|
||||
{
|
||||
updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
|
||||
|
||||
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}");
|
||||
}
|
||||
catch (MissingKeyException exception)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
(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 &= ~0xFUL;
|
||||
|
||||
// Load update information if exists.
|
||||
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
|
||||
|
||||
if (File.Exists(titleUpdateMetadataPath))
|
||||
{
|
||||
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, TitleSerializerContext.TitleUpdateMetadata).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);
|
||||
}
|
||||
|
||||
(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"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().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);
|
||||
}
|
||||
|
||||
return gameInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public class SwitchDevice : IDisposable
|
||||
@ -160,4 +561,14 @@ namespace LibRyujinx
|
||||
EmulationContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class GameInfo
|
||||
{
|
||||
public double FileSize;
|
||||
public string TitleName;
|
||||
public string TitleId;
|
||||
public string Developer;
|
||||
public string Version;
|
||||
public byte[] Icon;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user