sunder/scripts/multiplayer/ping.gd
2026-02-18 18:33:17 -05:00

145 lines
4.4 KiB
GDScript

# This file is part of sunder.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
## This defines a ping manager.
##
## When added to your scene and connect the [member synchronized] signal to your
## custom handler in order to receive ping updates on a regular basis (as
## defined by [param ping_interval] and synchronize the state for each peer.
## [codeblock]
## func _ready() -> void:
## $Ping.synchronized.connect(_on_ping_sync)
##
## func _on_ping_sync(state: Dictionary) -> void:
## for peer_id in state:
## print("peer `%s` latency is %d ms" % [peer_id, state[peer_id]])
## [/codeblock]
@icon("res://assets/icons/ping.svg")
class_name Ping extends Timer
## Emitted when the state is synchronized.
signal synchronized(state : Dictionary)
## The size of the ping history buffer used to store past ping times for each client.
@export var ping_history: int = 8:
set = set_ping_history
## The ping state being synchronized to connected peers
@export var _state := {}
## The dictionary of [PingInfo] for each connected peer id
var _infos := {}
## The array of times when a ping request occured
var _pings := []
var _last_ping := 0
## This class defines a ping information to keep track of ping results
## and provide average ping.
class PingInfo:
const SIZE := 32
var samples := PackedInt32Array()
var pos := 0
func _init() -> void:
samples.resize(SIZE)
samples.fill(0)
func get_average() -> int:
var value := 0
for sample in samples:
value += sample
@warning_ignore("integer_division")
return value / SIZE if value > 0 else 0
func set_next(value: int) -> void:
samples[pos] = value
pos = (pos + 1) % SIZE
func _ready() -> void:
clear_pings()
multiplayer.peer_connected.connect(_add_peer)
multiplayer.peer_disconnected.connect(_del_peer)
timeout.connect(_on_timeout)
func _on_timeout() -> void:
if _infos:
# check and update ping intervals
var now := Time.get_ticks_msec()
if now >= _pings[_last_ping] + wait_time:
_last_ping = (_last_ping + 1) % ping_history
_pings[_last_ping] = now
_ping.rpc(now)
# reset state
_state.clear()
# iterate over registered clients
for peer_id:int in _infos:
# set client average ping state
_state[peer_id] = _infos[peer_id].get_average()
# send state to connected peers
_synchronize.rpc(_state)
## Clears the ping history array.
func clear_pings() -> void:
_last_ping = 0
_pings.resize(ping_history)
_pings.fill(0)
## Sets the size of the ping history buffer and clears existing ping data.
func set_ping_history(value: int) -> void:
if value < 1:
return
ping_history = value
clear_pings()
## Adds a new peer to the registry
func _add_peer(peer_id: int) -> void:
_infos[peer_id] = PingInfo.new()
## Deletes a peer from the registry
func _del_peer(peer_id: int) -> void:
_infos.erase(peer_id)
@rpc("authority", "call_remote", "unreliable")
func _ping(time: int) -> void:
_pong.rpc_id(get_multiplayer_authority(), time)
@rpc("any_peer", "call_remote", "unreliable")
func _pong(time: int) -> void:
if not multiplayer.is_server():
return
# get id of the peer sending the pong
var peer_id: int = multiplayer.get_remote_sender_id()
# check if peer exists in the registered clients dictionary
if not _infos.has(peer_id):
return
# init variables
var now := Time.get_ticks_msec()
var last := (_last_ping + 1) % ping_history
var found := ping_history * wait_time
# search related ping in history
for i in range(ping_history):
# check if current ping matches received pong
if time == _pings[last]:
found = _pings[last]
break
# move to next ping in history
last = (last + 1) % ping_history
# compute round-trip time (RTT) and update ping info for this peer
_infos[peer_id].set_next((now - found) / 2)
@rpc("authority", "call_local", "unreliable")
func _synchronize(state: Dictionary) -> void:
_state = state
synchronized.emit(_state)