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