Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addons/netfox.extras/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox.extras"
description="Game-specific utilities for Netfox"
author="Tamas Galffy and contributors"
version="1.37.0"
version="1.38.0"
script="netfox-extras.gd"
2 changes: 1 addition & 1 deletion addons/netfox.internals/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox.internals"
description="Shared internals for netfox addons"
author="Tamas Galffy and contributors"
version="1.37.0"
version="1.38.0"
script="plugin.gd"
2 changes: 1 addition & 1 deletion addons/netfox.noray/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox.noray"
description="Bulletproof your connectivity with noray integration for netfox"
author="Tamas Galffy and contributors"
version="1.37.0"
version="1.38.0"
script="netfox-noray.gd"
19 changes: 14 additions & 5 deletions addons/netfox/netfox.gd
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ var SETTINGS: Array[Dictionary] = [
"value": true,
"type": TYPE_BOOL
},
{
"name": "netfox/general/use_raw_commands",
"value": false,
"type": TYPE_BOOL
},
# Logging
NetfoxLogger._make_setting("netfox/logging/netfox_log_level"),
# Time settings
Expand Down Expand Up @@ -145,6 +150,10 @@ const AUTOLOADS: Array[Dictionary] = [
{
"name": "NetworkPerformance",
"path": ROOT + "/network-performance.gd"
},
{
"name": "NetworkCommandServer",
"path": ROOT + "/servers/network-command-server.gd"
}
]

