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.12, 1.12.1, 1.12.2
* 1.13, 1.13.1, 1.13.2
* 1.14
In addition, some development snapshots and pre-release versions are supported:
`<minecraft/__init__.py>`_ contains a full list of supported Minecraft versions

View File

@ -6,132 +6,170 @@ with a MineCraft server.
__version__ = "0.5.0"
SUPPORTED_MINECRAFT_VERSIONS = {
'1.8': 47,
'1.8.1': 47,
'1.8.2': 47,
'1.8.3': 47,
'1.8.4': 47,
'1.8.5': 47,
'1.8.6': 47,
'1.8.7': 47,
'1.8.8': 47,
'1.8.9': 47,
'1.9': 107,
'1.9.1': 108,
'1.9.2': 109,
'1.9.3': 110,
'1.9.4': 110,
'1.10': 210,
'1.10.1': 210,
'1.10.2': 210,
'16w32a': 301,
'16w32b': 302,
'16w33a': 303,
'16w35a': 304,
'16w36a': 305,
'16w38a': 306,
'16w39a': 307,
'16w39b': 308,
'16w39c': 309,
'16w40a': 310,
'16w41a': 311,
'16w42a': 312,
'16w43a': 313,
'16w44a': 313,
'1.11-pre1': 314,
'1.11': 315,
'16w50a': 316,
'1.11.1': 316,
'1.11.2': 316,
'17w06a': 317,
'17w13a': 318,
'17w13b': 319,
'17w14a': 320,
'17w15a': 321,
'17w16a': 322,
'17w16b': 323,
'17w17a': 324,
'17w17b': 325,
'17w18a': 326,
'17w18b': 327,
'1.12-pre1': 328,
'1.12-pre2': 329,
'1.12-pre3': 330,
'1.12-pre4': 331,
'1.12-pre5': 332,
'1.12-pre6': 333,
'1.12-pre7': 334,
'1.12': 335,
'17w31a': 336,
'1.12.1-pre1': 337,
'1.12.1': 338,
'1.12.2-pre1': 339,
'1.12.2-pre2': 339,
'1.12.2': 340,
'17w43a': 341,
'17w43b': 342,
'17w45a': 343,
'17w45b': 344,
'17w46a': 345,
'17w47a': 346,
'17w47b': 347,
'17w48a': 348,
'17w49a': 349,
'17w49b': 350,
'17w50a': 351,
'18w01a': 352,
'18w02a': 353,
'18w03a': 354,
'18w03b': 355,
'18w05a': 356,
'18w06a': 357,
'18w07a': 358,
'18w07b': 359,
'18w07c': 360,
'18w08a': 361,
'18w08b': 362,
'18w09a': 363,
'18w10a': 364,
'18w10b': 365,
'18w10c': 366,
'18w10d': 367,
'18w11a': 368,
'18w14a': 369,
'18w14b': 370,
'18w15a': 371,
'18w16a': 372,
'18w19a': 373,
'18w19b': 374,
'18w20a': 375,
'18w20b': 376,
'18w20c': 377,
'18w21a': 378,
'18w21b': 379,
'18w22a': 380,
'18w22b': 381,
'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,
'18w30a': 394,
'18w30b': 395,
'18w31a': 396,
'18w32a': 397,
'18w33a': 398,
'1.13.1-pre1': 399,
'1.13.1-pre2': 400,
'1.13.1': 401,
'1.13.2-pre1': 402,
'1.13.2-pre2': 403,
'1.13.2': 404,
'1.8': 47,
'1.8.1': 47,
'1.8.2': 47,
'1.8.3': 47,
'1.8.4': 47,
'1.8.5': 47,
'1.8.6': 47,
'1.8.7': 47,
'1.8.8': 47,
'1.8.9': 47,
'1.9': 107,
'1.9.1': 108,
'1.9.2': 109,
'1.9.3': 110,
'1.9.4': 110,
'1.10': 210,
'1.10.1': 210,
'1.10.2': 210,
'16w32a': 301,
'16w32b': 302,
'16w33a': 303,
'16w35a': 304,
'16w36a': 305,
'16w38a': 306,
'16w39a': 307,
'16w39b': 308,
'16w39c': 309,
'16w40a': 310,
'16w41a': 311,
'16w42a': 312,
'16w43a': 313,
'16w44a': 313,
'1.11-pre1': 314,
'1.11': 315,
'16w50a': 316,
'1.11.1': 316,
'1.11.2': 316,
'17w06a': 317,
'17w13a': 318,
'17w13b': 319,
'17w14a': 320,
'17w15a': 321,
'17w16a': 322,
'17w16b': 323,
'17w17a': 324,
'17w17b': 325,
'17w18a': 326,
'17w18b': 327,
'1.12-pre1': 328,
'1.12-pre2': 329,
'1.12-pre3': 330,
'1.12-pre4': 331,
'1.12-pre5': 332,
'1.12-pre6': 333,
'1.12-pre7': 334,
'1.12': 335,
'17w31a': 336,
'1.12.1-pre1': 337,
'1.12.1': 338,
'1.12.2-pre1': 339,
'1.12.2-pre2': 339,
'1.12.2': 340,
'17w43a': 341,
'17w43b': 342,
'17w45a': 343,
'17w45b': 344,
'17w46a': 345,
'17w47a': 346,
'17w47b': 347,
'17w48a': 348,
'17w49a': 349,
'17w49b': 350,
'17w50a': 351,
'18w01a': 352,
'18w02a': 353,
'18w03a': 354,
'18w03b': 355,
'18w05a': 356,
'18w06a': 357,
'18w07a': 358,
'18w07b': 359,
'18w07c': 360,
'18w08a': 361,
'18w08b': 362,
'18w09a': 363,
'18w10a': 364,
'18w10b': 365,
'18w10c': 366,
'18w10d': 367,
'18w11a': 368,
'18w14a': 369,
'18w14b': 370,
'18w15a': 371,
'18w16a': 372,
'18w19a': 373,
'18w19b': 374,
'18w20a': 375,
'18w20b': 376,
'18w20c': 377,
'18w21a': 378,
'18w21b': 379,
'18w22a': 380,
'18w22b': 381,
'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,
'18w30a': 394,
'18w30b': 395,
'18w31a': 396,
'18w32a': 397,
'18w33a': 398,
'1.13.1-pre1': 399,
'1.13.1-pre2': 400,
'1.13.1': 401,
'1.13.2-pre1': 402,
'1.13.2-pre2': 403,
'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 = \

View File

@ -47,7 +47,8 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod
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 \
0x1F if context.protocol_version >= 332 else \
0x20 if context.protocol_version >= 318 else \
@ -70,9 +71,10 @@ class JoinGamePacket(Packet):
{'entity_id': Integer},
{'game_mode': UnsignedByte},
{'dimension': Integer if context.protocol_version >= 108 else Byte},
{'difficulty': UnsignedByte},
{'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'max_players': UnsignedByte},
{'level_type': String},
{'render_distance': VarInt} if context.protocol_version >= 468 else {},
{'reduced_debug_info': Boolean}])
@ -99,7 +101,8 @@ class ChatMessagePacket(Packet):
class DisconnectPacket(Packet):
@staticmethod
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 \
0x1B if context.protocol_version >= 318 else \
0x1A if context.protocol_version >= 107 else \
@ -145,7 +148,10 @@ class SpawnPlayerPacket(Packet):
class EntityVelocityPacket(Packet):
@staticmethod
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 \
0x3F if context.protocol_version >= 345 else \
0x3E if context.protocol_version >= 336 else \
@ -167,7 +173,10 @@ class EntityVelocityPacket(Packet):
class UpdateHealthPacket(Packet):
@staticmethod
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 \
0x42 if context.protocol_version >= 345 else \
0x41 if context.protocol_version >= 336 else \
@ -188,7 +197,8 @@ class UpdateHealthPacket(Packet):
class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod
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 \
0x19 if context.protocol_version >= 318 else \
0x18 if context.protocol_version >= 70 else \
@ -198,7 +208,11 @@ class PluginMessagePacket(AbstractPluginMessagePacket):
class PlayerListHeaderAndFooterPacket(Packet):
@staticmethod
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 \
0x49 if context.protocol_version >= 335 else \
0x47 if context.protocol_version >= 110 else \

