Add support for Minecraft 1.13 and 1.13-pre3 to pre10 (protocols 385 to 393).

Add clientbound.login.PluginRequestPacket and serverbound.login.PluginResponsePacket.
This commit is contained in:
joo 2018-07-19 01:59:48 +01:00
parent 745aa054b0
commit adc8d15ddc
19 changed files with 258 additions and 48 deletions

View File

@ -28,6 +28,7 @@ pyCraft is compatible with the following Minecraft releases:
* 1.10, 1.10.1, 1.10.2
* 1.11, 1.11.1, 1.11.2
* 1.12, 1.12.1, 1.12.2
* 1.13
In addition, some development snapshots and pre-release versions are supported:
`<minecraft/__init__.py>`_ contains a full list of supported Minecraft versions

View File

@ -112,6 +112,15 @@ SUPPORTED_MINECRAFT_VERSIONS = {
'18w22c': 382,
'1.13-pre1': 383,
'1.13-pre2': 384,
'1.13-pre3': 385,
'1.13-pre4': 386,
'1.13-pre5': 387,
'1.13-pre6': 388,
'1.13-pre7': 389,
'1.13-pre8': 390,
'1.13-pre9': 391,
'1.13-pre10': 392,
'1.13': 393,
}
SUPPORTED_PROTOCOL_VERSIONS = \

View File

@ -568,13 +568,18 @@ class PacketReactor(object):
return None
def react(self, packet):
"""Called with each incoming packet after early packet listeners are
run (if none of them raise 'IgnorePacket'), but before regular
packet listeners are run. If this method raises 'IgnorePacket', no
subsequent packet listeners will be called for this packet.
"""
raise NotImplementedError("Call to base reactor")
""" Called when an exception is raised in the networking thread. If this
method returns True, the default action will be prevented and the
exception ignored (but the networking thread will still terminate).
"""
def handle_exception(self, exc, exc_info):
"""Called when an exception is raised in the networking thread. If this
method returns True, the default action will be prevented and the
exception ignored (but the networking thread will still terminate).
"""
return False
@ -613,7 +618,7 @@ class LoginReactor(PacketReactor):
encryption.EncryptedFileObjectWrapper(
self.connection.file_object, decryptor)
if packet.packet_name == "disconnect":
elif packet.packet_name == "disconnect":
# Receiving a disconnect packet in the login state indicates an
# abnormal condition. Raise an exception explaining the situation.
try:
@ -628,13 +633,24 @@ class LoginReactor(PacketReactor):
raise LoginDisconnect('The server rejected our login attempt '
'with: "%s".' % msg)
if packet.packet_name == "login success":
elif packet.packet_name == "login success":
self.connection.reactor = PlayingReactor(self.connection)
if packet.packet_name == "set compression":
elif packet.packet_name == "set compression":
self.connection.options.compression_threshold = packet.threshold
self.connection.options.compression_enabled = True
elif packet.packet_name == "login plugin request":
self.connection.write_packet(
serverbound.login.PluginResponsePacket(
message_id=packet.message_id, successful=False))
def react_not_handled(self, packet):
if packet.name == "login plugin request":
self.connection.write_packet(
serverbound.login.PluginResponsePacket(
message_id=packet.message_id, successful=False))
class PlayingReactor(PacketReactor):
get_clientbound_packets = staticmethod(clientbound.play.get_packets)
@ -644,12 +660,12 @@ class PlayingReactor(PacketReactor):
self.connection.options.compression_threshold = packet.threshold
self.connection.options.compression_enabled = True
if packet.packet_name == "keep alive":
elif packet.packet_name == "keep alive":
keep_alive_packet = serverbound.play.KeepAlivePacket()
keep_alive_packet.keep_alive_id = packet.keep_alive_id
self.connection.write_packet(keep_alive_packet)
if packet.packet_name == "player position and look":
elif packet.packet_name == "player position and look":
if self.connection.context.protocol_version >= 107:
teleport_confirm = serverbound.play.TeleportConfirmPacket()
teleport_confirm.teleport_id = packet.teleport_id
@ -665,7 +681,7 @@ class PlayingReactor(PacketReactor):
self.connection.write_packet(position_response)
self.connection.spawned = True
if packet.packet_name == "disconnect":
elif packet.packet_name == "disconnect":
self.connection.disconnect()
@ -689,10 +705,11 @@ class StatusReactor(PacketReactor):
self.connection.disconnect()
self.handle_status(status_dict)
elif packet.packet_name == "ping" and self.do_ping:
now = int(1000 * timeit.default_timer())
self.connection.disconnect()
self.handle_ping(now - packet.time)
elif packet.packet_name == "ping":
if self.do_ping:
now = int(1000 * timeit.default_timer())
self.connection.disconnect()
self.handle_ping(now - packet.time)
def handle_status(self, status_dict):
print(status_dict)

