diff --git a/server/tests/test_networking_v2.py b/server/tests/test_networking_v2.py new file mode 100644 index 0000000..92174ea --- /dev/null +++ b/server/tests/test_networking_v2.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +import json +import socket +import time + +import pytest + +from server_core import PROTOCOL_VERSION, FalloutTogetherServer, states_share_interest + + +def recv_packet(sock: socket.socket, timeout: float = 2.0) -> dict: + sock.settimeout(timeout) + data = b"" + while b"\n" not in data: + data += sock.recv(4096) + line, _ = data.split(b"\n", 1) + return json.loads(line.decode("utf-8")) + + +def send_packet(sock: socket.socket, packet: dict) -> None: + sock.sendall(json.dumps(packet, separators=(",", ":")).encode() + b"\n") + + +def connect(server: FalloutTogetherServer) -> tuple[socket.socket, dict]: + sock = socket.create_connection(("127.0.0.1", server.port), timeout=2.0) + return sock, recv_packet(sock) + + +def hello(sock: socket.socket) -> dict: + send_packet(sock, {"type": "hello", "protocolVersion": PROTOCOL_VERSION}) + return recv_packet(sock) + + +def transform(cell: str, x: float, y: float, world: str = "0000003C") -> dict: + return { + "type": "transform", + "x": x, + "y": y, + "z": 0.0, + "angleZ": 0.0, + "cellId": cell, + "worldspaceId": world, + } + + +def start_server(max_players: int = 16) -> FalloutTogetherServer: + server = FalloutTogetherServer(host="127.0.0.1", port=0, max_players=max_players) + server.start() + deadline = time.time() + 2 + while not server.is_running() and time.time() < deadline: + time.sleep(0.01) + return server + + +def test_interest_exact_cell_and_exterior_radius(): + a = transform("00000001", 0, 0) + same = transform("00000001", 50000, 50000) + near = transform("00000002", 1000, 1000) + far = transform("00000002", 20000, 20000) + other_world = transform("00000002", 1000, 1000, world="0000003D") + assert states_share_interest(a, same) + assert states_share_interest(a, near) + assert not states_share_interest(a, far) + assert not states_share_interest(a, other_world) + + +def test_idle_transport_does_not_take_world_authority(): + server = start_server() + idle, _ = connect(server) + active, active_welcome = connect(server) + ready = hello(active) + assert ready["playerId"] == active_welcome["playerId"] + stats = server.get_stats() + assert stats["connectedClients"] == 1 + assert stats["pendingConnections"] == 1 + assert server._world_state_host_player_id == active_welcome["playerId"] + idle.close() + active.close() + server.stop() + + +def test_max_players_applies_to_activated_sessions_not_probes(): + server = start_server(max_players=1) + first, _ = connect(server) + hello(first) + second, _ = connect(server) + send_packet(second, {"type": "hello", "protocolVersion": PROTOCOL_VERSION}) + ended = recv_packet(second) + assert ended["type"] == "sessionEnded" + assert ended["code"] == "server_full" + first.close() + second.close() + server.stop() + + +def test_protocol_mismatch_is_rejected(): + server = start_server() + sock, _ = connect(server) + send_packet(sock, {"type": "hello", "protocolVersion": PROTOCOL_VERSION + 1}) + ended = recv_packet(sock) + assert ended["type"] == "sessionEnded" + assert ended["code"] == "protocol_mismatch" + sock.close() + server.stop() + + +def test_nan_transform_is_rejected_and_not_cached(): + server = start_server() + sock, welcome = connect(server) + hello(sock) + sock.sendall( + b'{"type":"transform","x":NaN,"y":0,"z":0,"angleZ":0,"cellId":"00000001"}\n' + ) + time.sleep(0.05) + client = server._find_client_by_player_id(welcome["playerId"]) + assert client is not None + assert client.last_transform is None + assert server.get_stats()["packetsRejected"] >= 1 + sock.close() + server.stop() + + +def test_transform_interest_filters_distant_peer(): + server = start_server() + a, _ = connect(server) + hello(a) + b, _ = connect(server) + hello(b) + send_packet(b, transform("00000020", 25000, 25000)) + time.sleep(0.05) + send_packet(a, transform("00000010", 0, 0)) + b.settimeout(0.2) + with pytest.raises(socket.timeout): + b.recv(4096) + assert server.get_stats()["transformPacketsInterestFiltered"] >= 1 + a.close() + b.close() + server.stop() + + +def test_oversized_unterminated_packet_closes_session(): + server = start_server() + sock, _ = connect(server) + sock.sendall(b"x" * (64 * 1024 + 1)) + ended = recv_packet(sock) + assert ended["type"] == "sessionEnded" + assert ended["code"] == "packet_too_large" + sock.close() + server.stop()