diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate index 786dfad1c..ede918aa3 100644 Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/src/MeloNX/MeloNX/Core/Headers/Ryujinx-Header.h b/src/MeloNX/MeloNX/Core/Headers/Ryujinx-Header.h index 57687632f..ea350fbc8 100644 --- a/src/MeloNX/MeloNX/Core/Headers/Ryujinx-Header.h +++ b/src/MeloNX/MeloNX/Core/Headers/Ryujinx-Header.h @@ -15,9 +15,19 @@ extern "C" { #endif +struct GameInfo { + long FileSize; + char TitleName[512]; + long TitleId; + char Developer[256]; + int Version; +}; +extern struct GameInfo get_game_info(int, char*); // Declare the main_ryujinx_sdl function, matching the signature int main_ryujinx_sdl(int argc, char **argv); +void initialize(); + const char* get_game_controllers(); #ifdef __cplusplus diff --git a/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift b/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift index c43c94a58..33e9a09fe 100644 --- a/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift +++ b/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift @@ -50,7 +50,6 @@ class Ryujinx { var listinputids: Bool var fullscreen: Bool var memoryManagerMode: String - var disableVSync: Bool var disableShaderCache: Bool var disableDockedMode: Bool var enableTextureRecompression: Bool @@ -64,7 +63,6 @@ class Ryujinx { listinputids: Bool = false, fullscreen: Bool = true, memoryManagerMode: String = "HostMapped", - disableVSync: Bool = false, disableShaderCache: Bool = false, disableDockedMode: Bool = false, nintendoinput: Bool = true, @@ -79,7 +77,6 @@ class Ryujinx { self.tracelogs = tracelogs self.listinputids = listinputids self.fullscreen = fullscreen - self.disableVSync = disableVSync self.disableShaderCache = disableShaderCache self.disableDockedMode = disableDockedMode self.enableTextureRecompression = enableTextureRecompression @@ -157,10 +154,6 @@ class Ryujinx { args.append("--correct-controller") } - // Adding default args directly into additionalArgs - if config.disableVSync { - // args.append("--disable-vsync") - } args.append("--disable-vsync") diff --git a/src/MeloNX/MeloNX/Views/ContentView.swift b/src/MeloNX/MeloNX/Views/ContentView.swift index 1bea44c7f..2516b7e5c 100644 --- a/src/MeloNX/MeloNX/Views/ContentView.swift +++ b/src/MeloNX/MeloNX/Views/ContentView.swift @@ -130,6 +130,7 @@ struct ContentView: View { SDL_SetMainReady() SDL_iPhoneSetEventPump(SDL_TRUE) SDL_Init(SdlInitFlags) + initialize() } private func setupEmulation() { diff --git a/src/MeloNX/MeloNX/Views/GamesList/GameListView.swift b/src/MeloNX/MeloNX/Views/GamesList/GameListView.swift index ec8891526..71f912bee 100644 --- a/src/MeloNX/MeloNX/Views/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/Views/GamesList/GameListView.swift @@ -7,17 +7,33 @@ // MARK: - This will most likely not be used in prod import SwiftUI +import UniformTypeIdentifiers + +public struct Game: Identifiable, Equatable { + public var id = UUID() + + var containerFolder: URL + var fileType: UTType + + var fileURL: URL + + var titleName: String + var titleId: String + var developer: String + var version: String + var icon: Image? +} struct GameListView: View { @Binding var startemu: URL? - @State private var games: [URL] = [] + @State private var games: [Game] = [] var body: some View { - List(games, id: \.self) { game in + List($games, id: \.id) { $game in Button { - startemu = game + startemu = $game.wrappedValue.fileURL } label: { - Text(game.lastPathComponent) + Text(game.titleName) } } .navigationTitle("Games") @@ -42,7 +58,41 @@ struct GameListView: View { // Load games only from "roms" folder do { let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil) - games = files + + files.forEach { fileURLCandidate in + do { + let handle = try FileHandle(forReadingFrom: fileURLCandidate) + let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String + let extensionPtr = UnsafeMutablePointer(mutating: fileExtension) + + var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr) + + var game = Game(containerFolder: romsDirectory, fileType: .item, fileURL: fileURLCandidate, titleName: "", titleId: "", developer: "", version: "") + + game.titleName = withUnsafePointer(to: &gameInfo.TitleName) { + $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) { + String(cString: $0) + } + } + + game.developer = withUnsafePointer(to: &gameInfo.Developer) { + $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) { + String(cString: $0) + } + } + + game.titleId = String(gameInfo.TitleId) + + + game.version = String(gameInfo.Version) + + + games.append(game) + } catch { + print(error) + } + } + } catch { print("Error loading games from roms folder: \(error)") } diff --git a/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift index 93012d024..bd22061cd 100644 --- a/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift @@ -27,7 +27,6 @@ struct SettingsView: View { VStack { Section(header: Title("Graphics and Performance")) { Toggle("Ryujinx Fullscreen", isOn: $config.fullscreen) - Toggle("Disable V-Sync", isOn: $config.disableVSync) Toggle("Disable Shader Cache", isOn: $config.disableShaderCache) Toggle("Enable Texture Recompression", isOn: $config.enableTextureRecompression) Toggle("Disable Docked Mode", isOn: $config.disableDockedMode) @@ -72,7 +71,6 @@ struct SettingsView: View { //TextField("Game Path", text: $config.gamepath) Text("PageSize \(String(Int(getpagesize())))") - Toggle("Ignore JIT Enabeld Popup", isOn: $ignoreJIT) TextField("Additional Arguments", text: Binding( get: { config.additionalArgs.joined(separator: ", ") diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 906094fea..79d96eedd 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -29,6 +29,9 @@ using Ryujinx.Input; using Ryujinx.Input.HLE; using Ryujinx.Input.SDL2; using Ryujinx.SDL2.Common; +using Ryujinx.Ui.Common.Configuration; +using Ryujinx.Ui.Common.Configuration.System; +using Ryujinx.Ui.Common; using Silk.NET.Vulkan; using System; using System.Collections.Generic; @@ -40,6 +43,56 @@ using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.Gamepad using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using Key = Ryujinx.Common.Configuration.Hid.Key; using System.Linq; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.HOS; +using Ryujinx.Input.HLE; +using Ryujinx.HLE; +using System; +using System.Runtime.InteropServices; +using Ryujinx.Common.Configuration; +using LibHac.Tools.FsSystem; +using Ryujinx.Graphics.GAL.Multithreading; +using Ryujinx.Audio.Backends.Dummy; +using Ryujinx.HLE.HOS.SystemState; +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 LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Fs; +using Path = System.IO.Path; +using LibHac; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.HLE.Loaders.Npdm; +using Ryujinx.Common.Utilities; +using System.Globalization; +using Ryujinx.Ui.Common.Configuration.System; +using Ryujinx.Common.Logging.Targets; +using System.Collections.Generic; +using LibHac.Bcat; +using Ryujinx.Ui.App.Common; +using System.Text; +using Ryujinx.HLE.Ui; +using ARMeilleure.Translation; +using LibHac.Ncm; +using LibHac.Tools.FsSystem.NcaUtils; +using Microsoft.Win32.SafeHandles; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.SystemState; +using Ryujinx.Input.HLE; +using Silk.NET.Vulkan; +using System; +using System.IO; +using System.Runtime.InteropServices; public class GamepadInfo { @@ -93,6 +146,42 @@ namespace Ryujinx.Headless.SDL2 return 0; } + [UnmanagedCallersOnly(EntryPoint = "initialize")] + public static unsafe void Initialize() + { + + AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)); + + if (_virtualFileSystem == null) + { + _virtualFileSystem = VirtualFileSystem.CreateInstance(); + } + + if (_libHacHorizonManager == null) + { + _libHacHorizonManager = new LibHacHorizonManager(); + _libHacHorizonManager.InitializeFsServer(_virtualFileSystem); + _libHacHorizonManager.InitializeArpServer(); + _libHacHorizonManager.InitializeBcatServer(); + _libHacHorizonManager.InitializeSystemClients(); + } + + if (_contentManager == null) + { + _contentManager = new ContentManager(_virtualFileSystem); + } + + if (_accountManager == null) + { + _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, ""); + } + + if (_userChannelPersistence == null) + { + _userChannelPersistence = new UserChannelPersistence(); + } + } + static void Main(string[] args) { // Make process DPI aware for proper window sizing on high-res screens. @@ -173,6 +262,465 @@ namespace Ryujinx.Headless.SDL2 return ptr; } + [UnmanagedCallersOnly(EntryPoint = "get_game_info")] + public static GameInfoNative GetGameInfoNative(int descriptor, IntPtr extensionPtr) + { + if (_virtualFileSystem == null) { + _virtualFileSystem = VirtualFileSystem.CreateInstance(); + } + var extension = Marshal.PtrToStringAnsi(extensionPtr); + var stream = OpenFile(descriptor); + + var gameInfo = GetGameInfo(stream, extension); + + return new GameInfoNative(0, gameInfo.TitleName, 0, gameInfo.Developer, 0); + } + + public static GameInfo? GetGameInfo(Stream gameStream, string extension) + { + + var gameInfo = new GameInfo + { + FileSize = gameStream.Length * 0.000000000931, + TitleName = "Unknown", + TitleId = "0000000000000000", + Developer = "Unknown", + Version = "0", + Icon = null + }; + + const Language TitleLanguage = Language.AmericanEnglish; + + BlitStruct controlHolder = new(1); + + try + { + try + { + if (extension == "nsp" || extension == "pfs0" || extension == "xci") + { + IFileSystem pfs; + + bool isExeFs = false; + + if (extension == "xci") + { + Xci xci = new(_virtualFileSystem.KeySet, gameStream.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(gameStream.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + + // 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(_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(); + + LibHac.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 + { + GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id); + + gameInfo.TitleId = id; + + if (controlFs == null) + { + Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}"); + return null; + } + + // 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; + } + } + + } + } + } + else if (extension == "nro") + { + BinaryReader reader = new(gameStream); + + byte[] Read(long position, int size) + { + gameStream.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + gameStream.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + gameInfo.Icon = Read(assetOffset + iconOffset, (int)iconSize); + } + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version); + } + } + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException exception) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. {exception}"); + } + 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(IFileSystem pfs, out IFileSystem? controlFs, out string? titleId) + { + (_, _, Nca? controlNca) = GetGameData(_virtualFileSystem, pfs, 0); + + if (controlNca == null) + { + Logger.Warning?.Print(LogClass.Application, "Control NCA is null. Unable to load control FS."); + } + + // Return the ControlFS + controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + titleId = controlNca?.Header.TitleId.ToString("x16"); + } + + (Nca? mainNca, Nca? patchNca, Nca? controlNca) GetGameData(VirtualFileSystem fileSystem, IFileSystem 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(); + + Logger.Info?.Print(LogClass.Application, $"Loading file from PFS: {fileEntry.FullPath}"); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(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)"; + + if (_virtualFileSystem == null) + { + Logger.Error?.Print(LogClass.Application, "SwitchDevice was not initialized."); + return false; + } + + try + { + (Nca? patchNca, Nca? controlNca) = GetGameUpdateData(_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 = ""; + + 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).Selected; + + if (File.Exists(updatePath)) + { + FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + + return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); + } + } + } + + return (null, null); + } + + (Nca? patchNca, Nca? controlNca) 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(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; + } + private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option) { if (inputId == null) @@ -207,7 +755,11 @@ namespace Ryujinx.Headless.SDL2 { Logger.Error?.Print(LogClass.Application, $"{index} gamepad not found (\"{inputId}\")"); - return null; + inputId = "0"; + + gamepad = _inputManager.KeyboardDriver.GetGamepad(inputId); + + isKeyboard = true; } } @@ -408,17 +960,34 @@ namespace Ryujinx.Headless.SDL2 { AppDataManager.Initialize(option.BaseDataDir); - _virtualFileSystem = VirtualFileSystem.CreateInstance(); - _libHacHorizonManager = new LibHacHorizonManager(); + if (_virtualFileSystem == null) + { + _virtualFileSystem = VirtualFileSystem.CreateInstance(); + } - _libHacHorizonManager.InitializeFsServer(_virtualFileSystem); - _libHacHorizonManager.InitializeArpServer(); - _libHacHorizonManager.InitializeBcatServer(); - _libHacHorizonManager.InitializeSystemClients(); + if (_libHacHorizonManager == null) + { + _libHacHorizonManager = new LibHacHorizonManager(); + _libHacHorizonManager.InitializeFsServer(_virtualFileSystem); + _libHacHorizonManager.InitializeArpServer(); + _libHacHorizonManager.InitializeBcatServer(); + _libHacHorizonManager.InitializeSystemClients(); + } - _contentManager = new ContentManager(_virtualFileSystem); - _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile); - _userChannelPersistence = new UserChannelPersistence(); + if (_contentManager == null) + { + _contentManager = new ContentManager(_virtualFileSystem); + } + + if (_accountManager == null) + { + _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile); + } + + if (_userChannelPersistence == null) + { + _userChannelPersistence = new UserChannelPersistence(); + } if (_inputManager == null) { @@ -800,5 +1369,51 @@ namespace Ryujinx.Headless.SDL2 return true; } + + private static FileStream OpenFile(int descriptor) + { + var safeHandle = new SafeFileHandle(descriptor, false); + + return new FileStream(safeHandle, FileAccess.ReadWrite); + } + + public class GameInfo + { + public double FileSize; + public string? TitleName; + public string? TitleId; + public string? Developer; + public string? Version; + public byte[]? Icon; + } + + public unsafe struct GameInfoNative + { + public ulong FileSize; + public fixed byte TitleName[512]; + public ulong TitleId; + public fixed byte Developer[256]; + public uint Version; + + public GameInfoNative(ulong fileSize, string titleName, ulong titleId, string developer, uint version) + { + FileSize = fileSize; + TitleId = titleId; + Version = version; + + fixed (byte* developerPtr = Developer) + fixed (byte* titleNamePtr = TitleName) + { + CopyStringToFixedArray(titleName, titleNamePtr, 512); + CopyStringToFixedArray(developer, developerPtr, 256); + } + } + + private void CopyStringToFixedArray(string source, byte* destination, int length) + { + var span = new Span(destination, length); + Encoding.UTF8.GetBytes(source, span); + } + } } } diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index bb43ced2e..e6cfbda96 100644 --- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -97,6 +97,8 @@ + +