diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 7d31b44..2225033 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -3,8 +3,8 @@ from minecraft.networking.packets import ( ) from minecraft.networking.types import ( - Integer, FixedPointInteger, UnsignedByte, Byte, Boolean, UUID, Short, - VarInt, Double, Float, String, Enum, Difficulty, Dimension, GameMode, + Integer, FixedPointInteger, Angle, UnsignedByte, Byte, Boolean, UUID, + Short, VarInt, Double, Float, String, Enum, Difficulty, Dimension, GameMode ) from .combat_event_packet import CombatEventPacket @@ -15,6 +15,7 @@ from .spawn_object_packet import SpawnObjectPacket from .block_change_packet import BlockChangePacket, MultiBlockChangePacket from .explosion_packet import ExplosionPacket from .sound_effect_packet import SoundEffectPacket +from .face_player_packet import FacePlayerPacket # Formerly known as state_playing_clientbound. @@ -39,6 +40,7 @@ def get_packets(context): RespawnPacket, PluginMessagePacket, PlayerListHeaderAndFooterPacket, + EntityLookPacket } if context.protocol_version <= 47: packets |= { @@ -48,6 +50,10 @@ def get_packets(context): packets |= { SoundEffectPacket, } + if context.protocol_version >= 352: + packets |= { + FacePlayerPacket + } return packets @@ -173,8 +179,8 @@ class SpawnPlayerPacket(Packet): else {'y': FixedPointInteger}, {'z': Double} if context.protocol_version >= 100 else {'z': FixedPointInteger}, - {'yaw': Float}, - {'pitch': Float}, + {'yaw': Angle}, + {'pitch': Angle}, # TODO: read entity metadata {'current_item': Short} if context.protocol_version <= 49 else {} ]) @@ -291,3 +297,22 @@ class PlayerListHeaderAndFooterPacket(Packet): definition = [ {'header': String}, {'footer': String}] + + +class EntityLookPacket(Packet): + @staticmethod + def get_id(context): + return 0x2A if context.protocol_version >= 389 else \ + 0x29 if context.protocol_version >= 345 else \ + 0x28 if context.protocol_version >= 318 else \ + 0x27 if context.protocol_version >= 94 else \ + 0x28 if context.protocol_version >= 70 else \ + 0x16 + + packet_name = 'entity look' + definition = [ + {'entity_id': VarInt}, + {'yaw': Angle}, + {'pitch': Angle}, + {'on_ground': Boolean} + ] diff --git a/minecraft/networking/packets/clientbound/play/face_player_packet.py b/minecraft/networking/packets/clientbound/play/face_player_packet.py new file mode 100644 index 0000000..39b76ec --- /dev/null +++ b/minecraft/networking/packets/clientbound/play/face_player_packet.py @@ -0,0 +1,69 @@ +from minecraft.networking.types import ( + VarInt, Double, Boolean, OriginPoint, Vector, multi_attribute_alias +) + +from minecraft.networking.packets import Packet + + +class FacePlayerPacket(Packet): + @staticmethod + def get_id(context): + return 0x34 if context.protocol_version >= 471 else \ + 0x32 if context.protocol_version >= 451 else \ + 0x31 if context.protocol_version >= 389 else \ + 0x30 + + packet_name = 'face player' + + # Access the 'x', 'y', 'z' fields as a Vector. + target = multi_attribute_alias(Vector, 'x', 'y', 'z') + + def read(self, file_object): + if self.context.protocol_version >= 353: + self.origin = VarInt.read(file_object) + self.x = Double.read(file_object) + self.y = Double.read(file_object) + self.z = Double.read(file_object) + is_entity = Boolean.read(file_object) + if is_entity: + # If the entity given by entity ID cannot be found, + # this packet should be treated as if is_entity was false. + self.entity_id = VarInt.read(file_object) + self.entity_origin = VarInt.read(file_object) + else: + self.entity_id = None + + else: # Protocol version 352 + is_entity = Boolean.read(file_object) + self.entity_id = VarInt.read(file_object) if is_entity else None + if not is_entity: + self.x = Double.read(file_object) + self.y = Double.read(file_object) + self.z = Double.read(file_object) + + def write_fields(self, packet_buffer): + if self.context.protocol_version >= 353: + VarInt.send(self.origin, packet_buffer) + Double.send(self.x, packet_buffer) + Double.send(self.y, packet_buffer) + Double.send(self.z, packet_buffer) + if self.entity_id is not None: + Boolean.send(True, packet_buffer) + VarInt.send(self.entity_id, packet_buffer) + VarInt.send(self.entity_origin, packet_buffer) + else: + Boolean.send(False, packet_buffer) + + else: # Protocol version 352 + if self.entity_id is not None: + Boolean.send(True, packet_buffer) + VarInt.send(self.entity_id, packet_buffer) + else: + Boolean.send(False, packet_buffer) + Double.send(self.x, packet_buffer) + Double.send(self.y, packet_buffer) + Double.send(self.z, packet_buffer) + + # These aliases declare the Enum type corresponding to each field: + Origin = OriginPoint + EntityOrigin = OriginPoint diff --git a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py index c8e48b7..3c93de9 100644 --- a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py +++ b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py @@ -2,7 +2,7 @@ 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, + VarInt, UUID, Byte, Double, Integer, Angle, Short, Enum, Vector, PositionAndLook, attribute_alias, multi_attribute_alias, ) @@ -103,7 +103,7 @@ class SpawnObjectPacket(Packet): for attr in 'x', 'y', 'z': setattr(self, attr, xyz_type.read(file_object)) for attr in 'pitch', 'yaw': - setattr(self, attr, UnsignedByte.read(file_object)) + setattr(self, attr, Angle.read(file_object)) self.data = Integer.read(file_object) if self.context.protocol_version >= 49 or self.data > 0: @@ -125,7 +125,7 @@ class SpawnObjectPacket(Packet): for coord in self.x, self.y, self.z: xyz_type.send(coord, packet_buffer) for coord in self.pitch, self.yaw: - UnsignedByte.send(coord, packet_buffer) + Angle.send(coord, packet_buffer) Integer.send(self.data, packet_buffer) if self.context.protocol_version >= 49 or self.data > 0: diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index ab43198..4ccf627 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -11,9 +11,10 @@ from .utility import Vector __all__ = ( 'Type', 'Boolean', 'UnsignedByte', 'Byte', 'Short', 'UnsignedShort', - 'Integer', 'FixedPointInteger', 'VarInt', 'Long', 'UnsignedLong', 'Float', - 'Double', 'ShortPrefixedByteArray', 'VarIntPrefixedByteArray', - 'TrailingByteArray', 'String', 'UUID', 'Position', + 'Integer', 'FixedPointInteger', 'Angle', 'VarInt', 'Long', + 'UnsignedLong', 'Float', 'Double', 'ShortPrefixedByteArray', + 'VarIntPrefixedByteArray', 'TrailingByteArray', 'String', 'UUID', + 'Position', ) @@ -117,6 +118,18 @@ class FixedPointInteger(Type): Integer.send(int(value * 32), socket) +class Angle(Type): + @staticmethod + def read(file_object): + # Linearly transform angle in steps of 1/256 into steps of 1/360 + return 360 * UnsignedByte.read(file_object) / 256 + + @staticmethod + def send(value, socket): + # Normalize angle between 0 and 255 and convert to int. + UnsignedByte.send(round(256 * ((value % 360) / 360)), socket) + + class VarInt(Type): @staticmethod def read(file_object): diff --git a/minecraft/networking/types/enum.py b/minecraft/networking/types/enum.py index 8d43374..61aa238 100644 --- a/minecraft/networking/types/enum.py +++ b/minecraft/networking/types/enum.py @@ -12,7 +12,7 @@ from .utility import Vector __all__ = ( 'Enum', 'BitFieldEnum', 'AbsoluteHand', 'RelativeHand', 'BlockFace', - 'Difficulty', 'Dimension', 'GameMode' + 'Difficulty', 'Dimension', 'GameMode', 'OriginPoint' ) @@ -106,3 +106,10 @@ class GameMode(Enum): CREATIVE = 1 ADVENTURE = 2 SPECTATOR = 3 + + +# Currently designates an entity's feet or eyes. +# Used in the Face Player Packet +class OriginPoint(Enum): + FEET = 0 + EYES = 1 diff --git a/tests/test_packets.py b/tests/test_packets.py index f6736f3..3db2fb2 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -9,7 +9,7 @@ from random import choice from minecraft import SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS from minecraft.networking.connection import ConnectionContext from minecraft.networking.types import ( - VarInt, Enum, Vector, PositionAndLook + VarInt, Enum, Vector, PositionAndLook, OriginPoint, ) from minecraft.networking.packets import ( Packet, PacketBuffer, PacketListener, KeepAlivePacket, serverbound, @@ -199,9 +199,9 @@ class TestReadWritePackets(unittest.TestCase): 'type_id', context) pos_look = PositionAndLook( - position=(Vector(68.0, 38.0, 76.0) if context.protocol_version - >= 100 else Vector(68, 38, 76)), - yaw=16, pitch=23) + position=(Vector(68.0, 38.0, 76.0) if protocol_version >= 100 + else Vector(68, 38, 76)), + yaw=263.494, pitch=180) velocity = Vector(21, 55, 41) entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG @@ -212,7 +212,7 @@ class TestReadWritePackets(unittest.TestCase): velocity_x=velocity.x, velocity_y=velocity.y, velocity_z=velocity.z, entity_id=entity_id, type_id=type_id, data=1) - if context.protocol_version >= 49: + if protocol_version >= 49: object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa' packet.object_uuid = object_uuid self.assertEqual(packet.objectUUID, object_uuid) @@ -225,7 +225,7 @@ class TestReadWritePackets(unittest.TestCase): context=context, position_and_look=pos_look, velocity=velocity, type=type_name, entity_id=entity_id, data=1) - if context.protocol_version >= 49: + if protocol_version >= 49: packet2.object_uuid = object_uuid self.assertEqual(packet.__dict__, packet2.__dict__) @@ -233,10 +233,12 @@ class TestReadWritePackets(unittest.TestCase): self.assertEqual(packet.position, packet2.position) packet2.data = 0 - if context.protocol_version < 49: + if protocol_version < 49: del packet2.velocity - self._test_read_write_packet(packet, context) - self._test_read_write_packet(packet2, context) + self._test_read_write_packet(packet, context, + yaw=360/256, pitch=360/256) + self._test_read_write_packet(packet2, context, + yaw=360/256, pitch=360/256) def test_sound_effect_packet(self): for protocol_version in TEST_VERSIONS: @@ -245,7 +247,7 @@ class TestReadWritePackets(unittest.TestCase): packet = clientbound.play.SoundEffectPacket( sound_id=545, effect_position=Vector(0.125, 300.0, 50.5), volume=0.75) - if context.protocol_version >= 201: + if protocol_version >= 201: packet.pitch = struct.unpack('f', struct.pack('f', 1.5))[0] else: packet.pitch = int(1.5 / 63.5) * 63.5 @@ -254,7 +256,30 @@ class TestReadWritePackets(unittest.TestCase): clientbound.play.SoundEffectPacket.SoundCategory.NEUTRAL self._test_read_write_packet(packet, context) - def _test_read_write_packet(self, packet_in, context=None): + def test_face_player_packet(self): + for protocol_version in TEST_VERSIONS: + context = ConnectionContext(protocol_version=protocol_version) + + packet = clientbound.play.FacePlayerPacket() + packet.target = 1.0, -2.0, 3.5 + packet.entity_id = None + if protocol_version >= 353: + packet.origin = OriginPoint.EYES + self._test_read_write_packet(packet, context) + + packet.entity_id = 123 + if protocol_version >= 353: + packet.entity_origin = OriginPoint.FEET + else: + del packet.target + self._test_read_write_packet(packet, context) + + def _test_read_write_packet(self, packet_in, context=None, **kwargs): + """ + If kwargs are specified, the key will be tested against the + respective delta value. Useful for testing FixedPointNumbers + where there is precision lost in the resulting value. + """ if context is None: for protocol_version in TEST_VERSIONS: logging.debug('protocol_version = %r' % protocol_version) @@ -272,4 +297,12 @@ class TestReadWritePackets(unittest.TestCase): packet_out = type(packet_in)(context=context) packet_out.read(packet_buffer) self.assertIs(type(packet_in), type(packet_out)) + + for packet_attr, precision in kwargs.items(): + packet_attribute_in = packet_in.__dict__.pop(packet_attr) + packet_attribute_out = packet_out.__dict__.pop(packet_attr) + self.assertAlmostEqual(packet_attribute_in, + packet_attribute_out, + delta=precision) + self.assertEqual(packet_in.__dict__, packet_out.__dict__) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index dc07572..b991913 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -3,7 +3,7 @@ import unittest from minecraft.networking.types import ( Type, Boolean, UnsignedByte, Byte, Short, UnsignedShort, - Integer, FixedPointInteger, VarInt, Long, Float, Double, + Integer, FixedPointInteger, Angle, VarInt, Long, Float, Double, ShortPrefixedByteArray, VarIntPrefixedByteArray, UUID, String as StringType, Position, TrailingByteArray, UnsignedLong, ) @@ -25,6 +25,7 @@ TEST_DATA = { UnsignedLong: [0, 400], Integer: [-1000, 1000], FixedPointInteger: [float(-13098.3435), float(-0.83), float(1000)], + Angle: [0, 360.0, 720, 47.12947238973, -108.7], VarInt: [1, 250, 50000, 10000000], Long: [50000000], Float: [21.000301], @@ -58,8 +59,12 @@ class SerializationTest(unittest.TestCase): if data_type is FixedPointInteger: self.assertAlmostEqual( test_data, deserialized, delta=1.0/32.0) + elif data_type is Angle: + self.assertAlmostEqual(test_data % 360, + deserialized, + delta=360/256) elif data_type is Float or data_type is Double: - self.assertAlmostEquals(test_data, deserialized, 3) + self.assertAlmostEqual(test_data, deserialized, 3) else: self.assertEqual(test_data, deserialized)