From 56a6ac2a65116b8a1201806dbac9b79d3317f70a Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 2 Jul 2023 16:48:53 +0000 Subject: [PATCH] add interface fore loading games from storage --- .../Jni/References/JClassLocalRef.cs | 2 +- .../Jni/References/JStringLocalRef.cs | 2 +- src/LibRyujinx/JniExportedMethods.cs | 89 +++- src/LibRyujinx/LibRyujinx.Device.cs | 6 + src/LibRyujinx/LibRyujinx.cs | 413 +++++++++++++++++- 5 files changed, 507 insertions(+), 5 deletions(-) diff --git a/src/LibRyujinx/Jni/References/JClassLocalRef.cs b/src/LibRyujinx/Jni/References/JClassLocalRef.cs index d1bb55b81..5880f5ddb 100644 --- a/src/LibRyujinx/Jni/References/JClassLocalRef.cs +++ b/src/LibRyujinx/Jni/References/JClassLocalRef.cs @@ -6,7 +6,7 @@ namespace LibRyujinx.Jni.References public readonly struct JClassLocalRef : IEquatable { #pragma warning disable 0649 - private readonly JObjectLocalRef _value; + public readonly JObjectLocalRef _value; #pragma warning restore 0649 #region Public Methods diff --git a/src/LibRyujinx/Jni/References/JStringLocalRef.cs b/src/LibRyujinx/Jni/References/JStringLocalRef.cs index fb79ecf7a..002fd9942 100644 --- a/src/LibRyujinx/Jni/References/JStringLocalRef.cs +++ b/src/LibRyujinx/Jni/References/JStringLocalRef.cs @@ -6,7 +6,7 @@ namespace LibRyujinx.Jni.References public readonly struct JStringLocalRef : IEquatable { #pragma warning disable 0649 - private readonly JObjectLocalRef _value; + public readonly JObjectLocalRef _value; #pragma warning restore 0649 #region Public Methods diff --git a/src/LibRyujinx/JniExportedMethods.cs b/src/LibRyujinx/JniExportedMethods.cs index 9571a2121..c27e488f1 100644 --- a/src/LibRyujinx/JniExportedMethods.cs +++ b/src/LibRyujinx/JniExportedMethods.cs @@ -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 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(); var getFieldId = getFieldIdPtr.GetUnsafeDelegate(); @@ -171,7 +193,6 @@ namespace LibRyujinx var getObjectArrayElement = getObjectArrayElementPtr.GetUnsafeDelegate(); var getLongField = getLongFieldPtr.GetUnsafeDelegate(); var getObjectField = getObjectFieldPtr.GetUnsafeDelegate(); - var getJvm = getJvmPtr.GetUnsafeDelegate(); List extensions = new List(); @@ -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(); + var newGlobalRef = newGlobalRefPtr.GetUnsafeDelegate (); + var getFieldId = getFieldIdPtr.GetUnsafeDelegate(); + var getMethod = getMethodPtr.GetUnsafeDelegate(); + var newObject = newObjectPtr.GetUnsafeDelegate(); + var setObjectField = setObjectFieldPtr.GetUnsafeDelegate(); + var setDoubleField = setDoubleFieldPtr.GetUnsafeDelegate(); + + var javaClass = findClass(jEnv, javaClassName); + var newGlobal = newGlobalRef(jEnv, javaClass._value); + var constructor = getMethod(jEnv, javaClass, GetCCharSequence(""), 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 diff --git a/src/LibRyujinx/LibRyujinx.Device.cs b/src/LibRyujinx/LibRyujinx.Device.cs index 4afbde1e0..88885c312 100644 --- a/src/LibRyujinx/LibRyujinx.Device.cs +++ b/src/LibRyujinx/LibRyujinx.Device.cs @@ -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; diff --git a/src/LibRyujinx/LibRyujinx.cs b/src/LibRyujinx/LibRyujinx.cs index 1a0c925aa..48093a6ed 100644 --- a/src/LibRyujinx/LibRyujinx.cs +++ b/src/LibRyujinx/LibRyujinx.cs @@ -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 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 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 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 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(); + + 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 outProperty) + { + using UniqueRef 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(); + + 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(); + + 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; + } } \ No newline at end of file