forked from MeloNX/MeloNX
Compare commits
3 Commits
cb71eadb53
...
9db3659abd
Author | SHA1 | Date | |
---|---|---|---|
9db3659abd | |||
df2b17ddd6 | |||
757fb1f6d1 |
@ -1,237 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
@ -78,7 +78,7 @@ class VirtualController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
|
static func rumble(lowFreq: Float, highFreq: Float) {
|
||||||
do {
|
do {
|
||||||
// Low-frequency haptic pattern
|
// Low-frequency haptic pattern
|
||||||
let lowFreqPattern = try CHHapticPattern(events: [
|
let lowFreqPattern = try CHHapticPattern(events: [
|
||||||
@ -96,23 +96,9 @@ class VirtualController {
|
|||||||
], relativeTime: 0.2, duration: 0.2)
|
], relativeTime: 0.2, duration: 0.2)
|
||||||
], parameters: [])
|
], parameters: [])
|
||||||
|
|
||||||
// Mutable engine
|
// Create and start the haptic engine
|
||||||
var engine = engine
|
let engine = try CHHapticEngine()
|
||||||
|
try engine.start()
|
||||||
// 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
|
// Create and play the low-frequency player
|
||||||
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
|
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
|
||||||
@ -127,8 +113,6 @@ class VirtualController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var hapticEngine: CHHapticEngine?
|
|
||||||
|
|
||||||
|
|
||||||
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
||||||
guard controller != nil else { return }
|
guard controller != nil else { return }
|
||||||
|
@ -26,7 +26,6 @@ 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
|
||||||
|
|
||||||
@ -51,7 +50,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: "")
|
||||||
@ -153,7 +152,6 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,8 +163,6 @@ 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]?.cleanup()
|
|
||||||
nativeControllers[controller] = nil
|
|
||||||
refreshControllersList()
|
refreshControllersList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,9 +306,8 @@ struct ContentView: View {
|
|||||||
self.onscreencontroller = onscreen
|
self.onscreencontroller = onscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
|
controllersList.removeAll(where: { $0.id == "0"})
|
||||||
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
|
|
||||||
|
|
||||||
currentControllers = []
|
currentControllers = []
|
||||||
|
|
||||||
if controllersList.count == 1 {
|
if controllersList.count == 1 {
|
||||||
@ -402,10 +397,3 @@ 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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user