Files
Commonwealth-Online-Server/server/tests/test_gns_gameplay_adapter.py
T

282 lines
10 KiB
Python

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 PROTOCOL_VERSION
from transport_policy import Delivery
from transport_server import TransportAwareFalloutTogetherServer
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 = TransportAwareFalloutTogetherServer(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 = TransportAwareFalloutTogetherServer(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 = TransportAwareFalloutTogetherServer(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 = TransportAwareFalloutTogetherServer(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.send_encoded(encode_packet({"type": "transform", "x": 1}))
transform_wire = transport.sent[-1][1]
assert decode_snapshot(transform_wire.payload) is not None
transport.send_results.append(SendResult.BACKPRESSURE)
connection.send_encoded(encode_packet({"type": "npcState", "npcs": []}))
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.send_encoded(encode_packet({"type": "playerState", "characterName": "Nomad"}))
transport.send_results.append(SendResult.BACKPRESSURE)
with pytest.raises(OSError):
connection.send_encoded(encode_packet({"type": "combatHit", "sequence": 1}))
def test_gns_adapter_enforces_existing_ban_policy(tmp_path):
server = TransportAwareFalloutTogetherServer(
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 = TransportAwareFalloutTogetherServer(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"