Add support for Minecraft 18w43a to 1.14 (protocols 441 to 477)

This commit introduces two backward-incompatible changes which may break
existing code:

(1) `networking.packets.clientbound.play.SpawnObjectPacket.EntityType'
is no longer accessible as an attribute of the the `SpawnObjectPacket'
class: the values now depend on a `ConnectionContext`, and must be
accessed through an instance, or using `SpawnObjectPacket.field_enum'.
See the text of the `AttributeError` raised from the descriptor for
`SpawnObjectPacket.EntityType` for the full details.

(2) For some subclasses of `networking.types.Type', it is necessary to
call the methods `read_with_context' and `send_with_context' instead of
`read' and `send', supplying a `ConnectionContext' for those data types
- currently only `Position` - whose layout depends on it.
This commit is contained in:
joodicator 2019-05-11 08:43:08 +02:00
parent 9b43d6f004
commit 612fa8e324
20 changed files with 463 additions and 219 deletions

View File

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

View File

@ -132,6 +132,44 @@ SUPPORTED_MINECRAFT_VERSIONS = {
'1.13.2-pre1': 402, '1.13.2-pre1': 402,
'1.13.2-pre2': 403, '1.13.2-pre2': 403,
'1.13.2': 404, '1.13.2': 404,
'18w43a': 441,
'18w43b': 441,
'18w43c': 442,
'18w44a': 443,
'18w45a': 444,
'18w46a': 445,
'18w47a': 446,
'18w47b': 447,
'18w48a': 448,
'18w48b': 449,
'18w49a': 450,
'18w50a': 451,
'19w02a': 452,
'19w03a': 453,
'19w03b': 454,
'19w03c': 455,
'19w04a': 456,
'19w04b': 457,
'19w05a': 458,
'19w06a': 459,
'19w07a': 460,
'19w08a': 461,
'19w08b': 462,
'19w09a': 463,
'19w11a': 464,
'19w11b': 465,
'19w12a': 466,
'19w12b': 467,
'19w13a': 468,
'19w13b': 469,
'19w14a': 470,
'19w14b': 471,
'1.14 Pre-Release 1': 472,
'1.14 Pre-Release 2': 473,
'1.14 Pre-Release 3': 474,
'1.14 Pre-Release 4': 475,
'1.14 Pre-Release 5': 476,
'1.14': 477,
} }
SUPPORTED_PROTOCOL_VERSIONS = \ SUPPORTED_PROTOCOL_VERSIONS = \

View File

