from __future__ import annotations import json from collections import deque import pytest from gns_gameplay_adapter import GnsConnectionAdapter, GnsGameplayAdapter from gns_snapshot_envelope import decode_snapshot, encode_snapshot from gns_transport import EventType, GnsEvent, RemoteEndpoint, SendResult from packet_codec import encode_packet from server_core import FalloutTogetherServer, PROTOCOL_VERSION from transport_policy import Delivery class FakeGnsTransport: def __init__(self): self.events = deque() self.endpoints = {} self.sent = [] self.send_results = deque() self.disconnected = [] self.incoming_sequences = {} self.closed = False def poll(self): return self.events.popleft() if self.events else None def remote_endpoint(self, connection_id): return self.endpoints.get(connection_id) def send_encoded(self, connection_id, encoded): self.sent.append((connection_id, encoded)) return self.send_results.popleft() if self.send_results else SendResult.SENT def send_packet(self, connection_id, packet): return self.send_encoded(connection_id, encode_packet(packet)) def disconnect(self, connection_id, *, reason=0, debug=""): self.disconnected.append((connection_id, reason, debug)) return True def close(self): self.closed = True def decoded_sent_packet(encoded): envelope = decode_snapshot(encoded.payload) payload = envelope.payload if envelope is not None else encoded.payload return json.loads(payload) def packet_types_sent(transport: FakeGnsTransport, connection_id: int) -> list[str]: return [ decoded_sent_packet(encoded)["type"] for target, encoded in transport.sent if target == connection_id ] def queue_connect(transport: FakeGnsTransport, connection_id: int, port: int = 50000): transport.endpoints[connection_id] = RemoteEndpoint("127.0.0.1", port) transport.events.append(GnsEvent(EventType.CONNECTED, connection_id)) def queue_packet( transport: FakeGnsTransport, connection_id: int, packet: dict, *, sequence: int | None = None, raw_snapshot: bool = False, ): payload = json.dumps(packet, separators=(",", ":")).encode() packet_type = packet.get("type") if packet_type in {"transform", "npcState"} and not raw_snapshot: key = (connection_id, packet_type) if sequence is None: sequence = transport.incoming_sequences.get(key, 0) + 1 transport.incoming_sequences[key] = sequence payload = encode_snapshot(packet_type, payload, sequence) transport.events.append(GnsEvent(EventType.MESSAGE, connection_id, payload)) def complete_handshake(adapter: GnsGameplayAdapter, transport: FakeGnsTransport, connection_id: int): queue_connect(transport, connection_id, 50000 + connection_id) assert adapter.pump_once() == 1 assert "welcome" in packet_types_sent(transport, connection_id) queue_packet(transport, connection_id, {"type": "hello", "protocolVersion": PROTOCOL_VERSION}) assert adapter.pump_once() == 1 assert "sessionReady" in packet_types_sent(transport, connection_id) def transform_packet(x: float) -> dict: return { "type": "transform", "x": x, "y": 0.0, "z": 0.0, "angleZ": 0.0, "cellId": "00000010", "worldspaceId": "0000003C", "movementType": "normal", } def test_gns_adapter_reuses_v2_admission_and_server_owned_identity(): server = FalloutTogetherServer(host="127.0.0.1", port=0) transport = FakeGnsTransport() adapter = GnsGameplayAdapter(server, transport) complete_handshake(adapter, transport, 101) client = adapter._client_for_connection(101) assert client is not None assert client.gameplay_active assert client.protocol_version == PROTOCOL_VERSION packet = transform_packet(1.0) packet["playerId"] = 999999 queue_packet(transport, 101, packet) adapter.pump_once() assert client.last_transform is not None assert client.last_transform["playerId"] == client.player_id assert client.last_transform["playerId"] != 999999 def test_gns_adapter_preserves_reliable_and_snapshot_delivery_policy(): server = FalloutTogetherServer(host="127.0.0.1", port=0) transport = FakeGnsTransport() adapter = GnsGameplayAdapter(server, transport) complete_handshake(adapter, transport, 201) complete_handshake(adapter, transport, 202) transport.sent.clear() queue_packet(transport, 202, transform_packet(10.0)) adapter.pump_once() queue_packet(transport, 201, transform_packet(0.0)) adapter.pump_once() transform_relays = [ encoded for target, encoded in transport.sent if target == 202 and decoded_sent_packet(encoded).get("type") == "transform" ] assert transform_relays transform_relay = transform_relays[-1] assert transform_relay.delivery is Delivery.UNRELIABLE_SEQUENCED transform_envelope = decode_snapshot(transform_relay.payload) assert transform_envelope is not None assert transform_envelope.packet_type == "transform" assert transform_envelope.sequence > 0 queue_packet( transport, 201, {"type": "playerState", "characterName": "Nomad", "actionEvents": []}, ) adapter.pump_once() player_state_relays = [ encoded for target, encoded in transport.sent if target == 202 and decoded_sent_packet(encoded).get("type") == "playerState" ] assert player_state_relays player_state_relay = player_state_relays[-1] assert player_state_relay.delivery is Delivery.RELIABLE_ORDERED assert decode_snapshot(player_state_relay.payload) is None def test_gns_snapshot_reordering_and_duplicates_never_roll_state_backward(): server = FalloutTogetherServer(host="127.0.0.1", port=0) transport = FakeGnsTransport() adapter = GnsGameplayAdapter(server, transport) complete_handshake(adapter, transport, 501) client = adapter._client_for_connection(501) assert client is not None queue_packet(transport, 501, transform_packet(1.0), sequence=1) adapter.pump_once() assert client.last_transform is not None assert client.last_transform["x"] == 1.0 queue_packet(transport, 501, transform_packet(3.0), sequence=3) adapter.pump_once() assert client.last_transform["x"] == 3.0 rejected_before = server.get_stats()["packetsRejected"] queue_packet(transport, 501, transform_packet(2.0), sequence=2) adapter.pump_once() assert client.last_transform["x"] == 3.0 assert server.get_stats()["packetsRejected"] == rejected_before + 1 queue_packet(transport, 501, transform_packet(9.0), sequence=3) adapter.pump_once() assert client.last_transform["x"] == 3.0 assert server.get_stats()["packetsRejected"] == rejected_before + 2 def test_gns_raw_unsequenced_snapshot_is_rejected_before_state_mutation(): server = FalloutTogetherServer(host="127.0.0.1", port=0) transport = FakeGnsTransport() adapter = GnsGameplayAdapter(server, transport) complete_handshake(adapter, transport, 601) client = adapter._client_for_connection(601) assert client is not None assert client.last_transform is None rejected_before = server.get_stats()["packetsRejected"] queue_packet(transport, 601, transform_packet(1.0), raw_snapshot=True) adapter.pump_once() assert client.last_transform is None assert server.get_stats()["packetsRejected"] == rejected_before + 1 def test_connection_adapter_may_drop_snapshots_but_never_reliable_messages_silently(): transport = FakeGnsTransport() connection = GnsConnectionAdapter(transport, 250) transport.send_results.append(SendResult.DROPPED) connection.sendall(b'{"type":"transform","x":1}\n') transform_wire = transport.sent[-1][1] assert decode_snapshot(transform_wire.payload) is not None transport.send_results.append(SendResult.BACKPRESSURE) connection.sendall(b'{"type":"npcState","npcs":[]}\n') npc_wire = transport.sent[-1][1] assert decode_snapshot(npc_wire.payload) is not None transport.send_results.append(SendResult.DROPPED) with pytest.raises(OSError): connection.sendall(b'{"type":"playerState","characterName":"Nomad"}\n') transport.send_results.append(SendResult.BACKPRESSURE) with pytest.raises(OSError): connection.sendall(b'{"type":"combatHit","sequence":1}\n') def test_gns_adapter_enforces_existing_ban_policy(tmp_path): server = FalloutTogetherServer(host="127.0.0.1", port=0, bans_path=str(tmp_path / "bans.json")) server.ban_ip("127.0.0.1", "test ban") transport = FakeGnsTransport() adapter = GnsGameplayAdapter(server, transport) queue_connect(transport, 301) adapter.pump_once() assert adapter._client_for_connection(301) is None assert transport.disconnected session_ended = [ decoded_sent_packet(encoded) for target, encoded in transport.sent if target == 301 and decoded_sent_packet(encoded).get("type") == "sessionEnded" ] assert session_ended assert session_ended[-1]["code"] == "banned" assert server.get_stats()["bannedConnectionsRejected"] == 1 def test_gns_adapter_ends_session_on_native_oversize_event(): server = FalloutTogetherServer(host="127.0.0.1", port=0) transport = FakeGnsTransport() adapter = GnsGameplayAdapter(server, transport) complete_handshake(adapter, transport, 401) transport.events.append( GnsEvent(EventType.OVERSIZE_MESSAGE, 401, reason=0, debug="too large") ) adapter.pump_once() assert adapter._client_for_connection(401) is None assert transport.disconnected ended_packets = [ decoded_sent_packet(encoded) for target, encoded in transport.sent if target == 401 and decoded_sent_packet(encoded).get("type") == "sessionEnded" ] assert ended_packets assert ended_packets[-1]["code"] == "packet_too_large"