Add multiplayer item use and session chat
This commit is contained in:
parent
5e04b70609
commit
5dda74c9e4
27 changed files with 1361 additions and 31 deletions
388
network/network_item_use_service.gd
Normal file
388
network/network_item_use_service.gd
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
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(),
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue