forked from MeloNX/MeloNX
Compare commits
4 Commits
7277e1fa9b
...
61dd13d0e8
Author | SHA1 | Date | |
---|---|---|---|
61dd13d0e8 | |||
17952f172e | |||
446f1b312a | |||
c5bb7805ae |
@ -43,6 +43,8 @@ int main_ryujinx_sdl(int argc, char **argv);
|
|||||||
|
|
||||||
int get_current_fps();
|
int get_current_fps();
|
||||||
|
|
||||||
|
void set_title_update(const char* titleIdPtr, const char* updatePathPtr);
|
||||||
|
|
||||||
void initialize();
|
void initialize();
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -0,0 +1,231 @@
|
|||||||
|
//
|
||||||
|
// 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 let nativeController: GCController
|
||||||
|
|
||||||
|
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
|
||||||
|
|
||||||
|
init(_ controller: GCController) {
|
||||||
|
nativeController = controller
|
||||||
|
setupHandheldController()
|
||||||
|
}
|
||||||
|
|
||||||
|
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: nil,
|
||||||
|
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)")
|
||||||
|
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
|
||||||
|
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: .leftTrigger)
|
||||||
|
setupButtonChangeListener(gamepad.rightShoulder, for: .rightTrigger)
|
||||||
|
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 = controller {
|
||||||
|
SDL_GameControllerClose(controller)
|
||||||
|
self.controller = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(nativeController)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: NativeController, rhs: NativeController) -> Bool {
|
||||||
|
lhs.nativeController == rhs.nativeController
|
||||||
|
}
|
||||||
|
}
|
@ -227,8 +227,9 @@ class Ryujinx {
|
|||||||
|
|
||||||
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||||
|
|
||||||
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
|
guard let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
|
||||||
|
else { continue }
|
||||||
|
|
||||||
games.append(game)
|
games.append(game)
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
print(error)
|
||||||
@ -372,7 +373,18 @@ class Ryujinx {
|
|||||||
self.firmwareversion = version
|
self.firmwareversion = version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setTitleUpdate(titleId: String, updatePath: String) {
|
||||||
|
guard let titleIdPtr = titleId.cString(using: .utf8),
|
||||||
|
let updatePathPtr = updatePath.cString(using: .utf8)
|
||||||
|
else {
|
||||||
|
print("Invalid firmware path")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
set_title_update(titleIdPtr, updatePathPtr)
|
||||||
|
}
|
||||||
|
|
||||||
private func generateGamepadId(joystickIndex: Int32) -> String? {
|
private func generateGamepadId(joystickIndex: Int32) -> String? {
|
||||||
let guid = SDL_JoystickGetDeviceGUID(joystickIndex)
|
let guid = SDL_JoystickGetDeviceGUID(joystickIndex)
|
||||||
|
|
||||||
|
@ -22,10 +22,10 @@ public struct Game: Identifiable, Equatable, Hashable {
|
|||||||
var icon: UIImage?
|
var icon: UIImage?
|
||||||
|
|
||||||
|
|
||||||
static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game {
|
static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game? {
|
||||||
var gameInfo = gameInfo
|
var gameInfo = gameInfo
|
||||||
var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
|
var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
|
||||||
|
|
||||||
gameTemp.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
|
gameTemp.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
|
||||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||||
String(cString: $0)
|
String(cString: $0)
|
||||||
|
@ -26,6 +26,7 @@ struct ContentView: View {
|
|||||||
@State private var controllersList: [Controller] = []
|
@State private var controllersList: [Controller] = []
|
||||||
@State private var currentControllers: [Controller] = []
|
@State private var currentControllers: [Controller] = []
|
||||||
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
||||||
|
@State var nativeControllers: [GCController: NativeController] = [:]
|
||||||
@State private var isVirtualControllerActive: Bool = false
|
@State private var isVirtualControllerActive: Bool = false
|
||||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ struct ContentView: View {
|
|||||||
private let animationDuration: Double = 1.0
|
private let animationDuration: Double = 1.0
|
||||||
@State private var isAnimating = false
|
@State private var isAnimating = false
|
||||||
@State var isLoading = true
|
@State var isLoading = true
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init() {
|
init() {
|
||||||
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
||||||
@ -152,6 +153,7 @@ struct ContentView: View {
|
|||||||
queue: .main) { notification in
|
queue: .main) { notification in
|
||||||
if let controller = notification.object as? GCController {
|
if let controller = notification.object as? GCController {
|
||||||
print("Controller connected: \(controller.productCategory)")
|
print("Controller connected: \(controller.productCategory)")
|
||||||
|
nativeControllers[controller] = .init(controller)
|
||||||
refreshControllersList()
|
refreshControllersList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,6 +165,7 @@ struct ContentView: View {
|
|||||||
queue: .main) { notification in
|
queue: .main) { notification in
|
||||||
if let controller = notification.object as? GCController {
|
if let controller = notification.object as? GCController {
|
||||||
print("Controller disconnected: \(controller.productCategory)")
|
print("Controller disconnected: \(controller.productCategory)")
|
||||||
|
nativeControllers[controller] = nil
|
||||||
refreshControllersList()
|
refreshControllersList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -306,8 +309,9 @@ struct ContentView: View {
|
|||||||
self.onscreencontroller = onscreen
|
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 = []
|
currentControllers = []
|
||||||
|
|
||||||
if controllersList.count == 1 {
|
if controllersList.count == 1 {
|
||||||
@ -397,3 +401,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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ struct GameLibraryView: View {
|
|||||||
@State var startgame = false
|
@State var startgame = false
|
||||||
@State var isSelectingGameFile = false
|
@State var isSelectingGameFile = false
|
||||||
@State var isViewingGameInfo: Bool = false
|
@State var isViewingGameInfo: Bool = false
|
||||||
|
@State var isSelectingGameUpdate: Bool = false
|
||||||
@State var gameInfo: Game?
|
@State var gameInfo: Game?
|
||||||
var games: Binding<[Game]> {
|
var games: Binding<[Game]> {
|
||||||
Binding(
|
Binding(
|
||||||
@ -99,7 +100,7 @@ struct GameLibraryView: View {
|
|||||||
|
|
||||||
LazyVStack(spacing: 2) {
|
LazyVStack(spacing: 2) {
|
||||||
ForEach(filteredGames) { game in
|
ForEach(filteredGames) { game in
|
||||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
addToRecentGames(game)
|
addToRecentGames(game)
|
||||||
}
|
}
|
||||||
@ -109,7 +110,7 @@ struct GameLibraryView: View {
|
|||||||
} else {
|
} else {
|
||||||
LazyVStack(spacing: 2) {
|
LazyVStack(spacing: 2) {
|
||||||
ForEach(filteredGames) { game in
|
ForEach(filteredGames) { game in
|
||||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
addToRecentGames(game)
|
addToRecentGames(game)
|
||||||
}
|
}
|
||||||
@ -277,6 +278,36 @@ struct GameLibraryView: View {
|
|||||||
print("File import failed: \(err.localizedDescription)")
|
print("File import failed: \(err.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.nsp]) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let url):
|
||||||
|
guard let gameInfo, 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 romUpdatedDirectory = documentsDirectory.appendingPathComponent("updates")
|
||||||
|
|
||||||
|
if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
|
||||||
|
try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
|
try? fileManager.copyItem(at: url, to: destinationURL)
|
||||||
|
|
||||||
|
Ryujinx.shared.setTitleUpdate(titleId: gameInfo.titleId, updatePath: destinationURL.path)
|
||||||
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
|
} catch {
|
||||||
|
print("Error copying game file: \(error)")
|
||||||
|
}
|
||||||
|
case .failure(let err):
|
||||||
|
print("File import failed: \(err.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
.sheet(isPresented: Binding(
|
.sheet(isPresented: Binding(
|
||||||
get: { isViewingGameInfo && gameInfo != nil },
|
get: { isViewingGameInfo && gameInfo != nil },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
@ -421,6 +452,7 @@ struct GameListRow: View {
|
|||||||
@Binding var startemu: Game?
|
@Binding var startemu: Game?
|
||||||
@Binding var games: [Game] // Add this binding
|
@Binding var games: [Game] // Add this binding
|
||||||
@Binding var isViewingGameInfo: Bool
|
@Binding var isViewingGameInfo: Bool
|
||||||
|
@Binding var isSelectingGameUpdate: Bool
|
||||||
@Binding var gameInfo: Game?
|
@Binding var gameInfo: Game?
|
||||||
@State var gametoDelete: Game?
|
@State var gametoDelete: Game?
|
||||||
@State var showGameDeleteConfirmation: Bool = false
|
@State var showGameDeleteConfirmation: Bool = false
|
||||||
@ -486,6 +518,13 @@ struct GameListRow: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Label("Game Info", systemImage: "info.circle")
|
Label("Game Info", systemImage: "info.circle")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
gameInfo = game
|
||||||
|
isSelectingGameUpdate.toggle()
|
||||||
|
} label: {
|
||||||
|
Label("Add Game Update", systemImage: "chevron.up.circle")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
@ -115,6 +115,7 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
private static bool _enableMouse;
|
private static bool _enableMouse;
|
||||||
|
|
||||||
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")]
|
[UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")]
|
||||||
public static unsafe int MainExternal(int argCount, IntPtr* pArgs)
|
public static unsafe int MainExternal(int argCount, IntPtr* pArgs)
|
||||||
@ -141,6 +142,20 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "set_title_update")]
|
||||||
|
public static unsafe void SetTitleUpdate(IntPtr titleIdPtr, IntPtr updatePathPtr) {
|
||||||
|
var titleId = Marshal.PtrToStringAnsi(titleIdPtr);
|
||||||
|
var updatePath = Marshal.PtrToStringAnsi(updatePathPtr);
|
||||||
|
string _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
||||||
|
|
||||||
|
TitleUpdateMetadata _titleUpdateWindowData = new TitleUpdateMetadata
|
||||||
|
{
|
||||||
|
Selected = updatePath,
|
||||||
|
Paths = new List<string>(),
|
||||||
|
};
|
||||||
|
JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, _titleSerializerContext.TitleUpdateMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
||||||
public static unsafe int GetFPS()
|
public static unsafe int GetFPS()
|
||||||
@ -321,6 +336,9 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
var stream = OpenFile(descriptor);
|
var stream = OpenFile(descriptor);
|
||||||
|
|
||||||
var gameInfo = GetGameInfo(stream, extension);
|
var gameInfo = GetGameInfo(stream, extension);
|
||||||
|
if (gameInfo == null) {
|
||||||
|
return new GameInfoNative(0, "", "", "", "", new byte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
return new GameInfoNative(
|
return new GameInfoNative(
|
||||||
(ulong)gameInfo.FileSize,
|
(ulong)gameInfo.FileSize,
|
||||||
@ -719,7 +737,7 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
|
|
||||||
if (File.Exists(titleUpdateMetadataPath))
|
if (File.Exists(titleUpdateMetadataPath))
|
||||||
{
|
{
|
||||||
// updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected;
|
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
||||||
|
|
||||||
if (File.Exists(updatePath))
|
if (File.Exists(updatePath))
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user