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 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", "<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
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