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
|
|
@ -7,13 +7,50 @@ const DEFAULT_SESSION_MAX_PLAYERS: int = 8
|
|||
const DEFAULT_TRANSPORT_MAX_CLIENTS: int = 31
|
||||
const CONNECTION_TIMEOUT_SECONDS: float = 10.0
|
||||
const AUTHENTICATION_TIMEOUT_SECONDS: float = 60.0
|
||||
const ENET_TIMEOUT_LIMIT: int = 32
|
||||
const ENET_TIMEOUT_MINIMUM_MS: int = 10000
|
||||
const ENET_TIMEOUT_MAXIMUM_MS: int = 120000
|
||||
const INPUT_INTERVAL: float = 1.0 / 30.0
|
||||
const IDLE_INPUT_INTERVAL: float = 1.0 / 5.0
|
||||
const SNAPSHOT_INTERVAL: float = 1.0 / 30.0
|
||||
const NEAR_REMOTE_SNAPSHOT_DIVISOR: int = 2
|
||||
const FAR_REMOTE_SNAPSHOT_DIVISOR: int = 6
|
||||
const DISTANT_REMOTE_SNAPSHOT_DIVISOR: int = 8
|
||||
const NEAR_REMOTE_DISTANCE: float = 48.0
|
||||
const FAR_REMOTE_DISTANCE: float = 96.0
|
||||
const MOVEMENT_SNAPSHOT_BATCH_SIZE: int = 8
|
||||
const MOVEMENT_SNAPSHOT_FIELD_COUNT: int = 13
|
||||
const MOVEMENT_INPUT_FIELD_COUNT: int = 8
|
||||
const MOVEMENT_SNAPSHOT_FIELD_COUNT: int = 6
|
||||
const MOVEMENT_ANIMATION_FIELD_COUNT: int = 7
|
||||
const MOVEMENT_ANIMATION_ACTION_FIELD_COUNT: int = 5
|
||||
const MAX_PENDING_MOVEMENT_INPUTS: int = 96
|
||||
const ANIMATION_REFRESH_INTERVAL: float = 1.0
|
||||
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
||||
const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
|
||||
|
||||
const MOVEMENT_FLAG_JUMP: int = 1 << 0
|
||||
const MOVEMENT_FLAG_SPRINT: int = 1 << 1
|
||||
const MOVEMENT_FLAG_SNEAK: int = 1 << 2
|
||||
const MOVEMENT_FLAG_SLOW_WALK: int = 1 << 3
|
||||
const MOVEMENT_FLAG_SITTING: int = 1 << 4
|
||||
const MOVEMENT_FLAG_CASTING: int = 1 << 5
|
||||
const MOVEMENT_ALLOWED_FLAGS: int = (
|
||||
MOVEMENT_FLAG_JUMP
|
||||
| MOVEMENT_FLAG_SPRINT
|
||||
| MOVEMENT_FLAG_SNEAK
|
||||
| MOVEMENT_FLAG_SLOW_WALK
|
||||
| MOVEMENT_FLAG_SITTING
|
||||
| MOVEMENT_FLAG_CASTING
|
||||
)
|
||||
const SNAPSHOT_FLAG_GROUNDED: int = 1 << 0
|
||||
const SNAPSHOT_FLAG_SITTING: int = 1 << 1
|
||||
const SNAPSHOT_FLAG_CASTING: int = 1 << 2
|
||||
const SNAPSHOT_ALLOWED_FLAGS: int = (
|
||||
SNAPSHOT_FLAG_GROUNDED
|
||||
| SNAPSHOT_FLAG_SITTING
|
||||
| SNAPSHOT_FLAG_CASTING
|
||||
)
|
||||
|
||||
signal state_changed(state: State)
|
||||
signal status_message_changed(message: String)
|
||||
signal connection_error(message: String)
|
||||
|
|
@ -80,7 +117,21 @@ var _client_nonce: String = ""
|
|||
var _current_route: ConnectionRoute
|
||||
var _input_sequence: int = 0
|
||||
var _input_accumulator: float = 0.0
|
||||
var _idle_input_accumulator: float = 0.0
|
||||
var _last_input_state_hash: int = 0
|
||||
var _pending_movement_inputs: Array[Dictionary] = []
|
||||
var _snapshot_accumulator: float = 0.0
|
||||
var _movement_snapshot_tick: int = 0
|
||||
var _animation_refresh_accumulator: float = 0.0
|
||||
var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {}
|
||||
var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {}
|
||||
var _last_local_animation_action_signature: Array = []
|
||||
var _movement_inputs_sent: int = 0
|
||||
var _movement_inputs_received: int = 0
|
||||
var _movement_snapshot_packets_sent: int = 0
|
||||
var _movement_snapshot_states_sent: int = 0
|
||||
var _movement_animation_packets_sent: int = 0
|
||||
var _movement_animation_states_sent: int = 0
|
||||
var _last_server_max_players: int = DEFAULT_SESSION_MAX_PLAYERS
|
||||
var _last_server_player_count: int = 0
|
||||
var _last_server_display_name: String = ""
|
||||
|
|
@ -278,6 +329,9 @@ func _register_player_host() -> void:
|
|||
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
|
||||
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||
|
|
@ -602,6 +656,9 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
|
||||
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
|
||||
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
@ -649,6 +706,18 @@ func get_peer_rtt_ms(peer_id: int) -> int:
|
|||
return int(packet_peer.get_statistic(ENetPacketPeer.PEER_ROUND_TRIP_TIME))
|
||||
|
||||
|
||||
func get_movement_metrics() -> Dictionary:
|
||||
return {
|
||||
"inputs_sent": _movement_inputs_sent,
|
||||
"inputs_received": _movement_inputs_received,
|
||||
"snapshot_packets_sent": _movement_snapshot_packets_sent,
|
||||
"snapshot_states_sent": _movement_snapshot_states_sent,
|
||||
"animation_packets_sent": _movement_animation_packets_sent,
|
||||
"animation_states_sent": _movement_animation_states_sent,
|
||||
"pending_local_inputs": _pending_movement_inputs.size(),
|
||||
}
|
||||
|
||||
|
||||
func kick_authenticated_peer(
|
||||
peer_id: int,
|
||||
fingerprint: String,
|
||||
|
|
@ -813,6 +882,10 @@ func get_local_peer_id() -> int:
|
|||
return multiplayer.get_unique_id() if is_gameplay_session_active() else 0
|
||||
|
||||
|
||||
func get_latest_movement_input_sequence() -> int:
|
||||
return _input_sequence if state == State.JOINED_CLIENT else 0
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var now: float = Time.get_ticks_msec() / 1000.0
|
||||
if (
|
||||
|
|
@ -833,9 +906,13 @@ func _process(delta: float) -> void:
|
|||
return
|
||||
_input_accumulator += delta
|
||||
_snapshot_accumulator += delta
|
||||
_animation_refresh_accumulator += delta
|
||||
if state == State.JOINED_CLIENT and _input_accumulator >= INPUT_INTERVAL:
|
||||
var elapsed_input_time: float = _input_accumulator
|
||||
_input_accumulator = fmod(_input_accumulator, INPUT_INTERVAL)
|
||||
_send_local_input()
|
||||
_idle_input_accumulator += elapsed_input_time
|
||||
_maybe_send_local_animation_action()
|
||||
_maybe_send_local_input()
|
||||
if is_host() and _snapshot_accumulator >= SNAPSHOT_INTERVAL:
|
||||
_snapshot_accumulator = fmod(_snapshot_accumulator, SNAPSHOT_INTERVAL)
|
||||
_broadcast_movement_snapshots()
|
||||
|
|
@ -873,6 +950,7 @@ func _on_peer_connected(peer_id: int) -> void:
|
|||
if state != State.OPEN_HOST:
|
||||
multiplayer.multiplayer_peer.disconnect_peer(peer_id)
|
||||
return
|
||||
_configure_enet_peer_timeout(peer_id)
|
||||
_pending_authentication[peer_id] = (
|
||||
Time.get_ticks_msec() / 1000.0 + AUTHENTICATION_TIMEOUT_SECONDS
|
||||
)
|
||||
|
|
@ -881,6 +959,7 @@ func _on_peer_connected(peer_id: int) -> void:
|
|||
func _on_connected_to_server() -> void:
|
||||
if state != State.CONNECTING:
|
||||
return
|
||||
_configure_enet_peer_timeout(1)
|
||||
_set_state(State.AUTHENTICATING, "Authenticating...")
|
||||
_client_nonce = NetworkIdentityCrypto.secure_id(32)
|
||||
_client_identity_attempt = NetworkProtocol.make_identity_hello(
|
||||
|
|
@ -892,6 +971,24 @@ func _on_connected_to_server() -> void:
|
|||
submit_identity_hello.rpc_id(1, _client_identity_attempt)
|
||||
|
||||
|
||||
func _configure_enet_peer_timeout(peer_id: int) -> void:
|
||||
var enet := multiplayer.multiplayer_peer as ENetMultiplayerPeer
|
||||
if enet == null:
|
||||
return
|
||||
var packet_peer: ENetPacketPeer = enet.get_peer(peer_id)
|
||||
if packet_peer == null:
|
||||
return
|
||||
# A deterministic generated world can temporarily occupy the client's main
|
||||
# thread during the authenticated join transition. Keep that bounded load
|
||||
# from looking like a dead connection while retaining normal ENet liveness
|
||||
# checks once the client resumes polling.
|
||||
packet_peer.set_timeout(
|
||||
ENET_TIMEOUT_LIMIT,
|
||||
ENET_TIMEOUT_MINIMUM_MS,
|
||||
ENET_TIMEOUT_MAXIMUM_MS,
|
||||
)
|
||||
|
||||
|
||||
func _on_connection_failed() -> void:
|
||||
if state not in [
|
||||
State.CONNECTING,
|
||||
|
|
@ -931,6 +1028,8 @@ func _on_peer_disconnected(peer_id: int) -> void:
|
|||
_pending_identity_challenges.erase(peer_id)
|
||||
_authenticated_identity_cache.erase(peer_id)
|
||||
_operator_peer_ids.erase(peer_id)
|
||||
_last_animation_state_by_peer.erase(peer_id)
|
||||
_pending_animation_state_by_peer.erase(peer_id)
|
||||
var recovery_attempt: String = _recovery_attempts.get(peer_id, "")
|
||||
if not recovery_attempt.is_empty():
|
||||
_recovery_attempts.erase(peer_id)
|
||||
|
|
@ -1209,6 +1308,16 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
|
||||
)
|
||||
return
|
||||
for required_capability: String in [
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
]:
|
||||
if required_capability not in client_capabilities:
|
||||
_reject_peer(
|
||||
sender_id,
|
||||
NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
|
||||
)
|
||||
return
|
||||
if (
|
||||
_host_world_layout == WorldLayout.STARTER_ISLAND
|
||||
and NetworkProtocol.WORLD_LAYOUT_CAPABILITY not in client_capabilities
|
||||
|
|
@ -1432,6 +1541,14 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
_teardown_peer()
|
||||
_fail("This server does not support generated worlds.")
|
||||
return
|
||||
for required_capability: String in [
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
]:
|
||||
if required_capability not in _server_capabilities:
|
||||
_teardown_peer()
|
||||
_fail("This server is missing required network capabilities.")
|
||||
return
|
||||
if (
|
||||
received_world_layout == WorldLayout.STARTER_ISLAND
|
||||
and NetworkProtocol.WORLD_LAYOUT_CAPABILITY not in _server_capabilities
|
||||
|
|
@ -1456,6 +1573,9 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
|
||||
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||
|
|
@ -1501,6 +1621,8 @@ func receive_peer_despawn(peer_id: int) -> void:
|
|||
if state != State.JOINED_CLIENT:
|
||||
return
|
||||
_operator_peer_ids.erase(peer_id)
|
||||
_last_animation_state_by_peer.erase(peer_id)
|
||||
_pending_animation_state_by_peer.erase(peer_id)
|
||||
_registry.remove_peer(peer_id)
|
||||
_spawn_service.remove_peer(peer_id)
|
||||
peer_removed.emit(peer_id)
|
||||
|
|
@ -1542,6 +1664,10 @@ func _apply_spawn_entry(entry: Dictionary) -> void:
|
|||
or typeof(entry.get("display_name")) != TYPE_STRING
|
||||
or typeof(entry.get("position")) != TYPE_ARRAY
|
||||
or typeof(entry.get("yaw")) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(entry.get("sitting")) != TYPE_BOOL
|
||||
or not NetworkPlayerAnimationProtocol.validate_state(
|
||||
entry.get("animation_state")
|
||||
)
|
||||
):
|
||||
return
|
||||
if not _verify_spawn_identity(entry):
|
||||
|
|
@ -1557,6 +1683,7 @@ func _apply_spawn_entry(entry: Dictionary) -> void:
|
|||
)
|
||||
own_snapshot["position"] = own_position
|
||||
own_snapshot["visual_yaw"] = float(entry["yaw"])
|
||||
own_snapshot["sitting"] = bool(entry["sitting"])
|
||||
own_avatar.apply_network_teleport(own_snapshot)
|
||||
return
|
||||
var peer_was_added: bool = false
|
||||
|
|
@ -1603,6 +1730,18 @@ func _apply_spawn_entry(entry: Dictionary) -> void:
|
|||
var record := _registry.get_peer(peer_id)
|
||||
if avatar != null and record != null:
|
||||
avatar.apply_appearance_snapshot(record.appearance_snapshot)
|
||||
# Lifecycle and animation updates use separate reliable ENet channels.
|
||||
# Include the current authoritative state in the spawn record so a peer
|
||||
# joining during an action presents it immediately, regardless of which
|
||||
# channel arrives first.
|
||||
avatar.apply_network_animation_state(entry["animation_state"])
|
||||
avatar.apply_network_sitting_state(bool(entry["sitting"]))
|
||||
var pending_animation: Dictionary = (
|
||||
_pending_animation_state_by_peer.get(peer_id, {})
|
||||
)
|
||||
if not pending_animation.is_empty():
|
||||
avatar.apply_network_animation_state(pending_animation)
|
||||
_pending_animation_state_by_peer.erase(peer_id)
|
||||
if peer_was_added and record != null:
|
||||
peer_authenticated.emit(peer_id, record.display_name)
|
||||
|
||||
|
|
@ -1627,6 +1766,15 @@ func _make_spawn_entry(
|
|||
transform: Transform3D,
|
||||
) -> Dictionary:
|
||||
var record: PeerRegistry.PeerRecord = _registry.get_peer(peer_id)
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
var animation_state: Dictionary = (
|
||||
avatar.make_network_animation_state()
|
||||
if avatar != null
|
||||
else NetworkPlayerAnimationProtocol.make_state(
|
||||
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
|
||||
true,
|
||||
)
|
||||
)
|
||||
return {
|
||||
"peer_id": peer_id,
|
||||
"profile_id": record.profile_id if record != null else "",
|
||||
|
|
@ -1637,6 +1785,12 @@ func _make_spawn_entry(
|
|||
transform.origin.z,
|
||||
],
|
||||
"yaw": transform.basis.get_euler().y,
|
||||
"sitting": (
|
||||
avatar.get_network_sitting_state()
|
||||
if avatar != null
|
||||
else false
|
||||
),
|
||||
"animation_state": animation_state,
|
||||
"appearance": (
|
||||
record.appearance_snapshot.duplicate(true)
|
||||
if record != null
|
||||
|
|
@ -1862,14 +2016,69 @@ func _fail_identity(message: String) -> void:
|
|||
_fail(message)
|
||||
|
||||
|
||||
func _maybe_send_local_input() -> void:
|
||||
var peer_id: int = multiplayer.get_unique_id()
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
if avatar == null:
|
||||
return
|
||||
var state_hash: int = avatar.get_network_input_state_hash()
|
||||
var state_changed: bool = state_hash != _last_input_state_hash
|
||||
if (
|
||||
avatar.has_active_network_input()
|
||||
or state_changed
|
||||
or _idle_input_accumulator >= IDLE_INPUT_INTERVAL
|
||||
):
|
||||
_send_local_input()
|
||||
_last_input_state_hash = state_hash
|
||||
_idle_input_accumulator = 0.0
|
||||
|
||||
|
||||
func _send_local_input() -> void:
|
||||
var peer_id: int = multiplayer.get_unique_id()
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
if avatar == null:
|
||||
return
|
||||
_input_sequence += 1
|
||||
_input_sequence = (
|
||||
1
|
||||
if _input_sequence >= MAX_MOVEMENT_INPUT_SEQUENCE
|
||||
else _input_sequence + 1
|
||||
)
|
||||
var input: Dictionary = avatar.capture_network_input(_input_sequence)
|
||||
submit_movement_input.rpc_id(1, input)
|
||||
var encoded: Array = _encode_movement_input(input)
|
||||
if encoded.is_empty():
|
||||
return
|
||||
_pending_movement_inputs.append(input.duplicate(true))
|
||||
while _pending_movement_inputs.size() > MAX_PENDING_MOVEMENT_INPUTS:
|
||||
_pending_movement_inputs.pop_front()
|
||||
_movement_inputs_sent += 1
|
||||
submit_movement_input.rpc_id(1, encoded)
|
||||
|
||||
|
||||
func _maybe_send_local_animation_action() -> void:
|
||||
var peer_id: int = multiplayer.get_unique_id()
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
if avatar == null:
|
||||
return
|
||||
var state: Dictionary = avatar.make_network_animation_state()
|
||||
var action: Dictionary = state.get("action", {})
|
||||
if not NetworkPlayerAnimationProtocol.validate_action_state(action):
|
||||
return
|
||||
var signature: Array = [
|
||||
str(action["id"]),
|
||||
int(action["sequence"]),
|
||||
bool(action.get("paused", false)),
|
||||
avatar.get_network_sitting_state(),
|
||||
]
|
||||
if signature == _last_local_animation_action_signature:
|
||||
return
|
||||
var encoded: Array = _encode_movement_animation_action(
|
||||
action,
|
||||
avatar.get_network_sitting_state(),
|
||||
)
|
||||
if encoded.is_empty():
|
||||
return
|
||||
_last_local_animation_action_signature = signature
|
||||
submit_movement_animation_action.rpc_id(1, encoded)
|
||||
|
||||
|
||||
func submit_neutral_local_movement() -> void:
|
||||
|
|
@ -1879,17 +2088,112 @@ func submit_neutral_local_movement() -> void:
|
|||
|
||||
|
||||
@rpc("any_peer", "call_remote", "unreliable_ordered", 1)
|
||||
func submit_movement_input(data: Dictionary) -> void:
|
||||
func submit_movement_input(encoded: Array) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if not is_host() or not _registry.has_peer(sender_id):
|
||||
return
|
||||
var avatar: Player = _spawn_service.get_avatar(sender_id)
|
||||
if avatar == null or not _is_valid_movement_input(data):
|
||||
var data: Dictionary = _decode_movement_input(encoded)
|
||||
if avatar == null or data.is_empty():
|
||||
return
|
||||
avatar.apply_authoritative_network_input(data)
|
||||
_movement_inputs_received += 1
|
||||
avatar.apply_authoritative_network_input(
|
||||
data,
|
||||
Player.resolve_network_input_stale_timeout_seconds(
|
||||
get_peer_rtt_ms(sender_id)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
func _is_valid_movement_input(data: Dictionary) -> bool:
|
||||
@rpc(
|
||||
"any_peer",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL,
|
||||
)
|
||||
func submit_movement_animation_action(encoded: Array) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if not is_host() or not _registry.has_peer(sender_id):
|
||||
return
|
||||
var avatar: Player = _spawn_service.get_avatar(sender_id)
|
||||
var state: Dictionary = _decode_movement_animation_action(encoded)
|
||||
if avatar == null or state.is_empty():
|
||||
return
|
||||
avatar.apply_authoritative_network_animation_action(state["action"])
|
||||
avatar.apply_authoritative_network_sitting_state(bool(state["sitting"]))
|
||||
|
||||
|
||||
static func _encode_movement_input(data: Dictionary) -> Array:
|
||||
if not _is_valid_movement_input(data):
|
||||
return []
|
||||
var axis: Array = data["axis"]
|
||||
var flags: int = 0
|
||||
if bool(data["jump"]):
|
||||
flags |= MOVEMENT_FLAG_JUMP
|
||||
if bool(data["sprint"]):
|
||||
flags |= MOVEMENT_FLAG_SPRINT
|
||||
if bool(data["sneak"]):
|
||||
flags |= MOVEMENT_FLAG_SNEAK
|
||||
if bool(data["slow_walk"]):
|
||||
flags |= MOVEMENT_FLAG_SLOW_WALK
|
||||
if bool(data["sitting"]):
|
||||
flags |= MOVEMENT_FLAG_SITTING
|
||||
if bool(data["casting"]):
|
||||
flags |= MOVEMENT_FLAG_CASTING
|
||||
var action: Dictionary = data["animation_action"]
|
||||
return [
|
||||
int(data["sequence"]),
|
||||
Vector2(float(axis[0]), float(axis[1])),
|
||||
float(data["camera_yaw"]),
|
||||
flags,
|
||||
str(action["id"]),
|
||||
int(action["sequence"]),
|
||||
float(action["elapsed"]),
|
||||
bool(action.get("paused", false)),
|
||||
]
|
||||
|
||||
|
||||
static func _decode_movement_input(value: Variant) -> Dictionary:
|
||||
if typeof(value) != TYPE_ARRAY:
|
||||
return {}
|
||||
var fields: Array = value
|
||||
if (
|
||||
fields.size() != MOVEMENT_INPUT_FIELD_COUNT
|
||||
or typeof(fields[0]) != TYPE_INT
|
||||
or typeof(fields[1]) != TYPE_VECTOR2
|
||||
or typeof(fields[2]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(fields[3]) != TYPE_INT
|
||||
or typeof(fields[4]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
or typeof(fields[5]) != TYPE_INT
|
||||
or typeof(fields[6]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(fields[7]) != TYPE_BOOL
|
||||
):
|
||||
return {}
|
||||
var axis: Vector2 = fields[1]
|
||||
var flags: int = int(fields[3])
|
||||
if flags < 0 or flags & ~MOVEMENT_ALLOWED_FLAGS != 0:
|
||||
return {}
|
||||
var decoded: Dictionary = {
|
||||
"sequence": int(fields[0]),
|
||||
"axis": [axis.x, axis.y],
|
||||
"camera_yaw": float(fields[2]),
|
||||
"jump": bool(flags & MOVEMENT_FLAG_JUMP),
|
||||
"sprint": bool(flags & MOVEMENT_FLAG_SPRINT),
|
||||
"sneak": bool(flags & MOVEMENT_FLAG_SNEAK),
|
||||
"slow_walk": bool(flags & MOVEMENT_FLAG_SLOW_WALK),
|
||||
"sitting": bool(flags & MOVEMENT_FLAG_SITTING),
|
||||
"casting": bool(flags & MOVEMENT_FLAG_CASTING),
|
||||
"animation_action": NetworkPlayerAnimationProtocol.make_action_state(
|
||||
StringName(str(fields[4])),
|
||||
int(fields[5]),
|
||||
float(fields[6]),
|
||||
bool(fields[7]),
|
||||
),
|
||||
}
|
||||
return decoded if _is_valid_movement_input(decoded) else {}
|
||||
|
||||
|
||||
static func _is_valid_movement_input(data: Dictionary) -> bool:
|
||||
if (
|
||||
typeof(data.get("sequence")) != TYPE_INT
|
||||
or typeof(data.get("axis")) != TYPE_ARRAY
|
||||
|
|
@ -1929,49 +2233,142 @@ func _is_valid_movement_input(data: Dictionary) -> bool:
|
|||
|
||||
|
||||
func _broadcast_movement_snapshots() -> void:
|
||||
var snapshots: Array = []
|
||||
for peer_id: int in _registry.get_peer_ids():
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
if avatar == null:
|
||||
_movement_snapshot_tick += 1
|
||||
var peer_ids: Array[int] = _registry.get_peer_ids()
|
||||
for recipient_id: int in peer_ids:
|
||||
# Peer 1 is the local listen-server player. Dedicated servers do not
|
||||
# register a peer 1, so every remaining record is a remote recipient.
|
||||
if recipient_id == 1:
|
||||
continue
|
||||
var encoded: Array = _encode_movement_snapshot(
|
||||
avatar.make_network_snapshot(peer_id)
|
||||
)
|
||||
if not encoded.is_empty():
|
||||
snapshots.append(encoded)
|
||||
# The compact v5 representation keeps a normal eight-player update near
|
||||
# 1 KiB instead of the roughly 3.7 KiB dictionary representation. Rooms
|
||||
# configured above the normal cap are divided into the same safe size.
|
||||
var recipient_avatar: Player = _spawn_service.get_avatar(recipient_id)
|
||||
if recipient_avatar == null:
|
||||
continue
|
||||
var snapshots: Array = []
|
||||
for subject_id: int in peer_ids:
|
||||
var subject_avatar: Player = _spawn_service.get_avatar(subject_id)
|
||||
if subject_avatar == null:
|
||||
continue
|
||||
if (
|
||||
subject_id != recipient_id
|
||||
and not _should_send_remote_snapshot(
|
||||
recipient_avatar.global_position,
|
||||
subject_avatar.global_position,
|
||||
)
|
||||
):
|
||||
continue
|
||||
var encoded: Array = _encode_movement_snapshot(
|
||||
subject_avatar.make_network_snapshot(subject_id)
|
||||
)
|
||||
if not encoded.is_empty():
|
||||
snapshots.append(encoded)
|
||||
_send_movement_snapshot_batches(recipient_id, snapshots)
|
||||
_broadcast_movement_animation_updates(peer_ids)
|
||||
|
||||
|
||||
func _should_send_remote_snapshot(
|
||||
recipient_position: Vector3,
|
||||
subject_position: Vector3,
|
||||
) -> bool:
|
||||
var distance_squared: float = recipient_position.distance_squared_to(
|
||||
subject_position
|
||||
)
|
||||
var divisor: int = DISTANT_REMOTE_SNAPSHOT_DIVISOR
|
||||
if distance_squared <= NEAR_REMOTE_DISTANCE * NEAR_REMOTE_DISTANCE:
|
||||
divisor = NEAR_REMOTE_SNAPSHOT_DIVISOR
|
||||
elif distance_squared <= FAR_REMOTE_DISTANCE * FAR_REMOTE_DISTANCE:
|
||||
divisor = FAR_REMOTE_SNAPSHOT_DIVISOR
|
||||
return _movement_snapshot_tick % divisor == 0
|
||||
|
||||
|
||||
func _send_movement_snapshot_batches(
|
||||
recipient_id: int,
|
||||
snapshots: Array,
|
||||
) -> void:
|
||||
for start_index: int in range(
|
||||
0,
|
||||
snapshots.size(),
|
||||
MOVEMENT_SNAPSHOT_BATCH_SIZE,
|
||||
):
|
||||
receive_movement_snapshots.rpc(
|
||||
snapshots.slice(
|
||||
start_index,
|
||||
mini(
|
||||
start_index + MOVEMENT_SNAPSHOT_BATCH_SIZE,
|
||||
snapshots.size(),
|
||||
),
|
||||
)
|
||||
var batch: Array = snapshots.slice(
|
||||
start_index,
|
||||
mini(
|
||||
start_index + MOVEMENT_SNAPSHOT_BATCH_SIZE,
|
||||
snapshots.size(),
|
||||
),
|
||||
)
|
||||
receive_movement_snapshots.rpc_id(recipient_id, batch)
|
||||
_movement_snapshot_packets_sent += 1
|
||||
_movement_snapshot_states_sent += batch.size()
|
||||
|
||||
|
||||
func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
|
||||
var refresh_all: bool = (
|
||||
_animation_refresh_accumulator >= ANIMATION_REFRESH_INTERVAL
|
||||
)
|
||||
if refresh_all:
|
||||
_animation_refresh_accumulator = fmod(
|
||||
_animation_refresh_accumulator,
|
||||
ANIMATION_REFRESH_INTERVAL,
|
||||
)
|
||||
var updates: Array = []
|
||||
for subject_id: int in peer_ids:
|
||||
var avatar: Player = _spawn_service.get_avatar(subject_id)
|
||||
if avatar == null:
|
||||
continue
|
||||
var state: Dictionary = avatar.make_network_animation_state()
|
||||
var previous: Dictionary = _last_animation_state_by_peer.get(
|
||||
subject_id, {}
|
||||
)
|
||||
if (
|
||||
not refresh_all
|
||||
and _movement_animation_signature(state)
|
||||
== _movement_animation_signature(previous)
|
||||
):
|
||||
continue
|
||||
_last_animation_state_by_peer[subject_id] = state.duplicate(true)
|
||||
var encoded: Array = _encode_movement_animation(subject_id, state)
|
||||
if not encoded.is_empty():
|
||||
updates.append(encoded)
|
||||
if updates.is_empty():
|
||||
return
|
||||
for recipient_id: int in peer_ids:
|
||||
if recipient_id == 1:
|
||||
continue
|
||||
receive_movement_animations.rpc_id(recipient_id, updates)
|
||||
_movement_animation_packets_sent += 1
|
||||
_movement_animation_states_sent += updates.size()
|
||||
|
||||
|
||||
static func _movement_animation_signature(state: Dictionary) -> Array:
|
||||
if not NetworkPlayerAnimationProtocol.validate_state(state):
|
||||
return []
|
||||
var action: Dictionary = state["action"]
|
||||
return [
|
||||
str(state["locomotion_id"]),
|
||||
bool(state["grounded"]),
|
||||
str(action["id"]),
|
||||
int(action["sequence"]),
|
||||
bool(action.get("paused", false)),
|
||||
]
|
||||
|
||||
|
||||
static func _encode_movement_snapshot(snapshot: Dictionary) -> Array:
|
||||
var position: Variant = snapshot.get("position")
|
||||
var snapshot_velocity: Variant = snapshot.get("velocity")
|
||||
var animation_value: Variant = snapshot.get("animation_state")
|
||||
if (
|
||||
typeof(position) != TYPE_ARRAY
|
||||
or position.size() != 3
|
||||
or typeof(snapshot_velocity) != TYPE_ARRAY
|
||||
or snapshot_velocity.size() != 3
|
||||
or not NetworkPlayerAnimationProtocol.validate_state(animation_value)
|
||||
):
|
||||
return []
|
||||
var animation: Dictionary = animation_value
|
||||
var action: Dictionary = animation["action"]
|
||||
var flags: int = 0
|
||||
if bool(snapshot.get("grounded", false)):
|
||||
flags |= SNAPSHOT_FLAG_GROUNDED
|
||||
if bool(snapshot.get("sitting", false)):
|
||||
flags |= SNAPSHOT_FLAG_SITTING
|
||||
if bool(snapshot.get("casting", false)):
|
||||
flags |= SNAPSHOT_FLAG_CASTING
|
||||
return [
|
||||
int(snapshot.get("peer_id", 0)),
|
||||
int(snapshot.get("acknowledged_input", 0)),
|
||||
|
|
@ -1982,14 +2379,7 @@ static func _encode_movement_snapshot(snapshot: Dictionary) -> Array:
|
|||
float(snapshot_velocity[2]),
|
||||
),
|
||||
float(snapshot.get("visual_yaw", 0.0)),
|
||||
str(animation["locomotion_id"]),
|
||||
bool(animation["grounded"]),
|
||||
str(action["id"]),
|
||||
int(action["sequence"]),
|
||||
float(action["elapsed"]),
|
||||
bool(action.get("paused", false)),
|
||||
bool(snapshot.get("sitting", false)),
|
||||
bool(snapshot.get("casting", false)),
|
||||
flags,
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -2004,18 +2394,22 @@ static func _decode_movement_snapshot(value: Variant) -> Dictionary:
|
|||
or typeof(fields[2]) != TYPE_VECTOR3
|
||||
or typeof(fields[3]) != TYPE_VECTOR3
|
||||
or typeof(fields[4]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(fields[5]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
or typeof(fields[6]) != TYPE_BOOL
|
||||
or typeof(fields[7]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
or typeof(fields[8]) != TYPE_INT
|
||||
or typeof(fields[9]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(fields[10]) != TYPE_BOOL
|
||||
or typeof(fields[11]) != TYPE_BOOL
|
||||
or typeof(fields[12]) != TYPE_BOOL
|
||||
or typeof(fields[5]) != TYPE_INT
|
||||
):
|
||||
return {}
|
||||
var position: Vector3 = fields[2]
|
||||
var snapshot_velocity: Vector3 = fields[3]
|
||||
var flags: int = int(fields[5])
|
||||
if (
|
||||
int(fields[0]) <= 0
|
||||
or int(fields[1]) < 0
|
||||
or not position.is_finite()
|
||||
or not snapshot_velocity.is_finite()
|
||||
or not is_finite(float(fields[4]))
|
||||
or flags < 0
|
||||
or flags & ~SNAPSHOT_ALLOWED_FLAGS != 0
|
||||
):
|
||||
return {}
|
||||
return {
|
||||
"peer_id": int(fields[0]),
|
||||
"acknowledged_input": int(fields[1]),
|
||||
|
|
@ -2026,19 +2420,100 @@ static func _decode_movement_snapshot(value: Variant) -> Dictionary:
|
|||
snapshot_velocity.z,
|
||||
],
|
||||
"visual_yaw": float(fields[4]),
|
||||
"animation_state": NetworkPlayerAnimationProtocol.make_state(
|
||||
StringName(str(fields[5])),
|
||||
bool(fields[6]),
|
||||
StringName(str(fields[7])),
|
||||
int(fields[8]),
|
||||
float(fields[9]),
|
||||
bool(fields[10]),
|
||||
),
|
||||
"sitting": bool(fields[11]),
|
||||
"casting": bool(fields[12]),
|
||||
"grounded": bool(flags & SNAPSHOT_FLAG_GROUNDED),
|
||||
"sitting": bool(flags & SNAPSHOT_FLAG_SITTING),
|
||||
"casting": bool(flags & SNAPSHOT_FLAG_CASTING),
|
||||
}
|
||||
|
||||
|
||||
static func _encode_movement_animation(
|
||||
peer_id: int,
|
||||
state: Dictionary,
|
||||
) -> Array:
|
||||
if not NetworkPlayerAnimationProtocol.validate_state(state):
|
||||
return []
|
||||
var action: Dictionary = state["action"]
|
||||
return [
|
||||
peer_id,
|
||||
str(state["locomotion_id"]),
|
||||
bool(state["grounded"]),
|
||||
str(action["id"]),
|
||||
int(action["sequence"]),
|
||||
float(action["elapsed"]),
|
||||
bool(action.get("paused", false)),
|
||||
]
|
||||
|
||||
|
||||
static func _decode_movement_animation(value: Variant) -> Dictionary:
|
||||
if typeof(value) != TYPE_ARRAY:
|
||||
return {}
|
||||
var fields: Array = value
|
||||
if (
|
||||
fields.size() != MOVEMENT_ANIMATION_FIELD_COUNT
|
||||
or typeof(fields[0]) != TYPE_INT
|
||||
or typeof(fields[1]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
or typeof(fields[2]) != TYPE_BOOL
|
||||
or typeof(fields[3]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
or typeof(fields[4]) != TYPE_INT
|
||||
or typeof(fields[5]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(fields[6]) != TYPE_BOOL
|
||||
):
|
||||
return {}
|
||||
var state: Dictionary = NetworkPlayerAnimationProtocol.make_state(
|
||||
StringName(str(fields[1])),
|
||||
bool(fields[2]),
|
||||
StringName(str(fields[3])),
|
||||
int(fields[4]),
|
||||
float(fields[5]),
|
||||
bool(fields[6]),
|
||||
)
|
||||
if (
|
||||
int(fields[0]) <= 0
|
||||
or not NetworkPlayerAnimationProtocol.validate_state(state)
|
||||
):
|
||||
return {}
|
||||
return {"peer_id": int(fields[0]), "state": state}
|
||||
|
||||
|
||||
static func _encode_movement_animation_action(
|
||||
action: Dictionary,
|
||||
sitting: bool,
|
||||
) -> Array:
|
||||
if not NetworkPlayerAnimationProtocol.validate_action_state(action):
|
||||
return []
|
||||
return [
|
||||
str(action["id"]),
|
||||
int(action["sequence"]),
|
||||
float(action["elapsed"]),
|
||||
bool(action.get("paused", false)),
|
||||
sitting,
|
||||
]
|
||||
|
||||
|
||||
static func _decode_movement_animation_action(value: Variant) -> Dictionary:
|
||||
if typeof(value) != TYPE_ARRAY:
|
||||
return {}
|
||||
var fields: Array = value
|
||||
if (
|
||||
fields.size() != MOVEMENT_ANIMATION_ACTION_FIELD_COUNT
|
||||
or typeof(fields[0]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
or typeof(fields[1]) != TYPE_INT
|
||||
or typeof(fields[2]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(fields[3]) != TYPE_BOOL
|
||||
or typeof(fields[4]) != TYPE_BOOL
|
||||
):
|
||||
return {}
|
||||
var action: Dictionary = NetworkPlayerAnimationProtocol.make_action_state(
|
||||
StringName(str(fields[0])),
|
||||
int(fields[1]),
|
||||
float(fields[2]),
|
||||
bool(fields[3]),
|
||||
)
|
||||
if not NetworkPlayerAnimationProtocol.validate_action_state(action):
|
||||
return {}
|
||||
return {"action": action, "sitting": bool(fields[4])}
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "unreliable_ordered", 2)
|
||||
func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
||||
if state != State.JOINED_CLIENT:
|
||||
|
|
@ -2058,11 +2533,13 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
|||
if avatar == null:
|
||||
continue
|
||||
if peer_id == local_peer_id:
|
||||
_discard_acknowledged_movement_inputs(
|
||||
int(snapshot.get("acknowledged_input", 0))
|
||||
)
|
||||
avatar.apply_local_prediction_correction(
|
||||
snapshot,
|
||||
_input_sequence,
|
||||
_pending_movement_inputs,
|
||||
INPUT_INTERVAL,
|
||||
estimated_transit_seconds,
|
||||
)
|
||||
else:
|
||||
avatar.push_network_snapshot(
|
||||
|
|
@ -2071,6 +2548,39 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void:
|
||||
while (
|
||||
not _pending_movement_inputs.is_empty()
|
||||
and int(_pending_movement_inputs[0].get("sequence", 0))
|
||||
<= acknowledged_sequence
|
||||
):
|
||||
_pending_movement_inputs.pop_front()
|
||||
|
||||
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL,
|
||||
)
|
||||
func receive_movement_animations(encoded_states: Array) -> void:
|
||||
if state != State.JOINED_CLIENT:
|
||||
return
|
||||
for value: Variant in encoded_states:
|
||||
var decoded: Dictionary = _decode_movement_animation(value)
|
||||
if decoded.is_empty():
|
||||
continue
|
||||
var avatar: Player = _spawn_service.get_avatar(
|
||||
int(decoded["peer_id"])
|
||||
)
|
||||
if avatar == null:
|
||||
_pending_animation_state_by_peer[int(decoded["peer_id"])] = (
|
||||
Dictionary(decoded["state"]).duplicate(true)
|
||||
)
|
||||
continue
|
||||
avatar.apply_network_animation_state(decoded["state"])
|
||||
|
||||
|
||||
func _estimated_movement_transit_seconds() -> float:
|
||||
if state != State.JOINED_CLIENT:
|
||||
return -1.0
|
||||
|
|
@ -2346,7 +2856,15 @@ func _teardown_peer() -> void:
|
|||
_connection_deadline = 0.0
|
||||
_input_sequence = 0
|
||||
_input_accumulator = 0.0
|
||||
_idle_input_accumulator = 0.0
|
||||
_last_input_state_hash = 0
|
||||
_pending_movement_inputs.clear()
|
||||
_snapshot_accumulator = 0.0
|
||||
_movement_snapshot_tick = 0
|
||||
_animation_refresh_accumulator = 0.0
|
||||
_last_animation_state_by_peer.clear()
|
||||
_pending_animation_state_by_peer.clear()
|
||||
_last_local_animation_action_signature.clear()
|
||||
_server_capabilities = PackedStringArray()
|
||||
_server_identity_fingerprint = ""
|
||||
_server_identity_public_key = ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue