1
0
forked from MeloNX/MeloNX

Compare commits

...

5 Commits

8 changed files with 583 additions and 50 deletions

View File

@ -31,8 +31,21 @@ struct GameInfo {
unsigned int ImageSize;
};
struct DlcNcaListItem {
char Path[256];
unsigned long TitleId;
};
struct DlcNcaList {
bool success;
unsigned int size;
struct DlcNcaListItem* items;
};
extern struct GameInfo get_game_info(int, char*);
extern struct DlcNcaList get_dlc_nca_list(const char* titleIdPtr, const char* pathPtr);
void install_firmware(const char* inputPtr);
char* installed_firmware_version();
@ -43,8 +56,6 @@ int main_ryujinx_sdl(int argc, char **argv);
int get_current_fps();
void set_title_update(const char* titleIdPtr, const char* updatePathPtr);
void initialize();
#ifdef __cplusplus

View File

@ -0,0 +1,237 @@
//
// NativeController.swift
// MeloNX
//
// Created by XITRIX on 15/02/2025.
//
import CoreHaptics
import GameController
class NativeController: Hashable {
private var instanceID: SDL_JoystickID = -1
private var controller: OpaquePointer?
private var nativeController: GCController
private let controllerHaptics: CHHapticEngine?
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
init(_ controller: GCController) {
nativeController = controller
controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
try? controllerHaptics?.start()
setupHandheldController()
}
deinit {
cleanup()
}
private func setupHandheldController() {
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
}
var joystickDesc = SDL_VirtualJoystickDesc(
version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION),
type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue),
naxes: 6,
nbuttons: 15,
nhats: 1,
vendor_id: 0,
product_id: 0,
padding: 0,
button_mask: 0,
axis_mask: 0,
name: (controllername as NSString).utf8String,
userdata: Unmanaged.passUnretained(self).toOpaque(),
Update: { userdata in
// Update joystick state here
},
SetPlayerIndex: { userdata, playerIndex in
print("Player index set to \(playerIndex)")
},
Rumble: { userdata, lowFreq, highFreq in
print("Rumble with \(lowFreq), \(highFreq)")
guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
return 0
},
RumbleTriggers: { userdata, leftRumble, rightRumble in
print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0
},
SetLED: { userdata, red, green, blue in
print("Set LED to RGB(\(red), \(green), \(blue))")
return 0
},
SendEffect: { userdata, data, size in
print("Effect sent with size \(size)")
return 0
}
)
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
if instanceID < 0 {
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return
}
// Open a game controller for the virtual joystick
let joystick = SDL_JoystickFromInstanceID(instanceID)
controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil {
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return
}
if #available(iOS 16, *) {
guard let gamepad = nativeController.extendedGamepad
else { return }
setupButtonChangeListener(gamepad.buttonA, for: .B)
setupButtonChangeListener(gamepad.buttonB, for: .A)
setupButtonChangeListener(gamepad.buttonX, for: .Y)
setupButtonChangeListener(gamepad.buttonY, for: .X)
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
setupButtonChangeListener(gamepad.dpad.left, for: .dPadLeft)
setupButtonChangeListener(gamepad.dpad.right, for: .dPadRight)
setupButtonChangeListener(gamepad.leftShoulder, for: .leftShoulder)
setupButtonChangeListener(gamepad.rightShoulder, for: .rightShoulder)
gamepad.leftThumbstickButton.map { setupButtonChangeListener($0, for: .leftStick) }
gamepad.rightThumbstickButton.map { setupButtonChangeListener($0, for: .rightStick) }
setupButtonChangeListener(gamepad.buttonMenu, for: .start)
gamepad.buttonOptions.map { setupButtonChangeListener($0, for: .back) }
setupStickChangeListener(gamepad.leftThumbstick, for: .left)
setupStickChangeListener(gamepad.rightThumbstick, for: .right)
setupTriggerChangeListener(gamepad.leftTrigger, for: .left)
setupTriggerChangeListener(gamepad.rightTrigger, for: .right)
}
}
func setupButtonChangeListener(_ button: GCControllerButtonInput, for key: VirtualControllerButton) {
button.valueChangedHandler = { [unowned self] _, _, pressed in
setButtonState(pressed ? 1 : 0, for: key)
}
}
func setupStickChangeListener(_ button: GCControllerDirectionPad, for key: ThumbstickType) {
button.valueChangedHandler = { [unowned self] _, xValue, yValue in
let scaledX = Sint16(xValue * 32767.0)
let scaledY = -Sint16(yValue * 32767.0)
switch key {
case .left:
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTX.rawValue))
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTY.rawValue))
case .right:
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTY.rawValue))
}
}
}
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
button.valueChangedHandler = { [unowned self] _, value, pressed in
// print("Value: \(value), Is pressed: \(pressed)")
let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let scaledValue = Sint16(value * 32767.0)
updateAxisValue(value: scaledValue, forAxis: axis)
}
}
static func rumble(lowFreq: Float, highFreq: Float) {
do {
// Low-frequency haptic pattern
let lowFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
], relativeTime: 0, duration: 0.2)
], parameters: [])
// High-frequency haptic pattern
let highFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
], relativeTime: 0.2, duration: 0.2)
], parameters: [])
// Create and start the haptic engine
let engine = try CHHapticEngine()
try engine.start()
// Create and play the low-frequency player
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
try lowFreqPlayer.start(atTime: 0)
// Create and play the high-frequency player after a short delay
let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
try highFreqPlayer.start(atTime: 0.2)
} catch {
print("Error creating haptic patterns: \(error)")
}
}
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID)
SDL_JoystickSetVirtualAxis(joystick, axis.rawValue, value)
}
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
let scaleFactor = 32767.0 / 160
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
if stick == .right {
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTY.rawValue))
} else { // ThumbstickType.left
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTX.rawValue))
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTY.rawValue))
}
}
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return }
// print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0
updateAxisValue(value: Sint16(value), forAxis: axis)
} else {
let joystick = SDL_JoystickFromInstanceID(instanceID)
SDL_JoystickSetVirtualButton(joystick, Int32(button.rawValue), state)
}
}
func cleanup() {
if let controller {
SDL_JoystickDetachVirtual(instanceID)
SDL_GameControllerClose(controller)
self.controller = nil
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(nativeController)
}
static func == (lhs: NativeController, rhs: NativeController) -> Bool {
lhs.nativeController == rhs.nativeController
}
}

View File

@ -78,7 +78,7 @@ class VirtualController {
}
}
static func rumble(lowFreq: Float, highFreq: Float) {
static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
do {
// Low-frequency haptic pattern
let lowFreqPattern = try CHHapticPattern(events: [
@ -96,9 +96,23 @@ class VirtualController {
], relativeTime: 0.2, duration: 0.2)
], parameters: [])
// Create and start the haptic engine
let engine = try CHHapticEngine()
try engine.start()
// Mutable engine
var engine = engine
// If no engine passed, use device engine
if engine == nil {
// Create and start the haptic engine
if hapticEngine == nil {
hapticEngine = try CHHapticEngine()
try hapticEngine?.start()
}
engine = hapticEngine
}
guard let engine else {
return print("Error creating haptic patterns: hapticEngine is nil")
}
// Create and play the low-frequency player
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
@ -113,6 +127,8 @@ class VirtualController {
}
}
private static var hapticEngine: CHHapticEngine?
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }

View File

@ -366,16 +366,27 @@ class Ryujinx {
}
}
func setTitleUpdate(titleId: String, updatePath: String) {
guard let titleIdPtr = titleId.cString(using: .utf8),
let updatePathPtr = updatePath.cString(using: .utf8)
func getDlcNcaList(titleId: String, path: String) -> [DownloadableContentNca] {
guard let titleIdCString = titleId.cString(using: .utf8),
let pathCString = path.cString(using: .utf8)
else {
print("Invalid firmware path")
return
print("Invalid path")
return []
}
set_title_update(titleIdPtr, updatePathPtr)
let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
print("DLC parcing success: \(listPointer.success)")
guard listPointer.success else { return [] }
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
return list.map { item in
.init(fullPath: withUnsafePointer(to: item.Path) {
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
String(cString: $0)
}
}, titleId: item.TitleId, enabled: true)
}
}
private func generateGamepadId(joystickIndex: Int32) -> String? {

View File

@ -26,6 +26,7 @@ struct ContentView: View {
@State private var controllersList: [Controller] = []
@State private var currentControllers: [Controller] = []
@State var onscreencontroller: Controller = Controller(id: "", name: "")
@State var nativeControllers: [GCController: NativeController] = [:]
@State private var isVirtualControllerActive: Bool = false
@AppStorage("isVirtualController") var isVCA: Bool = true
@ -152,6 +153,7 @@ struct ContentView: View {
queue: .main) { notification in
if let controller = notification.object as? GCController {
print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList()
}
}
@ -163,6 +165,8 @@ struct ContentView: View {
queue: .main) { notification in
if let controller = notification.object as? GCController {
print("Controller disconnected: \(controller.productCategory)")
nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil
refreshControllersList()
}
}
@ -306,7 +310,8 @@ struct ContentView: View {
self.onscreencontroller = onscreen
}
controllersList.removeAll(where: { $0.id == "0"})
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
currentControllers = []
@ -397,3 +402,10 @@ func loadSettings() -> Ryujinx.Configuration? {
}
}
extension Array {
@inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
for index in self.indices {
try body(&self[index])
}
}
}

View File

@ -28,6 +28,7 @@ struct GameLibraryView: View {
@State var isSelectingGameFile = false
@State var isViewingGameInfo: Bool = false
@State var isSelectingGameUpdate: Bool = false
@State var isSelectingGameDLC: Bool = false
@State var gameInfo: Game?
var games: Binding<[Game]> {
Binding(
@ -92,7 +93,7 @@ struct GameLibraryView: View {
LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
.onTapGesture {
addToRecentGames(game)
}
@ -101,7 +102,7 @@ struct GameLibraryView: View {
}
} else {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
.onTapGesture {
addToRecentGames(game)
}
@ -271,6 +272,9 @@ struct GameLibraryView: View {
.sheet(isPresented: $isSelectingGameUpdate) {
UpdateManagerSheet(game: $gameInfo)
}
.sheet(isPresented: $isSelectingGameDLC) {
DLCManagerSheet(game: $gameInfo)
}
.sheet(isPresented: Binding(
get: { isViewingGameInfo && gameInfo != nil },
set: { newValue in
@ -414,6 +418,7 @@ struct GameListRow: View {
@Binding var games: [Game] // Add this binding
@Binding var isViewingGameInfo: Bool
@Binding var isSelectingGameUpdate: Bool
@Binding var isSelectingGameDLC: Bool
@Binding var gameInfo: Game?
@State var gametoDelete: Game?
@State var showGameDeleteConfirmation: Bool = false
@ -476,13 +481,22 @@ struct GameListRow: View {
} label: {
Label("Game Info", systemImage: "info.circle")
}
}
Section {
Button {
gameInfo = game
isSelectingGameUpdate.toggle()
} label: {
Label("Game Update Manager", systemImage: "chevron.up.circle")
}
Button {
gameInfo = game
isSelectingGameDLC.toggle()
} label: {
Label("Game DLC Manager", systemImage: "plus.viewfinder")
}
}
Section {

View File

@ -0,0 +1,159 @@
//
// GameDLCManagerSheet.swift
// MeloNX
//
// Created by XITRIX on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
struct DownloadableContentNca: Codable, Hashable {
var fullPath: String
var titleId: UInt
var enabled: Bool
enum CodingKeys: String, CodingKey {
case fullPath = "path"
case titleId = "title_id"
case enabled = "is_enabled"
}
}
struct DownloadableContentContainer: Codable, Hashable {
var containerPath: String
var downloadableContentNcaList: [DownloadableContentNca]
enum CodingKeys: String, CodingKey {
case containerPath = "path"
case downloadableContentNcaList = "dlc_nca_list"
}
}
struct DLCManagerSheet: View {
@Binding var game: Game!
@State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = []
var body: some View {
NavigationView {
let withIndex = dlcs.enumerated().map { $0 }
List(withIndex, id: \.element.containerPath) { index, dlc in
Button(action: {
let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true
dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle }
Self.saveDlcs(game, dlc: dlcs)
}) {
HStack {
Text(dlc.containerPath)
.foregroundStyle(Color(uiColor: .label))
Spacer()
if dlc.downloadableContentNcaList.first?.enabled == true {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
.font(.system(size: 24))
} else {
Image(systemName: "circle")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.font(.system(size: 24))
}
}
}
.contextMenu {
Button {
let path = URL.documentsDirectory.appendingPathComponent(dlc.containerPath)
try? FileManager.default.removeItem(atPath: path.path)
dlcs.remove(at: index)
Self.saveDlcs(game, dlc: dlcs)
} label: {
Text("Remove DLC")
}
}
}
.navigationTitle("\(game.titleName) DLCs")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Add", systemImage: "plus") {
isSelectingGameDLC = true
}
}
}
.onAppear {
dlcs = Self.loadDlc(game)
}
.fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in
switch result {
case .success(let urls):
for url in urls {
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let dlcDirectory = documentsDirectory.appendingPathComponent("dlc")
let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
if !fileManager.fileExists(atPath: dlcDirectory.path) {
try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil)
}
if !fileManager.fileExists(atPath: romDlcDirectory.path) {
try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil)
}
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path)
guard !dlcContent.isEmpty else { return }
let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL)
let container = DownloadableContentContainer(
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
downloadableContentNcaList: dlcContent
)
dlcs.append(container)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Error copying game file: \(error)")
}
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
}
}
}
}
private extension DLCManagerSheet {
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
let jsonURL = dlcJsonPath(for: game)
guard let data = try? Data(contentsOf: jsonURL),
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
else { return [] }
result = result.filter { container in
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
return FileManager.default.fileExists(atPath: path.path)
}
return result
}
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
guard let data = try? JSONEncoder().encode(dlc) else { return }
try? data.write(to: dlcJsonPath(for: game))
}
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
}
static func dlcJsonPath(for game: Game) -> URL {
URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game.titleId).appendingPathComponent("dlc.json")
}
}

