From 908cf2733a1be2c55c330e153bba995025a5c692 Mon Sep 17 00:00:00 2001 From: Voyager Date: Sun, 16 Aug 2026 12:26:17 -0400 Subject: [PATCH] fix: repair gameplay controller input handling --- main/main.gd | 3 --- player/player.gd | 31 ++++++++++++--------- project.godot | 2 +- settings/controller_mapping_manager.gd | 4 +-- tests/android_readiness_validation.gd | 3 ++- tests/art_tools_validation.gd | 21 ++++++++++++++- tests/controller_mapping_validation.gd | 37 +++++++++++++++++++++++--- 7 files changed, 77 insertions(+), 24 deletions(-) diff --git a/main/main.gd b/main/main.gd index 210b01b..93f98c0 100644 --- a/main/main.gd +++ b/main/main.gd @@ -1362,9 +1362,6 @@ func _unhandled_input(event: InputEvent) -> void: ): _game_ui.set_shop_prompt_visible(false) get_viewport().set_input_as_handled() - elif event is InputEventJoypadButton and _player != null: - _player.hotbar.clear_slot(_player.hotbar.get_selected_slot()) - get_viewport().set_input_as_handled() func _process(_delta: float) -> void: diff --git a/player/player.gd b/player/player.gd index b7b7e4d..6c90518 100644 --- a/player/player.gd +++ b/player/player.gd @@ -1251,6 +1251,25 @@ func is_sitting() -> bool: return _sitting +func _input(event: InputEvent) -> void: + # Camera dragging must observe the complete mouse gesture before GUI controls + # get a chance to consume one part of it. Do not claim the event here: the + # Art Kit still needs the same right-button state to suspend its pointer. + if not local_control_enabled or not _is_camera_input_enabled(): + return + var mouse_button := event as InputEventMouseButton + if mouse_button != null and event.is_action("camera_drag"): + _set_camera_dragging(mouse_button.pressed) + return + var mouse_motion := event as InputEventMouseMotion + if mouse_motion == null or not _camera_dragging: + return + if _free_camera_active: + _rotate_free_camera(mouse_motion.relative * mouse_sensitivity) + else: + _rotate_camera(mouse_motion.relative * mouse_sensitivity) + + func _unhandled_input(event: InputEvent) -> void: if not local_control_enabled or not _is_camera_input_enabled(): return @@ -1259,18 +1278,6 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() return - if event.is_action("camera_drag"): - _set_camera_dragging(event.is_pressed()) - get_viewport().set_input_as_handled() - return - - if event is InputEventMouseMotion and _camera_dragging: - if _free_camera_active: - _rotate_free_camera(event.relative * mouse_sensitivity) - else: - _rotate_camera(event.relative * mouse_sensitivity) - get_viewport().set_input_as_handled() - return if _free_camera_active: return diff --git a/project.godot b/project.godot index 334669c..c10c8c2 100644 --- a/project.godot +++ b/project.godot @@ -98,7 +98,6 @@ slow_walk={ interact={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) -, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null) ] } fish_primary={ @@ -197,6 +196,7 @@ open_emotes={ character_call={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null) ] } open_quick_actions={ diff --git a/settings/controller_mapping_manager.gd b/settings/controller_mapping_manager.gd index 7c54777..31df408 100644 --- a/settings/controller_mapping_manager.gd +++ b/settings/controller_mapping_manager.gd @@ -80,7 +80,7 @@ const ROLE_LABELS: Dictionary = { ROLE_A: "jump / menu accept", ROLE_B: "menu back", ROLE_X: "player menu", - ROLE_Y: "interact", + ROLE_Y: "character call", ROLE_LB: "focus chat or world", ROLE_RB: "primary action", ROLE_POINTER_MODIFIER: "virtual mouse modifier", @@ -138,7 +138,7 @@ const BUTTON_ACTION_ROLES: Dictionary = { &"ui_accept": ROLE_A, &"ui_cancel": ROLE_B, &"open_backpack": ROLE_X, - &"interact": ROLE_Y, + &"character_call": ROLE_Y, &"focus_gameplay": ROLE_LB, &"fish_primary": ROLE_RB, &"open_chat": ROLE_SELECT, diff --git a/tests/android_readiness_validation.gd b/tests/android_readiness_validation.gd index b383202..41bde86 100644 --- a/tests/android_readiness_validation.gd +++ b/tests/android_readiness_validation.gd @@ -20,7 +20,8 @@ func _init() -> void: assert(_has_joypad_button(&"jump", JOY_BUTTON_A)) assert(_has_joypad_button(&"ui_accept", JOY_BUTTON_A)) assert(_has_joypad_button(&"ui_cancel", JOY_BUTTON_B)) - assert(_has_joypad_button(&"interact", JOY_BUTTON_Y)) + assert(not _has_joypad_button(&"interact", JOY_BUTTON_Y)) + assert(_has_joypad_button(&"character_call", JOY_BUTTON_Y)) assert(_has_joypad_button(&"fish_primary", JOY_BUTTON_RIGHT_SHOULDER)) assert(not _has_joypad_motion( &"fish_primary", JOY_AXIS_TRIGGER_LEFT, 1.0 diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 6384d19..b4f4339 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -684,7 +684,26 @@ func _validate_pause_browser_transition( ) == Input.MOUSE_MODE_VISIBLE ) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE - player.call("_set_camera_dragging", true) + var drag_press := InputEventMouseButton.new() + drag_press.button_index = MOUSE_BUTTON_RIGHT + drag_press.button_mask = MOUSE_BUTTON_MASK_RIGHT + drag_press.pressed = true + player.call("_input", drag_press) + assert(bool(player.get("_camera_dragging"))) + var camera_yaw := player.get_node("%CameraYaw") as Node3D + var yaw_before_drag: float = camera_yaw.rotation.y + var drag_motion := InputEventMouseMotion.new() + drag_motion.button_mask = MOUSE_BUTTON_MASK_RIGHT + drag_motion.relative = Vector2(24.0, -8.0) + player.call("_input", drag_motion) + assert(not is_equal_approx(camera_yaw.rotation.y, yaw_before_drag)) + var drag_release := InputEventMouseButton.new() + drag_release.button_index = MOUSE_BUTTON_RIGHT + drag_release.button_mask = 0 + drag_release.pressed = false + player.call("_input", drag_release) + assert(not bool(player.get("_camera_dragging"))) + player.call("_input", drag_press) assert(bool(player.get("_camera_dragging"))) pause_menu.open_menu() while bool(pause_menu.get("_root_transition_active")): diff --git a/tests/controller_mapping_validation.gd b/tests/controller_mapping_validation.gd index 78e0ee8..e67224c 100644 --- a/tests/controller_mapping_validation.gd +++ b/tests/controller_mapping_validation.gd @@ -102,6 +102,20 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String: &"sneak", &"" ) != ControllerMappingManagerType.ROLE_RIGHT_STICK_CLICK: return "right-stick click is not assigned to the sneak action" + if ControllerMappingManagerType.ROLE_LABELS.get( + ControllerMappingManagerType.ROLE_Y, "" + ) != "character call": + return "Y is not presented as the character-call button" + if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get( + &"character_call", &"" + ) != ControllerMappingManagerType.ROLE_Y: + return "Y is not assigned to the character-call action" + if ControllerMappingManagerType.BUTTON_ACTION_ROLES.has(&"interact"): + return "Y still exposes the obsolete controller interact binding" + if not _has_joy_button(&"character_call", JOY_BUTTON_Y): + return "character call does not default to Y" + if _has_joy_button(&"interact", JOY_BUTTON_Y): + return "Y still defaults to interact" if not _has_joy_button(&"sneak", JOY_BUTTON_RIGHT_STICK): return "sneak does not default to right-stick click" var trigger_button := InputEventJoypadButton.new() @@ -221,17 +235,32 @@ func _validate_portmaster_launcher() -> String: "leftstick:b9", "rightstick:b12", "netfishing_controllerconfig=\"$GODOT_MUOS_MAPPING\"", - "$GPTOKEYB \"NETfishing.aarch64\" &", + "GPTOKEYB_CONFIG=\"$GAMEDIR/netfishing.gptk\"", + "$GPTOKEYB \"NETfishing.aarch64\" -c \"$GPTOKEYB_CONFIG\" &", "pm_platform_helper \"$GAME_EXECUTABLE\"", - "SDL_GAMECONTROLLERCONFIG=\"$netfishing_controllerconfig\" \\", + "\"$GAME_LAUNCHER\"", + "\"$CONTROLLER_MAPPING_FILE\"", "pm_finish", ]: 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" - if "$GPTOKEYB \"NETfishing.aarch64\" -c" in launcher: - return "PortMaster launcher injects duplicate GPTOKEYB gameplay mappings" + const game_launcher_path: String = ( + "res://scripts/portmaster/launch-netfishing.sh" + ) + if not FileAccess.file_exists(game_launcher_path): + return "PortMaster game launcher wrapper is missing" + var game_launcher: String = FileAccess.get_file_as_string( + game_launcher_path + ) + for expected_fragment: String in [ + "SDL_GAMECONTROLLERCONFIG=\"$(<\"$CONTROLLER_MAPPING_FILE\")\"", + "export SDL_GAMECONTROLLERCONFIG", + "exec \"$@\"", + ]: + if expected_fragment not in game_launcher: + return "PortMaster game launcher omitted " + expected_fragment return ""