diff --git a/src/aiocraft/client.py b/src/aiocraft/client.py index 8fe43b5..425b2d2 100644 --- a/src/aiocraft/client.py +++ b/src/aiocraft/client.py @@ -6,14 +6,17 @@ import uuid from dataclasses import dataclass from asyncio import Task from enum import Enum +from time import time -from typing import Dict, List, Callable, Type, Optional, Tuple, AsyncIterator +from typing import Dict, List, Callable, Type, Optional, Tuple, AsyncIterator, Any from .dispatcher import Dispatcher from .traits import CallbacksHolder, Runnable from .mc.packet import Packet from .mc.auth import AuthInterface, AuthException, MojangToken, MicrosoftAuthenticator from .mc.definitions import Dimension, Difficulty, Gamemode, ConnectionState +from .mc.proto.status.serverbound import PacketPing, PacketPingStart +from .mc.proto.status.clientbound import PacketServerInfo, PacketPing as PacketPong from .mc.proto.handshaking.serverbound import PacketSetProtocol from .mc.proto.play.serverbound import PacketKeepAlive as PacketKeepAliveResponse from .mc.proto.play.clientbound import PacketKeepAlive, PacketSetCompression, PacketKickDisconnect @@ -44,7 +47,7 @@ class MinecraftClient(CallbacksHolder, Runnable): _authenticator:MicrosoftAuthenticator _username:str - code:str + code:Optional[str] dispatcher : Dispatcher _processing : bool @@ -169,7 +172,65 @@ class MinecraftClient(CallbacksHolder, Runnable): await self.join_callbacks() await super().stop(force) + async def info(self, host:str="", port:int=0, ping:bool=False) -> Dict[str, Any]: + """Make a mini connection to asses server status and version""" + await self.dispatcher.connect( + host or self.host, + port or self.port, + ) + #Handshake + await self.dispatcher.write( + PacketSetProtocol( + self.dispatcher.proto, + protocolVersion=self.dispatcher.proto, + serverHost=host or self.host, + serverPort=port or self.port, + nextState=ConnectionState.STATUS.value, + ) + ) + self.dispatcher.state = ConnectionState.STATUS + #Request + await self.dispatcher.write( + PacketPingStart(self.dispatcher.proto) #empty packet + ) + #Response + data : Dict[str, Any] = {} + ping_id : int = 0 + ping_time : float = 0 + async for packet in self.dispatcher.packets(): + if isinstance(packet, PacketServerInfo): + data = json.loads(packet.response) + self._logger.debug("Server data : %s", json.dumps(data, indent=2)) + if not ping: + break + ping_id = int(time()) + ping_time = time() + await self.dispatcher.write( + PacketPing( + self.dispatcher.proto, + time=ping_id, + ) + ) + if isinstance(packet, PacketPong): + if packet.time == ping_id: + data['ping'] = int(time() - ping_time) + break + await self.dispatcher.disconnect() + return data + async def _client_worker(self): + try: + self._logger.info("Pinging server") + server_data = await self.info() + self._logger.info( + "Connecting to: %s (%d/%d)", + server_data['version']['name'], + server_data['players']['online'], + server_data['players']['max'] + ) + except Exception: + self._logger.exception("Exception checking server stats") + return while self._processing: if self.online_mode: try: @@ -183,11 +244,13 @@ class MinecraftClient(CallbacksHolder, Runnable): try: packet_whitelist = self.callback_keys(filter=Packet) if self.options.use_packet_whitelist else set() await self.dispatcher.connect( - self.host, - self.port, + host=self.host, + port=self.port, + proto=server_data['version']['protocol'], queue_timeout=self.options.poll_interval, packet_whitelist=packet_whitelist ) + self.dispatcher.proto = server_data['version']['protocol'] # TODO maybe check if it's supported? await self._handshake() if await self._login(): await self._play() @@ -209,8 +272,8 @@ class MinecraftClient(CallbacksHolder, Runnable): async def _handshake(self) -> bool: # TODO make this fancier! poll for version and status first await self.dispatcher.write( PacketSetProtocol( - 340, # TODO!!!! - protocolVersion=340, + self.dispatcher.proto, + protocolVersion=self.dispatcher.proto, serverHost=self.host, serverPort=self.port, nextState=2, # play @@ -218,7 +281,7 @@ class MinecraftClient(CallbacksHolder, Runnable): ) await self.dispatcher.write( PacketLoginStart( - 340, + self.dispatcher.proto, username=self._authenticator.selectedProfile.name if self.online_mode else self._username ) ) @@ -256,7 +319,7 @@ class MinecraftClient(CallbacksHolder, Runnable): else: self._logger.warning("Server gave an offline-mode serverId but still requested Encryption") encryption_response = PacketEncryptionResponse( - 340, # TODO!!!! + self.dispatcher.proto, sharedSecret=encrypted_secret, verifyToken=token ) diff --git a/src/aiocraft/dispatcher.py b/src/aiocraft/dispatcher.py index 7905e81..cde8ed4 100644 --- a/src/aiocraft/dispatcher.py +++ b/src/aiocraft/dispatcher.py @@ -42,8 +42,8 @@ class Dispatcher: _packet_whitelist : Set[Packet] _packet_id_whitelist : Set[int] - _host : str - _port : int + host : str + port : int proto : int state : ConnectionState @@ -53,9 +53,10 @@ class Dispatcher: _logger : logging.Logger def __init__(self, server:bool = False): + self.proto = 757 self._is_server = server - self._host = "localhost" - self._port = 25565 + self.host = "localhost" + self.port = 25565 self._prepare() @property @@ -93,13 +94,15 @@ class Dispatcher: def _prepare(self, host:Optional[str] = None, port:Optional[int] = None, + proto:Optional[int] = None, queue_timeout:int = 1, queue_size:int = 100, packet_whitelist : List[Packet] = None ): - self._host = host or self._host or "localhost" - self._port = port or self._port or 25565 - self._logger = LOGGER.getChild(f"on({self._host}:{self._port})") + self.proto = proto or self.proto or 757 # TODO not hardcode this? + self.host = host or self.host or "localhost" + self.port = port or self.port or 25565 + self._logger = LOGGER.getChild(f"on({self.host}:{self.port})") self._packet_whitelist = set(packet_whitelist) if packet_whitelist else set() if self._packet_whitelist: self._packet_whitelist.add(minecraft_protocol.play.clientbound.PacketKeepAlive) @@ -108,7 +111,6 @@ class Dispatcher: self.encryption = False self.compression = None self.state = ConnectionState.HANDSHAKING - self.proto = 340 # TODO # This can only happen after we know the connection protocol self._packet_id_whitelist = set((P(self.proto).id for P in self._packet_whitelist)) if self._packet_whitelist else set() @@ -123,6 +125,7 @@ class Dispatcher: async def connect(self, host : Optional[str] = None, port : Optional[int] = None, + proto : Optional[int] = None, reader : Optional[StreamReader] = None, writer : Optional[StreamWriter] = None, queue_timeout : int = 1, @@ -132,14 +135,14 @@ class Dispatcher: if self.connected: raise InvalidState("Dispatcher already connected") - self._prepare(host, port, queue_timeout, queue_size, packet_whitelist) + self._prepare(host, port, proto, queue_timeout, queue_size, packet_whitelist) if reader and writer: self._down, self._up = reader, writer else: self._down, self._up = await asyncio.open_connection( - host=self._host, - port=self._port, + host=self.host, + port=self.port, ) self._dispatching = True diff --git a/src/aiocraft/mc/auth/microsoft.py b/src/aiocraft/mc/auth/microsoft.py index 8ec3b12..06c5353 100644 --- a/src/aiocraft/mc/auth/microsoft.py +++ b/src/aiocraft/mc/auth/microsoft.py @@ -85,7 +85,7 @@ class MicrosoftAuthenticator(AuthInterface): async def validate(self): prof = await self.fetch_profile() self.selectedProfile = GameProfile(id=prof['id'], name=prof['name']) - logging.info("Session validated : %s", self.selectedProfile) + logging.info("Session validated : %s", repr(self.selectedProfile)) async def authenticate(self, code:str="") -> str: """Authorize Microsoft account"""