netfishing/network/network_item_use_service.gd

389 lines
12 KiB
GDScript3
Raw Normal View History

class_name NetworkItemUseService
extends Node
const MAX_LEDGER_ENTRIES: int = 64
signal local_item_use_pending(request_id: String)
signal local_item_use_finished(accepted: bool, message: String)
signal equipped_state_changed(peer_id: int, item_id: StringName, category: int)
var _session: NetworkSession
var _spawn_service: PlayerSpawnService
var _catalog: ItemCatalog
var _local_bag: PlayerBag
var _local_effects: PlayerItemEffects
var _save_manager: PlayerSaveManager
var _requests: Dictionary[int, Dictionary] = {}
var _pending_by_peer: Dictionary[int, String] = {}
var _result_owners: Dictionary[String, int] = {}
var _received_results: Dictionary[String, bool] = {}
var _pending_local: Dictionary = {}
var _equipped_states: Dictionary[int, Dictionary] = {}
var _local_equipped_revision: int = 0
func setup(
session: NetworkSession,
spawn_service: PlayerSpawnService,
catalog: ItemCatalog,
local_bag: PlayerBag,
local_effects: PlayerItemEffects,
save_manager: PlayerSaveManager,
) -> void:
_session = session
_spawn_service = spawn_service
_catalog = catalog
_local_bag = local_bag
_local_effects = local_effects
_save_manager = save_manager
_session.peer_removed.connect(_on_peer_removed)
_session.state_changed.connect(_on_session_state_changed)
_session.peer_authenticated.connect(_on_peer_authenticated)
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 (
_session == null
or not _session.is_gameplay_session_active()
or (
not _session.is_host()
and not _session.supports_server_capability(
NetworkItemProtocol.ITEM_USE_CAPABILITY
)
)
):
local_item_use_finished.emit(false, "Item use is unavailable.")
return ""
var request_id := _new_id("item")
var data := {
"request_id": request_id,
"session_id": _session.get_session_id(),
"item_id": str(item_id),
"quantity": _local_bag.get_quantity(item_id),
}
_pending_local = data.duplicate(true)
local_item_use_pending.emit(request_id)
if _session.is_host():
_handle_use_request(_session.get_local_peer_id(), data)
else:
submit_item_use.rpc_id(1, data)
return request_id
@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func submit_item_use(data: Dictionary) -> void:
var sender_id := multiplayer.get_remote_sender_id()
if _session.is_host() and _session.is_authenticated_peer(sender_id):
_handle_use_request(sender_id, data)
func _handle_use_request(peer_id: int, data: Dictionary) -> void:
var request_id := str(data.get("request_id", ""))
var ledger: Dictionary = _requests.get(peer_id, {})
if ledger.has(request_id):
_send_result(peer_id, ledger[request_id])
return
var error := NetworkItemProtocol.validate_use_request(data)
if (
error.is_empty()
and str(data["session_id"]) != _session.get_session_id()
):
error = "Item use could not be completed."
if error.is_empty() and _pending_by_peer.has(peer_id):
error = "An item use is already pending."
var item_id := StringName(str(data.get("item_id", "")))
var item: ItemData = _catalog.get_item_by_id(item_id)
var avatar: Player = _spawn_service.get_avatar(peer_id)
var duration: float = 0.0
if error.is_empty() and (
item == null
or item.category != ItemData.Category.CONSUMABLE
or not item.usable
):
error = "That item cannot be used now."
if error.is_empty() and int(data["quantity"]) < 1:
error = "You do not have that item."
if error.is_empty() and (
avatar == null
or avatar.is_water_recovery_active()
):
error = "That item cannot be used now."
if error.is_empty():
duration = avatar.item_effects.get_effect_duration(item_id)
if duration <= 0.0:
error = "That item cannot be used now."
var result := {
"result_id": _new_id("item_result"),
"request_id": request_id if not request_id.is_empty() else "invalid",
"session_id": _session.get_session_id(),
"target_peer_id": peer_id,
"accepted": error.is_empty(),
"item_id": str(item_id) if not item_id.is_empty() else "invalid",
"quantity": 1 if error.is_empty() else 0,
"duration": duration if error.is_empty() else 0.0,
"message": "Used %s." % item.display_name if error.is_empty() else error,
}
ledger[request_id] = result.duplicate(true)
_bound(ledger)
_requests[peer_id] = ledger
_result_owners[result["result_id"]] = peer_id
_pending_by_peer[peer_id] = request_id
_send_result(peer_id, result)
func _send_result(peer_id: int, result: Dictionary) -> void:
if peer_id == _session.get_local_peer_id():
_apply_result(result)
else:
receive_item_use_result.rpc_id(peer_id, result)
@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func receive_item_use_result(data: Dictionary) -> void:
_apply_result(data)
func _apply_result(data: Dictionary) -> void:
if (
not NetworkItemProtocol.validate_use_result(data)
or str(data["session_id"]) != _session.get_session_id()
or int(data["target_peer_id"]) != _session.get_local_peer_id()
):
return
var result_id: String = data["result_id"]
if _received_results.has(result_id):
_acknowledge(data, true, "")
return
if (
_pending_local.is_empty()
or str(data["request_id"]) != str(_pending_local.get("request_id", ""))
):
return
if not bool(data["accepted"]):
_received_results[result_id] = true
_pending_local.clear()
local_item_use_finished.emit(false, str(data["message"]))
_acknowledge(data, false, str(data["message"]))
return
var item_id := StringName(str(data["item_id"]))
var bag_snapshot := _local_bag.get_all_items()
if (
_local_bag.get_quantity(item_id) < 1
or not _local_bag.remove_item(item_id, 1)
or not _save_manager.save_if_dirty()
):
_local_bag.replace_all_items(bag_snapshot)
_save_manager.save_if_dirty()
_pending_local.clear()
local_item_use_finished.emit(false, "Item use could not be completed.")
_acknowledge(data, false, "Item use could not be completed.")
return
_received_results[result_id] = true
_bound(_received_results)
_pending_local.clear()
local_item_use_finished.emit(true, str(data["message"]))
_acknowledge(data, true, "")
func _acknowledge(data: Dictionary, applied: bool, message: String) -> void:
if _session.is_host():
_handle_ack(
_session.get_local_peer_id(),
str(data["result_id"]),
str(data["request_id"]),
applied,
message
)
else:
acknowledge_item_use.rpc_id(
1, str(data["result_id"]), str(data["request_id"]), applied,
message.left(NetworkItemProtocol.MAX_MESSAGE_LENGTH)
)
@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func acknowledge_item_use(
result_id: String,
request_id: String,
applied: bool,
message: String,
) -> void:
var sender_id := multiplayer.get_remote_sender_id()
if _session.is_host() and _session.is_authenticated_peer(sender_id):
_handle_ack(sender_id, result_id, request_id, applied, message)
func _handle_ack(
peer_id: int,
result_id: String,
request_id: String,
applied: bool,
_message: String,
) -> void:
if (
_result_owners.get(result_id, 0) != peer_id
or _pending_by_peer.get(peer_id, "") != request_id
):
return
_pending_by_peer.erase(peer_id)
if not applied:
return
var result: Dictionary = _requests.get(peer_id, {}).get(request_id, {})
var avatar := _spawn_service.get_avatar(peer_id)
if avatar == null or result.is_empty():
return
var item_id := StringName(str(result["item_id"]))
avatar.item_effects.activate_authoritative(item_id, float(result["duration"]))
_broadcast_effect(peer_id, item_id, float(result["duration"]))
func get_effects_for_peer(peer_id: int) -> PlayerItemEffects:
var avatar := _spawn_service.get_avatar(peer_id)
return avatar.item_effects if avatar != null else null
func submit_local_equipped(item_id: StringName, owns_item: bool) -> void:
if _session == null or not _session.is_gameplay_session_active():
return
_local_equipped_revision += 1
var item: ItemData = _catalog.get_item_by_id(item_id)
var data := {
"session_id": _session.get_session_id(),
"owner_peer_id": _session.get_local_peer_id(),
"item_id": str(item_id) if item != null and owns_item else "",
"category": int(item.category) if item != null and owns_item else -1,
"revision": _local_equipped_revision,
"owns_item": owns_item and item != null,
}
if _session.is_host():
_handle_equipped_state(_session.get_local_peer_id(), data)
elif _session.supports_server_capability(
NetworkItemProtocol.EQUIPMENT_CAPABILITY
):
submit_equipped_state.rpc_id(1, data)
@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func submit_equipped_state(data: Dictionary) -> void:
var sender_id := multiplayer.get_remote_sender_id()
if _session.is_host() and _session.is_authenticated_peer(sender_id):
_handle_equipped_state(sender_id, data)
func _handle_equipped_state(peer_id: int, data: Dictionary) -> void:
if (
not NetworkItemProtocol.validate_equipped_state(data)
or str(data["session_id"]) != _session.get_session_id()
or int(data["owner_peer_id"]) != peer_id
):
return
var previous: Dictionary = _equipped_states.get(peer_id, {})
if int(data["revision"]) <= int(previous.get("revision", -1)):
return
var item_id := StringName(str(data["item_id"]))
var item: ItemData = _catalog.get_item_by_id(item_id)
if not item_id.is_empty() and (
item == null
or not bool(data["owns_item"])
or int(item.category) != int(data["category"])
):
return
_equipped_states[peer_id] = data.duplicate(true)
_apply_equipped(data)
receive_equipped_state.rpc(data)
@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func receive_equipped_state(data: Dictionary) -> void:
if NetworkItemProtocol.validate_equipped_state(data):
_equipped_states[int(data["owner_peer_id"])] = data.duplicate(true)
_apply_equipped(data)
func _apply_equipped(data: Dictionary) -> void:
var peer_id: int = data["owner_peer_id"]
var avatar := _spawn_service.get_avatar(peer_id)
if avatar != null:
avatar.set_active_item_is_rod(
int(data["category"]) == ItemData.Category.ROD
)
equipped_state_changed.emit(
peer_id, StringName(str(data["item_id"])), int(data["category"])
)
func _broadcast_effect(peer_id: int, item_id: StringName, duration: float) -> void:
var data := {
"session_id": _session.get_session_id(),
"owner_peer_id": peer_id,
"item_id": str(item_id),
"remaining": duration,
}
_apply_effect_snapshot(data)
receive_effect_snapshot.rpc(data)
@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func receive_effect_snapshot(data: Dictionary) -> void:
_apply_effect_snapshot(data)
func _apply_effect_snapshot(data: Dictionary) -> void:
if (
typeof(data.get("owner_peer_id")) != TYPE_INT
or typeof(data.get("item_id")) != TYPE_STRING
or typeof(data.get("remaining")) not in [TYPE_FLOAT, TYPE_INT]
or str(data.get("session_id", "")) != _session.get_session_id()
):
return
var avatar := _spawn_service.get_avatar(int(data["owner_peer_id"]))
if avatar != null:
avatar.item_effects.activate_authoritative(
StringName(data["item_id"]), float(data["remaining"])
)
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
if not _session.is_host():
return
for state: Dictionary in _equipped_states.values():
receive_equipped_state.rpc_id(peer_id, state)
func _on_peer_removed(peer_id: int) -> void:
_requests.erase(peer_id)
_pending_by_peer.erase(peer_id)
_equipped_states.erase(peer_id)
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,
]:
var pending := not _pending_local.is_empty()
_requests.clear()
_pending_by_peer.clear()
_result_owners.clear()
_received_results.clear()
_equipped_states.clear()
_pending_local.clear()
if pending:
local_item_use_finished.emit(false, "Connection lost.")
func _bound(values: Dictionary) -> void:
while values.size() > MAX_LEDGER_ENTRIES:
values.erase(values.keys().front())
func _new_id(prefix: String) -> String:
return "%s:%s" % [
prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
]