From 8f7ee31bd21dae37043fc315a6c96f8fa14e3fa7 Mon Sep 17 00:00:00 2001 From: Nomads_Reach <144523850+NomadsReach@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:49:33 -0400 Subject: [PATCH] Test last-known-good movement correction --- server/tests/test_networking_v2.py | 104 ++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/server/tests/test_networking_v2.py b/server/tests/test_networking_v2.py index 92174ea..8d1c19b 100644 --- a/server/tests/test_networking_v2.py +++ b/server/tests/test_networking_v2.py @@ -18,6 +18,16 @@ def recv_packet(sock: socket.socket, timeout: float = 2.0) -> dict: return json.loads(line.decode("utf-8")) +def recv_until(sock: socket.socket, packet_type: str, timeout: float = 2.0) -> dict: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + remaining = max(0.01, deadline - time.monotonic()) + packet = recv_packet(sock, timeout=remaining) + if packet.get("type") == packet_type: + return packet + raise AssertionError(f"did not receive packet type {packet_type}") + + def send_packet(sock: socket.socket, packet: dict) -> None: sock.sendall(json.dumps(packet, separators=(",", ":")).encode() + b"\n") @@ -29,10 +39,16 @@ def connect(server: FalloutTogetherServer) -> tuple[socket.socket, dict]: def hello(sock: socket.socket) -> dict: send_packet(sock, {"type": "hello", "protocolVersion": PROTOCOL_VERSION}) - return recv_packet(sock) + return recv_until(sock, "sessionReady") -def transform(cell: str, x: float, y: float, world: str = "0000003C") -> dict: +def transform( + cell: str, + x: float, + y: float, + world: str = "0000003C", + movement_type: str = "normal", +) -> dict: return { "type": "transform", "x": x, @@ -41,6 +57,7 @@ def transform(cell: str, x: float, y: float, world: str = "0000003C") -> dict: "angleZ": 0.0, "cellId": cell, "worldspaceId": world, + "movementType": movement_type, } @@ -139,6 +156,89 @@ def test_transform_interest_filters_distant_peer(): server.stop() +def test_rejected_normal_teleport_is_corrected_and_never_relayed(): + server = start_server() + a, a_welcome = connect(server) + hello(a) + b, _ = connect(server) + hello(b) + + send_packet(b, transform("00000010", 64.0, 0.0)) + recv_until(a, "transform") + send_packet(a, transform("00000010", 0.0, 0.0)) + first_relay = recv_until(b, "transform") + assert first_relay["x"] == 0.0 + + send_packet(a, transform("00000010", 100000.0, 0.0)) + correction = recv_until(a, "positionCorrection") + assert correction["x"] == 0.0 + assert correction["cellId"] == "00000010" + + b.settimeout(0.2) + with pytest.raises(socket.timeout): + b.recv(4096) + + client = server._find_client_by_player_id(a_welcome["playerId"]) + assert client is not None + assert client.last_transform is not None + assert client.last_transform["x"] == 0.0 + stats = server.get_stats() + assert stats["movementPacketsRejected"] >= 1 + assert stats["movementCorrectionsSent"] >= 1 + + a.close() + b.close() + server.stop() + + +def test_scope_change_requires_explicit_transition(): + server = start_server() + sock, welcome = connect(server) + hello(sock) + + send_packet(sock, transform("00000010", 0.0, 0.0)) + time.sleep(0.05) + send_packet(sock, transform("00000011", 10.0, 0.0)) + correction = recv_until(sock, "positionCorrection") + assert correction["cellId"] == "00000010" + + client = server._find_client_by_player_id(welcome["playerId"]) + assert client is not None + assert client.last_transform is not None + assert client.last_transform["cellId"] == "00000010" + + send_packet(sock, transform("00000011", 10.0, 0.0, movement_type="cell_change")) + deadline = time.monotonic() + 1.0 + while time.monotonic() < deadline: + if client.last_transform is not None and client.last_transform.get("cellId") == "00000011": + break + time.sleep(0.01) + assert client.last_transform is not None + assert client.last_transform["cellId"] == "00000011" + + sock.close() + server.stop() + + +def test_zero_elapsed_burst_cannot_bypass_movement_envelope(): + server = start_server() + sock, welcome = connect(server) + hello(sock) + + send_packet(sock, transform("00000010", 0.0, 0.0)) + send_packet(sock, transform("00000010", 50000.0, 0.0)) + correction = recv_until(sock, "positionCorrection") + assert correction["x"] == 0.0 + + client = server._find_client_by_player_id(welcome["playerId"]) + assert client is not None + assert client.last_transform is not None + assert client.last_transform["x"] == 0.0 + + sock.close() + server.stop() + + def test_oversized_unterminated_packet_closes_session(): server = start_server() sock, _ = connect(server)