From bb1942a057cecf779c9ae451cfbc24bcc5c56fe5 Mon Sep 17 00:00:00 2001 From: Voyager Date: Fri, 21 Aug 2026 21:33:02 -0400 Subject: [PATCH] Stabilize camera drag and Tab menu input --- player/player.gd | 47 ++++++------------- tests/camera_drag_validation.gd | 15 +++--- ...ontroller_focus_presentation_validation.gd | 13 +++++ ui/controller_focus_presentation.gd | 6 +++ 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/player/player.gd b/player/player.gd index d6281b3..680350c 100644 --- a/player/player.gd +++ b/player/player.gd @@ -466,11 +466,10 @@ var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04) var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) var _camera_dragging: bool = false -var _camera_drag_button_held: bool = false var _camera_input_enabled: bool = true var _camera_drag_prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE -var _camera_drag_capture_pending: bool = false -var _camera_drag_generation: int = 0 +var _camera_drag_pointer_origin: Vector2 = Vector2.ZERO +var _camera_drag_restore_pointer: bool = false var _free_camera_active: bool = false var _free_camera_body: CharacterBody3D var _free_camera: Camera3D @@ -1616,7 +1615,6 @@ func _input(event: InputEvent) -> void: return var mouse_button := event as InputEventMouseButton if mouse_button != null and event.is_action("camera_drag"): - _camera_drag_button_held = mouse_button.pressed _set_camera_dragging(mouse_button.pressed) return var mouse_motion := event as InputEventMouseMotion @@ -1626,7 +1624,6 @@ func _input(event: InputEvent) -> void: _rotate_free_camera(mouse_motion.screen_relative * mouse_sensitivity) else: _rotate_camera(mouse_motion.screen_relative * mouse_sensitivity) - _request_camera_drag_capture() func _notification(what: int) -> void: @@ -1689,38 +1686,24 @@ func _set_camera_dragging(active: bool) -> void: if _camera_dragging == active: return _camera_dragging = active - _camera_drag_generation += 1 - _camera_drag_capture_pending = false if active: _camera_drag_prior_mouse_mode = Input.mouse_mode + _camera_drag_restore_pointer = _camera_drag_prior_mouse_mode in [ + Input.MOUSE_MODE_VISIBLE, + Input.MOUSE_MODE_CONFINED, + ] + if _camera_drag_restore_pointer: + _camera_drag_pointer_origin = get_viewport().get_mouse_position() + # Capture starts with the physical press, before any motion is applied. + # Switching modes after the first movement can synthesize a center warp + # in the middle of the drag and interrupt camera control. + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED else: - _camera_drag_button_held = false if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: Input.mouse_mode = _camera_drag_prior_mouse_mode - - -func _request_camera_drag_capture() -> void: - if ( - _camera_drag_capture_pending - or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED - ): - return - _camera_drag_capture_pending = true - _capture_camera_drag_pointer.call_deferred(_camera_drag_generation) - - -func _capture_camera_drag_pointer(generation: int) -> void: - if generation != _camera_drag_generation: - return - _camera_drag_capture_pending = false - if ( - not _camera_dragging - or not local_control_enabled - or not _is_camera_input_enabled() - or not _camera_drag_button_held - ): - return - Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + if _camera_drag_restore_pointer: + Input.warp_mouse(_camera_drag_pointer_origin) + _camera_drag_restore_pointer = false func _get_network_aware_speed() -> float: diff --git a/tests/camera_drag_validation.gd b/tests/camera_drag_validation.gd index a64f5ac..0299492 100644 --- a/tests/camera_drag_validation.gd +++ b/tests/camera_drag_validation.gd @@ -20,10 +20,11 @@ func _run() -> void: press.button_index = MOUSE_BUTTON_RIGHT press.button_mask = MOUSE_BUTTON_MASK_RIGHT press.pressed = true - player.call("_input", press) + Input.parse_input_event(press) + await process_frame assert(bool(player.get("_camera_dragging"))) if DisplayServer.get_name() != "headless": - assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE) + assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED) var yaw := player.get_node("%CameraYaw") as Node3D var previous_yaw: float = yaw.rotation.y @@ -31,9 +32,9 @@ func _run() -> void: motion.button_mask = MOUSE_BUTTON_MASK_RIGHT motion.relative = Vector2(18.0, -4.0) motion.screen_relative = motion.relative - player.call("_input", motion) - assert(not is_equal_approx(yaw.rotation.y, previous_yaw)) + Input.parse_input_event(motion) await process_frame + assert(not is_equal_approx(yaw.rotation.y, previous_yaw)) assert(bool(player.get("_camera_dragging"))) if DisplayServer.get_name() != "headless": assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED) @@ -41,12 +42,14 @@ func _run() -> void: var release := InputEventMouseButton.new() release.button_index = MOUSE_BUTTON_RIGHT release.pressed = false - player.call("_input", release) + Input.parse_input_event(release) + await process_frame assert(not bool(player.get("_camera_dragging"))) if DisplayServer.get_name() != "headless": assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE) - player.call("_input", press) + Input.parse_input_event(press) + await process_frame assert(bool(player.get("_camera_dragging"))) player.notification(NOTIFICATION_APPLICATION_FOCUS_OUT) assert(not bool(player.get("_camera_dragging"))) diff --git a/tests/controller_focus_presentation_validation.gd b/tests/controller_focus_presentation_validation.gd index 28ce0b4..7f59f70 100644 --- a/tests/controller_focus_presentation_validation.gd +++ b/tests/controller_focus_presentation_validation.gd @@ -52,6 +52,19 @@ func _run() -> void: presentation.get("_neutral_focus_seed") == standard_button, "the neutral menu focus seed did not remember the first action", ) + var player_menu_tab := InputEventKey.new() + player_menu_tab.physical_keycode = KEY_TAB + player_menu_tab.pressed = true + presentation._input(player_menu_tab) + await process_frame + _expect( + root.gui_get_focus_owner() == null, + "the default player-menu Tab was consumed as focus navigation", + ) + _expect( + presentation.get("_neutral_focus_seed") == standard_button, + "the default player-menu Tab discarded the neutral focus seed", + ) var first_navigation := InputEventJoypadButton.new() first_navigation.button_index = JOY_BUTTON_DPAD_DOWN first_navigation.pressed = true diff --git a/ui/controller_focus_presentation.gd b/ui/controller_focus_presentation.gd index aed03ef..405b834 100644 --- a/ui/controller_focus_presentation.gd +++ b/ui/controller_focus_presentation.gd @@ -245,6 +245,12 @@ func _clear_directional_input_in_flight(generation: int) -> void: func _is_directional_navigation(event: InputEvent) -> bool: if event is InputEventKey and (event as InputEventKey).echo: return false + # Tab is both Godot's default ui_focus_next key and NETfishing's default + # player-menu binding. The global focus presenter must leave that gameplay + # action alone; otherwise a remembered neutral focus seed consumes the key + # before PlayerMenu can open or close. + if event is InputEventKey and event.is_action_pressed(&"open_backpack"): + return false return ( event.is_action_pressed(&"ui_left") or event.is_action_pressed(&"ui_right")