Add session mail and player gifts
This commit is contained in:
parent
5dda74c9e4
commit
e53ad1ac38
19 changed files with 1929 additions and 22 deletions
|
|
@ -20,6 +20,7 @@ var _received_results: Dictionary[String, bool] = {}
|
|||
var _pending_local: Dictionary = {}
|
||||
var _equipped_states: Dictionary[int, Dictionary] = {}
|
||||
var _local_equipped_revision: int = 0
|
||||
var _reservations: PlayerAssetReservationService
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -29,6 +30,7 @@ func setup(
|
|||
local_bag: PlayerBag,
|
||||
local_effects: PlayerItemEffects,
|
||||
save_manager: PlayerSaveManager,
|
||||
reservations: PlayerAssetReservationService,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
|
|
@ -36,6 +38,7 @@ func setup(
|
|||
_local_bag = local_bag
|
||||
_local_effects = local_effects
|
||||
_save_manager = save_manager
|
||||
_reservations = reservations
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
_session.state_changed.connect(_on_session_state_changed)
|
||||
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||
|
|
@ -45,6 +48,12 @@ func request_use(item_id: StringName) -> String:
|
|||
if not _pending_local.is_empty():
|
||||
local_item_use_finished.emit(false, "An item use is already pending.")
|
||||
return ""
|
||||
if (
|
||||
_reservations != null
|
||||
and _reservations.get_available_item_quantity(item_id) < 1
|
||||
):
|
||||
local_item_use_finished.emit(false, "Reserved in a letter.")
|
||||
return ""
|
||||
if (
|
||||
_session == null
|
||||
or not _session.is_gameplay_session_active()
|
||||
|
|
@ -169,6 +178,14 @@ func _apply_result(data: Dictionary) -> void:
|
|||
_acknowledge(data, false, str(data["message"]))
|
||||
return
|
||||
var item_id := StringName(str(data["item_id"]))
|
||||
if (
|
||||
_reservations != null
|
||||
and _reservations.get_available_item_quantity(item_id) < 1
|
||||
):
|
||||
_pending_local.clear()
|
||||
local_item_use_finished.emit(false, "Reserved in a letter.")
|
||||
_acknowledge(data, false, "Reserved in a letter.")
|
||||
return
|
||||
var bag_snapshot := _local_bag.get_all_items()
|
||||
if (
|
||||
_local_bag.get_quantity(item_id) < 1
|
||||
|
|
|
|||
97
network/network_mail_protocol.gd
Normal file
97
network/network_mail_protocol.gd
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
class_name NetworkMailProtocol
|
||||
extends RefCounted
|
||||
|
||||
const CAPABILITY: StringName = &"mail_v1"
|
||||
const RELIABLE_CHANNEL: int = NetworkProtocol.MAIL_RELIABLE_CHANNEL
|
||||
const MAX_ID_LENGTH: int = 96
|
||||
const MAX_BODY_CHARACTERS: int = 2000
|
||||
const MAX_BODY_BYTES: int = 8000
|
||||
const MAX_RECIPIENT_LETTERS: int = 100
|
||||
const MAX_SESSION_LETTERS: int = 200
|
||||
|
||||
const GREETINGS: PackedStringArray = ["dear", "to", "hey", "greetings"]
|
||||
const SALUTATIONS: PackedStringArray = [
|
||||
"love", "from", "cheers", "salutations", "good_luck_have_fun",
|
||||
]
|
||||
|
||||
enum State {
|
||||
SENT_UNREAD,
|
||||
READ,
|
||||
ACCEPTANCE_PENDING,
|
||||
ACCEPTED,
|
||||
DECLINED,
|
||||
ATTACHMENT_RECALLED,
|
||||
CANCELLED,
|
||||
SESSION_ENDED,
|
||||
}
|
||||
|
||||
|
||||
static func sanitize_body(value: Variant) -> String:
|
||||
if typeof(value) != TYPE_STRING:
|
||||
return ""
|
||||
var body := str(value).strip_edges(false, true)
|
||||
if (
|
||||
body.strip_edges().is_empty()
|
||||
or body.length() > MAX_BODY_CHARACTERS
|
||||
or body.to_utf8_buffer().size() > MAX_BODY_BYTES
|
||||
):
|
||||
return ""
|
||||
for index: int in body.length():
|
||||
var codepoint := body.unicode_at(index)
|
||||
if codepoint < 32 and codepoint not in [9, 10, 13]:
|
||||
return ""
|
||||
if codepoint == 127:
|
||||
return ""
|
||||
return body.replace("\r\n", "\n").replace("\r", "\n").replace("\t", " ")
|
||||
|
||||
|
||||
static func valid_id(value: Variant) -> bool:
|
||||
return (
|
||||
typeof(value) == TYPE_STRING
|
||||
and not str(value).is_empty()
|
||||
and str(value).length() <= MAX_ID_LENGTH
|
||||
)
|
||||
|
||||
|
||||
static func validate_send_request(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
return (
|
||||
valid_id(value.get("request_id"))
|
||||
and valid_id(value.get("session_id"))
|
||||
and typeof(value.get("recipient_peer_id")) == TYPE_INT
|
||||
and int(value["recipient_peer_id"]) > 0
|
||||
and typeof(value.get("greeting_id")) == TYPE_STRING
|
||||
and str(value["greeting_id"]) in GREETINGS
|
||||
and not sanitize_body(value.get("body")).is_empty()
|
||||
and typeof(value.get("salutation_id")) == TYPE_STRING
|
||||
and str(value["salutation_id"]) in SALUTATIONS
|
||||
and typeof(value.get("reservation_id")) == TYPE_STRING
|
||||
and str(value["reservation_id"]).length() <= MAX_ID_LENGTH
|
||||
and typeof(value.get("attachment")) == TYPE_DICTIONARY
|
||||
and Dictionary(value["attachment"]).size() <= 3
|
||||
)
|
||||
|
||||
|
||||
static func validate_mail(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
return (
|
||||
valid_id(value.get("mail_id"))
|
||||
and valid_id(value.get("session_id"))
|
||||
and typeof(value.get("sequence")) == TYPE_INT
|
||||
and typeof(value.get("sender_peer_id")) == TYPE_INT
|
||||
and typeof(value.get("recipient_peer_id")) == TYPE_INT
|
||||
and typeof(value.get("sender_display_name")) == TYPE_STRING
|
||||
and str(value["sender_display_name"]).length() <= 24
|
||||
and str(value.get("greeting_id", "")) in GREETINGS
|
||||
and not sanitize_body(value.get("body")).is_empty()
|
||||
and str(value.get("salutation_id", "")) in SALUTATIONS
|
||||
and typeof(value.get("attachment")) == TYPE_DICTIONARY
|
||||
and Dictionary(value["attachment"]).size() <= 3
|
||||
and typeof(value.get("state")) == TYPE_INT
|
||||
and int(value["state"]) >= 0
|
||||
and int(value["state"]) < State.size()
|
||||
)
|
||||
1
network/network_mail_protocol.gd.uid
Normal file
1
network/network_mail_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://5ttl0pytgtf7
|
||||
802
network/network_mail_service.gd
Normal file
802
network/network_mail_service.gd
Normal file
|
|
@ -0,0 +1,802 @@
|
|||
class_name NetworkMailService
|
||||
extends Node
|
||||
|
||||
signal mailbox_changed
|
||||
signal unread_count_changed(count: int)
|
||||
signal operation_finished(success: bool, message: String)
|
||||
signal peers_changed
|
||||
|
||||
const MAX_LEDGER: int = 256
|
||||
|
||||
var _session: NetworkSession
|
||||
var _reservations: PlayerAssetReservationService
|
||||
var _wallet: PlayerWallet
|
||||
var _inventory: FishInventory
|
||||
var _bag: PlayerBag
|
||||
var _collection_log: CollectionLog
|
||||
var _cooler_capacity: PlayerCoolerCapacity
|
||||
var _item_catalog: ItemCatalog
|
||||
var _fish_catalog: FishPool
|
||||
var _save_manager: PlayerSaveManager
|
||||
|
||||
var _letters: Dictionary[String, Dictionary] = {}
|
||||
var _local_letters: Dictionary[String, Dictionary] = {}
|
||||
var _send_ledger: Dictionary[int, Dictionary] = {}
|
||||
var _action_ledger: Dictionary[String, bool] = {}
|
||||
var _sequence := 0
|
||||
var _pending_local_send: Dictionary = {}
|
||||
var _pending_transfers: Dictionary[String, Dictionary] = {}
|
||||
var _local_removal_snapshots: Dictionary[String, Dictionary] = {}
|
||||
var _received_awards: Dictionary[String, bool] = {}
|
||||
|
||||
|
||||
func setup(
|
||||
session: NetworkSession,
|
||||
reservations: PlayerAssetReservationService,
|
||||
wallet: PlayerWallet,
|
||||
inventory: FishInventory,
|
||||
bag: PlayerBag,
|
||||
collection_log: CollectionLog,
|
||||
cooler_capacity: PlayerCoolerCapacity,
|
||||
item_catalog: ItemCatalog,
|
||||
fish_catalog: FishPool,
|
||||
save_manager: PlayerSaveManager,
|
||||
) -> void:
|
||||
_session = session
|
||||
_reservations = reservations
|
||||
_wallet = wallet
|
||||
_inventory = inventory
|
||||
_bag = bag
|
||||
_collection_log = collection_log
|
||||
_cooler_capacity = cooler_capacity
|
||||
_item_catalog = item_catalog
|
||||
_fish_catalog = fish_catalog
|
||||
_save_manager = save_manager
|
||||
_session.peer_authenticated.connect(func(_id: int, _name: String) -> void:
|
||||
peers_changed.emit()
|
||||
)
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
_session.peer_display_name_changed.connect(
|
||||
func(_id: int, _name: String) -> void: peers_changed.emit()
|
||||
)
|
||||
_session.state_changed.connect(_on_session_state_changed)
|
||||
|
||||
|
||||
func get_local_letters() -> Array[Dictionary]:
|
||||
var values: Array[Dictionary] = []
|
||||
for letter: Dictionary in _local_letters.values():
|
||||
if int(letter["recipient_peer_id"]) == _session.get_local_peer_id():
|
||||
values.append(letter.duplicate(true))
|
||||
values.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
var a_unread := int(a["state"]) == NetworkMailProtocol.State.SENT_UNREAD
|
||||
var b_unread := int(b["state"]) == NetworkMailProtocol.State.SENT_UNREAD
|
||||
return a_unread != b_unread if a_unread != b_unread else (
|
||||
int(a["sequence"]) > int(b["sequence"])
|
||||
)
|
||||
)
|
||||
return values
|
||||
|
||||
|
||||
func get_letter(mail_id: String) -> Dictionary:
|
||||
return _local_letters.get(mail_id, {}).duplicate(true)
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func is_local_recipient(letter: Dictionary) -> bool:
|
||||
return (
|
||||
int(letter.get("recipient_peer_id", 0))
|
||||
== _session.get_local_peer_id()
|
||||
)
|
||||
|
||||
|
||||
func get_local_display_name() -> String:
|
||||
var record := _session.get_peer_record(_session.get_local_peer_id())
|
||||
return record.display_name if record != null else "Player"
|
||||
|
||||
|
||||
func get_recipient_choices() -> Array[Dictionary]:
|
||||
var choices: Array[Dictionary] = []
|
||||
var local_id := _session.get_local_peer_id()
|
||||
for peer_id: int in _session.get_authenticated_peer_ids():
|
||||
if peer_id == local_id:
|
||||
continue
|
||||
var record := _session.get_peer_record(peer_id)
|
||||
if record != null:
|
||||
choices.append({"peer_id": peer_id, "name": record.display_name})
|
||||
choices.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return str(a["name"]).naturalnocasecmp_to(str(b["name"])) < 0
|
||||
)
|
||||
var seen: Dictionary[String, int] = {}
|
||||
for choice: Dictionary in choices:
|
||||
var name: String = choice["name"]
|
||||
seen[name] = int(seen.get(name, 0)) + 1
|
||||
if seen[name] > 1:
|
||||
choice["label"] = "%s (%d)" % [name, seen[name]]
|
||||
else:
|
||||
choice["label"] = name
|
||||
return choices
|
||||
|
||||
|
||||
func send_letter(
|
||||
recipient_peer_id: int,
|
||||
greeting_id: String,
|
||||
body: String,
|
||||
salutation_id: String,
|
||||
attachment: Dictionary,
|
||||
) -> bool:
|
||||
if (
|
||||
not _pending_local_send.is_empty()
|
||||
or not _session.is_gameplay_session_active()
|
||||
or recipient_peer_id == _session.get_local_peer_id()
|
||||
or not _session.is_authenticated_peer(recipient_peer_id)
|
||||
or (
|
||||
not _session.is_host()
|
||||
and not _session.supports_server_capability(
|
||||
NetworkMailProtocol.CAPABILITY
|
||||
)
|
||||
)
|
||||
):
|
||||
operation_finished.emit(false, "That player is no longer connected.")
|
||||
return false
|
||||
var request_id := _new_id("mail_request")
|
||||
var reservation_id := ""
|
||||
if int(attachment.get("type", 0)) != 0:
|
||||
reservation_id = _new_id("reservation")
|
||||
if not _reservations.reserve(reservation_id, attachment):
|
||||
operation_finished.emit(false, "That gift is not available.")
|
||||
return false
|
||||
var request := {
|
||||
"request_id": request_id,
|
||||
"session_id": _session.get_session_id(),
|
||||
"recipient_peer_id": recipient_peer_id,
|
||||
"greeting_id": greeting_id,
|
||||
"body": NetworkMailProtocol.sanitize_body(body),
|
||||
"salutation_id": salutation_id,
|
||||
"reservation_id": reservation_id,
|
||||
"attachment": attachment.duplicate(true),
|
||||
}
|
||||
if not NetworkMailProtocol.validate_send_request(request):
|
||||
if not reservation_id.is_empty():
|
||||
_reservations.release(reservation_id)
|
||||
operation_finished.emit(false, "Letter could not be sent.")
|
||||
return false
|
||||
_pending_local_send = request.duplicate(true)
|
||||
if _session.is_host():
|
||||
_handle_send(_session.get_local_peer_id(), request)
|
||||
else:
|
||||
submit_mail.rpc_id(1, request)
|
||||
return true
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func submit_mail(data: Dictionary) -> void:
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
if _session.is_host() and _session.is_authenticated_peer(sender):
|
||||
_handle_send(sender, data)
|
||||
|
||||
|
||||
func _handle_send(sender: int, data: Dictionary) -> void:
|
||||
var request_id := str(data.get("request_id", ""))
|
||||
var ledger: Dictionary = _send_ledger.get(sender, {})
|
||||
if ledger.has(request_id):
|
||||
_deliver_send_result(sender, ledger[request_id])
|
||||
return
|
||||
var error := ""
|
||||
if (
|
||||
not NetworkMailProtocol.validate_send_request(data)
|
||||
or str(data.get("session_id", "")) != _session.get_session_id()
|
||||
):
|
||||
error = "Letter could not be sent."
|
||||
var recipient := int(data.get("recipient_peer_id", 0))
|
||||
if error.is_empty() and (
|
||||
recipient == sender or not _session.is_authenticated_peer(recipient)
|
||||
):
|
||||
error = "That player is no longer connected."
|
||||
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():
|
||||
if int(letter["recipient_peer_id"]) == recipient:
|
||||
recipient_count += 1
|
||||
if (
|
||||
error.is_empty()
|
||||
and recipient_count >= NetworkMailProtocol.MAX_RECIPIENT_LETTERS
|
||||
):
|
||||
error = "That inbox is full."
|
||||
var attachment: Dictionary = data.get("attachment", {})
|
||||
if error.is_empty() and not _validate_attachment_structure(attachment):
|
||||
error = "That gift is not valid."
|
||||
var result := {
|
||||
"request_id": request_id if not request_id.is_empty() else "invalid",
|
||||
"accepted": error.is_empty(),
|
||||
"message": "Letter sent." if error.is_empty() else error,
|
||||
"reservation_id": str(data.get("reservation_id", "")),
|
||||
}
|
||||
if error.is_empty():
|
||||
_sequence += 1
|
||||
var record := _session.get_peer_record(sender)
|
||||
var recipient_record := _session.get_peer_record(recipient)
|
||||
var letter := {
|
||||
"mail_id": _new_id("mail"),
|
||||
"session_id": _session.get_session_id(),
|
||||
"sequence": _sequence,
|
||||
"sender_peer_id": sender,
|
||||
"sender_display_name": 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,
|
||||
}
|
||||
_letters[letter["mail_id"]] = letter
|
||||
result["mail"] = letter
|
||||
_deliver_private_letter(sender, letter)
|
||||
_deliver_private_letter(recipient, letter)
|
||||
ledger[result["request_id"]] = result.duplicate(true)
|
||||
_bound(ledger)
|
||||
_send_ledger[sender] = ledger
|
||||
_deliver_send_result(sender, result)
|
||||
|
||||
|
||||
func _deliver_private_letter(peer_id: int, letter: Dictionary) -> void:
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
_receive_letter(letter)
|
||||
elif _session.is_authenticated_peer(peer_id):
|
||||
receive_private_letter.rpc_id(peer_id, letter)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func receive_private_letter(letter: Dictionary) -> void:
|
||||
_receive_letter(letter)
|
||||
|
||||
|
||||
func _receive_letter(letter: Dictionary) -> void:
|
||||
if (
|
||||
not NetworkMailProtocol.validate_mail(letter)
|
||||
or str(letter["session_id"]) != _session.get_session_id()
|
||||
or _session.get_local_peer_id() not in [
|
||||
int(letter["sender_peer_id"]), int(letter["recipient_peer_id"]),
|
||||
]
|
||||
):
|
||||
return
|
||||
_local_letters[letter["mail_id"]] = letter.duplicate(true)
|
||||
_emit_mailbox()
|
||||
|
||||
|
||||
func _deliver_send_result(peer_id: int, result: Dictionary) -> void:
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
_receive_send_result(result)
|
||||
else:
|
||||
receive_send_result.rpc_id(peer_id, result)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func receive_send_result(result: Dictionary) -> void:
|
||||
_receive_send_result(result)
|
||||
|
||||
|
||||
func _receive_send_result(result: Dictionary) -> void:
|
||||
if (
|
||||
_pending_local_send.is_empty()
|
||||
or str(result.get("request_id", ""))
|
||||
!= str(_pending_local_send.get("request_id", ""))
|
||||
):
|
||||
return
|
||||
if not bool(result.get("accepted", false)):
|
||||
var reservation_id := str(_pending_local_send.get("reservation_id", ""))
|
||||
if not reservation_id.is_empty():
|
||||
_reservations.release(reservation_id)
|
||||
_pending_local_send.clear()
|
||||
operation_finished.emit(
|
||||
bool(result.get("accepted", false)),
|
||||
str(result.get("message", "Letter could not be sent."))
|
||||
)
|
||||
|
||||
|
||||
func mark_read(mail_id: String) -> void:
|
||||
var request := _action_request(mail_id)
|
||||
if request.is_empty():
|
||||
return
|
||||
if _session.is_host():
|
||||
_handle_read(_session.get_local_peer_id(), request)
|
||||
else:
|
||||
submit_read.rpc_id(1, request)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func submit_read(data: Dictionary) -> void:
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
if _session.is_host() and _session.is_authenticated_peer(sender):
|
||||
_handle_read(sender, data)
|
||||
|
||||
|
||||
func _handle_read(peer_id: int, data: Dictionary) -> void:
|
||||
var action_id := str(data.get("request_id", ""))
|
||||
if _action_ledger.has(action_id):
|
||||
return
|
||||
var letter: Dictionary = _letters.get(str(data.get("mail_id", "")), {})
|
||||
if (
|
||||
letter.is_empty()
|
||||
or int(letter["recipient_peer_id"]) != peer_id
|
||||
or str(data.get("session_id", "")) != _session.get_session_id()
|
||||
):
|
||||
return
|
||||
_action_ledger[action_id] = true
|
||||
if int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD:
|
||||
letter["state"] = NetworkMailProtocol.State.READ
|
||||
_update_participants(letter)
|
||||
|
||||
|
||||
func accept_gift(mail_id: String) -> void:
|
||||
var request := _action_request(mail_id)
|
||||
if request.is_empty():
|
||||
return
|
||||
if _session.is_host():
|
||||
_handle_accept(_session.get_local_peer_id(), request)
|
||||
else:
|
||||
submit_accept.rpc_id(1, request)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func submit_accept(data: Dictionary) -> void:
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
if _session.is_host() and _session.is_authenticated_peer(sender):
|
||||
_handle_accept(sender, data)
|
||||
|
||||
|
||||
func _handle_accept(peer_id: int, data: Dictionary) -> void:
|
||||
var action_id := str(data.get("request_id", ""))
|
||||
var letter: Dictionary = _letters.get(str(data.get("mail_id", "")), {})
|
||||
if _action_ledger.has(action_id):
|
||||
return
|
||||
if (
|
||||
letter.is_empty()
|
||||
or int(letter["recipient_peer_id"]) != peer_id
|
||||
or int(letter["state"]) not in [
|
||||
NetworkMailProtocol.State.SENT_UNREAD,
|
||||
NetworkMailProtocol.State.READ,
|
||||
]
|
||||
or int(letter["attachment"].get("type", 0)) == 0
|
||||
or not _session.is_authenticated_peer(int(letter["sender_peer_id"]))
|
||||
):
|
||||
return
|
||||
_action_ledger[action_id] = true
|
||||
var transfer_id := _new_id("mail_transfer")
|
||||
letter["state"] = NetworkMailProtocol.State.ACCEPTANCE_PENDING
|
||||
_pending_transfers[transfer_id] = {
|
||||
"letter": letter,
|
||||
"recipient_prepared": false,
|
||||
"sender_removed": false,
|
||||
}
|
||||
_update_participants(letter)
|
||||
_request_prepare(int(letter["recipient_peer_id"]), transfer_id, letter)
|
||||
|
||||
|
||||
func _request_prepare(peer_id: int, transfer_id: String, letter: Dictionary) -> void:
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
_prepare_recipient(transfer_id, letter)
|
||||
else:
|
||||
prepare_recipient.rpc_id(peer_id, transfer_id, letter)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func prepare_recipient(transfer_id: String, letter: Dictionary) -> void:
|
||||
_prepare_recipient(transfer_id, letter)
|
||||
|
||||
|
||||
func _prepare_recipient(transfer_id: String, letter: Dictionary) -> void:
|
||||
var ready := _can_receive(letter["attachment"])
|
||||
_send_phase_ack("recipient_prepared", transfer_id, ready)
|
||||
|
||||
|
||||
func _request_sender_commit(peer_id: int, transfer_id: String, letter: Dictionary) -> void:
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
_commit_sender(transfer_id, letter)
|
||||
else:
|
||||
commit_sender.rpc_id(peer_id, transfer_id, letter)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func commit_sender(transfer_id: String, letter: Dictionary) -> void:
|
||||
_commit_sender(transfer_id, letter)
|
||||
|
||||
|
||||
func _commit_sender(transfer_id: String, letter: Dictionary) -> void:
|
||||
var reservation_id: String = letter["reservation_id"]
|
||||
var snapshot := _capture_assets()
|
||||
var applied := (
|
||||
_reservations.has_reservation(reservation_id)
|
||||
and _reservations.commit_removal(reservation_id)
|
||||
and _save_manager.save_if_dirty()
|
||||
)
|
||||
if not applied:
|
||||
_restore_assets(snapshot)
|
||||
_save_manager.save_if_dirty()
|
||||
else:
|
||||
_local_removal_snapshots[transfer_id] = snapshot
|
||||
_send_phase_ack("sender_removed", transfer_id, applied)
|
||||
|
||||
|
||||
func _request_recipient_award(
|
||||
peer_id: int, transfer_id: String, letter: Dictionary
|
||||
) -> void:
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
_award_recipient(transfer_id, letter)
|
||||
else:
|
||||
award_recipient.rpc_id(peer_id, transfer_id, letter)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func award_recipient(transfer_id: String, letter: Dictionary) -> void:
|
||||
_award_recipient(transfer_id, letter)
|
||||
|
||||
|
||||
func _award_recipient(transfer_id: String, letter: Dictionary) -> void:
|
||||
if _received_awards.has(transfer_id):
|
||||
_send_phase_ack("recipient_awarded", transfer_id, true)
|
||||
return
|
||||
var snapshot := _capture_assets()
|
||||
var applied := _apply_award(letter["attachment"])
|
||||
if applied:
|
||||
applied = _save_manager.save_if_dirty()
|
||||
if not applied:
|
||||
_restore_assets(snapshot)
|
||||
_save_manager.save_if_dirty()
|
||||
else:
|
||||
_received_awards[transfer_id] = true
|
||||
_bound(_received_awards)
|
||||
_send_phase_ack("recipient_awarded", transfer_id, applied)
|
||||
|
||||
|
||||
func _send_phase_ack(phase: String, transfer_id: String, applied: bool) -> void:
|
||||
if _session.is_host():
|
||||
_handle_phase_ack(_session.get_local_peer_id(), phase, transfer_id, applied)
|
||||
else:
|
||||
acknowledge_transfer_phase.rpc_id(1, phase, transfer_id, applied)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func acknowledge_transfer_phase(
|
||||
phase: String, transfer_id: String, applied: bool
|
||||
) -> void:
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
if _session.is_host() and _session.is_authenticated_peer(sender):
|
||||
_handle_phase_ack(sender, phase, transfer_id, applied)
|
||||
|
||||
|
||||
func _handle_phase_ack(
|
||||
peer_id: int, phase: String, transfer_id: String, applied: bool
|
||||
) -> void:
|
||||
var transfer: Dictionary = _pending_transfers.get(transfer_id, {})
|
||||
if transfer.is_empty():
|
||||
return
|
||||
var letter: Dictionary = transfer["letter"]
|
||||
if phase == "recipient_prepared":
|
||||
if peer_id != int(letter["recipient_peer_id"]):
|
||||
return
|
||||
if not applied:
|
||||
_finish_transfer(transfer_id, false, "Gift could not be transferred.")
|
||||
return
|
||||
transfer["recipient_prepared"] = true
|
||||
_request_sender_commit(int(letter["sender_peer_id"]), transfer_id, letter)
|
||||
elif phase == "sender_removed":
|
||||
if peer_id != int(letter["sender_peer_id"]):
|
||||
return
|
||||
if not applied:
|
||||
_finish_transfer(transfer_id, false, "Gift returned to sender.")
|
||||
return
|
||||
transfer["sender_removed"] = true
|
||||
_request_recipient_award(
|
||||
int(letter["recipient_peer_id"]), transfer_id, letter
|
||||
)
|
||||
elif phase == "recipient_awarded":
|
||||
if peer_id != int(letter["recipient_peer_id"]):
|
||||
return
|
||||
if applied:
|
||||
_finish_transfer(transfer_id, true, "Gift accepted.")
|
||||
else:
|
||||
_request_sender_rollback(
|
||||
int(letter["sender_peer_id"]), transfer_id
|
||||
)
|
||||
_finish_transfer(transfer_id, false, "Gift could not be transferred.")
|
||||
|
||||
|
||||
func _request_sender_rollback(peer_id: int, transfer_id: String) -> void:
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
_rollback_sender(transfer_id)
|
||||
else:
|
||||
rollback_sender.rpc_id(peer_id, transfer_id)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func rollback_sender(transfer_id: String) -> void:
|
||||
_rollback_sender(transfer_id)
|
||||
|
||||
|
||||
func _rollback_sender(transfer_id: String) -> void:
|
||||
var snapshot: Dictionary = _local_removal_snapshots.get(transfer_id, {})
|
||||
if not snapshot.is_empty():
|
||||
_restore_assets(snapshot)
|
||||
_save_manager.save_if_dirty()
|
||||
_local_removal_snapshots.erase(transfer_id)
|
||||
|
||||
|
||||
func _finish_transfer(transfer_id: String, accepted: bool, message: String) -> void:
|
||||
var transfer: Dictionary = _pending_transfers.get(transfer_id, {})
|
||||
if transfer.is_empty():
|
||||
return
|
||||
var letter: Dictionary = transfer["letter"]
|
||||
letter["state"] = (
|
||||
NetworkMailProtocol.State.ACCEPTED
|
||||
if accepted else NetworkMailProtocol.State.ATTACHMENT_RECALLED
|
||||
)
|
||||
_pending_transfers.erase(transfer_id)
|
||||
_update_participants(letter)
|
||||
operation_finished.emit(accepted, message)
|
||||
|
||||
|
||||
func decline_gift(mail_id: String) -> void:
|
||||
var request := _action_request(mail_id)
|
||||
if request.is_empty():
|
||||
return
|
||||
if _session.is_host():
|
||||
_handle_decline(_session.get_local_peer_id(), request)
|
||||
else:
|
||||
submit_decline.rpc_id(1, request)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func submit_decline(data: Dictionary) -> void:
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
if _session.is_host() and _session.is_authenticated_peer(sender):
|
||||
_handle_decline(sender, data)
|
||||
|
||||
|
||||
func _handle_decline(peer_id: int, data: Dictionary) -> void:
|
||||
var action_id := str(data.get("request_id", ""))
|
||||
var letter: Dictionary = _letters.get(str(data.get("mail_id", "")), {})
|
||||
if _action_ledger.has(action_id):
|
||||
return
|
||||
if (
|
||||
letter.is_empty()
|
||||
or int(letter["recipient_peer_id"]) != peer_id
|
||||
or int(letter["state"]) not in [
|
||||
NetworkMailProtocol.State.SENT_UNREAD,
|
||||
NetworkMailProtocol.State.READ,
|
||||
]
|
||||
):
|
||||
return
|
||||
_action_ledger[action_id] = true
|
||||
letter["state"] = NetworkMailProtocol.State.DECLINED
|
||||
_request_release(int(letter["sender_peer_id"]), letter["reservation_id"])
|
||||
_update_participants(letter)
|
||||
|
||||
|
||||
func _request_release(peer_id: int, reservation_id: String) -> void:
|
||||
if reservation_id.is_empty():
|
||||
return
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
_reservations.release(reservation_id)
|
||||
else:
|
||||
release_reservation.rpc_id(peer_id, reservation_id)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
|
||||
func release_reservation(reservation_id: String) -> void:
|
||||
_reservations.release(reservation_id)
|
||||
|
||||
|
||||
func _update_participants(letter: Dictionary) -> void:
|
||||
_letters[letter["mail_id"]] = letter
|
||||
_deliver_private_letter(int(letter["sender_peer_id"]), letter)
|
||||
_deliver_private_letter(int(letter["recipient_peer_id"]), letter)
|
||||
|
||||
|
||||
func _action_request(mail_id: String) -> Dictionary:
|
||||
if mail_id.is_empty() or not _local_letters.has(mail_id):
|
||||
return {}
|
||||
return {
|
||||
"request_id": _new_id("mail_action"),
|
||||
"mail_id": mail_id,
|
||||
"session_id": _session.get_session_id(),
|
||||
}
|
||||
|
||||
|
||||
func _validate_attachment_structure(attachment: Dictionary) -> bool:
|
||||
if typeof(attachment.get("type")) != TYPE_INT:
|
||||
return false
|
||||
match int(attachment["type"]):
|
||||
PlayerAssetReservationService.AttachmentType.NONE:
|
||||
return attachment.size() == 1
|
||||
PlayerAssetReservationService.AttachmentType.FISH_COIN:
|
||||
return (
|
||||
attachment.size() == 2
|
||||
and
|
||||
typeof(attachment.get("amount")) == TYPE_INT
|
||||
and int(attachment["amount"]) >= 1
|
||||
and int(attachment["amount"]) <= 1000000000
|
||||
)
|
||||
PlayerAssetReservationService.AttachmentType.FISH:
|
||||
if not (
|
||||
attachment.size() == 3
|
||||
and
|
||||
typeof(attachment.get("catch_id")) == TYPE_STRING
|
||||
and typeof(attachment.get("catch")) == TYPE_DICTIONARY
|
||||
):
|
||||
return false
|
||||
var fish_catch := _decode_catch(attachment)
|
||||
return (
|
||||
fish_catch != null
|
||||
and str(fish_catch.catch_id) == str(attachment["catch_id"])
|
||||
and not bool(
|
||||
Dictionary(attachment["catch"]).get("is_favorited", false)
|
||||
)
|
||||
)
|
||||
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
||||
var item := _item_catalog.get_item_by_id(
|
||||
StringName(str(attachment.get("item_id", "")))
|
||||
)
|
||||
return (
|
||||
attachment.size() == 3
|
||||
and
|
||||
item != null
|
||||
and item.category == ItemData.Category.CONSUMABLE
|
||||
and typeof(attachment.get("quantity")) == TYPE_INT
|
||||
and int(attachment["quantity"]) >= 1
|
||||
and int(attachment["quantity"]) <= item.max_stack
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
func _can_receive(attachment: Dictionary) -> bool:
|
||||
match int(attachment.get("type", 0)):
|
||||
PlayerAssetReservationService.AttachmentType.FISH_COIN:
|
||||
return _wallet.can_credit(int(attachment["amount"]))
|
||||
PlayerAssetReservationService.AttachmentType.FISH:
|
||||
return (
|
||||
_inventory.get_all_catches().size() < _cooler_capacity.get_capacity()
|
||||
and not _inventory.contains_catch_id(
|
||||
StringName(str(attachment["catch_id"]))
|
||||
)
|
||||
and _decode_catch(attachment) != null
|
||||
)
|
||||
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
||||
return _bag.can_add_item(
|
||||
StringName(str(attachment["item_id"])),
|
||||
int(attachment["quantity"])
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
func _apply_award(attachment: Dictionary) -> bool:
|
||||
if not _can_receive(attachment):
|
||||
return false
|
||||
match int(attachment["type"]):
|
||||
PlayerAssetReservationService.AttachmentType.FISH_COIN:
|
||||
return _wallet.credit(int(attachment["amount"]))
|
||||
PlayerAssetReservationService.AttachmentType.FISH:
|
||||
var fish_catch := _decode_catch(attachment)
|
||||
_inventory.add_catch(fish_catch)
|
||||
_collection_log.mark_discovered(fish_catch.fish_id)
|
||||
return _inventory.contains_catch_id(fish_catch.catch_id)
|
||||
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
||||
return _bag.add_item(
|
||||
StringName(str(attachment["item_id"])),
|
||||
int(attachment["quantity"])
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
func _decode_catch(attachment: Dictionary) -> FishCatch:
|
||||
var data: Dictionary = attachment.get("catch", {})
|
||||
var fish := _fish_catalog.get_fish_by_id(
|
||||
StringName(str(data.get("fish_id", "")))
|
||||
)
|
||||
return FishCatch.from_network_dict(data, fish)
|
||||
|
||||
|
||||
func _capture_assets() -> Dictionary:
|
||||
return {
|
||||
"wallet": _wallet.get_balance(),
|
||||
"bag": _bag.get_all_items(),
|
||||
"catches": _inventory.get_all_catches(),
|
||||
"next_sequence": _inventory.get_next_catch_sequence(),
|
||||
"discovered": _collection_log.get_discovered_ids(),
|
||||
}
|
||||
|
||||
|
||||
func _restore_assets(snapshot: Dictionary) -> void:
|
||||
_wallet.restore_balance(int(snapshot["wallet"]))
|
||||
_bag.replace_all_items(snapshot["bag"])
|
||||
_inventory.replace_all_catches(
|
||||
snapshot["catches"], int(snapshot["next_sequence"])
|
||||
)
|
||||
_collection_log.replace_discovered_ids(snapshot["discovered"])
|
||||
|
||||
|
||||
func _emit_mailbox() -> void:
|
||||
mailbox_changed.emit()
|
||||
unread_count_changed.emit(get_unread_count())
|
||||
|
||||
|
||||
func _on_peer_removed(peer_id: int) -> void:
|
||||
if not _session.is_host():
|
||||
_clear_local_mail_for_peer(peer_id)
|
||||
return
|
||||
for letter: Dictionary in _letters.values():
|
||||
if int(letter["recipient_peer_id"]) == peer_id:
|
||||
if int(letter["state"]) in [
|
||||
NetworkMailProtocol.State.SENT_UNREAD,
|
||||
NetworkMailProtocol.State.READ,
|
||||
]:
|
||||
letter["state"] = NetworkMailProtocol.State.CANCELLED
|
||||
_request_release(
|
||||
int(letter["sender_peer_id"]), letter["reservation_id"]
|
||||
)
|
||||
_deliver_private_letter(
|
||||
int(letter["sender_peer_id"]), letter
|
||||
)
|
||||
elif (
|
||||
int(letter["sender_peer_id"]) == peer_id
|
||||
and int(letter["state"]) in [
|
||||
NetworkMailProtocol.State.SENT_UNREAD,
|
||||
NetworkMailProtocol.State.READ,
|
||||
]
|
||||
):
|
||||
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,
|
||||
NetworkSession.State.DISCONNECTING,
|
||||
NetworkSession.State.CONNECTION_FAILED,
|
||||
NetworkSession.State.SERVER_LOST,
|
||||
]:
|
||||
_reservations.release_all()
|
||||
_letters.clear()
|
||||
_local_letters.clear()
|
||||
_send_ledger.clear()
|
||||
_action_ledger.clear()
|
||||
_pending_local_send.clear()
|
||||
_pending_transfers.clear()
|
||||
_local_removal_snapshots.clear()
|
||||
_received_awards.clear()
|
||||
_sequence = 0
|
||||
_emit_mailbox()
|
||||
|
||||
|
||||
func _bound(values: Dictionary) -> void:
|
||||
while values.size() > MAX_LEDGER:
|
||||
values.erase(values.keys().front())
|
||||
|
||||
|
||||
func _new_id(prefix: String) -> String:
|
||||
return "%s:%s" % [
|
||||
prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
|
||||
]
|
||||
1
network/network_mail_service.gd.uid
Normal file
1
network/network_mail_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://t235k67v0dno
|
||||
|
|
@ -9,12 +9,13 @@ const MAX_NONCE_LENGTH: int = 96
|
|||
# ENet channels: 0 reliable lifecycle, 1 movement input, 2 movement
|
||||
# snapshots, 3 fishing input, 4 fishing snapshots, 5 reliable sales,
|
||||
# 6 reliable shop transactions, 7 reliable item/equipment lifecycle,
|
||||
# 8 reliable ordered session chat.
|
||||
# 8 reliable ordered session chat, 9 reliable private session mail.
|
||||
const SALE_RELIABLE_CHANNEL: int = 5
|
||||
const SHOP_RELIABLE_CHANNEL: int = 6
|
||||
const ITEM_RELIABLE_CHANNEL: int = 7
|
||||
const CHAT_RELIABLE_CHANNEL: int = 8
|
||||
const ENET_CHANNEL_COUNT: int = 9
|
||||
const MAIL_RELIABLE_CHANNEL: int = 9
|
||||
const ENET_CHANNEL_COUNT: int = 10
|
||||
|
||||
enum RejectionCode {
|
||||
NONE,
|
||||
|
|
@ -116,6 +117,7 @@ static func make_server_hello(
|
|||
"item_use_v1",
|
||||
"equipment_v1",
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
]),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ var _applied_results: Dictionary[String, bool] = {}
|
|||
var _received_results: Dictionary[String, bool] = {}
|
||||
var _pending_local_request_id: String = ""
|
||||
var _pending_local_catch_ids: Array[StringName] = []
|
||||
var _reservations: PlayerAssetReservationService
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -51,6 +52,7 @@ func setup(
|
|||
fish_catalog: FishPoolType,
|
||||
buyer: FishBuyerProfileType,
|
||||
pelican_landmark: Node3D,
|
||||
reservations: PlayerAssetReservationService,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
|
|
@ -62,6 +64,7 @@ func setup(
|
|||
_fish_catalog = fish_catalog
|
||||
_buyer = buyer
|
||||
_pelican_landmark = pelican_landmark
|
||||
_reservations = reservations
|
||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||
|
|
@ -86,6 +89,12 @@ func is_local_sale_pending() -> bool:
|
|||
|
||||
|
||||
func request_local_sale(catch_ids: Array[StringName]) -> String:
|
||||
for catch_id: StringName in catch_ids:
|
||||
if _reservations != null and _reservations.is_fish_reserved(catch_id):
|
||||
local_sale_finished.emit(
|
||||
"", false, "Reserved in a letter.", [], 0
|
||||
)
|
||||
return ""
|
||||
if is_local_sale_pending():
|
||||
local_sale_finished.emit(
|
||||
"", false, "Selling…", [], 0
|
||||
|
|
@ -346,6 +355,10 @@ func _apply_sale_result(data: Dictionary) -> void:
|
|||
if catch_ids != _pending_local_catch_ids:
|
||||
_fail_local_apply(data, "Sale could not be completed.")
|
||||
return
|
||||
for catch_id: StringName in catch_ids:
|
||||
if _reservations != null and _reservations.is_fish_reserved(catch_id):
|
||||
_fail_local_apply(data, "Reserved in a letter.")
|
||||
return
|
||||
var preview: FishSaleResultType = _sale_service.preview_batch(
|
||||
catch_ids, _buyer
|
||||
)
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
return str(capability) in PackedStringArray([
|
||||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||
"item_use_v1", "equipment_v1", "chat_v1",
|
||||
"mail_v1",
|
||||
])
|
||||
return str(capability) in _server_capabilities
|
||||
|
||||
|
|
@ -285,6 +286,10 @@ func get_peer_record(peer_id: int) -> PeerRegistry.PeerRecord:
|
|||
return _registry.get_peer(peer_id)
|
||||
|
||||
|
||||
func get_authenticated_peer_ids() -> Array[int]:
|
||||
return _registry.get_peer_ids()
|
||||
|
||||
|
||||
func update_local_display_name(value: String) -> bool:
|
||||
if _profile == null or not _profile.set_display_name(value):
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ var _acknowledged_results: Dictionary[String, bool] = {}
|
|||
var _received_results: Dictionary[String, bool] = {}
|
||||
var _applied_results: Dictionary[String, bool] = {}
|
||||
var _pending_local_request: Dictionary = {}
|
||||
var _reservations: PlayerAssetReservationService
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -54,6 +55,7 @@ func setup(
|
|||
upgrades: PlayerFishingUpgrades,
|
||||
cooler_capacity: PlayerCoolerCapacity,
|
||||
save_manager: PlayerSaveManager,
|
||||
reservations: PlayerAssetReservationService,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
|
|
@ -65,6 +67,7 @@ func setup(
|
|||
_upgrades = upgrades
|
||||
_cooler_capacity = cooler_capacity
|
||||
_save_manager = save_manager
|
||||
_reservations = reservations
|
||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||
|
|
@ -464,6 +467,11 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
var product_id := StringName(str(data["product_id"]))
|
||||
var expected_state: int = data["expected_state"]
|
||||
var cost: int = data["total_cost"]
|
||||
if (
|
||||
_reservations != null
|
||||
and _reservations.get_available_fish_coin() < cost
|
||||
):
|
||||
return "Reserved in a letter."
|
||||
if int(data["resulting_state"]) != expected_state + 1:
|
||||
return "Purchase could not be completed."
|
||||
if not _wallet.can_afford(cost):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue