96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
import socket
|
|
import threading
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from packet_codec import EncodedPacket
|
|
from tcp_transport import send_message
|
|
|
|
|
|
@dataclass
|
|
class ClientSession:
|
|
connection: Any
|
|
address: tuple[str, int]
|
|
player_id: int
|
|
connected_at: float
|
|
last_packet_at: float | None = None
|
|
last_transform: dict[str, Any] | None = None
|
|
last_transform_monotonic: float | None = None
|
|
packets_received: int = 0
|
|
packets_sent: int = 0
|
|
packets_broadcast: int = 0
|
|
gameplay_active: bool = False
|
|
protocol_version: int = 0
|
|
_lock: Any = field(repr=False, compare=False, default_factory=threading.RLock)
|
|
_send_lock: Any = field(repr=False, compare=False, default_factory=threading.Lock)
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
return f"{self.address[0]}:{self.address[1]}"
|
|
|
|
def record_received(self, received_at: float) -> int:
|
|
with self._lock:
|
|
self.last_packet_at = received_at
|
|
self.packets_received += 1
|
|
return self.packets_received
|
|
|
|
def record_transform(self, packet: dict[str, Any], accepted_monotonic: float | None = None) -> None:
|
|
with self._lock:
|
|
self.last_transform = dict(packet)
|
|
self.last_transform_monotonic = accepted_monotonic
|
|
|
|
def get_last_transform_anchor(self) -> tuple[dict[str, Any] | None, float | None]:
|
|
with self._lock:
|
|
transform = dict(self.last_transform) if self.last_transform is not None else None
|
|
return transform, self.last_transform_monotonic
|
|
|
|
def record_sent(self, *, broadcast: bool = False) -> None:
|
|
with self._lock:
|
|
self.packets_sent += 1
|
|
if broadcast:
|
|
self.packets_broadcast += 1
|
|
|
|
def mark_gameplay_active(self, protocol_version: int) -> bool:
|
|
with self._lock:
|
|
if self.gameplay_active:
|
|
return False
|
|
self.gameplay_active = True
|
|
self.protocol_version = int(protocol_version)
|
|
return True
|
|
|
|
def send_bytes(self, payload: bytes) -> None:
|
|
with self._send_lock:
|
|
self.connection.sendall(payload)
|
|
|
|
def send_packet(self, encoded: EncodedPacket) -> None:
|
|
"""Send one already-serialized protocol message through the active transport.
|
|
|
|
TCP compatibility adds its newline delimiter here. Message-oriented
|
|
transports can expose ``send_encoded`` and receive the raw JSON payload
|
|
plus its delivery policy without TCP framing leaking into the protocol.
|
|
"""
|
|
with self._send_lock:
|
|
send_encoded = getattr(self.connection, "send_encoded", None)
|
|
if callable(send_encoded):
|
|
send_encoded(encoded)
|
|
return
|
|
send_message(self.connection, encoded.payload)
|
|
|
|
def to_snapshot(self) -> dict[str, Any]:
|
|
with self._lock:
|
|
return {
|
|
"playerId": self.player_id,
|
|
"address": self.address[0],
|
|
"port": self.address[1],
|
|
"connectedAt": self.connected_at,
|
|
"lastPacketAt": self.last_packet_at,
|
|
"lastTransform": dict(self.last_transform) if self.last_transform is not None else None,
|
|
"packetsReceived": self.packets_received,
|
|
"packetsSent": self.packets_sent,
|
|
"packetsBroadcast": self.packets_broadcast,
|
|
"gameplayActive": self.gameplay_active,
|
|
"protocolVersion": self.protocol_version,
|
|
}
|