From 679986e79f6b1b9b08752bb9721579b66802ae96 Mon Sep 17 00:00:00 2001 From: Tyler Wilding Date: Fri, 23 Jun 2023 14:35:32 -0500 Subject: [PATCH] sdl: safer code to resolve intermittent controller crashes related to disconnections (#2755) --- game/graphics/pipelines/opengl.cpp | 4 + game/kernel/common/kmachine.cpp | 19 +++- game/sce/libpad.cpp | 6 +- game/system/hid/devices/game_controller.cpp | 31 ++++-- game/system/hid/devices/game_controller.h | 2 + game/system/hid/input_manager.cpp | 107 +++++++++++++++++--- game/system/hid/input_manager.h | 75 ++++++++++---- game/system/hid/sdl_util.cpp | 10 ++ game/system/hid/sdl_util.h | 1 + goal_src/jak1/kernel-defs.gc | 1 + goal_src/jak1/pc/progress-pc.gc | 2 +- 11 files changed, 214 insertions(+), 44 deletions(-) diff --git a/game/graphics/pipelines/opengl.cpp b/game/graphics/pipelines/opengl.cpp index ee30e38788..079c31b048 100644 --- a/game/graphics/pipelines/opengl.cpp +++ b/game/graphics/pipelines/opengl.cpp @@ -470,6 +470,10 @@ void GLDisplay::render() { auto p = scoped_prof("display-manager-ee-events"); m_display_manager->process_ee_events(); } + { + auto p = scoped_prof("input-manager-ee-events"); + m_input_manager->process_ee_events(); + } // imgui start of frame { diff --git a/game/kernel/common/kmachine.cpp b/game/kernel/common/kmachine.cpp index 4788c89225..353b05d0cc 100644 --- a/game/kernel/common/kmachine.cpp +++ b/game/kernel/common/kmachine.cpp @@ -648,7 +648,7 @@ void pc_set_keyboard_enabled(u32 sym_val) { void pc_set_mouse_options(u32 enabled, u32 control_camera, u32 control_movement) { if (Display::GetMainDisplay()) { - Display::GetMainDisplay()->get_input_manager()->update_mouse_options( + Display::GetMainDisplay()->get_input_manager()->enqueue_update_mouse_options( symbol_to_bool(enabled), symbol_to_bool(control_camera), symbol_to_bool(control_movement)); } } @@ -665,7 +665,7 @@ void pc_set_mouse_camera_sens(u32 xsens, u32 ysens) { void pc_ignore_background_controller_events(u32 sym_val) { if (Display::GetMainDisplay()) { - Display::GetMainDisplay()->get_input_manager()->ignore_background_controller_events( + Display::GetMainDisplay()->get_input_manager()->enqueue_ignore_background_controller_events( symbol_to_bool(sym_val)); } } @@ -677,9 +677,17 @@ u64 pc_current_controller_has_led() { return bool_to_symbol(false); } +u64 pc_current_controller_has_rumble() { + if (Display::GetMainDisplay()) { + return bool_to_symbol(Display::GetMainDisplay()->get_input_manager()->controller_has_rumble(0)); + } + return bool_to_symbol(false); +} + void pc_set_controller_led(const int port, const u8 red, const u8 green, const u8 blue) { if (Display::GetMainDisplay()) { - Display::GetMainDisplay()->get_input_manager()->set_controller_led(port, red, green, blue); + Display::GetMainDisplay()->get_input_manager()->enqueue_set_controller_led(port, red, green, + blue); } } @@ -715,7 +723,8 @@ void pc_reset_bindings_to_defaults(const int port, const InputDeviceType device_ void pc_set_auto_hide_cursor(u32 val) { if (Display::GetMainDisplay()) { - Display::GetMainDisplay()->get_input_manager()->set_auto_hide_mouse(symbol_to_bool(val)); + Display::GetMainDisplay()->get_input_manager()->enqueue_set_auto_hide_mouse( + symbol_to_bool(val)); } } @@ -873,6 +882,8 @@ void init_common_pc_port_functions( make_func_symbol_func("pc-ignore-background-controller-events!", (void*)pc_ignore_background_controller_events); make_func_symbol_func("pc-current-controller-has-led?", (void*)pc_current_controller_has_led); + make_func_symbol_func("pc-current-controller-has-rumble?", + (void*)pc_current_controller_has_rumble); make_func_symbol_func("pc-set-controller-led!", (void*)pc_set_controller_led); make_func_symbol_func("pc-waiting-for-bind?", (void*)pc_waiting_for_bind); make_func_symbol_func("pc-set-waiting-for-bind!", (void*)pc_set_waiting_for_bind); diff --git a/game/sce/libpad.cpp b/game/sce/libpad.cpp index 6186ae671f..72adb34674 100644 --- a/game/sce/libpad.cpp +++ b/game/sce/libpad.cpp @@ -95,7 +95,11 @@ int scePadSetActDirect(int port, int /*slot*/, const u8* data) { // for small motor // also, the "slow" motor corresponds to the "large" motor on the PS2 if (Display::GetMainDisplay()) { - return Display::GetMainDisplay()->get_input_manager()->update_rumble(port, data[1], data[0]); + if (!Display::GetMainDisplay()->get_input_manager()->controller_has_rumble(port)) { + return 0; + } + Display::GetMainDisplay()->get_input_manager()->enqueue_update_rumble(port, data[1], data[0]); + return 1; // TODO - assuming 1 means rumble is supported, and 0 is no? } return 0; } diff --git a/game/system/hid/devices/game_controller.cpp b/game/system/hid/devices/game_controller.cpp index 1010943dc2..8868586ced 100644 --- a/game/system/hid/devices/game_controller.cpp +++ b/game/system/hid/devices/game_controller.cpp @@ -11,7 +11,8 @@ GameController::GameController(int sdl_device_id, m_loaded = false; m_device_handle = SDL_GameControllerOpen(sdl_device_id); if (!m_device_handle) { - sdl_util::log_error(fmt::format("Could not read data from device index: {}", sdl_device_id)); + sdl_util::log_error( + fmt::format("Could not open game controller with device id: {}", sdl_device_id)); return; } @@ -24,14 +25,27 @@ GameController::GameController(int sdl_device_id, m_sdl_instance_id = SDL_JoystickInstanceID(joystick); if (m_sdl_instance_id < 0) { sdl_util::log_error( - fmt::format("Could not get instance id for gamecontroller: id {}", sdl_device_id)); + fmt::format("Could not determine instance id for gamecontroller: id {}", sdl_device_id)); return; } const auto controller_guid = SDL_JoystickGetGUID(joystick); + if (sdl_util::is_SDL_GUID_zero(controller_guid)) { + sdl_util::log_error(fmt::format("Could not determine controller guid: id {}", sdl_device_id)); + return; + } char guidStr[33]; SDL_JoystickGetGUIDString(controller_guid, guidStr, sizeof(guidStr)); m_guid = guidStr; + // NOTE - it has been observed that this function will return `NULL` which indicates a bad + // controller this seems to happen if you disconnect the controller while it's in the process of + // connecting + // + // However, SDL_GameControllerGetAttached returns true, and `NULL` here is not an outright failure + // there could be controllers that just have no name. + // + // So I'm letting this one go through, in most cases it had an invalid guid as well (so caught + // above). auto name = SDL_GameControllerName(m_device_handle); if (!name) { sdl_util::log_error(fmt::format("Could not get device name with id: {}", sdl_device_id)); @@ -40,6 +54,7 @@ GameController::GameController(int sdl_device_id, m_device_name = name; } m_has_led = sdl_util::from_sdl_bool(SDL_GameControllerHasLED(m_device_handle)); + m_has_rumble = sdl_util::from_sdl_bool(SDL_GameControllerHasRumble(m_device_handle)); if (m_settings->controller_binds.find(m_guid) == m_settings->controller_binds.end()) { m_settings->controller_binds[m_guid] = DEFAULT_CONTROLLER_BINDS; @@ -52,7 +67,7 @@ void GameController::process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, std::optional& bind_assignment, - bool /*ignore_inputs*/) { + bool ignore_inputs) { if (event.type == SDL_CONTROLLERAXISMOTION && event.caxis.which == m_sdl_instance_id) { // https://wiki.libsdl.org/SDL2/SDL_GameControllerAxis if ((int)event.caxis.axis <= SDL_CONTROLLER_AXIS_INVALID || @@ -134,7 +149,7 @@ void GameController::close_device() { } int GameController::update_rumble(const u8 low_rumble, const u8 high_rumble) { - if (m_device_handle) { + if (m_has_rumble && m_device_handle) { // https://wiki.libsdl.org/SDL2/SDL_GameControllerRumble // Arbitrary duration, since it's called every frame anyway, just so the vibration doesn't last // forever. SDL expects a value in the range of 0-0xFFFF, so the `257` is just normalizing the @@ -150,8 +165,10 @@ void GameController::set_led(const u8 red, const u8 green, const u8 blue) { if (!m_has_led) { return; } - auto ok = SDL_GameControllerSetLED(m_device_handle, red, green, blue); - if (ok != 0) { - m_has_led = false; + if (m_device_handle) { + auto ok = SDL_GameControllerSetLED(m_device_handle, red, green, blue); + if (ok != 0) { + m_has_led = false; + } } } diff --git a/game/system/hid/devices/game_controller.h b/game/system/hid/devices/game_controller.h index 96db4abe2a..a88c2bce38 100644 --- a/game/system/hid/devices/game_controller.h +++ b/game/system/hid/devices/game_controller.h @@ -17,6 +17,7 @@ class GameController : public InputDevice { int update_rumble(const u8 low_rumble, const u8 high_rumble); std::string get_name() const { return m_device_name; } bool has_led() { return m_has_led; } + bool has_rumble() { return m_has_rumble; } void set_led(const u8 red, const u8 green, const u8 blue); std::string get_guid() { return m_guid; } @@ -25,5 +26,6 @@ class GameController : public InputDevice { SDL_GameController* m_device_handle; std::string m_device_name = ""; bool m_has_led; + bool m_has_rumble; std::string m_guid = ""; }; diff --git a/game/system/hid/input_manager.cpp b/game/system/hid/input_manager.cpp index e8026fc360..b22b124b9b 100644 --- a/game/system/hid/input_manager.cpp +++ b/game/system/hid/input_manager.cpp @@ -53,6 +53,8 @@ void InputManager::refresh_device_list() { m_available_controllers.clear(); m_controller_port_mapping.clear(); // Enumerate devices + // TODO - if this was done on a separate thread, there would be no hitch in the game thread + // but of course, that presents other synchronization challenges. const auto num_joysticks = SDL_NumJoysticks(); if (num_joysticks > 0) { for (int i = 0; i < num_joysticks; i++) { @@ -61,6 +63,10 @@ void InputManager::refresh_device_list() { continue; } auto controller = std::make_shared(i, m_settings); + if (!controller->is_loaded()) { + lg::error("Unable to successfully connect to GameController with id {}, skipping", i); + continue; + } m_available_controllers.push_back(controller); // By default, controller port mapping is on a first-come-first-served basis // @@ -103,6 +109,11 @@ void InputManager::refresh_device_list() { } } +void InputManager::enqueue_ignore_background_controller_events(const bool ignore) { + const std::lock_guard lock(m_event_queue_mtx); + ee_event_queue.push({EEInputEventType::IGNORE_BACKGROUND_CONTROLLER_EVENTS, ignore}); +} + void InputManager::ignore_background_controller_events(const bool ignore) { m_ignore_background_controller_events = ignore; // TODO - ignoring return value (atleast log it) @@ -169,19 +180,33 @@ void InputManager::process_sdl_event(const SDL_Event& event, } } -std::optional> InputManager::get_current_data(const int port) const { - if (m_data.find(port) == m_data.end()) { - return {}; +void InputManager::process_ee_events() { + const std::lock_guard lock(m_event_queue_mtx); + // Fully process any events from the EE + while (!ee_event_queue.empty()) { + const auto& evt = ee_event_queue.front(); + switch (evt.type) { + case EEInputEventType::IGNORE_BACKGROUND_CONTROLLER_EVENTS: + ignore_background_controller_events(std::get(evt.param1)); + break; + case EEInputEventType::UPDATE_RUMBLE: + update_rumble(std::get(evt.param1), std::get(evt.param2), + std::get(evt.param3)); + break; + case EEInputEventType::SET_CONTROLLER_LED: + set_controller_led(std::get(evt.param1), std::get(evt.param2), + std::get(evt.param3), std::get(evt.param4)); + break; + case EEInputEventType::UPDATE_MOUSE_OPTIONS: + update_mouse_options(std::get(evt.param1), std::get(evt.param2), + std::get(evt.param3)); + break; + case EEInputEventType::SET_AUTO_HIDE_MOUSE: + set_auto_hide_mouse(std::get(evt.param1)); + break; + } + ee_event_queue.pop(); } - return m_data.at(port); -} - -int InputManager::update_rumble(int port, u8 low_intensity, u8 high_intensity) { - if (m_controller_port_mapping.find(port) == m_controller_port_mapping.end()) { - return 0; - } - return m_available_controllers.at(m_controller_port_mapping.at(port)) - ->update_rumble(low_intensity, high_intensity); } void InputManager::register_command(const CommandBinding::Source source, @@ -210,6 +235,13 @@ void InputManager::register_command(const CommandBinding::Source source, } } +std::optional> InputManager::get_current_data(const int port) const { + if (m_data.find(port) == m_data.end()) { + return {}; + } + return m_data.at(port); +} + std::string InputManager::get_controller_name(const int controller_id) { if ((size_t)controller_id >= m_available_controllers.size()) { return ""; @@ -271,7 +303,7 @@ void InputManager::set_controller_for_port(const int controller_id, const int po } bool InputManager::controller_has_led(const int port) { - if (m_controller_port_mapping.find(0) == m_controller_port_mapping.end()) { + if (m_controller_port_mapping.find(port) == m_controller_port_mapping.end()) { return false; } const auto id = m_controller_port_mapping.at(port); @@ -281,8 +313,27 @@ bool InputManager::controller_has_led(const int port) { return m_available_controllers.at(id)->has_led(); } +bool InputManager::controller_has_rumble(const int port) { + if (m_controller_port_mapping.find(port) == m_controller_port_mapping.end()) { + return false; + } + const auto id = m_controller_port_mapping.at(port); + if (id >= (int)m_available_controllers.size()) { + return false; + } + return m_available_controllers.at(id)->has_rumble(); +} + +void InputManager::enqueue_set_controller_led(const int port, + const u8 red, + const u8 green, + const u8 blue) { + const std::lock_guard lock(m_event_queue_mtx); + ee_event_queue.push({EEInputEventType::SET_CONTROLLER_LED, port, red, green, blue}); +} + void InputManager::set_controller_led(const int port, const u8 red, const u8 green, const u8 blue) { - if (m_controller_port_mapping.find(0) == m_controller_port_mapping.end()) { + if (m_controller_port_mapping.find(port) == m_controller_port_mapping.end()) { return; } const auto id = m_controller_port_mapping.at(port); @@ -292,6 +343,21 @@ void InputManager::set_controller_led(const int port, const u8 red, const u8 gre m_available_controllers.at(id)->set_led(red, green, blue); } +void InputManager::enqueue_update_rumble(const int port, + const u8 low_intensity, + const u8 high_intensity) { + const std::lock_guard lock(m_event_queue_mtx); + ee_event_queue.push({EEInputEventType::UPDATE_RUMBLE, port, low_intensity, high_intensity}); +} + +int InputManager::update_rumble(int port, u8 low_intensity, u8 high_intensity) { + if (m_controller_port_mapping.find(port) == m_controller_port_mapping.end()) { + return 0; + } + return m_available_controllers.at(m_controller_port_mapping.at(port)) + ->update_rumble(low_intensity, high_intensity); +} + void InputManager::enable_keyboard(const bool enabled) { m_keyboard_enabled = enabled; if (!m_keyboard_enabled) { @@ -300,6 +366,14 @@ void InputManager::enable_keyboard(const bool enabled) { } } +void InputManager::enqueue_update_mouse_options(const bool enabled, + const bool control_camera, + const bool control_movement) { + const std::lock_guard lock(m_event_queue_mtx); + ee_event_queue.push( + {EEInputEventType::UPDATE_MOUSE_OPTIONS, enabled, control_camera, control_movement}); +} + void InputManager::update_mouse_options(const bool enabled, const bool control_camera, const bool control_movement) { @@ -360,6 +434,11 @@ void InputManager::reset_input_bindings_to_defaults(const int port, } } +void InputManager::enqueue_set_auto_hide_mouse(const bool auto_hide_mouse) { + const std::lock_guard lock(m_event_queue_mtx); + ee_event_queue.push({EEInputEventType::SET_AUTO_HIDE_MOUSE, auto_hide_mouse}); +} + void InputManager::set_auto_hide_mouse(const bool auto_hide_mouse) { m_auto_hide_mouse = auto_hide_mouse; if (!auto_hide_mouse) { diff --git a/game/system/hid/input_manager.h b/game/system/hid/input_manager.h index 27ba30b5b6..600e202ae5 100644 --- a/game/system/hid/input_manager.h +++ b/game/system/hid/input_manager.h @@ -2,9 +2,12 @@ #include #include +#include #include +#include #include #include +#include #include "common/common_types.h" @@ -21,22 +24,52 @@ /// - polls data from the input devices considered active /// - fetches said data to be sent to the game class InputManager { + private: + enum class EEInputEventType { + IGNORE_BACKGROUND_CONTROLLER_EVENTS, + UPDATE_RUMBLE, + SET_CONTROLLER_LED, + UPDATE_MOUSE_OPTIONS, + SET_AUTO_HIDE_MOUSE + }; + + struct EEInputEvent { + EEInputEventType type; + std::variant param1; + std::variant param2; + std::variant param3; + std::variant param4; + }; + public: InputManager(); ~InputManager(); // Propagate and handle the SDL event, ignored it if it's not relevant void process_sdl_event(const SDL_Event& event, const bool ignore_mouse, const bool ignore_kb); - // TODO - organize the functions here - void refresh_device_list(); - void ignore_background_controller_events(const bool ignore); - - std::optional> get_current_data(const int port) const; - int update_rumble(const int port, const u8 low_intensity, const u8 high_intensity); - std::pair get_mouse_pos() const { return m_mouse.get_mouse_pos(); } - + /// Any event coming from the EE thread that interacts directly with SDL should be enqueued as an + /// event so it can be ran from the proper thread context (the graphics thread) + void process_ee_events(); void register_command(const CommandBinding::Source source, const CommandBinding bind); + std::optional> get_current_data(const int port) const; + std::pair get_mouse_pos() const { return m_mouse.get_mouse_pos(); } + + // These functions can be called from the EE and interact with SDL directly + // These should be enqueued in the event that for example, you try to set + // the controller LED while the controller is disconnecting + void enqueue_ignore_background_controller_events(const bool ignore); + void enqueue_update_rumble(const int port, const u8 low_intensity, const u8 high_intensity); + void enqueue_set_controller_led(const int port, const u8 red, const u8 green, const u8 blue); + void enqueue_update_mouse_options(const bool enabled, + const bool control_camera, + const bool control_movement); + void enqueue_set_auto_hide_mouse(const bool auto_hide_mouse); + + // These functions can be called from the EE but they only interact with + // the InputManager, so it shouldn't hurt (they don't need to be enqueued) + // Also these are generally waiting for a response and...OpenGOAL does not have an async/await + // pattern! int get_num_controllers() const { return m_available_controllers.size(); } std::string get_controller_name(const int controller_id); std::string get_current_bind(const int port, @@ -44,13 +77,10 @@ class InputManager { const bool buttons, const int input_idx, const bool analog_for_minimum); - bool controller_has_led(const int port); void set_controller_for_port(const int controller_id, const int port); - void set_controller_led(const int port, const u8 red, const u8 green, const u8 blue); + bool controller_has_led(const int port); + bool controller_has_rumble(const int port); void enable_keyboard(const bool enabled); - void update_mouse_options(const bool enabled, - const bool control_camera, - const bool control_movement); bool get_waiting_for_bind() const { return m_waiting_for_bind.has_value(); } void set_wait_for_bind(const InputDeviceType device_type, const bool for_analog, @@ -59,10 +89,11 @@ class InputManager { void stop_waiting_for_bind() { m_waiting_for_bind = std::nullopt; } void set_camera_sens(const float xsens, const float ysens); void reset_input_bindings_to_defaults(const int port, const InputDeviceType device_type); - void set_auto_hide_mouse(const bool auto_hide_mouse); - void clear_inputs(); private: + std::mutex m_event_queue_mtx; + std::queue ee_event_queue; + std::shared_ptr m_settings; /// This data can be shared throughout the runtime, it is the current state of the @@ -91,8 +122,6 @@ class InputManager { bool m_mouse_enabled = false; bool m_auto_hide_mouse = true; bool m_mouse_currently_hidden = false; - void hide_cursor(const bool hide_cursor); - bool m_ignore_background_controller_events = false; /// No inputs will be processed while in this mode the first input detected from the relevant @@ -100,4 +129,16 @@ class InputManager { /// /// The game will poll for the status of this flag to know if a bind has been assigned std::optional m_waiting_for_bind = std::nullopt; + + void refresh_device_list(); + void hide_cursor(const bool hide_cursor); + void clear_inputs(); + + void ignore_background_controller_events(const bool ignore); + int update_rumble(const int port, const u8 low_intensity, const u8 high_intensity); + void set_controller_led(const int port, const u8 red, const u8 green, const u8 blue); + void update_mouse_options(const bool enabled, + const bool control_camera, + const bool control_movement); + void set_auto_hide_mouse(const bool auto_hide_mouse); }; diff --git a/game/system/hid/sdl_util.cpp b/game/system/hid/sdl_util.cpp index b88688ec74..2b82a59552 100644 --- a/game/system/hid/sdl_util.cpp +++ b/game/system/hid/sdl_util.cpp @@ -23,6 +23,15 @@ SDL_bool sdl_bool(const bool val) { bool from_sdl_bool(const SDL_bool val) { return val == SDL_TRUE ? true : false; } +bool is_SDL_GUID_zero(SDL_GUID guid) { + for (int i = 0; i < 16; i++) { + if (guid.data[i] != 0) { + return false; + } + } + return true; +} + std::vector get_modifier_strings(InputModifiers modifiers) { std::vector result = {}; if (modifiers.need_ctrl) { @@ -91,6 +100,7 @@ std::string get_controller_axis_name(const int sdl_axis_id) { } return result; } + bool is_modifier_key(const SDL_Keycode key_code) { return key_code == SDLK_LSHIFT || key_code == SDLK_RSHIFT || key_code == SDLK_LALT || key_code == SDLK_RALT || key_code == SDLK_LCTRL || key_code == SDLK_RCTRL || diff --git a/game/system/hid/sdl_util.h b/game/system/hid/sdl_util.h index 14a752d3c0..bbda888260 100644 --- a/game/system/hid/sdl_util.h +++ b/game/system/hid/sdl_util.h @@ -12,6 +12,7 @@ void log_error(const std::string& msg = ""); bool is_any_event_type(uint32_t event_type, const std::vector& allowed_types); SDL_bool sdl_bool(const bool val); bool from_sdl_bool(const SDL_bool val); +bool is_SDL_GUID_zero(SDL_GUID guid); std::string get_mouse_button_name(const int sdl_mouse_button_id, InputModifiers modifiers); std::string get_keyboard_button_name(const int sdl_key_code, InputModifiers modifiers); diff --git a/goal_src/jak1/kernel-defs.gc b/goal_src/jak1/kernel-defs.gc index ecf6d00954..5b44decd10 100644 --- a/goal_src/jak1/kernel-defs.gc +++ b/goal_src/jak1/kernel-defs.gc @@ -349,6 +349,7 @@ (define-extern pc-set-mouse-options! (function symbol symbol symbol none)) (define-extern pc-set-mouse-camera-sens! (function float float none)) (define-extern pc-current-controller-has-led? (function symbol)) +(define-extern pc-current-controller-has-rumble? (function symbol)) (define-extern pc-set-controller-led! (function int int int int none)) (define-extern pc-ignore-background-controller-events! (function symbol none)) (define-extern pc-reset-bindings-to-defaults! (function int int none)) diff --git a/goal_src/jak1/pc/progress-pc.gc b/goal_src/jak1/pc/progress-pc.gc index b5d57a434b..feb02e8e72 100644 --- a/goal_src/jak1/pc/progress-pc.gc +++ b/goal_src/jak1/pc/progress-pc.gc @@ -188,7 +188,7 @@ (define *controller-options* (new 'static 'boxed-array :type game-option (new 'static 'game-option :option-type (game-option-type menu) :name (text-id input-opts-select-controller) :scale #t :param3 (game-option-menu select-controller)) - (new 'static 'game-option :option-type (game-option-type on-off) :name (text-id vibrations) :scale #t) + (new 'static 'game-option :option-type (game-option-type on-off) :name (text-id vibrations) :scale #t :option-disabled-func (lambda () (not (pc-current-controller-has-rumble?)))) (new 'static 'game-option :option-type (game-option-type slider) :name (text-id input-opts-analog-deadzone) :scale #t :param1 0.0 :param2 1.0 :slider-step-size 0.01 :slider-show-decimal? #t) (new 'static 'game-option :option-type (game-option-type on-off) :name (text-id input-opts-ignore-controller-win-focus) :scale #t