View File

@ -8,7 +8,9 @@ from minecraft.networking.types import (
class CombatEventPacket(Packet):
@staticmethod
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 \
0x2D if context.protocol_version >= 336 else \
0x2C if context.protocol_version >= 332 else \

View File

@ -5,7 +5,8 @@ from minecraft.networking.packets import Packet
class ExplosionPacket(Packet):
@staticmethod
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 \
0x1C if context.protocol_version >= 332 else \
0x1D if context.protocol_version >= 318 else \

View File

@ -28,7 +28,7 @@ class MapPacket(Packet):
class Map(MutableRecord):
__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):
self.id = id
@ -38,6 +38,7 @@ class MapPacket(Packet):
self.height = height
self.pixels = bytearray(0 for i in range(width*height))
self.is_tracking_position = True
self.is_locked = False
class MapSet(object):
__slots__ = 'maps_by_id'
@ -58,6 +59,11 @@ class MapPacket(Packet):
else:
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)
self.icons = []
for i in range(icon_count):
@ -99,6 +105,7 @@ class MapPacket(Packet):
z = self.offset[1] + i // self.width
map.pixels[x + map.width * z] = self.pixels[i]
map.is_tracking_position = self.is_tracking_position
map.is_locked = self.is_locked
def apply_to_map_set(self, map_set):
map = map_set.maps_by_id.get(self.map_id)

