Initialized openttd-client repo
This commit is contained in:
179
lib/openttd/client.py
Normal file
179
lib/openttd/client.py
Normal file
@@ -0,0 +1,179 @@
|
||||
import asyncio
|
||||
import logging
|
||||
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
|
||||
|
||||
class OpenTTDClient:
|
||||
"""High-level OpenTTD client for easy integration."""
|
||||
def __init__(self, host, port=3979, username="GeminiUser"):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.unique_id = str(uuid.uuid4())
|
||||
self.log = logging.getLogger(f"OTTDS-{username}")
|
||||
|
||||
# State
|
||||
self.encryption_enabled = False
|
||||
self.joined = asyncio.Event()
|
||||
self.shutdown_event = asyncio.Event()
|
||||
self.client_id = None
|
||||
|
||||
# Internal crypto
|
||||
self._server_password = ""
|
||||
self._company_password = ""
|
||||
self._target_company = 255
|
||||
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
|
||||
|
||||
async def connect(self, server_password=""):
|
||||
"""Connect to the server and initiate handshake."""
|
||||
self._server_password = server_password
|
||||
self.log.info(f"Connecting to {self.host}:{self.port}...")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
self._transport, self._protocol = await loop.create_connection(
|
||||
lambda: OpenTTDProtocol(self), self.host, self.port
|
||||
)
|
||||
d = write_init(PacketGameType.ClientGameInfo)
|
||||
write_uint8(d, 4)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
except Exception as e:
|
||||
self.log.error(f"Connection failed: {e}")
|
||||
raise
|
||||
|
||||
async def join_company(self, company_id=255, company_password=""):
|
||||
"""Join a specific company (0-14, or 255 for spectator)."""
|
||||
self._target_company = company_id
|
||||
self._company_password = company_password
|
||||
if not self.joined.is_set():
|
||||
self.log.info(f"Join for company {company_id} configured.")
|
||||
else:
|
||||
self.log.warning("Already joined.")
|
||||
|
||||
def disconnect(self, source):
|
||||
"""Library callback for when connection is lost."""
|
||||
self.log.info("Disconnected.")
|
||||
self.shutdown_event.set()
|
||||
|
||||
async def quit(self):
|
||||
"""Gracefully disconnect from the server."""
|
||||
if hasattr(self, '_protocol') and not self._transport.is_closing():
|
||||
self.log.info("Quitting...")
|
||||
try:
|
||||
d = write_init(PacketGameType.ClientQuit)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
except Exception:
|
||||
pass
|
||||
self._transport.close()
|
||||
self.shutdown_event.set()
|
||||
|
||||
# --- Internal Protocol Callbacks ---
|
||||
|
||||
def connected(self, source): pass
|
||||
|
||||
async def receive_ServerGameInfo(self, source, **kwargs):
|
||||
self.log.info(f"Server Info: {kwargs.get('name')} ({kwargs.get('openttd_version')})")
|
||||
d = write_init(PacketGameType.ClientJoin)
|
||||
write_string(d, kwargs.get("openttd_version", "jgrpp-0.71.1"))
|
||||
write_uint32(d, 0x20006D64)
|
||||
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"}
|
||||
self.log.error(f"Server Error: {error_names.get(error_code, f'Code {error_code}')}")
|
||||
await self.quit()
|
||||
|
||||
async def receive_ServerAuthenticationRequest(self, source, auth_type, data):
|
||||
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)
|
||||
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)
|
||||
mac, ciphertext = monocypher.lock(self._session_key_send, nonce, challenge, associated_data=our_pub)
|
||||
d = write_init(PacketGameType.ClientAuthenticationResponse)
|
||||
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
|
||||
d = write_init(PacketGameType.ClientIdentify)
|
||||
write_string(d, self.username)
|
||||
write_uint8(d, self._target_company)
|
||||
write_uint8(d, 1)
|
||||
write_string(d, self.unique_id)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def receive_ServerCheckNewGRFs(self, source):
|
||||
d = write_init(PacketGameType.ClientNewGRFsChecked)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def receive_ServerNeedCompanyPassword(self, source, seed, server_id):
|
||||
if not self._company_password:
|
||||
self.log.error("Server needs company password but none provided.")
|
||||
return
|
||||
salted = bytearray()
|
||||
p_bytes, s_bytes = self._company_password.encode('utf-8'), server_id.encode('utf-8')
|
||||
for i in range(32):
|
||||
p_char = p_bytes[i] if i < len(p_bytes) else 0
|
||||
s_char = s_bytes[i] if i < len(s_bytes) else 0
|
||||
seed_char = (seed >> (i % 32)) & 0xFF
|
||||
salted.append(p_char ^ s_char ^ seed_char)
|
||||
hashed = hashlib.md5(salted, usedforsecurity=False).hexdigest()
|
||||
d = write_init(PacketGameType.ClientCompanyPassword)
|
||||
write_string(d, hashed)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def receive_ServerWelcome(self, source, **kwargs):
|
||||
self.client_id = kwargs.get('client_id')
|
||||
self.log.info(f"Successfully joined as client {self.client_id}")
|
||||
d = write_init(PacketGameType.ClientGetMap)
|
||||
write_uint8(d, 0)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def receive_ServerMapDone(self, source):
|
||||
d = write_init(PacketGameType.ClientMapOk)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
self.joined.set()
|
||||
|
||||
async def receive_ServerFrame(self, source, frame, token):
|
||||
d = write_init(PacketGameType.ClientAck)
|
||||
write_uint32(d, frame)
|
||||
write_uint8(d, token)
|
||||
await self._protocol.send_packet(write_presend(d, SEND_TCP_MTU))
|
||||
|
||||
async def receive_ServerChat(self, source, client_id, message, **kwargs):
|
||||
if self.on_chat:
|
||||
self.on_chat(client_id, message)
|
||||
else:
|
||||
self.log.info(f"CHAT: <{client_id}> {message}")
|
||||
|
||||
async def receive_ServerUnused(self, source, **kwargs): pass
|
||||
async def receive_ServerCompanyUpdate(self, source, **kwargs): pass
|
||||
async def receive_ServerClientInfo(self, source, **kwargs): pass
|
||||
async def receive_ServerSync(self, source, **kwargs): pass
|
||||
async def receive_ServerClientJoined(self, source, **kwargs): pass
|
||||
async def receive_ServerMapBegin(self, source, **kwargs): pass
|
||||
async def receive_ServerMapSize(self, source, **kwargs): pass
|
||||
async def receive_ServerMapData(self, source, **kwargs): pass
|
||||
async def receive_ServerConfigurationUpdate(self, source, **kwargs): pass
|
||||
async def receive_ServerExternalChat(self, source, **kwargs): pass
|
||||
async def receive_ServerCommand(self, source, **kwargs): pass
|
||||
async def receive_ServerFull(self, source, **kwargs): pass
|
||||
async def receive_ServerBanned(self, source, **kwargs): pass
|
||||
async def receive_ClientAck(self, source, **kwargs): pass
|
||||
async def receive_ClientIdentify(self, source, **kwargs): pass
|
||||
Reference in New Issue
Block a user