@ -47,7 +47,8 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket): class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x21 if context.protocol_version >= 389 else \ return 0x20 if context.protocol_version >= 471 else \
0x21 if context.protocol_version >= 389 else \
0x20 if context.protocol_version >= 345 else \ 0x20 if context.protocol_version >= 345 else \
0x1F if context.protocol_version >= 332 else \ 0x1F if context.protocol_version >= 332 else \
0x20 if context.protocol_version >= 318 else \ 0x20 if context.protocol_version >= 318 else \
@ -70,9 +71,10 @@ class JoinGamePacket(Packet):
{'entity_id': Integer}, {'entity_id': Integer},
{'game_mode': UnsignedByte}, {'game_mode': UnsignedByte},
{'dimension': Integer if context.protocol_version >= 108 else Byte}, {'dimension': Integer if context.protocol_version >= 108 else Byte},
{'difficulty': UnsignedByte}, {'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'max_players': UnsignedByte}, {'max_players': UnsignedByte},
{'level_type': String}, {'level_type': String},
{'render_distance': VarInt} if context.protocol_version >= 468 else {},
{'reduced_debug_info': Boolean}]) {'reduced_debug_info': Boolean}])
@ -99,7 +101,8 @@ class ChatMessagePacket(Packet):
class DisconnectPacket(Packet): class DisconnectPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x1B if context.protocol_version >= 345 else \ return 0x1A if context.protocol_version >= 471 else \
0x1B if context.protocol_version >= 345 else \
0x1A if context.protocol_version >= 332 else \ 0x1A if context.protocol_version >= 332 else \
0x1B if context.protocol_version >= 318 else \ 0x1B if context.protocol_version >= 318 else \
0x1A if context.protocol_version >= 107 else \ 0x1A if context.protocol_version >= 107 else \
@ -145,7 +148,10 @@ class SpawnPlayerPacket(Packet):
class EntityVelocityPacket(Packet): class EntityVelocityPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x41 if context.protocol_version >= 389 else \ return 0x45 if context.protocol_version >= 471 else \
0x41 if context.protocol_version >= 461 else \
0x42 if context.protocol_version >= 451 else \
0x41 if context.protocol_version >= 389 else \
0x40 if context.protocol_version >= 352 else \ 0x40 if context.protocol_version >= 352 else \
0x3F if context.protocol_version >= 345 else \ 0x3F if context.protocol_version >= 345 else \
0x3E if context.protocol_version >= 336 else \ 0x3E if context.protocol_version >= 336 else \
@ -167,7 +173,10 @@ class EntityVelocityPacket(Packet):
class UpdateHealthPacket(Packet): class UpdateHealthPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x44 if context.protocol_version >= 389 else \ return 0x48 if context.protocol_version >= 471 else \
0x44 if context.protocol_version >= 461 else \
0x45 if context.protocol_version >= 451 else \
0x44 if context.protocol_version >= 389 else \
0x43 if context.protocol_version >= 352 else \ 0x43 if context.protocol_version >= 352 else \
0x42 if context.protocol_version >= 345 else \ 0x42 if context.protocol_version >= 345 else \
0x41 if context.protocol_version >= 336 else \ 0x41 if context.protocol_version >= 336 else \
@ -188,7 +197,8 @@ class UpdateHealthPacket(Packet):
class PluginMessagePacket(AbstractPluginMessagePacket): class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x19 if context.protocol_version >= 345 else \ return 0x18 if context.protocol_version >= 471 else \
0x19 if context.protocol_version >= 345 else \
0x18 if context.protocol_version >= 332 else \ 0x18 if context.protocol_version >= 332 else \
0x19 if context.protocol_version >= 318 else \ 0x19 if context.protocol_version >= 318 else \
0x18 if context.protocol_version >= 70 else \ 0x18 if context.protocol_version >= 70 else \
@ -198,7 +208,11 @@ class PluginMessagePacket(AbstractPluginMessagePacket):
class PlayerListHeaderAndFooterPacket(Packet): class PlayerListHeaderAndFooterPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x4E if context.protocol_version >= 393 else \ return 0x53 if context.protocol_version >= 471 else \
0x5F if context.protocol_version >= 461 else \
0x50 if context.protocol_version >= 451 else \
0x4F if context.protocol_version >= 441 else \
0x4E if context.protocol_version >= 393 else \
0x4A if context.protocol_version >= 338 else \ 0x4A if context.protocol_version >= 338 else \
0x49 if context.protocol_version >= 335 else \ 0x49 if context.protocol_version >= 335 else \
0x47 if context.protocol_version >= 110 else \ 0x47 if context.protocol_version >= 110 else \

View File

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

View File

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

View File

@ -28,7 +28,7 @@ class MapPacket(Packet):
class Map(MutableRecord): class Map(MutableRecord):
__slots__ = ('id', 'scale', 'icons', 'pixels', 'width', 'height', __slots__ = ('id', 'scale', 'icons', 'pixels', 'width', 'height',
'is_tracking_position') 'is_tracking_position', 'is_locked')
def __init__(self, id=None, scale=None, width=128, height=128): def __init__(self, id=None, scale=None, width=128, height=128):
self.id = id self.id = id
@ -38,6 +38,7 @@ class MapPacket(Packet):
self.height = height self.height = height
self.pixels = bytearray(0 for i in range(width*height)) self.pixels = bytearray(0 for i in range(width*height))
self.is_tracking_position = True self.is_tracking_position = True
self.is_locked = False
class MapSet(object): class MapSet(object):
__slots__ = 'maps_by_id' __slots__ = 'maps_by_id'
@ -58,6 +59,11 @@ class MapPacket(Packet):
else: else:
self.is_tracking_position = True self.is_tracking_position = True
if self.context.protocol_version >= 452:
self.is_locked = Boolean.read(file_object)
else:
self.is_locked = False
icon_count = VarInt.read(file_object) icon_count = VarInt.read(file_object)
self.icons = [] self.icons = []
for i in range(icon_count): for i in range(icon_count):
@ -99,6 +105,7 @@ class MapPacket(Packet):
z = self.offset[1] + i // self.width z = self.offset[1] + i // self.width
map.pixels[x + map.width * z] = self.pixels[i] map.pixels[x + map.width * z] = self.pixels[i]
map.is_tracking_position = self.is_tracking_position map.is_tracking_position = self.is_tracking_position
map.is_locked = self.is_locked
def apply_to_map_set(self, map_set): def apply_to_map_set(self, map_set):
map = map_set.maps_by_id.get(self.map_id) map = map_set.maps_by_id.get(self.map_id)

View File

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

View File

@ -8,7 +8,9 @@ from minecraft.networking.types import (
class PlayerPositionAndLookPacket(Packet, BitFieldEnum): class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x32 if context.protocol_version >= 389 else \ return 0x35 if context.protocol_version >= 471 else \
0x33 if context.protocol_version >= 451 else \
0x32 if context.protocol_version >= 389 else \
0x31 if context.protocol_version >= 352 else \ 0x31 if context.protocol_version >= 352 else \
0x30 if context.protocol_version >= 345 else \ 0x30 if context.protocol_version >= 345 else \
0x2F if context.protocol_version >= 336 else \ 0x2F if context.protocol_version >= 336 else \
@ -29,7 +31,7 @@ class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
]) ])
field_enum = classmethod( field_enum = classmethod(
lambda cls, field: cls if field == 'flags' else None) lambda cls, field, context: cls if field == 'flags' else None)
FLAG_REL_X = 0x01 FLAG_REL_X = 0x01
FLAG_REL_Y = 0x02 FLAG_REL_Y = 0x02

View File

@ -1,4 +1,5 @@
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types.utility import descriptor
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector, VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector,
@ -14,39 +15,88 @@ class SpawnObjectPacket(Packet):
packet_name = 'spawn object' packet_name = 'spawn object'
fields = ('entity_id', 'object_uuid', 'type_id',
'x', 'y', 'z', 'pitch', 'yaw')
@descriptor
def EntityType(desc, self, cls): # pylint: disable=no-self-argument
if self is None:
# EntityType is being accessed as a class attribute.
raise AttributeError(
'This interface is deprecated:\n\n'
'As of pyCraft\'s support for Minecraft 1.14, the nested '
'class "SpawnObjectPacket.EntityType" cannot be accessed as a '
'class attribute, because it depends on the protocol version. '
'There are two ways to access the correct version of the '
'class:\n\n'
'1. Access the "EntityType" attribute of a '
'"SpawnObjectPacket" instance with its "context" property '
'set.\n\n'
'2. Call "SpawnObjectPacket.field_enum(\'type_id\', '
'context)".')
else:
# EntityType is being accessed as an instance attribute.
return self.field_enum('type_id', self.context)
@classmethod
def field_enum(cls, field, context):
if field != 'type_id' or context is None:
return
pv = context.protocol_version
name = 'EntityType_%d' % pv
if hasattr(cls, name):
return getattr(cls, name)
class EntityType(Enum): class EntityType(Enum):
BOAT = 1 ACTIVATED_TNT = 50 if pv < 458 else 55 # PrimedTnt
ITEM_STACK = 2 AREA_EFFECT_CLOUD = 3 if pv < 458 else 0
AREA_EFFECT_CLOUD = 3 ARMORSTAND = 78 if pv < 458 else 1
MINECART = 10 ARROW = 60 if pv < 458 else 2
ACTIVATED_TNT = 50 BOAT = 1 if pv < 458 else 5
ENDERCRYSTAL = 51 DRAGON_FIREBALL = 93 if pv < 458 else 13
ARROW = 60 EGG = 62 if pv < 458 else 74 # ThrownEgg
SNOWBALL = 61 ENDERCRYSTAL = 51 if pv < 458 else 16
EGG = 62 ENDERPEARL = 65 if pv < 458 else 75 # ThrownEnderpearl
FIREBALL = 63 EVOCATION_FANGS = 79 if pv < 458 else 20
FIRECHARGE = 64 EXP_BOTTLE = 75 if pv < 458 else 76 # ThrownExpBottle
ENDERPERL = 65 EYE_OF_ENDER = 72 if pv < 458 else 23 # EyeOfEnderSignal
WITHER_SKULL = 66 FALLING_OBJECT = 70 if pv < 458 else 24 # FallingSand
SHULKER_BULLET = 67 FIREBALL = 63 if pv < 458 else 34 # Fireball (ghast)
LLAMA_SPIT = 68 FIRECHARGE = 64 if pv < 458 else 65 # SmallFireball (blaze)
FALLING_OBJECT = 70 FIREWORK_ROCKET = 76 if pv < 458 else 25 # FireworksRocketEntity
ITEM_FRAMES = 71 FISHING_HOOK = 90 if pv < 458 else 93 # Fishing bobber
EYE_OF_ENDER = 72 ITEM_FRAMES = 71 if pv < 458 else 33 # ItemFrame
POTION = 73 ITEM_STACK = 2 if pv < 458 else 32 # Item
EXP_BOTTLE = 75 LEASH_KNOT = 77 if pv < 458 else 35
FIREWORK_ROCKET = 76 LLAMA_SPIT = 68 if pv < 458 else 37
LEASH_KNOT = 77 MINECART = 10 if pv < 458 else 39 # MinecartRideable
ARMORSTAND = 78 POTION = 73 if pv < 458 else 77 # ThrownPotion
EVOCATION_FANGS = 79 SHULKER_BULLET = 67 if pv < 458 else 60
FISHING_HOOK = 90 SNOWBALL = 61 if pv < 458 else 67
SPECTRAL_ARROW = 91 SPECTRAL_ARROW = 91 if pv < 458 else 68
DRAGON_FIREBALL = 93 WITHER_SKULL = 66 if pv < 458 else 85
if pv >= 393:
TRIDENT = 94
if pv >= 458:
MINECART_CHEST = 40
MINECART_COMMAND_BLOCK = 41
MINECART_FURNACE = 42
MINECART_HOPPER = 43
MINECART_SPAWNER = 44
MINECART_TNT = 45
setattr(cls, name, EntityType)
return EntityType
def read(self, file_object): def read(self, file_object):
self.entity_id = VarInt.read(file_object) self.entity_id = VarInt.read(file_object)
if self.context.protocol_version >= 49: if self.context.protocol_version >= 49:
self.object_uuid = UUID.read(file_object) self.object_uuid = UUID.read(file_object)
if self.context.protocol_version >= 458:
self.type_id = VarInt.read(file_object)
else:
self.type_id = Byte.read(file_object) self.type_id = Byte.read(file_object)
xyz_type = Double if self.context.protocol_version >= 100 else Integer xyz_type = Double if self.context.protocol_version >= 100 else Integer
@ -64,6 +114,10 @@ class SpawnObjectPacket(Packet):
VarInt.send(self.entity_id, packet_buffer) VarInt.send(self.entity_id, packet_buffer)
if self.context.protocol_version >= 49: if self.context.protocol_version >= 49:
UUID.send(self.object_uuid, packet_buffer) UUID.send(self.object_uuid, packet_buffer)
if self.context.protocol_version >= 458:
VarInt.send(self.type_id, packet_buffer)
else:
Byte.send(self.type_id, packet_buffer) Byte.send(self.type_id, packet_buffer)
xyz_type = Double if self.context.protocol_version >= 100 else Integer xyz_type = Double if self.context.protocol_version >= 100 else Integer
@ -78,9 +132,20 @@ class SpawnObjectPacket(Packet):
Short.send(coord, packet_buffer) Short.send(coord, packet_buffer)
# Access the entity type as a string, according to the EntityType enum. # Access the entity type as a string, according to the EntityType enum.
@property
def type(self):
if self.context is None:
raise ValueError('This packet must have a non-None "context" '
'in order to read the "type" property.')
# pylint: disable=no-member
return self.EntityType.name_from_value(self.type_id)
@type.setter
def type(self, type_name): def type(self, type_name):
if self.context is None:
raise ValueError('This packet must have a non-None "context" '
'in order to set the "type" property.')
self.type_id = getattr(self.EntityType, type_name) self.type_id = getattr(self.EntityType, type_name)
type = property(lambda p: p.EntityType.name_from_value(p.type_id), type)
# Access the fields 'x', 'y', 'z' as a Vector. # Access the fields 'x', 'y', 'z' as a Vector.
def position(self, position): def position(self, position):

