diff --git a/src/eolib/__init__.py b/src/eolib/__init__.py index aefe442..cea6df7 100644 --- a/src/eolib/__init__.py +++ b/src/eolib/__init__.py @@ -1,2 +1,3 @@ from .data import * from .encrypt import * +from .packet import * diff --git a/src/eolib/packet/__init__.py b/src/eolib/packet/__init__.py new file mode 100644 index 0000000..2f62b6f --- /dev/null +++ b/src/eolib/packet/__init__.py @@ -0,0 +1,2 @@ +from .sequence_start import * +from .packet_sequencer import * diff --git a/src/eolib/packet/packet_sequencer.py b/src/eolib/packet/packet_sequencer.py new file mode 100644 index 0000000..5fbc7a8 --- /dev/null +++ b/src/eolib/packet/packet_sequencer.py @@ -0,0 +1,45 @@ +from .sequence_start import SequenceStart + + +class PacketSequencer: + """A class for generating packet sequences.""" + + _start: SequenceStart + _counter: int + + def __init__(self, start: SequenceStart): + """ + Constructs a new PacketSequencer with the provided SequenceStart. + + Args: + start (SequenceStart): The sequence start. + """ + self._start = start + self._counter = 0 + + def next_sequence(self) -> int: + """ + Returns the next sequence value, updating the sequence counter in the process. + + Note: + This is not a monotonic operation. The sequence counter increases from 0 to 9 before + looping back around to 0. + + Returns: + int: The next sequence value. + """ + result = self._start.value + self._counter + self._counter = (self._counter + 1) % 10 + return result + + def set_sequence_start(self, start: SequenceStart) -> None: + """ + Sets the sequence start, also known as the "starting counter ID". + + Note: + This does not reset the sequence counter. + + Args: + start (SequenceStart): The new sequence start. + """ + self._start = start diff --git a/src/eolib/packet/sequence_start.py b/src/eolib/packet/sequence_start.py new file mode 100644 index 0000000..a1accc8 --- /dev/null +++ b/src/eolib/packet/sequence_start.py @@ -0,0 +1,211 @@ +import random +from abc import ABC, abstractproperty +from eolib.data.eo_numeric_limits import CHAR_MAX + + +class SequenceStart(ABC): + """ + A value sent by the server to update the client's sequence start, also known as the 'starting + counter ID'. + """ + + @abstractproperty + def value(self) -> int: + """ + int: Gets the sequence start value. + """ + raise NotImplementedError() + + @staticmethod + def zero() -> 'SequenceStart': + """ + Returns an instance of SequenceStart with a value of 0. + + Returns: + SequenceStart: An instance of SequenceStart. + """ + return SimpleSequenceStart(0) + + +class SimpleSequenceStart(SequenceStart): + _value: int + + def __init__(self, value: int): + self._value = value + + @property + def value(self): + return self._value + + +class AccountReplySequenceStart(SimpleSequenceStart): + """ + A class representing the sequence start value sent with the `ACCOUNT_REPLY` server packet. + + See Also: + - [`AccountReplyServerPacket`][eolib.protocol._generated.net.server.AccountReplyServerPacket] + """ + + def __init__(self, value: int): + super().__init__(value) + + @staticmethod + def from_value(value: int) -> 'AccountReplySequenceStart': + """ + Creates an instance of AccountReplySequenceStart from the value sent with the + `ACCOUNT_REPLY` server packet. + + Args: + value (int): The sequence start value sent with the `ACCOUNT_REPLY` server packet. + + Returns: + AccountReplySequenceStart: An instance of AccountReplySequenceStart. + """ + return AccountReplySequenceStart(value) + + @staticmethod + def generate() -> 'AccountReplySequenceStart': + """ + Generates an instance of AccountReplySequenceStart with a random value in the range 0-240. + + Returns: + AccountReplySequenceStart: An instance of AccountReplySequenceStart. + """ + return AccountReplySequenceStart(random.randrange(0, 240)) + + +class InitSequenceStart(SimpleSequenceStart): + """ + A class representing the sequence start value sent with the `INIT_INIT` server packet. + + See Also: + - [`InitInitServerPacket`][eolib.protocol._generated.net.server.InitInitServerPacket] + """ + + _seq1: int + _seq2: int + + def __init__(self, value: int, seq1: int, seq2: int): + super().__init__(value) + self._seq1 = seq1 + self._seq2 = seq2 + + @property + def seq1(self) -> int: + """ + Returns the `seq1` byte value sent with the INIT_INIT server packet. + + Returns: + int: The seq1 byte value. + """ + return self._seq1 + + @property + def seq2(self) -> int: + """ + Returns the `seq2` byte value sent with the INIT_INIT server packet. + + Returns: + int: The seq2 byte value. + """ + return self._seq2 + + @staticmethod + def from_init_values(seq1: int, seq2: int) -> 'InitSequenceStart': + """ + Creates an instance of InitSequenceStart from the values sent with the `INIT_INIT` server + packet. + + Args: + seq1 (int): The `seq1` byte value sent with the `INIT_INIT` server packet. + seq2 (int): The `seq2` byte value sent with the `INIT_INIT` server packet. + + Returns: + InitSequenceStart: An instance of InitSequenceStart + """ + value = seq1 * 7 + seq2 - 13 + return InitSequenceStart(value, seq1, seq2) + + @staticmethod + def generate() -> 'InitSequenceStart': + """ + Generates an instance of InitSequenceStart with a random value in the range 0-1757. + + Returns: + InitSequenceStart: An instance of InitSequenceStart. + """ + value = random.randrange(0, 1757) + seq1_max = int((value + 13) / 7) + seq1_min = max(0, int((value - (CHAR_MAX - 1) + 13 + 6) / 7)) + + seq1 = random.randrange(0, seq1_max - seq1_min) + seq1_min + seq2 = value - seq1 * 7 + 13 + + return InitSequenceStart(value, seq1, seq2) + + +class PingSequenceStart(SimpleSequenceStart): + """ + A class representing the sequence start value sent with the `CONNECTION_PLAYER` server packet. + + See Also: + - [`ConnectionPlayerServerPacket`][eolib.protocol._generated.net.server.ConnectionPlayerServerPacket] + """ + + def __init__(self, value: int, seq1: int, seq2: int): + super().__init__(value) + self._seq1 = seq1 + self._seq2 = seq2 + + @property + def seq1(self) -> int: + """ + Returns the seq1 short value sent with the `CONNECTION_PLAYER` server packet. + + Returns: + int: The seq1 short value. + """ + return self._seq1 + + @property + def seq2(self) -> int: + """ + Returns the seq2 char value sent with the CONNECTION_PLAYER server packet. + + Returns: + int: The seq2 char value. + """ + return self._seq2 + + @staticmethod + def from_ping_values(seq1: int, seq2: int) -> 'PingSequenceStart': + """ + Creates an instance of PingSequenceStart from the values sent with the `CONNECTION_PLAYER` + server packet. + + Args: + seq1 (int): The `seq1` short value sent with the `CONNECTION_PLAYER` server packet. + seq2 (int): The `seq2` char value sent with the `CONNECTION_PLAYER` server packet. + + Returns: + PingSequenceStart: An instance of PingSequenceStart. + """ + value = seq1 - seq2 + return PingSequenceStart(value, seq1, seq2) + + @staticmethod + def generate() -> 'PingSequenceStart': + """ + Generates an instance of PingSequenceStart with a random value in the range 0-1757. + + Returns: + PingSequenceStart: An instance of PingSequenceStart. + """ + value = random.randrange(0, 1757) + seq1 = value + random.randrange(0, CHAR_MAX - 1) + seq2 = seq1 - value + + return PingSequenceStart(value, seq1, seq2) + + +__all__ = ['SequenceStart', 'AccountReplySequenceStart', 'InitSequenceStart', 'PingSequenceStart'] diff --git a/tests/packet/test_packet_sequencer.py b/tests/packet/test_packet_sequencer.py new file mode 100644 index 0000000..6645265 --- /dev/null +++ b/tests/packet/test_packet_sequencer.py @@ -0,0 +1,23 @@ +from eolib.packet.packet_sequencer import PacketSequencer +from eolib.packet.sequence_start import AccountReplySequenceStart + + +def test_next_sequence(): + sequence_start = AccountReplySequenceStart.from_value(123) + sequencer = PacketSequencer(sequence_start) + + for i in range(10): + assert sequencer.next_sequence() == 123 + i + + assert sequencer.next_sequence() == 123 + + +def test_sequence_start(): + sequence_start = AccountReplySequenceStart.from_value(100) + sequencer = PacketSequencer(sequence_start) + + assert sequencer.next_sequence() == 100 + + sequencer.set_sequence_start(AccountReplySequenceStart.from_value(200)) + + assert sequencer.next_sequence() == 201 diff --git a/tests/packet/test_sequence_start.py b/tests/packet/test_sequence_start.py new file mode 100644 index 0000000..a31f7a5 --- /dev/null +++ b/tests/packet/test_sequence_start.py @@ -0,0 +1,59 @@ +import math +from eolib.packet.sequence_start import ( + SequenceStart, + AccountReplySequenceStart, + InitSequenceStart, + PingSequenceStart, +) + + +def randrange_midpoint(a, b): + return math.ceil((a + b) / 2) + + +def test_zero(): + assert SequenceStart.zero().value == 0 + + +def test_account_reply_from_value(): + sequence_start = AccountReplySequenceStart.from_value(22) + assert sequence_start.value == 22 + + +def test_account_reply_generate(monkeypatch): + monkeypatch.setattr('random.randrange', randrange_midpoint) + + sequence_start = AccountReplySequenceStart.generate() + assert sequence_start.value == 120 + + +def test_init__from_init_values(): + sequence_start = InitSequenceStart.from_init_values(110, 122) + assert sequence_start.value == 879 + assert sequence_start.seq1 == 110 + assert sequence_start.seq2 == 122 + + +def test_init_generate(monkeypatch): + monkeypatch.setattr('random.randrange', randrange_midpoint) + + sequence_start = InitSequenceStart.generate() + assert sequence_start.value == 879 + assert sequence_start.seq1 == 110 + assert sequence_start.seq2 == 122 + + +def test_ping_from_ping_values(): + sequence_start = PingSequenceStart.from_ping_values(1005, 126) + assert sequence_start.value == 879 + assert sequence_start.seq1 == 1005 + assert sequence_start.seq2 == 126 + + +def test_ping_generate(monkeypatch): + monkeypatch.setattr('random.randrange', randrange_midpoint) + + sequence_start = PingSequenceStart.generate() + assert sequence_start.value == 879 + assert sequence_start.seq1 == 1005 + assert sequence_start.seq2 == 126