diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 45be9ddb..547eaccf 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -4,8 +4,9 @@ from minecraft.networking.types import ( Integer, FixedPointInteger, Angle, UnsignedByte, Byte, Boolean, UUID, - Short, VarInt, Double, Float, String, Enum, Difficulty, Dimension, - GameMode, Vector, Direction, PositionAndLook, multi_attribute_alias, + Short, VarInt, Double, Float, String, Position, Enum, Difficulty, + Dimension, GameMode, Vector, Direction, PositionAndLook, + multi_attribute_alias, ) from .combat_event_packet import CombatEventPacket @@ -17,6 +18,8 @@ from .explosion_packet import ExplosionPacket from .sound_effect_packet import SoundEffectPacket from .face_player_packet import FacePlayerPacket +from .destroy_entities_packet import DestroyEntitiesPacket +from .spawn_mob_packet import SpawnMobPacket # Formerly known as state_playing_clientbound. @@ -41,7 +44,12 @@ def get_packets(context): RespawnPacket, PluginMessagePacket, PlayerListHeaderAndFooterPacket, - EntityLookPacket + EntityLookPacket, + EntityPacket, + DestroyEntitiesPacket, + SpawnMobPacket, + BlockActionPacket, + EntityHeadLookPacket, } if context.protocol_version <= 47: packets |= { @@ -50,10 +58,11 @@ def get_packets(context): if context.protocol_version >= 94: packets |= { SoundEffectPacket, + VehicleMovePacket, } if context.protocol_version >= 352: packets |= { - FacePlayerPacket + FacePlayerPacket, } return packets @@ -321,3 +330,95 @@ def get_id(context): {'pitch': Angle}, {'on_ground': Boolean} ] + + +class EntityPacket(Packet): + @staticmethod + def get_id(context): + return 0x2B if context.protocol_version >= 471 else \ + 0x27 if context.protocol_version >= 389 else \ + 0x26 if context.protocol_version >= 345 else \ + 0x25 if context.protocol_version >= 332 else \ + 0x29 if context.protocol_version >= 318 else \ + 0x28 if context.protocol_version >= 94 else \ + 0x29 if context.protocol_version >= 70 else \ + 0x14 + + packet_name = "entity" + definition = [{"entity_id": VarInt}] + + +class VehicleMovePacket(Packet): + @staticmethod + def get_id(context): + return 0x2C if context.protocol_version >= 471 else \ + 0x2B if context.protocol_version >= 389 else \ + 0x2A if context.protocol_version >= 345 else \ + 0x29 if context.protocol_version >= 332 else \ + 0x2A if context.protocol_version >= 318 else \ + 0x29 # Note: Packet added in protocol version 94 + + packet_name = "vehicle move clientbound" + definition = [ + {'x': Double}, + {'y': Double}, + {'z': Double}, + {'yaw': Float}, + {'pitch': Float}, + ] + + # Access the 'x', 'y', 'z' fields as a Vector tuple. + position = multi_attribute_alias(Vector, 'x', 'y', 'z') + + # Access the 'yaw', 'pitch' fields as a Direction tuple. + look = multi_attribute_alias(Direction, 'yaw', 'pitch') + + # Access the 'x', 'y', 'z', 'yaw', 'pitch' fields as a PositionAndLook. + # NOTE: modifying the object retrieved from this property will not change + # the packet; it can only be changed by attribute or property assignment. + position_and_look = multi_attribute_alias( + PositionAndLook, 'x', 'y', 'z', 'yaw', 'pitch') + + +class BlockActionPacket(Packet): + @staticmethod + def get_id(context): + return 0x0A if context.protocol_version >= 332 else \ + 0x0B if context.protocol_version >= 318 else \ + 0x0A if context.protocol_version >= 70 else \ + 0x25 if context.protocol_version >= 69 else \ + 0x24 + + packet_name = "block action" + get_definition = staticmethod(lambda context: [ + {'location': Position}, + {'block_type': VarInt} if context.protocol_version == 347 else {}, + {'action_id': UnsignedByte}, # TODO Interpret action_id and + {'action_param': UnsignedByte}, # action_param fields. + {'block_type': VarInt} if context.protocol_version != 347 else {}, + ]) + + +class EntityHeadLookPacket(Packet): + @staticmethod + def get_id(context): + return 0x3B if context.protocol_version >= 471 else \ + 0x39 if context.protocol_version >= 461 else \ + 0x3A if context.protocol_version >= 451 else \ + 0x39 if context.protocol_version >= 389 else \ + 0x38 if context.protocol_version >= 352 else \ + 0x37 if context.protocol_version >= 345 else \ + 0x36 if context.protocol_version >= 336 else \ + 0x35 if context.protocol_version >= 332 else \ + 0x36 if context.protocol_version >= 318 else \ + 0x34 if context.protocol_version >= 70 else \ + 0x19 + + packet_name = 'entity head look' + + fields = 'entity_id', 'head_yaw' + + definition = [ + {'entity_id': VarInt}, + {'head_yaw': Angle}, + ] diff --git a/minecraft/networking/packets/clientbound/play/destroy_entities_packet.py b/minecraft/networking/packets/clientbound/play/destroy_entities_packet.py new file mode 100644 index 00000000..6536f8b3 --- /dev/null +++ b/minecraft/networking/packets/clientbound/play/destroy_entities_packet.py @@ -0,0 +1,33 @@ +from minecraft.networking.packets import Packet + +from minecraft.networking.types import VarInt + + +class DestroyEntitiesPacket(Packet): + @staticmethod + def get_id(context): + return 0x37 if context.protocol_version >= 471 else \ + 0x35 if context.protocol_version >= 461 else \ + 0x36 if context.protocol_version >= 451 else \ + 0x35 if context.protocol_version >= 389 else \ + 0x34 if context.protocol_version >= 352 else \ + 0x33 if context.protocol_version >= 345 else \ + 0x32 if context.protocol_version >= 336 else \ + 0x31 if context.protocol_version >= 332 else \ + 0x32 if context.protocol_version >= 318 else \ + 0x30 if context.protocol_version >= 70 else \ + 0x13 + + packet_name = 'destroy entities' + + fields = 'entity_ids', + + def read(self, file_object): + self.entity_ids = [VarInt.read(file_object) + for i in range(VarInt.read(file_object))] + + def write_fields(self, packet_buffer): + count = len(self.entity_ids) + VarInt.send(count, packet_buffer) + for entity_id in self.entity_ids: + VarInt.send(entity_id, packet_buffer) diff --git a/minecraft/networking/packets/clientbound/play/spawn_mob_packet.py b/minecraft/networking/packets/clientbound/play/spawn_mob_packet.py new file mode 100644 index 00000000..660dac2a --- /dev/null +++ b/minecraft/networking/packets/clientbound/play/spawn_mob_packet.py @@ -0,0 +1,314 @@ +from minecraft.networking.packets import Packet + +from minecraft.networking.types import ( + VarInt, UUID, Double, Integer, Angle, Short, UnsignedByte, Enum, Vector, + Direction, PositionAndLook, LookAndDirection, PositionLookAndDirection, + multi_attribute_alias, descriptor +) + + +class SpawnMobPacket(Packet): + @staticmethod + def get_id(context): + return 0x03 if context.protocol_version >= 70 else \ + 0x0F + + packet_name = 'spawn mob' + + fields = ('entity_id', 'entity_uuid', 'type_id', 'x', 'y', 'z', 'pitch', + 'yaw', 'head_pitch', 'velocity_x', 'velocity_y', 'velocity_z') + + @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( + '"SpawnMobPacket.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 ' + '"SpawnMobPacket" instance with its "context" property ' + 'set.\n\n' + '2. Call "SpawnMobPacket.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): + BAT = 3 if pv >= 393 else \ + 65 + BLAZE = 4 if pv >= 393 else \ + 61 + CAVE_SPIDER = 7 if pv >= 447 else \ + 6 if pv >= 393 else \ + 59 + CHICKEN = 8 if pv >= 447 else \ + 7 if pv >= 393 else \ + 93 + COW = 10 if pv >= 447 else \ + 9 if pv >= 393 else \ + 92 + CREEPER = 11 if pv >= 447 else \ + 10 if pv >= 393 else \ + 50 + ENDER_DRAGON = 18 if pv >= 447 else \ + 17 if pv >= 393 else \ + 63 + ENDERMAN = 19 if pv >= 447 else \ + 18 if pv >= 393 else \ + 58 + ENDERMITE = 20 if pv >= 447 else \ + 19 if pv >= 393 else \ + 67 + GHAST = 28 if pv >= 447 else \ + 26 if pv >= 393 else \ + 56 + GIANT = 29 if pv >= 447 else \ + 27 if pv >= 393 else \ + 53 + GUARDIAN = 30 if pv >= 447 else \ + 28 if pv >= 393 else \ + 68 + MAGMA_CUBE = 40 if pv >= 447 else \ + 38 if pv >= 393 else \ + 62 # Lava Slime + MOOSHROOM = 49 if pv >= 447 else \ + 47 if pv >= 393 else \ + 96 # Mushroom Cow + OCELOT = 50 if pv >= 447 else \ + 48 if pv >= 393 else \ + 98 # Ozelot + PIG = 54 if pv >= 447 else \ + 51 if pv >= 393 else \ + 90 + ZOMBIE_PIGMAN = 56 if pv >= 447 else \ + 53 if pv >= 393 else \ + 57 + RABBIT = 59 if pv >= 447 else \ + 56 if pv >= 393 else \ + 101 + SHEEP = 61 if pv >= 447 else \ + 58 if pv >= 393 else \ + 91 + SILVERFISH = 64 if pv >= 447 else \ + 61 if pv >= 393 else \ + 60 + SKELETON = 65 if pv >= 447 else \ + 62 if pv >= 393 else \ + 51 + SLIME = 67 if pv >= 447 else \ + 64 if pv >= 393 else \ + 55 + SNOW_GOLEM = 69 if pv >= 447 else \ + 66 if pv >= 393 else \ + 97 # Snow Man + SPIDER = 72 if pv >= 447 else \ + 69 if pv >= 393 else \ + 52 + SQUID = 73 if pv >= 447 else \ + 70 if pv >= 393 else \ + 94 + VILLAGER = 84 if pv >= 447 else \ + 79 if pv >= 393 else \ + 120 + IRON_GOLEM = 85 if pv >= 447 else \ + 80 if pv >= 393 else \ + 99 # Villager Golem + WITCH = 89 if pv >= 447 else \ + 82 if pv >= 393 else \ + 66 + WITHER = 90 if pv >= 447 else \ + 83 if pv >= 393 else \ + 64 # Wither Boss + # Issue with Wither Skeletons in PrismarineJS? + # Not present in some protocol versions so + # only 99% certain this enum is 100% accurate. + WITHER_SKELETON = 91 if pv >= 447 else \ + 84 if pv >= 393 else \ + 5 + WOLF = 93 if pv >= 447 else \ + 86 if pv >= 393 else \ + 95 + ZOMBIE = 94 if pv >= 447 else \ + 87 if pv >= 393 else \ + 54 + if pv >= 447: + TRADER_LLAMA = 75 + if pv >= 393: + COD = 9 if pv >= 447 else \ + 8 + DOLPHIN = 13 if pv >= 447 else \ + 12 + DROWNED = 15 if pv >= 447 else \ + 14 + PUFFERFISH = 55 if pv >= 447 else \ + 52 + SALMON = 60 if pv >= 447 else \ + 57 + TROPICAL_FISH = 76 if pv >= 447 else \ + 72 + TURTLE = 77 if pv >= 447 else \ + 73 + PHANTOM = 97 if pv >= 447 else \ + 90 + if pv >= 335: + ILLUSIONER = 33 if pv >= 447 else \ + 31 if pv >= 393 else \ + 37 # Illusion Illager + PARROT = 53 if pv >= 447 else \ + 50 if pv >= 393 else \ + 105 + if pv >= 315: + DONKEY = 12 if pv >= 447 else \ + 11 if pv >= 393 else \ + 31 + ELDER_GUARDIAN = 16 if pv >= 447 else \ + 15 if pv >= 393 else \ + 4 + EVOKER = 22 if pv >= 447 else \ + 21 if pv >= 393 else \ + 34 + HORSE = 31 if pv >= 447 else \ + 29 if pv >= 393 else \ + 100 + HUSK = 32 if pv >= 447 else \ + 30 if pv >= 393 else \ + 23 + LLAMA = 38 if pv >= 447 else \ + 36 if pv >= 393 else \ + 103 + MULE = 48 if pv >= 447 else \ + 46 if pv >= 393 else \ + 32 + POLAR_BEAR = 57 if pv >= 447 else \ + 54 if pv >= 393 else \ + 102 + SKELETON_HORSE = 66 if pv >= 447 else \ + 63 if pv >= 393 else \ + 28 + STRAY = 74 if pv >= 447 else \ + 71 if pv >= 393 else \ + 6 + VEX = 83 if pv >= 447 else \ + 78 if pv >= 393 else \ + 35 + VINDICATOR = 86 if pv >= 447 else \ + 81 if pv >= 393 else \ + 36 # Vindication Illager + ZOMBIE_HORSE = 95 if pv >= 447 else \ + 88 if pv >= 393 else \ + 29 + ZOMBIE_VILLAGER = 96 if pv >= 447 else \ + 89 if pv >= 393 else \ + 27 + if pv >= 76: + SHULKER = 62 if pv >= 447 else \ + 59 if pv >= 393 else \ + 69 + + setattr(cls, name, EntityType) + return EntityType + + def read(self, file_object): + self.entity_id = VarInt.read(file_object) + if self.context.protocol_version >= 69: + self.entity_uuid = UUID.read(file_object) + + if self.context.protocol_version >= 301: + self.type_id = VarInt.read(file_object) + else: + self.type_id = UnsignedByte.read(file_object) + + xyz_type = Double if self.context.protocol_version >= 97 else Integer + for attr in 'x', 'y', 'z': + setattr(self, attr, xyz_type.read(file_object)) + + for attr in 'pitch', 'yaw', 'head_pitch': + setattr(self, attr, Angle.read(file_object)) + + for attr in 'velocity_x', 'velocity_y', 'velocity_z': + setattr(self, attr, Short.read(file_object)) + + # TODO: read entity metadata + + def write_fields(self, packet_buffer): + VarInt.send(self.entity_id, packet_buffer) + if self.context.protocol_version >= 69: + UUID.send(self.entity_uuid, packet_buffer) + + if self.context.protocol_version >= 301: + VarInt.send(self.type_id, packet_buffer) + else: + UnsignedByte.send(self.type_id, packet_buffer) + + # pylint: disable=no-member + xyz_type = Double if self.context.protocol_version >= 97 else Integer + for coord in self.x, self.y, self.z: + xyz_type.send(coord, packet_buffer) + + for angle in self.pitch, self.yaw, self.head_pitch: + Angle.send(angle, packet_buffer) + + for velocity in self.velocity_x, self.velocity_y, self.velocity_z: + Short.send(velocity, packet_buffer) + + # TODO: write entity metadata + + # 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.') + # pylint: disable=no-member + self.type_id = getattr(self.EntityType, type_name) + + @type.deleter + def type(self): + del self.type_id + + # Access the 'x', 'y', 'z' fields as a Vector. + position = multi_attribute_alias(Vector, 'x', 'y', 'z') + + # Access the 'yaw', 'pitch' fields as a Direction. + look = multi_attribute_alias(Direction, 'yaw', 'pitch') + + # Access the 'yaw', 'pitch' 'head_pitch' fields as a LookAndDirection. + look_and_direction = multi_attribute_alias(LookAndDirection, + 'yaw', 'pitch', 'head_pitch') + + # Access the 'x', 'y', 'z', 'pitch', 'yaw' fields as a PositionAndLook. + # NOTE: modifying the object retrieved from this property will not change + # the packet; it can only be changed by attribute or property assignment. + position_and_look = multi_attribute_alias( + PositionAndLook, x='x', y='y', z='z', yaw='yaw', pitch='pitch') + + # Access the 'x', 'y', 'z', 'pitch', 'yaw', 'head_pitch' fields as a + # PositionLookAndDirection + position_look_and_direction = multi_attribute_alias( + PositionLookAndDirection, x='x', y='y', z='z', yaw='yaw', + pitch='pitch', head_pitch='head_pitch') + + # Access the 'velocity_{x,y,z}' fields as a Vector. + velocity = multi_attribute_alias( + Vector, 'velocity_x', 'velocity_y', 'velocity_z') diff --git a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py index d856f3fa..05b74598 100644 --- a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py +++ b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py @@ -124,8 +124,8 @@ def write_fields(self, packet_buffer): xyz_type = Double if self.context.protocol_version >= 100 else Integer for coord in self.x, self.y, self.z: xyz_type.send(coord, packet_buffer) - for coord in self.pitch, self.yaw: - Angle.send(coord, packet_buffer) + for angle in self.pitch, self.yaw: + Angle.send(angle, packet_buffer) Integer.send(self.data, packet_buffer) if self.context.protocol_version >= 49 or self.data > 0: diff --git a/minecraft/networking/packets/serverbound/play/__init__.py b/minecraft/networking/packets/serverbound/play/__init__.py index fff5ecdc..dc93ec29 100644 --- a/minecraft/networking/packets/serverbound/play/__init__.py +++ b/minecraft/networking/packets/serverbound/play/__init__.py @@ -9,6 +9,7 @@ ) from .client_settings_packet import ClientSettingsPacket +from .use_entity_packet import UseEntityPacket # Formerly known as state_playing_serverbound. @@ -22,11 +23,16 @@ def get_packets(context): ClientSettingsPacket, PluginMessagePacket, PlayerBlockPlacementPacket, + UseEntityPacket } if context.protocol_version >= 69: packets |= { UseItemPacket, } + if context.protocol_version >= 94: + packets |= { + VehicleMovePacket, + } if context.protocol_version >= 107: packets |= { TeleportConfirmPacket, @@ -251,3 +257,37 @@ def get_id(context): {'hand': VarInt}]) Hand = RelativeHand + + +class VehicleMovePacket(Packet): + @staticmethod + def get_id(context): + return 0x15 if context.protocol_version >= 464 else \ + 0x13 if context.protocol_version >= 389 else \ + 0x11 if context.protocol_version >= 386 else \ + 0x10 if context.protocol_version >= 345 else \ + 0x0F if context.protocol_version >= 343 else \ + 0x10 if context.protocol_version >= 336 else \ + 0x11 if context.protocol_version >= 318 else \ + 0x10 # Note: Packet added in protocol version 94 + + packet_name = "vehicle move serverbound" + definition = [ + {'x': Double}, + {'y': Double}, + {'z': Double}, + {'yaw': Float}, + {'pitch': Float}, + ] + + # Access the 'x', 'y', 'z' fields as a Vector tuple. + position = multi_attribute_alias(Vector, 'x', 'y', 'z') + + # Access the 'yaw', 'pitch' fields as a Direction tuple. + look = multi_attribute_alias(Direction, 'yaw', 'pitch') + + # Access the 'x', 'y', 'z', 'yaw', 'pitch' fields as a PositionAndLook. + # NOTE: modifying the object retrieved from this property will not change + # the packet; it can only be changed by attribute or property assignment. + position_and_look = multi_attribute_alias( + PositionAndLook, 'x', 'y', 'z', 'yaw', 'pitch') diff --git a/minecraft/networking/packets/serverbound/play/use_entity_packet.py b/minecraft/networking/packets/serverbound/play/use_entity_packet.py new file mode 100644 index 00000000..6566159e --- /dev/null +++ b/minecraft/networking/packets/serverbound/play/use_entity_packet.py @@ -0,0 +1,54 @@ +from minecraft.networking.packets import Packet +from minecraft.networking.types import ( + VarInt, Float, RelativeHand, ClickType, multi_attribute_alias, Vector +) + + +class UseEntityPacket(Packet): + @staticmethod + def get_id(context): + return 0x0E if context.protocol_version >= 464 else \ + 0x0D if context.protocol_version >= 389 else \ + 0x0B if context.protocol_version >= 386 else \ + 0x0A if context.protocol_version >= 345 else \ + 0x09 if context.protocol_version >= 343 else \ + 0x0A if context.protocol_version >= 336 else \ + 0x0B if context.protocol_version >= 318 else \ + 0x0A if context.protocol_version >= 94 else \ + 0x09 if context.protocol_version >= 70 else \ + 0x02 + + packet_name = "use entity" + + fields = ('entity_id', 'click_type', 'target_x', 'target_y', 'target_z', + 'hand') + + def read(self, file_object): + self.entity_id = VarInt.read(file_object) + self.click_type = VarInt.read(file_object) + + if self.click_type is ClickType.INTERACT_AT: + for attr in 'target_x', 'target_y', 'target_z': + setattr(self, attr, Float.read(file_object)) + + if self.click_type in [ClickType.INTERACT_AT, ClickType.INTERACT]: + self.hand = VarInt.read(file_object) + + def write_fields(self, packet_buffer): + # pylint: disable=no-member + VarInt.send(self.entity_id, packet_buffer) + VarInt.send(self.click_type, packet_buffer) + + if self.click_type is ClickType.INTERACT_AT: + for attr in self.target_x, self.target_y, self.target_z: + Float.send(attr, packet_buffer) + + if self.click_type in [ClickType.INTERACT_AT, ClickType.INTERACT]: + VarInt.send(self.hand, packet_buffer) + + ClickType = ClickType + + Hand = RelativeHand + + # Access the 'target_{x,y,z}' fields as a Vector. + target = multi_attribute_alias(Vector, 'target_x', 'target_y', 'target_z') diff --git a/minecraft/networking/types/enum.py b/minecraft/networking/types/enum.py index 61aa2384..98451a98 100644 --- a/minecraft/networking/types/enum.py +++ b/minecraft/networking/types/enum.py @@ -12,7 +12,7 @@ __all__ = ( 'Enum', 'BitFieldEnum', 'AbsoluteHand', 'RelativeHand', 'BlockFace', - 'Difficulty', 'Dimension', 'GameMode', 'OriginPoint' + 'Difficulty', 'Dimension', 'GameMode', 'OriginPoint', 'ClickType' ) @@ -113,3 +113,11 @@ class GameMode(Enum): class OriginPoint(Enum): FEET = 0 EYES = 1 + + +# Designation of a player's click action. +# Used in Use Entity Packet +class ClickType(Enum): + INTERACT = 0 + ATTACK = 1 + INTERACT_AT = 2 diff --git a/minecraft/networking/types/utility.py b/minecraft/networking/types/utility.py index 29164379..a4a6b96a 100644 --- a/minecraft/networking/types/utility.py +++ b/minecraft/networking/types/utility.py @@ -8,7 +8,8 @@ __all__ = ( - 'Vector', 'MutableRecord', 'Direction', 'PositionAndLook', 'descriptor', + 'Vector', 'MutableRecord', 'Direction', 'PositionAndLook', + 'LookAndDirection', 'PositionLookAndDirection', 'descriptor', 'attribute_alias', 'multi_attribute_alias', ) @@ -196,10 +197,30 @@ def __delete__(self, instance): class PositionAndLook(MutableRecord): """A mutable record containing 3 spatial position coordinates - and 2 rotational coordinates for a look direction. + and 2 rotational components for a look direction. """ __slots__ = 'x', 'y', 'z', 'yaw', 'pitch' position = multi_attribute_alias(Vector, 'x', 'y', 'z') look = multi_attribute_alias(Direction, 'yaw', 'pitch') + + +LookAndDirection = namedtuple('LookAndDirection', + ('yaw', 'pitch', 'head_pitch')) + + +class PositionLookAndDirection(MutableRecord): + """ + A mutable record containing 3 spatial position coordinates, + 2 rotational components and an additional rotational component for + the head of the object. + """ + __slots__ = 'x', 'y', 'z', 'yaw', 'pitch', 'head_pitch' + + position = multi_attribute_alias(Vector, 'x', 'y', 'z') + + look = multi_attribute_alias(Direction, 'yaw', 'pitch') + + look_and_direction = multi_attribute_alias(LookAndDirection, + 'yaw', 'pitch', 'head_pitch') diff --git a/tests/test_packets.py b/tests/test_packets.py index a0044887..cb4765ce 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -5,11 +5,13 @@ import struct from zlib import decompress from random import choice +from collections import OrderedDict from minecraft import SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS from minecraft.networking.connection import ConnectionContext from minecraft.networking.types import ( - VarInt, Enum, Vector, PositionAndLook, OriginPoint, + VarInt, Enum, Vector, PositionAndLook, PositionLookAndDirection, + LookAndDirection, OriginPoint, ClickType, RelativeHand ) from minecraft.networking.packets import ( Packet, PacketBuffer, PacketListener, KeepAlivePacket, serverbound, @@ -156,6 +158,50 @@ class Beta(Enum): class TestReadWritePackets(unittest.TestCase): maxDiff = None + def test_entity_head_look_packet(self): + for protocol_version in TEST_VERSIONS: + logging.debug('protocol_version = %r' % protocol_version) + context = ConnectionContext(protocol_version=protocol_version) + packet = clientbound.play.EntityHeadLookPacket( + entity_id=897, head_yaw=93.87) + self.assertEqual( + str(packet), + 'EntityHeadLookPacket(entity_id=897, head_yaw=93.87)' + ) + self._test_read_write_packet(packet, context, head_yaw=360/256) + + def test_destroy_entities_packet(self): + for protocol_version in TEST_VERSIONS: + logging.debug('protocol_version = %r' % protocol_version) + context = ConnectionContext(protocol_version=protocol_version) + packet = clientbound.play.DestroyEntitiesPacket( + entity_ids=[593, 388, 1856]) + self.assertEqual( + str(packet), + 'DestroyEntitiesPacket(entity_ids=[593, 388, 1856])' + ) + self._test_read_write_packet(packet, context) + + def test_use_entity_packet(self): + for protocol_version in TEST_VERSIONS: + logging.debug('protocol_version = %r' % protocol_version) + context = ConnectionContext(protocol_version=protocol_version) + packet = serverbound.play.UseEntityPacket(context) + packet.entity_id = 495 + packet.click_type = ClickType.INTERACT_AT + packet.target = 51.0, 2.0, 50.0 + packet.hand = RelativeHand.MAIN + + self.assertEqual( + str(packet), + '0x%02X UseEntityPacket(entity_id=495, ' + 'click_type=INTERACT_AT, ' + 'target_x=51.0, target_y=2.0, target_z=50.0, hand=MAIN)' % + packet.id + ) + + self._test_read_write_packet(packet, context) + def test_explosion_packet(self): Record = clientbound.play.ExplosionPacket.Record packet = clientbound.play.ExplosionPacket( @@ -226,6 +272,98 @@ def test_multi_block_change_packet(self): self._test_read_write_packet(packet) + def test_spawn_mob_packet(self): + for protocol_version in TEST_VERSIONS: + logging.debug('protocol_version = %r' % protocol_version) + context = ConnectionContext(protocol_version=protocol_version) + + EntityType = clientbound.play.SpawnMobPacket.field_enum( + 'type_id', context) + + pos_look_dir = PositionLookAndDirection( + position=(Vector(48.35, 82.0, 11.28) if protocol_version >= 97 + else Vector(48, 82, 11)), + look_and_direction=(LookAndDirection( + yaw=87.9, pitch=90, head_pitch=19.07))) + + velocity = Vector(100, 98, 2) + entity_id, type_name, type_id = 573, 'CREEPER', EntityType.CREEPER + + packet = clientbound.play.SpawnMobPacket( + context=context, + x=pos_look_dir.x, y=pos_look_dir.y, z=pos_look_dir.z, + yaw=pos_look_dir.yaw, pitch=pos_look_dir.pitch, + head_pitch=pos_look_dir.head_pitch, + velocity_x=velocity.x, velocity_y=velocity.y, + velocity_z=velocity.z, + entity_id=entity_id, type_id=type_id) + + if protocol_version >= 69: + entity_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa' + packet.entity_uuid = entity_uuid + self.assertEqual(packet.entity_uuid, entity_uuid) + self.assertEqual(packet.position_look_and_direction, pos_look_dir) + self.assertEqual(packet.position, pos_look_dir.position) + self.assertEqual(packet.look_and_direction, + pos_look_dir.look_and_direction) + self.assertEqual(packet.look, pos_look_dir.look) + self.assertEqual(packet.velocity, velocity) + self.assertEqual(packet.type, type_name) + + self.assertEqual( + str(packet), + "0x%02X SpawnMobPacket(entity_id=573, " + "entity_uuid='d9568851-85bc-4a10-8d6a-261d130626fa', " + "type_id=CREEPER, x=48.35, y=82.0, z=11.28, " + "pitch=90, yaw=87.9, head_pitch=19.07, " + "velocity_x=100, velocity_y=98, velocity_z=2)" + % packet.id if protocol_version >= 97 else + "0x%02X SpawnMobPacket(entity_id=573, " + "entity_uuid='d9568851-85bc-4a10-8d6a-261d130626fa', " + "type_id=CREEPER, x=48, y=82, z=11, " + "pitch=90, yaw=87.9, head_pitch=19.07, " + "velocity_x=100, velocity_y=98, velocity_z=2)" + % packet.id if protocol_version >= 69 else + "0x%02X SpawnMobPacket(entity_id=573, " + "type_id=CREEPER, x=48, y=82, z=11, " + "pitch=90, yaw=87.9, head_pitch=19.07, " + "velocity_x=100, velocity_y=98, velocity_z=2)" % packet.id) + + # Assert no repeating values / names for each protocol_version + # in EntityType Enum + names = [] + values = [] + for name, name_value in packet.field_enum( + 'type_id', context).__dict__.items(): + if name.isupper(): + names.append(name) + values.append(name_value) + + # Remove duplicates + no_repeats_names = list(OrderedDict.fromkeys(names)) + no_repeats_values = list(OrderedDict.fromkeys(values)) + self.assertEqual(names, no_repeats_names) + self.assertEqual(values, no_repeats_values) + + packet2 = clientbound.play.SpawnMobPacket( + context=context, + position_look_and_direction=pos_look_dir, + velocity=velocity, + entity_id=entity_id, type_id=type_id) + + if protocol_version >= 69: + packet2.entity_uuid = entity_uuid + self.assertEqual(packet.__dict__, packet2.__dict__) + packet2.position = pos_look_dir.position + self.assertEqual(packet2.position, pos_look_dir.position) + + self._test_read_write_packet(packet, context, + yaw=360/256, pitch=360/256, + head_pitch=360/256) + self._test_read_write_packet(packet2, context, + yaw=360/256, pitch=360/256, + head_pitch=360/256) + def test_spawn_object_packet(self): for protocol_version in TEST_VERSIONS: logging.debug('protocol_version = %r' % protocol_version) diff --git a/tests/test_utility_types.py b/tests/test_utility_types.py index 3f8cc357..e2dd2b07 100644 --- a/tests/test_utility_types.py +++ b/tests/test_utility_types.py @@ -1,7 +1,8 @@ import unittest from minecraft.networking.types import ( - Enum, BitFieldEnum, Vector, Position, PositionAndLook + Enum, BitFieldEnum, Vector, Position, PositionAndLook, + PositionLookAndDirection ) @@ -73,3 +74,26 @@ def test_properties(self): self.assertFalse(pos_look_1 != pos_look_2) pos_look_1.position += Vector(1, 1, 1) self.assertTrue(pos_look_1 != pos_look_2) + + +class PositionLookAndDirectionTest(unittest.TestCase): + """ This also tests the MutableRecord base type. """ + def test_properties(self): + pos_look_1 = PositionLookAndDirection(position=(1, 2, 3), + look_and_direction=(4, 5, 6)) + pos_look_2 = PositionLookAndDirection(x=1, y=2, z=3, + yaw=4, pitch=5, head_pitch=6) + string_repr = ('PositionLookAndDirection(x=1, y=2, z=3, ' + 'yaw=4, pitch=5, head_pitch=6)') + + self.assertEqual(pos_look_1, pos_look_2) + self.assertEqual(pos_look_1.position, pos_look_1.position) + self.assertEqual(pos_look_1.look, pos_look_2.look) + self.assertEqual(hash(pos_look_1), hash(pos_look_2)) + self.assertEqual(pos_look_1.look_and_direction, + pos_look_2.look_and_direction) + self.assertEqual(str(pos_look_1), string_repr) + + self.assertFalse(pos_look_1 != pos_look_2) + pos_look_1.position += Vector(1, 1, 1) + self.assertTrue(pos_look_1 != pos_look_2) diff --git a/tox.ini b/tox.ini index c1ffb046..46e321fa 100644 --- a/tox.ini +++ b/tox.ini @@ -56,6 +56,7 @@ deps = [flake8] per-file-ignores = */clientbound/play/spawn_object_packet.py:E221,E222,E271,E272 + */clientbound/play/spawn_mob_packet.py:E127,E128,E221,E222,E271,E272 [testenv:pylint-errors] basepython = python3.6