diff --git a/server/tests/test_networking_v2_load.py b/server/tests/test_networking_v2_load.py new file mode 100644 index 0000000..18cf056 --- /dev/null +++ b/server/tests/test_networking_v2_load.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +import json +import socket +import time + +from server_core import PROTOCOL_VERSION, FalloutTogetherServer + + +_RECV_BUFFERS: dict[socket.socket, bytes] = {} + + +def recv_packet(sock: socket.socket, timeout: float = 2.0) -> dict: + sock.settimeout(timeout) + data = _RECV_BUFFERS.get(sock, b"") + while b"\n" not in data: + chunk = sock.recv(4096) + if not chunk: + raise ConnectionError("socket closed before a complete packet was received") + data += chunk + line, remainder = data.split(b"\n", 1) + _RECV_BUFFERS[sock] = remainder + return json.loads(line.decode("utf-8")) + + +def recv_until(sock: socket.socket, predicate, timeout: float = 2.0) -> dict: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + packet = recv_packet(sock, timeout=max(0.01, deadline - time.monotonic())) + except socket.timeout: + break + if predicate(packet): + return packet + raise AssertionError("did not receive matching packet") + + +def send_packet(sock: socket.socket, packet: dict) -> None: + sock.sendall(json.dumps(packet, separators=(",", ":")).encode() + b"\n") + + +def connect_v2(server: FalloutTogetherServer) -> tuple[socket.socket, int]: + sock = socket.create_connection(("127.0.0.1", server.port), timeout=2.0) + welcome = recv_until(sock, lambda packet: packet.get("type") == "welcome") + send_packet(sock, {"type": "hello", "protocolVersion": PROTOCOL_VERSION}) + ready = recv_until(sock, lambda packet: packet.get("type") == "sessionReady") + assert ready["playerId"] == welcome["playerId"] + return sock, int(welcome["playerId"]) + + +def transform(cell: str, x: float, y: float = 0.0) -> dict: + return { + "type": "transform", + "x": x, + "y": y, + "z": 0.0, + "angleZ": 0.0, + "cellId": cell, + "worldspaceId": "0000003C", + "movementType": "normal", + } + + +def start_server() -> FalloutTogetherServer: + server = FalloutTogetherServer(host="127.0.0.1", port=0, max_players=24) + server.start() + deadline = time.monotonic() + 2.0 + while not server.is_running() and time.monotonic() < deadline: + time.sleep(0.01) + return server + + +def test_sixteen_clients_do_not_cross_broadcast_distant_cell_transforms(): + server = start_server() + clients: list[tuple[socket.socket, int]] = [] + try: + clients = [connect_v2(server) for _ in range(16)] + assert server.get_stats()["connectedClients"] == 16 + + near = clients[:8] + far = clients[8:] + for index, (sock, _player_id) in enumerate(near): + send_packet(sock, transform("00000010", float(index * 32))) + for index, (sock, _player_id) in enumerate(far): + send_packet(sock, transform("00000020", 30000.0 + float(index * 32))) + + deadline = time.monotonic() + 2.0 + while time.monotonic() < deadline: + sessions = [server._find_client_by_player_id(player_id) for _sock, player_id in clients] + if all(session is not None and session.last_transform is not None for session in sessions): + break + time.sleep(0.01) + assert all( + session is not None and session.last_transform is not None + for session in (server._find_client_by_player_id(player_id) for _sock, player_id in clients) + ) + + sender_sock, sender_id = near[0] + near_peer_sock, _near_peer_id = near[1] + far_peer_sock, _far_peer_id = far[0] + marker_x = 128.0 + send_packet(sender_sock, transform("00000010", marker_x)) + + relayed = recv_until( + near_peer_sock, + lambda packet: packet.get("type") == "transform" + and packet.get("playerId") == sender_id + and packet.get("x") == marker_x, + ) + assert relayed["cellId"] == "00000010" + + try: + recv_until( + far_peer_sock, + lambda packet: packet.get("type") == "transform" + and packet.get("playerId") == sender_id + and packet.get("x") == marker_x, + timeout=0.4, + ) + except AssertionError: + pass + else: + raise AssertionError("distant cell received a transform that should have been interest-filtered") + + stats = server.get_stats() + assert stats["connectedClients"] == 16 + assert stats["transformPacketsInterestFiltered"] >= len(far) + finally: + for sock, _player_id in clients: + _RECV_BUFFERS.pop(sock, None) + try: + sock.close() + except OSError: + pass + server.stop() diff --git a/server/tests/test_snapshot_sequence.py b/server/tests/test_snapshot_sequence.py index 0a1b94e..ad555c1 100644 --- a/server/tests/test_snapshot_sequence.py +++ b/server/tests/test_snapshot_sequence.py @@ -36,6 +36,14 @@ def test_counter_and_window_enforce_latest_wins(): assert not window.accept(MAX_SEQUENCE) +def test_loss_and_reordering_do_not_stall_newer_snapshots(): + window = SequenceWindow() + arrivals = (1, 3, 2, 6, 4, 5, 6, 8) + accepted = [sequence for sequence in arrivals if window.accept(sequence)] + assert accepted == [1, 3, 6, 8] + assert window.last_accepted == 8 + + def test_invalid_sequence_inputs_are_rejected(): assert not is_newer(-1, 0) assert not is_newer(1, -1)