View File

@ -61,7 +61,7 @@ class Packet(object):
def read(self, file_object): def read(self, file_object):
for field in self.definition: for field in self.definition:
for var_name, data_type in field.items(): for var_name, data_type in field.items():
value = data_type.read(file_object) value = data_type.read_with_context(file_object, self.context)
setattr(self, var_name, value) setattr(self, var_name, value)
# Writes a packet buffer to the socket with the appropriate headers # Writes a packet buffer to the socket with the appropriate headers
@ -104,7 +104,7 @@ class Packet(object):
for field in self.definition: for field in self.definition:
for var_name, data_type in field.items(): for var_name, data_type in field.items():
data = getattr(self, var_name) data = getattr(self, var_name)
data_type.send(data, packet_buffer) data_type.send_with_context(data, packet_buffer, self.context)
def __repr__(self): def __repr__(self):
str = type(self).__name__ str = type(self).__name__
@ -129,7 +129,7 @@ class Packet(object):
""" """
value = getattr(self, field, None) value = getattr(self, field, None)
enum_class = self.field_enum(field) enum_class = self.field_enum(field, self.context)
if enum_class is not None: if enum_class is not None:
name = enum_class.name_from_value(value) name = enum_class.name_from_value(value)
if name is not None: if name is not None:
@ -138,7 +138,7 @@ class Packet(object):
return repr(value) return repr(value)
@classmethod @classmethod
def field_enum(cls, field): def field_enum(cls, field, context=None):
""" The subclass of 'minecraft.networking.types.Enum' associated with """ The subclass of 'minecraft.networking.types.Enum' associated with
this field, or None if there is no such class. this field, or None if there is no such class.
""" """