View File

@ -142,34 +142,95 @@ namespace Ryujinx.Headless.SDL2
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "set_title_update")]
public static unsafe void SetTitleUpdate(IntPtr titleIdPtr, IntPtr updatePathPtr) {
[UnmanagedCallersOnly(EntryPoint = "get_dlc_nca_list")]
public static unsafe DlcNcaList GetDlcNcaList(IntPtr titleIdPtr, IntPtr pathPtr)
{
var titleId = Marshal.PtrToStringAnsi(titleIdPtr);
var updatePath = Marshal.PtrToStringAnsi(updatePathPtr);
string _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
var containerPath = Marshal.PtrToStringAnsi(pathPtr);
TitleUpdateMetadata _titleUpdateWindowData;
if (File.Exists(_updateJsonPath)) {
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath, _titleSerializerContext.TitleUpdateMetadata);
_titleUpdateWindowData.Paths ??= new List<string>();
if (!_titleUpdateWindowData.Paths.Contains(updatePath)) {
_titleUpdateWindowData.Paths.Add(updatePath);
}
_titleUpdateWindowData.Selected = updatePath;
} else {
_titleUpdateWindowData = new TitleUpdateMetadata {
Selected = updatePath,
Paths = new List<string> { updatePath },
};
if (!File.Exists(containerPath))
{
return new DlcNcaList { success = false };
}
JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, _titleSerializerContext.TitleUpdateMetadata);
using FileStream containerFile = File.OpenRead(containerPath);
PartitionFileSystem pfs = new();
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDlc = false;
_virtualFileSystem.ImportTickets(pfs);
// TreeIter? parentIter = null;
List<DlcNcaListItem> listItems = new();
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 = TryCreateNca(ncaFile.Get.AsStorage(), containerPath);
if (nca == null)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.PublicData)
{
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != titleId)
{
break;
}
Logger.Warning?.Print(LogClass.Application, $"ContainerPath: {containerPath}");
Logger.Warning?.Print(LogClass.Application, $"TitleId: {nca.Header.TitleId}");
Logger.Warning?.Print(LogClass.Application, $"fileEntry.FullPath: {fileEntry.FullPath}");
// parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
// ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
DlcNcaListItem item = new();
CopyStringToFixedArray(fileEntry.FullPath, item.Path, 256);
item.TitleId = nca.Header.TitleId;
listItems.Add(item);
containsDlc = true;
}
}
if (!containsDlc)
{
return new DlcNcaList { success = false };
// GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
}
var list = new DlcNcaList { success = true, size = (uint) listItems.Count };
DlcNcaListItem[] items = listItems.ToArray();
fixed (DlcNcaListItem* p = &items[0])
{
list.items = p;
}
return list;
}
private static Nca TryCreateNca(IStorage ncaStorage, string containerPath)
{
try
{
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
}
catch (Exception exception)
{
// ignored
}
return null;
}
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
public static unsafe int GetFPS()
@ -1518,6 +1579,19 @@ namespace Ryujinx.Headless.SDL2
public byte[]? Icon;
}
public unsafe struct DlcNcaListItem
{
public fixed byte Path[256];
public ulong TitleId;
}
public unsafe struct DlcNcaList
{
public bool success;
public uint size;
public unsafe DlcNcaListItem* items;
}
public unsafe struct GameInfoNative
{
public ulong FileSize;
@ -1565,14 +1639,13 @@ namespace Ryujinx.Headless.SDL2
ImageData = null;
}
}
private static void CopyStringToFixedArray(string source, byte* destination, int length)
{
var span = new Span<byte>(destination, length);
span.Clear();
Encoding.UTF8.GetBytes(source, span);
}
}
private static unsafe void CopyStringToFixedArray(string source, byte* destination, int length)
{
var span = new Span<byte>(destination, length);
span.Clear();
Encoding.UTF8.GetBytes(source, span);
}
}
}