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.
- pyphotonrealtime/__init__.py +30 -0
- pyphotonrealtime/peer.py +192 -0
- pyphotonrealtime/protocol/__init__.py +1 -0
- pyphotonrealtime/protocol/command_code.py +32 -0
- pyphotonrealtime/protocol/consts.py +12 -0
- pyphotonrealtime/protocol/deserializer.py +60 -0
- pyphotonrealtime/protocol/enum_lookups.py +67 -0
- pyphotonrealtime/protocol/event_code.py +61 -0
- pyphotonrealtime/protocol/operation_code.py +35 -0
- pyphotonrealtime/protocol/packet/__init__.py +4 -0
- pyphotonrealtime/protocol/packet/base.py +70 -0
- pyphotonrealtime/protocol/packet/disconnect.py +40 -0
- pyphotonrealtime/protocol/packet/event_packet.py +30 -0
- pyphotonrealtime/protocol/packet/factory.py +99 -0
- pyphotonrealtime/protocol/packet/format.py +6 -0
- pyphotonrealtime/protocol/packet/header.py +106 -0
- pyphotonrealtime/protocol/packet/init.py +133 -0
- pyphotonrealtime/protocol/packet/keep_alive.py +54 -0
- pyphotonrealtime/protocol/packet/key_exchange.py +125 -0
- pyphotonrealtime/protocol/packet/operation_packet.py +110 -0
- pyphotonrealtime/protocol/packet/operation_payload.py +127 -0
- pyphotonrealtime/protocol/packet/packet_stream.py +166 -0
- pyphotonrealtime/protocol/param/__init__.py +0 -0
- pyphotonrealtime/protocol/param/base.py +76 -0
- pyphotonrealtime/protocol/param/bool_param.py +19 -0
- pyphotonrealtime/protocol/param/custom_param.py +29 -0
- pyphotonrealtime/protocol/param/dict_param.py +55 -0
- pyphotonrealtime/protocol/param/dictionary_param_base.py +65 -0
- pyphotonrealtime/protocol/param/float32_param.py +18 -0
- pyphotonrealtime/protocol/param/float64_param.py +18 -0
- pyphotonrealtime/protocol/param/hashtable_param.py +56 -0
- pyphotonrealtime/protocol/param/int32_param.py +18 -0
- pyphotonrealtime/protocol/param/int64_param.py +18 -0
- pyphotonrealtime/protocol/param/int8_param.py +62 -0
- pyphotonrealtime/protocol/param/int8_slice_param.py +21 -0
- pyphotonrealtime/protocol/param/nil_param.py +20 -0
- pyphotonrealtime/protocol/param/object_slice_param.py +30 -0
- pyphotonrealtime/protocol/param/parameter_key.py +62 -0
- pyphotonrealtime/protocol/param/parameter_type.py +40 -0
- pyphotonrealtime/protocol/param/read_param.py +44 -0
- pyphotonrealtime/protocol/param/slice_param.py +57 -0
- pyphotonrealtime/protocol/param/string_param.py +28 -0
- pyphotonrealtime/protocol/param/type_registry.py +39 -0
- pyphotonrealtime/protocol/photon_enc.py +67 -0
- pyphotonrealtime/protocol/property_keys.py +35 -0
- pyphotonrealtime/protocol/serialization_protocol.py +7 -0
- pyphotonrealtime/protocol/serializer.py +48 -0
- pyphotonrealtime/py.typed +0 -0
- pyphotonrealtime/realtime/__init__.py +46 -0
- pyphotonrealtime/realtime/callbacks.py +115 -0
- pyphotonrealtime/realtime/client.py +278 -0
- pyphotonrealtime/realtime/options.py +70 -0
- pyphotonrealtime/realtime/player.py +30 -0
- pyphotonrealtime/realtime/room.py +49 -0
- pyphotonrealtime/realtime/settings.py +23 -0
- pyphotonrealtime/realtime/state.py +54 -0
- pyphotonrealtime/transport/__init__.py +6 -0
- pyphotonrealtime/transport/base.py +32 -0
- pyphotonrealtime/transport/tcp.py +67 -0
- pyphotonrealtime-0.0.1.dist-info/METADATA +93 -0
- pyphotonrealtime-0.0.1.dist-info/RECORD +62 -0
- 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
|
+
]
|
pyphotonrealtime/peer.py
ADDED
|
@@ -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,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
|