cleaned and improved server implementation

This commit is contained in:
əlemi 2022-04-21 01:15:16 +02:00
parent 801231a868
commit 4ba6b67d75
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E

View file

@ -1,3 +1,5 @@
import re
import json
import asyncio import asyncio
import logging import logging
import uuid import uuid
@ -10,10 +12,11 @@ from enum import Enum
from typing import Dict, List, Callable, Coroutine, Type, Optional, Tuple, AsyncIterator from typing import Dict, List, Callable, Coroutine, Type, Optional, Tuple, AsyncIterator
from .dispatcher import Dispatcher from .dispatcher import Dispatcher
from .traits import CallbacksHolder, Runnable
from .mc.packet import Packet 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.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.handshaking.serverbound import PacketSetProtocol
from .mc.proto.play.serverbound import PacketKeepAlive as PacketKeepAliveResponse from .mc.proto.play.serverbound import PacketKeepAlive as PacketKeepAliveResponse
from .mc.proto.play.clientbound import PacketKeepAlive, PacketSetCompression, PacketKickDisconnect, PacketPosition, PacketLogin 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 from .util import encryption
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@dataclass @dataclass
@ -30,12 +34,10 @@ class ServerOptions:
online_mode : bool online_mode : bool
spawn_player : bool spawn_player : bool
poll_interval : float poll_interval : float
motd : str
max_players : int
class ServerEvent(Enum): class MinecraftServer:
CLIENT_CONNECTED = 0
CLIENT_DISCONNECTED = 1
class MinecraftServer(CallbacksHolder, Runnable):
host:str host:str
port:int port:int
options:ServerOptions options:ServerOptions
@ -45,15 +47,17 @@ class MinecraftServer(CallbacksHolder, Runnable):
_server : Server _server : Server
_worker : Task _worker : Task
_logger : logging.Logger logger : logging.Logger
def __init__( def __init__(
self, self,
host:str, host:str = "127.0.0.1",
port:int = 25565, port:int = 25565,
online_mode:bool = False, online_mode:bool = False,
spawn_player:bool = True, spawn_player:bool = True,
poll_interval:float = 1.0, poll_interval:float = 1.0,
motd:str = "aiocraft server",
max_players:int = 10,
): ):
super().__init__() super().__init__()
self.host = host self.host = host
@ -63,12 +67,14 @@ class MinecraftServer(CallbacksHolder, Runnable):
online_mode=online_mode, online_mode=online_mode,
spawn_player=spawn_player, spawn_player=spawn_player,
poll_interval=poll_interval, poll_interval=poll_interval,
motd=motd,
max_players=max_players,
) )
self._dispatcher_pool = [] self._dispatcher_pool = []
self._processing = False self._processing = False
self._logger = LOGGER.getChild(f"@({self.host}:{self.port})") self.logger = LOGGER.getChild(f"@({self.host}:{self.port})")
@property @property
def started(self) -> bool: def started(self) -> bool:
@ -78,24 +84,6 @@ class MinecraftServer(CallbacksHolder, Runnable):
def connected(self) -> int: def connected(self) -> int:
return len(self._dispatcher_pool) 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): async def start(self):
if self.started: if self.started:
return return
@ -105,35 +93,21 @@ class MinecraftServer(CallbacksHolder, Runnable):
self._processing = True self._processing = True
await self._server.start_serving() await self._server.start_serving()
self._logger.info("Minecraft server started") self.logger.info("Minecraft server started")
async def stop(self, force:bool = False): async def stop(self, force:bool = False):
self._processing = False self._processing = False
self._server.close() self._server.close()
await asyncio.gather(*[d.disconnect(block=not force) for d in self._dispatcher_pool]) await asyncio.gather(*[d.disconnect(block=not force) for d in self._dispatcher_pool])
if not force: if not force:
await asyncio.gather( await self._server.wait_closed()
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"))
async def _server_worker(self, reader:StreamReader, writer:StreamWriter): 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._dispatcher_pool.append(dispatcher)
self._logger.debug("Starting dispatcher for client") self.logger.debug("Starting dispatcher for client")
await dispatcher.connect( await dispatcher.connect(reader=reader, writer=writer)
host=self.host,
port=self.port,
reader=reader,
writer=writer,
)
await self._handshake(dispatcher) await self._handshake(dispatcher)
if dispatcher.state == ConnectionState.STATUS: if dispatcher.state == ConnectionState.STATUS:
@ -143,33 +117,75 @@ class MinecraftServer(CallbacksHolder, Runnable):
await self._play(dispatcher) await self._play(dispatcher)
if dispatcher.connected: 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() await dispatcher.disconnect()
async def _handshake(self, dispatcher:Dispatcher) -> bool: # TODO make this fancier! poll for version and status first async def _handshake(self, dispatcher:Dispatcher) -> bool:
self._logger.info("Awaiting handshake") self.logger.info("Awaiting handshake")
async for packet in dispatcher.packets(): async for packet in dispatcher.packets():
if isinstance(packet, PacketSetProtocol): if isinstance(packet, PacketSetProtocol):
self._logger.info("Received set protocol packet") self.logger.info("Received set protocol packet")
dispatcher.proto = packet.protocolVersion dispatcher.proto = packet.protocolVersion
if packet.nextState == 1: if packet.nextState == 1:
self._logger.debug("Changing state to STATUS") self.logger.debug("Changing state to STATUS")
dispatcher.state = ConnectionState.STATUS dispatcher.state = ConnectionState.STATUS
return True return True
elif packet.nextState == 2: elif packet.nextState == 2:
self._logger.debug("Changing state to LOGIN") self.logger.debug("Changing state to LOGIN")
dispatcher.state = ConnectionState.LOGIN dispatcher.state = ConnectionState.LOGIN
return True return True
return False return False
async def _status(self, dispatcher:Dispatcher) -> bool: async def _status(self, dispatcher:Dispatcher) -> bool:
self._logger.info("Answering ping") self.logger.info("Answering ping")
async for packet in dispatcher.packets(): 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", "<br/>"),
},
"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 return False
async def _login(self, dispatcher:Dispatcher) -> bool: 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(): async for packet in dispatcher.packets():
if isinstance(packet, PacketLoginStart): if isinstance(packet, PacketLoginStart):
if self.options.online_mode: if self.options.online_mode:
@ -199,7 +215,7 @@ class MinecraftServer(CallbacksHolder, Runnable):
return False return False
async def _play(self, dispatcher:Dispatcher) -> bool: async def _play(self, dispatcher:Dispatcher) -> bool:
self._logger.info("Player connected") self.logger.info("Player connected")
if self.options.spawn_player: if self.options.spawn_player:
await dispatcher.write( await dispatcher.write(
@ -239,14 +255,9 @@ class MinecraftServer(CallbacksHolder, Runnable):
) )
) )
self.run_callbacks(ServerEvent.CLIENT_CONNECTED)
async for packet in dispatcher.packets(): async for packet in dispatcher.packets():
# TODO handle play # TODO handle play
self.run_callbacks(Packet, packet) pass
self.run_callbacks(type(packet), packet)
self.run_callbacks(ServerEvent.CLIENT_DISCONNECTED)
return False return False