Prepare to support multiple protocol versions.

This commit is contained in:
joo 2016-03-05 07:28:14 +00:00
parent 9affeee041
commit 3927400178
3 changed files with 112 additions and 57 deletions

View File

@ -2,6 +2,10 @@
A modern, Python3-compatible, well-documented library for communicating A modern, Python3-compatible, well-documented library for communicating
with a MineCraft server. with a MineCraft server.
""" """
__version__ = "0.0.1"
MINECRAFT_VERSION = "1.8.3" __version__ = "0.1.0"
PROTOCOL_VERSION = 47
SUPPORTED_PROTOCOL_VERSIONS = (
47, # Minecraft 1.8 - 1.8.9
)

View File

@ -1,4 +1,5 @@
from collections import deque from collections import deque
from collections import namedtuple
from threading import Lock from threading import Lock
from zlib import decompress from zlib import decompress
import threading import threading
@ -10,8 +11,7 @@ import sys
from .types import VarInt from .types import VarInt
from . import packets from . import packets
from . import encryption from . import encryption
from .. import PROTOCOL_VERSION from .. import SUPPORTED_PROTOCOL_VERSIONS
class _ConnectionOptions(object): class _ConnectionOptions(object):
def __init__(self, def __init__(self,
@ -25,13 +25,22 @@ class _ConnectionOptions(object):
self.compression_threshold = compression_threshold self.compression_threshold = compression_threshold
self.compression_enabled = compression_enabled self.compression_enabled = compression_enabled
ConnectionContext = namedtuple('ConnectionContext', (
'protocol_version'
))
class Connection(object): class Connection(object):
"""This class represents a connection to a minecraft """This class represents a connection to a minecraft
server, it handles everything from connecting, sending packets to server, it handles everything from connecting, sending packets to
handling default network behaviour 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 """Sets up an instance of this object to be able to connect to a
minecraft server. minecraft server.
@ -56,6 +65,10 @@ class Connection(object):
self.options.port = port self.options.port = port
self.auth_token = auth_token self.auth_token = auth_token
self.context = ConnectionContext(
protocol_version = protocol_version
)
# The reactor handles all the default responses to packets, # The reactor handles all the default responses to packets,
# it should be changed per networking state # it should be changed per networking state
self.reactor = PacketReactor(self) self.reactor = PacketReactor(self)
@ -76,6 +89,7 @@ class Connection(object):
:param packet: The :class:`network.packets.Packet` to write :param packet: The :class:`network.packets.Packet` to write
:param force(bool): Specifies if the packet write should be immediate :param force(bool): Specifies if the packet write should be immediate
""" """
packet.context = self.context
if force: if force:
self._write_lock.acquire() self._write_lock.acquire()
if self.options.compression_enabled: if self.options.compression_enabled:
@ -149,7 +163,7 @@ class Connection(object):
def _handshake(self, next_state=2): def _handshake(self, next_state=2):
handshake = packets.HandShakePacket() handshake = packets.HandShakePacket()
handshake.protocol_version = PROTOCOL_VERSION handshake.protocol_version = self.context.protocol_version
handshake.server_address = self.options.address handshake.server_address = self.options.address
handshake.server_port = self.options.port handshake.server_port = self.options.port
handshake.next_state = next_state handshake.next_state = next_state
@ -210,11 +224,16 @@ class PacketReactor(object):
Reads and reacts to packets Reads and reacts to packets
""" """
state_name = None state_name = None
clientbound_packets = None
TIME_OUT = 0 TIME_OUT = 0
get_clientbound_packets = staticmethod(lambda context: set())
def __init__(self, connection): def __init__(self, connection):
self.connection = 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): def read_packet(self, stream):
ready_to_read = select.select([self.connection.socket], [], [], ready_to_read = select.select([self.connection.socket], [], [],
@ -247,10 +266,11 @@ class PacketReactor(object):
# otherwise just skip it # otherwise just skip it
if packet_id in self.clientbound_packets: if packet_id in self.clientbound_packets:
packet = self.clientbound_packets[packet_id]() packet = self.clientbound_packets[packet_id]()
packet.context = self.connection.context
packet.read(packet_data) packet.read(packet_data)
return packet return packet
else: else:
return packets.Packet() return packets.Packet(context=self.connection.context)
else: else:
return None return None
@ -259,7 +279,7 @@ class PacketReactor(object):
class LoginReactor(PacketReactor): class LoginReactor(PacketReactor):
clientbound_packets = packets.STATE_LOGIN_CLIENTBOUND get_clientbound_packets = staticmethod(packets.state_login_clientbound)
def react(self, packet): def react(self, packet):
if packet.packet_name == "encryption request": if packet.packet_name == "encryption request":
@ -307,7 +327,7 @@ class LoginReactor(PacketReactor):
class PlayingReactor(PacketReactor): class PlayingReactor(PacketReactor):
clientbound_packets = packets.STATE_PLAYING_CLIENTBOUND get_clientbound_packets = staticmethod(packets.state_playing_clientbound)
def react(self, packet): def react(self, packet):
if packet.packet_name == "set compression": if packet.packet_name == "set compression":
@ -337,10 +357,10 @@ class PlayingReactor(PacketReactor):
''' '''
class StatusReactor(PacketReactor): class StatusReactor(PacketReactor):
clientbound_packets = packets.STATE_STATUS_CLIENTBOUND get_clientbound_packets = staticmethod(packets.state_status_clientbound)
def react(self, packet): def react(self, packet):
if packet.id == packets.ResponsePacket.id: if packet.id == packets.ResponsePacket.get_id(self.connection.context):
import json import json
print(json.loads(packet.json_response)) print(json.loads(packet.json_response))

View File

@ -50,11 +50,34 @@ class PacketListener(object):
class Packet(object): class Packet(object):
packet_name = "base" packet_name = "base"
id = -0x01 id = None
definition = [] definition = None
def __init__(self, **kwargs): # To define the packet ID, either:
pass # 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): def set_values(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
@ -112,13 +135,14 @@ class HandShakePacket(Packet):
{'next_state': VarInt}] {'next_state': VarInt}]
STATE_HANDSHAKE_CLIENTBOUND = { def state_handshake_clientbound(context):
return {
}
STATE_HANDSHAKE_SERVERBOUND = { }
0x00: HandShakePacket def state_handshake_serverbound(context):
} return {
HandShakePacket
}
# Status State # Status State
# ============== # ==============
@ -136,10 +160,11 @@ class PingPacketResponse(Packet):
{'time': Long}] {'time': Long}]
STATE_STATUS_CLIENTBOUND = { def state_status_clientbound(context):
0x00: ResponsePacket, return {
0x01: PingPacketResponse ResponsePacket,
} PingPacketResponse
}
class RequestPacket(Packet): class RequestPacket(Packet):
@ -155,10 +180,11 @@ class PingPacket(Packet):
{'time': Long}] {'time': Long}]
STATE_STATUS_SERVERBOUND = { def state_status_serverbound(context):
0x00: RequestPacket, return {
0x01: PingPacket RequestPacket,
} PingPacket
}
# Login State # Login State
@ -194,12 +220,13 @@ class SetCompressionPacket(Packet):
{'threshold': VarInt}] {'threshold': VarInt}]
STATE_LOGIN_CLIENTBOUND = { def state_login_clientbound(context):
0x00: DisconnectPacket, return {
0x01: EncryptionRequestPacket, DisconnectPacket,
0x02: LoginSuccessPacket, EncryptionRequestPacket,
0x03: SetCompressionPacket LoginSuccessPacket,
} SetCompressionPacket
}
class LoginStartPacket(Packet): class LoginStartPacket(Packet):
@ -217,10 +244,11 @@ class EncryptionResponsePacket(Packet):
{'verify_token': VarIntPrefixedByteArray}] {'verify_token': VarIntPrefixedByteArray}]
STATE_LOGIN_SERVERBOUND = { def state_login_serverbound(context):
0x00: LoginStartPacket, return {
0x01: EncryptionResponsePacket LoginStartPacket,
} EncryptionResponsePacket
}
# Playing State # Playing State
@ -496,16 +524,17 @@ class MapPacket(Packet):
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
STATE_PLAYING_CLIENTBOUND = { def state_playing_clientbound(context):
0x00: KeepAlivePacket, return {
0x01: JoinGamePacket, KeepAlivePacket,
0x02: ChatMessagePacket, JoinGamePacket,
0x08: PlayerPositionAndLookPacket, ChatMessagePacket,
0x34: MapPacket, PlayerPositionAndLookPacket,
0x38: PlayerListItemPacket, MapPacket,
0x40: DisconnectPacketPlayState, PlayerListItemPacket,
0x46: SetCompressionPacketPlayState DisconnectPacketPlayState,
} SetCompressionPacketPlayState
}
class ChatPacket(Packet): class ChatPacket(Packet):
@ -526,8 +555,10 @@ class PositionAndLookPacket(Packet):
{'pitch': Float}, {'pitch': Float},
{'on_ground': Boolean}] {'on_ground': Boolean}]
STATE_PLAYING_SERVERBOUND = {
0x00: KeepAlivePacket, def state_playing_serverbound(context):
0x01: ChatPacket, return {
0x06: PositionAndLookPacket KeepAlivePacket,
} ChatPacket,
PositionAndLookPacket
}