Add admin port support with other major refactorations
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from .client import OpenTTDClient
|
||||
from .decorators import exclude_call_check
|
||||
from .client import OpenTTDClient, OpenTTDAdminClient
|
||||
|
||||
__all__ = ['OpenTTDClient']
|
||||
__all__ = ['OpenTTDClient', 'OpenTTDAdminClient', 'exclude_call_check']
|
||||
|
||||
@@ -4,8 +4,9 @@ import uuid
|
||||
import monocypher
|
||||
import os
|
||||
import hashlib
|
||||
from openttd_protocol.wire.write import write_init, write_string, write_uint8, write_uint32, write_presend, SEND_TCP_MTU
|
||||
from .protocol import PacketGameType, OpenTTDProtocol
|
||||
from openttd_protocol.wire.write import write_init, write_string, write_uint8, write_uint16, write_uint32, write_presend, SEND_TCP_MTU
|
||||
from .protocol import PacketGameType, OpenTTDProtocol, PacketAdminType, OpenTTDAdminProtocol, NetworkAuthenticationMethod
|
||||
from .decorators import exclude_call_check
|
||||
|
||||
class OpenTTDClient:
|
||||
"""High-level OpenTTD client for easy integration."""
|
||||
@@ -80,6 +81,7 @@ class OpenTTDClient:
|
||||
|
||||
# --- Internal Protocol Callbacks ---
|
||||
|
||||
@exclude_call_check
|
||||
def connected(self, source): pass
|
||||
|
||||
async def receive_ServerGameInfo(self, source, **kwargs):
|
||||
@@ -90,7 +92,31 @@ class OpenTTDClient:
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def receive_ServerError(self, source, error_code):
|
||||
error_names = {8: "WrongRevision", 10: "WrongPassword", 11: "NameInUse", 17: "TimeoutComputer"}
|
||||
error_names = {
|
||||
0: "General",
|
||||
1: "Desync",
|
||||
2: "SavegameFailed",
|
||||
3: "ConnectionLost",
|
||||
4: "IllegalPacket",
|
||||
5: "NewGRFMismatch",
|
||||
6: "NotAuthorized",
|
||||
7: "NotExpected",
|
||||
8: "WrongRevision",
|
||||
9: "NameInUse",
|
||||
10: "WrongPassword",
|
||||
11: "CompanyMismatch",
|
||||
12: "Kicked",
|
||||
13: "Cheater",
|
||||
14: "ServerFull",
|
||||
15: "TooManyCommands",
|
||||
16: "TimeoutPassword",
|
||||
17: "TimeoutComputer",
|
||||
18: "TimeoutMap",
|
||||
19: "TimeoutJoin",
|
||||
20: "InvalidClientName",
|
||||
21: "NotOnAllowList",
|
||||
22: "NoAuthenticationMethodAvailable"
|
||||
}
|
||||
self.log.error(f"Server Error: {error_names.get(error_code, f'Code {error_code}')}")
|
||||
await self.quit()
|
||||
|
||||
@@ -98,8 +124,9 @@ class OpenTTDClient:
|
||||
if auth_type == 1:
|
||||
server_pub = bytes(data[:32])
|
||||
nonce = bytes(data[32:56])
|
||||
our_priv, our_pub = monocypher.generate_key_exchange_key_pair()
|
||||
shared_secret = monocypher.key_exchange(our_priv, server_pub)
|
||||
our_priv = monocypher.generate_key()
|
||||
our_pub = monocypher.x25519_public_key(our_priv)
|
||||
shared_secret = monocypher.x25519(our_priv, server_pub)
|
||||
derived = monocypher.blake2b(shared_secret + server_pub + our_pub + self._server_password.encode())
|
||||
self._session_key_send, self._session_key_recv = derived[:32], derived[32:64]
|
||||
challenge = os.urandom(8)
|
||||
@@ -177,3 +204,230 @@ class OpenTTDClient:
|
||||
async def receive_ServerBanned(self, source, **kwargs): pass
|
||||
async def receive_ClientAck(self, source, **kwargs): pass
|
||||
async def receive_ClientIdentify(self, source, **kwargs): pass
|
||||
|
||||
class OpenTTDAdminClient:
|
||||
"""High-level OpenTTD Admin client."""
|
||||
def __init__(self, host, port=3977, admin_name="GeminiAdmin"):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.admin_name = admin_name
|
||||
self.log = logging.getLogger(f"OTTDA-{admin_name}")
|
||||
|
||||
# State
|
||||
self.encryption_enabled = False
|
||||
self.joined = asyncio.Event()
|
||||
self.shutdown_event = asyncio.Event()
|
||||
|
||||
# Internal crypto
|
||||
self._admin_password = ""
|
||||
self._session_key_send = None
|
||||
self._session_key_recv = None
|
||||
self._encryption_nonce = None
|
||||
self._send_aead = None
|
||||
self._recv_aead = None
|
||||
|
||||
# Callbacks
|
||||
self.on_chat = None
|
||||
self.on_console = None
|
||||
self.on_gamescript = None
|
||||
|
||||
async def connect(self, admin_password="", secure=False):
|
||||
"""Connect to the admin port and initiate handshake."""
|
||||
self._admin_password = admin_password
|
||||
self.log.info(f"Connecting to admin {self.host}:{self.port}...")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
self._transport, self._protocol = await loop.create_connection(
|
||||
lambda: OpenTTDAdminProtocol(self), self.host, self.port
|
||||
)
|
||||
if secure:
|
||||
d = write_init(PacketAdminType.AdminJoinSecure)
|
||||
write_string(d, self.admin_name)
|
||||
write_string(d, "1.0")
|
||||
# Bitmask: 1 << NetworkAuthenticationMethod.X25519_PAKE
|
||||
write_uint16(d, 1 << NetworkAuthenticationMethod.X25519_PAKE)
|
||||
else:
|
||||
d = write_init(PacketAdminType.AdminJoin)
|
||||
write_string(d, self._admin_password)
|
||||
write_string(d, self.admin_name)
|
||||
write_string(d, "1.0")
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
except Exception as e:
|
||||
self.log.error(f"Admin connection failed: {e}")
|
||||
raise
|
||||
|
||||
def disconnect(self, source):
|
||||
"""Library callback for when connection is lost."""
|
||||
self.log.info("Admin disconnected.")
|
||||
self.shutdown_event.set()
|
||||
|
||||
async def quit(self):
|
||||
"""Gracefully disconnect from the server."""
|
||||
if hasattr(self, '_protocol') and not self._transport.is_closing():
|
||||
try:
|
||||
d = write_init(PacketAdminType.AdminQuit)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
except Exception:
|
||||
pass
|
||||
self._transport.close()
|
||||
self.shutdown_event.set()
|
||||
|
||||
async def send_rcon(self, command):
|
||||
"""Send an RCON command."""
|
||||
d = write_init(PacketAdminType.AdminRcon)
|
||||
write_string(d, command)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def send_chat(self, message, action=1, dest_type=0, dest_id=0):
|
||||
"""Send a chat message as admin. action=1 (CHAT), dest_type=0 (BROADCAST)."""
|
||||
d = write_init(PacketAdminType.AdminChat)
|
||||
write_uint8(d, action)
|
||||
write_uint8(d, dest_type)
|
||||
write_uint32(d, dest_id)
|
||||
write_string(d, message)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def update_frequency(self, update_type, frequency):
|
||||
"""Update the frequency of a certain piece of information."""
|
||||
d = write_init(PacketAdminType.AdminUpdateFrequency)
|
||||
write_uint16(d, update_type)
|
||||
write_uint16(d, frequency)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def poll(self, update_type, data=0xFFFFFFFF):
|
||||
"""Poll the server for certain updates."""
|
||||
d = write_init(PacketAdminType.AdminPoll)
|
||||
write_uint8(d, update_type)
|
||||
write_uint32(d, data)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def poll_clients(self, client_id=0xFFFFFFFF):
|
||||
"""Poll for client information."""
|
||||
from .protocol import AdminUpdateType
|
||||
await self.poll(AdminUpdateType.ClientInfo, client_id)
|
||||
|
||||
async def poll_companies(self, company_id=0xFFFFFFFF):
|
||||
"""Poll for company information."""
|
||||
from .protocol import AdminUpdateType
|
||||
await self.poll(AdminUpdateType.CompanyInfo, company_id)
|
||||
|
||||
async def poll_economy(self, company_id=0xFFFFFFFF):
|
||||
"""Poll for company economy information."""
|
||||
from .protocol import AdminUpdateType
|
||||
await self.poll(AdminUpdateType.CompanyEconomy, company_id)
|
||||
|
||||
async def poll_stats(self, company_id=0xFFFFFFFF):
|
||||
"""Poll for company statistics."""
|
||||
from .protocol import AdminUpdateType
|
||||
await self.poll(AdminUpdateType.CompanyStats, company_id)
|
||||
|
||||
async def send_gamescript(self, json_data):
|
||||
"""Send a JSON string to the GameScript."""
|
||||
import json
|
||||
d = write_init(PacketAdminType.AdminGamescript)
|
||||
write_string(d, json.dumps(json_data))
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
# --- Internal Protocol Callbacks ---
|
||||
|
||||
@exclude_call_check
|
||||
def connected(self, source): pass
|
||||
|
||||
async def receive_ServerAuthRequest(self, source, auth_type, data):
|
||||
if auth_type == 1: # X25519_PAKE
|
||||
server_pub = bytes(data[:32])
|
||||
nonce = bytes(data[32:56])
|
||||
our_priv = monocypher.generate_key()
|
||||
our_pub = monocypher.x25519_public_key(our_priv)
|
||||
shared_secret = monocypher.x25519(our_priv, server_pub)
|
||||
derived = monocypher.blake2b(shared_secret + server_pub + our_pub + self._admin_password.encode())
|
||||
self._session_key_send, self._session_key_recv = derived[:32], derived[32:64]
|
||||
challenge = os.urandom(8)
|
||||
mac, ciphertext = monocypher.lock(self._session_key_send, nonce, challenge, associated_data=our_pub)
|
||||
d = write_init(PacketAdminType.AdminAuthResponse)
|
||||
d.extend(our_pub + mac + ciphertext)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def receive_ServerEnableEncryption(self, source, data):
|
||||
self._encryption_nonce = bytes(data)
|
||||
self.encryption_enabled = True
|
||||
self.log.info("Admin encryption enabled.")
|
||||
|
||||
async def receive_ServerProtocol(self, source, version, updates):
|
||||
self.log.info(f"Admin Protocol version {version}")
|
||||
|
||||
async def receive_ServerWelcome(self, source, **kwargs):
|
||||
self.log.info(f"Admin welcomed to {kwargs.get('server_name')}")
|
||||
self.joined.set()
|
||||
|
||||
async def receive_ServerError(self, source, error_code):
|
||||
self.log.error(f"Admin Server Error: {error_code}")
|
||||
await self.quit()
|
||||
|
||||
async def receive_ServerChat(self, source, **kwargs):
|
||||
if self.on_chat:
|
||||
self.on_chat(**kwargs)
|
||||
else:
|
||||
self.log.info(f"ADMIN CHAT: <{kwargs.get('client_id')}> {kwargs.get('message')}")
|
||||
|
||||
async def receive_ServerConsole(self, source, **kwargs):
|
||||
if self.on_console:
|
||||
self.on_console(**kwargs)
|
||||
else:
|
||||
self.log.info(f"CONSOLE: [{kwargs.get('origin')}] {kwargs.get('text')}")
|
||||
|
||||
async def receive_ServerRcon(self, source, **kwargs):
|
||||
self.log.info(f"RCON: {kwargs.get('text')}")
|
||||
|
||||
async def receive_ServerRconEnd(self, source, **kwargs):
|
||||
self.log.info(f"RCON End: {kwargs.get('command')}")
|
||||
|
||||
async def receive_ServerClientJoin(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Client {kwargs.get('client_id')} joined.")
|
||||
|
||||
async def receive_ServerClientInfo(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Client Info: {kwargs.get('name')} (ID: {kwargs.get('client_id')}, IP: {kwargs.get('network_address')})")
|
||||
|
||||
async def receive_ServerClientUpdate(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Client {kwargs.get('client_id')} updated.")
|
||||
|
||||
async def receive_ServerClientQuit(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Client {kwargs.get('client_id')} quit.")
|
||||
|
||||
async def receive_ServerClientError(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Client {kwargs.get('client_id')} error: {kwargs.get('error_code')}")
|
||||
|
||||
async def receive_ServerCompanyNew(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Company {kwargs.get('company_id')} created.")
|
||||
|
||||
async def receive_ServerCompanyInfo(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Company Info: {kwargs.get('name')} (ID: {kwargs.get('company_id')})")
|
||||
|
||||
async def receive_ServerCompanyUpdate(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Company {kwargs.get('company_id')} updated.")
|
||||
|
||||
async def receive_ServerCompanyRemove(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Company {kwargs.get('company_id')} removed.")
|
||||
|
||||
async def receive_ServerCompanyEconomy(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Company {kwargs.get('company_id')} Economy: Money={kwargs.get('money')}, Loan={kwargs.get('loan')}")
|
||||
|
||||
async def receive_ServerCompanyStats(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Company {kwargs.get('company_id')} Stats: Vehicles={kwargs.get('vehicles')}, Stations={kwargs.get('stations')}")
|
||||
|
||||
async def receive_ServerGamescript(self, source, **kwargs):
|
||||
if self.on_gamescript:
|
||||
self.on_gamescript(kwargs.get('data'))
|
||||
else:
|
||||
self.log.info(f"GAMESCRIPT: {kwargs.get('data')}")
|
||||
|
||||
async def receive_ServerDate(self, source, **kwargs): pass
|
||||
async def receive_ServerFull(self, source, **kwargs): await self.quit()
|
||||
async def receive_ServerBanned(self, source, **kwargs): await self.quit()
|
||||
async def receive_ServerShutdown(self, source, **kwargs): await self.quit()
|
||||
async def receive_ServerNewGame(self, source, **kwargs): pass
|
||||
async def receive_ServerPong(self, source, **kwargs):
|
||||
self.log.info(f"Admin: Pong received: {kwargs.get('payload')}")
|
||||
|
||||
|
||||
|
||||
4
lib/openttd/decorators.py
Normal file
4
lib/openttd/decorators.py
Normal file
@@ -0,0 +1,4 @@
|
||||
def exclude_call_check(obj):
|
||||
"""Decorator to mark a class or method to be excluded from public calls verification checks."""
|
||||
obj.__exclude_call_check__ = True
|
||||
return obj
|
||||
@@ -49,6 +49,80 @@ class PacketGameType(IntEnum):
|
||||
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
|
||||
@@ -69,9 +143,6 @@ class OpenTTDProtocol(TCPProtocol):
|
||||
raise SocketClosed("Decryption failed")
|
||||
data = memoryview(struct.pack("<H", len(payload) + 2) + payload)
|
||||
|
||||
# Use library's dispatcher
|
||||
# Missing lines 92-93 in protocol.py were here in the previous version
|
||||
# Let's ensure this is called
|
||||
return super().receive_packet(source, data)
|
||||
except Exception:
|
||||
return PacketGameType.ServerUnused, {}
|
||||
@@ -84,7 +155,6 @@ class OpenTTDProtocol(TCPProtocol):
|
||||
mac, ciphertext = self.handler._send_aead.lock(payload.tobytes())
|
||||
data = struct.pack("<H", 18 + len(ciphertext)) + mac + ciphertext
|
||||
|
||||
# Coverage for protocol.py:92-93: original send logic
|
||||
await self._can_write.wait()
|
||||
if self.transport.is_closing():
|
||||
raise SocketClosed
|
||||
@@ -171,3 +241,227 @@ class OpenTTDProtocol(TCPProtocol):
|
||||
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:
|
||||
return {"raw_data": json_str}
|
||||
|
||||
@staticmethod
|
||||
def receive_ServerPong(source, data):
|
||||
payload, _ = read_uint32(data)
|
||||
return {"payload": payload}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user