From 985fe1c28fe1db48102e6553939f1cd92a3bfc8b Mon Sep 17 00:00:00 2001 From: Voyager Date: Thu, 30 Jul 2026 14:36:48 -0400 Subject: [PATCH] Fix moderation and mail session behavior --- network/network_chat_service.gd | 21 +-- network/network_mail_service.gd | 160 ++++++++++++++++----- network/player_relationship_store.gd | 6 +- ui/mail_page.gd | 207 ++++++++++++++++++++++----- ui/player_menu.gd | 17 +++ 5 files changed, 329 insertions(+), 82 deletions(-) diff --git a/network/network_chat_service.gd b/network/network_chat_service.gd index 36f26b6..5920a0d 100644 --- a/network/network_chat_service.gd +++ b/network/network_chat_service.gd @@ -210,11 +210,18 @@ func _apply_message(data: Dictionary) -> void: if not valid_signature: return _seen_messages[data["message_id"]] = true - _history.append(data.duplicate(true)) + var stored_message := data.duplicate(true) + # Suppression is immutable for this received copy. Relationship changes + # later must never resurrect text that was hidden on arrival. + stored_message["locally_suppressed"] = ( + kind == NetworkChatProtocol.Kind.PLAYER + and is_sender_filtered(str(data.get("sender_fingerprint", ""))) + ) + _history.append(stored_message) while _history.size() > NetworkChatProtocol.MAX_HISTORY: _history.pop_front() - if _message_is_visible(data): - message_received.emit(data.duplicate(true)) + if _message_is_visible(stored_message): + message_received.emit(stored_message.duplicate(true)) func _send_rejection(peer_id: int, message: String) -> void: @@ -265,13 +272,11 @@ func receive_chat_history(values: Array) -> void: func _message_is_visible(message: Dictionary) -> bool: + if bool(message.get("locally_suppressed", false)): + return false if int(message.get("kind", -1)) == NetworkChatProtocol.Kind.SYSTEM: return true - if _relationships == null: - return true - return not _relationships.is_muted( - str(message.get("sender_fingerprint", "")) - ) + return true func _consume_rate(peer_id: int) -> bool: diff --git a/network/network_mail_service.gd b/network/network_mail_service.gd index 4d122b6..04893d1 100644 --- a/network/network_mail_service.gd +++ b/network/network_mail_service.gd @@ -29,6 +29,8 @@ var _pending_transfers: Dictionary[String, Dictionary] = {} var _local_removal_snapshots: Dictionary[String, Dictionary] = {} var _received_awards: Dictionary[String, bool] = {} var _relationship_policy: NetworkPlayerListService +var _archived_local_letters: Dictionary[String, bool] = {} +var _deleted_local_letters: Dictionary[String, bool] = {} func setup( @@ -72,11 +74,16 @@ func refresh_relationship_filters() -> void: peers_changed.emit() -func get_local_letters() -> Array[Dictionary]: +func get_local_letters(archived: bool = false) -> Array[Dictionary]: var values: Array[Dictionary] = [] for letter: Dictionary in _local_letters.values(): if ( - int(letter["recipient_peer_id"]) == _session.get_local_peer_id() + _session.get_local_peer_id() in [ + int(letter["sender_peer_id"]), + int(letter["recipient_peer_id"]), + ] + and not _deleted_local_letters.has(str(letter["mail_id"])) + and _archived_local_letters.has(str(letter["mail_id"])) == archived and not _letter_is_locally_blocked(letter) ): values.append(letter.duplicate(true)) @@ -91,15 +98,44 @@ func get_local_letters() -> Array[Dictionary]: func get_letter(mail_id: String) -> Dictionary: + if _deleted_local_letters.has(mail_id): + return {} return _local_letters.get(mail_id, {}).duplicate(true) +func archive_local_letter(mail_id: String, archived: bool = true) -> bool: + if not _local_letters.has(mail_id) or _deleted_local_letters.has(mail_id): + return false + if archived: + _archived_local_letters[mail_id] = true + else: + _archived_local_letters.erase(mail_id) + _emit_mailbox() + return true + + +func delete_local_letter(mail_id: String) -> bool: + var letter: Dictionary = _local_letters.get(mail_id, {}) + if letter.is_empty() or _incoming_gift_is_unresolved(letter): + return false + _deleted_local_letters[mail_id] = true + _archived_local_letters.erase(mail_id) + _emit_mailbox() + return true + + +func is_letter_archived(mail_id: String) -> bool: + return _archived_local_letters.has(mail_id) + + func get_unread_count() -> int: var count := 0 for letter: Dictionary in _local_letters.values(): if ( int(letter["recipient_peer_id"]) == _session.get_local_peer_id() and int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD + and not _archived_local_letters.has(str(letter["mail_id"])) + and not _deleted_local_letters.has(str(letter["mail_id"])) and not _letter_is_locally_blocked(letter) ): count += 1 @@ -260,16 +296,19 @@ func _handle_send(sender: int, data: Dictionary) -> void: ) ): error = "Letter identity could not be verified." - if ( + var silently_dropped := ( error.is_empty() and _relationship_policy != null and _relationship_policy.pair_is_blocked( sender_record.identity_fingerprint, recipient_record.identity_fingerprint, ) + ) + if ( + error.is_empty() + and not silently_dropped + and _letters.size() >= NetworkMailProtocol.MAX_SESSION_LETTERS ): - error = "That player is unavailable." - if error.is_empty() and _letters.size() >= NetworkMailProtocol.MAX_SESSION_LETTERS: error = "The session mailbox is full." var recipient_count := 0 for letter: Dictionary in _letters.values(): @@ -277,6 +316,7 @@ func _handle_send(sender: int, data: Dictionary) -> void: recipient_count += 1 if ( error.is_empty() + and not silently_dropped and recipient_count >= NetworkMailProtocol.MAX_RECIPIENT_LETTERS ): error = "That inbox is full." @@ -288,29 +328,21 @@ func _handle_send(sender: int, data: Dictionary) -> void: "accepted": error.is_empty(), "message": "Letter sent." if error.is_empty() else error, "reservation_id": str(data.get("reservation_id", "")), + "silent_drop": silently_dropped, } - if error.is_empty(): + if error.is_empty() and silently_dropped: _sequence += 1 - var letter := { - "mail_id": _new_id("mail"), - "session_id": _session.get_session_id(), - "sequence": _sequence, - "sender_peer_id": sender, - "sender_display_name": sender_record.display_name, - "recipient_peer_id": recipient, - "recipient_display_name": recipient_record.display_name, - "greeting_id": str(data["greeting_id"]), - "body": NetworkMailProtocol.sanitize_body(data["body"]), - "salutation_id": str(data["salutation_id"]), - "reservation_id": str(data["reservation_id"]), - "attachment": attachment.duplicate(true), - "state": NetworkMailProtocol.State.SENT_UNREAD, - "request_id": request_id, - "sender_fingerprint": data["sender_fingerprint"], - "recipient_fingerprint": data["recipient_fingerprint"], - "sender_signature": data["sender_signature"], - "sender_public_key": sender_record.identity_public_key, - } + var sender_copy := _build_letter( + data, sender, recipient, sender_record, recipient_record, _sequence + ) + result["mail"] = sender_copy + _deliver_private_letter(sender, sender_copy) + _request_release(sender, str(data.get("reservation_id", ""))) + elif error.is_empty(): + _sequence += 1 + var letter := _build_letter( + data, sender, recipient, sender_record, recipient_record, _sequence + ) _letters[letter["mail_id"]] = letter result["mail"] = letter _deliver_private_letter(sender, letter) @@ -321,6 +353,36 @@ func _handle_send(sender: int, data: Dictionary) -> void: _deliver_send_result(sender, result) +func _build_letter( + data: Dictionary, + sender: int, + recipient: int, + sender_record: PeerRegistry.PeerRecord, + recipient_record: PeerRegistry.PeerRecord, + sequence: int, +) -> Dictionary: + return { + "mail_id": _new_id("mail"), + "session_id": _session.get_session_id(), + "sequence": sequence, + "sender_peer_id": sender, + "sender_display_name": sender_record.display_name, + "recipient_peer_id": recipient, + "recipient_display_name": recipient_record.display_name, + "greeting_id": str(data["greeting_id"]), + "body": NetworkMailProtocol.sanitize_body(data["body"]), + "salutation_id": str(data["salutation_id"]), + "reservation_id": str(data["reservation_id"]), + "attachment": (data.get("attachment", {}) as Dictionary).duplicate(true), + "state": NetworkMailProtocol.State.SENT_UNREAD, + "request_id": str(data["request_id"]), + "sender_fingerprint": data["sender_fingerprint"], + "recipient_fingerprint": data["recipient_fingerprint"], + "sender_signature": data["sender_signature"], + "sender_public_key": sender_record.identity_public_key, + } + + func _deliver_private_letter(peer_id: int, letter: Dictionary) -> void: if peer_id == _session.get_local_peer_id(): _receive_letter(letter) @@ -401,7 +463,10 @@ func _receive_send_result(result: Dictionary) -> void: != str(_pending_local_send.get("request_id", "")) ): return - if not bool(result.get("accepted", false)): + if ( + not bool(result.get("accepted", false)) + or bool(result.get("silent_drop", false)) + ): var reservation_id := str(_pending_local_send.get("reservation_id", "")) if not reservation_id.is_empty(): _reservations.release(reservation_id) @@ -939,10 +1004,38 @@ func _letter_is_locally_blocked(letter: Dictionary) -> bool: return _relationship_policy.is_locally_blocked(other_fingerprint) +func _incoming_gift_is_unresolved(letter: Dictionary) -> bool: + return ( + is_local_recipient(letter) + and int(letter.get("attachment", {}).get("type", 0)) + != PlayerAssetReservationService.AttachmentType.NONE + and int(letter.get("state", -1)) in [ + NetworkMailProtocol.State.SENT_UNREAD, + NetworkMailProtocol.State.READ, + NetworkMailProtocol.State.ACCEPTANCE_PENDING, + ] + ) + + func _on_peer_removed(peer_id: int) -> void: if not _session.is_host(): - _clear_local_mail_for_peer(peer_id) + # The host delivers any unresolved final state. Keep local copies so + # disconnect feedback cannot disappear or overwrite settled mail. + peers_changed.emit() return + for transfer_id: String in _pending_transfers.keys().duplicate(): + var transfer: Dictionary = _pending_transfers.get(transfer_id, {}) + var transfer_letter: Dictionary = transfer.get("letter", {}) + if peer_id not in [ + int(transfer_letter.get("sender_peer_id", 0)), + int(transfer_letter.get("recipient_peer_id", 0)), + ]: + continue + if bool(transfer.get("sender_removed", false)): + var sender_id := int(transfer_letter.get("sender_peer_id", 0)) + if _session.is_authenticated_peer(sender_id): + _request_sender_rollback(sender_id, transfer_id) + _finish_transfer(transfer_id, false, "Gift returned to sender.") for letter: Dictionary in _letters.values(): if int(letter["recipient_peer_id"]) == peer_id: if int(letter["state"]) in [ @@ -966,18 +1059,9 @@ func _on_peer_removed(peer_id: int) -> void: letter["state"] = NetworkMailProtocol.State.ATTACHMENT_RECALLED _update_participants(letter) _send_ledger.erase(peer_id) - _clear_local_mail_for_peer(peer_id) peers_changed.emit() -func _clear_local_mail_for_peer(peer_id: int) -> void: - for mail_id: String in _local_letters.keys(): - var letter: Dictionary = _local_letters[mail_id] - if int(letter["recipient_peer_id"]) == peer_id: - _local_letters.erase(mail_id) - _emit_mailbox() - - func _on_session_state_changed(state: NetworkSession.State) -> void: if state in [ NetworkSession.State.INACTIVE, @@ -994,6 +1078,8 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: _pending_transfers.clear() _local_removal_snapshots.clear() _received_awards.clear() + _archived_local_letters.clear() + _deleted_local_letters.clear() _sequence = 0 _emit_mailbox() diff --git a/network/player_relationship_store.gd b/network/player_relationship_store.gd index 7ef7b6a..2ea4502 100644 --- a/network/player_relationship_store.gd +++ b/network/player_relationship_store.gd @@ -44,8 +44,10 @@ func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool _ensure_loaded() var record := _record(fingerprint, display_name) record["blocked"] = value - if value: - record["muted"] = true + # Blocking owns the accompanying mute. Removing the block establishes a + # fresh visibility boundary for future messages; already suppressed + # messages retain their immutable local suppression flag. + record["muted"] = value return _commit(fingerprint, record) diff --git a/ui/mail_page.gd b/ui/mail_page.gd index 08cedd4..751b668 100644 --- a/ui/mail_page.gd +++ b/ui/mail_page.gd @@ -36,7 +36,10 @@ var _salutation: OptionButton var _signature: Label var _attachment_kind: OptionButton var _attachment_choice: OptionButton -var _attachment_amount: SpinBox +var _attachment_amount: LineEdit +var _coin_available: Label +var _amount_minus: Button +var _amount_plus: Button var _attachment_summary: Label var _send_button: Button var _status: Label @@ -44,7 +47,10 @@ var _letter_text: Label var _letter_gift: Label var _accept: Button var _decline: Button +var _archive: Button +var _delete: Button var _current_mail_id := "" +var _showing_archive := false func _ready() -> void: @@ -145,6 +151,16 @@ func _build_inbox() -> Control: send.size = Vector2(150, 48) send.pressed.connect(_show_compose) page.add_child(send) + var archive_view := Button.new() + archive_view.text = "archive" + archive_view.position = Vector2(654, 0) + archive_view.size = Vector2(154, 48) + archive_view.pressed.connect(func() -> void: + _showing_archive = not _showing_archive + archive_view.text = "inbox" if _showing_archive else "archive" + _refresh_inbox() + ) + page.add_child(archive_view) var scroll := ScrollContainer.new() scroll.position = Vector2(12, 62) scroll.size = Vector2(958, 342) @@ -214,19 +230,32 @@ func _build_compose() -> Control: _update_attachment_summary() ) page.add_child(_attachment_choice) - _attachment_amount = SpinBox.new() - _attachment_amount.position = Vector2(688, 164) - _attachment_amount.size = Vector2(180, 46) - _attachment_amount.min_value = 1 - _attachment_amount.max_value = 999 - _attachment_amount.value = 1 - _attachment_amount.value_changed.connect( - func(_value: float) -> void: _update_attachment_summary() - ) + _coin_available = Label.new() + _coin_available.position = Vector2(688, 158) + _coin_available.size = Vector2(280, 28) + page.add_child(_coin_available) + _attachment_amount = LineEdit.new() + _attachment_amount.position = Vector2(746, 190) + _attachment_amount.size = Vector2(164, 46) + _attachment_amount.placeholder_text = "0" + _attachment_amount.text = "0" + _attachment_amount.text_changed.connect(_on_attachment_amount_changed) page.add_child(_attachment_amount) + _amount_minus = Button.new() + _amount_minus.text = "−" + _amount_minus.position = Vector2(688, 190) + _amount_minus.size = Vector2(48, 46) + _amount_minus.pressed.connect(_step_attachment_amount.bind(-1)) + page.add_child(_amount_minus) + _amount_plus = Button.new() + _amount_plus.text = "+" + _amount_plus.position = Vector2(920, 190) + _amount_plus.size = Vector2(48, 46) + _amount_plus.pressed.connect(_step_attachment_amount.bind(1)) + page.add_child(_amount_plus) _attachment_summary = Label.new() - _attachment_summary.position = Vector2(688, 224) - _attachment_summary.size = Vector2(280, 100) + _attachment_summary.position = Vector2(688, 246) + _attachment_summary.size = Vector2(280, 78) _attachment_summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART page.add_child(_attachment_summary) var cancel := Button.new() @@ -241,7 +270,10 @@ func _build_compose() -> Control: _send_button.size = Vector2(142, 48) _send_button.pressed.connect(_send) page.add_child(_send_button) - _recipient.item_selected.connect(func(_i: int) -> void: _update_send_state()) + _recipient.item_selected.connect(func(_i: int) -> void: + _reset_attachment_controls() + _update_send_state() + ) _refresh_attachment_choices(0) return page @@ -281,6 +313,18 @@ func _build_letter() -> Control: close.size = Vector2(150, 48) close.pressed.connect(_show_inbox) page.add_child(close) + _archive = Button.new() + _archive.text = "archive" + _archive.position = Vector2(550, 370) + _archive.size = Vector2(150, 48) + _archive.pressed.connect(_archive_current) + page.add_child(_archive) + _delete = Button.new() + _delete.text = "delete" + _delete.position = Vector2(710, 370) + _delete.size = Vector2(150, 48) + _delete.pressed.connect(_delete_current) + page.add_child(_delete) return page @@ -296,15 +340,30 @@ func _show_compose() -> void: _inbox.hide() _compose.show() _letter.hide() - _body.clear() + _reset_compose() _signature.text = _service.get_local_display_name() - _status.text = "" _refresh_recipients() - _refresh_attachment_choices(_attachment_kind.selected) _update_send_state() _greeting.grab_focus() +func _reset_compose() -> void: + _recipient.select(0) + _greeting.select(0) + _body.clear() + _salutation.select(0) + _status.text = "" + _reset_attachment_controls() + + +func _reset_attachment_controls() -> void: + _attachment_kind.select(0) + _attachment_choice.clear() + _attachment_choice.add_item("No attachment") + _attachment_amount.text = "0" + _refresh_attachment_choices(0) + + func _open_letter(mail_id: String) -> void: var letter := _service.get_letter(mail_id) if letter.is_empty(): @@ -332,6 +391,15 @@ func _open_letter(mail_id: String) -> void: ) _accept.visible = pending _decline.visible = pending + _archive.text = ( + "restore to inbox" + if _service.is_letter_archived(mail_id) + else "archive" + ) + _delete.disabled = pending + _delete.tooltip_text = ( + "Resolve this gift before deleting the letter." if pending else "" + ) if pending: _accept.grab_focus() @@ -341,10 +409,12 @@ func _refresh_inbox() -> void: return for child: Node in _inbox_list.get_children(): child.queue_free() - var letters := _service.get_local_letters() + var letters := _service.get_local_letters(_showing_archive) if letters.is_empty(): var empty := Label.new() - empty.text = "No letters yet." + empty.text = ( + "No archived letters." if _showing_archive else "No letters yet." + ) empty.custom_minimum_size = Vector2(930, 80) empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _inbox_list.add_child(empty) @@ -352,11 +422,23 @@ func _refresh_inbox() -> void: for letter: Dictionary in letters: var button := Button.new() var first_line: String = str(letter["body"]).split("\n", false)[0] - var unread := int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD + var unread := ( + _service.is_local_recipient(letter) + and int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD + ) var gift := int(letter["attachment"].get("type", 0)) != 0 - button.text = "%s%s — %s%s" % [ + var local_is_sender := ( + int(letter["sender_peer_id"]) != 0 + and not _service.is_local_recipient(letter) + ) + button.text = "%s%s%s — %s%s" % [ "new · " if unread else "", - letter["sender_display_name"], + "to " if local_is_sender else "", + ( + letter["recipient_display_name"] + if local_is_sender + else letter["sender_display_name"] + ), first_line.left(72), " · gift enclosed" if gift else "", ] @@ -398,17 +480,18 @@ func _refresh_attachment_choices(_index: int) -> void: if _attachment_choice == null: return _attachment_choice.clear() - _attachment_amount.visible = _attachment_kind.selected in [1, 3] + var amount_visible := _attachment_kind.selected in [1, 3] + _attachment_amount.visible = amount_visible + _amount_minus.visible = amount_visible + _amount_plus.visible = amount_visible + _coin_available.visible = _attachment_kind.selected == 1 match _attachment_kind.selected: 0: _attachment_choice.add_item("No attachment") 1: - _attachment_choice.add_item( - "available: %d fish coin" - % _reservations.get_available_fish_coin() - ) - _attachment_amount.max_value = maxi( - _reservations.get_available_fish_coin(), 1 + _attachment_choice.add_item("Fish coin") + _coin_available.text = "Available: %d fish coin" % ( + _reservations.get_available_fish_coin() ) 2: for fish_catch: FishCatch in _inventory.get_all_catches(): @@ -441,11 +524,7 @@ func _refresh_attachment_choices(_index: int) -> void: func _update_attachment_amount_limit() -> void: - if _attachment_kind.selected == 3 and _attachment_choice.selected >= 0: - var item_id := StringName(str(_attachment_choice.get_selected_metadata())) - _attachment_amount.max_value = maxi( - _reservations.get_available_item_quantity(item_id), 1 - ) + _set_attachment_amount(_attachment_amount_value()) func _make_attachment() -> Dictionary: @@ -453,7 +532,7 @@ func _make_attachment() -> Dictionary: 1: return { "type": PlayerAssetReservationService.AttachmentType.FISH_COIN, - "amount": int(_attachment_amount.value), + "amount": _attachment_amount_value(), } 2: if _attachment_choice.selected < 0: @@ -473,7 +552,7 @@ func _make_attachment() -> Dictionary: return { "type": PlayerAssetReservationService.AttachmentType.CONSUMABLE, "item_id": str(_attachment_choice.get_selected_metadata()), - "quantity": int(_attachment_amount.value), + "quantity": _attachment_amount_value(), } return {"type": PlayerAssetReservationService.AttachmentType.NONE} @@ -528,6 +607,7 @@ func _attachment_is_available(attachment: Dictionary) -> bool: func _send() -> void: + _set_attachment_amount(_attachment_amount_value()) var recipient_id := int(_recipient.get_selected_metadata()) var greeting_id := str(_greeting.get_selected_metadata()) var salutation_id := str(_salutation.get_selected_metadata()) @@ -548,6 +628,63 @@ func _on_operation_finished(success: bool, message: String) -> void: _update_send_state() +func _attachment_amount_value() -> int: + return int(_attachment_amount.text) if _attachment_amount.text.is_valid_int() else 0 + + +func _attachment_amount_maximum() -> int: + if _attachment_kind.selected == 1: + return _reservations.get_available_fish_coin() + if _attachment_kind.selected == 3 and _attachment_choice.selected >= 0: + return _reservations.get_available_item_quantity( + StringName(str(_attachment_choice.get_selected_metadata())) + ) + return 0 + + +func _set_attachment_amount(value: int) -> void: + var clamped := clampi(value, 0, _attachment_amount_maximum()) + var text := str(clamped) + if _attachment_amount.text != text: + _attachment_amount.text = text + _attachment_amount.caret_column = text.length() + _amount_minus.disabled = clamped <= 0 + _amount_plus.disabled = clamped >= _attachment_amount_maximum() + _update_attachment_summary() + + +func _on_attachment_amount_changed(value: String) -> void: + var digits := "" + for character: String in value: + if character >= "0" and character <= "9": + digits += character + _set_attachment_amount(int(digits) if not digits.is_empty() else 0) + + +func _step_attachment_amount(delta: int) -> void: + _set_attachment_amount(_attachment_amount_value() + delta) + _attachment_amount.grab_focus() + + +func _archive_current() -> void: + if _current_mail_id.is_empty(): + return + var archived := not _service.is_letter_archived(_current_mail_id) + if _service.archive_local_letter(_current_mail_id, archived): + _status.text = "Letter archived." if archived else "Letter restored." + _show_inbox() + + +func _delete_current() -> void: + if _current_mail_id.is_empty(): + return + if _service.delete_local_letter(_current_mail_id): + _status.text = "Letter deleted locally." + _show_inbox() + else: + _status.text = "Resolve the gift before deleting this letter." + + func _attachment_state_text(letter: Dictionary) -> String: var attachment: Dictionary = letter["attachment"] if int(attachment.get("type", 0)) == 0: diff --git a/ui/player_menu.gd b/ui/player_menu.gd index b6ea62a..3d670f2 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -336,6 +336,7 @@ func _ready() -> void: _apply_bag_styles() _bag_item_field.gui_input.connect(_on_bag_field_gui_input) _apply_logbook_styles() + _apply_mail_notification_style() _logbook_previous.pressed.connect(_request_logbook_page.bind(-1)) _logbook_next.pressed.connect(_request_logbook_page.bind(1)) resized.connect(_update_shell_layout) @@ -345,6 +346,22 @@ func _ready() -> void: set_process(false) +func _apply_mail_notification_style() -> void: + var background := StyleBoxFlat.new() + background.bg_color = Color(0.95, 0.88, 0.63, 0.98) + background.border_color = Color(0.20, 0.14, 0.08, 0.92) + background.set_border_width_all(2) + background.set_corner_radius_all(12) + background.content_margin_left = 5 + background.content_margin_right = 5 + background.content_margin_top = 2 + background.content_margin_bottom = 2 + _mail_unread_badge.add_theme_stylebox_override("normal", background) + _mail_unread_badge.add_theme_font_override( + "font", UtilityPageStyle.TuffyFont + ) + + func _update_cooler_water_mask() -> void: var shader_material := _cooler_water_surface.material as ShaderMaterial if shader_material == null: