class_name SessionLink extends RefCounted ## Carries the messages of a GwSession over a NetLink, and the positions of ## the figures, which are not part of the shared world. Tells the game what ## happened on the way. ## Bytes of a position: x and y as 32-bit floats. const POSITION_BYTES := 8 var session: GwSession var link: NetLink func _init(game_session: GwSession, net_link: NetLink) -> void: session = game_session link = net_link ## Moves messages in both directions and returns notices, oldest first, as ## dictionaries with a `type` entry: ## - "guest_arrived" and "guest_left" (entry `peer`), at the host ## - "world_received" (entry `resync`), "refused" (entry `reason`), ## "protocol_error", "host_lost" and "failed", at a guest ## - "divergence" (entries `peer`, `tick`, `hash`), at the host ## - "figure" (entries `player`, `position`), everywhere, for another player ## who is online, with a position of real numbers ## `delta` is the time since the last call. func pump(delta: float) -> Array[Dictionary]: var notices: Array[Dictionary] = [] for event in link.poll(delta): match event["type"]: "connected": if session.is_host(): session.connect_peer(event["peer"]) notices.append({"type": "guest_arrived", "peer": event["peer"]}) "disconnected": if session.is_host(): session.disconnect_peer(event["peer"]) notices.append({"type": "guest_left", "peer": event["peer"]}) else: notices.append({"type": "host_lost"}) "failed": notices.append({"type": "failed"}) "message": notices.append_array(session.receive(event["peer"], event["bytes"])) "figure": _receive_figure(event["peer"], event["bytes"], notices) for divergence in session.take_divergences(): divergence["type"] = "divergence" notices.append(divergence) flush() return notices ## Sends what the session has for the network and closes the connections ## the host gave up. A guest's messages wait until its host is reached. func flush() -> void: if not link.is_connected_to_peers(): return for message in session.take_messages(): link.send(message["peer"], message["bytes"]) for peer in session.take_closing(): link.close_peer(peer) ## Tells the other machines where this machine's figure stands. func send_figure(position: Vector2) -> void: if session.is_host(): var player := session.local_player() if player >= 0: link.send_figure(0, _figure_bytes(player, position)) elif session.local_player() >= 0: link.send_figure(GwSession.HOST_PEER, _position_bytes(position)) func _receive_figure(peer: int, bytes: PackedByteArray, notices: Array[Dictionary]) -> void: if session.is_host(): # A guest sends only a position; the host knows whose it is and # passes it on to every other guest. var player := session.player_of_peer(peer) if player < 0 or bytes.size() != POSITION_BYTES: return var position := Vector2(bytes.decode_float(0), bytes.decode_float(4)) # Not a number or infinite, as a broken or changed client may send it: # such a position would spoil the figure for good. if not position.is_finite(): return notices.append({"type": "figure", "player": player, "position": position}) link.send_figure(-peer, _figure_bytes(player, position)) elif bytes.size() == 1 + POSITION_BYTES: var player := bytes.decode_u8(0) var position := Vector2(bytes.decode_float(1), bytes.decode_float(5)) # A position can arrive after its player left the world. if player != session.local_player() and session.online_players().has(player) and position.is_finite(): notices.append({"type": "figure", "player": player, "position": position}) func _position_bytes(position: Vector2) -> PackedByteArray: var bytes := PackedByteArray() bytes.resize(POSITION_BYTES) bytes.encode_float(0, position.x) bytes.encode_float(4, position.y) return bytes func _figure_bytes(player: int, position: Vector2) -> PackedByteArray: var bytes := PackedByteArray([player]) bytes.append_array(_position_bytes(position)) return bytes