Fix multiplayer parity and release regressions

This commit is contained in:
Alexander Sellite 2026-07-30 14:01:07 -04:00
parent b18d9392d8
commit c0ff78322d
17 changed files with 378 additions and 17 deletions

View file

@ -9,6 +9,7 @@ enum Status {
INVALID_BUYER,
INVALID_OFFER,
INVALID_SELECTION,
RESERVED,
TRANSACTION_FAILED,
}
@ -46,5 +47,7 @@ func get_message() -> String:
return "invalid buyer offer."
Status.INVALID_SELECTION:
return "the fish selection is invalid."
Status.RESERVED:
return "reserved fish cannot be sold."
_:
return "transaction failed."

View file

@ -11,14 +11,17 @@ signal sale_completed(result: FishSaleResultType)
var _inventory: FishInventoryType
var _wallet: PlayerWalletType
var _reservations: PlayerAssetReservationService
func setup(
inventory: FishInventoryType,
wallet: PlayerWalletType,
reservations: PlayerAssetReservationService = null,
) -> void:
_inventory = inventory
_wallet = wallet
_reservations = reservations
func can_sell(
@ -114,6 +117,12 @@ func _validate_batch(
if fish_catch == null:
result.status = FishSaleResultType.Status.NOT_FOUND
return result
if (
_reservations != null
and _reservations.is_fish_reserved(catch_id)
):
result.status = FishSaleResultType.Status.RESERVED
return result
if fish_catch.is_favorited:
contains_favorite = true
if fish_catch.sale_value < 0:

View file

@ -243,6 +243,9 @@ func show_reel_position(world_position: Vector3, _input_held: bool) -> void:
func play_outcome(outcome: StringName) -> void:
if _mode == VisualMode.NONE:
cleanup()
# Gameplay completion must not depend on a presentation tween still
# being active. The caller owns idempotency for the outcome.
outcome_completed.emit(outcome)
return
_kill_active_tween()

View file

@ -134,6 +134,7 @@ var _pending_catch: FishCatchType
var _showcase_ready: bool = false
var _put_away_press_armed: bool = false
var _showcase_restore_generation: int = 0
var _showcase_outcome_completed: bool = false
var _network_auto_click_accumulator: float = 0.0
var _network_active_barrier_index: int = -1
@ -874,6 +875,7 @@ func _on_catch_completed() -> void:
return
state = FishingState.SHOWING_CATCH
_showcase_ready = false
_showcase_outcome_completed = false
_put_away_press_armed = false
_catch_controller.reset()
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
@ -896,8 +898,10 @@ func _on_outcome_completed(outcome: StringName) -> void:
or state != FishingState.SHOWING_CATCH
or _active_player == null
or _pending_catch == null
or _showcase_outcome_completed
):
return
_showcase_outcome_completed = true
_showcase_ready = true
_active_player.begin_catch_showcase(_pending_catch)
showcase_changed.emit(
@ -923,6 +927,7 @@ func _put_away_catch() -> void:
_local_collection_log.mark_discovered(_pending_catch.fish_id)
_pending_catch = null
_showcase_ready = false
_showcase_outcome_completed = false
_put_away_press_armed = false
showcase_changed.emit("", "", 0.0, false)
_showcase_restore_generation += 1
@ -973,6 +978,7 @@ func _cleanup_attempt(
_selected_fish = null
_pending_catch = null
_showcase_ready = false
_showcase_outcome_completed = false
_put_away_press_armed = false
showcase_changed.emit("", "", 0.0, false)
_catch_controller.reset()
@ -1257,6 +1263,7 @@ func _on_network_catch_received(fish_catch: FishCatchType) -> void:
_pending_catch = fish_catch
state = FishingState.SHOWING_CATCH
_showcase_ready = false
_showcase_outcome_completed = false
_put_away_press_armed = false
catch_display_changed.emit(
0.0, 0.0, PackedFloat32Array(), PackedInt32Array(),

View file

@ -8,6 +8,13 @@ signal catches_changed
var _catches: Array[FishCatchType] = []
var _next_catch_sequence: int = 1
var _reservation_service: PlayerAssetReservationService
func set_reservation_service(
reservation_service: PlayerAssetReservationService,
) -> void:
_reservation_service = reservation_service
func add_catch(fish_catch: FishCatchType) -> void:
@ -101,6 +108,25 @@ func remove_catch_by_id(catch_id: StringName) -> FishCatchType:
func remove_catches_by_ids(
catch_ids: Array[StringName],
) -> Array[FishCatchType]:
return _remove_catches_by_ids(catch_ids, "", "")
func remove_reserved_catch_for_mail_transfer(
catch_id: StringName,
reservation_id: String,
transfer_id: String,
) -> FishCatchType:
var removed := _remove_catches_by_ids(
[catch_id], reservation_id, transfer_id
)
return removed.front() if removed.size() == 1 else null
func _remove_catches_by_ids(
catch_ids: Array[StringName],
mail_reservation_id: String,
mail_transfer_id: String,
) -> Array[FishCatchType]:
var removed: Array[FishCatchType] = []
if catch_ids.is_empty():
@ -115,6 +141,17 @@ func remove_catches_by_ids(
if fish_catch == null:
removed.clear()
return removed
if (
_reservation_service != null
and _reservation_service.is_fish_reserved(catch_id)
and not _reservation_service.authorize_mail_fish_removal(
mail_reservation_id,
catch_id,
mail_transfer_id,
)
):
removed.clear()
return removed
removed.append(fish_catch)
var remaining: Array[FishCatchType] = []

View file

@ -133,6 +133,7 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0
)
@onready var _players_root: Node3D = $Players
@onready var _title_background: ColorRect = %TitleBackground
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
var _gameplay_started: bool = false
var _shop_interaction: FishingShopInteractionType
@ -152,10 +153,13 @@ var _data_folder_picker_generation: int = 0
var _restore_data_setup_after_picker: bool = false
var _application_initialized := false
var _pending_existing_root_path := ""
var _local_recovery_attempt_id: String = ""
func _ready() -> void:
DisplayServer.window_set_title("NETfishing")
get_window().size_changed.connect(_resize_native_overlays)
_resize_native_overlays()
if not _settings_manager.settings_changed.is_connected(
_apply_runtime_settings
):
@ -254,11 +258,15 @@ func _initialize_after_data_root() -> void:
_network_session.remote_recovery_requested.connect(
_on_remote_recovery_requested
)
_network_session.remote_recovery_presentation_changed.connect(
_on_remote_recovery_presentation_changed
)
_player.fish_sale_service.setup(
_player.inventory,
_player.wallet
_player.wallet,
_asset_reservations
)
_player.bag.setup(item_catalog)
_player.hotbar.setup(_player.bag, item_catalog)
@ -282,6 +290,7 @@ func _initialize_after_data_root() -> void:
_asset_reservations.setup(
_player.wallet, _player.inventory, _player.bag, item_catalog
)
_player.inventory.set_reservation_service(_asset_reservations)
_network_mail.setup(
_network_session,
_asset_reservations,
@ -426,6 +435,9 @@ func _initialize_after_data_root() -> void:
_game_ui.interactive_pointer_ui_changed.connect(
_ui_pixelation.set_interactive_ui_open
)
_game_ui.player_menu_backdrop_visibility_changed.connect(
_set_player_menu_backdrop_visible
)
_pixelation_reset.return_to_settings_requested.connect(
_game_ui.focus_open_settings_back_button
)
@ -475,6 +487,9 @@ func _initialize_after_data_root() -> void:
_water_recovery.recovery_starting.connect(
_on_water_recovery_starting
)
_water_recovery.recovery_finished.connect(
_on_water_recovery_finished
)
_water_recovery.local_respawn_completed.connect(
_on_local_respawn_completed
)
@ -885,6 +900,19 @@ func _set_gameplay_active(active: bool) -> void:
_save_manager.set_autosave_enabled(active)
if active:
_refresh_active_hotbar_item()
else:
_set_player_menu_backdrop_visible(false)
func _set_player_menu_backdrop_visible(is_visible: bool) -> void:
_player_menu_backdrop.visible = is_visible and _gameplay_started
func _resize_native_overlays() -> void:
if not is_node_ready():
return
_player_menu_backdrop.position = Vector2.ZERO
_player_menu_backdrop.size = Vector2(get_window().size)
func _on_new_game_requested() -> void:
@ -1128,11 +1156,39 @@ func _on_reset_progress_requested() -> void:
func _on_water_recovery_starting() -> void:
_local_recovery_attempt_id = (
"recovery:%s"
% Crypto.new().generate_random_bytes(16).hex_encode()
)
_network_session.request_recovery_presentation(
true, _local_recovery_attempt_id
)
_game_ui.close_player_menu_for_water_recovery()
_game_ui.get_pause_menu().close_for_water_recovery()
_game_ui.get_fishing_shop().close_for_water_recovery()
func _on_water_recovery_finished() -> void:
if _local_recovery_attempt_id.is_empty():
return
_network_session.request_recovery_presentation(
false, _local_recovery_attempt_id
)
_local_recovery_attempt_id = ""
func _on_remote_recovery_presentation_changed(
peer_id: int,
active: bool,
_attempt_id: String,
) -> void:
var avatar: PlayerType = _player_spawn_service.get_avatar(peer_id)
if avatar == null or avatar == _player:
return
avatar.set_water_recovery_active(active)
avatar.set_remote_recovery_presentation(active)
func _on_active_hotbar_item_changed(
_slot_index: int,
item_id: StringName,

View file

@ -65,6 +65,13 @@ grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="PlayerMenuBackdrop" type="ColorRect" parent="."]
unique_name_in_owner = true
visible = false
z_index = 10
mouse_filter = 2
color = Color(0.015, 0.02, 0.03, 0.72)
[node name="InterfaceFontController" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("39_fonts")
@ -192,6 +199,7 @@ unique_name_in_owner = true
[node name="UIPresentation" type="SubViewportContainer" parent="."]
unique_name_in_owner = true
z_index = 20
texture_filter = 1
offset_right = 1152.0
offset_bottom = 648.0

View file

@ -552,7 +552,7 @@ func _commit_sender(transfer_id: String, letter: Dictionary) -> void:
var snapshot := _capture_assets()
var applied := (
_reservations.has_reservation(reservation_id)
and _reservations.commit_removal(reservation_id)
and _reservations.commit_removal(reservation_id, transfer_id)
and _save_manager.save_if_dirty()
)
if not applied:

View file

@ -32,6 +32,11 @@ signal server_trust_required(
signal peer_identity_observed(peer_id: int, status: String)
signal server_lost
signal remote_recovery_requested(peer_id: int, entry_position: Vector3)
signal remote_recovery_presentation_changed(
peer_id: int,
active: bool,
attempt_id: String,
)
enum State {
INACTIVE,
@ -84,6 +89,7 @@ var _pending_identity_challenges: Dictionary[int, Dictionary] = {}
var _authenticated_identity_cache: Dictionary[int, Dictionary] = {}
var _client_identity_attempt: Dictionary = {}
var _pending_server_proof: Dictionary = {}
var _recovery_attempts: Dictionary[int, String] = {}
var _server_identity_fingerprint: String = ""
var _server_identity_public_key: String = ""
var _session_identity_keys: Dictionary[String, String] = {}
@ -668,6 +674,12 @@ func _on_peer_disconnected(peer_id: int) -> void:
_pending_authentication.erase(peer_id)
_pending_identity_challenges.erase(peer_id)
_authenticated_identity_cache.erase(peer_id)
var recovery_attempt: String = _recovery_attempts.get(peer_id, "")
if not recovery_attempt.is_empty():
_recovery_attempts.erase(peer_id)
remote_recovery_presentation_changed.emit(
peer_id, false, recovery_attempt
)
if _registry.has_peer(peer_id):
_registry.remove_peer(peer_id)
_spawn_service.remove_peer(peer_id)
@ -1320,6 +1332,12 @@ func _send_local_input() -> void:
submit_movement_input.rpc_id(1, input)
func submit_neutral_local_movement() -> void:
if state != State.JOINED_CLIENT:
return
_send_local_input()
@rpc("any_peer", "call_remote", "unreliable_ordered", 1)
func submit_movement_input(data: Dictionary) -> void:
var sender_id: int = multiplayer.get_remote_sender_id()
@ -1400,6 +1418,99 @@ func publish_authoritative_teleport(peer_id: int) -> void:
)
func request_recovery_presentation(
active: bool,
attempt_id: String,
) -> void:
if (
attempt_id.is_empty()
or attempt_id.length() > 96
or not is_gameplay_session_active()
):
return
if is_host():
_set_authoritative_recovery_presentation(
get_local_peer_id(), active, attempt_id
)
elif state == State.JOINED_CLIENT:
submit_recovery_presentation.rpc_id(
1, _session_id, active, attempt_id
)
@rpc("any_peer", "call_remote", "reliable", 0)
func submit_recovery_presentation(
session_id: String,
active: bool,
attempt_id: String,
) -> void:
var sender_id: int = multiplayer.get_remote_sender_id()
if (
not is_host()
or not _registry.has_peer(sender_id)
or session_id != _session_id
or attempt_id.is_empty()
or attempt_id.length() > 96
):
return
_set_authoritative_recovery_presentation(sender_id, active, attempt_id)
func _set_authoritative_recovery_presentation(
peer_id: int,
active: bool,
attempt_id: String,
) -> void:
var current_attempt: String = _recovery_attempts.get(peer_id, "")
if active:
if not current_attempt.is_empty() and current_attempt != attempt_id:
return
elif current_attempt != attempt_id:
return
var data := {
"session_id": _session_id,
"generation": _operation_generation,
"peer_id": peer_id,
"active": active,
"attempt_id": attempt_id,
}
_apply_recovery_presentation(data)
receive_recovery_presentation.rpc(data)
@rpc("authority", "call_remote", "reliable", 0)
func receive_recovery_presentation(data: Dictionary) -> void:
_apply_recovery_presentation(data)
func _apply_recovery_presentation(data: Dictionary) -> void:
if (
typeof(data.get("session_id")) != TYPE_STRING
or str(data["session_id"]) != _session_id
or typeof(data.get("generation")) != TYPE_INT
or int(data["generation"]) < 1
or typeof(data.get("peer_id")) != TYPE_INT
or typeof(data.get("active")) != TYPE_BOOL
or typeof(data.get("attempt_id")) != TYPE_STRING
or str(data["attempt_id"]).is_empty()
or str(data["attempt_id"]).length() > 96
):
return
var peer_id: int = data["peer_id"]
var attempt_id: String = data["attempt_id"]
var active: bool = data["active"]
var current_attempt: String = _recovery_attempts.get(peer_id, "")
if active:
if not current_attempt.is_empty() and current_attempt != attempt_id:
return
_recovery_attempts[peer_id] = attempt_id
elif current_attempt != attempt_id:
return
else:
_recovery_attempts.erase(peer_id)
remote_recovery_presentation_changed.emit(peer_id, active, attempt_id)
func request_safe_respawn(entry_position: Vector3) -> void:
if not entry_position.is_finite():
return
@ -1535,6 +1646,11 @@ func _fail(message: String) -> void:
func _teardown_peer() -> void:
for peer_id: int in _recovery_attempts.keys():
remote_recovery_presentation_changed.emit(
peer_id, false, _recovery_attempts[peer_id]
)
_recovery_attempts.clear()
_pending_authentication.clear()
_pending_identity_challenges.clear()
_authenticated_identity_cache.clear()

View file

@ -105,6 +105,9 @@ var _camera_dragging: bool = false
var _camera_input_enabled: bool = true
var _movement_enabled: bool = true
var _water_recovery_active: bool = false
var _remote_recovery_presentation_active: bool = false
var _remote_recovery_visual_origin: Vector3
var _remote_recovery_elapsed: float = 0.0
var _target_zoom: float = 5.0
var _showcase_rod_visibility: bool = true
var _remote_presentation_visible := true
@ -212,6 +215,14 @@ func _physics_process(delta: float) -> void:
func _process(delta: float) -> void:
if _remote_recovery_presentation_active:
_remote_recovery_elapsed += delta
_visuals.position = (
_remote_recovery_visual_origin
+ Vector3.UP
* sin(_remote_recovery_elapsed * 2.0 * TAU)
* 0.08
)
if not local_control_enabled:
return
@ -338,6 +349,16 @@ func configure_network_remote(authoritative_simulation: bool) -> void:
func capture_network_input(sequence: int) -> Dictionary:
if not _movement_enabled or _water_recovery_active:
return {
"sequence": sequence,
"axis": [0.0, 0.0],
"camera_yaw": _camera_yaw.global_rotation.y,
"jump": false,
"sprint": false,
"sneak": false,
"slow_walk": false,
}
var axis: Vector2 = Input.get_vector(
"move_left",
"move_right",
@ -525,6 +546,17 @@ func is_water_recovery_active() -> bool:
return _water_recovery_active
func set_remote_recovery_presentation(active: bool) -> void:
if local_control_enabled or active == _remote_recovery_presentation_active:
return
_remote_recovery_presentation_active = active
_remote_recovery_elapsed = 0.0
if active:
_remote_recovery_visual_origin = _visuals.position
else:
_visuals.position = _remote_recovery_visual_origin
func prepare_for_water_recovery() -> void:
_restore_gameplay_presentation_for_recovery()

View file

@ -10,6 +10,7 @@ var _inventory: FishInventory
var _bag: PlayerBag
var _catalog: ItemCatalog
var _reservations: Dictionary[String, Dictionary] = {}
var _mail_transfer_authorizations: Dictionary[String, Dictionary] = {}
func setup(
@ -63,6 +64,7 @@ func reserve(reservation_id: String, attachment: Dictionary) -> bool:
func release(reservation_id: String) -> bool:
if not _reservations.erase(reservation_id):
return false
_mail_transfer_authorizations.erase(reservation_id)
reservations_changed.emit()
return true
@ -71,9 +73,46 @@ func release_all() -> void:
if _reservations.is_empty():
return
_reservations.clear()
_mail_transfer_authorizations.clear()
reservations_changed.emit()
func authorize_mail_transfer(
reservation_id: String,
catch_id: StringName,
transfer_id: String,
) -> bool:
var attachment := get_reservation(reservation_id)
if (
transfer_id.is_empty()
or transfer_id.length() > 96
or attachment.is_empty()
or int(attachment.get("type", 0)) != AttachmentType.FISH
or StringName(str(attachment.get("catch_id", ""))) != catch_id
):
return false
_mail_transfer_authorizations[reservation_id] = {
"catch_id": catch_id,
"transfer_id": transfer_id,
}
return true
func authorize_mail_fish_removal(
reservation_id: String,
catch_id: StringName,
transfer_id: String,
) -> bool:
var authorization: Dictionary = _mail_transfer_authorizations.get(
reservation_id, {}
)
return (
not authorization.is_empty()
and StringName(authorization.get("catch_id", StringName())) == catch_id
and str(authorization.get("transfer_id", "")) == transfer_id
)
func has_reservation(reservation_id: String) -> bool:
return _reservations.has(reservation_id)
@ -138,7 +177,10 @@ func can_commit(reservation_id: String) -> bool:
return false
func commit_removal(reservation_id: String) -> bool:
func commit_removal(
reservation_id: String,
mail_transfer_id: String = "",
) -> bool:
var attachment := get_reservation(reservation_id)
if attachment.is_empty() or not can_commit(reservation_id):
return false
@ -147,9 +189,16 @@ func commit_removal(reservation_id: String) -> bool:
AttachmentType.FISH_COIN:
applied = _wallet.debit(int(attachment["amount"]))
AttachmentType.FISH:
applied = _inventory.remove_catch_by_id(
StringName(str(attachment["catch_id"]))
) != null
var catch_id := StringName(str(attachment["catch_id"]))
if authorize_mail_transfer(
reservation_id, catch_id, mail_transfer_id
):
applied = (
_inventory.remove_reserved_catch_for_mail_transfer(
catch_id, reservation_id, mail_transfer_id
)
!= null
)
AttachmentType.CONSUMABLE:
applied = _bag.remove_item(
StringName(str(attachment["item_id"])),
@ -157,6 +206,8 @@ func commit_removal(reservation_id: String) -> bool:
)
if applied:
release(reservation_id)
else:
_mail_transfer_authorizations.erase(reservation_id)
return applied

View file

@ -60,6 +60,9 @@ func open_chat() -> void:
_player.set_movement_enabled(false)
_player.set_camera_input_enabled(false)
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
# Send the host a zeroed frame immediately so it cannot keep applying the
# last movement state while this LineEdit owns keyboard input.
_session.submit_neutral_local_movement()
_panel.show()
_entry.show()
_entry.grab_focus()

View file

@ -39,6 +39,9 @@ enum CloseReason {
}
@onready var _wallet_label: Label = %WalletLabel
@onready var _fish_sales: VBoxContainer = (
$ShopPanel/Margin/Layout/Body/FishSales
)
@onready var _fish_list: ItemList = %FishList
@onready var _sales_title: Label = %SalesTitle
@onready var _fish_empty: Label = %FishEmpty
@ -94,6 +97,9 @@ var _closing: bool = false
func _ready() -> void:
# Catch selling belongs to the Pelican action in the Player Menu. Keep the
# legacy shop sales controls out of the active interface.
_fish_sales.hide()
%CloseButton.pressed.connect(close_shop)
_fish_list.item_selected.connect(_on_fish_selected)
_fish_list.item_clicked.connect(_on_fish_clicked)
@ -425,8 +431,15 @@ func _refresh_upgrades() -> void:
reel_cost < 0
or _transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_purchase()
or not _upgrades.can_purchase_reel_speed(_wallet)
)
_reel_purchase.tooltip_text = (
"Purchases are unavailable in this session."
if _network_shop == null or not _network_shop.can_request_purchase()
else ""
)
if reel_cost < 0:
_reel_effect.text = "%.2f×" % _upgrades.get_reel_speed_multiplier()
_reel_cost.text = "max"
@ -447,8 +460,11 @@ func _refresh_upgrades() -> void:
barrier_cost < 0
or _transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_purchase()
or not _upgrades.can_purchase_barrier_power(_wallet)
)
_barrier_purchase.tooltip_text = _reel_purchase.tooltip_text
if barrier_cost < 0:
_barrier_effect.text = "%d damage" % _upgrades.get_barrier_damage()
_barrier_cost.text = "max"
@ -488,11 +504,18 @@ func _refresh_supplies() -> void:
button.disabled = (
_transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_purchase()
or not _bag.can_add_item(item_id, 1)
or not _wallet.can_afford(
FishingShopStockType.get_price(item_id)
)
)
button.tooltip_text = (
"Purchases are unavailable in this session."
if _network_shop == null or not _network_shop.can_request_purchase()
else item.description
)
button.pressed.connect(_purchase_supply.bind(item_id))
_supplies_list.add_child(button)
@ -508,8 +531,11 @@ func _refresh_cooler_capacity() -> void:
cost < 0
or _transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_purchase()
or not _cooler_capacity.can_purchase(_wallet)
)
_cooler_purchase.tooltip_text = _reel_purchase.tooltip_text
if cost < 0:
_cooler_effect.text = "%d fish" % _cooler_capacity.get_capacity()
_cooler_cost.text = "max"

View file

@ -36,6 +36,7 @@ const ChatUIType = preload("res://ui/chat_ui.gd")
signal pixelation_settings_visibility_changed(is_visible: bool)
signal crisp_reset_focus_requested
signal interactive_pointer_ui_changed(is_open: bool)
signal player_menu_backdrop_visibility_changed(is_visible: bool)
@onready var _status_label: Label = %StatusLabel
@onready var _catch_track: Control = %CatchTrack
@ -490,6 +491,7 @@ func _on_showcase_changed(
func _on_player_menu_visibility_changed(is_open: bool) -> void:
_player_menu_open = is_open
player_menu_backdrop_visibility_changed.emit(is_open)
_refresh_chat_availability()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled and not is_open

View file

@ -229,6 +229,7 @@ var _sale_service: FishSaleServiceType
var _network_session: NetworkSessionType
var _network_sale_service: NetworkSaleService
var _network_mail_service: NetworkMailService
var _reservations: PlayerAssetReservationService
var _network_profile_service: NetworkProfileService
var _network_player_list: NetworkPlayerListService
var _default_buyer: FishBuyerProfileType
@ -389,6 +390,7 @@ func setup(
_network_session = network_session
_network_sale_service = network_sale_service
_network_mail_service = network_mail_service
_reservations = reservations
_network_profile_service = network_profile_service
_network_player_list = network_player_list
_mail_page.setup(
@ -2436,6 +2438,11 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
else -1
)
_cooler_weight_value.text = "%.2f" % fish_catch.weight_lb
if (
_reservations != null
and _reservations.is_fish_reserved(fish_catch.catch_id)
):
_cooler_detail_name.text += " • reserved in mail"
_cooler_weight_unit.text = "lb"
if buyer_offer >= 0 and _default_buyer != null:
_cooler_offer_label.text = "%s offer" % _default_buyer.display_name
@ -2734,6 +2741,16 @@ func _on_network_sale_finished(
)
_refresh_all()
if _current_section == Section.COOLER:
call_deferred("_restore_inventory_tab_focus")
func _restore_inventory_tab_focus() -> void:
if (
visible
and _current_section == Section.COOLER
and _inventory_tab.focus_mode != Control.FOCUS_NONE
and _inventory_tab.is_visible_in_tree()
):
_inventory_tab.grab_focus()

View file

@ -44,17 +44,6 @@ mouse_filter = 2
theme = ExtResource("2_theme")
script = ExtResource("1_menu")
[node name="Dimmer" type="ColorRect" parent="."]
z_index = -20
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.015, 0.02, 0.03, 0.72)
[node name="ResponsivePlayerMenuStage" type="Control" parent="."]
layout_mode = 1
anchors_preset = 15

View file

@ -2,6 +2,7 @@ class_name WaterRecoveryController
extends Node
signal recovery_starting
signal recovery_finished
signal local_respawn_completed(entry_position: Vector3)
enum RecoveryState {
@ -177,3 +178,4 @@ func _finish_recovery() -> void:
_fishing_spot.end_water_recovery()
state = RecoveryState.IDLE
_bob_elapsed = 0.0
recovery_finished.emit()