Sync from GitHub main #1

Open
nomad wants to merge 145 commits from sync/from-github into main
Showing only changes of commit 39750a72ee - Show all commits
+65 -3
View File
@@ -6,8 +6,10 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from client_session import ClientSession from client_session import ClientSession
from gns_snapshot_envelope import SnapshotEnvelopeError, decode_snapshot, encode_snapshot
from gns_transport import EventType, GnsEvent, GnsServerTransport, GnsTransportError, SendResult from gns_transport import EventType, GnsEvent, GnsServerTransport, GnsTransportError, SendResult
from packet_codec import EncodedPacket, PacketCodecError, decode_packet from packet_codec import EncodedPacket, PacketCodecError, decode_packet
from snapshot_sequence import SequenceCounter, SequenceWindow
from transport_policy import delivery_for_packet_type, is_snapshot_packet from transport_policy import delivery_for_packet_type, is_snapshot_packet
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -26,6 +28,10 @@ class GnsConnectionAdapter:
self.connection_id = connection_id self.connection_id = connection_id
self._closed = False self._closed = False
self._lock = threading.Lock() self._lock = threading.Lock()
self._snapshot_counters = {
"transform": SequenceCounter(),
"npcState": SequenceCounter(),
}
def fileno(self) -> int: def fileno(self) -> int:
with self._lock: with self._lock:
@@ -48,7 +54,15 @@ class GnsConnectionAdapter:
except PacketCodecError as error: except PacketCodecError as error:
raise OSError(f"invalid outbound GNS packet: {error}") from error raise OSError(f"invalid outbound GNS packet: {error}") from error
packet_type = packet["type"] packet_type = packet["type"]
encoded = EncodedPacket(packet_type, payload, delivery_for_packet_type(packet_type)) wire_payload = payload
if is_snapshot_packet(packet_type):
with self._lock:
sequence = self._snapshot_counters[packet_type].advance()
try:
wire_payload = encode_snapshot(packet_type, payload, sequence)
except SnapshotEnvelopeError as error:
raise ValueError(str(error)) from error
encoded = EncodedPacket(packet_type, wire_payload, delivery_for_packet_type(packet_type))
result = self.transport.send_encoded(self.connection_id, encoded) result = self.transport.send_encoded(self.connection_id, encoded)
if result is SendResult.SENT: if result is SendResult.SENT:
return return
@@ -84,6 +98,7 @@ class GnsGameplayAdapter:
self.transport = transport self.transport = transport
self._clients: dict[int, ClientSession] = {} self._clients: dict[int, ClientSession] = {}
self._connected_monotonic: dict[int, float] = {} self._connected_monotonic: dict[int, float] = {}
self._incoming_snapshot_windows: dict[tuple[int, str], SequenceWindow] = {}
self._lock = threading.RLock() self._lock = threading.RLock()
self._thread: threading.Thread | None = None self._thread: threading.Thread | None = None
self._thread_running = False self._thread_running = False
@@ -115,6 +130,7 @@ class GnsGameplayAdapter:
"npc-authority-epoch-v1", "npc-authority-epoch-v1",
"player-state-v1", "player-state-v1",
"gns-message-transport-v1", "gns-message-transport-v1",
"gns-snapshot-sequence-v1",
], ],
} }
@@ -167,15 +183,57 @@ class GnsGameplayAdapter:
self.server._disconnect_client(client) self.server._disconnect_client(client)
self._purge_closed_clients() self._purge_closed_clients()
def _account_transport_reject(self, client: ClientSession, reason: str, *, warning: bool = False) -> None:
if not self.server._allow_packet(client):
return
client.record_received(time.time())
with self.server._lock:
self.server._stats["packetsReceived"] += 1
self.server._reject_packet(client, reason, warning=warning)
def _handle_message(self, event: GnsEvent) -> None: def _handle_message(self, event: GnsEvent) -> None:
client = self._client_for_connection(event.connection_id) client = self._client_for_connection(event.connection_id)
if client is None: if client is None:
self.transport.disconnect(event.connection_id, debug="Message before GNS admission") self.transport.disconnect(event.connection_id, debug="Message before GNS admission")
return return
payload = event.payload
try: try:
line = event.payload.decode("utf-8", errors="strict") envelope = decode_snapshot(payload)
except SnapshotEnvelopeError as error:
self._account_transport_reject(client, f"Malformed GNS snapshot envelope: {error}")
return
if envelope is not None:
window_key = (event.connection_id, envelope.packet_type)
with self._lock:
window = self._incoming_snapshot_windows.setdefault(window_key, SequenceWindow())
accepted = window.accept(envelope.sequence)
if not accepted:
self._account_transport_reject(client, "Stale or duplicate GNS snapshot sequence", warning=False)
return
try:
packet = decode_packet(envelope.payload)
except PacketCodecError as error:
self._account_transport_reject(client, f"Invalid GNS snapshot payload: {error}")
return
if packet["type"] != envelope.packet_type:
self._account_transport_reject(client, "GNS snapshot envelope family does not match packet type")
return
payload = envelope.payload
else:
try:
packet = decode_packet(payload)
except PacketCodecError:
packet = None
if packet is not None and is_snapshot_packet(packet["type"]):
self._account_transport_reject(client, "GNS snapshot missing required sequence envelope")
return
try:
line = payload.decode("utf-8", errors="strict")
except UnicodeDecodeError: except UnicodeDecodeError:
self.server._reject_packet(client, "Packet is not valid UTF-8") self._account_transport_reject(client, "Packet is not valid UTF-8")
return return
self.server._handle_line(client, line) self.server._handle_line(client, line)
self._purge_closed_clients() self._purge_closed_clients()
@@ -211,6 +269,9 @@ class GnsGameplayAdapter:
with self._lock: with self._lock:
self._clients.pop(connection_id, None) self._clients.pop(connection_id, None)
self._connected_monotonic.pop(connection_id, None) self._connected_monotonic.pop(connection_id, None)
stale_windows = [key for key in self._incoming_snapshot_windows if key[0] == connection_id]
for key in stale_windows:
self._incoming_snapshot_windows.pop(key, None)
def _purge_closed_clients(self) -> None: def _purge_closed_clients(self) -> None:
with self._lock: with self._lock:
@@ -312,4 +373,5 @@ class GnsGameplayAdapter:
with self._lock: with self._lock:
self._clients.clear() self._clients.clear()
self._connected_monotonic.clear() self._connected_monotonic.clear()
self._incoming_snapshot_windows.clear()
self.transport.close() self.transport.close()