View File

@ -8,7 +8,9 @@ from minecraft.networking.types import (
class PlayerListItemPacket(Packet):
@staticmethod
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 \
0x2E if context.protocol_version >= 336 else \
0x2D if context.protocol_version >= 332 else \

View File

@ -8,7 +8,9 @@ from minecraft.networking.types import (
class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
@staticmethod
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 \
0x30 if context.protocol_version >= 345 else \
0x2F if context.protocol_version >= 336 else \
@ -29,7 +31,7 @@ class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
])
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_Y = 0x02

View File

@ -1,4 +1,5 @@
from minecraft.networking.packets import Packet
from minecraft.networking.types.utility import descriptor
from minecraft.networking.types import (
VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector,
@ -14,40 +15,89 @@ class SpawnObjectPacket(Packet):
packet_name = 'spawn object'
class EntityType(Enum):
BOAT = 1
ITEM_STACK = 2
AREA_EFFECT_CLOUD = 3
MINECART = 10
ACTIVATED_TNT = 50
ENDERCRYSTAL = 51
ARROW = 60
SNOWBALL = 61
EGG = 62
FIREBALL = 63
FIRECHARGE = 64
ENDERPERL = 65
WITHER_SKULL = 66
SHULKER_BULLET = 67
LLAMA_SPIT = 68
FALLING_OBJECT = 70
ITEM_FRAMES = 71
EYE_OF_ENDER = 72
POTION = 73
EXP_BOTTLE = 75
FIREWORK_ROCKET = 76
LEASH_KNOT = 77
ARMORSTAND = 78
EVOCATION_FANGS = 79
FISHING_HOOK = 90
SPECTRAL_ARROW = 91
DRAGON_FIREBALL = 93
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):
ACTIVATED_TNT = 50 if pv < 458 else 55 # PrimedTnt
AREA_EFFECT_CLOUD = 3 if pv < 458 else 0
ARMORSTAND = 78 if pv < 458 else 1
ARROW = 60 if pv < 458 else 2
BOAT = 1 if pv < 458 else 5
DRAGON_FIREBALL = 93 if pv < 458 else 13
EGG = 62 if pv < 458 else 74 # ThrownEgg
ENDERCRYSTAL = 51 if pv < 458 else 16
ENDERPEARL = 65 if pv < 458 else 75 # ThrownEnderpearl
EVOCATION_FANGS = 79 if pv < 458 else 20
EXP_BOTTLE = 75 if pv < 458 else 76 # ThrownExpBottle
EYE_OF_ENDER = 72 if pv < 458 else 23 # EyeOfEnderSignal
FALLING_OBJECT = 70 if pv < 458 else 24 # FallingSand
FIREBALL = 63 if pv < 458 else 34 # Fireball (ghast)
FIRECHARGE = 64 if pv < 458 else 65 # SmallFireball (blaze)
FIREWORK_ROCKET = 76 if pv < 458 else 25 # FireworksRocketEntity
FISHING_HOOK = 90 if pv < 458 else 93 # Fishing bobber
ITEM_FRAMES = 71 if pv < 458 else 33 # ItemFrame
ITEM_STACK = 2 if pv < 458 else 32 # Item
LEASH_KNOT = 77 if pv < 458 else 35
LLAMA_SPIT = 68 if pv < 458 else 37
MINECART = 10 if pv < 458 else 39 # MinecartRideable
POTION = 73 if pv < 458 else 77 # ThrownPotion
SHULKER_BULLET = 67 if pv < 458 else 60
SNOWBALL = 61 if pv < 458 else 67
SPECTRAL_ARROW = 91 if pv < 458 else 68
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):
self.entity_id = VarInt.read(file_object)
if self.context.protocol_version >= 49:
self.object_uuid = UUID.read(file_object)
self.type_id = Byte.read(file_object)
if self.context.protocol_version >= 458:
self.type_id = VarInt.read(file_object)
else:
self.type_id = Byte.read(file_object)
xyz_type = Double if self.context.protocol_version >= 100 else Integer
for attr in 'x', 'y', 'z':
@ -64,7 +114,11 @@ class SpawnObjectPacket(Packet):
VarInt.send(self.entity_id, packet_buffer)
if self.context.protocol_version >= 49:
UUID.send(self.object_uuid, packet_buffer)
Byte.send(self.type_id, packet_buffer)
if self.context.protocol_version >= 458:
VarInt.send(self.type_id, packet_buffer)
else:
Byte.send(self.type_id, packet_buffer)
xyz_type = Double if self.context.protocol_version >= 100 else Integer
for coord in self.x, self.y, self.z:
@ -78,9 +132,20 @@ class SpawnObjectPacket(Packet):
Short.send(coord, packet_buffer)
# 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):
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)
type = property(lambda p: p.EntityType.name_from_value(p.type_id), type)
# Access the fields 'x', 'y', 'z' as a Vector.
def position(self, position):

