pyPhotonRealtime 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. pyphotonrealtime/__init__.py +30 -0
  2. pyphotonrealtime/peer.py +192 -0
  3. pyphotonrealtime/protocol/__init__.py +1 -0
  4. pyphotonrealtime/protocol/command_code.py +32 -0
  5. pyphotonrealtime/protocol/consts.py +12 -0
  6. pyphotonrealtime/protocol/deserializer.py +60 -0
  7. pyphotonrealtime/protocol/enum_lookups.py +67 -0
  8. pyphotonrealtime/protocol/event_code.py +61 -0
  9. pyphotonrealtime/protocol/operation_code.py +35 -0
  10. pyphotonrealtime/protocol/packet/__init__.py +4 -0
  11. pyphotonrealtime/protocol/packet/base.py +70 -0
  12. pyphotonrealtime/protocol/packet/disconnect.py +40 -0
  13. pyphotonrealtime/protocol/packet/event_packet.py +30 -0
  14. pyphotonrealtime/protocol/packet/factory.py +99 -0
  15. pyphotonrealtime/protocol/packet/format.py +6 -0
  16. pyphotonrealtime/protocol/packet/header.py +106 -0
  17. pyphotonrealtime/protocol/packet/init.py +133 -0
  18. pyphotonrealtime/protocol/packet/keep_alive.py +54 -0
  19. pyphotonrealtime/protocol/packet/key_exchange.py +125 -0
  20. pyphotonrealtime/protocol/packet/operation_packet.py +110 -0
  21. pyphotonrealtime/protocol/packet/operation_payload.py +127 -0
  22. pyphotonrealtime/protocol/packet/packet_stream.py +166 -0
  23. pyphotonrealtime/protocol/param/__init__.py +0 -0
  24. pyphotonrealtime/protocol/param/base.py +76 -0
  25. pyphotonrealtime/protocol/param/bool_param.py +19 -0
  26. pyphotonrealtime/protocol/param/custom_param.py +29 -0
  27. pyphotonrealtime/protocol/param/dict_param.py +55 -0
  28. pyphotonrealtime/protocol/param/dictionary_param_base.py +65 -0
  29. pyphotonrealtime/protocol/param/float32_param.py +18 -0
  30. pyphotonrealtime/protocol/param/float64_param.py +18 -0
  31. pyphotonrealtime/protocol/param/hashtable_param.py +56 -0
  32. pyphotonrealtime/protocol/param/int32_param.py +18 -0
  33. pyphotonrealtime/protocol/param/int64_param.py +18 -0
  34. pyphotonrealtime/protocol/param/int8_param.py +62 -0
  35. pyphotonrealtime/protocol/param/int8_slice_param.py +21 -0
  36. pyphotonrealtime/protocol/param/nil_param.py +20 -0
  37. pyphotonrealtime/protocol/param/object_slice_param.py +30 -0
  38. pyphotonrealtime/protocol/param/parameter_key.py +62 -0
  39. pyphotonrealtime/protocol/param/parameter_type.py +40 -0
  40. pyphotonrealtime/protocol/param/read_param.py +44 -0
  41. pyphotonrealtime/protocol/param/slice_param.py +57 -0
  42. pyphotonrealtime/protocol/param/string_param.py +28 -0
  43. pyphotonrealtime/protocol/param/type_registry.py +39 -0
  44. pyphotonrealtime/protocol/photon_enc.py +67 -0
  45. pyphotonrealtime/protocol/property_keys.py +35 -0
  46. pyphotonrealtime/protocol/serialization_protocol.py +7 -0
  47. pyphotonrealtime/protocol/serializer.py +48 -0
  48. pyphotonrealtime/py.typed +0 -0
  49. pyphotonrealtime/realtime/__init__.py +46 -0
  50. pyphotonrealtime/realtime/callbacks.py +115 -0
  51. pyphotonrealtime/realtime/client.py +278 -0
  52. pyphotonrealtime/realtime/options.py +70 -0
  53. pyphotonrealtime/realtime/player.py +30 -0
  54. pyphotonrealtime/realtime/room.py +49 -0
  55. pyphotonrealtime/realtime/settings.py +23 -0
  56. pyphotonrealtime/realtime/state.py +54 -0
  57. pyphotonrealtime/transport/__init__.py +6 -0
  58. pyphotonrealtime/transport/base.py +32 -0
  59. pyphotonrealtime/transport/tcp.py +67 -0
  60. pyphotonrealtime-0.0.1.dist-info/METADATA +93 -0
  61. pyphotonrealtime-0.0.1.dist-info/RECORD +62 -0
  62. pyphotonrealtime-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,30 @@
1
+ """Pure-Python client for games running on Photon Realtime.
2
+
3
+ Layers, bottom to top:
4
+
5
+ * :mod:`pyphotonrealtime.protocol` -- wire format: parameter (de)serialization,
6
+ packet framing, encryption primitives.
7
+ * :mod:`pyphotonrealtime.transport` -- byte transports (TCP now; UDP/WebSocket later).
8
+ * :mod:`pyphotonrealtime.peer` -- ``PhotonPeer``: one connection to one server,
9
+ operation/event/response queues, keep-alive, timeouts, encryption.
10
+ * :mod:`pyphotonrealtime.realtime` -- ``RealtimeClient``: Name Server -> Master
11
+ Server -> Game Server workflow, matchmaking, rooms, players, callbacks.
12
+ """
13
+
14
+ from pyphotonrealtime.peer import PeerState, PhotonPeer
15
+ from pyphotonrealtime.realtime import (
16
+ AppSettings,
17
+ ClientState,
18
+ RealtimeClient,
19
+ )
20
+
21
+ __version__ = "0.0.1"
22
+
23
+ __all__ = [
24
+ "AppSettings",
25
+ "ClientState",
26
+ "PeerState",
27
+ "PhotonPeer",
28
+ "RealtimeClient",
29
+ "__version__",
30
+ ]
@@ -0,0 +1,192 @@
1
+ """``PhotonPeer``: a single connection to a single Photon server.
2
+
3
+ Mirrors the official SDKs' service loop: nothing happens on the network unless
4
+ the application calls :meth:`PhotonPeer.service` (or the two halves,
5
+ :meth:`PhotonPeer.dispatch_incoming_commands` and
6
+ :meth:`PhotonPeer.send_outgoing_commands`) regularly, typically 10-50 times per
7
+ second, from a single thread.
8
+
9
+ Callbacks to the :class:`PeerListener` are only ever invoked from inside
10
+ ``dispatch_incoming_commands``, i.e. on the thread that calls ``service``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import time
16
+ from collections import deque
17
+ from dataclasses import dataclass, field
18
+ from enum import Enum, auto
19
+ from typing import TYPE_CHECKING, Any, Protocol
20
+
21
+ from pyphotonrealtime.protocol.packet.packet_stream import PhotonStreamParser
22
+ from pyphotonrealtime.transport import TcpTransport
23
+
24
+ if TYPE_CHECKING:
25
+ from pyphotonrealtime.protocol.packet.base import PhotonPacket
26
+ from pyphotonrealtime.protocol.param.base import ParameterBase
27
+ from pyphotonrealtime.transport import Transport
28
+
29
+ type Parameters = dict[int, ParameterBase[Any]]
30
+
31
+
32
+ class PeerState(Enum):
33
+ """Connection state of a :class:`PhotonPeer`."""
34
+
35
+ Disconnected = auto()
36
+ Connecting = auto()
37
+ Connected = auto()
38
+ Disconnecting = auto()
39
+
40
+
41
+ class StatusCode(Enum):
42
+ """Peer status changes reported to :meth:`PeerListener.on_status_changed`."""
43
+
44
+ Connect = auto()
45
+ Disconnect = auto()
46
+ ExceptionOnConnect = auto()
47
+ Exception = auto()
48
+ TimeoutDisconnect = auto()
49
+ DisconnectByServer = auto()
50
+ EncryptionEstablished = auto()
51
+ EncryptionFailedToEstablish = auto()
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class OperationResponse:
56
+ """The server's answer to an operation sent with ``send_operation``."""
57
+
58
+ operation_code: int
59
+ return_code: int
60
+ debug_message: str | None
61
+ parameters: Parameters = field(default_factory=dict)
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class EventData:
66
+ """A server- or player-originated event."""
67
+
68
+ code: int
69
+ parameters: Parameters = field(default_factory=dict)
70
+
71
+
72
+ class PeerListener(Protocol):
73
+ """Receives everything a peer dispatches (``IPhotonPeerListener``)."""
74
+
75
+ def on_status_changed(self, status: StatusCode) -> None:
76
+ """Handle a connection status change."""
77
+
78
+ def on_operation_response(self, response: OperationResponse) -> None:
79
+ """Handle the response to an operation."""
80
+
81
+ def on_event(self, event: EventData) -> None:
82
+ """Handle an incoming event."""
83
+
84
+
85
+ class PhotonPeer:
86
+ """One connection to one Photon server, driven by :meth:`service`."""
87
+
88
+ def __init__(
89
+ self,
90
+ listener: PeerListener,
91
+ transport: Transport | None = None,
92
+ *,
93
+ disconnect_timeout: float = 10.0,
94
+ keep_alive_interval: float = 2.0,
95
+ ) -> None:
96
+ """Create a disconnected peer.
97
+
98
+ Args:
99
+ listener: Receives status changes, responses and events.
100
+ transport: Byte transport to use; defaults to TCP.
101
+ disconnect_timeout: Seconds without traffic before giving up.
102
+ keep_alive_interval: Seconds of idle sending before a ping.
103
+ """
104
+ self.listener = listener
105
+ self.transport = transport if transport is not None else TcpTransport()
106
+ self.disconnect_timeout = disconnect_timeout
107
+ self.keep_alive_interval = keep_alive_interval
108
+
109
+ self.state = PeerState.Disconnected
110
+ self.server_address: str | None = None
111
+ self.app_id: str | None = None
112
+
113
+ self._parser = PhotonStreamParser()
114
+ self._outgoing: deque[PhotonPacket] = deque()
115
+ self._incoming: deque[PhotonPacket] = deque()
116
+ self._aes_key: bytes | None = None
117
+ self._last_receive = 0.0
118
+ self._last_send = 0.0
119
+
120
+ # -- connection -------------------------------------------------------
121
+
122
+ def connect(self, server_address: str, app_id: str) -> bool:
123
+ """Start connecting to ``host:port``; the result arrives as a status.
124
+
125
+ Returns:
126
+ Whether the connection attempt was started.
127
+ """
128
+ # M1: send InitRequestPacket(app id, protocol version), move to
129
+ # Connected on InitResponse.
130
+ raise NotImplementedError
131
+
132
+ def disconnect(self) -> None:
133
+ """Send a disconnect message, then close the transport."""
134
+ raise NotImplementedError
135
+
136
+ def establish_encryption(self) -> bool:
137
+ """Start a Diffie-Hellman key exchange (``InitEncryptionRequest``).
138
+
139
+ Returns:
140
+ Whether the exchange was started.
141
+ """
142
+ raise NotImplementedError
143
+
144
+ @property
145
+ def is_encryption_available(self) -> bool:
146
+ """Whether a shared AES key has been negotiated."""
147
+ return self._aes_key is not None
148
+
149
+ # -- service loop -----------------------------------------------------
150
+
151
+ def service(self) -> None:
152
+ """Dispatch everything received, then send everything queued."""
153
+ while self.dispatch_incoming_commands():
154
+ pass
155
+ while self.send_outgoing_commands():
156
+ pass
157
+
158
+ def dispatch_incoming_commands(self) -> bool:
159
+ """Read from the transport and dispatch at most one command.
160
+
161
+ Returns:
162
+ Whether there may be more to dispatch.
163
+ """
164
+ # M1: map parsed packets to OperationResponse/EventData, answer
165
+ # keep-alives, detect timeouts (disconnect_timeout).
166
+ raise NotImplementedError
167
+
168
+ def send_outgoing_commands(self) -> bool:
169
+ """Flush queued commands to the transport.
170
+
171
+ Returns:
172
+ Whether more remain to be sent.
173
+ """
174
+ # M1: also emit a keep-alive when idle for keep_alive_interval.
175
+ raise NotImplementedError
176
+
177
+ # -- operations -------------------------------------------------------
178
+
179
+ def send_operation(
180
+ self, operation_code: int, parameters: Parameters, *, encrypt: bool = False
181
+ ) -> bool:
182
+ """Queue an operation; it goes out on the next ``send_outgoing_commands``.
183
+
184
+ Returns:
185
+ Whether the operation was queued.
186
+ """
187
+ # M1: build via PacketFactory.operation (encrypted when requested).
188
+ raise NotImplementedError
189
+
190
+ @staticmethod
191
+ def _now() -> float:
192
+ return time.monotonic()
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1,32 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class CommandCode(IntEnum):
5
+ Init = 0
6
+ InitResponse = 1
7
+
8
+ Operation = 2
9
+ OperationResponse = 3
10
+
11
+ Event = 4
12
+ DisconnectMessage = 5
13
+
14
+ KeyExchangeRequest = 6
15
+ KeyExchangeResponse = 7
16
+
17
+ Message = 8
18
+ RawMessage = 9
19
+
20
+ EncryptedOperation = 0x82
21
+ EncryptedOperationResponse = 0x83
22
+
23
+ EncryptedEvent = 0x84
24
+
25
+ Unknown = 0xFF
26
+
27
+
28
+ ENCRYPTED_FLAG = 0x80
29
+
30
+
31
+ def is_encrypted(command_code: CommandCode) -> bool:
32
+ return command_code.value > ENCRYPTED_FLAG
@@ -0,0 +1,12 @@
1
+ """Wire-level constants of the Photon TCP protocol."""
2
+
3
+ MSG_MAGIC = 0xF3
4
+
5
+ # Diffie-Hellman parameters used by Photon's key exchange (Oakley group 1).
6
+ OAKLEY_PRIME_768_HEX = (
7
+ "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA6"
8
+ "3B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
9
+ "E485B576625E7EC6F44C42E9A63A3620FFFFFFFFFFFFFFFF"
10
+ )
11
+ OAKLEY_PRIME_768 = int(OAKLEY_PRIME_768_HEX, 16)
12
+ DH_GENERATOR = 22
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import struct
4
+ from io import BytesIO
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ from pyphotonrealtime.protocol.command_code import CommandCode
8
+ from pyphotonrealtime.protocol.operation_code import OperationCode
9
+ from pyphotonrealtime.protocol.param.read_param import read_parameter
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import Any
13
+
14
+ from pyphotonrealtime.protocol.packet.header import PhotonDataPacketHeader
15
+ from pyphotonrealtime.protocol.param.base import ParameterBase
16
+ from pyphotonrealtime.protocol.param.parameter_key import ParameterKey
17
+
18
+
19
+ def deserialize_photon_payload(
20
+ header: PhotonDataPacketHeader,
21
+ data: bytes,
22
+ ) -> tuple[
23
+ OperationCode,
24
+ dict[ParameterKey, ParameterBase[Any]],
25
+ tuple[int, ParameterBase[Any]] | None,
26
+ ]:
27
+ stream = BytesIO(data)
28
+
29
+ is_response = header.command in (
30
+ CommandCode.OperationResponse,
31
+ CommandCode.KeyExchangeResponse,
32
+ CommandCode.EncryptedOperationResponse,
33
+ CommandCode.DisconnectMessage,
34
+ )
35
+ skip_operation_code = header.command == CommandCode.DisconnectMessage
36
+
37
+ operation_code = OperationCode.DiffieHellmanRequest
38
+ if not skip_operation_code:
39
+ operation_code = cast("OperationCode", struct.unpack(">B", stream.read(1))[0])
40
+
41
+ response_debug_data = None
42
+ if is_response:
43
+ return_code = struct.unpack(">h", stream.read(2))[0]
44
+
45
+ debug_message_param = read_parameter(stream)
46
+
47
+ response_debug_data = (return_code, debug_message_param)
48
+
49
+ param_count = struct.unpack(">h", stream.read(2))[0]
50
+
51
+ params: dict[ParameterKey, ParameterBase[Any]] = {}
52
+ for _ in range(param_count):
53
+ key_bytes = stream.read(1)
54
+ if not key_bytes:
55
+ break
56
+ param_key = cast("ParameterKey", struct.unpack(">B", key_bytes)[0])
57
+
58
+ params[param_key] = read_parameter(stream)
59
+
60
+ return operation_code, params, response_debug_data
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from pyphotonrealtime.protocol.command_code import CommandCode
7
+ from pyphotonrealtime.protocol.event_code import EventCode
8
+ from pyphotonrealtime.protocol.operation_code import OperationCode
9
+ from pyphotonrealtime.protocol.param.base import ParameterBase
10
+ from pyphotonrealtime.protocol.param.parameter_key import ParameterKey
11
+ from pyphotonrealtime.protocol.param.parameter_type import ParameterType
12
+ from pyphotonrealtime.protocol.serialization_protocol import SerializationProtocol
13
+
14
+ if TYPE_CHECKING:
15
+ from enum import IntEnum
16
+
17
+
18
+ def build_value_lookup(enum_cls: type[IntEnum]) -> dict[int, list[str]]:
19
+ lookup: dict[int, list[str]] = defaultdict(list)
20
+
21
+ for name, member in enum_cls.__members__.items():
22
+ lookup[int(member.value)].append(name)
23
+
24
+ return dict(lookup)
25
+
26
+
27
+ OPERATION_CODE_LOOKUP = build_value_lookup(OperationCode)
28
+ EVENT_CODE_LOOKUP = build_value_lookup(EventCode)
29
+ PARAMETER_KEY_LOOKUP = build_value_lookup(ParameterKey)
30
+ PARAMETER_TYPE_LOOKUP = build_value_lookup(ParameterType)
31
+ COMMAND_CODE_LOOKUP = build_value_lookup(CommandCode)
32
+ SERIALIZATION_PROTOCOL_LOOKUP = build_value_lookup(SerializationProtocol)
33
+
34
+ CommandParams = dict[ParameterKey, ParameterBase[Any]]
35
+
36
+
37
+ def _get_x_name[XT](lookup_table: dict[XT, list[str]], v: XT) -> str:
38
+ n = lookup_table.get(v)
39
+ if n is None:
40
+ return f"UNKNOWN[{v!s}]"
41
+ if len(n) == 1:
42
+ return n[0]
43
+ return "/".join(n)
44
+
45
+
46
+ def get_parameter_key_name(key: ParameterKey) -> str:
47
+ return _get_x_name(PARAMETER_KEY_LOOKUP, key)
48
+
49
+
50
+ def get_parameter_type_name(param_type: ParameterType) -> str:
51
+ return _get_x_name(PARAMETER_TYPE_LOOKUP, param_type)
52
+
53
+
54
+ def get_operation_name(operation_code: OperationCode) -> str:
55
+ return _get_x_name(OPERATION_CODE_LOOKUP, operation_code)
56
+
57
+
58
+ def get_event_name(event_code: EventCode) -> str:
59
+ return _get_x_name(EVENT_CODE_LOOKUP, event_code)
60
+
61
+
62
+ def get_command_name(command_code: CommandCode) -> str:
63
+ return _get_x_name(COMMAND_CODE_LOOKUP, command_code)
64
+
65
+
66
+ def get_serialization_protocol_name(protocol: SerializationProtocol) -> str:
67
+ return _get_x_name(SERIALIZATION_PROTOCOL_LOOKUP, protocol)
@@ -0,0 +1,61 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ def _as_code(value: object) -> int:
5
+ if not isinstance(value, int):
6
+ msg = f"Event codes are ints, got {type(value).__name__}"
7
+ raise TypeError(msg)
8
+ return value
9
+
10
+
11
+ class PrisonArchitectEventCode(IntEnum):
12
+ """Reverse-engineered custom Photon event codes for Prison Architect.
13
+
14
+ Photon custom events are strictly in the 1-199 range.
15
+ """
16
+
17
+ PlayerJoin_Unknown = 2 # Suspected Player Join
18
+ PasswordAuthRequest = 5
19
+ KickPlayer = 6 # Soft kick, just asks the client to leave
20
+ WorldUpdate = 9
21
+ TickSpeedUpdate = 96
22
+
23
+ # Placeholders for identified but unmapped events
24
+ Unknown1 = 1
25
+ Unknown3 = 3
26
+ Unknown4 = 4
27
+ Unknown8 = 8
28
+ Unknown13 = 13
29
+ Unknown14 = 14
30
+ Unknown47 = 47
31
+ Unknown89 = 89
32
+ Unknown95 = 95
33
+ Unknown118 = 118
34
+
35
+ @classmethod
36
+ def _missing_(cls, value: object) -> int:
37
+ """Map unknown codes to plain ints so undocumented events don't crash."""
38
+ return _as_code(value)
39
+
40
+
41
+ CUSTOM_EVENT_CODE_MAX = 200
42
+
43
+
44
+ class EventCode(IntEnum):
45
+ GameList = 0xE6 # 230
46
+ GameListUpdate = 0xE5
47
+ QueueState = 0xE4
48
+ AppStats = 0xE2 # 226
49
+ GameServerOffline = 0xE1
50
+ LobbyStats = 0xE0
51
+ AuthEvent = 0xDF
52
+
53
+ Event = 0xFF
54
+
55
+ @classmethod
56
+ def _missing_(cls, value: object) -> int:
57
+ """Map unknown codes to plain ints so undocumented events don't crash."""
58
+ code = _as_code(value)
59
+ if 0 < code <= CUSTOM_EVENT_CODE_MAX:
60
+ return PrisonArchitectEventCode(code)
61
+ return code
@@ -0,0 +1,35 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class OperationCode(IntEnum):
5
+ DiffieHellmanRequest = 0
6
+ DiffieHellmanResponse = 0x2A # 42
7
+
8
+ GetGameList = 0xD9 # 217
9
+ Settings = 0xDA # 218
10
+ Rpc = 0xDB # 219
11
+ GetRegions = 0xDC # 220
12
+ LobbyStats = 0xDD # 221
13
+ FindFriends = 0xDE # 222
14
+ DebugGame = 0xDF # 223
15
+
16
+ JoinRandomGame = 0xE1 # 225
17
+ JoinGame = 0xE2 # 226
18
+ CreateGame = 0xE3 # 227
19
+ LeaveLobby = 0xE4 # 228
20
+ JoinLobby = 0xE5 # 229
21
+
22
+ GameList = 0xE6 # 230
23
+ Authenticate = 0xE6 # 230 (Alias for GameList)
24
+ AuthOnce = 0xE7 # 231
25
+
26
+ ChangeGroups = 0xF8 # 248
27
+ Ping = 0xF9 # 249
28
+
29
+ GetProperties = 0xFB # 251
30
+ SetProperties = 0xFC # 252
31
+ RaiseEvent = 0xFD # 253
32
+ Leave = 0xFE # 254
33
+ Join = 0xFF # 255
34
+
35
+ Event = 0xFF # 255
@@ -0,0 +1,4 @@
1
+ from .base import PhotonPacket
2
+ from .factory import PacketFactory
3
+
4
+ __all__ = ["PacketFactory", "PhotonPacket"]
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from struct import pack
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ from pyphotonrealtime.protocol.packet.format import PacketFormat
8
+ from pyphotonrealtime.protocol.packet.header import PhotonDataPacketHeader
9
+
10
+ if TYPE_CHECKING:
11
+ from io import BytesIO
12
+
13
+
14
+ class PhotonPacket(ABC):
15
+ _packet_format: PacketFormat
16
+
17
+ def __init__(self, packet_format: PacketFormat = PacketFormat.Data) -> None:
18
+ self._packet_format = packet_format
19
+
20
+ @classmethod
21
+ def from_bytes(cls, data: BytesIO) -> PhotonPacket:
22
+ packet_format = cast("PacketFormat", int.from_bytes(data.read(1)))
23
+ return cls(packet_format)
24
+
25
+ def serialize(self) -> bytes:
26
+ format_val = (
27
+ self._packet_format.value
28
+ if hasattr(self._packet_format, "value")
29
+ else self._packet_format
30
+ )
31
+ return pack(">B", format_val)
32
+
33
+ def get_format(self) -> PacketFormat:
34
+ return self._packet_format
35
+
36
+ def log(self) -> list[str]:
37
+ return [
38
+ f"Abstract Packet {self.get_format()}",
39
+ ]
40
+
41
+
42
+ class PhotonDataPacket(PhotonPacket):
43
+ _header: PhotonDataPacketHeader
44
+ _data_length: int | None
45
+
46
+ def __init__(self, header: PhotonDataPacketHeader) -> None:
47
+ super().__init__(PacketFormat.Data)
48
+ self._header = header
49
+ self._data_length = None
50
+
51
+ @classmethod
52
+ def from_bytes(cls, data: BytesIO) -> PhotonDataPacket:
53
+ return cls(PhotonDataPacketHeader.from_bytes(data))
54
+
55
+ def get_header(self) -> PhotonDataPacketHeader:
56
+ return self._header
57
+
58
+ def resize(self, new_data_length: int) -> None:
59
+ self._data_length = new_data_length
60
+
61
+ def serialize(self) -> bytes:
62
+ return super().serialize() + self._header.serialize(self._data_length)
63
+
64
+ def log(self) -> list[str]:
65
+ length = (
66
+ self._data_length
67
+ if self._data_length is not None
68
+ else self.get_header().packet_length
69
+ )
70
+ return [f"{self.get_header().get_command_name()}, Length: {length}"]
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Self
4
+
5
+ from pyphotonrealtime.protocol.packet.operation_packet import PhotonOperationPacket
6
+
7
+ if TYPE_CHECKING:
8
+ from io import BytesIO
9
+
10
+ from pyphotonrealtime.protocol.packet.header import PhotonDataPacketHeader
11
+ from pyphotonrealtime.protocol.packet.operation_payload import PhotonPacketPayload
12
+
13
+
14
+ class DisconnectMessagePacket(PhotonOperationPacket):
15
+ def __init__(
16
+ self,
17
+ header: PhotonDataPacketHeader,
18
+ payload: PhotonPacketPayload,
19
+ aes_key: bytes | None = None,
20
+ ) -> None:
21
+ super().__init__(header=header, payload=payload, aes_key=aes_key)
22
+
23
+ @classmethod
24
+ def from_bytes(
25
+ cls,
26
+ data: BytesIO,
27
+ *,
28
+ header: PhotonDataPacketHeader | None = None,
29
+ aes_key: bytes | None = None,
30
+ ) -> Self:
31
+ return super().from_bytes(data, header=header, aes_key=aes_key)
32
+
33
+ def log(self) -> list[str]:
34
+ return [
35
+ (
36
+ f"Command: {self.get_header().get_command_name()} "
37
+ f"{self.get_payload().get_operation_name()}"
38
+ ),
39
+ f"Reason: {self.get_payload().get_debug_message()}",
40
+ ]
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from pyphotonrealtime.protocol.command_code import CommandCode
6
+ from pyphotonrealtime.protocol.packet.operation_packet import PhotonOperationPacket
7
+
8
+ if TYPE_CHECKING:
9
+ from pyphotonrealtime.protocol.packet.header import PhotonDataPacketHeader
10
+ from pyphotonrealtime.protocol.packet.operation_payload import (
11
+ PhotonPacketPayload,
12
+ )
13
+
14
+
15
+ class PhotonEventPacket(PhotonOperationPacket):
16
+ def __init__(
17
+ self,
18
+ header: PhotonDataPacketHeader,
19
+ payload: PhotonPacketPayload,
20
+ aes_key: bytes | None = None,
21
+ ) -> None:
22
+ super().__init__(header=header, payload=payload, aes_key=aes_key)
23
+ if header.get_command_code() not in (
24
+ CommandCode.Event,
25
+ CommandCode.EncryptedEvent,
26
+ ):
27
+ msg = "Packet is not an Event!"
28
+ raise TypeError(msg)
29
+ self._operation_payload = payload
30
+ self._aes_key = aes_key