feat: expand progression and multiplayer systems
Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
This commit is contained in:
parent
1db1a5b754
commit
3b84bfe3a0
97 changed files with 7869 additions and 982 deletions
|
|
@ -3,6 +3,15 @@ extends Node
|
|||
|
||||
signal entries_changed
|
||||
signal moderation_finished(success: bool, message: String)
|
||||
signal friend_request_received(
|
||||
request_id: String,
|
||||
fingerprint: String,
|
||||
display_name: String,
|
||||
)
|
||||
signal friend_action_finished(success: bool, message: String)
|
||||
|
||||
const MAX_PENDING_FRIEND_REQUESTS := 16
|
||||
const FRIEND_REQUEST_ID_LENGTH := 32
|
||||
|
||||
var _session: NetworkSession
|
||||
var _relationships: PlayerRelationshipStore
|
||||
|
|
@ -18,6 +27,9 @@ var _peer_fingerprints: Dictionary[int, String] = {}
|
|||
var _remote_bans: Array[Dictionary] = []
|
||||
var _ban_snapshot_requested: bool = false
|
||||
var _ban_snapshot_loaded: bool = false
|
||||
var _pending_outgoing_friends: Dictionary[String, Dictionary] = {}
|
||||
var _pending_incoming_friends: Dictionary[String, Dictionary] = {}
|
||||
var _host_friend_routes: Dictionary[String, Dictionary] = {}
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -74,6 +86,18 @@ func get_entries() -> Array[PlayerListEntry]:
|
|||
entry.ping_to_host_ms = _session.get_peer_rtt_ms(peer_id)
|
||||
entry.muted = _relationships.is_muted(record.identity_fingerprint)
|
||||
entry.blocked = blocked
|
||||
entry.is_friend = _relationships.is_friend(record.identity_fingerprint)
|
||||
entry.can_request_friend = (
|
||||
not entry.is_local_player
|
||||
and not entry.is_friend
|
||||
and not entry.blocked
|
||||
and _session.supports_server_capability(
|
||||
NetworkProtocol.FRIENDS_CAPABILITY
|
||||
)
|
||||
and _session.peer_supports_capability(
|
||||
peer_id, NetworkProtocol.FRIENDS_CAPABILITY
|
||||
)
|
||||
)
|
||||
entry.can_kick = (
|
||||
_session.can_local_moderate()
|
||||
and peer_id != local_id
|
||||
|
|
@ -125,6 +149,10 @@ func get_relationships() -> Array[Dictionary]:
|
|||
return _relationships.get_records()
|
||||
|
||||
|
||||
func get_friends() -> Array[Dictionary]:
|
||||
return _relationships.get_friends()
|
||||
|
||||
|
||||
func get_bans() -> Array[Dictionary]:
|
||||
if _session.is_host():
|
||||
return _bans.get_bans(_session.get_host_identity_fingerprint())
|
||||
|
|
@ -180,6 +208,160 @@ func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool
|
|||
return _relationships.set_blocked(fingerprint, display_name, value)
|
||||
|
||||
|
||||
func remove_friend(fingerprint: String, display_name: String) -> bool:
|
||||
if fingerprint == _session.get_local_identity_fingerprint():
|
||||
return false
|
||||
var ok := _relationships.remove_friend(fingerprint, display_name)
|
||||
friend_action_finished.emit(
|
||||
ok,
|
||||
"Friend removed." if ok else "Friend could not be removed.",
|
||||
)
|
||||
return ok
|
||||
|
||||
|
||||
func send_friend_request(
|
||||
peer_id: int,
|
||||
fingerprint: String,
|
||||
display_name: String,
|
||||
) -> bool:
|
||||
if (
|
||||
_pending_outgoing_friends.size() >= MAX_PENDING_FRIEND_REQUESTS
|
||||
or fingerprint == _session.get_local_identity_fingerprint()
|
||||
or _relationships.is_blocked(fingerprint)
|
||||
or _relationships.is_friend(fingerprint)
|
||||
or not _valid_friend_target(peer_id, fingerprint)
|
||||
):
|
||||
friend_action_finished.emit(
|
||||
false, "This person needs to be online to do this."
|
||||
)
|
||||
return false
|
||||
var local_capabilities: Dictionary = (
|
||||
_relationships.create_friend_capabilities()
|
||||
)
|
||||
if local_capabilities.is_empty():
|
||||
friend_action_finished.emit(false, "Friend request could not be created.")
|
||||
return false
|
||||
var request_id := NetworkIdentityCrypto.secure_id(16)
|
||||
_pending_outgoing_friends[request_id] = {
|
||||
"target_peer_id": peer_id,
|
||||
"target_fingerprint": fingerprint,
|
||||
"target_display_name": display_name,
|
||||
"local_capabilities": local_capabilities,
|
||||
}
|
||||
var public_capabilities := _public_friend_capabilities(local_capabilities)
|
||||
if _session.is_host():
|
||||
_route_friend_request(
|
||||
_session.get_local_peer_id(), peer_id, request_id, public_capabilities
|
||||
)
|
||||
else:
|
||||
request_friendship.rpc_id(
|
||||
1, peer_id, request_id, public_capabilities
|
||||
)
|
||||
friend_action_finished.emit(true, "Friend request sent.")
|
||||
return true
|
||||
|
||||
|
||||
func respond_friend_request(request_id: String, accepted: bool) -> bool:
|
||||
var pending: Dictionary = _pending_incoming_friends.get(request_id, {})
|
||||
if pending.is_empty():
|
||||
friend_action_finished.emit(
|
||||
false, "This friend request is no longer available."
|
||||
)
|
||||
return false
|
||||
_pending_incoming_friends.erase(request_id)
|
||||
var local_capabilities: Dictionary = {}
|
||||
var public_capabilities: Dictionary = {}
|
||||
var response_accepted := accepted
|
||||
if accepted:
|
||||
local_capabilities = _relationships.create_friend_capabilities()
|
||||
response_accepted = (
|
||||
not local_capabilities.is_empty()
|
||||
and _relationships.add_friend(
|
||||
str(pending["requester_fingerprint"]),
|
||||
str(pending["requester_display_name"]),
|
||||
local_capabilities,
|
||||
pending["remote_capabilities"],
|
||||
)
|
||||
)
|
||||
if response_accepted:
|
||||
public_capabilities = _public_friend_capabilities(local_capabilities)
|
||||
if _session.is_host():
|
||||
_route_friend_response(
|
||||
_session.get_local_peer_id(),
|
||||
request_id,
|
||||
response_accepted,
|
||||
public_capabilities,
|
||||
)
|
||||
else:
|
||||
respond_friendship.rpc_id(
|
||||
1, request_id, response_accepted, public_capabilities
|
||||
)
|
||||
friend_action_finished.emit(
|
||||
response_accepted or not accepted,
|
||||
"Friend added."
|
||||
if response_accepted
|
||||
else "Friend request declined."
|
||||
if not accepted
|
||||
else "Friend could not be saved.",
|
||||
)
|
||||
return response_accepted or not accepted
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||
func request_friendship(
|
||||
target_peer_id: int,
|
||||
request_id: String,
|
||||
public_capabilities: Dictionary,
|
||||
) -> void:
|
||||
_route_friend_request(
|
||||
multiplayer.get_remote_sender_id(),
|
||||
target_peer_id,
|
||||
request_id,
|
||||
public_capabilities,
|
||||
)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
func receive_friend_request(
|
||||
request_id: String,
|
||||
requester_fingerprint: String,
|
||||
requester_display_name: String,
|
||||
public_capabilities: Dictionary,
|
||||
) -> void:
|
||||
_receive_friend_request_local(
|
||||
request_id,
|
||||
requester_fingerprint,
|
||||
requester_display_name,
|
||||
public_capabilities,
|
||||
)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||
func respond_friendship(
|
||||
request_id: String,
|
||||
accepted: bool,
|
||||
public_capabilities: Dictionary,
|
||||
) -> void:
|
||||
_route_friend_response(
|
||||
multiplayer.get_remote_sender_id(),
|
||||
request_id,
|
||||
accepted,
|
||||
public_capabilities,
|
||||
)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
func receive_friend_result(
|
||||
request_id: String,
|
||||
accepted: bool,
|
||||
public_capabilities: Dictionary,
|
||||
message: String,
|
||||
) -> void:
|
||||
_receive_friend_result_local(
|
||||
request_id, accepted, public_capabilities, message
|
||||
)
|
||||
|
||||
|
||||
func kick(peer_id: int, fingerprint: String, revision: int) -> bool:
|
||||
if _session.is_host():
|
||||
return _kick_on_host(peer_id, fingerprint, revision, true)
|
||||
|
|
@ -454,6 +636,32 @@ func _on_peer_removed(peer_id: int) -> void:
|
|||
for key: String in _host_block_pairs.keys():
|
||||
if fingerprint in key.split(":"):
|
||||
_host_block_pairs.erase(key)
|
||||
for request_id: String in _host_friend_routes.keys():
|
||||
var route: Dictionary = _host_friend_routes[request_id]
|
||||
if int(route.get("target_peer_id", 0)) == peer_id:
|
||||
_send_friend_result_to_peer(
|
||||
int(route.get("requester_peer_id", 0)),
|
||||
request_id,
|
||||
false,
|
||||
{},
|
||||
"This person needs to be online to do this.",
|
||||
)
|
||||
if (
|
||||
int(route.get("target_peer_id", 0)) == peer_id
|
||||
or int(route.get("requester_peer_id", 0)) == peer_id
|
||||
):
|
||||
_host_friend_routes.erase(request_id)
|
||||
for request_id: String in _pending_outgoing_friends.keys():
|
||||
var pending: Dictionary = _pending_outgoing_friends[request_id]
|
||||
if str(pending.get("target_fingerprint", "")) == fingerprint:
|
||||
_pending_outgoing_friends.erase(request_id)
|
||||
friend_action_finished.emit(
|
||||
false, "This person needs to be online to do this."
|
||||
)
|
||||
for request_id: String in _pending_incoming_friends.keys():
|
||||
var pending: Dictionary = _pending_incoming_friends[request_id]
|
||||
if str(pending.get("requester_fingerprint", "")) == fingerprint:
|
||||
_pending_incoming_friends.erase(request_id)
|
||||
_changed()
|
||||
|
||||
|
||||
|
|
@ -469,6 +677,9 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_remote_bans.clear()
|
||||
_ban_snapshot_requested = false
|
||||
_ban_snapshot_loaded = false
|
||||
_pending_outgoing_friends.clear()
|
||||
_pending_incoming_friends.clear()
|
||||
_host_friend_routes.clear()
|
||||
elif state == NetworkSession.State.JOINED_CLIENT:
|
||||
_request_ban_snapshot()
|
||||
_changed()
|
||||
|
|
@ -506,6 +717,245 @@ func _peer_for_fingerprint(fingerprint: String) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
func _valid_friend_target(peer_id: int, fingerprint: String) -> bool:
|
||||
if (
|
||||
not _session.is_gameplay_session_active()
|
||||
or not _session.supports_server_capability(
|
||||
NetworkProtocol.FRIENDS_CAPABILITY
|
||||
)
|
||||
or not _session.peer_supports_capability(
|
||||
peer_id, NetworkProtocol.FRIENDS_CAPABILITY
|
||||
)
|
||||
):
|
||||
return false
|
||||
var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
|
||||
return (
|
||||
record != null
|
||||
and record.identity_authenticated
|
||||
and record.identity_fingerprint == fingerprint
|
||||
)
|
||||
|
||||
|
||||
func _route_friend_request(
|
||||
requester_peer_id: int,
|
||||
target_peer_id: int,
|
||||
request_id: String,
|
||||
public_capabilities: Dictionary,
|
||||
) -> void:
|
||||
if (
|
||||
not _session.is_host()
|
||||
or not _valid_friend_request_id(request_id)
|
||||
or not _valid_public_friend_capabilities(public_capabilities)
|
||||
or not _session.is_authenticated_peer(requester_peer_id)
|
||||
):
|
||||
return
|
||||
var requester: PeerRegistry.PeerRecord = (
|
||||
_session.get_peer_record(requester_peer_id)
|
||||
)
|
||||
var target: PeerRegistry.PeerRecord = _session.get_peer_record(target_peer_id)
|
||||
var can_route := (
|
||||
requester != null
|
||||
and target != null
|
||||
and requester_peer_id != target_peer_id
|
||||
and _valid_friend_target(
|
||||
target_peer_id, target.identity_fingerprint
|
||||
)
|
||||
and _session.peer_supports_capability(
|
||||
requester_peer_id, NetworkProtocol.FRIENDS_CAPABILITY
|
||||
)
|
||||
and not pair_is_blocked(
|
||||
requester.identity_fingerprint, target.identity_fingerprint
|
||||
)
|
||||
and _host_friend_routes.size() < (
|
||||
MAX_PENDING_FRIEND_REQUESTS * 8
|
||||
)
|
||||
)
|
||||
if not can_route:
|
||||
_send_friend_result_to_peer(
|
||||
requester_peer_id,
|
||||
request_id,
|
||||
false,
|
||||
{},
|
||||
"This person needs to be online to do this.",
|
||||
)
|
||||
return
|
||||
var requester_pending_count := 0
|
||||
for route: Dictionary in _host_friend_routes.values():
|
||||
if int(route.get("requester_peer_id", 0)) == requester_peer_id:
|
||||
requester_pending_count += 1
|
||||
if requester_pending_count >= MAX_PENDING_FRIEND_REQUESTS:
|
||||
_send_friend_result_to_peer(
|
||||
requester_peer_id,
|
||||
request_id,
|
||||
false,
|
||||
{},
|
||||
"Too many friend requests are pending.",
|
||||
)
|
||||
return
|
||||
_host_friend_routes[request_id] = {
|
||||
"requester_peer_id": requester_peer_id,
|
||||
"target_peer_id": target_peer_id,
|
||||
}
|
||||
if target_peer_id == _session.get_local_peer_id():
|
||||
_receive_friend_request_local(
|
||||
request_id,
|
||||
requester.identity_fingerprint,
|
||||
requester.display_name,
|
||||
public_capabilities,
|
||||
)
|
||||
else:
|
||||
receive_friend_request.rpc_id(
|
||||
target_peer_id,
|
||||
request_id,
|
||||
requester.identity_fingerprint,
|
||||
requester.display_name,
|
||||
public_capabilities,
|
||||
)
|
||||
|
||||
|
||||
func _receive_friend_request_local(
|
||||
request_id: String,
|
||||
requester_fingerprint: String,
|
||||
requester_display_name: String,
|
||||
public_capabilities: Dictionary,
|
||||
) -> void:
|
||||
if (
|
||||
not _valid_friend_request_id(request_id)
|
||||
or not _valid_public_friend_capabilities(public_capabilities)
|
||||
or not NetworkIdentityCrypto.valid_fingerprint(requester_fingerprint)
|
||||
or not NetworkProfilePreferences.is_valid_display_name(
|
||||
requester_display_name
|
||||
)
|
||||
or _pending_incoming_friends.size()
|
||||
>= MAX_PENDING_FRIEND_REQUESTS
|
||||
or _relationships.is_blocked(requester_fingerprint)
|
||||
or _relationships.is_friend(requester_fingerprint)
|
||||
):
|
||||
_decline_friend_request(request_id)
|
||||
return
|
||||
_pending_incoming_friends[request_id] = {
|
||||
"requester_fingerprint": requester_fingerprint,
|
||||
"requester_display_name": requester_display_name,
|
||||
"remote_capabilities": public_capabilities.duplicate(true),
|
||||
}
|
||||
friend_request_received.emit(
|
||||
request_id, requester_fingerprint, requester_display_name
|
||||
)
|
||||
|
||||
|
||||
func _decline_friend_request(request_id: String) -> void:
|
||||
if _session.is_host():
|
||||
_route_friend_response(
|
||||
_session.get_local_peer_id(), request_id, false, {}
|
||||
)
|
||||
elif _session.is_joined_client():
|
||||
respond_friendship.rpc_id(1, request_id, false, {})
|
||||
|
||||
|
||||
func _route_friend_response(
|
||||
responder_peer_id: int,
|
||||
request_id: String,
|
||||
accepted: bool,
|
||||
public_capabilities: Dictionary,
|
||||
) -> void:
|
||||
if not _session.is_host():
|
||||
return
|
||||
var route: Dictionary = _host_friend_routes.get(request_id, {})
|
||||
if (
|
||||
route.is_empty()
|
||||
or int(route.get("target_peer_id", 0)) != responder_peer_id
|
||||
):
|
||||
return
|
||||
_host_friend_routes.erase(request_id)
|
||||
var response_accepted := (
|
||||
accepted and _valid_public_friend_capabilities(public_capabilities)
|
||||
)
|
||||
_send_friend_result_to_peer(
|
||||
int(route["requester_peer_id"]),
|
||||
request_id,
|
||||
response_accepted,
|
||||
public_capabilities if response_accepted else {},
|
||||
"Friend request accepted."
|
||||
if response_accepted
|
||||
else "Friend request declined."
|
||||
if not accepted
|
||||
else "Friend request could not be completed.",
|
||||
)
|
||||
|
||||
|
||||
func _send_friend_result_to_peer(
|
||||
peer_id: int,
|
||||
request_id: String,
|
||||
accepted: bool,
|
||||
public_capabilities: Dictionary,
|
||||
message: String,
|
||||
) -> void:
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
_receive_friend_result_local(
|
||||
request_id, accepted, public_capabilities, message
|
||||
)
|
||||
elif _session.is_authenticated_peer(peer_id):
|
||||
receive_friend_result.rpc_id(
|
||||
peer_id,
|
||||
request_id,
|
||||
accepted,
|
||||
public_capabilities,
|
||||
message.left(120),
|
||||
)
|
||||
|
||||
|
||||
func _receive_friend_result_local(
|
||||
request_id: String,
|
||||
accepted: bool,
|
||||
public_capabilities: Dictionary,
|
||||
message: String,
|
||||
) -> void:
|
||||
var pending: Dictionary = _pending_outgoing_friends.get(request_id, {})
|
||||
if pending.is_empty():
|
||||
return
|
||||
_pending_outgoing_friends.erase(request_id)
|
||||
var success := false
|
||||
if accepted and _valid_public_friend_capabilities(public_capabilities):
|
||||
success = _relationships.add_friend(
|
||||
str(pending["target_fingerprint"]),
|
||||
str(pending["target_display_name"]),
|
||||
pending["local_capabilities"],
|
||||
public_capabilities,
|
||||
)
|
||||
message = "Friend added." if success else "Friend could not be saved."
|
||||
friend_action_finished.emit(success, message.left(120))
|
||||
|
||||
|
||||
func _public_friend_capabilities(capabilities: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"presence_channel": str(capabilities.get("presence_channel", "")),
|
||||
"invite_token": str(capabilities.get("invite_token", "")),
|
||||
}
|
||||
|
||||
|
||||
func _valid_public_friend_capabilities(capabilities: Dictionary) -> bool:
|
||||
return (
|
||||
_valid_friend_hex(str(capabilities.get("presence_channel", "")))
|
||||
and _valid_friend_hex(str(capabilities.get("invite_token", "")))
|
||||
)
|
||||
|
||||
|
||||
func _valid_friend_request_id(request_id: String) -> bool:
|
||||
return _valid_friend_hex(request_id, FRIEND_REQUEST_ID_LENGTH)
|
||||
|
||||
|
||||
func _valid_friend_hex(
|
||||
value: String,
|
||||
expected_length: int = PlayerRelationshipStore.SOCIAL_TOKEN_LENGTH,
|
||||
) -> bool:
|
||||
if value.length() != expected_length:
|
||||
return false
|
||||
for character: String in value:
|
||||
if character not in "0123456789abcdef":
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _valid_moderation_target(
|
||||
peer_id: int,
|
||||
fingerprint: String,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue