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
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
)

View File

@ -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))

View File

@ -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,14 +135,15 @@ class HandShakePacket(Packet):
{'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
# ==============
class ResponsePacket(Packet):
@ -136,9 +160,10 @@ class PingPacketResponse(Packet):
{'time': Long}]
STATE_STATUS_CLIENTBOUND = {
0x00: ResponsePacket,
0x01: PingPacketResponse
def state_status_clientbound(context):
return {
ResponsePacket,
PingPacketResponse
}
@ -155,9 +180,10 @@ class PingPacket(Packet):
{'time': Long}]
STATE_STATUS_SERVERBOUND = {
0x00: RequestPacket,
0x01: PingPacket
def state_status_serverbound(context):
return {
RequestPacket,
PingPacket
}
@ -194,11 +220,12 @@ 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
}
@ -217,9 +244,10 @@ class EncryptionResponsePacket(Packet):
{'verify_token': VarIntPrefixedByteArray}]
STATE_LOGIN_SERVERBOUND = {
0x00: LoginStartPacket,
0x01: EncryptionResponsePacket
def state_login_serverbound(context):
return {
LoginStartPacket,
EncryptionResponsePacket
}
@ -496,15 +524,16 @@ 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
}
@ -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
}