implemented core game logic

This commit is contained in:
əlemi 2021-11-10 18:56:14 +01:00
parent ba3c0e14e9
commit 8e68f70e1d

View file

@ -1,16 +1,19 @@
import asyncio
import logging
from asyncio import Task
from enum import Enum
from typing import Dict
from typing import Dict, List, Callable, Type
from .dispatcher import Dispatcher, ConnectionState
from .mc.mctypes import VarInt
from .mc.packet import Packet
from .mc import proto
from .mc.identity import Token
from .mc import proto, encryption
logger = logging.getLogger(__name__)
def _registry_from_state(state:ConnectionState) -> Dict[int, Dict[int, Packet]]:
def _registry_from_state(state:ConnectionState) -> Dict[int, Dict[int, Type[Packet]]]:
if state == ConnectionState.HANDSHAKING:
return proto.handshaking.clientbound.REGISTRY
if state == ConnectionState.STATUS:
@ -19,65 +22,185 @@ def _registry_from_state(state:ConnectionState) -> Dict[int, Dict[int, Packet]]:
return proto.login.clientbound.REGISTRY
if state == ConnectionState.PLAY:
return proto.play.clientbound.REGISTRY
return {}
_STATE_REGS = {
ConnectionState.HANDSHAKING : proto.handshaking,
ConnectionState.STATUS : proto.status,
ConnectionState.LOGIN : proto.login,
ConnectionState.PLAY : proto.play,
}
class Client:
host:str
port:int
proto : int
state : ConnectionState
token:Token
dispatcher : Dispatcher
_processing : bool
_worker : Task
_packet_callbacks : Dict[ConnectionState, Dict[Packet, List[Callable]]]
def __init__(
self,
username : str,
password : str,
host: str = "localhost",
port: int = 25565,
token:Token,
host:str,
port:int = 25565,
):
self.host = host
self.port = port
self.token = token
self.proto = 340 # TODO default to max available proto
self.state = ConnectionState.HANDSHAKING
self.dispatcher = Dispatcher(host, port)
self._processing = False
self._packet_callbacks = {}
async def run(self):
await self.dispatcher.run()
def on(self, hook):
def wrapper(fun):
pass # TODO
return wrapper
def on_packet(self, packet:Type[Packet], state:ConnectionState) -> Callable:
def wrapper(fun):
if state not in self._packet_callbacks:
self._packet_callbacks[state] = {}
if packet not in self._packet_callbacks[state]:
self._packet_callbacks[state][packet] = []
self._packet_callbacks[state][packet].append(fun)
return fun
return wrapper
async def start(self):
await self.dispatcher.start()
self._processing = True
self._worker = asyncio.get_event_loop().create_task(self._logic_worker())
await self.dispatcher.write(
proto.handshaking.serverbound.PacketSetProtocol(
340, # TODO!!!!
protocolVersion=340,
serverHost=self.host,
serverPort=self.port,
nextState=2, # play
)
)
async def _logic_worker(self):
await self.dispatcher.write( # TODO PROTO!!!!
proto.login.serverbound.PacketLoginStart(340, username=self.token.profile.name)
)
self.dispatcher.state = ConnectionState.LOGIN
self._processing = True
self._worker = asyncio.get_event_loop().create_task(self._client_worker())
async def stop(self, block=True):
await self.dispatcher.stop()
self._processing = False
if block:
await self._worker
async def _client_worker(self):
while self._processing:
buffer = await self.dispatcher.incoming.get()
packet_id = VarInt.deserialize(buffer)
cls = _registry_from_state(self.state)[self.proto][packet_id]
packet = cls.deserialize(buffer)
try:
# logger.info("Awaiting packet")
packet = await asyncio.wait_for(self.dispatcher.incoming.get(), timeout=5)
# logger.info("Client processing packet %s [state %s]", str(packet), str(self.dispatcher.state))
# Process packets? switch state, invoke callbacks? Maybe implement Reactors?
if self.state == ConnectionState.HANDSHAKING:
self.handshaking_logic(packet)
elif self.state == ConnectionState.LOGIN:
self.login_logic(packet)
elif self.state == ConnectionState.PLAY:
self.play_logic(packet)
if self.dispatcher.state == ConnectionState.LOGIN:
await self.login_logic(packet)
elif self.dispatcher.state == ConnectionState.PLAY:
await self.play_logic(packet)
async def handshaking_logic(self, packet:Packet):
pass
async def status_logic(self, packet:Packet):
pass
if self.dispatcher.state in self._packet_callbacks:
if Packet in self._packet_callbacks[self.dispatcher.state]: # callback for any packet
for cb in self._packet_callbacks[self.dispatcher.state][Packet]:
await cb(packet)
if packet.__class__ in self._packet_callbacks[self.dispatcher.state]: # callback for this packet
for cb in self._packet_callbacks[self.dispatcher.state][packet.__class__]:
await cb(packet)
self.dispatcher.incoming.task_done()
except asyncio.TimeoutError:
pass # need this to recheck self._processing periodically
except Exception:
logger.exception("Exception while processing packet %s", packet)
async def login_logic(self, packet:Packet):
pass
if isinstance(packet, proto.login.clientbound.PacketEncryptionBegin):
logger.info("Encryption request")
secret = encryption.generate_shared_secret()
token, encrypted_secret = encryption.encrypt_token_and_secret(
packet.publicKey,
packet.verifyToken,
secret
)
if packet.serverId != '-' and self.token:
await self.token.join(
encryption.generate_verification_hash(
packet.serverId,
secret,
packet.publicKey
)
)
encryption_response = proto.login.serverbound.PacketEncryptionBegin(
340, # TODO!!!!
sharedSecret=encrypted_secret,
verifyToken=token
)
await self.dispatcher.write(encryption_response, wait=True)
await self.dispatcher.encrypt(secret)
elif isinstance(packet, proto.login.clientbound.PacketDisconnect):
logger.error("Disconnected while logging in")
await self.stop(False)
# raise Exception("Disconnected while logging in") # TODO make a more specific one, do some shit
elif isinstance(packet, proto.login.clientbound.PacketCompress):
logger.info("Set compression")
self.dispatcher.compression = packet.threshold
elif isinstance(packet, proto.login.clientbound.PacketSuccess):
logger.info("Login success")
self.dispatcher.state = ConnectionState.PLAY
elif isinstance(packet, proto.login.clientbound.PacketLoginPluginRequest):
pass # TODO ?
async def play_logic(self, packet:Packet):
if isinstance(packet, proto.play.clientbound.PacketSetCompression):
logger.info("Set compression")
self.dispatcher.compression = packet.threshold
elif isinstance(packet, proto.play.clientbound.PacketKeepAlive):
logger.info("Keep Alive")
keep_alive_packet = proto.play.serverbound.packet_keep_alive.PacketKeepAlive(340, keepAliveId=packet.keepAliveId)
await self.dispatcher.write(keep_alive_packet)
elif isinstance(packet, proto.play.clientbound.PacketPosition):
logger.info("PlayerPosLook")
# if self.connection.context.protocol_later_eq(107):
# teleport_confirm = serverbound.play.TeleportConfirmPacket()
# teleport_confirm.teleport_id = packet.teleport_id
# self.connection.write_packet(teleport_confirm)
# else:
# position_response = serverbound.play.PositionAndLookPacket()
# position_response.x = packet.x
# position_response.feet_y = packet.y
# position_response.z = packet.z
# position_response.yaw = packet.yaw
# position_response.pitch = packet.pitch
# position_response.on_ground = True
# self.connection.write_packet(position_response)
# self.connection.spawned = True
pass
elif isinstance(packet, proto.play.clientbound.PacketKickDisconnect):
logger.info("Play Disconnect")
raise Exception("Disconnected while playing") # TODO make a more specific one, do some shit