diff --git a/src/aiocraft/server.py b/src/aiocraft/server.py index bbc7312..f610c77 100644 --- a/src/aiocraft/server.py +++ b/src/aiocraft/server.py @@ -1,3 +1,5 @@ +import re +import json import asyncio import logging import uuid @@ -10,10 +12,11 @@ from enum import Enum from typing import Dict, List, Callable, Coroutine, Type, Optional, Tuple, AsyncIterator from .dispatcher import Dispatcher -from .traits import CallbacksHolder, Runnable from .mc.packet import Packet -from .mc.token import Token, AuthException +from .mc.auth import AuthException, AuthInterface 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, PacketPosition, PacketLogin @@ -23,6 +26,7 @@ from .mc.proto.login.clientbound import ( ) from .util import encryption +REMOVE_COLOR_FORMATS = re.compile(r"ยง[0-9a-z]") LOGGER = logging.getLogger(__name__) @dataclass @@ -30,12 +34,10 @@ class ServerOptions: online_mode : bool spawn_player : bool poll_interval : float + motd : str + max_players : int -class ServerEvent(Enum): - CLIENT_CONNECTED = 0 - CLIENT_DISCONNECTED = 1 - -class MinecraftServer(CallbacksHolder, Runnable): +class MinecraftServer: host:str port:int options:ServerOptions @@ -45,15 +47,17 @@ class MinecraftServer(CallbacksHolder, Runnable): _server : Server _worker : Task - _logger : logging.Logger + logger : logging.Logger def __init__( self, - host:str, + host:str = "127.0.0.1", port:int = 25565, online_mode:bool = False, spawn_player:bool = True, poll_interval:float = 1.0, + motd:str = "aiocraft server", + max_players:int = 10, ): super().__init__() self.host = host @@ -63,12 +67,14 @@ class MinecraftServer(CallbacksHolder, Runnable): online_mode=online_mode, spawn_player=spawn_player, poll_interval=poll_interval, + motd=motd, + max_players=max_players, ) self._dispatcher_pool = [] self._processing = False - self._logger = LOGGER.getChild(f"@({self.host}:{self.port})") + self.logger = LOGGER.getChild(f"@({self.host}:{self.port})") @property def started(self) -> bool: @@ -78,24 +84,6 @@ class MinecraftServer(CallbacksHolder, Runnable): def connected(self) -> int: return len(self._dispatcher_pool) - def on_connect(self): - def wrapper(fun): - self.register(ServerEvent.CLIENT_CONNECTED, fun) - return fun - return wrapper - - def on_disconnect(self): - def wrapper(fun): - self.register(ServerEvent.CLIENT_DISCONNECTED, fun) - return fun - return wrapper - - def on_packet(self, packet:Type[Packet]): - def wrapper(fun): - self.register(packet, fun) - return fun - return wrapper - async def start(self): if self.started: return @@ -105,35 +93,21 @@ class MinecraftServer(CallbacksHolder, Runnable): self._processing = True await self._server.start_serving() - self._logger.info("Minecraft server started") + self.logger.info("Minecraft server started") async def stop(self, force:bool = False): self._processing = False self._server.close() await asyncio.gather(*[d.disconnect(block=not force) for d in self._dispatcher_pool]) if not force: - await asyncio.gather( - self._server.wait_closed(), - self.join_callbacks(), - ) - - async def _disconnect_client(self, dispatcher): - if dispatcher.state == ConnectionState.LOGIN: - await dispatcher.write(PacketDisconnect(dispatcher.proto, reason="Connection terminated")) - else: - await dispatcher.write(PacketKickDisconnect(dispatcher.proto, reason="Connection terminated")) + await self._server.wait_closed() async def _server_worker(self, reader:StreamReader, writer:StreamWriter): - dispatcher = Dispatcher(server=True) + dispatcher = Dispatcher(server=True).set_host(self.host, self.port) self._dispatcher_pool.append(dispatcher) - self._logger.debug("Starting dispatcher for client") - await dispatcher.connect( - host=self.host, - port=self.port, - reader=reader, - writer=writer, - ) + self.logger.debug("Starting dispatcher for client") + await dispatcher.connect(reader=reader, writer=writer) await self._handshake(dispatcher) if dispatcher.state == ConnectionState.STATUS: @@ -143,33 +117,75 @@ class MinecraftServer(CallbacksHolder, Runnable): await self._play(dispatcher) if dispatcher.connected: - await self._disconnect_client(dispatcher) + await dispatcher.write( + PacketKickDisconnect(dispatcher.proto, reason="Connection terminated") + if dispatcher.state == ConnectionState.PLAY else + PacketDisconnect(dispatcher.proto, reason="Connection terminated") + ) await dispatcher.disconnect() - async def _handshake(self, dispatcher:Dispatcher) -> bool: # TODO make this fancier! poll for version and status first - self._logger.info("Awaiting handshake") + async def _handshake(self, dispatcher:Dispatcher) -> bool: + self.logger.info("Awaiting handshake") async for packet in dispatcher.packets(): if isinstance(packet, PacketSetProtocol): - self._logger.info("Received set protocol packet") + self.logger.info("Received set protocol packet") dispatcher.proto = packet.protocolVersion if packet.nextState == 1: - self._logger.debug("Changing state to STATUS") + self.logger.debug("Changing state to STATUS") dispatcher.state = ConnectionState.STATUS return True elif packet.nextState == 2: - self._logger.debug("Changing state to LOGIN") + self.logger.debug("Changing state to LOGIN") dispatcher.state = ConnectionState.LOGIN return True return False async def _status(self, dispatcher:Dispatcher) -> bool: - self._logger.info("Answering ping") + self.logger.info("Answering ping") async for packet in dispatcher.packets(): - pass # TODO handle status! + if isinstance(packet, PacketPingStart): + await dispatcher.write( + PacketServerInfo( + dispatcher.proto, + response=json.dumps({ + "online": True, + "ip": self.host, + "port": self.port, + "motd": { + "raw": self.options.motd.split("\n"), + "clean": REMOVE_COLOR_FORMATS.sub("", self.options.motd).split("\n"), + "html": self.options.motd.replace("\n", "
"), + }, + "players": { + "online": len(self._dispatcher_pool), + "max": self.options.max_players, + # "list": [ + # "notch", + # ], + # "uuid": { + # "notch": "xxx-xxx...", + # } + }, + "version": "many", # TODO proto number to string + "protocol": dispatcher.proto, + # "hostname": "server.mymcserver.tld", # TODO get from config? + # "icon": "data:image\/png;base64,iVBORw0KGgoAAAANSUhEU...dSk6AAAAAElFTkSuQmCC", + "software": "aiocraft", + "map": "null", + # "info": { # TODO overrules default player names list + # "raw": [], + # "clean": [], + # "html": [] + # } + }) + ) + ) + elif isinstance(packet, PacketPing): + await dispatcher.write(PacketPong(dispatcher.proto, packet.time)) return False async def _login(self, dispatcher:Dispatcher) -> bool: - self._logger.info("Logging in player") + self.logger.info("Logging in player") async for packet in dispatcher.packets(): if isinstance(packet, PacketLoginStart): if self.options.online_mode: @@ -199,7 +215,7 @@ class MinecraftServer(CallbacksHolder, Runnable): return False async def _play(self, dispatcher:Dispatcher) -> bool: - self._logger.info("Player connected") + self.logger.info("Player connected") if self.options.spawn_player: await dispatcher.write( @@ -239,14 +255,9 @@ class MinecraftServer(CallbacksHolder, Runnable): ) ) - self.run_callbacks(ServerEvent.CLIENT_CONNECTED) - async for packet in dispatcher.packets(): # TODO handle play - self.run_callbacks(Packet, packet) - self.run_callbacks(type(packet), packet) - - self.run_callbacks(ServerEvent.CLIENT_DISCONNECTED) + pass return False