From f2d078f80b4f74c573267d15e6a83f2daa3610ea Mon Sep 17 00:00:00 2001
From: Daniil Vinogradov <xitrix@bk.ru>
Date: Sat, 15 Feb 2025 15:42:10 +0100
Subject: [PATCH] GCController wrapper

---
 .../Ryujinx/Controller/NativeController.swift | 231 ++++++++++++++++++
 src/MeloNX/MeloNX/App/Views/ContentView.swift |   7 +-
 2 files changed, 236 insertions(+), 2 deletions(-)
 create mode 100644 src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift

diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift
new file mode 100644
index 000000000..1660c8cf6
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift
@@ -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: .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 = 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
+    }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/ContentView.swift b/src/MeloNX/MeloNX/App/Views/ContentView.swift
index cc4c695ff..640db7af5 100644
--- a/src/MeloNX/MeloNX/App/Views/ContentView.swift
+++ b/src/MeloNX/MeloNX/App/Views/ContentView.swift
@@ -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
     
@@ -50,7 +51,7 @@ struct ContentView: View {
     private let animationDuration: Double = 1.0
     @State private var isAnimating = false
     @State var isLoading = true
-    
+
     // MARK: - Initialization
     init() {
         let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
@@ -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,7 @@ struct ContentView: View {
             queue: .main) { notification in
                 if let controller = notification.object as? GCController {
                     print("Controller disconnected: \(controller.productCategory)")
+                    nativeControllers[controller] = nil
                     refreshControllersList()
                 }
         }
@@ -315,7 +318,7 @@ struct ContentView: View {
             currentControllers.append(controller)
         } else if (controllersList.count - 1) >= 1 {
             for controller in controllersList {
-                if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
+                if controller.id != onscreencontroller.id && controller.name.starts(with: "GC - ") && !currentControllers.contains(where: { $0.id == controller.id }) {
                     currentControllers.append(controller)
                 }
             }