View File

@ -1,7 +1,7 @@
from minecraft.networking.packets import Packet
from minecraft.networking.types import (
String, VarIntPrefixedByteArray, VarInt
VarInt, String, VarIntPrefixedByteArray, TrailingByteArray
)
@ -11,20 +11,34 @@ def get_packets(context):
DisconnectPacket,
EncryptionRequestPacket,
LoginSuccessPacket,
SetCompressionPacket
SetCompressionPacket,
}
if context.protocol_version >= 385:
packets |= {
PluginRequestPacket,
}
return packets
class DisconnectPacket(Packet):
id = 0x00
@staticmethod
def get_id(context):
return 0x00 if context.protocol_version >= 391 else \
0x01 if context.protocol_version >= 385 else \
0x00
packet_name = "disconnect"
definition = [
{'json_data': String}]
class EncryptionRequestPacket(Packet):
id = 0x01
@staticmethod
def get_id(context):
return 0x01 if context.protocol_version >= 391 else \
0x02 if context.protocol_version >= 385 else \
0x01
packet_name = "encryption request"
definition = [
{'server_id': String},
@ -33,7 +47,12 @@ class EncryptionRequestPacket(Packet):
class LoginSuccessPacket(Packet):
id = 0x02
@staticmethod
def get_id(context):
return 0x02 if context.protocol_version >= 391 else \
0x03 if context.protocol_version >= 385 else \
0x02
packet_name = "login success"
definition = [
{'UUID': String},
@ -41,7 +60,39 @@ class LoginSuccessPacket(Packet):
class SetCompressionPacket(Packet):
id = 0x03
@staticmethod
def get_id(context):
return 0x03 if context.protocol_version >= 391 else \
0x04 if context.protocol_version >= 385 else \
0x03
packet_name = "set compression"
definition = [
{'threshold': VarInt}]
class PluginRequestPacket(Packet):
""" NOTE: pyCraft's default behaviour on receiving a 'PluginRequestPacket'
is to send a corresponding 'PluginResponsePacket' with
'successful=False'. To override this, set a packet listener that:
(1) has the keyword argument 'early=True' set when calling
'register_packet_listener'; and
(2) raises 'minecraft.networking.connection.IgnorePacket' after
sending a corresponding 'PluginResponsePacket'.
Otherwise, one 'PluginRequestPacket' may result in multiple responses,
which contravenes Minecraft's protocol.
"""
@staticmethod
def get_id(context):
return 0x04 if context.protocol_version >= 391 else \
0x00
packet_name = 'login plugin request'
definition = [
{'message_id': VarInt},
{'channel': String},
{'data': TrailingByteArray}]

View File

@ -46,7 +46,8 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod
def get_id(context):
return 0x20 if context.protocol_version >= 345 else \
return 0x21 if context.protocol_version >= 389 else \
0x20 if context.protocol_version >= 345 else \
0x1F if context.protocol_version >= 332 else \
0x20 if context.protocol_version >= 318 else \
0x1F if context.protocol_version >= 107 else \
@ -56,7 +57,8 @@ class KeepAlivePacket(AbstractKeepAlivePacket):
class JoinGamePacket(Packet):
@staticmethod
def get_id(context):
return 0x24 if context.protocol_version >= 345 else \
return 0x25 if context.protocol_version >= 389 else \
0x24 if context.protocol_version >= 345 else \
0x23 if context.protocol_version >= 332 else \
0x24 if context.protocol_version >= 318 else \
0x23 if context.protocol_version >= 107 else \
@ -142,7 +144,8 @@ class SpawnPlayerPacket(Packet):
class EntityVelocityPacket(Packet):
@staticmethod
def get_id(context):
return 0x40 if context.protocol_version >= 352 else \
return 0x41 if context.protocol_version >= 389 else \
0x40 if context.protocol_version >= 352 else \
0x3F if context.protocol_version >= 345 else \
0x3E if context.protocol_version >= 336 else \
0x3D if context.protocol_version >= 332 else \
@ -163,7 +166,8 @@ class EntityVelocityPacket(Packet):
class UpdateHealthPacket(Packet):
@staticmethod
def get_id(context):
return 0x43 if context.protocol_version >= 352 else \
return 0x44 if context.protocol_version >= 389 else \
0x43 if context.protocol_version >= 352 else \
0x42 if context.protocol_version >= 345 else \
0x41 if context.protocol_version >= 336 else \
0x40 if context.protocol_version >= 318 else \

View File

@ -8,7 +8,8 @@ from minecraft.networking.types import (
class CombatEventPacket(Packet):
@staticmethod
def get_id(context):
return 0x2E if context.protocol_version >= 345 else \
return 0x2F if context.protocol_version >= 389 else \
0x2E if context.protocol_version >= 345 else \
0x2D if context.protocol_version >= 336 else \
0x2C if context.protocol_version >= 332 else \
0x2D if context.protocol_version >= 318 else \

View File

@ -5,7 +5,8 @@ from minecraft.networking.packets import Packet
class ExplosionPacket(Packet):
@staticmethod
def get_id(context):
return 0x1D if context.protocol_version >= 345 else \
return 0x1E if context.protocol_version >= 389 else \
0x1D if context.protocol_version >= 345 else \
0x1C if context.protocol_version >= 332 else \
0x1D if context.protocol_version >= 318 else \
0x1C if context.protocol_version >= 80 else \

View File

@ -8,7 +8,8 @@ from minecraft.networking.types import (
class MapPacket(Packet):
@staticmethod
def get_id(context):
return 0x25 if context.protocol_version >= 345 else \
return 0x26 if context.protocol_version >= 389 else \
0x25 if context.protocol_version >= 345 else \
0x24 if context.protocol_version >= 334 else \
0x25 if context.protocol_version >= 318 else \
0x24 if context.protocol_version >= 107 else \

View File

@ -8,7 +8,8 @@ from minecraft.networking.types import (
class PlayerListItemPacket(Packet):
@staticmethod
def get_id(context):
return 0x2F if context.protocol_version >= 345 else \
return 0x30 if context.protocol_version >= 389 else \
0x2F if context.protocol_version >= 345 else \
0x2E if context.protocol_version >= 336 else \
0x2D if context.protocol_version >= 332 else \
0x2E if context.protocol_version >= 318 else \

View File

@ -8,7 +8,8 @@ from minecraft.networking.types import (
class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
@staticmethod
def get_id(context):
return 0x31 if context.protocol_version >= 352 else \
return 0x32 if context.protocol_version >= 389 else \
0x31 if context.protocol_version >= 352 else \
0x30 if context.protocol_version >= 345 else \
0x2F if context.protocol_version >= 336 else \
0x2E if context.protocol_version >= 332 else \

View File

@ -110,12 +110,17 @@ class Packet(object):
str = type(self).__name__
if self.id is not None:
str = '0x%02X %s' % (self.id, str)
if self.definition is not None:
str = '%s(%s)' % (str, ', '.join(
'%s=%s' % (a, self.field_string(a))
for d in self.definition for a in d))
fields = self.fields
if fields is not None:
str = '%s(%s)' % (str, ', '.join('%s=%s' %
(a, self.field_string(a)) for a in fields))
return str
@property
def fields(self):
""" An iterable of the names of the packet's fields, or None. """
return (field for defn in self.definition for field in defn)
def field_string(self, field):
""" The string representation of the value of a the given named field
of this packet. Override to customise field value representation.

View File

@ -13,3 +13,5 @@ class PacketListener(object):
for packet_type in self.packets_to_listen:
if isinstance(packet, packet_type):
self.callback(packet)
return True
return False

View File

@ -3,6 +3,10 @@ from minecraft.networking.types import String, TrailingByteArray
class AbstractPluginMessagePacket(Packet):
"""NOTE: Plugin channels were significantly changed, including changing the
names of channels, between Minecraft 1.12 and 1.13 - see <http://wiki.vg
/index.php?title=Pre-release_protocol&oldid=14132#Plugin_Channels>.
"""
definition = [
{'channel': String},
{'data': TrailingByteArray}]

View File

@ -1,7 +1,7 @@
from minecraft.networking.packets import Packet
from minecraft.networking.types import (
String, VarIntPrefixedByteArray
VarInt, Boolean, String, VarIntPrefixedByteArray, TrailingByteArray
)
@ -11,19 +11,67 @@ def get_packets(context):
LoginStartPacket,
EncryptionResponsePacket
}
if context.protocol_version >= 385:
packets |= {
PluginResponsePacket
}
return packets
class LoginStartPacket(Packet):
id = 0x00
@staticmethod
def get_id(context):
return 0x00 if context.protocol_version >= 391 else \
0x01 if context.protocol_version >= 385 else \
0x00
packet_name = "login start"
definition = [
{'name': String}]
class EncryptionResponsePacket(Packet):
id = 0x01
@staticmethod
def get_id(context):
return 0x01 if context.protocol_version >= 391 else \
0x02 if context.protocol_version >= 385 else \
0x01
packet_name = "encryption response"
definition = [
{'shared_secret': VarIntPrefixedByteArray},
{'verify_token': VarIntPrefixedByteArray}]
class PluginResponsePacket(Packet):
""" NOTE: see comments on 'clientbound.login.PluginRequestPacket' for
important information on the usage of this packet.
"""
@staticmethod
def get_id(context):
return 0x02 if context.protocol_version >= 391 else \
0x00
packet_name = 'login plugin response'
fields = (
'message_id', # str
'successful', # bool
'data', # bytes, or None if 'successful' is False
)
def read(self, file_object):
self.message_id = VarInt.read(file_object)
self.successful = Boolean.read(file_object)
if self.successful:
self.data = TrailingByteArray.read(file_object)
else:
self.data = None
def write_fields(self, packet_buffer):
VarInt.send(self.message_id, packet_buffer)
successful = getattr(self, 'data', None) is not None
successful = getattr(self, 'successful', successful)
Boolean.send(successful, packet_buffer)
if successful:
TrailingByteArray.send(self.data, packet_buffer)

View File

@ -32,7 +32,9 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod
def get_id(context):
return 0x0B if context.protocol_version >= 345 else \
return 0x0E if context.protocol_version >= 389 else \
0x0C if context.protocol_version >= 386 else \
0x0B if context.protocol_version >= 345 else \
0x0A if context.protocol_version >= 343 else \
0x0B if context.protocol_version >= 336 else \
0x0C if context.protocol_version >= 318 else \
@ -43,7 +45,8 @@ class KeepAlivePacket(AbstractKeepAlivePacket):
class ChatPacket(Packet):
@staticmethod
def get_id(context):
return 0x01 if context.protocol_version >= 343 else \
return 0x02 if context.protocol_version >= 389 else \
0x01 if context.protocol_version >= 343 else \
0x02 if context.protocol_version >= 336 else \
0x03 if context.protocol_version >= 318 else \
0x02 if context.protocol_version >= 107 else \
@ -67,7 +70,9 @@ class ChatPacket(Packet):
class PositionAndLookPacket(Packet):
@staticmethod
def get_id(context):
return 0x0E if context.protocol_version >= 345 else \
return 0x11 if context.protocol_version >= 389 else \
0x0F if context.protocol_version >= 386 else \
0x0E if context.protocol_version >= 345 else \
0x0D if context.protocol_version >= 343 else \
0x0E if context.protocol_version >= 336 else \
0x0F if context.protocol_version >= 332 else \
@ -96,7 +101,9 @@ class TeleportConfirmPacket(Packet):
class AnimationPacket(Packet):
@staticmethod
def get_id(context):
return 0x1D if context.protocol_version >= 345 else \
return 0x27 if context.protocol_version >= 389 else \
0x25 if context.protocol_version >= 386 else \
0x1D if context.protocol_version >= 345 else \
0x1C if context.protocol_version >= 343 else \
0x1D if context.protocol_version >= 332 else \
0x1C if context.protocol_version >= 318 else \
@ -114,7 +121,8 @@ class AnimationPacket(Packet):
class ClientStatusPacket(Packet, Enum):
@staticmethod
def get_id(context):
return 0x02 if context.protocol_version >= 343 else \
return 0x03 if context.protocol_version >= 389 else \
0x02 if context.protocol_version >= 343 else \
0x03 if context.protocol_version >= 336 else \
0x04 if context.protocol_version >= 318 else \
0x03 if context.protocol_version >= 80 else \
@ -137,7 +145,8 @@ class ClientStatusPacket(Packet, Enum):
class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod
def get_id(context):
return 0x09 if context.protocol_version >= 345 else \
return 0x0A if context.protocol_version >= 389 else \
0x09 if context.protocol_version >= 345 else \
0x08 if context.protocol_version >= 343 else \
0x09 if context.protocol_version >= 336 else \
0x0A if context.protocol_version >= 317 else \
@ -161,7 +170,9 @@ class PlayerBlockPlacementPacket(Packet):
@staticmethod
def get_id(context):
return 0x1F if context.protocol_version >= 345 else \
return 0x29 if context.protocol_version >= 389 else \
0x27 if context.protocol_version >= 386 else \
0x1F if context.protocol_version >= 345 else \
0x1E if context.protocol_version >= 343 else \
0x1F if context.protocol_version >= 332 else \
0x1E if context.protocol_version >= 318 else \

View File

@ -8,7 +8,8 @@ from minecraft.networking.types import (
class ClientSettingsPacket(Packet):
@staticmethod
def get_id(context):
return 0x03 if context.protocol_version >= 343 else \
return 0x04 if context.protocol_version >= 389 else \
0x03 if context.protocol_version >= 343 else \
0x04 if context.protocol_version >= 336 else \
0x05 if context.protocol_version >= 318 else \
0x04 if context.protocol_version >= 94 else \

View File

@ -89,11 +89,11 @@ class FakeClientHandler(object):
# Handshake packet, which is provided as an argument.
pass
def handle_login(self, join_game_packet):
def handle_login(self, login_start_packet):
# Called to transition from the login state to the play state, after
# compression and encryption, if applicable, have been set up. The
# client's LoginStartPacket is given as an argument.
self.user_name = join_game_packet.name
self.user_name = login_start_packet.name
self.user_uuid = uuid.UUID(bytes=hashlib.md5(
('OfflinePlayer:%s' % self.user_name).encode('utf8')).digest())
self.write_packet(clientbound.login.LoginSuccessPacket(

View File

@ -1,9 +1,9 @@
from minecraft import SUPPORTED_MINECRAFT_VERSIONS
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
from minecraft.networking.packets import clientbound, serverbound
from minecraft.networking.connection import Connection, IgnorePacket
from minecraft.networking.connection import Connection
from minecraft.exceptions import (
VersionMismatch, LoginDisconnect, InvalidState
VersionMismatch, LoginDisconnect, InvalidState, IgnorePacket
)
from minecraft.compat import unicode
@ -134,6 +134,51 @@ class ConnectStatusTest(ConnectTwiceTest):
client.status()
class LoginPluginTest(fake_server._FakeServerTest):
class client_handler_type(fake_server.FakeClientHandler):
def handle_login(self, login_start_packet):
request = clientbound.login.PluginRequestPacket(
message_id=1, channel='pyCraft:tests/fail', data=b'ignored')
self.write_packet(request)
response = self.read_packet()
assert isinstance(response, serverbound.login.PluginResponsePacket)
assert response.message_id == request.message_id
assert response.successful is False
assert response.data is None
request = clientbound.login.PluginRequestPacket(
message_id=2, channel='pyCraft:tests/echo', data=b'hello')
self.write_packet(request)
response = self.read_packet()
assert isinstance(response, serverbound.login.PluginResponsePacket)
assert response.message_id == request.message_id
assert response.successful is True
assert response.data == request.data
super(LoginPluginTest.client_handler_type, self) \
.handle_login(login_start_packet)
def handle_play_start(self):
super(LoginPluginTest.client_handler_type, self) \
.handle_play_start()
raise fake_server.FakeServerDisconnect
def _start_client(self, client):
def handle_plugin_request(packet):
if packet.channel == 'pyCraft:tests/echo':
client.write_packet(serverbound.login.PluginResponsePacket(
message_id=packet.message_id, data=packet.data))
raise IgnorePacket
client.register_packet_listener(
handle_plugin_request, clientbound.login.PluginRequestPacket,
early=True)
super(LoginPluginTest, self)._start_client(client)
def test_login_plugin_messages(self):
self._test_connect()
class EarlyPacketListenerTest(ConnectTest):
""" Early packet listeners should be called before ordinary ones, even when
the early packet listener is registered afterwards.

View File

@ -3,17 +3,23 @@ try:
from unittest import mock
except ImportError:
import mock
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
from minecraft.networking.connection import (
LoginReactor, PlayingReactor, ConnectionContext
)
from minecraft.networking.packets import clientbound
max_proto_ver = max(SUPPORTED_PROTOCOL_VERSIONS)
class LoginReactorTest(unittest.TestCase):
@mock.patch('minecraft.networking.connection.encryption')
def test_encryption_online_server(self, encrypt):
connection = mock.MagicMock()
connection.context = ConnectionContext(protocol_version=max_proto_ver)
reactor = LoginReactor(connection)
packet = clientbound.login.EncryptionRequestPacket()
@ -37,6 +43,7 @@ class LoginReactorTest(unittest.TestCase):
@mock.patch('minecraft.networking.connection.encryption')
def test_encryption_offline_server(self, encrypt):
connection = mock.MagicMock()
connection.context = ConnectionContext(protocol_version=max_proto_ver)
reactor = LoginReactor(connection)
packet = clientbound.login.EncryptionRequestPacket()