Expand Down Expand Up @@ -184,23 +193,23 @@ const TYPES: Array[Dictionary] = [
func _enter_tree():
for setting in SETTINGS:
add_setting(setting)

for autoload in AUTOLOADS:
if not has_autoload(autoload.name):
add_autoload_singleton(autoload.name, autoload.path)

for type in TYPES:
add_custom_type(type.name, type.base, load(type.script), load(type.icon))

func _exit_tree() -> void:
if ProjectSettings.get_setting(&"netfox/general/clear_settings", false):
for setting in SETTINGS:
remove_setting(setting)

for autoload in AUTOLOADS:
if has_autoload(autoload.name):
remove_autoload_singleton(autoload.name)

for type in TYPES:
remove_custom_type(type.name)

Expand All @@ -220,7 +229,7 @@ func add_setting(setting: Dictionary) -> void:
func remove_setting(setting: Dictionary) -> void:
if not ProjectSettings.has_setting(setting.name):
return

ProjectSettings.clear(setting.name)

func has_autoload(name: String) -> bool:
Expand Down
35 changes: 22 additions & 13 deletions addons/netfox/network-time-synchronizer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ var _offset: float = 0.
var _rtt: float = 0.
var _rtt_jitter: float = 0.

@onready var _cmd_ping := NetworkCommandServer.register_command_at(_NetworkCommands.NTP_PING, _handle_ping, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
@onready var _cmd_pong := NetworkCommandServer.register_command_at(_NetworkCommands.NTP_PONG, _handle_pong, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
@onready var _cmd_req_time := NetworkCommandServer.register_command_at(_NetworkCommands.NTP_REQ_TIME, _handle_request_timestamp, MultiplayerPeer.TRANSFER_MODE_RELIABLE)
@onready var _cmd_set_time := NetworkCommandServer.register_command_at(_NetworkCommands.NTP_SET_TIME, _handle_set_timestamp, MultiplayerPeer.TRANSFER_MODE_RELIABLE)

## Emitted after the initial time sync.
##
## At the start of the game, clients request an initial timestamp to kickstart
Expand Down Expand Up @@ -145,7 +150,7 @@ func start() -> void:
_sample_idx = 0
_sample_buffer = _RingBuffer.new(sync_samples)

_request_timestamp.rpc_id(1)
_cmd_req_time.send(PackedByteArray(), 1)

## Stop the time synchronization loop.
func stop() -> void:
Expand All @@ -169,7 +174,7 @@ func _loop() -> void:
_awaiting_samples[_sample_idx] = sample

sample.ping_sent = _clock.get_time()
_send_ping.rpc_id(1, _sample_idx)
_cmd_ping.send(var_to_bytes(_sample_idx), 1)

_sample_idx += 1

Expand Down Expand Up @@ -235,15 +240,19 @@ func _discipline_clock() -> void:

_offset = offset - nudge

@rpc("any_peer", "call_remote", "unreliable")
func _send_ping(idx: int) -> void:
func _handle_ping(sender: int, data: PackedByteArray) -> void:
var idx := bytes_to_var(data) as int
var ping_received := _clock.get_time()
var sender := multiplayer.get_remote_sender_id()

_send_pong.rpc_id(sender, idx, ping_received, _clock.get_time())
_cmd_pong.send(var_to_bytes([idx, ping_received, _clock.get_time()]), sender)

func _handle_pong(sender: int, data: PackedByteArray) -> void:
var args := bytes_to_var(data)

var idx := args[0] as int
var ping_received := args[1] as float
var pong_sent := args[2] as float

@rpc("any_peer", "call_remote", "unreliable")
func _send_pong(idx: int, ping_received: float, pong_sent: float) -> void:
var pong_received := _clock.get_time()

if not _awaiting_samples.has(idx):
Expand All @@ -264,13 +273,13 @@ func _send_pong(idx: int, ping_received: float, pong_sent: float) -> void:
# Discipline clock based on new sample
_discipline_clock()

@rpc("any_peer", "call_remote", "reliable")
func _request_timestamp() -> void:
func _handle_request_timestamp(sender: int, data: PackedByteArray) -> void:
_logger.debug("Requested initial timestamp @ %.4fs raw time", [_clock.get_raw_time()])
_set_timestamp.rpc_id(multiplayer.get_remote_sender_id(), _clock.get_time())
_cmd_set_time.send(var_to_bytes(_clock.get_time()), sender)

func _handle_set_timestamp(sender: int, data: PackedByteArray) -> void:
var timestamp := bytes_to_var(data) as float

@rpc("any_peer", "call_remote", "reliable")
func _set_timestamp(timestamp: float) -> void:
_logger.debug("Received initial timestamp @ %.4fs raw time", [_clock.get_raw_time()])
_clock.set_time(timestamp)
_loop()
2 changes: 1 addition & 1 deletion addons/netfox/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox"
description="Shared internals for netfox addons"
author="Tamas Galffy and contributors"
version="1.37.0"
version="1.38.0"
script="netfox.gd"
7 changes: 7 additions & 0 deletions addons/netfox/servers/data/network-commands.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extends Object
class_name _NetworkCommands

const NTP_PING := 1
const NTP_PONG := 2
const NTP_REQ_TIME := 3
const NTP_SET_TIME := 4
1 change: 1 addition & 0 deletions addons/netfox/servers/data/network-commands.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cptij4jrx07ix
179 changes: 179 additions & 0 deletions addons/netfox/servers/network-command-server.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
extends Node
class_name _NetworkCommandServer

## Transmits commands over the network
##
## Commands are a simpler, lightweight alternative to RPCs. Commands consist of
## a single byte for ID, and the raw binary data. The ID lets the receiving peer
## decide what to execute, with the binary data serving as the input.
## [br][br]
## Being a simpler construct makes commands a good fit for regular, fundamental
## operations.
## [br][br]
## Commands are, by default, transmitted over regular RPCs. To use less data,
## commands can also be transmitted as raw packets, using
## [method SceneMultiplayer.send_bytes]. This is an opt-in feature - if the game
## is already using [method SceneMultiplayer.send_bytes], it needs to be aware
## of commands, and must check each packet whether it's a command or one of its
## own packets. To check if a packet is a command, use [method
## is_command_packet].

var _packet_prefix := PackedByteArray([0, 78, 70]) # "\0nf"
var _next_idx := 0
var _rpc_transport := _RPCTransport.new()
var _packet_transport := _PacketTransport.new(_packet_prefix)
var _commands := {} # id to `Command`

var _use_raw := ProjectSettings.get_setting("netfox/general/use_raw_commands", false) as bool

static var _logger := NetfoxLogger._for_netfox("NetworkCommandServer")

func _ready():
add_child(_rpc_transport, true)
add_child(_packet_transport, true)

_rpc_transport.on_receive.connect(_handle_command)
_packet_transport.on_receive.connect(_handle_command)

## Register a command at the next available ID
func register_command(handler: Callable) -> Command:
var idx := _next_idx
_next_idx += 1
return register_command_at(idx, handler)

## Register a command at a specific index
## [br][br]
## A specific ID should only be registered once. Doing otherwise will trigger an
## assert in the editor, but will overwrite the previous command in release.
func register_command_at(idx: int, handler: Callable, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> Command:
assert(not _commands.has(idx), "Command #%d is already taken!" % idx)
var command := Command.new(self, idx, handler, mode, channel)
_commands[idx] = command

_next_idx = maxi(_next_idx, idx + 1)

return command

## Send a command with index and data
func send_command(idx: int, data: PackedByteArray, target_peer: int = 0, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> void:
if _use_raw:
_packet_transport.send(idx, data, target_peer, mode, channel)
else:
_rpc_transport.send(idx, data, target_peer, mode, channel)

## Return true if [param packet] is a command packet
## [br][br]
## Always returns [code]true[/code] if RPCs are used for transmitting commands.
func is_command_packet(packet: PackedByteArray) -> bool:
if not _use_raw:
return true
return _packet_transport.is_command_packet(packet)

## Return the prefix bytes for command packets
## [br][br]
## Can be used to avoid conflicts between command packets and game packets.
func get_command_packet_prefix() -> PackedByteArray:
return _packet_prefix

func _handle_command(sender: int, idx: int, data: PackedByteArray) -> void:
var command := _commands.get(idx) as Command
if not command:
_logger.error("Received unknown command #%d!", [idx])
return
command._handle(sender, data)

## Networked command
##
## Provides a convenient interface for sending and managing networked commands.
## [br][br]
## Should not be instantiated manually.
class Command:
var _command_server: _NetworkCommandServer

var _idx: int
var _handler: Callable
var _mode: MultiplayerPeer.TransferMode
var _channel: int

func _init(p_command_server: _NetworkCommandServer, p_idx: int, p_handler: Callable, p_mode: MultiplayerPeer.TransferMode, p_channel: int):
_command_server = p_command_server
_idx = p_idx
_handler = p_handler
_mode = p_mode
_channel = p_channel

## Send command
func send(data: PackedByteArray, target_peer: int = 0) -> void:
_command_server.send_command(_idx, data, target_peer, _mode, _channel)

func _handle(sender: int, data: PackedByteArray) -> void:
_handler.call(sender, data)

class _Transport extends Node:
signal on_receive(idx: int, data: PackedByteArray)

func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, channel: int) -> void:
pass

class _PacketTransport extends _Transport:
var _packet_prefix: PackedByteArray

func _init(p_packet_prefix: PackedByteArray):
_packet_prefix = p_packet_prefix

func _ready():
(multiplayer as SceneMultiplayer).peer_packet.connect(_handle_packet)

func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, channel: int) -> void:
var buffer := StreamPeerBuffer.new()
buffer.put_data(_packet_prefix)
buffer.put_u8(idx)
buffer.put_data(data)

(multiplayer as SceneMultiplayer).send_bytes(buffer.data_array, target_peer, mode, channel)

func is_command_packet(packet: PackedByteArray) -> bool:
if packet.size() < _packet_prefix.size():
return false

for i in _packet_prefix.size():
if packet[i] != _packet_prefix[i]:
return false
return true

func _handle_packet(peer: int, packet: PackedByteArray) -> void:
var buffer := StreamPeerBuffer.new()
buffer.data_array = packet

# Check header
if not is_command_packet(packet):
return

# Grab data
buffer.seek(_packet_prefix.size())
var idx := buffer.get_u8()
var data := buffer.get_partial_data(buffer.get_available_bytes())[1] as PackedByteArray

on_receive.emit(peer, idx, data)

class _RPCTransport extends _Transport:
func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, _channel: int) -> void:
match mode:
MultiplayerPeer.TRANSFER_MODE_UNRELIABLE: _submit_unreliable.rpc_id(target_peer, idx, data)
MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED: _submit_unreliable_ordered.rpc_id(target_peer, idx, data)
MultiplayerPeer.TRANSFER_MODE_RELIABLE: _submit_reliable.rpc_id(target_peer, idx, data)

@rpc("any_peer", "call_remote", "unreliable")
func _submit_unreliable(idx: int, data: PackedByteArray):
var sender := multiplayer.get_remote_sender_id()
on_receive.emit(sender, idx, data)

@rpc("any_peer", "call_remote", "unreliable_ordered")
func _submit_unreliable_ordered(idx: int, data: PackedByteArray):
var sender := multiplayer.get_remote_sender_id()
on_receive.emit(sender, idx, data)

@rpc("any_peer", "call_remote", "reliable")
func _submit_reliable(idx: int, data: PackedByteArray):
var sender := multiplayer.get_remote_sender_id()
on_receive.emit(sender, idx, data)
1 change: 1 addition & 0 deletions addons/netfox/servers/network-command-server.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://d1mbg8wkqq7ko
2 changes: 1 addition & 1 deletion addons/netfox/state-synchronizer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ var _property_cache: PropertyCache
var _property_config: _PropertyConfig = _PropertyConfig.new()
var _properties_dirty: bool = false

var _schema: _NetworkSchema
var _schema: _NetworkSchema = _NetworkSchema.new({})

var _state_history := _PropertyHistoryBuffer.new()

Expand Down
Loading
Loading