View File

@ -32,7 +32,9 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket): class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x0E if context.protocol_version >= 389 else \ return 0x0F if context.protocol_version >= 471 else \
0x10 if context.protocol_version >= 464 else \
0x0E if context.protocol_version >= 389 else \
0x0C if context.protocol_version >= 386 else \ 0x0C if context.protocol_version >= 386 else \
0x0B if context.protocol_version >= 345 else \ 0x0B if context.protocol_version >= 345 else \
0x0A if context.protocol_version >= 343 else \ 0x0A if context.protocol_version >= 343 else \
@ -45,7 +47,8 @@ class KeepAlivePacket(AbstractKeepAlivePacket):
class ChatPacket(Packet): class ChatPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x02 if context.protocol_version >= 389 else \ return 0x03 if context.protocol_version >= 464 else \
0x02 if context.protocol_version >= 389 else \
0x01 if context.protocol_version >= 343 else \ 0x01 if context.protocol_version >= 343 else \
0x02 if context.protocol_version >= 336 else \ 0x02 if context.protocol_version >= 336 else \
0x03 if context.protocol_version >= 318 else \ 0x03 if context.protocol_version >= 318 else \
@ -70,7 +73,9 @@ class ChatPacket(Packet):
class PositionAndLookPacket(Packet): class PositionAndLookPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x11 if context.protocol_version >= 389 else \ return 0x12 if context.protocol_version >= 471 else \
0x13 if context.protocol_version >= 464 else \
0x11 if context.protocol_version >= 389 else \
0x0F if context.protocol_version >= 386 else \ 0x0F if context.protocol_version >= 386 else \
0x0E if context.protocol_version >= 345 else \ 0x0E if context.protocol_version >= 345 else \
0x0D if context.protocol_version >= 343 else \ 0x0D if context.protocol_version >= 343 else \
@ -101,7 +106,9 @@ class TeleportConfirmPacket(Packet):
class AnimationPacket(Packet): class AnimationPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x27 if context.protocol_version >= 389 else \ return 0x2A if context.protocol_version >= 468 else \
0x29 if context.protocol_version >= 464 else \
0x27 if context.protocol_version >= 389 else \
0x25 if context.protocol_version >= 386 else \ 0x25 if context.protocol_version >= 386 else \
0x1D if context.protocol_version >= 345 else \ 0x1D if context.protocol_version >= 345 else \
0x1C if context.protocol_version >= 343 else \ 0x1C if context.protocol_version >= 343 else \
@ -121,7 +128,8 @@ class AnimationPacket(Packet):
class ClientStatusPacket(Packet, Enum): class ClientStatusPacket(Packet, Enum):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x03 if context.protocol_version >= 389 else \ return 0x04 if context.protocol_version >= 464 else \
0x03 if context.protocol_version >= 389 else \
0x02 if context.protocol_version >= 343 else \ 0x02 if context.protocol_version >= 343 else \
0x03 if context.protocol_version >= 336 else \ 0x03 if context.protocol_version >= 336 else \
0x04 if context.protocol_version >= 318 else \ 0x04 if context.protocol_version >= 318 else \
@ -134,7 +142,7 @@ class ClientStatusPacket(Packet, Enum):
get_definition = staticmethod(lambda context: [ get_definition = staticmethod(lambda context: [
{'action_id': VarInt}]) {'action_id': VarInt}])
field_enum = classmethod( field_enum = classmethod(
lambda cls, field: cls if field == 'action_id' else None) lambda cls, field, context: cls if field == 'action_id' else None)
RESPAWN = 0 RESPAWN = 0
REQUEST_STATS = 1 REQUEST_STATS = 1
@ -145,7 +153,8 @@ class ClientStatusPacket(Packet, Enum):
class PluginMessagePacket(AbstractPluginMessagePacket): class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x0A if context.protocol_version >= 389 else \ return 0x0B if context.protocol_version >= 464 else \
0x0A if context.protocol_version >= 389 else \
0x09 if context.protocol_version >= 345 else \ 0x09 if context.protocol_version >= 345 else \
0x08 if context.protocol_version >= 343 else \ 0x08 if context.protocol_version >= 343 else \
0x09 if context.protocol_version >= 336 else \ 0x09 if context.protocol_version >= 336 else \
@ -170,7 +179,9 @@ class PlayerBlockPlacementPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x29 if context.protocol_version >= 389 else \ return 0x2C if context.protocol_version >= 468 else \
0x2B if context.protocol_version >= 464 else \
0x29 if context.protocol_version >= 389 else \
0x27 if context.protocol_version >= 386 else \ 0x27 if context.protocol_version >= 386 else \
0x1F if context.protocol_version >= 345 else \ 0x1F if context.protocol_version >= 345 else \
0x1E if context.protocol_version >= 343 else \ 0x1E if context.protocol_version >= 343 else \
@ -184,12 +195,15 @@ class PlayerBlockPlacementPacket(Packet):
@staticmethod @staticmethod
def get_definition(context): def get_definition(context):
return [ return [
{'hand': VarInt} if context.protocol_version >= 453 else {},
{'location': Position}, {'location': Position},
{'face': VarInt if context.protocol_version >= 69 else Byte}, {'face': VarInt if context.protocol_version >= 69 else Byte},
{'hand': VarInt}, {'hand': VarInt} if context.protocol_version < 453 else {},
{'x': Float if context.protocol_version >= 309 else Byte}, {'x': Float if context.protocol_version >= 309 else Byte},
{'y': Float if context.protocol_version >= 309 else Byte}, {'y': Float if context.protocol_version >= 309 else Byte},
{'z': Float if context.protocol_version >= 309 else Byte}, {'z': Float if context.protocol_version >= 309 else Byte},
({'inside_block': Boolean}
if context.protocol_version >= 453 else {}),
] ]
# PlayerBlockPlacementPacket.Hand is an alias for RelativeHand. # PlayerBlockPlacementPacket.Hand is an alias for RelativeHand.

