Skip to content

Commit 8821797

Browse files
authored
Merge branch 'foxssake:main' into network-area-node
2 parents 6419bdd + 7c1e9ef commit 8821797

File tree

76 files changed

+1868
-141
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1868
-141
lines changed

.github/workflows/site.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ jobs:
3232
run: pip install mkdocs && pip install $(mkdocs get-deps) && pip install mkdocs-material
3333
- name: Setup mike
3434
run: pip install mike
35+
- name: Setup graphviz
36+
run: sudo apt install graphviz
3537
- name: Start PlantUML server
3638
run: sh/setup-plantuml.sh
3739
- name: Build

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ buildtmp
2525
vest.log
2626

2727
plantuml-*.jar
28+
29+
# Only for local use
30+
sh/ensure-uids.sh

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ and netfox.
119119

120120
#### Feature examples
121121

122+
Each of these examples give a starting point to a specific feature:
123+
122124
* [Input gathering](examples/input-gathering)
123125
* [State machines](examples/multiplayer-state-machine)
124126
* [Multiplayer FPS](examples/multiplayer-fps)
@@ -127,18 +129,25 @@ and netfox.
127129
* [Input prediction](examples/input-prediction)
128130
* [NPCs with rollback](examples/rollback-npc)
129131
* [NPCs with StateSynchronizer](examples/state-synchronizer-npc)
130-
* [Godot Rocket League](https://github.com/albertok/godot-rocket-league)
132+
* [Visibility filtering](examples/visibility-filtering)
131133

132-
#### Example game
134+
#### Example games
133135

134-
* [Forest Brawl]
136+
##### Forest Brawl
135137

136138
To provide examples of netfox usage in an actual game, [Forest Brawl] was
137139
created and included specifically for this purpose.
138140

139141
It's a party game where an arbitrary amount of players compete by trying to
140142
knock eachother off of the map.
141143

144+
##### Godot Rocket League
145+
146+
Play soccer with cars, in Godot, with [Godot Rocket League](https://github.com/albertok/godot-rocket-league)!
147+
148+
Demonstrates *netfox*'s physics rollback capabilities, ensuring smooth and
149+
responsive physics-based gameplay.
150+
142151
## Built with netfox
143152

144153
Games built with netfox, coming to a Steam near you! See the more on the site's

addons/netfox.extras/netfox-extras.gd

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const ROOT = "res://addons/netfox.extras"
66
var SETTINGS = [
77
_NetfoxLogger.make_setting("netfox/logging/netfox_extras_log_level"),
88

9-
#Window Tiler Settings
9+
# Window Tiler Settings
1010
{
1111
"name": "netfox/extras/auto_tile_windows",
1212
"value": false,
@@ -22,12 +22,54 @@ var SETTINGS = [
2222
"value": false,
2323
"type": TYPE_BOOL
2424
},
25+
26+
# Autoconnect settings
27+
{
28+
"name": "netfox/autoconnect/enabled",
29+
"value": false,
30+
"type": TYPE_BOOL
31+
},
32+
{
33+
"name": "netfox/autoconnect/host",
34+
"value": "127.0.0.1",
35+
"type": TYPE_STRING
36+
},
37+
{
38+
"name": "netfox/autoconnect/port",
39+
"value": 9999,
40+
"type": TYPE_INT,
41+
"hint": PROPERTY_HINT_RANGE,
42+
"hint_string": "1,65535,hide_slider"
43+
},
44+
{
45+
"name": "netfox/autoconnect/use_compression",
46+
"value": false,
47+
"type": TYPE_BOOL
48+
},
49+
{
50+
"name": "netfox/autoconnect/simulated_latency_ms",
51+
"value": 0.0,
52+
"type": TYPE_INT,
53+
"hint": PROPERTY_HINT_RANGE,
54+
"hint_string": "0,200,or_greater"
55+
},
56+
{
57+
"name": "netfox/autoconnect/simulated_packet_loss_chance",
58+
"value": 0.0,
59+
"type": TYPE_FLOAT,
60+
"hint": PROPERTY_HINT_RANGE,
61+
"hint_string": "0,1"
62+
}
2563
]
2664

2765
const AUTOLOADS = [
2866
{
2967
"name": "WindowTiler",
3068
"path": ROOT + "/window-tiler.gd"
69+
},
70+
{
71+
"name": "NetworkSimulator",
72+
"path": ROOT + "/network-simulator.gd"
3173
}
3274
]
3375

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
extends Node
2+
3+
## Network Simulator
4+
##
5+
## Auto connects launched instances and simulates network conditions like
6+
## latency and packet loss. To use simply add this node to your scene tree and
7+
## hook up the signals.
8+
9+
## Signal emitted on the instance that successfully started a server.
10+
## Can be used for custom initialization logic, e.g. automatic login during
11+
## testing.
12+
signal server_created
13+
14+
## Signal emitted on instances that successfully connected as a client.
15+
## Can be used for custom initialization logic, e.g. automatic login during
16+
## testing.
17+
signal client_connected
18+
19+
## Enable to automatically host and connect on start
20+
var enabled: bool = false
21+
## Server listening address. Use [code]*[/code] for all interfaces, or
22+
#3 [code]127.0.0.1[/code] for localhost.
23+
var hostname: String = "127.0.0.1"
24+
25+
## Server port to listen on, UDP proxy will use port + 1 if simulating latency
26+
## or packet loss
27+
var server_port: int = 9999
28+
29+
## Use ENet's built-in range encoding for compression
30+
var use_compression: bool = true
31+
32+
## Simulated latency in milliseconds. Total ping time will be double this value
33+
## (to and from)
34+
var latency_ms: int = 0
35+
## Simulated packet loss percentage
36+
var packet_loss_percent: float = 0.0
37+
38+
static var _logger: _NetfoxLogger = _NetfoxLogger.for_extras("NetworkSimulator")
39+
40+
var _enet_peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new()
41+
42+
# UDP proxy
43+
var _proxy_thread: Thread
44+
var _udp_proxy_server: PacketPeerUDP
45+
var _udp_proxy_port: int
46+
var _rng_packet_loss: RandomNumberGenerator = RandomNumberGenerator.new()
47+
48+
# Connection tracking
49+
var _client_peers: Dictionary = {} # port to PacketPeerUDP
50+
var _client_to_server_queue: Array[QueueEntry] = []
51+
var _server_to_client_queue: Array[QueueEntry] = []
52+
53+
class QueueEntry:
54+
var packet_data: PackedByteArray
55+
var queued_at: int
56+
var source_port: int # Which client port this came from
57+
58+
func _init(packet: PackedByteArray, timestamp: int, port: int) -> void:
59+
self.packet_data = packet
60+
self.queued_at = timestamp
61+
self.source_port = port
62+
63+
func _ready() -> void:
64+
# Check if enabled
65+
if not OS.has_feature("editor"):
66+
_logger.debug("Running outside editor, disabling")
67+
return
68+
69+
_load_project_settings()
70+
if not enabled:
71+
_logger.debug("Feature disabled")
72+
return
73+
74+
for env_var in ["CI", "NETFOX_CI", "NETFOX_NO_AUTOCONNECT"]:
75+
if OS.get_environment(env_var) != "":
76+
_logger.debug("Environment variable %s set, disabling", [env_var])
77+
return
78+
79+
await get_tree().process_frame
80+
_udp_proxy_port = server_port + 1
81+
82+
var status = _try_and_host()
83+
if status == Error.ERR_CANT_CREATE:
84+
_try_and_join()
85+
elif status != OK:
86+
_logger.error("Autoconnect failed with error - %s", [error_string(status)])
87+
88+
if use_compression:
89+
_enet_peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER)
90+
91+
multiplayer.multiplayer_peer = _enet_peer
92+
93+
func _is_proxy_required() -> bool:
94+
return latency_ms > 0 or packet_loss_percent > 0.0
95+
96+
func _try_and_host() -> Error:
97+
var status = _enet_peer.create_server(server_port)
98+
if status == OK:
99+
if _is_proxy_required():
100+
_start_udp_proxy()
101+
server_created.emit()
102+
_logger.info("Server started on port %s", [server_port])
103+
return status
104+
105+
func _try_and_join() -> Error:
106+
var connect_port = server_port
107+
if _is_proxy_required():
108+
connect_port = _udp_proxy_port
109+
var status = _enet_peer.create_client(hostname, connect_port)
110+
if status == OK:
111+
client_connected.emit()
112+
_logger.info("Client connected to %s:%s", [hostname, connect_port])
113+
return status
114+
115+
# Starts a UDP proxy server to simulate network conditions
116+
# This will listen on _udp_proxy_port and forward packets to the server_port
117+
# Runs on its own thread to avoid blocking the main thread
118+
func _start_udp_proxy() -> void:
119+
_proxy_thread = Thread.new()
120+
_udp_proxy_server = PacketPeerUDP.new()
121+
122+
var bind_status = _udp_proxy_server.bind(_udp_proxy_port, hostname)
123+
if bind_status != OK:
124+
_logger.error("Failed to bind UDP proxy port: ", bind_status)
125+
return
126+
127+
_proxy_thread.start(_process_loop)
128+
129+
func _process_packets() -> void:
130+
var current_time: int = Time.get_ticks_msec()
131+
var send_threshold: int = current_time - latency_ms
132+
133+
_read_client_to_server_packets(current_time)
134+
_process_client_to_server_packets(send_threshold)
135+
136+
if not _client_peers.is_empty():
137+
_read_server_to_client_packets(current_time)
138+
_process_server_to_client_queue(send_threshold)
139+
140+
func _process_loop():
141+
while true:
142+
_process_packets()
143+
OS.delay_msec(1)
144+
145+
func _load_project_settings() -> void:
146+
enabled = ProjectSettings.get_setting(&"netfox/autoconnect/enabled", false)
147+
hostname = ProjectSettings.get_setting(&"netfox/autoconnect/host", "127.0.0.1")
148+
server_port = ProjectSettings.get_setting(&"netfox/autoconnect/port", 9999)
149+
use_compression = ProjectSettings.get_setting(&"netfox/autoconnect/use_compression", false)
150+
latency_ms = ProjectSettings.get_setting(&"netfox/autoconnect/simulated_latency_ms", 0)
151+
packet_loss_percent = ProjectSettings.get_setting(&"netfox/autoconnect/simulated_packet_loss_chance", 0.0)
152+
153+
func _is_data_available() -> bool:
154+
if _udp_proxy_server.get_available_packet_count() > 0:
155+
return true
156+
157+
if not _client_to_server_queue.is_empty() or not _server_to_client_queue.is_empty():
158+
return true
159+
160+
# Check if any client peers have packets waiting
161+
for client_peer in _client_peers.values():
162+
if client_peer.get_available_packet_count() > 0:
163+
return true
164+
165+
return false
166+
167+
func _read_client_to_server_packets(current_time: int) -> void:
168+
while _udp_proxy_server.get_available_packet_count() > 0:
169+
var packet = _udp_proxy_server.get_packet()
170+
var err = _udp_proxy_server.get_packet_error()
171+
172+
if err != OK:
173+
_logger.error("UDP proxy incoming packet error: ", err)
174+
continue
175+
176+
var from_port = _udp_proxy_server.get_packet_port()
177+
_register_client_if_new(from_port)
178+
179+
_client_to_server_queue.push_back(QueueEntry.new(packet, current_time, from_port))
180+
181+
func _register_client_if_new(port: int) -> void:
182+
if _client_peers.has(port):
183+
return
184+
185+
# Create a dedicated peer for this client
186+
var client_peer = PacketPeerUDP.new()
187+
client_peer.set_dest_address(hostname, server_port)
188+
_client_peers[port] = client_peer
189+
190+
func _process_client_to_server_packets(send_threshold: int) -> void:
191+
var packets_to_keep: Array[QueueEntry] = []
192+
193+
for entry in _client_to_server_queue:
194+
if send_threshold < entry.queued_at:
195+
packets_to_keep.append(entry)
196+
else:
197+
if _should_send_packet():
198+
var peer = _client_peers[entry.source_port] as PacketPeerUDP
199+
peer.put_packet(entry.packet_data)
200+
201+
_client_to_server_queue = packets_to_keep
202+
203+
func _read_server_to_client_packets(current_time: int) -> void:
204+
for client_port in _client_peers.keys():
205+
var client_peer = _client_peers[client_port] as PacketPeerUDP
206+
207+
while client_peer.get_available_packet_count() > 0:
208+
var packet = client_peer.get_packet()
209+
var err = client_peer.get_packet_error()
210+
211+
if err != OK:
212+
_logger.error("UDP proxy server-to-client packet error from port %s : %s", [client_port, err])
213+
continue
214+
215+
_server_to_client_queue.push_back(QueueEntry.new(packet, current_time, client_port))
216+
217+
func _process_server_to_client_queue(send_threshold: int) -> void:
218+
var packets_to_keep: Array[QueueEntry] = []
219+
220+
for entry in _server_to_client_queue:
221+
if send_threshold < entry.queued_at:
222+
packets_to_keep.append(entry)
223+
else:
224+
if _should_send_packet():
225+
_udp_proxy_server.set_dest_address(hostname, entry.source_port)
226+
_udp_proxy_server.put_packet(entry.packet_data)
227+
228+
_server_to_client_queue = packets_to_keep
229+
230+
# Send packet or simulate loss
231+
func _should_send_packet() -> bool:
232+
return packet_loss_percent <= 0.0 or _rng_packet_loss.randf() >= (packet_loss_percent / 100.0)
233+
234+
func _exit_tree() -> void:
235+
if _proxy_thread and _proxy_thread.is_started():
236+
_proxy_thread.wait_to_finish()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://1y2ey1e6qjwr

0 commit comments

Comments
 (0)