View File

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

View File

@ -32,7 +32,9 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod
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 \
0x0B if context.protocol_version >= 345 else \
0x0A if context.protocol_version >= 343 else \
@ -45,7 +47,8 @@ class KeepAlivePacket(AbstractKeepAlivePacket):
class ChatPacket(Packet):
@staticmethod
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 \
0x02 if context.protocol_version >= 336 else \
0x03 if context.protocol_version >= 318 else \
@ -70,7 +73,9 @@ class ChatPacket(Packet):
class PositionAndLookPacket(Packet):
@staticmethod
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 \
0x0E if context.protocol_version >= 345 else \
0x0D if context.protocol_version >= 343 else \
@ -101,7 +106,9 @@ class TeleportConfirmPacket(Packet):
class AnimationPacket(Packet):
@staticmethod
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 \
0x1D if context.protocol_version >= 345 else \
0x1C if context.protocol_version >= 343 else \
@ -121,7 +128,8 @@ class AnimationPacket(Packet):
class ClientStatusPacket(Packet, Enum):
@staticmethod
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 \
0x03 if context.protocol_version >= 336 else \
0x04 if context.protocol_version >= 318 else \
@ -134,7 +142,7 @@ class ClientStatusPacket(Packet, Enum):
get_definition = staticmethod(lambda context: [
{'action_id': VarInt}])
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
REQUEST_STATS = 1
@ -145,7 +153,8 @@ class ClientStatusPacket(Packet, Enum):
class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod
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 \
0x08 if context.protocol_version >= 343 else \
0x09 if context.protocol_version >= 336 else \
@ -170,7 +179,9 @@ class PlayerBlockPlacementPacket(Packet):
@staticmethod
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 \
0x1F if context.protocol_version >= 345 else \
0x1E if context.protocol_version >= 343 else \
@ -184,12 +195,15 @@ class PlayerBlockPlacementPacket(Packet):
@staticmethod
def get_definition(context):
return [
{'hand': VarInt} if context.protocol_version >= 453 else {},
{'location': Position},
{'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},
{'y': 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.

View File

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

View File

@ -20,13 +20,31 @@ __all__ = (
class Type(object):
__slots__ = ()
@staticmethod
def read(file_object):
raise NotImplementedError("Base data type not serializable")
@classmethod
def read_with_context(cls, file_object, _context):
return cls.read(file_object)
@staticmethod
def send(value, socket):
raise NotImplementedError("Base data type not serializable")
@classmethod
def send_with_context(cls, value, socket, _context):
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):
@ -263,11 +281,16 @@ class Position(Type, Vector):
__slots__ = ()
@staticmethod
def read(file_object):
def read_with_context(file_object, context):
location = UnsignedLong.read(file_object)
x = int(location >> 38)
y = int((location >> 26) & 0xFFF)
z = int(location & 0x3FFFFFF)
x = int(location >> 38) # 26 most significant bits
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):
x -= pow(2, 26)
@ -281,8 +304,10 @@ class Position(Type, Vector):
return Position(x=x, y=y, z=z)
@staticmethod
def send(position, socket):
def send_with_context(position, socket, context):
# 'position' can be either a tuple or Position object.
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)

View File

@ -90,3 +90,50 @@ class PositionAndLook(MutableRecord):
def look(self, look):
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.
self.write_packet(clientbound.play.JoinGamePacket(
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):
# 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"}}'
class ClientHandler(fake_server.FakeClientHandler):
def _run_status(self):
def handle_status(self, request_packet):
packet = clientbound.status.ResponsePacket()
packet.json_response = status_response
self.write_packet(packet)

View File

@ -187,7 +187,8 @@ class TestReadWritePackets(unittest.TestCase):
self._test_read_write_packet(packet)
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'
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
packet = clientbound.play.SpawnObjectPacket(
context=self.context,
x=pos_look.x, y=pos_look.y, z=pos_look.z,
yaw=pos_look.yaw, pitch=pos_look.pitch,
velocity_x=velocity.x, velocity_y=velocity.y,
@ -207,9 +209,9 @@ class TestReadWritePackets(unittest.TestCase):
self.assertEqual(packet.type, type_name)
packet2 = clientbound.play.SpawnObjectPacket(
position_and_look=pos_look, velocity=velocity,
type=type_name, object_uuid=object_uuid,
entity_id=entity_id, data=1)
context=self.context, position_and_look=pos_look,
velocity=velocity, type=type_name,
object_uuid=object_uuid, entity_id=entity_id, data=1)
self.assertEqual(packet.__dict__, packet2.__dict__)
packet2.position = pos_look.position

View File

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

View File

@ -8,6 +8,8 @@ from minecraft.networking.types import (
String as StringType, Position, TrailingByteArray, UnsignedLong,
)
from minecraft.networking.packets import PacketBuffer
from minecraft.networking.connection import ConnectionContext
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
TEST_DATA = {
@ -33,34 +35,50 @@ TEST_DATA = {
class SerializationTest(unittest.TestCase):
def test_serialization(self):
for data_type in Type.__subclasses__():
if data_type in TEST_DATA:
test_cases = TEST_DATA[data_type]
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version)
for test_data in test_cases:
packet_buffer = PacketBuffer()
data_type.send(test_data, packet_buffer)
packet_buffer.reset_cursor()
for data_type in Type.__subclasses__():
if data_type in TEST_DATA:
test_cases = TEST_DATA[data_type]
deserialized = data_type.read(packet_buffer)
if data_type is FixedPointInteger:
self.assertAlmostEqual(
test_data, deserialized, delta=1.0/32.0)
elif data_type is Float or data_type is Double:
self.assertAlmostEquals(test_data, deserialized, 3)
else:
self.assertEqual(test_data, deserialized)
for test_data in test_cases:
packet_buffer = PacketBuffer()
data_type.send_with_context(
test_data, packet_buffer, context)
packet_buffer.reset_cursor()
deserialized = data_type.read_with_context(
packet_buffer, context)
if data_type is FixedPointInteger:
self.assertAlmostEqual(
test_data, deserialized, delta=1.0/32.0)
elif data_type is Float or data_type is Double:
self.assertAlmostEquals(test_data, deserialized, 3)
else:
self.assertEqual(test_data, deserialized)
def test_exceptions(self):
base_type = Type()
with self.assertRaises(NotImplementedError):
base_type.read(None)
with self.assertRaises(NotImplementedError):
base_type.read_with_context(None, None)
with self.assertRaises(NotImplementedError):
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()
with self.assertRaises(Exception):
VarInt.read(empty_socket)

View File

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