View File

@ -8,7 +8,8 @@ from minecraft.networking.types import (
class ClientSettingsPacket(Packet): class ClientSettingsPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x04 if context.protocol_version >= 389 else \ return 0x05 if context.protocol_version >= 464 else \
0x04 if context.protocol_version >= 389 else \
0x03 if context.protocol_version >= 343 else \ 0x03 if context.protocol_version >= 343 else \
0x04 if context.protocol_version >= 336 else \ 0x04 if context.protocol_version >= 336 else \
0x05 if context.protocol_version >= 318 else \ 0x05 if context.protocol_version >= 318 else \
@ -26,7 +27,7 @@ class ClientSettingsPacket(Packet):
{'main_hand': VarInt} if context.protocol_version > 49 else {}]) {'main_hand': VarInt} if context.protocol_version > 49 else {}])
field_enum = classmethod( field_enum = classmethod(
lambda cls, field: { lambda cls, field, context: {
'chat_mode': cls.ChatMode, 'chat_mode': cls.ChatMode,
'displayed_skin_parts': cls.SkinParts, 'displayed_skin_parts': cls.SkinParts,
'main_hand': AbsoluteHand, 'main_hand': AbsoluteHand,

View File

@ -20,13 +20,31 @@ __all__ = (
class Type(object): class Type(object):
__slots__ = () __slots__ = ()
@staticmethod @classmethod
def read(file_object): def read_with_context(cls, file_object, _context):
raise NotImplementedError("Base data type not serializable") return cls.read(file_object)
@staticmethod @classmethod
def send(value, socket): def send_with_context(cls, value, socket, _context):
raise NotImplementedError("Base data type not serializable") return cls.send(value, socket)
@classmethod
def read(cls, file_object):
if cls.read_with_context == Type.read_with_context:
raise NotImplementedError('One of "read" or "read_with_context" '
'must be overridden in a subclass.')
else:
raise TypeError('This type requires a ConnectionContext: '
'call "read_with_context" instead of "read".')
@classmethod
def send(cls, value, socket):
if cls.send_with_context == Type.send_with_context:
raise NotImplementedError('One of "send" or "send_with_context" '
'must be overridden in a subclass.')
else:
raise TypeError('This type requires a ConnectionContext: '
'call "send_with_context" instead of "send".')
class Boolean(Type): class Boolean(Type):
@ -263,11 +281,16 @@ class Position(Type, Vector):
__slots__ = () __slots__ = ()
@staticmethod @staticmethod
def read(file_object): def read_with_context(file_object, context):
location = UnsignedLong.read(file_object) location = UnsignedLong.read(file_object)
x = int(location >> 38) x = int(location >> 38) # 26 most significant bits
y = int((location >> 26) & 0xFFF)
z = int(location & 0x3FFFFFF) if context.protocol_version >= 443:
z = int((location >> 12) & 0x3FFFFFF) # 26 intermediate bits
y = int(location & 0xFFF) # 12 least signficant bits
else:
y = int((location >> 26) & 0xFFF) # 12 intermediate bits
z = int(location & 0x3FFFFFF) # 26 least significant bits
if x >= pow(2, 25): if x >= pow(2, 25):
x -= pow(2, 26) x -= pow(2, 26)
@ -281,8 +304,10 @@ class Position(Type, Vector):
return Position(x=x, y=y, z=z) return Position(x=x, y=y, z=z)
@staticmethod @staticmethod
def send(position, socket): def send_with_context(position, socket, context):
# 'position' can be either a tuple or Position object. # 'position' can be either a tuple or Position object.
x, y, z = position x, y, z = position
value = ((x & 0x3FFFFFF) << 38) | ((y & 0xFFF) << 26) | (z & 0x3FFFFFF) value = ((x & 0x3FFFFFF) << 38 | (z & 0x3FFFFFF) << 12 | (y & 0xFFF)
if context.protocol_version >= 443 else
(x & 0x3FFFFFF) << 38 | (y & 0xFFF) << 26 | (z & 0x3FFFFFF))
UnsignedLong.send(value, socket) UnsignedLong.send(value, socket)

View File

@ -90,3 +90,50 @@ class PositionAndLook(MutableRecord):
def look(self, look): def look(self, look):
self.yaw, self.pitch = look self.yaw, self.pitch = look
look = property(lambda self: (self.yaw, self.pitch), look) look = property(lambda self: (self.yaw, self.pitch), look)
class descriptor(object):
"""Behaves identically to the builtin 'property' function of Python,
except that the getter, setter and deleter functions given by the
user are used as the raw __get__, __set__ and __delete__ functions
as defined in Python's descriptor protocol.
"""
__slots__ = '_fget', '_fset', '_fdel'
def __init__(self, fget=None, fset=None, fdel=None):
self._fget = fget if fget is not None else self._default_get
self._fset = fset if fset is not None else self._default_set
self._fdel = fdel if fdel is not None else self._default_del
def getter(self, fget):
self._fget = fget
return self
def setter(self, fset):
self._fset = fset
return self
def deleter(self, fdel):
self._fdel = fdel
return self
@staticmethod
def _default_get(instance, owner):
raise AttributeError('unreadable attribute')
@staticmethod
def _default_set(instance, value):
raise AttributeError("can't set attribute")
@staticmethod
def _default_del(instance):
raise AttributeError("can't delete attribute")
def __get__(self, instance, owner):
return self._fget(self, instance, owner)
def __set__(self, instance, value):
return self._fset(self, instance, value)
def __delete__(self, instance):
return self._fdel(self, instance)

View File

@ -103,7 +103,7 @@ class FakeClientHandler(object):
# Called upon entering the play state. # Called upon entering the play state.
self.write_packet(clientbound.play.JoinGamePacket( self.write_packet(clientbound.play.JoinGamePacket(
entity_id=0, game_mode=0, dimension=0, difficulty=2, max_players=1, entity_id=0, game_mode=0, dimension=0, difficulty=2, max_players=1,
level_type='default', reduced_debug_info=False)) level_type='default', reduced_debug_info=False, render_distance=9))
def handle_play_packet(self, packet): def handle_play_packet(self, packet):
# Called upon each packet received after handle_play_start() returns. # Called upon each packet received after handle_play_start() returns.

View File

@ -409,7 +409,7 @@ class VersionNegotiationEdgeCases(fake_server._FakeServerTest):
status_response = '{"description": {"text": "FakeServer"}}' status_response = '{"description": {"text": "FakeServer"}}'
class ClientHandler(fake_server.FakeClientHandler): class ClientHandler(fake_server.FakeClientHandler):
def _run_status(self): def handle_status(self, request_packet):
packet = clientbound.status.ResponsePacket() packet = clientbound.status.ResponsePacket()
packet.json_response = status_response packet.json_response = status_response
self.write_packet(packet) self.write_packet(packet)

View File

@ -187,7 +187,8 @@ class TestReadWritePackets(unittest.TestCase):
self._test_read_write_packet(packet) self._test_read_write_packet(packet)
def test_spawn_object_packet(self): def test_spawn_object_packet(self):
EntityType = clientbound.play.SpawnObjectPacket.EntityType EntityType = clientbound.play.SpawnObjectPacket.field_enum(
'type_id', self.context)
object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa' object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa'
pos_look = PositionAndLook(x=68.0, y=38.0, z=76.0, yaw=16, pitch=23) pos_look = PositionAndLook(x=68.0, y=38.0, z=76.0, yaw=16, pitch=23)
@ -195,6 +196,7 @@ class TestReadWritePackets(unittest.TestCase):
entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
packet = clientbound.play.SpawnObjectPacket( packet = clientbound.play.SpawnObjectPacket(
context=self.context,
x=pos_look.x, y=pos_look.y, z=pos_look.z, x=pos_look.x, y=pos_look.y, z=pos_look.z,
yaw=pos_look.yaw, pitch=pos_look.pitch, yaw=pos_look.yaw, pitch=pos_look.pitch,
velocity_x=velocity.x, velocity_y=velocity.y, velocity_x=velocity.x, velocity_y=velocity.y,
@ -207,9 +209,9 @@ class TestReadWritePackets(unittest.TestCase):
self.assertEqual(packet.type, type_name) self.assertEqual(packet.type, type_name)
packet2 = clientbound.play.SpawnObjectPacket( packet2 = clientbound.play.SpawnObjectPacket(
position_and_look=pos_look, velocity=velocity, context=self.context, position_and_look=pos_look,
type=type_name, object_uuid=object_uuid, velocity=velocity, type=type_name,
entity_id=entity_id, data=1) object_uuid=object_uuid, entity_id=entity_id, data=1)
self.assertEqual(packet.__dict__, packet2.__dict__) self.assertEqual(packet.__dict__, packet2.__dict__)
packet2.position = pos_look.position packet2.position = pos_look.position

View File

@ -51,6 +51,7 @@ class MapPacketTest(unittest.TestCase):
packet.map_id = 1 packet.map_id = 1
packet.scale = 42 packet.scale = 42
packet.is_tracking_position = True packet.is_tracking_position = True
packet.is_locked = False
packet.icons = [] packet.icons = []
d_name = u'Marshmallow' if context.protocol_version >= 364 else None d_name = u'Marshmallow' if context.protocol_version >= 364 else None
packet.icons.append(MapPacket.MapIcon( packet.icons.append(MapPacket.MapIcon(

View File

@ -8,6 +8,8 @@ from minecraft.networking.types import (
String as StringType, Position, TrailingByteArray, UnsignedLong, String as StringType, Position, TrailingByteArray, UnsignedLong,
) )
from minecraft.networking.packets import PacketBuffer from minecraft.networking.packets import PacketBuffer
from minecraft.networking.connection import ConnectionContext
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
TEST_DATA = { TEST_DATA = {
@ -33,18 +35,22 @@ TEST_DATA = {
class SerializationTest(unittest.TestCase): class SerializationTest(unittest.TestCase):
def test_serialization(self): def test_serialization(self):
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version)
for data_type in Type.__subclasses__(): for data_type in Type.__subclasses__():
if data_type in TEST_DATA: if data_type in TEST_DATA:
test_cases = TEST_DATA[data_type] test_cases = TEST_DATA[data_type]
for test_data in test_cases: for test_data in test_cases:
packet_buffer = PacketBuffer() packet_buffer = PacketBuffer()
data_type.send(test_data, packet_buffer) data_type.send_with_context(
test_data, packet_buffer, context)
packet_buffer.reset_cursor() packet_buffer.reset_cursor()
deserialized = data_type.read(packet_buffer) deserialized = data_type.read_with_context(
packet_buffer, context)
if data_type is FixedPointInteger: if data_type is FixedPointInteger:
self.assertAlmostEqual( self.assertAlmostEqual(
test_data, deserialized, delta=1.0/32.0) test_data, deserialized, delta=1.0/32.0)
@ -58,9 +64,21 @@ class SerializationTest(unittest.TestCase):
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
base_type.read(None) base_type.read(None)
with self.assertRaises(NotImplementedError):
base_type.read_with_context(None, None)
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
base_type.send(None, None) base_type.send(None, None)
with self.assertRaises(NotImplementedError):
base_type.send_with_context(None, None, None)
with self.assertRaises(TypeError):
Position.read(None)
with self.assertRaises(TypeError):
Position.send(None, None)
empty_socket = PacketBuffer() empty_socket = PacketBuffer()
with self.assertRaises(Exception): with self.assertRaises(Exception):
VarInt.read(empty_socket) VarInt.read(empty_socket)

View File

@ -52,6 +52,10 @@ deps =
{[testenv]deps} {[testenv]deps}
flake8 flake8
[flake8]
per-file-ignores =
*/clientbound/play/spawn_object_packet.py:E221,E222,E271,E272
[testenv:pylint-errors] [testenv:pylint-errors]
basepython = python3.6 basepython = python3.6
deps = deps =