diff --git a/game/scripts/net_link.gd b/game/scripts/net_link.gd index 1e5ed2c..65aad40 100644 --- a/game/scripts/net_link.gd +++ b/game/scripts/net_link.gd @@ -20,6 +20,9 @@ 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 @@ -67,6 +70,7 @@ func _start() -> void: close() _peer = ENetMultiplayerPeer.new() _peers.clear() + _closing.clear() _peer.peer_connected.connect(_on_connected) _peer.peer_disconnected.connect(_on_disconnected) @@ -78,6 +82,7 @@ func _on_connected(peer: int) -> void: func _on_disconnected(peer: int) -> void: _peers.erase(peer) + _closing.erase(peer) _events.append({"type": "disconnected", "peer": peer}) @@ -118,7 +123,7 @@ func poll(delta: float) -> Array[Dictionary]: ## Sends a message of the core to `peer`, reliably and in order. A message -## to a connection that is gone is dropped. +## 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) @@ -130,19 +135,34 @@ func send_figure(peer: int, bytes: PackedByteArray) -> void: func _put(peer: int, bytes: PackedByteArray, channel: int, mode: MultiplayerPeer.TransferMode) -> void: - if not is_connected_to_peers() or _peers.is_empty(): + if not is_connected_to_peers(): return - if peer > 0 and not _peers.has(peer): - return - _peer.set_target_peer(peer) _peer.set_transfer_channel(channel) _peer.set_transfer_mode(mode) - _peer.put_packet(bytes) + # 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): + if _peers.has(peer) and not _closing.has(peer): + _closing[peer] = true _peer.get_peer(peer).peer_disconnect_later() @@ -151,4 +171,5 @@ func close() -> void: if is_open(): _peer.close() _peers.clear() + _closing.clear() _connecting = false diff --git a/game/tests/test_net_link.gd b/game/tests/test_net_link.gd index 4cf5a85..68daffa 100644 --- a/game/tests/test_net_link.gd +++ b/game/tests/test_net_link.gd @@ -100,14 +100,43 @@ func test_a_guest_that_quits_is_seen_leaving() -> void: host.close() -func test_messages_to_a_connection_that_is_gone_are_dropped() -> void: +## Godot reports an error for every packet to a connection that is gone or +## being closed; the test runner turns each such error into a failure. +func test_nothing_is_sent_to_connections_that_are_gone_or_closing() -> void: var host := NetLink.new() check_eq(host.host(0, "127.0.0.1"), OK, "the host listens") + var first := NetLink.new() + var second := NetLink.new() + first.join("127.0.0.1", host.local_port()) + second.join("127.0.0.1", host.local_port()) + await _pump([host, first, second], 10) + var peers: Array = _events[host].map(func(event: Dictionary) -> int: return event["peer"]) + check_eq(peers.size(), 2, "two guests connected") + var leaving: int = peers[0] + var staying: int = peers[1] + + # Connections that never existed. host.send(12345, PackedByteArray([1])) host.send_figure(12345, PackedByteArray([1])) + host.send_figure(-12345, PackedByteArray([1])) host.close_peer(12345) - check_eq(host.poll(0.0), [], "nothing happened") - host.close() + # A connection that is being closed, and everybody while it closes. + host.close_peer(leaving) + for frame in 10: + host.send(leaving, PackedByteArray([2])) + host.send_figure(leaving, PackedByteArray([2])) + host.send_figure(-staying, PackedByteArray([2])) + host.send_figure(0, PackedByteArray([2])) + await _pump([host, first, second], 1) + # The connection is gone now. + host.send(leaving, PackedByteArray([3])) + host.send_figure(-leaving, PackedByteArray([3])) + await _pump([host, first, second], 5) + + check(_types(first).has("disconnected"), "the first guest was closed") + check(not _types(second).has("disconnected"), "the second guest stays") + for link in [host, first, second]: + link.close() func test_a_guest_gives_up_when_nobody_answers() -> void: