Server scaffold: protocol + config + transport abstraction + tick loop skeleton
- Protocol: PacketType, TLV Body types, QUIC varint, header, input_broadcast and config_update builders, 2-bit body bitpacking helper - Config/model: live-config ServerConfig, basic GameState/Snake/Session - Transport: InMemoryTransport placeholder and QUIC server stub - Server: asyncio tick loop, periodic config_update broadcast, immediate input_broadcast relay; main entry and run.py
This commit is contained in:
193
server/protocol.py
Normal file
193
server/protocol.py
Normal file
@@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Iterable, List, Sequence, Tuple
|
||||
|
||||
|
||||
class PacketType(IntEnum):
|
||||
JOIN = 0
|
||||
JOIN_ACK = 1
|
||||
JOIN_DENY = 2
|
||||
INPUT = 3
|
||||
INPUT_BROADCAST = 4
|
||||
STATE_DELTA = 5
|
||||
STATE_FULL = 6
|
||||
PART = 7
|
||||
CONFIG_UPDATE = 8
|
||||
PING = 9
|
||||
PONG = 10
|
||||
ERROR = 11
|
||||
|
||||
|
||||
class BodyTLV(IntEnum):
|
||||
BODY_2BIT = 0x00
|
||||
BODY_RLE = 0x01
|
||||
BODY_2BIT_CHUNK = 0x10
|
||||
BODY_RLE_CHUNK = 0x11
|
||||
|
||||
|
||||
class Direction(IntEnum):
|
||||
UP = 0
|
||||
RIGHT = 1
|
||||
DOWN = 2
|
||||
LEFT = 3
|
||||
|
||||
|
||||
def quic_varint_encode(value: int) -> bytes:
|
||||
"""Encode QUIC varint (RFC 9000)."""
|
||||
if value < 0:
|
||||
raise ValueError("varint must be non-negative")
|
||||
if value <= 0x3F: # 6 bits, 1 byte, 00xx xxxx
|
||||
return bytes([value & 0x3F])
|
||||
if value <= 0x3FFF: # 14 bits, 2 bytes, 01xx xxxx
|
||||
v = 0x4000 | value
|
||||
return v.to_bytes(2, "big")
|
||||
if value <= 0x3FFFFFFF: # 30 bits, 4 bytes, 10xx xxxx
|
||||
v = 0x80000000 | value
|
||||
return v.to_bytes(4, "big")
|
||||
if value <= 0x3FFFFFFFFFFFFFFF: # 62 bits, 8 bytes, 11xx xxxx
|
||||
v = 0xC000000000000000 | value
|
||||
return v.to_bytes(8, "big")
|
||||
raise ValueError("varint too large")
|
||||
|
||||
|
||||
def quic_varint_decode(buf: bytes, offset: int = 0) -> Tuple[int, int]:
|
||||
"""Decode QUIC varint starting at offset. Returns (value, next_offset)."""
|
||||
first = buf[offset]
|
||||
prefix = first >> 6
|
||||
if prefix == 0:
|
||||
return (first & 0x3F, offset + 1)
|
||||
if prefix == 1:
|
||||
v = int.from_bytes(buf[offset : offset + 2], "big") & 0x3FFF
|
||||
return (v, offset + 2)
|
||||
if prefix == 2:
|
||||
v = int.from_bytes(buf[offset : offset + 4], "big") & 0x3FFFFFFF
|
||||
return (v, offset + 4)
|
||||
v = int.from_bytes(buf[offset : offset + 8], "big") & 0x3FFFFFFFFFFFFFFF
|
||||
return (v, offset + 8)
|
||||
|
||||
|
||||
def pack_header(version: int, ptype: PacketType, flags: int, seq: int, tick: int | None) -> bytes:
|
||||
"""Pack common header fields.
|
||||
|
||||
Layout:
|
||||
- ver: u8
|
||||
- type: u8
|
||||
- flags: u8
|
||||
- seq: u16 (network order)
|
||||
- tick: optional u16
|
||||
"""
|
||||
if not (0 <= version <= 255):
|
||||
raise ValueError("version out of range")
|
||||
if not (0 <= flags <= 255):
|
||||
raise ValueError("flags out of range")
|
||||
if not (0 <= seq <= 0xFFFF):
|
||||
raise ValueError("seq out of range")
|
||||
parts = bytearray()
|
||||
parts.append(version & 0xFF)
|
||||
parts.append(int(ptype) & 0xFF)
|
||||
parts.append(flags & 0xFF)
|
||||
parts.extend(seq.to_bytes(2, "big"))
|
||||
if tick is not None:
|
||||
if not (0 <= tick <= 0xFFFF):
|
||||
raise ValueError("tick out of range")
|
||||
parts.extend(tick.to_bytes(2, "big"))
|
||||
return bytes(parts)
|
||||
|
||||
|
||||
def unpack_header(buf: bytes, expect_tick: bool) -> Tuple[int, PacketType, int, int, int | None, int]:
|
||||
"""Unpack header; returns (ver, type, flags, seq, tick, next_offset)."""
|
||||
if len(buf) < 5:
|
||||
raise ValueError("buffer too small for header")
|
||||
ver = buf[0]
|
||||
ptype = PacketType(buf[1])
|
||||
flags = buf[2]
|
||||
seq = int.from_bytes(buf[3:5], "big")
|
||||
off = 5
|
||||
tick = None
|
||||
if expect_tick:
|
||||
if len(buf) < 7:
|
||||
raise ValueError("buffer too small for tick")
|
||||
tick = int.from_bytes(buf[5:7], "big")
|
||||
off = 7
|
||||
return ver, ptype, flags, seq, tick, off
|
||||
|
||||
|
||||
# Message builders (subset/skeleton)
|
||||
|
||||
def build_config_update(
|
||||
*,
|
||||
version: int,
|
||||
seq: int,
|
||||
tick: int,
|
||||
tick_rate: int,
|
||||
wrap_edges: bool,
|
||||
apples_per_snake: int,
|
||||
apples_cap: int,
|
||||
) -> bytes:
|
||||
header = pack_header(version, PacketType.CONFIG_UPDATE, 0, seq, tick)
|
||||
body = bytearray()
|
||||
body.append(tick_rate & 0xFF) # u8
|
||||
body.append(1 if wrap_edges else 0) # bool u8
|
||||
body.append(apples_per_snake & 0xFF) # u8
|
||||
body.append(apples_cap & 0xFF) # u8
|
||||
return header + bytes(body)
|
||||
|
||||
|
||||
@dataclass
|
||||
class InputEvent:
|
||||
rel_tick_offset: int # relative to base_tick
|
||||
direction: Direction
|
||||
|
||||
|
||||
def build_input_broadcast(
|
||||
*,
|
||||
version: int,
|
||||
seq: int,
|
||||
tick: int,
|
||||
player_id: int,
|
||||
input_seq: int,
|
||||
base_tick: int,
|
||||
events: Sequence[InputEvent],
|
||||
apply_at_tick: int | None = None,
|
||||
) -> bytes:
|
||||
header = pack_header(version, PacketType.INPUT_BROADCAST, 0, seq, tick)
|
||||
body = bytearray()
|
||||
body.append(player_id & 0xFF)
|
||||
body.extend(int(input_seq & 0xFFFF).to_bytes(2, "big"))
|
||||
body.extend(int(base_tick & 0xFFFF).to_bytes(2, "big"))
|
||||
# number of events as QUIC varint
|
||||
body.extend(quic_varint_encode(len(events)))
|
||||
for ev in events:
|
||||
# rel offset as QUIC varint, direction as u8 (low 2 bits)
|
||||
body.extend(quic_varint_encode(int(ev.rel_tick_offset)))
|
||||
body.append(int(ev.direction) & 0x03)
|
||||
# Optional absolute apply_at_tick presence flag + value
|
||||
if apply_at_tick is None:
|
||||
body.append(0)
|
||||
else:
|
||||
body.append(1)
|
||||
body.extend(int(apply_at_tick & 0xFFFF).to_bytes(2, "big"))
|
||||
return header + bytes(body)
|
||||
|
||||
|
||||
def pack_body_tlv(t: BodyTLV, payload: bytes) -> bytes:
|
||||
return quic_varint_encode(int(t)) + quic_varint_encode(len(payload)) + payload
|
||||
|
||||
|
||||
def bitpack_2bit_directions(directions: Iterable[Direction]) -> bytes:
|
||||
out = bytearray()
|
||||
acc = 0
|
||||
bits = 0
|
||||
for d in directions:
|
||||
acc |= (int(d) & 0x03) << bits
|
||||
bits += 2
|
||||
if bits >= 8:
|
||||
out.append(acc & 0xFF)
|
||||
acc = acc >> 8
|
||||
bits -= 8
|
||||
if bits:
|
||||
out.append(acc & 0xFF) # zero-padded high bits
|
||||
return bytes(out)
|
||||
|
||||
Reference in New Issue
Block a user