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 asyncio
import logging
from asyncio import Task from asyncio import Task
from enum import Enum from enum import Enum
from typing import Dict from typing import Dict, List, Callable, Type
from .dispatcher import Dispatcher, ConnectionState from .dispatcher import Dispatcher, ConnectionState
from .mc.mctypes import VarInt from .mc.mctypes import VarInt
from .mc.packet import Packet 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: if state == ConnectionState.HANDSHAKING:
return proto.handshaking.clientbound.REGISTRY return proto.handshaking.clientbound.REGISTRY
if state == ConnectionState.STATUS: if state == ConnectionState.STATUS:
@ -19,65 +22,185 @@ def _registry_from_state(state:ConnectionState) -> Dict[int, Dict[int, Packet]]:
return proto.login.clientbound.REGISTRY return proto.login.clientbound.REGISTRY
if state == ConnectionState.PLAY: if state == ConnectionState.PLAY:
return proto.play.clientbound.REGISTRY 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: class Client:
host:str host:str
port:int port:int
token:Token
proto : int
state : ConnectionState
dispatcher : Dispatcher dispatcher : Dispatcher
_processing : bool _processing : bool
_worker : Task _worker : Task
_packet_callbacks : Dict[ConnectionState, Dict[Packet, List[Callable]]]
def __init__( def __init__(
self, self,
username : str, token:Token,
password : str, host:str,
host: str = "localhost", port:int = 25565,
port: int = 25565,
): ):
self.host = host self.host = host
self.port = port 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.dispatcher = Dispatcher(host, port)
self._processing = False self._processing = False
self._packet_callbacks = {}
async def run(self): def on(self, hook):
await self.dispatcher.run() 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._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: while self._processing:
buffer = await self.dispatcher.incoming.get() 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))
packet_id = VarInt.deserialize(buffer) # Process packets? switch state, invoke callbacks? Maybe implement Reactors?
cls = _registry_from_state(self.state)[self.proto][packet_id] if self.dispatcher.state == ConnectionState.LOGIN:
packet = cls.deserialize(buffer) await self.login_logic(packet)
elif self.dispatcher.state == ConnectionState.PLAY:
await self.play_logic(packet)
# Process packets? switch state, invoke callbacks? Maybe implement Reactors? if self.dispatcher.state in self._packet_callbacks:
if self.state == ConnectionState.HANDSHAKING: if Packet in self._packet_callbacks[self.dispatcher.state]: # callback for any packet
self.handshaking_logic(packet) for cb in self._packet_callbacks[self.dispatcher.state][Packet]:
elif self.state == ConnectionState.LOGIN: await cb(packet)
self.login_logic(packet) if packet.__class__ in self._packet_callbacks[self.dispatcher.state]: # callback for this packet
elif self.state == ConnectionState.PLAY: for cb in self._packet_callbacks[self.dispatcher.state][packet.__class__]:
self.play_logic(packet) await cb(packet)
self.dispatcher.incoming.task_done()
async def handshaking_logic(self, packet:Packet): except asyncio.TimeoutError:
pass pass # need this to recheck self._processing periodically
except Exception:
async def status_logic(self, packet:Packet): logger.exception("Exception while processing packet %s", packet)
pass
async def login_logic(self, packet: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): async def play_logic(self, packet:Packet):
pass 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