/* Copyright 2021 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #pragma once #import #import #include #include "input/gamepad_device.h" #include "input/mouse.h" #include "rend/gui.h" enum IOSButton { IOS_BTN_A = 1, IOS_BTN_B, IOS_BTN_X, IOS_BTN_Y, IOS_BTN_UP, IOS_BTN_DOWN, IOS_BTN_LEFT, IOS_BTN_RIGHT, IOS_BTN_MENU, // aka Start IOS_BTN_OPTIONS, // aka Back (xbox), Select (dualshock) IOS_BTN_HOME, IOS_BTN_L1, IOS_BTN_R1, IOS_BTN_L2, IOS_BTN_R2, IOS_BTN_L3, IOS_BTN_R3, IOS_BTN_SHARE, IOS_BTN_PADDLE1, IOS_BTN_PADDLE2, IOS_BTN_PADDLE3, IOS_BTN_PADDLE4, IOS_BTN_TOUCHPAD, IOS_BTN_MAX }; enum IOSAxis { IOS_AXIS_L1 = 1, IOS_AXIS_R1, IOS_AXIS_L2, IOS_AXIS_R2, IOS_AXIS_LX, IOS_AXIS_LY, IOS_AXIS_RX, IOS_AXIS_RY, }; static NSString *GCInputXboxShareButton = @"Button Share"; template class DefaultIOSMapping : public InputMapping { public: DefaultIOSMapping() { name = Arcade ? Gamepad ? "Arcade Gamepad" : "Arcade Hitbox" : "Default"; if (Arcade) { if (Gamepad) { // 1 2 3 4 5 6 // A B X Y L R set_button(DC_BTN_A, IOS_BTN_A); set_button(DC_BTN_B, IOS_BTN_B); set_button(DC_BTN_C, IOS_BTN_X); set_button(DC_BTN_X, IOS_BTN_Y); set_button(DC_BTN_Y, IOS_BTN_L1); set_button(DC_BTN_Z, IOS_BTN_R1); set_axis(DC_AXIS_LT, IOS_AXIS_L2, true); set_axis(DC_AXIS_RT, IOS_AXIS_R2, true); } else { // Hitbox // 1 2 3 4 5 6 7 8 // X Y R1 A B R2 L1 L2 set_button(DC_BTN_A, IOS_BTN_X); set_button(DC_BTN_B, IOS_BTN_Y); set_button(DC_BTN_C, IOS_BTN_R1); set_button(DC_BTN_X, IOS_BTN_A); set_button(DC_BTN_Y, IOS_BTN_B); set_axis(DC_BTN_Z, IOS_AXIS_R2, true); set_button(DC_DPAD2_LEFT, IOS_BTN_L1); // L1 (Naomi button 7) set_axis(DC_DPAD2_RIGHT, IOS_AXIS_L2, true); // L2 (Naomi button 8) } } else { set_button(DC_BTN_A, IOS_BTN_A); set_button(DC_BTN_B, IOS_BTN_B); set_button(DC_BTN_X, IOS_BTN_X); set_button(DC_BTN_Y, IOS_BTN_Y); set_axis(DC_AXIS_RT, IOS_AXIS_R2, true); set_button(DC_BTN_C, IOS_BTN_R1); set_axis(DC_AXIS_LT, IOS_AXIS_L2, true); set_button(DC_BTN_Z, IOS_BTN_L1); } set_button(DC_DPAD_UP, IOS_BTN_UP); set_button(DC_DPAD_DOWN, IOS_BTN_DOWN); set_button(DC_DPAD_LEFT, IOS_BTN_LEFT); set_button(DC_DPAD_RIGHT, IOS_BTN_RIGHT); set_button(DC_BTN_START, IOS_BTN_MENU); set_button(EMU_BTN_MENU, IOS_BTN_OPTIONS); set_axis(DC_AXIS_LEFT, IOS_AXIS_LX, false); set_axis(DC_AXIS_RIGHT, IOS_AXIS_LX, true); set_axis(DC_AXIS_UP, IOS_AXIS_LY, false); set_axis(DC_AXIS_DOWN, IOS_AXIS_LY, true); set_axis(DC_AXIS2_LEFT, IOS_AXIS_RX, false); set_axis(DC_AXIS2_RIGHT, IOS_AXIS_RX, true); set_axis(DC_AXIS2_UP, IOS_AXIS_RY, false); set_axis(DC_AXIS2_DOWN, IOS_AXIS_RY, true); dirty = false; } }; class IOSGamepad : public GamepadDevice { public: IOSGamepad(int port, GCController *controller) : GamepadDevice(port, "iOS"), gcController(controller) { set_maple_port(port); if (gcController.vendorName != nullptr) _name = [gcController.vendorName UTF8String]; else _name = "MFi Gamepad"; //_unique_id = ? INFO_LOG(INPUT, "iOS: Opened joystick %d: '%s'", port, _name.c_str()); loadMapping(); if (gcController.extendedGamepad != nullptr) { [gcController.extendedGamepad.buttonA setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_A, pressed); }]; [gcController.extendedGamepad.buttonB setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_B, pressed); }]; [gcController.extendedGamepad.buttonX setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_X, pressed); }]; [gcController.extendedGamepad.buttonY setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_Y, pressed); }]; [gcController.extendedGamepad.dpad setValueChangedHandler:^(GCControllerDirectionPad *dpad, float xValue, float yValue) { gamepad_btn_input(IOS_BTN_RIGHT, dpad.right.isPressed); gamepad_btn_input(IOS_BTN_LEFT, dpad.left.isPressed); gamepad_btn_input(IOS_BTN_UP, dpad.up.isPressed); gamepad_btn_input(IOS_BTN_DOWN, dpad.down.isPressed); }]; if (@available(iOS 12.1, *)) { [gcController.extendedGamepad.rightThumbstickButton setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_R3, pressed); }]; [gcController.extendedGamepad.leftThumbstickButton setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_L3, pressed); }]; } if (@available(iOS 13.0, *)) { [gcController.extendedGamepad.buttonOptions setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_OPTIONS, pressed); }]; [gcController.extendedGamepad.buttonMenu setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_MENU, pressed); }]; [gcController.extendedGamepad.leftShoulder setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_L1, pressed); }]; [gcController.extendedGamepad.rightShoulder setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_R1, pressed); }]; } else { // Left shoulder for options/menu [gcController.extendedGamepad.leftShoulder setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_OPTIONS, pressed); }]; // Right shoulder for menu/start [gcController.extendedGamepad.rightShoulder setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_MENU, pressed); }]; } if (@available(iOS 14.0, *)) { [gcController.extendedGamepad.buttonHome setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_HOME, pressed); }]; } [gcController.extendedGamepad.rightTrigger setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_R2, pressed); gamepad_axis_input(IOS_AXIS_R2, (int)std::roundf(32767.f * value)); }]; [gcController.extendedGamepad.leftTrigger setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_L2, pressed); gamepad_axis_input(IOS_AXIS_L2, (int)std::roundf(32767.f * value)); }]; [gcController.extendedGamepad.leftThumbstick.xAxis setValueChangedHandler:^(GCControllerAxisInput *axis, float value) { gamepad_axis_input(IOS_AXIS_LX, (int)std::roundf(32767.f * value)); }]; [gcController.extendedGamepad.leftThumbstick.yAxis setValueChangedHandler:^(GCControllerAxisInput *axis, float value) { gamepad_axis_input(IOS_AXIS_LY, (int)std::roundf(-32767.f * value)); }]; [gcController.extendedGamepad.rightThumbstick.xAxis setValueChangedHandler:^(GCControllerAxisInput *axis, float value) { gamepad_axis_input(IOS_AXIS_RX, (int)std::roundf(32767.f * value)); }]; [gcController.extendedGamepad.rightThumbstick.yAxis setValueChangedHandler:^(GCControllerAxisInput *axis, float value) { gamepad_axis_input(IOS_AXIS_RY, (int)std::roundf(-32767.f * value)); }]; } else { // Legacy gamepad [gcController.gamepad.buttonA setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_A, pressed); }]; [gcController.gamepad.buttonB setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_B, pressed); }]; [gcController.gamepad.buttonX setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_X, pressed); }]; [gcController.gamepad.buttonY setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_Y, pressed); }]; [gcController.gamepad.dpad setValueChangedHandler:^(GCControllerDirectionPad *dpad, float xValue, float yValue) { gamepad_btn_input(IOS_BTN_RIGHT, dpad.right.isPressed); gamepad_btn_input(IOS_BTN_LEFT, dpad.left.isPressed); gamepad_btn_input(IOS_BTN_UP, dpad.up.isPressed); gamepad_btn_input(IOS_BTN_DOWN, dpad.down.isPressed); }]; [gcController.gamepad.rightShoulder setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { if (pressed) { if (gcController != nil && gcController.gamepad.leftShoulder.pressed) gamepad_btn_input(IOS_BTN_MENU, true); else gamepad_btn_input(IOS_BTN_R2, true); } else { gamepad_btn_input(IOS_BTN_R2, false); gamepad_btn_input(IOS_BTN_MENU, false); } }]; [gcController.gamepad.leftShoulder setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { if (pressed) { if (gcController != nil && gcController.gamepad.rightShoulder.pressed) gamepad_btn_input(IOS_BTN_MENU, true); else gamepad_btn_input(IOS_BTN_L2, true); } else { gamepad_btn_input(IOS_BTN_L2, false); gamepad_btn_input(IOS_BTN_MENU, false); } }]; } if (@available(iOS 14.0, *)) { if (gcController.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) { [gcController.physicalInputProfile.buttons[GCInputXboxPaddleOne] setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_PADDLE1, pressed); }]; } if (gcController.physicalInputProfile.buttons[GCInputXboxPaddleTwo] != nil) { [gcController.physicalInputProfile.buttons[GCInputXboxPaddleTwo] setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_PADDLE2, pressed); }]; } if (gcController.physicalInputProfile.buttons[GCInputXboxPaddleThree] != nil) { [gcController.physicalInputProfile.buttons[GCInputXboxPaddleThree] setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_PADDLE3, pressed); }]; } if (gcController.physicalInputProfile.buttons[GCInputXboxPaddleFour] != nil) { [gcController.physicalInputProfile.buttons[GCInputXboxPaddleFour] setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_PADDLE4, pressed); }]; } if (gcController.physicalInputProfile.buttons[GCInputXboxShareButton] != nil) { [gcController.physicalInputProfile.buttons[GCInputXboxShareButton] setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_SHARE, pressed); }]; } if (gcController.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) { [gcController.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { gamepad_btn_input(IOS_BTN_TOUCHPAD, pressed); }]; } if (gcController.haptics != nullptr) { CHHapticEngine *hapticEngine = [gcController.haptics createEngineWithLocality:GCHapticsLocalityDefault]; NSError *error = nil; [hapticEngine startAndReturnError:&error]; if (error != nullptr) NSLog(@"Haptic engine error: \(error)"); else { this->hapticEngine = hapticEngine; rumbleEnabled = true; } } } } ~IOSGamepad() override { if (hapticEngine != nullptr) [hapticEngine stopWithCompletionHandler:^(NSError * _Nullable error) {}]; } void set_maple_port(int port) override { GamepadDevice::set_maple_port(port); if (port <= 3 && gcController != nil) gcController.playerIndex = (GCControllerPlayerIndex)port; } const char *get_button_name(u32 code) override { switch ((IOSButton)code) { case IOS_BTN_A: return "A"; case IOS_BTN_B: return "B"; case IOS_BTN_X: return "X"; case IOS_BTN_Y: return "Y"; case IOS_BTN_UP: return "DPad Up"; case IOS_BTN_DOWN: return "DPad Down"; case IOS_BTN_LEFT: return "DPad Left"; case IOS_BTN_RIGHT: return "DPad Right"; case IOS_BTN_MENU: return "Menu"; case IOS_BTN_OPTIONS: return "Options"; case IOS_BTN_HOME: return "Home"; case IOS_BTN_L1: return "L Shoulder"; case IOS_BTN_R1: return "R Shoulder"; case IOS_BTN_L2: return "L Trigger"; case IOS_BTN_R2: return "R Trigger"; case IOS_BTN_L3: return "L Thumbstick"; case IOS_BTN_R3: return "R Thumbstick"; case IOS_BTN_SHARE: return "Share"; case IOS_BTN_PADDLE1: return "Paddle 1"; case IOS_BTN_PADDLE2: return "Paddle 2"; case IOS_BTN_PADDLE3: return "Paddle 3"; case IOS_BTN_PADDLE4: return "Paddle 4"; case IOS_BTN_TOUCHPAD: return "Touchpad"; default: return nullptr; } } const char *get_axis_name(u32 code) override { switch ((IOSAxis)code) { case IOS_AXIS_L1: return "L Shoulder"; case IOS_AXIS_R1: return "R Shoulder"; case IOS_AXIS_L2: return "L Trigger"; case IOS_AXIS_R2: return "R Trigger"; case IOS_AXIS_LX: return "L Stick X"; case IOS_AXIS_LY: return "L Stick Y"; case IOS_AXIS_RX: return "R Stick X"; case IOS_AXIS_RY: return "R Stick Y"; default: return nullptr; } } std::shared_ptr getDefaultMapping() override { return std::make_shared>(); } void rumble(float power, float inclination, u32 duration_ms) override { NOTICE_LOG(INPUT, "rumble %.1f inc %f duration %d", power, inclination, duration_ms); if (@available(iOS 13.0, *)) { CHHapticEvent *event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:@[ [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:power]] relativeTime:0.0 duration:duration_ms / 1000.0]; NSError *error = nil; CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:@[event] parameters:@[] error:&error]; if (error != nil) return; if (hapticPlayer != nil) [hapticPlayer stopAtTime:0 error:&error]; hapticPlayer = [hapticEngine createPlayerWithPattern:pattern error:&error]; if (error != nil) return; [hapticPlayer startAtTime:0 error:&error]; } } void resetMappingToDefault(bool arcade, bool gamepad) override { NOTICE_LOG(INPUT, "Resetting iOS gamepad to default: %d %d", arcade, gamepad); if (arcade) { if (gamepad) input_mapper = std::make_shared>(); else input_mapper = std::make_shared>(); } else input_mapper = std::make_shared>(); } static void addController(GCController *controller) { if (controllers.count(controller) > 0) return; if (controller.extendedGamepad == nullptr && controller.gamepad == nullptr) return; int port = std::min((int)controllers.size(), 3); controllers[controller] = std::make_shared(port, controller); GamepadDevice::Register(controllers[controller]); } static void removeController(GCController *controller) { auto it = controllers.find(controller); if (it == controllers.end()) return; GamepadDevice::Unregister(it->second); controllers.erase(it); } static bool controllerConnected() { return !controllers.empty(); } private: GCController * __weak gcController = nullptr; CHHapticEngine *hapticEngine = nullptr; id hapticPlayer; static std::map> controllers; }; class IOSVirtualGamepad : public GamepadDevice { public: IOSVirtualGamepad() : GamepadDevice(0, "iOS", false) { _name = "Virtual Gamepad"; _unique_id = "ios-virtual-gamepad"; input_mapper = getDefaultMapping(); } bool is_virtual_gamepad() override { return true; } std::shared_ptr getDefaultMapping() override { return std::make_shared>(); } bool gamepad_btn_input(u32 code, bool pressed) override { if (pressed) buttonState |= 1 << code; else buttonState &= ~(1 << code); switch (code) { case IOS_BTN_L2: gamepad_axis_input(IOS_AXIS_L2, pressed ? 0x7fff : 0); if (settings.platform.isArcade()) GamepadDevice::gamepad_btn_input(IOS_BTN_L1, pressed); // Z, btn5 return true; case IOS_BTN_R2: if (!pressed && maple_port() >= 0 && maple_port() <= 3) kcode[maple_port()] |= DC_DPAD2_UP | DC_BTN_D | DC_DPAD2_DOWN; gamepad_axis_input(IOS_AXIS_R2, pressed ? 0x7fff : 0); if (settings.platform.isArcade()) GamepadDevice::gamepad_btn_input(IOS_BTN_Y, pressed); // Y, btn4 return true; default: if ((buttonState & ((1 << IOS_BTN_UP) | (1 << IOS_BTN_DOWN))) == ((1 << IOS_BTN_UP) | (1 << IOS_BTN_DOWN)) || (buttonState & ((1 << IOS_BTN_LEFT) | (1 << IOS_BTN_RIGHT))) == ((1 << IOS_BTN_LEFT) | (1 << IOS_BTN_RIGHT))) { GamepadDevice::gamepad_btn_input(IOS_BTN_UP, false); GamepadDevice::gamepad_btn_input(IOS_BTN_DOWN, false); GamepadDevice::gamepad_btn_input(IOS_BTN_LEFT, false); GamepadDevice::gamepad_btn_input(IOS_BTN_RIGHT, false); buttonState = 0; gui_open_settings(); return true; } if (settings.platform.isArcade() && maple_port() >= 0 && maple_port() <= 3) { u32& keycode = kcode[maple_port()]; if ((buttonState & (1 << IOS_BTN_R2)) != 0) { switch (code) { case IOS_BTN_A: // RT + A -> D (coin) keycode = pressed ? keycode & ~DC_BTN_D : keycode | DC_BTN_D; break; case IOS_BTN_B: // RT + B -> Service keycode = pressed ? keycode & ~DC_DPAD2_UP : keycode | DC_DPAD2_UP; break; case IOS_BTN_X: // RT + X -> Test keycode = pressed ? keycode & ~DC_DPAD2_DOWN : keycode | DC_DPAD2_DOWN; break; default: break; } } // arcade mapping: X -> btn2, Y -> btn3 if (code == IOS_BTN_X) code = IOS_BTN_R1; // C, btn2 if (code == IOS_BTN_Y) code = IOS_BTN_X; // btn3 } return GamepadDevice::gamepad_btn_input(code, pressed); } } private: u32 buttonState = 0; }; class IOSTouchMouse : public SystemMouse { public: IOSTouchMouse() : SystemMouse("iOS") { _unique_id = "ios_mouse"; _name = "Touchscreen (Mouse)"; loadMapping(); } };