Resolve 51 findings from the I/RUF/BLE/TRY002/S110/PLR0402 rule set: - Sort imports and __all__ (I001, RUF022, PLR0402). The sys.path.insert calls in check_public_calls.py and tests/test_e2e.py still precede the openttd imports that depend on them. - Replace unused unpacked values with _ (RUF059) and annotate the two timetable lookup tables as ClassVar (RUF012). - Narrow the best-effort excepts in OpenTTDClient.quit and OpenTTDAdminClient.quit to (OSError, SocketClosed) and log at debug rather than swallowing silently (BLE001, S110). The test doubles now raise an OSError subclass so they still exercise that branch. - Narrow the gamescript JSON fallback to json.JSONDecodeError. The broad catch in receive_packet keeps a noqa: it guards untrusted wire data and must degrade to a no-op packet instead of killing the connection. - Use contextlib.suppress instead of try/except/pass in tests. ruff check . is clean, 102 tests pass, coverage stays at 100%. Co-Authored-By: Claude <[email protected]>
589 lines
19 KiB
Python
589 lines
19 KiB
Python
import struct
|
|
from enum import IntEnum
|
|
|
|
import monocypher
|
|
from openttd_protocol.wire.exceptions import SocketClosed
|
|
from openttd_protocol.wire.read import read_string, read_uint8, read_uint16, read_uint32
|
|
from openttd_protocol.wire.tcp import TCPProtocol
|
|
|
|
|
|
def write_varuint(buffer, value):
|
|
"""Encode a non-negative integer using OpenTTD's UTF-8-like varuint scheme."""
|
|
if value < 0:
|
|
raise ValueError("write_varuint requires a non-negative value")
|
|
thresholds = [1 << 7, 1 << 14, 1 << 21, 1 << 28, 1 << 35, 1 << 42, 1 << 49, 1 << 56]
|
|
for extra, limit in enumerate(thresholds):
|
|
if value < limit:
|
|
header_ones = (0xFF << (8 - extra)) & 0xFF
|
|
header = header_ones | (value >> (extra * 8))
|
|
buffer.append(header)
|
|
for i in range(extra - 1, -1, -1):
|
|
buffer.append((value >> (i * 8)) & 0xFF)
|
|
return
|
|
buffer.append(0xFF)
|
|
for i in range(7, -1, -1):
|
|
buffer.append((value >> (i * 8)) & 0xFF)
|
|
|
|
def read_varuint(data):
|
|
"""Decode a varuint written by write_varuint. Returns (value, rest)."""
|
|
header = data[0]
|
|
mask = 0x80
|
|
extra = 0
|
|
while header & mask:
|
|
extra += 1
|
|
mask >>= 1
|
|
value = header & (0x7F >> extra)
|
|
rest = data[1:]
|
|
for i in range(extra):
|
|
value = (value << 8) | rest[i]
|
|
return value, rest[extra:]
|
|
|
|
def write_varuint_signed(buffer, value):
|
|
"""Encode a signed integer using zigzag + write_varuint."""
|
|
zigzag = (value << 1) ^ (-1 if value < 0 else 0)
|
|
write_varuint(buffer, zigzag)
|
|
|
|
def read_varuint_signed(data):
|
|
"""Decode a signed varuint written by write_varuint_signed. Returns (value, rest)."""
|
|
zigzag, rest = read_varuint(data)
|
|
value = (zigzag >> 1) ^ -(zigzag & 1)
|
|
return value, rest
|
|
|
|
class GameCommand(IntEnum):
|
|
DeleteOrder = 51
|
|
InsertOrder = 52
|
|
ChangeTimetable = 174
|
|
SetVehicleOnTime = 176
|
|
AutofillTimetable = 177
|
|
SetTimetableStart = 180
|
|
# Scheduled dispatch (JGRPP)
|
|
SchDispatch = 205
|
|
SchDispatchAdd = 206
|
|
SchDispatchRemove = 207
|
|
SchDispatchSetDuration = 208
|
|
SchDispatchSetStartDate = 209
|
|
SchDispatchClear = 213
|
|
SchDispatchAddNewSchedule = 214
|
|
SchDispatchRemoveSchedule = 215
|
|
|
|
# Sentinel VehicleOrderID meaning "append to the end of the order list" for InsertOrder.
|
|
INVALID_VEH_ORDER_ID = 0xFFFF
|
|
|
|
class OrderType(IntEnum):
|
|
"""OrderType occupies bits 0-3 of an order's `type` byte (bits 6-7 hold OrderNonStopFlags)."""
|
|
GotoStation = 1
|
|
GotoDepot = 2
|
|
GotoWaypoint = 6
|
|
|
|
class OrderNonStopFlags(IntEnum):
|
|
"""Packed into bits 6-7 of an order's `type` byte."""
|
|
StopEverywhere = 0
|
|
NoStopAtIntermediate = 1
|
|
NoStopAtDestination = 2
|
|
NoStopAtAny = 3
|
|
|
|
class OrderStopLocation(IntEnum):
|
|
"""Packed into bits 4-5 of an order's `type` byte. Near-end/middle/through are train-only;
|
|
FarEnd is the only value the server accepts for every vehicle type, so it is the safe default."""
|
|
PlatformNearEnd = 0
|
|
PlatformMiddle = 1
|
|
PlatformFarEnd = 2
|
|
PlatformThrough = 3
|
|
|
|
class ModifyTimetableFlags(IntEnum):
|
|
WaitTime = 0
|
|
TravelTime = 1
|
|
TravelSpeed = 2
|
|
SetWaitFixed = 3
|
|
SetTravelFixed = 4
|
|
SetLeaveType = 5
|
|
AssignSchedule = 6
|
|
|
|
class ModifyTimetableCtrlFlag(IntEnum):
|
|
ClearField = 1 << 0
|
|
|
|
class PacketGameType(IntEnum):
|
|
ServerFull = 0
|
|
ServerBanned = 1
|
|
ClientJoin = 2
|
|
ServerError = 3
|
|
ClientUnused = 4
|
|
ServerUnused = 5
|
|
ServerGameInfo = 6
|
|
ClientGameInfo = 7
|
|
ServerNewGame = 8
|
|
ServerShutdown = 9
|
|
ServerGameInfoExtended = 10
|
|
ServerAuthenticationRequest = 11
|
|
ClientAuthenticationResponse = 12
|
|
ServerEnableEncryption = 13
|
|
ClientIdentify = 14
|
|
ServerCheckNewGRFs = 15
|
|
ClientNewGRFsChecked = 16
|
|
ServerNeedCompanyPassword = 17
|
|
ClientCompanyPassword = 18
|
|
ClientSettingsPassword = 19
|
|
ServerSettingsAccess = 20
|
|
ServerWelcome = 21
|
|
ServerClientInfo = 22
|
|
ClientGetMap = 23
|
|
ServerWaitForMap = 24
|
|
ServerMapBegin = 25
|
|
ServerMapSize = 26
|
|
ServerMapData = 27
|
|
ServerMapDone = 28
|
|
ClientMapOk = 29
|
|
ServerClientJoined = 30
|
|
ServerFrame = 31
|
|
ClientAck = 32
|
|
ServerSync = 33
|
|
ClientCommand = 34
|
|
ServerCommand = 35
|
|
ClientChat = 36
|
|
ServerChat = 37
|
|
ServerExternalChat = 38
|
|
ClientQuit = 47
|
|
ServerCompanyUpdate = 45
|
|
PACKET_END = 100
|
|
|
|
class PacketAdminType(IntEnum):
|
|
AdminJoin = 0
|
|
AdminQuit = 1
|
|
AdminUpdateFrequency = 2
|
|
AdminPoll = 3
|
|
AdminChat = 4
|
|
AdminRcon = 5
|
|
AdminGamescript = 6
|
|
AdminPing = 7
|
|
AdminExternalChat = 8
|
|
AdminJoinSecure = 9
|
|
AdminAuthResponse = 10
|
|
|
|
ServerFull = 100
|
|
ServerBanned = 101
|
|
ServerError = 102
|
|
ServerProtocol = 103
|
|
ServerWelcome = 104
|
|
ServerNewGame = 105
|
|
ServerShutdown = 106
|
|
|
|
ServerDate = 107
|
|
ServerClientJoin = 108
|
|
ServerClientInfo = 109
|
|
ServerClientUpdate = 110
|
|
ServerClientQuit = 111
|
|
ServerClientError = 112
|
|
ServerCompanyNew = 113
|
|
ServerCompanyInfo = 114
|
|
ServerCompanyUpdate = 115
|
|
ServerCompanyRemove = 116
|
|
ServerCompanyEconomy = 117
|
|
ServerCompanyStats = 118
|
|
ServerChat = 119
|
|
ServerRcon = 120
|
|
ServerConsole = 121
|
|
ServerCmdNames = 122
|
|
ServerCmdLoggingOld = 123
|
|
ServerGamescript = 124
|
|
ServerRconEnd = 125
|
|
ServerPong = 126
|
|
ServerCmdLogging = 127
|
|
ServerAuthRequest = 128
|
|
ServerEnableEncryption = 129
|
|
|
|
PACKET_END = 130
|
|
|
|
class AdminUpdateFrequency(IntEnum):
|
|
Poll = 1 << 0
|
|
Daily = 1 << 1
|
|
Weekly = 1 << 2
|
|
Monthly = 1 << 3
|
|
Quarterly = 1 << 4
|
|
Annually = 1 << 5
|
|
Automatic = 1 << 6
|
|
|
|
class AdminUpdateType(IntEnum):
|
|
Date = 0
|
|
ClientInfo = 1
|
|
CompanyInfo = 2
|
|
CompanyEconomy = 3
|
|
CompanyStats = 4
|
|
Chat = 5
|
|
Console = 6
|
|
CmdNames = 7
|
|
CmdLogging = 8
|
|
Gamescript = 9
|
|
End = 10
|
|
|
|
class NetworkAuthenticationMethod(IntEnum):
|
|
X25519_KeyExchangeOnly = 0
|
|
X25519_PAKE = 1
|
|
X25519_AuthorizedKey = 2
|
|
|
|
class OpenTTDProtocol(TCPProtocol):
|
|
"""Low-level OpenTTD TCP protocol handler with encryption support."""
|
|
PacketType = PacketGameType
|
|
PACKET_END = PacketGameType.PACKET_END
|
|
|
|
def __init__(self, handler):
|
|
super().__init__(handler)
|
|
self.handler = handler
|
|
|
|
def receive_packet(self, source, data):
|
|
try:
|
|
if self.handler.encryption_enabled:
|
|
if not self.handler._recv_aead:
|
|
self.handler._recv_aead = monocypher.IncrementalAuthenticatedEncryption(self.handler._session_key_recv, self.handler._encryption_nonce)
|
|
_, rest = read_uint16(data)
|
|
payload = self.handler._recv_aead.unlock(bytes(rest[:16]), bytes(rest[16:]))
|
|
if payload is None:
|
|
raise SocketClosed("Decryption failed")
|
|
data = memoryview(struct.pack("<H", len(payload) + 2) + payload)
|
|
|
|
return super().receive_packet(source, data)
|
|
except Exception: # noqa: BLE001 - untrusted wire data: any decode failure must degrade to a no-op packet rather than kill the connection
|
|
return PacketGameType.ServerUnused, {}
|
|
|
|
async def send_packet(self, data):
|
|
if self.handler.encryption_enabled:
|
|
if not self.handler._send_aead:
|
|
self.handler._send_aead = monocypher.IncrementalAuthenticatedEncryption(self.handler._session_key_send, self.handler._encryption_nonce)
|
|
_, payload = read_uint16(memoryview(data))
|
|
mac, ciphertext = self.handler._send_aead.lock(payload.tobytes())
|
|
data = struct.pack("<H", 18 + len(ciphertext)) + mac + ciphertext
|
|
|
|
await self._can_write.wait()
|
|
if self.transport.is_closing():
|
|
raise SocketClosed
|
|
self.transport.write(data)
|
|
return len(data)
|
|
|
|
# --- Static Parsers ---
|
|
@staticmethod
|
|
def receive_ServerGameInfo(source, data):
|
|
from openttd_protocol.protocol.game import GameProtocol
|
|
return GameProtocol.receive_PACKET_SERVER_GAME_INFO(source, data)
|
|
@staticmethod
|
|
def receive_ServerError(source, data):
|
|
ec, _ = read_uint8(data)
|
|
return {"error_code": ec}
|
|
@staticmethod
|
|
def receive_ServerAuthenticationRequest(source, data):
|
|
at, rest = read_uint8(data)
|
|
return {"auth_type": at, "data": rest}
|
|
@staticmethod
|
|
def receive_ServerEnableEncryption(source, data): return {"data": data}
|
|
@staticmethod
|
|
def receive_ServerCheckNewGRFs(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerUnused(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerWelcome(source, data):
|
|
cid, _ = read_uint32(data)
|
|
return {"client_id": cid}
|
|
@staticmethod
|
|
def receive_ServerNeedCompanyPassword(source, data):
|
|
seed, data = read_uint32(data)
|
|
sid, _ = read_string(data)
|
|
return {"seed": seed, "server_id": sid}
|
|
@staticmethod
|
|
def receive_ServerFrame(source, data):
|
|
f, data = read_uint32(data)
|
|
_, data = read_uint32(data)
|
|
token = 0
|
|
if len(data) > 0:
|
|
if len(data) >= 13:
|
|
data = data[12:]
|
|
token, _ = read_uint8(data)
|
|
return {"frame": f, "token": token}
|
|
@staticmethod
|
|
def receive_ServerChat(source, data):
|
|
_, data = read_uint8(data)
|
|
cid, data = read_uint32(data)
|
|
_, data = read_uint8(data)
|
|
msg, _ = read_string(data)
|
|
return {"client_id": cid, "message": msg}
|
|
@staticmethod
|
|
def receive_ServerCompanyUpdate(source, data):
|
|
mask, _ = read_uint16(data)
|
|
return {"passworded_mask": mask}
|
|
@staticmethod
|
|
def receive_ServerMapDone(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerClientInfo(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerSync(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerClientJoined(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerMapBegin(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerMapSize(source, data): return {"size": 0}
|
|
@staticmethod
|
|
def receive_ServerMapData(source, data): return {"data": data}
|
|
@staticmethod
|
|
def receive_ServerConfigurationUpdate(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerExternalChat(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerCommand(source, data):
|
|
company, data = read_uint8(data)
|
|
cmd, data = read_uint16(data)
|
|
error_msg, data = read_uint16(data)
|
|
tile, data = read_uint32(data)
|
|
payload_len, data = read_uint16(data)
|
|
payload = data[:payload_len]
|
|
data = data[payload_len:]
|
|
callback, data = read_uint8(data)
|
|
callback_param = 0
|
|
if callback != 0:
|
|
callback_param, data = read_uint32(data)
|
|
frame, data = read_uint32(data)
|
|
my_cmd, _ = read_uint8(data)
|
|
return {
|
|
"company": company, "cmd": cmd, "error_msg": error_msg, "tile": tile,
|
|
"payload": payload, "callback": callback, "callback_param": callback_param,
|
|
"frame": frame, "my_cmd": bool(my_cmd)
|
|
}
|
|
@staticmethod
|
|
def receive_ServerFull(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerBanned(source, data): return {}
|
|
@staticmethod
|
|
def receive_ClientAck(source, data):
|
|
f, data = read_uint32(data)
|
|
t, _ = read_uint8(data)
|
|
return {"frame": f, "token": t}
|
|
@staticmethod
|
|
def receive_ClientIdentify(source, data): return {}
|
|
|
|
class OpenTTDAdminProtocol(OpenTTDProtocol):
|
|
"""Low-level OpenTTD Admin TCP protocol handler."""
|
|
PacketType = PacketAdminType
|
|
PACKET_END = PacketAdminType.PACKET_END
|
|
|
|
@staticmethod
|
|
def receive_ServerProtocol(source, data):
|
|
v, data = read_uint8(data)
|
|
updates = {}
|
|
while True:
|
|
has_more, data = read_uint8(data)
|
|
if not has_more:
|
|
break
|
|
ut, data = read_uint16(data)
|
|
freqs, data = read_uint16(data)
|
|
updates[ut] = freqs
|
|
return {"version": v, "updates": updates}
|
|
|
|
@staticmethod
|
|
def receive_ServerWelcome(source, data):
|
|
name, data = read_string(data)
|
|
ver, data = read_string(data)
|
|
dedi, data = read_uint8(data)
|
|
map_name, data = read_string(data)
|
|
seed, data = read_uint32(data)
|
|
land, data = read_uint8(data)
|
|
date, data = read_uint32(data)
|
|
width, data = read_uint16(data)
|
|
height, _ = read_uint16(data)
|
|
return {
|
|
"server_name": name, "openttd_version": ver, "dedicated": bool(dedi),
|
|
"map_name": map_name, "generation_seed": seed, "landscape": land,
|
|
"start_date": date, "map_width": width, "map_height": height
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerDate(source, data):
|
|
d, _ = read_uint32(data)
|
|
return {"date": d}
|
|
|
|
@staticmethod
|
|
def receive_ServerChat(source, data):
|
|
action, data = read_uint8(data)
|
|
dest_type, data = read_uint8(data)
|
|
cid, data = read_uint32(data)
|
|
msg, data = read_string(data)
|
|
money = 0
|
|
if len(data) >= 8:
|
|
import struct
|
|
money = struct.unpack("<Q", data[:8])[0]
|
|
return {"action": action, "dest_type": dest_type, "client_id": cid, "message": msg, "money": money}
|
|
|
|
@staticmethod
|
|
def receive_ServerConsole(source, data):
|
|
origin, data = read_string(data)
|
|
text, _ = read_string(data)
|
|
return {"origin": origin, "text": text}
|
|
|
|
@staticmethod
|
|
def receive_ServerRcon(source, data):
|
|
color, data = read_uint16(data)
|
|
text, _ = read_string(data)
|
|
return {"color": color, "text": text}
|
|
|
|
@staticmethod
|
|
def receive_ServerRconEnd(source, data):
|
|
cmd, _ = read_string(data)
|
|
return {"command": cmd}
|
|
|
|
@staticmethod
|
|
def receive_ServerAuthRequest(source, data):
|
|
at, rest = read_uint8(data)
|
|
return {"auth_type": at, "data": rest}
|
|
|
|
@staticmethod
|
|
def receive_ServerEnableEncryption(source, data): return {"data": data}
|
|
@staticmethod
|
|
def receive_ServerError(source, data):
|
|
ec, _ = read_uint8(data)
|
|
return {"error_code": ec}
|
|
@staticmethod
|
|
def receive_ServerFull(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerBanned(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerShutdown(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerNewGame(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerClientJoin(source, data):
|
|
cid, _ = read_uint32(data)
|
|
return {"client_id": cid}
|
|
|
|
@staticmethod
|
|
def receive_ServerClientInfo(source, data):
|
|
cid, data = read_uint32(data)
|
|
addr, data = read_string(data)
|
|
name, data = read_string(data)
|
|
lang, data = read_uint8(data)
|
|
date, data = read_uint32(data)
|
|
playas, _ = read_uint8(data)
|
|
return {
|
|
"client_id": cid, "network_address": addr, "name": name,
|
|
"language": lang, "join_date": date, "play_as": playas
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerClientUpdate(source, data):
|
|
cid, data = read_uint32(data)
|
|
name, data = read_string(data)
|
|
playas, _ = read_uint8(data)
|
|
return {"client_id": cid, "name": name, "play_as": playas}
|
|
|
|
@staticmethod
|
|
def receive_ServerClientQuit(source, data):
|
|
cid, _ = read_uint32(data)
|
|
return {"client_id": cid}
|
|
|
|
@staticmethod
|
|
def receive_ServerClientError(source, data):
|
|
cid, data = read_uint32(data)
|
|
error, _ = read_uint8(data)
|
|
return {"client_id": cid, "error_code": error}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyNew(source, data):
|
|
cid, _ = read_uint8(data)
|
|
return {"company_id": cid}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyInfo(source, data):
|
|
cid, data = read_uint8(data)
|
|
name, data = read_string(data)
|
|
manager, data = read_string(data)
|
|
color, data = read_uint8(data)
|
|
protected, data = read_uint8(data)
|
|
year, data = read_uint32(data)
|
|
is_ai, _ = read_uint8(data)
|
|
return {
|
|
"company_id": cid, "name": name, "manager_name": manager,
|
|
"color": color, "password_protected": bool(protected),
|
|
"inaugurated_year": year, "is_ai": bool(is_ai)
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyUpdate(source, data):
|
|
cid, data = read_uint8(data)
|
|
name, data = read_string(data)
|
|
manager, data = read_string(data)
|
|
color, data = read_uint8(data)
|
|
protected, data = read_uint8(data)
|
|
bankrupt, data = read_uint8(data)
|
|
s1, data = read_uint8(data)
|
|
s2, data = read_uint8(data)
|
|
s3, data = read_uint8(data)
|
|
s4, _ = read_uint8(data)
|
|
return {
|
|
"company_id": cid, "name": name, "manager_name": manager,
|
|
"color": color, "password_protected": bool(protected),
|
|
"quarters_of_bankruptcy": bankrupt, "share_owners": [s1, s2, s3, s4]
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyRemove(source, data):
|
|
cid, data = read_uint8(data)
|
|
reason, _ = read_uint8(data)
|
|
return {"company_id": cid, "reason": reason}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyEconomy(source, data):
|
|
import struct
|
|
cid, data = read_uint8(data)
|
|
money = struct.unpack("<Q", data[:8])[0]
|
|
data = data[8:]
|
|
loan = struct.unpack("<Q", data[:8])[0]
|
|
data = data[8:]
|
|
income = struct.unpack("<q", data[:8])[0]
|
|
data = data[8:]
|
|
delivered, data = read_uint16(data)
|
|
val_lq = struct.unpack("<Q", data[:8])[0]
|
|
data = data[8:]
|
|
perf_lq, data = read_uint16(data)
|
|
del_lq, data = read_uint16(data)
|
|
val_pq = struct.unpack("<Q", data[:8])[0]
|
|
data = data[8:]
|
|
perf_pq, data = read_uint16(data)
|
|
del_pq, _ = read_uint16(data)
|
|
return {
|
|
"company_id": cid, "money": money, "loan": loan, "income": income,
|
|
"delivered_cargo": delivered, "value_last_quarter": val_lq,
|
|
"performance_last_quarter": perf_lq, "delivered_cargo_last_quarter": del_lq,
|
|
"value_previous_quarter": val_pq, "performance_previous_quarter": perf_pq,
|
|
"delivered_cargo_previous_quarter": del_pq
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyStats(source, data):
|
|
cid, data = read_uint8(data)
|
|
trains, data = read_uint16(data)
|
|
lorries, data = read_uint16(data)
|
|
buses, data = read_uint16(data)
|
|
planes, data = read_uint16(data)
|
|
ships, data = read_uint16(data)
|
|
t_stations, data = read_uint16(data)
|
|
l_stations, data = read_uint16(data)
|
|
b_stops, data = read_uint16(data)
|
|
airports, data = read_uint16(data)
|
|
harbours, _ = read_uint16(data)
|
|
return {
|
|
"company_id": cid,
|
|
"vehicles": {"trains": trains, "lorries": lorries, "buses": buses, "planes": planes, "ships": ships},
|
|
"stations": {"train": t_stations, "lorry": l_stations, "bus": b_stops, "airport": airports, "harbour": harbours}
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerGamescript(source, data):
|
|
json_str, _ = read_string(data)
|
|
import json
|
|
try:
|
|
return {"data": json.loads(json_str)}
|
|
except json.JSONDecodeError:
|
|
return {"raw_data": json_str}
|
|
|
|
@staticmethod
|
|
def receive_ServerPong(source, data):
|
|
payload, _ = read_uint32(data)
|
|
return {"payload": payload}
|
|
|
|
|