diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 43770c3..b8a0bdd 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -385,6 +385,24 @@ func set_gameplay_input_enabled(enabled: bool) -> void: if not enabled: _local_menu_input_owners.clear() _stop_reeling_audio() + # FishingSpot survives session and gameplay transitions. Restore the + # player's canonical movement state before input is disabled so a cast + # lock cannot follow that player into the next session. + reset_transient_state() + + +func reset_transient_state(restore_movement: bool = true) -> void: + var local_state_was_active: bool = ( + state != FishingState.READY or _active_player != null + ) + if local_state_was_active and _active_player == null and _local_player != null: + _local_player.set_fighting_visual(false) + _local_player.set_fishing_visual(false) + _local_player.end_catch_showcase(Callable(), true) + if restore_movement: + _local_player.set_movement_enabled(true) + _finalize_attempt_cleanup("", restore_movement) + _new_cast_press_armed = not Input.is_action_pressed("fish_primary") func configure_accessibility_auto_click( @@ -1339,13 +1357,17 @@ func _cleanup_attempt( _finalize_attempt_cleanup(cooldown_message) -func _finalize_attempt_cleanup(cooldown_message: String) -> void: +func _finalize_attempt_cleanup( + cooldown_message: String, + restore_movement: bool = true, +) -> void: _showcase_restore_generation += 1 if _active_player != null: _active_player.set_fighting_visual(false) _active_player.set_fishing_visual(false) _active_player.end_catch_showcase() - _active_player.set_movement_enabled(true) + if restore_movement: + _active_player.set_movement_enabled(true) _active_player = null _state_time_remaining = 0.0 _cast_charge = 0.0 @@ -1839,6 +1861,7 @@ func _on_network_session_state_changed(state_value: NetworkSession.State) -> voi ]: _stop_fight_audio() _stop_reeling_audio() + reset_transient_state() func _start_fight_audio() -> void: diff --git a/tests/fishing_authority_validation.gd b/tests/fishing_authority_validation.gd index 6b0429d..c06ab36 100644 --- a/tests/fishing_authority_validation.gd +++ b/tests/fishing_authority_validation.gd @@ -176,6 +176,31 @@ func _run() -> void: assert(fishing_status.text.is_empty()) assert(not fishing_status.visible) assert(not fishing_panel.visible) + var reset_test_deadline: int = Time.get_ticks_msec() + 3000 + while ( + Time.get_ticks_msec() < reset_test_deadline + and fishing_spot.state != FishingSpotType.FishingState.READY + ): + await process_frame + assert(fishing_spot.state == FishingSpotType.FishingState.READY) + + # Leaving a session during the cast presentation must release every local + # action and equipment lock. This is the same cleanup path used by an + # in-game server switch or a lost connection. + fishing_spot.call("_begin_aiming", player) + fishing_spot.set("_cast_charge", 0.32) + fishing_spot.call("_update_cast_charge", 0.0) + fishing_spot.call("_confirm_cast") + assert(fishing_spot.state == FishingSpotType.FishingState.CASTING) + assert(not player.is_movement_enabled()) + fishing_spot.call( + "_on_network_session_state_changed", + NetworkSession.State.DISCONNECTING, + ) + assert(fishing_spot.state == FishingSpotType.FishingState.READY) + assert(player.is_movement_enabled()) + assert(fishing_spot.can_change_hotbar_selection()) + assert(fishing_spot.can_open_fishing_shop()) print("Fishing authority validation: PASS") session.disconnect_session("") diff --git a/tests/fishing_multiplayer_validation.gd b/tests/fishing_multiplayer_validation.gd index c40e492..e19a92e 100644 --- a/tests/fishing_multiplayer_validation.gd +++ b/tests/fishing_multiplayer_validation.gd @@ -146,6 +146,17 @@ func _run_host() -> void: ): await process_frame assert(remote_animation_player.current_animation == &"retract_sit") + var retract_completion_deadline: int = Time.get_ticks_msec() + 5000 + while ( + Time.get_ticks_msec() < retract_completion_deadline + and not remote_avatar.is_retract_visual_complete() + ): + await process_frame + assert(remote_avatar.is_retract_visual_complete()) + # Give the client that owns the same presentation a frame boundary to finish + # local cleanup before the host intentionally tears down this test session. + for _frame: int in 4: + await physics_frame print("Fishing multiplayer host validation: PASS") session.disconnect_session("")