diff --git a/settings/controller_mapping_manager.gd b/settings/controller_mapping_manager.gd index 600c9b3..d5acd21 100644 --- a/settings/controller_mapping_manager.gd +++ b/settings/controller_mapping_manager.gd @@ -13,19 +13,19 @@ const MAX_PROFILE_BYTES: int = 1024 * 1024 const CAPTURE_AXIS_THRESHOLD: float = 0.55 const CAPTURE_AXIS_RELEASE_THRESHOLD: float = 0.30 const ACTIVE_DEVICE_AXIS_THRESHOLD: float = 0.35 -const MUOS_MAPPING_REVISION: String = "muos-v3" +const MUOS_MAPPING_REVISION: String = "muos-v4" const MUOS_CONTROLLER_NAMES: Array[String] = [ "muOS-Keys", "Deeplay-keys", ] const MUOS_MAPPING_BINDINGS: String = ( - "a:b4,b:b3,x:b5,y:b6," - + "leftshoulder:b7,rightshoulder:b8," - + "lefttrigger:b13,righttrigger:b14," - + "guide:b11,start:b10,back:b9," + "a:b0,b:b1,x:b3,y:b2," + + "leftshoulder:b4,rightshoulder:b5," + + "lefttrigger:b10,righttrigger:b11," + + "guide:b8,start:b7,back:b6," + "dpup:h0.1,dpleft:h0.8,dpright:h0.2,dpdown:h0.4," - + "leftx:a0,lefty:a1,leftstick:b12," - + "rightx:a2,righty:a3,rightstick:b15,platform:Linux," + + "leftx:a0,lefty:a1,leftstick:b9," + + "rightx:a2,righty:a3,rightstick:b12,platform:Linux," ) const ROLE_A: StringName = &"a" const ROLE_B: StringName = &"b" @@ -739,8 +739,8 @@ static func should_install_muos_compatibility_mapping( controller_name: String, is_known: bool, ) -> bool: - # Current SDL databases should remain authoritative. This fallback exists - # only for legacy muOS virtual controllers that SDL cannot identify. + # Never update an already-recognized virtual controller at runtime. Godot + # can stop delivering its standardized input events after that replacement. return is_muos_controller_name(controller_name) and not is_known diff --git a/tests/controller_mapping_validation.gd b/tests/controller_mapping_validation.gd index bb6ef3b..16c9ad0 100644 --- a/tests/controller_mapping_validation.gd +++ b/tests/controller_mapping_validation.gd @@ -18,6 +18,8 @@ func _run() -> void: root.add_child(manager) await process_frame var failure: String = _validate_manager(manager) + if failure.is_empty(): + failure = _validate_portmaster_launcher() if failure.is_empty(): failure = _validate_auto_map(manager) if failure.is_empty(): @@ -40,19 +42,19 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String: ) ) for expected_binding: String in [ - "a:b4", - "b:b3", - "x:b5", - "y:b6", - "leftshoulder:b7", - "rightshoulder:b8", - "lefttrigger:b13", - "righttrigger:b14", - "back:b9", - "start:b10", - "guide:b11", - "leftstick:b12", - "rightstick:b15", + "a:b0", + "b:b1", + "x:b3", + "y:b2", + "leftshoulder:b4", + "rightshoulder:b5", + "lefttrigger:b10", + "righttrigger:b11", + "back:b6", + "start:b7", + "guide:b8", + "leftstick:b9", + "rightstick:b12", "leftx:a0", "righty:a3", ]: @@ -79,7 +81,7 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String: "muOS-Keys", true, ): - return "recognized muOS controller would override its SDL mapping" + return "recognized muOS controller would be replaced at runtime" if ControllerMappingManagerType.should_install_muos_compatibility_mapping( "ordinary controller", false, @@ -194,6 +196,30 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String: return "" +func _validate_portmaster_launcher() -> String: + const launcher_path: String = "res://scripts/portmaster/NETfishing.sh" + if not FileAccess.file_exists(launcher_path): + return "PortMaster launcher template is missing" + var launcher: String = FileAccess.get_file_as_string(launcher_path) + for expected_fragment: String in [ + "19004ca6010000000100000000010000", + "19000000010000000100000000010000", + "a:b0,b:b1,x:b3,y:b2", + "leftshoulder:b4,rightshoulder:b5", + "lefttrigger:b10,righttrigger:b11", + "guide:b8,start:b7,back:b6", + "leftstick:b9", + "rightstick:b12", + "netfishing_controllerconfig=\"$GODOT_MUOS_MAPPING\"", + "SDL_GAMECONTROLLERCONFIG=\"$netfishing_controllerconfig\" \\", + ]: + if expected_fragment not in launcher: + return "PortMaster launcher omitted " + expected_fragment + if "$'\\n'" in launcher: + return "PortMaster launcher appends mappings across a Weston-unsafe newline" + return "" + + func _validate_auto_map(manager: ControllerMappingManagerType) -> String: var panel := ControllerMappingPanelType.new() root.add_child(panel) @@ -258,13 +284,35 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String: return "manual remapping still dictates a specific physical input" if "any button" not in panel._progress_label.text.to_lower(): return "manual remapping does not request a generic controller input" - var cancel_button := InputEventJoypadButton.new() - cancel_button.device = manager.get_active_device_id() - cancel_button.button_index = JOY_BUTTON_B - cancel_button.pressed = true - manager.controller_input_observed.emit(cancel_button) - if panel.is_capturing(): - return "mapped controller back input did not cancel capture" + panel._cancel_capture() + panel._begin_auto_map() + panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS) + var lone_button := InputEventJoypadButton.new() + lone_button.device = manager.get_active_device_id() + lone_button.button_index = JOY_BUTTON_B + lone_button.pressed = true + manager.controller_input_observed.emit(lone_button) + if not panel._auto_map_active or panel._auto_map_index != 1: + return "a single controller button cancelled auto-map" + panel._cancel_capture() + panel._begin_auto_map() + panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS) + var left_bumper := InputEventJoypadButton.new() + left_bumper.device = manager.get_active_device_id() + left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER + left_bumper.pressed = true + manager.controller_input_observed.emit(left_bumper) + var right_bumper := InputEventJoypadButton.new() + right_bumper.device = manager.get_active_device_id() + right_bumper.button_index = JOY_BUTTON_RIGHT_SHOULDER + right_bumper.pressed = true + manager.controller_input_observed.emit(right_bumper) + panel._process(ControllerMappingPanelType.CANCEL_COMBO_HOLD_SECONDS - 0.01) + if not panel._auto_map_active: + return "bumper combo cancelled auto-map before the hold threshold" + panel._process(0.02) + if panel._auto_map_active or panel.is_capturing(): + return "sustained bumper combo did not cancel auto-map" panel._begin_auto_map() panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS) var held_left := InputEventJoypadMotion.new() diff --git a/ui/controller_mapping_panel.gd b/ui/controller_mapping_panel.gd index 809f3b5..555f499 100644 --- a/ui/controller_mapping_panel.gd +++ b/ui/controller_mapping_panel.gd @@ -8,6 +8,7 @@ const ControllerMappingManagerType = preload( ) const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") const CAPTURE_NEUTRAL_SECONDS: float = 0.22 +const CANCEL_COMBO_HOLD_SECONDS: float = 1.25 var _mapping_manager: ControllerMappingManagerType var _binding_buttons: Dictionary = {} @@ -24,6 +25,9 @@ var _auto_map_draft: Dictionary = {} var _capture_device_id: int = 0 var _waiting_for_neutral: bool = false var _neutral_elapsed: float = 0.0 +var _cancel_left_bumper_pressed: bool = false +var _cancel_right_bumper_pressed: bool = false +var _cancel_combo_elapsed: float = 0.0 func _ready() -> void: @@ -112,6 +116,14 @@ func _process(delta: float) -> void: or _capturing_role.is_empty() ): return + if _cancel_left_bumper_pressed and _cancel_right_bumper_pressed: + _cancel_combo_elapsed += delta + _progress_label.text = "keep holding both bumpers to cancel" + if _cancel_combo_elapsed >= CANCEL_COMBO_HOLD_SECONDS: + _cancel_capture() + _progress_label.text = "controller mapping cancelled" + return + _cancel_combo_elapsed = 0.0 if _waiting_for_neutral: if not _mapping_manager.are_capture_inputs_neutral(_capture_device_id): _neutral_elapsed = 0.0 @@ -152,18 +164,7 @@ func _on_controller_input_observed(event: InputEvent) -> void: return if event_device != _capture_device_id: return - if ( - button_event != null - and button_event.pressed - and _capturing_role != ControllerMappingManagerType.ROLE_B - and _mapping_manager.event_matches_role( - event, - ControllerMappingManagerType.ROLE_B, - ) - ): - _cancel_capture() - _progress_label.text = "controller mapping cancelled" - return + _track_cancel_combo_button(button_event) if _waiting_for_neutral: return _try_capture_event(event) @@ -332,6 +333,7 @@ func _begin_auto_map() -> void: _auto_map_active = true _auto_map_index = 0 _auto_map_draft = {} + _reset_cancel_combo() _capture_device_id = _mapping_manager.get_active_device_id() _set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index]) @@ -340,6 +342,7 @@ func _begin_manual_capture(role: StringName) -> void: _auto_map_active = false _auto_map_index = -1 _auto_map_draft.clear() + _reset_cancel_combo() _capture_device_id = _mapping_manager.get_active_device_id() _set_capture_role(role) @@ -356,10 +359,14 @@ func _refresh_capture_prompt() -> void: if _capturing_role.is_empty(): return if _auto_map_active: - _progress_label.text = "step %d of %d: %s" % [ + _progress_label.text = ( + "step %d of %d: %s\n" + + "hold both bumpers for %.2f seconds to cancel" + ) % [ _auto_map_index + 1, ControllerMappingManagerType.ROLE_ORDER.size(), _mapping_manager.get_role_prompt(_capturing_role), + CANCEL_COMBO_HOLD_SECONDS, ] return var input_instruction: String = "press any controller button" @@ -408,11 +415,34 @@ func _cancel_capture() -> void: _auto_map_draft.clear() _waiting_for_neutral = false _neutral_elapsed = 0.0 + _reset_cancel_combo() _set_action_buttons_disabled(false) if _progress_label != null: _progress_label.text = "" +func _track_cancel_combo_button(button_event: InputEventJoypadButton) -> void: + if button_event == null: + return + match button_event.button_index: + JOY_BUTTON_LEFT_SHOULDER: + _cancel_left_bumper_pressed = button_event.pressed + JOY_BUTTON_RIGHT_SHOULDER: + _cancel_right_bumper_pressed = button_event.pressed + _: + return + if not ( + _cancel_left_bumper_pressed and _cancel_right_bumper_pressed + ): + _cancel_combo_elapsed = 0.0 + + +func _reset_cancel_combo() -> void: + _cancel_left_bumper_pressed = false + _cancel_right_bumper_pressed = false + _cancel_combo_elapsed = 0.0 + + func _set_action_buttons_disabled(disabled: bool) -> void: if _auto_map_button != null: _auto_map_button.disabled = disabled