Fix moderation and mail session behavior

This commit is contained in:
Alexander Sellite 2026-07-30 14:36:48 -04:00
parent c0ff78322d
commit 985fe1c28f
5 changed files with 329 additions and 82 deletions

View file

@ -210,11 +210,18 @@ func _apply_message(data: Dictionary) -> void:
if not valid_signature: if not valid_signature:
return return
_seen_messages[data["message_id"]] = true _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: while _history.size() > NetworkChatProtocol.MAX_HISTORY:
_history.pop_front() _history.pop_front()
if _message_is_visible(data): if _message_is_visible(stored_message):
message_received.emit(data.duplicate(true)) message_received.emit(stored_message.duplicate(true))
func _send_rejection(peer_id: int, message: String) -> void: 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: 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: if int(message.get("kind", -1)) == NetworkChatProtocol.Kind.SYSTEM:
return true return true
if _relationships == null:
return true return true
return not _relationships.is_muted(
str(message.get("sender_fingerprint", ""))
)
func _consume_rate(peer_id: int) -> bool: func _consume_rate(peer_id: int) -> bool:

View file

@ -29,6 +29,8 @@ var _pending_transfers: Dictionary[String, Dictionary] = {}
var _local_removal_snapshots: Dictionary[String, Dictionary] = {} var _local_removal_snapshots: Dictionary[String, Dictionary] = {}
var _received_awards: Dictionary[String, bool] = {} var _received_awards: Dictionary[String, bool] = {}
var _relationship_policy: NetworkPlayerListService var _relationship_policy: NetworkPlayerListService
var _archived_local_letters: Dictionary[String, bool] = {}
var _deleted_local_letters: Dictionary[String, bool] = {}
func setup( func setup(
@ -72,11 +74,16 @@ func refresh_relationship_filters() -> void:
peers_changed.emit() peers_changed.emit()
func get_local_letters() -> Array[Dictionary]: func get_local_letters(archived: bool = false) -> Array[Dictionary]:
var values: Array[Dictionary] = [] var values: Array[Dictionary] = []
for letter: Dictionary in _local_letters.values(): for letter: Dictionary in _local_letters.values():
if ( 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) and not _letter_is_locally_blocked(letter)
): ):
values.append(letter.duplicate(true)) values.append(letter.duplicate(true))
@ -91,15 +98,44 @@ func get_local_letters() -> Array[Dictionary]:
func get_letter(mail_id: String) -> Dictionary: func get_letter(mail_id: String) -> Dictionary:
if _deleted_local_letters.has(mail_id):
return {}
return _local_letters.get(mail_id, {}).duplicate(true) 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: func get_unread_count() -> int:
var count := 0 var count := 0
for letter: Dictionary in _local_letters.values(): for letter: Dictionary in _local_letters.values():
if ( if (
int(letter["recipient_peer_id"]) == _session.get_local_peer_id() int(letter["recipient_peer_id"]) == _session.get_local_peer_id()
and int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD 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) and not _letter_is_locally_blocked(letter)
): ):
count += 1 count += 1
@ -260,16 +296,19 @@ func _handle_send(sender: int, data: Dictionary) -> void:
) )
): ):
error = "Letter identity could not be verified." error = "Letter identity could not be verified."
if ( var silently_dropped := (
error.is_empty() error.is_empty()
and _relationship_policy != null and _relationship_policy != null
and _relationship_policy.pair_is_blocked( and _relationship_policy.pair_is_blocked(
sender_record.identity_fingerprint, sender_record.identity_fingerprint,
recipient_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." error = "The session mailbox is full."
var recipient_count := 0 var recipient_count := 0
for letter: Dictionary in _letters.values(): for letter: Dictionary in _letters.values():
@ -277,6 +316,7 @@ func _handle_send(sender: int, data: Dictionary) -> void:
recipient_count += 1 recipient_count += 1
if ( if (
error.is_empty() error.is_empty()
and not silently_dropped
and recipient_count >= NetworkMailProtocol.MAX_RECIPIENT_LETTERS and recipient_count >= NetworkMailProtocol.MAX_RECIPIENT_LETTERS
): ):
error = "That inbox is full." error = "That inbox is full."
@ -288,29 +328,21 @@ func _handle_send(sender: int, data: Dictionary) -> void:
"accepted": error.is_empty(), "accepted": error.is_empty(),
"message": "Letter sent." if error.is_empty() else error, "message": "Letter sent." if error.is_empty() else error,
"reservation_id": str(data.get("reservation_id", "")), "reservation_id": str(data.get("reservation_id", "")),
"silent_drop": silently_dropped,
} }
if error.is_empty(): if error.is_empty() and silently_dropped:
_sequence += 1 _sequence += 1
var letter := { var sender_copy := _build_letter(
"mail_id": _new_id("mail"), data, sender, recipient, sender_record, recipient_record, _sequence
"session_id": _session.get_session_id(), )
"sequence": _sequence, result["mail"] = sender_copy
"sender_peer_id": sender, _deliver_private_letter(sender, sender_copy)
"sender_display_name": sender_record.display_name, _request_release(sender, str(data.get("reservation_id", "")))
"recipient_peer_id": recipient, elif error.is_empty():
"recipient_display_name": recipient_record.display_name, _sequence += 1
"greeting_id": str(data["greeting_id"]), var letter := _build_letter(
"body": NetworkMailProtocol.sanitize_body(data["body"]), data, sender, recipient, sender_record, recipient_record, _sequence
"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,
}
_letters[letter["mail_id"]] = letter _letters[letter["mail_id"]] = letter
result["mail"] = letter result["mail"] = letter
_deliver_private_letter(sender, letter) _deliver_private_letter(sender, letter)
@ -321,6 +353,36 @@ func _handle_send(sender: int, data: Dictionary) -> void:
_deliver_send_result(sender, result) _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: func _deliver_private_letter(peer_id: int, letter: Dictionary) -> void:
if peer_id == _session.get_local_peer_id(): if peer_id == _session.get_local_peer_id():
_receive_letter(letter) _receive_letter(letter)
@ -401,7 +463,10 @@ func _receive_send_result(result: Dictionary) -> void:
!= str(_pending_local_send.get("request_id", "")) != str(_pending_local_send.get("request_id", ""))
): ):
return 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", "")) var reservation_id := str(_pending_local_send.get("reservation_id", ""))
if not reservation_id.is_empty(): if not reservation_id.is_empty():
_reservations.release(reservation_id) _reservations.release(reservation_id)
@ -939,10 +1004,38 @@ func _letter_is_locally_blocked(letter: Dictionary) -> bool:
return _relationship_policy.is_locally_blocked(other_fingerprint) 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: func _on_peer_removed(peer_id: int) -> void:
if not _session.is_host(): 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 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(): for letter: Dictionary in _letters.values():
if int(letter["recipient_peer_id"]) == peer_id: if int(letter["recipient_peer_id"]) == peer_id:
if int(letter["state"]) in [ if int(letter["state"]) in [
@ -966,18 +1059,9 @@ func _on_peer_removed(peer_id: int) -> void:
letter["state"] = NetworkMailProtocol.State.ATTACHMENT_RECALLED letter["state"] = NetworkMailProtocol.State.ATTACHMENT_RECALLED
_update_participants(letter) _update_participants(letter)
_send_ledger.erase(peer_id) _send_ledger.erase(peer_id)
_clear_local_mail_for_peer(peer_id)
peers_changed.emit() 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: func _on_session_state_changed(state: NetworkSession.State) -> void:
if state in [ if state in [
NetworkSession.State.INACTIVE, NetworkSession.State.INACTIVE,
@ -994,6 +1078,8 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
_pending_transfers.clear() _pending_transfers.clear()
_local_removal_snapshots.clear() _local_removal_snapshots.clear()
_received_awards.clear() _received_awards.clear()
_archived_local_letters.clear()
_deleted_local_letters.clear()
_sequence = 0 _sequence = 0
_emit_mailbox() _emit_mailbox()

View file

@ -44,8 +44,10 @@ func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool
_ensure_loaded() _ensure_loaded()
var record := _record(fingerprint, display_name) var record := _record(fingerprint, display_name)
record["blocked"] = value record["blocked"] = value
if value: # Blocking owns the accompanying mute. Removing the block establishes a
record["muted"] = true # fresh visibility boundary for future messages; already suppressed
# messages retain their immutable local suppression flag.
record["muted"] = value
return _commit(fingerprint, record) return _commit(fingerprint, record)

View file

@ -36,7 +36,10 @@ var _salutation: OptionButton
var _signature: Label var _signature: Label
var _attachment_kind: OptionButton var _attachment_kind: OptionButton
var _attachment_choice: 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 _attachment_summary: Label
var _send_button: Button var _send_button: Button
var _status: Label var _status: Label
@ -44,7 +47,10 @@ var _letter_text: Label
var _letter_gift: Label var _letter_gift: Label
var _accept: Button var _accept: Button
var _decline: Button var _decline: Button
var _archive: Button
var _delete: Button
var _current_mail_id := "" var _current_mail_id := ""
var _showing_archive := false
func _ready() -> void: func _ready() -> void:
@ -145,6 +151,16 @@ func _build_inbox() -> Control:
send.size = Vector2(150, 48) send.size = Vector2(150, 48)
send.pressed.connect(_show_compose) send.pressed.connect(_show_compose)
page.add_child(send) 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() var scroll := ScrollContainer.new()
scroll.position = Vector2(12, 62) scroll.position = Vector2(12, 62)
scroll.size = Vector2(958, 342) scroll.size = Vector2(958, 342)
@ -214,19 +230,32 @@ func _build_compose() -> Control:
_update_attachment_summary() _update_attachment_summary()
) )
page.add_child(_attachment_choice) page.add_child(_attachment_choice)
_attachment_amount = SpinBox.new() _coin_available = Label.new()
_attachment_amount.position = Vector2(688, 164) _coin_available.position = Vector2(688, 158)
_attachment_amount.size = Vector2(180, 46) _coin_available.size = Vector2(280, 28)
_attachment_amount.min_value = 1 page.add_child(_coin_available)
_attachment_amount.max_value = 999 _attachment_amount = LineEdit.new()
_attachment_amount.value = 1 _attachment_amount.position = Vector2(746, 190)
_attachment_amount.value_changed.connect( _attachment_amount.size = Vector2(164, 46)
func(_value: float) -> void: _update_attachment_summary() _attachment_amount.placeholder_text = "0"
) _attachment_amount.text = "0"
_attachment_amount.text_changed.connect(_on_attachment_amount_changed)
page.add_child(_attachment_amount) 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 = Label.new()
_attachment_summary.position = Vector2(688, 224) _attachment_summary.position = Vector2(688, 246)
_attachment_summary.size = Vector2(280, 100) _attachment_summary.size = Vector2(280, 78)
_attachment_summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _attachment_summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
page.add_child(_attachment_summary) page.add_child(_attachment_summary)
var cancel := Button.new() var cancel := Button.new()
@ -241,7 +270,10 @@ func _build_compose() -> Control:
_send_button.size = Vector2(142, 48) _send_button.size = Vector2(142, 48)
_send_button.pressed.connect(_send) _send_button.pressed.connect(_send)
page.add_child(_send_button) 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) _refresh_attachment_choices(0)
return page return page
@ -281,6 +313,18 @@ func _build_letter() -> Control:
close.size = Vector2(150, 48) close.size = Vector2(150, 48)
close.pressed.connect(_show_inbox) close.pressed.connect(_show_inbox)
page.add_child(close) 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 return page
@ -296,15 +340,30 @@ func _show_compose() -> void:
_inbox.hide() _inbox.hide()
_compose.show() _compose.show()
_letter.hide() _letter.hide()
_body.clear() _reset_compose()
_signature.text = _service.get_local_display_name() _signature.text = _service.get_local_display_name()
_status.text = ""
_refresh_recipients() _refresh_recipients()
_refresh_attachment_choices(_attachment_kind.selected)
_update_send_state() _update_send_state()
_greeting.grab_focus() _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: func _open_letter(mail_id: String) -> void:
var letter := _service.get_letter(mail_id) var letter := _service.get_letter(mail_id)
if letter.is_empty(): if letter.is_empty():
@ -332,6 +391,15 @@ func _open_letter(mail_id: String) -> void:
) )
_accept.visible = pending _accept.visible = pending
_decline.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: if pending:
_accept.grab_focus() _accept.grab_focus()
@ -341,10 +409,12 @@ func _refresh_inbox() -> void:
return return
for child: Node in _inbox_list.get_children(): for child: Node in _inbox_list.get_children():
child.queue_free() child.queue_free()
var letters := _service.get_local_letters() var letters := _service.get_local_letters(_showing_archive)
if letters.is_empty(): if letters.is_empty():
var empty := Label.new() 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.custom_minimum_size = Vector2(930, 80)
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_inbox_list.add_child(empty) _inbox_list.add_child(empty)
@ -352,11 +422,23 @@ func _refresh_inbox() -> void:
for letter: Dictionary in letters: for letter: Dictionary in letters:
var button := Button.new() var button := Button.new()
var first_line: String = str(letter["body"]).split("\n", false)[0] 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 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 "", "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), first_line.left(72),
" · gift enclosed" if gift else "", " · gift enclosed" if gift else "",
] ]
@ -398,17 +480,18 @@ func _refresh_attachment_choices(_index: int) -> void:
if _attachment_choice == null: if _attachment_choice == null:
return return
_attachment_choice.clear() _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: match _attachment_kind.selected:
0: 0:
_attachment_choice.add_item("No attachment") _attachment_choice.add_item("No attachment")
1: 1:
_attachment_choice.add_item( _attachment_choice.add_item("Fish coin")
"available: %d fish coin" _coin_available.text = "Available: %d fish coin" % (
% _reservations.get_available_fish_coin() _reservations.get_available_fish_coin()
)
_attachment_amount.max_value = maxi(
_reservations.get_available_fish_coin(), 1
) )
2: 2:
for fish_catch: FishCatch in _inventory.get_all_catches(): 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: func _update_attachment_amount_limit() -> void:
if _attachment_kind.selected == 3 and _attachment_choice.selected >= 0: _set_attachment_amount(_attachment_amount_value())
var item_id := StringName(str(_attachment_choice.get_selected_metadata()))
_attachment_amount.max_value = maxi(
_reservations.get_available_item_quantity(item_id), 1
)
func _make_attachment() -> Dictionary: func _make_attachment() -> Dictionary:
@ -453,7 +532,7 @@ func _make_attachment() -> Dictionary:
1: 1:
return { return {
"type": PlayerAssetReservationService.AttachmentType.FISH_COIN, "type": PlayerAssetReservationService.AttachmentType.FISH_COIN,
"amount": int(_attachment_amount.value), "amount": _attachment_amount_value(),
} }
2: 2:
if _attachment_choice.selected < 0: if _attachment_choice.selected < 0:
@ -473,7 +552,7 @@ func _make_attachment() -> Dictionary:
return { return {
"type": PlayerAssetReservationService.AttachmentType.CONSUMABLE, "type": PlayerAssetReservationService.AttachmentType.CONSUMABLE,
"item_id": str(_attachment_choice.get_selected_metadata()), "item_id": str(_attachment_choice.get_selected_metadata()),
"quantity": int(_attachment_amount.value), "quantity": _attachment_amount_value(),
} }
return {"type": PlayerAssetReservationService.AttachmentType.NONE} return {"type": PlayerAssetReservationService.AttachmentType.NONE}
@ -528,6 +607,7 @@ func _attachment_is_available(attachment: Dictionary) -> bool:
func _send() -> void: func _send() -> void:
_set_attachment_amount(_attachment_amount_value())
var recipient_id := int(_recipient.get_selected_metadata()) var recipient_id := int(_recipient.get_selected_metadata())
var greeting_id := str(_greeting.get_selected_metadata()) var greeting_id := str(_greeting.get_selected_metadata())
var salutation_id := str(_salutation.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() _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: func _attachment_state_text(letter: Dictionary) -> String:
var attachment: Dictionary = letter["attachment"] var attachment: Dictionary = letter["attachment"]
if int(attachment.get("type", 0)) == 0: if int(attachment.get("type", 0)) == 0:

View file

@ -336,6 +336,7 @@ func _ready() -> void:
_apply_bag_styles() _apply_bag_styles()
_bag_item_field.gui_input.connect(_on_bag_field_gui_input) _bag_item_field.gui_input.connect(_on_bag_field_gui_input)
_apply_logbook_styles() _apply_logbook_styles()
_apply_mail_notification_style()
_logbook_previous.pressed.connect(_request_logbook_page.bind(-1)) _logbook_previous.pressed.connect(_request_logbook_page.bind(-1))
_logbook_next.pressed.connect(_request_logbook_page.bind(1)) _logbook_next.pressed.connect(_request_logbook_page.bind(1))
resized.connect(_update_shell_layout) resized.connect(_update_shell_layout)
@ -345,6 +346,22 @@ func _ready() -> void:
set_process(false) 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: func _update_cooler_water_mask() -> void:
var shader_material := _cooler_water_surface.material as ShaderMaterial var shader_material := _cooler_water_surface.material as ShaderMaterial
if shader_material == null: if shader_material == null: