class Header(Comparable): """Create a PyGaSe package header. # Arguments sequence (int): package sequence number ack (int): sequence number of the last received package ack_bitfield (str): A 32 character string representing the 32 sequence numbers prior to the last one received, with the first character corresponding the packge directly preceding it and so forth. '1' means that package has been received, '0' means it hasn't. # Attributes sequence (int): see corresponding constructor argument ack (int): see corresponding constructor argument ack_bitfield (str): see corresponding constructor argument --- Sequence numbers: A sequence of 0 means no packages have been sent or received. After 65535 sequence numbers wrap around to 1, so they can be stored in 2 bytes. """ def __init__(self, sequence: int, ack: int, ack_bitfield: str): self.sequence = Sqn(sequence) self.ack = Sqn(ack) self.ack_bitfield = ack_bitfield def to_bytearray(self) -> bytearray: """Return 12 bytes representing the header.""" result = bytearray(PROTOCOL_ID) result.extend(self.sequence.to_sqn_bytes()) result.extend(self.ack.to_sqn_bytes()) result.extend(int(self.ack_bitfield, 2).to_bytes(4, "big")) return result def destructure(self) -> tuple: """Return the tuple `(sequence, ack, ack_bitfield)`.""" return (self.sequence, self.ack, self.ack_bitfield) @classmethod def deconstruct_datagram(cls, datagram: bytes) -> tuple: """Return a tuple containing the header and the rest of the datagram. # Arguments datagram (bytes): serialized PyGaSe package to deconstruct # Returns tuple: `(header, payload)` with `payload` being a bytestring of the rest of the datagram """ if datagram[:4] != PROTOCOL_ID: raise ProtocolIDMismatchError sequence = Sqn.from_sqn_bytes(datagram[4:6]) ack = Sqn.from_sqn_bytes(datagram[6:8]) ack_bitfield = bin(int.from_bytes(datagram[8:12], "big"))[2:].zfill(32) payload = datagram[12:] return (cls(sequence, ack, ack_bitfield), payload)
class ClientPackage(Package): """Subclass of #Package for packages sent by PyGaSe clients. # Arguments time_order (int): the clients last known time order of the game state # Attributes time_order (int): see corresponding constructor argument """ def __init__(self, header: Header, time_order: int, events: list = None): super().__init__(header, events) self.time_order = Sqn(time_order) def to_datagram(self) -> bytes: """Override `Package.to_datagram` to include `time_order`.""" if self._datagram is not None: return self._datagram datagram = self.header.to_bytearray() # The header makes up the first 12 bytes of the package datagram.extend(self.time_order.to_sqn_bytes()) datagram.extend(self._create_event_block()) datagram = datagram if len(datagram) > self._max_size: raise OverflowError("Package exceeds the maximum size of " + str(self._max_size) + " bytes.") self._datagram = bytes(datagram) return self._datagram @classmethod def from_datagram(cls, datagram: bytes) -> "ClientPackage": """Override #Package.from_datagram to include `time_order`.""" header, payload = Header.deconstruct_datagram(datagram) time_order = Sqn.from_sqn_bytes(payload[:2]) payload = payload[2:] events = cls._read_out_event_block(payload) result = cls(header, time_order, events) result._datagram = datagram # pylint: disable=protected-access return result