From 392740017802fc029cc9cbf6fff8baaa19b1d7ad Mon Sep 17 00:00:00 2001 From: joo Date: Sat, 5 Mar 2016 07:28:14 +0000 Subject: [PATCH] Prepare to support multiple protocol versions. --- minecraft/__init__.py | 10 ++- minecraft/networking/connection.py | 40 +++++++--- minecraft/networking/packets.py | 119 ++++++++++++++++++----------- 3 files changed, 112 insertions(+), 57 deletions(-) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 78fe4a4..c8a5970 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -2,6 +2,10 @@ A modern, Python3-compatible, well-documented library for communicating with a MineCraft server. """ -__version__ = "0.0.1" -MINECRAFT_VERSION = "1.8.3" -PROTOCOL_VERSION = 47 + +__version__ = "0.1.0" + +SUPPORTED_PROTOCOL_VERSIONS = ( + 47, # Minecraft 1.8 - 1.8.9 +) + diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index fe47b13..3abf95c 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -1,4 +1,5 @@ from collections import deque +from collections import namedtuple from threading import Lock from zlib import decompress import threading @@ -10,8 +11,7 @@ import sys from .types import VarInt from . import packets from . import encryption -from .. import PROTOCOL_VERSION - +from .. import SUPPORTED_PROTOCOL_VERSIONS class _ConnectionOptions(object): def __init__(self, @@ -25,13 +25,22 @@ class _ConnectionOptions(object): self.compression_threshold = compression_threshold self.compression_enabled = compression_enabled +ConnectionContext = namedtuple('ConnectionContext', ( + 'protocol_version' +)) class Connection(object): """This class represents a connection to a minecraft server, it handles everything from connecting, sending packets to handling default network behaviour """ - def __init__(self, address, port, auth_token): + def __init__( + self, + address, + port, + auth_token, + protocol_version=max(SUPPORTED_PROTOCOL_VERSIONS) + ): """Sets up an instance of this object to be able to connect to a minecraft server. @@ -56,6 +65,10 @@ class Connection(object): self.options.port = port self.auth_token = auth_token + self.context = ConnectionContext( + protocol_version = protocol_version + ) + # The reactor handles all the default responses to packets, # it should be changed per networking state self.reactor = PacketReactor(self) @@ -76,6 +89,7 @@ class Connection(object): :param packet: The :class:`network.packets.Packet` to write :param force(bool): Specifies if the packet write should be immediate """ + packet.context = self.context if force: self._write_lock.acquire() if self.options.compression_enabled: @@ -149,7 +163,7 @@ class Connection(object): def _handshake(self, next_state=2): handshake = packets.HandShakePacket() - handshake.protocol_version = PROTOCOL_VERSION + handshake.protocol_version = self.context.protocol_version handshake.server_address = self.options.address handshake.server_port = self.options.port handshake.next_state = next_state @@ -210,11 +224,16 @@ class PacketReactor(object): Reads and reacts to packets """ state_name = None - clientbound_packets = None TIME_OUT = 0 + get_clientbound_packets = staticmethod(lambda context: set()) + def __init__(self, connection): self.connection = connection + context = self.connection.context + self.clientbound_packets = { + packet.get_id(context): packet + for packet in self.__class__.get_clientbound_packets(context)} def read_packet(self, stream): ready_to_read = select.select([self.connection.socket], [], [], @@ -247,10 +266,11 @@ class PacketReactor(object): # otherwise just skip it if packet_id in self.clientbound_packets: packet = self.clientbound_packets[packet_id]() + packet.context = self.connection.context packet.read(packet_data) return packet else: - return packets.Packet() + return packets.Packet(context=self.connection.context) else: return None @@ -259,7 +279,7 @@ class PacketReactor(object): class LoginReactor(PacketReactor): - clientbound_packets = packets.STATE_LOGIN_CLIENTBOUND + get_clientbound_packets = staticmethod(packets.state_login_clientbound) def react(self, packet): if packet.packet_name == "encryption request": @@ -307,7 +327,7 @@ class LoginReactor(PacketReactor): class PlayingReactor(PacketReactor): - clientbound_packets = packets.STATE_PLAYING_CLIENTBOUND + get_clientbound_packets = staticmethod(packets.state_playing_clientbound) def react(self, packet): if packet.packet_name == "set compression": @@ -337,10 +357,10 @@ class PlayingReactor(PacketReactor): ''' class StatusReactor(PacketReactor): - clientbound_packets = packets.STATE_STATUS_CLIENTBOUND + get_clientbound_packets = staticmethod(packets.state_status_clientbound) def react(self, packet): - if packet.id == packets.ResponsePacket.id: + if packet.id == packets.ResponsePacket.get_id(self.connection.context): import json print(json.loads(packet.json_response)) diff --git a/minecraft/networking/packets.py b/minecraft/networking/packets.py index da1e5d9..0ff0cb2 100644 --- a/minecraft/networking/packets.py +++ b/minecraft/networking/packets.py @@ -50,11 +50,34 @@ class PacketListener(object): class Packet(object): packet_name = "base" - id = -0x01 - definition = [] + id = None + definition = None - def __init__(self, **kwargs): - pass + # To define the packet ID, either: + # 1. Define the attribute `id', of type int, in a subclass; or + # 2. Override `get_id' in a subclass and return the correct packet ID + # for the given ConnectionContext. This is necessary if the packet ID + # has changed across protocol versions, for example. + @classmethod + def get_id(cls, context): + return cls.id + + # To define the network data layout of a packet, either: + # 1. Define the attribute `definition', a list of fields, each of which + # is a dict mapping attribute names to data types; or + # 2. Override `get_definition' in a subclass and return the correct + # definition for the given ConnectionContext. This may be necessary + # if the layout has changed across protocol versions, for example; or + # 3. Override the methods `read' and/or `write' in a subclass. This may be + # necessary if the packet layout cannot be described as a list of fields. + @classmethod + def get_definition(cls, context): + return cls.definition + + def __init__(self, context=None, **kwargs): + self.context = context + self.id = self.get_id(context) + self.definition = self.get_definition(context) def set_values(self, **kwargs): for key, value in kwargs.items(): @@ -112,13 +135,14 @@ class HandShakePacket(Packet): {'next_state': VarInt}] -STATE_HANDSHAKE_CLIENTBOUND = { - -} -STATE_HANDSHAKE_SERVERBOUND = { - 0x00: HandShakePacket -} - +def state_handshake_clientbound(context): + return { + + } +def state_handshake_serverbound(context): + return { + HandShakePacket + } # Status State # ============== @@ -136,10 +160,11 @@ class PingPacketResponse(Packet): {'time': Long}] -STATE_STATUS_CLIENTBOUND = { - 0x00: ResponsePacket, - 0x01: PingPacketResponse -} +def state_status_clientbound(context): + return { + ResponsePacket, + PingPacketResponse + } class RequestPacket(Packet): @@ -155,10 +180,11 @@ class PingPacket(Packet): {'time': Long}] -STATE_STATUS_SERVERBOUND = { - 0x00: RequestPacket, - 0x01: PingPacket -} +def state_status_serverbound(context): + return { + RequestPacket, + PingPacket + } # Login State @@ -194,12 +220,13 @@ class SetCompressionPacket(Packet): {'threshold': VarInt}] -STATE_LOGIN_CLIENTBOUND = { - 0x00: DisconnectPacket, - 0x01: EncryptionRequestPacket, - 0x02: LoginSuccessPacket, - 0x03: SetCompressionPacket -} +def state_login_clientbound(context): + return { + DisconnectPacket, + EncryptionRequestPacket, + LoginSuccessPacket, + SetCompressionPacket + } class LoginStartPacket(Packet): @@ -217,10 +244,11 @@ class EncryptionResponsePacket(Packet): {'verify_token': VarIntPrefixedByteArray}] -STATE_LOGIN_SERVERBOUND = { - 0x00: LoginStartPacket, - 0x01: EncryptionResponsePacket -} +def state_login_serverbound(context): + return { + LoginStartPacket, + EncryptionResponsePacket + } # Playing State @@ -496,16 +524,17 @@ class MapPacket(Packet): def __str__(self): return self.__repr__() -STATE_PLAYING_CLIENTBOUND = { - 0x00: KeepAlivePacket, - 0x01: JoinGamePacket, - 0x02: ChatMessagePacket, - 0x08: PlayerPositionAndLookPacket, - 0x34: MapPacket, - 0x38: PlayerListItemPacket, - 0x40: DisconnectPacketPlayState, - 0x46: SetCompressionPacketPlayState -} +def state_playing_clientbound(context): + return { + KeepAlivePacket, + JoinGamePacket, + ChatMessagePacket, + PlayerPositionAndLookPacket, + MapPacket, + PlayerListItemPacket, + DisconnectPacketPlayState, + SetCompressionPacketPlayState + } class ChatPacket(Packet): @@ -526,8 +555,10 @@ class PositionAndLookPacket(Packet): {'pitch': Float}, {'on_ground': Boolean}] -STATE_PLAYING_SERVERBOUND = { - 0x00: KeepAlivePacket, - 0x01: ChatPacket, - 0x06: PositionAndLookPacket -} + +def state_playing_serverbound(context): + return { + KeepAlivePacket, + ChatPacket, + PositionAndLookPacket + }