a43ce66a89
NetLink merkt sich die Verbindungen, die es gerade schließt, und schickt ihnen nichts mehr. Pakete an alle oder an alle außer einem gehen einzeln an die offenen Verbindungen, statt Godot ein Ziel zu nennen, das es nicht mehr gibt. Beides löste sonst Fehler in Godot aus, etwa wenn der Host Figuren weiterreicht, während er einen abgelehnten Gast hinauswirft. Ein neuer Test mit zwei Gästen deckt das ab. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
176 lines
5.8 KiB
GDScript
176 lines
5.8 KiB
GDScript
class_name NetLink
|
|
extends RefCounted
|
|
## The network connection of a session, over ENet. A host listens for guests;
|
|
## a guest connects to its host. Messages of the core travel on a reliable,
|
|
## ordered channel. Positions of figures travel on a second, unreliable
|
|
## channel, because a lost one is soon replaced by the next.
|
|
|
|
## Port a host listens on unless told otherwise.
|
|
const DEFAULT_PORT := 24680
|
|
## Most guests that may connect to a host. More than can play, so that the
|
|
## core can tell the extra ones why it turns them away.
|
|
const MAX_CONNECTIONS := 8
|
|
const CHANNEL_CORE := 0
|
|
const CHANNEL_FIGURES := 1
|
|
## Seconds a guest waits for its host to answer before it gives up.
|
|
const CONNECT_SECONDS := 8.0
|
|
|
|
## Seconds this guest waits for its host. Tests shorten it.
|
|
var connect_seconds := CONNECT_SECONDS
|
|
|
|
var _peer: ENetMultiplayerPeer
|
|
var _peers: Dictionary[int, bool] = {}
|
|
## Connections that close_peer is closing. Nothing more goes to them: Godot
|
|
## reports an error for every packet to a connection in that state.
|
|
var _closing: Dictionary[int, bool] = {}
|
|
var _events: Array[Dictionary] = []
|
|
var _connecting := false
|
|
var _waited := 0.0
|
|
|
|
|
|
## Splits "host" or "host:port" into [host, port], with DEFAULT_PORT if the
|
|
## text names none. Returns an empty array if `text` is no such address.
|
|
static func split_address(text: String) -> Array:
|
|
var parts := text.strip_edges().split(":")
|
|
if parts[0].is_empty() or parts.size() > 2:
|
|
return []
|
|
if parts.size() == 1:
|
|
return [parts[0], DEFAULT_PORT]
|
|
if not parts[1].is_valid_int() or parts[1].to_int() < 1 or parts[1].to_int() > 65535:
|
|
return []
|
|
return [parts[0], parts[1].to_int()]
|
|
|
|
|
|
## Starts listening for guests on `port`, or on a free port that the system
|
|
## chooses if `port` is 0; see local_port(). `address` limits listening to
|
|
## one network interface, for example "127.0.0.1"; "*" listens on all.
|
|
func host(port: int, address := "*") -> Error:
|
|
_start()
|
|
_peer.set_bind_ip(address)
|
|
return _peer.create_server(port, MAX_CONNECTIONS, 2)
|
|
|
|
|
|
## The port a host listens on, or 0.
|
|
func local_port() -> int:
|
|
if not is_open() or _peer.host == null:
|
|
return 0
|
|
return _peer.host.get_local_port()
|
|
|
|
|
|
## Starts connecting to the host at `address` and `port`.
|
|
func join(address: String, port: int) -> Error:
|
|
_start()
|
|
var error := _peer.create_client(address, port, 2)
|
|
_connecting = error == OK
|
|
_waited = 0.0
|
|
return error
|
|
|
|
|
|
func _start() -> void:
|
|
close()
|
|
_peer = ENetMultiplayerPeer.new()
|
|
_peers.clear()
|
|
_closing.clear()
|
|
_peer.peer_connected.connect(_on_connected)
|
|
_peer.peer_disconnected.connect(_on_disconnected)
|
|
|
|
|
|
func _on_connected(peer: int) -> void:
|
|
_peers[peer] = true
|
|
_events.append({"type": "connected", "peer": peer})
|
|
|
|
|
|
func _on_disconnected(peer: int) -> void:
|
|
_peers.erase(peer)
|
|
_closing.erase(peer)
|
|
_events.append({"type": "disconnected", "peer": peer})
|
|
|
|
|
|
## True while the connection is up or being made.
|
|
func is_open() -> bool:
|
|
return _peer != null and _peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED
|
|
|
|
|
|
## True when a guest has reached its host, or a host is listening.
|
|
func is_connected_to_peers() -> bool:
|
|
return _peer != null and _peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
|
|
|
|
|
|
## Returns what happened since the last call, oldest first, as dictionaries
|
|
## with a `type` entry: "connected" and "disconnected" (entry `peer`),
|
|
## "message" and "figure" (entries `peer` and `bytes`), and "failed" when a
|
|
## guest could not reach its host. `delta` is the time since the last call.
|
|
func poll(delta: float) -> Array[Dictionary]:
|
|
if is_open():
|
|
_peer.poll()
|
|
while _peer.get_available_packet_count() > 0:
|
|
var from := _peer.get_packet_peer()
|
|
var channel := _peer.get_packet_channel()
|
|
var bytes := _peer.get_packet()
|
|
var type := "message" if channel == CHANNEL_CORE else "figure"
|
|
_events.append({"type": type, "peer": from, "bytes": bytes})
|
|
if _connecting:
|
|
_waited += delta
|
|
if is_connected_to_peers():
|
|
_connecting = false
|
|
elif not is_open() or _waited > connect_seconds:
|
|
_connecting = false
|
|
close()
|
|
_events.append({"type": "failed"})
|
|
var events := _events
|
|
_events = []
|
|
return events
|
|
|
|
|
|
## Sends a message of the core to `peer`, reliably and in order. A message
|
|
## to a connection that is gone or being closed is dropped.
|
|
func send(peer: int, bytes: PackedByteArray) -> void:
|
|
_put(peer, bytes, CHANNEL_CORE, MultiplayerPeer.TRANSFER_MODE_RELIABLE)
|
|
|
|
|
|
## Sends the position of a figure to `peer`, to every connection if `peer`
|
|
## is 0, or to every connection but one if `peer` is that one negated.
|
|
func send_figure(peer: int, bytes: PackedByteArray) -> void:
|
|
_put(peer, bytes, CHANNEL_FIGURES, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
|
|
|
|
|
func _put(peer: int, bytes: PackedByteArray, channel: int, mode: MultiplayerPeer.TransferMode) -> void:
|
|
if not is_connected_to_peers():
|
|
return
|
|
_peer.set_transfer_channel(channel)
|
|
_peer.set_transfer_mode(mode)
|
|
# One packet per connection, so that no packet goes to a connection that
|
|
# is gone or being closed; Godot's own sending to all would.
|
|
for target in _targets(peer):
|
|
_peer.set_target_peer(target)
|
|
_peer.put_packet(bytes)
|
|
|
|
|
|
## The connections a packet for `peer` goes to: that one, every one if
|
|
## `peer` is 0, or every one but -`peer` if `peer` is negative. Connections
|
|
## that are being closed get nothing.
|
|
func _targets(peer: int) -> Array[int]:
|
|
var targets: Array[int] = []
|
|
for open: int in _peers:
|
|
if _closing.has(open):
|
|
continue
|
|
if peer == 0 or open == peer or (peer < 0 and open != -peer):
|
|
targets.append(open)
|
|
return targets
|
|
|
|
|
|
## Closes the connection to `peer` once the messages waiting for it are out.
|
|
func close_peer(peer: int) -> void:
|
|
if _peers.has(peer) and not _closing.has(peer):
|
|
_closing[peer] = true
|
|
_peer.get_peer(peer).peer_disconnect_later()
|
|
|
|
|
|
## Closes everything at once.
|
|
func close() -> void:
|
|
if is_open():
|
|
_peer.close()
|
|
_peers.clear()
|
|
_closing.clear()
|
|
_connecting = false
|