M1c: Gleichschritt im Netz #9
@@ -0,0 +1,154 @@
|
|||||||
|
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] = {}
|
||||||
|
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()
|
||||||
|
_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)
|
||||||
|
_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 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() or _peers.is_empty():
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
## Closes the connection to `peer` once the messages waiting for it are out.
|
||||||
|
func close_peer(peer: int) -> void:
|
||||||
|
if _peers.has(peer):
|
||||||
|
_peer.get_peer(peer).peer_disconnect_later()
|
||||||
|
|
||||||
|
|
||||||
|
## Closes everything at once.
|
||||||
|
func close() -> void:
|
||||||
|
if is_open():
|
||||||
|
_peer.close()
|
||||||
|
_peers.clear()
|
||||||
|
_connecting = false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dmyy8qbtuhhdh
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
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
|
||||||
|
## `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))
|
||||||
|
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)
|
||||||
|
if player != session.local_player():
|
||||||
|
var position := Vector2(bytes.decode_float(1), bytes.decode_float(5))
|
||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dpau4ls365tsj
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
extends TestCase
|
||||||
|
## Two NetLinks in one process, connected over the loopback interface.
|
||||||
|
|
||||||
|
## Events per link, collected while frames pass.
|
||||||
|
var _events: Dictionary[NetLink, Array] = {}
|
||||||
|
|
||||||
|
|
||||||
|
## Polls `links` for `frames` frames and collects their events.
|
||||||
|
func _pump(links: Array[NetLink], frames: int) -> void:
|
||||||
|
for frame in frames:
|
||||||
|
for link in links:
|
||||||
|
if not _events.has(link):
|
||||||
|
_events[link] = []
|
||||||
|
_events[link].append_array(link.poll(1.0 / 60.0))
|
||||||
|
await tree.process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _types(link: NetLink) -> Array:
|
||||||
|
return _events.get(link, []).map(func(event: Dictionary) -> String: return event["type"])
|
||||||
|
|
||||||
|
|
||||||
|
func _first(link: NetLink, type: String) -> Dictionary:
|
||||||
|
for event: Dictionary in _events.get(link, []):
|
||||||
|
if event["type"] == type:
|
||||||
|
return event
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
## A host and a guest that have connected.
|
||||||
|
func _connected_pair() -> Array[NetLink]:
|
||||||
|
var host := NetLink.new()
|
||||||
|
check_eq(host.host(0, "127.0.0.1"), OK, "the host listens")
|
||||||
|
check(host.local_port() > 0, "on a port the system chose")
|
||||||
|
var guest := NetLink.new()
|
||||||
|
check_eq(guest.join("127.0.0.1", host.local_port()), OK, "the guest starts connecting")
|
||||||
|
await _pump([host, guest], 10)
|
||||||
|
return [host, guest]
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_guest_connects_and_both_sides_hear_of_it() -> void:
|
||||||
|
var links := await _connected_pair()
|
||||||
|
var host := links[0]
|
||||||
|
var guest := links[1]
|
||||||
|
check(guest.is_connected_to_peers(), "the guest is connected")
|
||||||
|
check_eq(_first(guest, "connected").get("peer"), GwSession.HOST_PEER, "the guest sees the host as peer 1")
|
||||||
|
check(_first(host, "connected").get("peer", 0) > 1, "the host sees the guest")
|
||||||
|
host.close()
|
||||||
|
guest.close()
|
||||||
|
|
||||||
|
|
||||||
|
func test_messages_arrive_in_order_on_both_channels() -> void:
|
||||||
|
var links := await _connected_pair()
|
||||||
|
var host := links[0]
|
||||||
|
var guest := links[1]
|
||||||
|
var guest_peer: int = _first(host, "connected")["peer"]
|
||||||
|
_events.clear()
|
||||||
|
var big := PackedByteArray()
|
||||||
|
big.resize(50000)
|
||||||
|
big.fill(7)
|
||||||
|
host.send(guest_peer, big)
|
||||||
|
host.send(guest_peer, PackedByteArray([1]))
|
||||||
|
guest.send(GwSession.HOST_PEER, PackedByteArray([2, 3]))
|
||||||
|
guest.send_figure(GwSession.HOST_PEER, PackedByteArray([4]))
|
||||||
|
await _pump([host, guest], 20)
|
||||||
|
|
||||||
|
var received := _events[guest].filter(func(event: Dictionary) -> bool: return event["type"] == "message")
|
||||||
|
check_eq(received.size(), 2, "two messages for the guest")
|
||||||
|
if received.size() == 2:
|
||||||
|
check_eq(received[0]["bytes"].size(), 50000, "the big one first")
|
||||||
|
check_eq(received[1]["bytes"], PackedByteArray([1]), "then the small one")
|
||||||
|
check_eq(_first(host, "message").get("bytes"), PackedByteArray([2, 3]), "the host got the guest's message")
|
||||||
|
check_eq(_first(host, "figure").get("bytes"), PackedByteArray([4]), "and the position on the other channel")
|
||||||
|
host.close()
|
||||||
|
guest.close()
|
||||||
|
|
||||||
|
|
||||||
|
func test_closing_a_guest_delivers_what_was_sent_before() -> void:
|
||||||
|
var links := await _connected_pair()
|
||||||
|
var host := links[0]
|
||||||
|
var guest := links[1]
|
||||||
|
var guest_peer: int = _first(host, "connected")["peer"]
|
||||||
|
_events.clear()
|
||||||
|
host.send(guest_peer, PackedByteArray([9, 9]))
|
||||||
|
host.close_peer(guest_peer)
|
||||||
|
await _pump([host, guest], 30)
|
||||||
|
check_eq(_types(guest), ["message", "disconnected"], "the message comes before the end")
|
||||||
|
check_eq(_types(host), ["disconnected"], "the host sees the guest go")
|
||||||
|
check(not guest.is_open(), "the guest's connection is closed")
|
||||||
|
host.close()
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_guest_that_quits_is_seen_leaving() -> void:
|
||||||
|
var links := await _connected_pair()
|
||||||
|
var host := links[0]
|
||||||
|
var guest := links[1]
|
||||||
|
_events.clear()
|
||||||
|
guest.close()
|
||||||
|
await _pump([host, guest], 10)
|
||||||
|
check_eq(_types(host), ["disconnected"], "the host sees the guest go")
|
||||||
|
host.close()
|
||||||
|
|
||||||
|
|
||||||
|
func test_messages_to_a_connection_that_is_gone_are_dropped() -> void:
|
||||||
|
var host := NetLink.new()
|
||||||
|
check_eq(host.host(0, "127.0.0.1"), OK, "the host listens")
|
||||||
|
host.send(12345, PackedByteArray([1]))
|
||||||
|
host.send_figure(12345, PackedByteArray([1]))
|
||||||
|
host.close_peer(12345)
|
||||||
|
check_eq(host.poll(0.0), [], "nothing happened")
|
||||||
|
host.close()
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_guest_gives_up_when_nobody_answers() -> void:
|
||||||
|
# A port that was free a moment ago and has nobody listening now.
|
||||||
|
var probe := NetLink.new()
|
||||||
|
probe.host(0, "127.0.0.1")
|
||||||
|
var port := probe.local_port()
|
||||||
|
probe.close()
|
||||||
|
var guest := NetLink.new()
|
||||||
|
guest.connect_seconds = 0.2
|
||||||
|
check_eq(guest.join("127.0.0.1", port), OK, "connecting starts")
|
||||||
|
await _pump([guest], 30)
|
||||||
|
check_eq(_types(guest), ["failed"], "the guest gives up")
|
||||||
|
check(not guest.is_open(), "and closes")
|
||||||
|
|
||||||
|
|
||||||
|
func test_addresses_are_split_into_host_and_port() -> void:
|
||||||
|
check_eq(NetLink.split_address("192.168.1.20"), ["192.168.1.20", NetLink.DEFAULT_PORT], "default port")
|
||||||
|
check_eq(NetLink.split_address(" example.net:25000 "), ["example.net", 25000], "with port")
|
||||||
|
for text in ["", ":25000", "host:", "host:0", "host:70000", "host:port", "a:1:2"]:
|
||||||
|
check_eq(NetLink.split_address(text), [], "refused: '%s'" % text)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bmma6gn72s36v
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
extends TestCase
|
||||||
|
## A host and guests in one process, each with its own GwSession and
|
||||||
|
## NetLink, connected over the loopback interface.
|
||||||
|
|
||||||
|
var host_key := PackedByteArray([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||||
|
var guest_key := PackedByteArray([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
|
||||||
|
|
||||||
|
var host: SessionLink
|
||||||
|
## Notices per SessionLink, collected while frames pass.
|
||||||
|
var notices: Dictionary[SessionLink, Array] = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _start_host() -> void:
|
||||||
|
var session := GwSession.new()
|
||||||
|
session.start_new(7)
|
||||||
|
session.join(host_key, "Host")
|
||||||
|
session.step()
|
||||||
|
var link := NetLink.new()
|
||||||
|
check_eq(link.host(0, "127.0.0.1"), OK, "the host listens")
|
||||||
|
host = SessionLink.new(session, link)
|
||||||
|
notices[host] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _start_guest(key: PackedByteArray, name: String) -> SessionLink:
|
||||||
|
var session := GwSession.new()
|
||||||
|
session.start_guest(key, name)
|
||||||
|
var link := NetLink.new()
|
||||||
|
link.join("127.0.0.1", host.link.local_port())
|
||||||
|
var guest := SessionLink.new(session, link)
|
||||||
|
notices[guest] = []
|
||||||
|
return guest
|
||||||
|
|
||||||
|
|
||||||
|
## Lets `frames` frames pass. The host runs a tick per frame if `ticking`;
|
||||||
|
## every guest runs what arrived.
|
||||||
|
func _play(guests: Array[SessionLink], frames: int, ticking := true) -> void:
|
||||||
|
for frame in frames:
|
||||||
|
notices[host].append_array(host.pump(1.0 / 60.0))
|
||||||
|
if ticking:
|
||||||
|
host.session.step()
|
||||||
|
host.flush()
|
||||||
|
for guest in guests:
|
||||||
|
notices[guest].append_array(guest.pump(1.0 / 60.0))
|
||||||
|
for i in guest.session.pending_ticks():
|
||||||
|
guest.session.step()
|
||||||
|
await tree.process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _types(link: SessionLink) -> Array:
|
||||||
|
return notices[link].map(func(notice: Dictionary) -> String: return notice["type"])
|
||||||
|
|
||||||
|
|
||||||
|
func _first(link: SessionLink, type: String) -> Dictionary:
|
||||||
|
for notice: Dictionary in notices[link]:
|
||||||
|
if notice["type"] == type:
|
||||||
|
return notice
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _close(links: Array[SessionLink]) -> void:
|
||||||
|
for link in links:
|
||||||
|
link.link.close()
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_guest_joins_over_the_network_and_follows_the_host() -> void:
|
||||||
|
_start_host()
|
||||||
|
var guest := _start_guest(guest_key, "Grace")
|
||||||
|
await _play([guest], 40)
|
||||||
|
await _play([guest], 10, false)
|
||||||
|
|
||||||
|
check(_types(host).has("guest_arrived"), "the host sees the guest arrive")
|
||||||
|
check_eq(_first(guest, "world_received").get("resync"), false, "the guest got the host's world")
|
||||||
|
check_eq(guest.session.local_player(), 1, "the guest is player 1")
|
||||||
|
check_eq(guest.session.player_name(0), "Host", "the guest knows the host's name")
|
||||||
|
check_eq(guest.session.tick(), host.session.tick(), "the guest is at the host's tick")
|
||||||
|
check_eq(guest.session.state_hash_hex(), host.session.state_hash_hex(), "both worlds are the same")
|
||||||
|
check_eq(guest.session.save().size(), 0, "a guest does not save the host's world")
|
||||||
|
_close([host, guest])
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_hit_by_the_guest_reaches_the_host() -> void:
|
||||||
|
_start_host()
|
||||||
|
var guest := _start_guest(guest_key, "Grace")
|
||||||
|
await _play([guest], 30)
|
||||||
|
var rock := host.session.spawn_point() + Vector2i(7, -2)
|
||||||
|
guest.session.dig(rock.x, rock.y)
|
||||||
|
await _play([guest], 10)
|
||||||
|
await _play([guest], 10, false)
|
||||||
|
var index := rock.y * host.session.map_width() + rock.x
|
||||||
|
check_eq(host.session.damage_codes()[index], 1, "the host's rock took the hit")
|
||||||
|
check_eq(guest.session.state_hash_hex(), host.session.state_hash_hex(), "both worlds are the same")
|
||||||
|
_close([host, guest])
|
||||||
|
|
||||||
|
|
||||||
|
func test_figures_travel_to_the_other_machines() -> void:
|
||||||
|
_start_host()
|
||||||
|
var guest := _start_guest(guest_key, "Grace")
|
||||||
|
var other := _start_guest(PackedByteArray([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]), "Linus")
|
||||||
|
await _play([guest, other], 30)
|
||||||
|
host.send_figure(Vector2(10.0, 20.0))
|
||||||
|
guest.send_figure(Vector2(5.5, 6.5))
|
||||||
|
await _play([guest, other], 10)
|
||||||
|
|
||||||
|
check_eq(_first(host, "figure"), {"type": "figure", "player": 1, "position": Vector2(5.5, 6.5)}, "the host sees the guest")
|
||||||
|
var seen := notices[other].filter(func(notice: Dictionary) -> bool: return notice["type"] == "figure")
|
||||||
|
check(seen.has({"type": "figure", "player": 0, "position": Vector2(10.0, 20.0)}), "the other guest sees the host")
|
||||||
|
check(seen.has({"type": "figure", "player": 1, "position": Vector2(5.5, 6.5)}), "and the first guest, passed on by the host")
|
||||||
|
check(not notices[guest].any(func(notice: Dictionary) -> bool: return notice["type"] == "figure" and notice["player"] == 1), "nobody gets their own figure back")
|
||||||
|
_close([host, guest, other])
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_guest_with_the_key_of_a_player_in_the_world_is_turned_away() -> void:
|
||||||
|
_start_host()
|
||||||
|
var twin := _start_guest(host_key, "Host")
|
||||||
|
await _play([twin], 30)
|
||||||
|
check_eq(_first(twin, "refused").get("reason"), GwSession.REFUSED_ALREADY_PLAYING, "the reason is given")
|
||||||
|
check(_types(twin).has("host_lost"), "then the host closes the connection")
|
||||||
|
check(not twin.session.is_running(), "the guest never got a world")
|
||||||
|
_close([host, twin])
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_guest_that_quits_leaves_the_world() -> void:
|
||||||
|
_start_host()
|
||||||
|
var guest := _start_guest(guest_key, "Grace")
|
||||||
|
await _play([guest], 30)
|
||||||
|
check_eq(host.session.online_players(), PackedInt64Array([0, 1]), "two players online")
|
||||||
|
guest.link.close()
|
||||||
|
await _play([], 10)
|
||||||
|
check(_types(host).has("guest_left"), "the host sees the guest leave")
|
||||||
|
check_eq(host.session.online_players(), PackedInt64Array([0]), "only the host is online")
|
||||||
|
_close([host])
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_guest_learns_when_the_host_is_gone() -> void:
|
||||||
|
_start_host()
|
||||||
|
var guest := _start_guest(guest_key, "Grace")
|
||||||
|
await _play([guest], 30)
|
||||||
|
host.link.close()
|
||||||
|
await _play([guest], 10, false)
|
||||||
|
check(_types(guest).has("host_lost"), "the guest notices")
|
||||||
|
_close([guest])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c6tn270be6rbu
|
||||||
Reference in New Issue
Block a user