cleaned and improved server implementation
This commit is contained in:
parent
801231a868
commit
4ba6b67d75
1 changed files with 74 additions and 63 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue