e7299b17e9
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
239 lines
7.7 KiB
GDScript
239 lines
7.7 KiB
GDScript
extends Node
|
|
|
|
## Network Simulator
|
|
##
|
|
## Auto connects launched instances and simulates network conditions like
|
|
## latency and packet loss. To use simply add this node to your scene tree and
|
|
## hook up the signals.
|
|
|
|
## Signal emitted on the instance that successfully started a server.
|
|
## Can be used for custom initialization logic, e.g. automatic login during
|
|
## testing.
|
|
signal server_created
|
|
|
|
## Signal emitted on instances that successfully connected as a client.
|
|
## Can be used for custom initialization logic, e.g. automatic login during
|
|
## testing.
|
|
signal client_connected
|
|
|
|
## Enable to automatically host and connect on start
|
|
var enabled: bool = false
|
|
## Server listening address. Use [code]*[/code] for all interfaces, or
|
|
#3 [code]127.0.0.1[/code] for localhost.
|
|
var hostname: String = "127.0.0.1"
|
|
|
|
## Server port to listen on, UDP proxy will use port + 1 if simulating latency
|
|
## or packet loss
|
|
var server_port: int = 9999
|
|
|
|
## Use ENet's built-in range encoding for compression
|
|
var use_compression: bool = true
|
|
|
|
## Simulated latency in milliseconds. Total ping time will be double this value
|
|
## (to and from)
|
|
var latency_ms: int = 0
|
|
## Simulated packet loss percentage
|
|
var packet_loss_percent: float = 0.0
|
|
|
|
static var _logger: NetfoxLogger = NetfoxLogger._for_extras("NetworkSimulator")
|
|
|
|
var _enet_peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new()
|
|
|
|
# UDP proxy
|
|
var _proxy_thread: Thread
|
|
var _proxy_loop_enabled := true
|
|
var _udp_proxy_server: PacketPeerUDP
|
|
var _udp_proxy_port: int
|
|
var _rng_packet_loss: RandomNumberGenerator = RandomNumberGenerator.new()
|
|
|
|
# Connection tracking
|
|
var _client_peers: Dictionary = {} # port to PacketPeerUDP
|
|
var _client_to_server_queue: Array[QueueEntry] = []
|
|
var _server_to_client_queue: Array[QueueEntry] = []
|
|
|
|
class QueueEntry:
|
|
var packet_data: PackedByteArray
|
|
var queued_at: int
|
|
var source_port: int # Which client port this came from
|
|
|
|
func _init(packet: PackedByteArray, timestamp: int, port: int) -> void:
|
|
self.packet_data = packet
|
|
self.queued_at = timestamp
|
|
self.source_port = port
|
|
|
|
func _ready() -> void:
|
|
# Check if enabled
|
|
if not OS.has_feature("editor"):
|
|
_logger.debug("Running outside editor, disabling")
|
|
return
|
|
|
|
_load_project_settings()
|
|
if not enabled:
|
|
_logger.debug("Feature disabled")
|
|
return
|
|
|
|
for env_var in ["CI", "NETFOX_CI", "NETFOX_NO_AUTOCONNECT"]:
|
|
if OS.get_environment(env_var) != "":
|
|
_logger.debug("Environment variable %s set, disabling", [env_var])
|
|
return
|
|
|
|
await get_tree().process_frame
|
|
_udp_proxy_port = server_port + 1
|
|
|
|
var status = _try_and_host()
|
|
if status == Error.ERR_CANT_CREATE:
|
|
_try_and_join()
|
|
elif status != OK:
|
|
_logger.error("Autoconnect failed with error - %s", [error_string(status)])
|
|
|
|
if use_compression:
|
|
_enet_peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER)
|
|
|
|
multiplayer.multiplayer_peer = _enet_peer
|
|
|
|
func _is_proxy_required() -> bool:
|
|
return latency_ms > 0 or packet_loss_percent > 0.0
|
|
|
|
func _try_and_host() -> Error:
|
|
var status = _enet_peer.create_server(server_port)
|
|
if status == OK:
|
|
if _is_proxy_required():
|
|
_start_udp_proxy()
|
|
server_created.emit()
|
|
_logger.info("Server started on port %s", [server_port])
|
|
return status
|
|
|
|
func _try_and_join() -> Error:
|
|
var connect_port = server_port
|
|
if _is_proxy_required():
|
|
connect_port = _udp_proxy_port
|
|
var status = _enet_peer.create_client(hostname, connect_port)
|
|
if status == OK:
|
|
client_connected.emit()
|
|
_logger.info("Client connected to %s:%s", [hostname, connect_port])
|
|
return status
|
|
|
|
# Starts a UDP proxy server to simulate network conditions
|
|
# This will listen on _udp_proxy_port and forward packets to the server_port
|
|
# Runs on its own thread to avoid blocking the main thread
|
|
func _start_udp_proxy() -> void:
|
|
_proxy_thread = Thread.new()
|
|
_udp_proxy_server = PacketPeerUDP.new()
|
|
|
|
var bind_status = _udp_proxy_server.bind(_udp_proxy_port, hostname)
|
|
if bind_status != OK:
|
|
_logger.error("Failed to bind UDP proxy port: ", bind_status)
|
|
return
|
|
|
|
_proxy_thread.start(_process_loop)
|
|
|
|
func _process_packets() -> void:
|
|
var current_time: int = Time.get_ticks_msec()
|
|
var send_threshold: int = current_time - latency_ms
|
|
|
|
_read_client_to_server_packets(current_time)
|
|
_process_client_to_server_packets(send_threshold)
|
|
|
|
if not _client_peers.is_empty():
|
|
_read_server_to_client_packets(current_time)
|
|
_process_server_to_client_queue(send_threshold)
|
|
|
|
func _process_loop():
|
|
while _proxy_loop_enabled:
|
|
_process_packets()
|
|
OS.delay_msec(1)
|
|
|
|
func _load_project_settings() -> void:
|
|
enabled = ProjectSettings.get_setting(&"netfox/autoconnect/enabled", false)
|
|
hostname = ProjectSettings.get_setting(&"netfox/autoconnect/host", "127.0.0.1")
|
|
server_port = ProjectSettings.get_setting(&"netfox/autoconnect/port", 9999)
|
|
use_compression = ProjectSettings.get_setting(&"netfox/autoconnect/use_compression", false)
|
|
latency_ms = ProjectSettings.get_setting(&"netfox/autoconnect/simulated_latency_ms", 0)
|
|
packet_loss_percent = ProjectSettings.get_setting(&"netfox/autoconnect/simulated_packet_loss_chance", 0.0)
|
|
|
|
func _is_data_available() -> bool:
|
|
if _udp_proxy_server.get_available_packet_count() > 0:
|
|
return true
|
|
|
|
if not _client_to_server_queue.is_empty() or not _server_to_client_queue.is_empty():
|
|
return true
|
|
|
|
# Check if any client peers have packets waiting
|
|
for client_peer in _client_peers.values():
|
|
if client_peer.get_available_packet_count() > 0:
|
|
return true
|
|
|
|
return false
|
|
|
|
func _read_client_to_server_packets(current_time: int) -> void:
|
|
while _udp_proxy_server.get_available_packet_count() > 0:
|
|
var packet = _udp_proxy_server.get_packet()
|
|
var err = _udp_proxy_server.get_packet_error()
|
|
|
|
if err != OK:
|
|
_logger.error("UDP proxy incoming packet error: ", err)
|
|
continue
|
|
|
|
var from_port = _udp_proxy_server.get_packet_port()
|
|
_register_client_if_new(from_port)
|
|
|
|
_client_to_server_queue.push_back(QueueEntry.new(packet, current_time, from_port))
|
|
|
|
func _register_client_if_new(port: int) -> void:
|
|
if _client_peers.has(port):
|
|
return
|
|
|
|
# Create a dedicated peer for this client
|
|
var client_peer = PacketPeerUDP.new()
|
|
client_peer.set_dest_address(hostname, server_port)
|
|
_client_peers[port] = client_peer
|
|
|
|
func _process_client_to_server_packets(send_threshold: int) -> void:
|
|
var packets_to_keep: Array[QueueEntry] = []
|
|
|
|
for entry in _client_to_server_queue:
|
|
if send_threshold < entry.queued_at:
|
|
packets_to_keep.append(entry)
|
|
else:
|
|
if _should_send_packet():
|
|
var peer = _client_peers[entry.source_port] as PacketPeerUDP
|
|
peer.put_packet(entry.packet_data)
|
|
|
|
_client_to_server_queue = packets_to_keep
|
|
|
|
func _read_server_to_client_packets(current_time: int) -> void:
|
|
for client_port in _client_peers.keys():
|
|
var client_peer = _client_peers[client_port] as PacketPeerUDP
|
|
|
|
while client_peer.get_available_packet_count() > 0:
|
|
var packet = client_peer.get_packet()
|
|
var err = client_peer.get_packet_error()
|
|
|
|
if err != OK:
|
|
_logger.error("UDP proxy server-to-client packet error from port %s : %s", [client_port, err])
|
|
continue
|
|
|
|
_server_to_client_queue.push_back(QueueEntry.new(packet, current_time, client_port))
|
|
|
|
func _process_server_to_client_queue(send_threshold: int) -> void:
|
|
var packets_to_keep: Array[QueueEntry] = []
|
|
|
|
for entry in _server_to_client_queue:
|
|
if send_threshold < entry.queued_at:
|
|
packets_to_keep.append(entry)
|
|
else:
|
|
if _should_send_packet():
|
|
_udp_proxy_server.set_dest_address(hostname, entry.source_port)
|
|
_udp_proxy_server.put_packet(entry.packet_data)
|
|
|
|
_server_to_client_queue = packets_to_keep
|
|
|
|
# Send packet or simulate loss
|
|
func _should_send_packet() -> bool:
|
|
return packet_loss_percent <= 0.0 or _rng_packet_loss.randf() >= (packet_loss_percent / 100.0)
|
|
|
|
func _exit_tree() -> void:
|
|
if _proxy_thread and _proxy_thread.is_started():
|
|
_proxy_loop_enabled = false
|
|
_proxy_thread.wait_to_finish()
|