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 channel that is ## unreliable but ordered: a lost position is soon replaced by the next, and ## an older one that arrives after a newer one is dropped. ## 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 ## Milliseconds after which a connection whose packets stay unanswered counts ## as lost. ENet's own limit of 30 seconds would leave a guest whose host ## vanished in a standing world for that long. const TIMEOUT_MSEC := 8000 ## 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 var connection := _peer.get_peer(peer) # ENet slows unreliable packets down after a spike in the round trip and # can keep them at nothing for the rest of a session, so that the figures # of one guest stop. Positions are a few bytes ten times a second; they # are never slowed down. connection.throttle_configure(5000, ENetPacketPeer.PACKET_THROTTLE_SCALE, 0) connection.set_timeout(32, 5000, TIMEOUT_MSEC) _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_ORDERED) 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