diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 2225033..45be9dd 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -4,7 +4,8 @@ from minecraft.networking.packets import ( from minecraft.networking.types import ( Integer, FixedPointInteger, Angle, UnsignedByte, Byte, Boolean, UUID, - Short, VarInt, Double, Float, String, Enum, Difficulty, Dimension, GameMode + Short, VarInt, Double, Float, String, Enum, Difficulty, Dimension, + GameMode, Vector, Direction, PositionAndLook, multi_attribute_alias, ) from .combat_event_packet import CombatEventPacket @@ -91,13 +92,9 @@ class JoinGamePacket(Packet): {'reduced_debug_info': Boolean}, ]) - # JoinGamePacket.Difficulty is an alias for Difficulty + # These aliases declare the Enum type corresponding to each field: Difficulty = Difficulty - - # JoinGamePacket.Gamemode is an alias for Gamemode GameMode = GameMode - - # JoinGamePacket.Dimension is an alias for Dimension Dimension = Dimension @@ -115,7 +112,7 @@ class ServerDifficultyPacket(Packet): {'is_locked': Boolean} if context.protocol_version >= 464 else {}, ]) - # ServerDifficultyPacket.Difficulty is an alias for Difficulty + # These aliases declare the Enum type corresponding to each field: Difficulty = Difficulty @@ -181,10 +178,22 @@ class SpawnPlayerPacket(Packet): else {'z': FixedPointInteger}, {'yaw': Angle}, {'pitch': Angle}, + {'current_item': Short} if context.protocol_version <= 49 else {}, # TODO: read entity metadata - {'current_item': Short} if context.protocol_version <= 49 else {} ]) + # 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 EntityVelocityPacket(Packet): @staticmethod @@ -258,13 +267,9 @@ class RespawnPacket(Packet): {'level_type': String}, ]) - # RespawnPacket.Difficulty is an alias for Difficulty. + # These aliases declare the Enum type corresponding to each field: Difficulty = Difficulty - - # RespawnPacket.Dimension is an alias for Dimension. Dimension = Dimension - - # RespawnPacket.Gamemode is an alias for Gamemode. GameMode = GameMode diff --git a/minecraft/networking/packets/clientbound/play/block_change_packet.py b/minecraft/networking/packets/clientbound/play/block_change_packet.py index 3825cb1..42764b8 100644 --- a/minecraft/networking/packets/clientbound/play/block_change_packet.py +++ b/minecraft/networking/packets/clientbound/play/block_change_packet.py @@ -53,6 +53,11 @@ class MultiBlockChangePacket(Packet): packet_name = 'multi block change' + fields = 'chunk_x', 'chunk_z', 'records' + + # Access the 'chunk_x' and 'chunk_z' fields as a tuple. + chunk_pos = multi_attribute_alias(tuple, 'chunk_x', 'chunk_z') + class Record(MutableRecord): __slots__ = 'x', 'y', 'z', 'block_state_id' diff --git a/minecraft/networking/packets/clientbound/play/combat_event_packet.py b/minecraft/networking/packets/clientbound/play/combat_event_packet.py index 75fd06c..a3fc48a 100644 --- a/minecraft/networking/packets/clientbound/play/combat_event_packet.py +++ b/minecraft/networking/packets/clientbound/play/combat_event_packet.py @@ -22,6 +22,8 @@ class CombatEventPacket(Packet): packet_name = 'combat event' + fields = 'event', + # The abstract type of the 'event' field of this packet. class EventType(MutableRecord): __slots__ = () diff --git a/minecraft/networking/packets/clientbound/play/explosion_packet.py b/minecraft/networking/packets/clientbound/play/explosion_packet.py index 9419917..25b2e4b 100644 --- a/minecraft/networking/packets/clientbound/play/explosion_packet.py +++ b/minecraft/networking/packets/clientbound/play/explosion_packet.py @@ -18,14 +18,19 @@ class ExplosionPacket(Packet): packet_name = 'explosion' - class Record(Vector): - __slots__ = () + fields = 'x', 'y', 'z', 'radius', 'records', \ + 'player_motion_x', 'player_motion_y', 'player_motion_z' + # Access the 'x', 'y', 'z' fields as a Vector tuple. position = multi_attribute_alias(Vector, 'x', 'y', 'z') + # Access the 'player_motion_{x,y,z}' fields as a Vector tuple. player_motion = multi_attribute_alias( Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z') + class Record(Vector): + __slots__ = () + def read(self, file_object): self.x = Float.read(file_object) self.y = Float.read(file_object) diff --git a/minecraft/networking/packets/clientbound/play/face_player_packet.py b/minecraft/networking/packets/clientbound/play/face_player_packet.py index 39b76ec..5ac351c 100644 --- a/minecraft/networking/packets/clientbound/play/face_player_packet.py +++ b/minecraft/networking/packets/clientbound/play/face_player_packet.py @@ -15,7 +15,13 @@ class FacePlayerPacket(Packet): packet_name = 'face player' - # Access the 'x', 'y', 'z' fields as a Vector. + @property + def fields(self): + return ('origin', 'x', 'y', 'z', 'entity_id', 'entity_origin') \ + if self.context.protocol_version >= 353 else \ + ('entity_id', 'x', 'y', 'z') + + # Access the 'x', 'y', 'z' fields as a Vector tuple. target = multi_attribute_alias(Vector, 'x', 'y', 'z') def read(self, file_object): diff --git a/minecraft/networking/packets/clientbound/play/map_packet.py b/minecraft/networking/packets/clientbound/play/map_packet.py index 88be7f6..6ee625f 100644 --- a/minecraft/networking/packets/clientbound/play/map_packet.py +++ b/minecraft/networking/packets/clientbound/play/map_packet.py @@ -17,6 +17,20 @@ class MapPacket(Packet): packet_name = 'map' + @property + def fields(self): + fields = 'id', 'scale', 'icons', 'width', 'height', 'pixels' + if self.context.protocol_version >= 107: + fields += 'is_tracking_position', + if self.context.protocol_version >= 452: + fields += 'is_locked', + return fields + + def field_string(self, field): + if field == 'pixels' and isinstance(self.pixels, bytearray): + return 'bytearray(...)' + return super(MapPacket, self).field_string(field) + class MapIcon(MutableRecord): __slots__ = 'type', 'direction', 'location', 'display_name' @@ -43,11 +57,11 @@ class MapPacket(Packet): class MapSet(object): __slots__ = 'maps_by_id' - def __init__(self): - self.maps_by_id = dict() + def __init__(self, *maps): + self.maps_by_id = {map.id: map for map in maps} def __repr__(self): - maps = (str(map) for map in self.maps_by_id.values()) + maps = (repr(map) for map in self.maps_by_id.values()) return 'MapSet(%s)' % ', '.join(maps) def read(self, file_object): @@ -143,9 +157,3 @@ class MapPacket(Packet): UnsignedByte.send(self.offset[0], packet_buffer) # x UnsignedByte.send(self.offset[1], packet_buffer) # z VarIntPrefixedByteArray.send(self.pixels, packet_buffer) - - def __repr__(self): - return '%sMapPacket(%s)' % ( - ('0x%02X ' % self.id) if self.id is not None else '', - ', '.join('%s=%r' % (k, v) for (k, v) in self.__dict__.items() - if k not in ('pixels', '_context', 'id', 'definition'))) diff --git a/minecraft/networking/packets/clientbound/play/player_list_item_packet.py b/minecraft/networking/packets/clientbound/play/player_list_item_packet.py index 62c4d37..c1003c3 100644 --- a/minecraft/networking/packets/clientbound/play/player_list_item_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_list_item_packet.py @@ -1,7 +1,7 @@ from minecraft.networking.packets import Packet from minecraft.networking.types import ( - String, Boolean, UUID, VarInt, + String, Boolean, UUID, VarInt, MutableRecord, ) @@ -20,21 +20,24 @@ class PlayerListItemPacket(Packet): packet_name = "player list item" + fields = 'action_type', 'actions' + + def field_string(self, field): + if field == 'action_type': + return self.action_type.__name__ + return super(PlayerListItemPacket, self).field_string(field) + class PlayerList(object): __slots__ = 'players_by_uuid' - def __init__(self): - self.players_by_uuid = dict() + def __init__(self, *items): + self.players_by_uuid = {item.uuid: item for item in items} - class PlayerListItem(object): + class PlayerListItem(MutableRecord): __slots__ = ( 'uuid', 'name', 'properties', 'gamemode', 'ping', 'display_name') - def __init__(self, **kwds): - for key, val in kwds.items(): - setattr(self, key, val) - - class PlayerProperty(object): + class PlayerProperty(MutableRecord): __slots__ = 'name', 'value', 'signature' def read(self, file_object): @@ -46,33 +49,44 @@ class PlayerListItemPacket(Packet): else: self.signature = None - class Action(object): - __slots__ = 'uuid' + def send(self, packet_buffer): + String.send(self.name, packet_buffer) + String.send(self.value, packet_buffer) + if self.signature is not None: + Boolean.send(True, packet_buffer) + String.send(self.signature, packet_buffer) + else: + Boolean.send(False, packet_buffer) + + class Action(MutableRecord): + __slots__ = 'uuid', def read(self, file_object): self.uuid = UUID.read(file_object) self._read(file_object) + def send(self, packet_buffer): + UUID.send(self.uuid, packet_buffer) + self._send(packet_buffer) + def _read(self, file_object): raise NotImplementedError( 'This abstract method must be overridden in a subclass.') + def _send(self, packet_buffer): + raise NotImplementedError( + 'This abstract method must be overridden in a subclass.') + @classmethod def type_from_id(cls, action_id): - subcls = { - 0: PlayerListItemPacket.AddPlayerAction, - 1: PlayerListItemPacket.UpdateGameModeAction, - 2: PlayerListItemPacket.UpdateLatencyAction, - 3: PlayerListItemPacket.UpdateDisplayNameAction, - 4: PlayerListItemPacket.RemovePlayerAction - }.get(action_id) - if subcls is None: - raise ValueError("Unknown player list action ID: %s." - % action_id) - return subcls + for subcls in cls.__subclasses__(): + if subcls.action_id == action_id: + return subcls + raise ValueError("Unknown player list action ID: %s." % action_id) class AddPlayerAction(Action): __slots__ = 'name', 'properties', 'gamemode', 'ping', 'display_name' + action_id = 0 def _read(self, file_object): self.name = String.read(file_object) @@ -90,6 +104,19 @@ class PlayerListItemPacket(Packet): else: self.display_name = None + def _send(self, packet_buffer): + String.send(self.name, packet_buffer) + VarInt.send(len(self.properties), packet_buffer) + for property in self.properties: + property.send(packet_buffer) + VarInt.send(self.gamemode, packet_buffer) + VarInt.send(self.ping, packet_buffer) + if self.display_name is not None: + Boolean.send(True, packet_buffer) + String.send(self.display_name, packet_buffer) + else: + Boolean.send(False, packet_buffer) + def apply(self, player_list): player = PlayerListItemPacket.PlayerListItem( uuid=self.uuid, @@ -102,10 +129,14 @@ class PlayerListItemPacket(Packet): class UpdateGameModeAction(Action): __slots__ = 'gamemode' + action_id = 1 def _read(self, file_object): self.gamemode = VarInt.read(file_object) + def _send(self, packet_buffer): + VarInt.send(self.gamemode, packet_buffer) + def apply(self, player_list): player = player_list.players_by_uuid.get(self.uuid) if player: @@ -113,10 +144,14 @@ class PlayerListItemPacket(Packet): class UpdateLatencyAction(Action): __slots__ = 'ping' + action_id = 2 def _read(self, file_object): self.ping = VarInt.read(file_object) + def _send(self, packet_buffer): + VarInt.send(self.ping, packet_buffer) + def apply(self, player_list): player = player_list.players_by_uuid.get(self.uuid) if player: @@ -124,6 +159,7 @@ class PlayerListItemPacket(Packet): class UpdateDisplayNameAction(Action): __slots__ = 'display_name' + action_id = 3 def _read(self, file_object): has_display_name = Boolean.read(file_object) @@ -132,15 +168,27 @@ class PlayerListItemPacket(Packet): else: self.display_name = None + def _send(self, packet_buffer): + if self.display_name is not None: + Boolean.send(True, packet_buffer) + String.send(self.display_name, packet_buffer) + else: + Boolean.send(False, packet_buffer) + def apply(self, player_list): player = player_list.players_by_uuid.get(self.uuid) if player: player.display_name = self.display_name class RemovePlayerAction(Action): + action_id = 4 + def _read(self, file_object): pass + def _send(self, packet_buffer): + pass + def apply(self, player_list): if self.uuid in player_list.players_by_uuid: del player_list.players_by_uuid[self.uuid] @@ -155,9 +203,12 @@ class PlayerListItemPacket(Packet): action.read(file_object) self.actions.append(action) + def write_fields(self, packet_buffer): + VarInt.send(self.action_type.action_id, packet_buffer) + VarInt.send(len(self.actions), packet_buffer) + for action in self.actions: + action.send(packet_buffer) + def apply(self, player_list): for action in self.actions: action.apply(player_list) - - def write_fields(self, packet_buffer): - raise NotImplementedError diff --git a/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py b/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py index 8f6f5cc..508875d 100644 --- a/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py @@ -1,7 +1,8 @@ from minecraft.networking.packets import Packet from minecraft.networking.types import ( - Double, Float, Byte, VarInt, BitFieldEnum, PositionAndLook + Double, Float, Byte, VarInt, BitFieldEnum, Vector, Direction, + PositionAndLook, multi_attribute_alias, ) @@ -30,6 +31,18 @@ class PlayerPositionAndLookPacket(Packet, BitFieldEnum): {'teleport_id': VarInt} if context.protocol_version >= 107 else {}, ]) + # 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') + field_enum = classmethod( lambda cls, field, context: cls if field == 'flags' else None) diff --git a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py index 3c93de9..d856f3f 100644 --- a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py +++ b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py @@ -3,7 +3,7 @@ from minecraft.networking.types.utility import descriptor from minecraft.networking.types import ( VarInt, UUID, Byte, Double, Integer, Angle, Short, Enum, Vector, - PositionAndLook, attribute_alias, multi_attribute_alias, + Direction, PositionAndLook, attribute_alias, multi_attribute_alias, ) @@ -15,8 +15,8 @@ class SpawnObjectPacket(Packet): packet_name = 'spawn object' - fields = ('entity_id', 'object_uuid', 'type_id', - 'x', 'y', 'z', 'pitch', 'yaw') + fields = ('entity_id', 'object_uuid', 'type_id', 'x', 'y', 'z', 'pitch', + 'yaw', 'data', 'velocity_x', 'velocity_y', 'velocity_z') @descriptor def EntityType(desc, self, cls): # pylint: disable=no-self-argument @@ -152,13 +152,19 @@ class SpawnObjectPacket(Packet): 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 '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 '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/packet.py b/minecraft/networking/packets/packet.py index ae39416..a847de0 100644 --- a/minecraft/networking/packets/packet.py +++ b/minecraft/networking/packets/packet.py @@ -112,8 +112,9 @@ class Packet(object): str = '0x%02X %s' % (self.id, str) fields = self.fields if fields is not None: - str = '%s(%s)' % (str, ', '.join('%s=%s' % - (a, self.field_string(a)) for a in fields)) + inner_str = ', '.join('%s=%s' % (a, self.field_string(a)) + for a in fields if hasattr(self, a)) + str = '%s(%s)' % (str, inner_str) return str @property diff --git a/minecraft/networking/packets/serverbound/play/__init__.py b/minecraft/networking/packets/serverbound/play/__init__.py index 54a7fdd..fff5ecd 100644 --- a/minecraft/networking/packets/serverbound/play/__init__.py +++ b/minecraft/networking/packets/serverbound/play/__init__.py @@ -4,7 +4,8 @@ from minecraft.networking.packets import ( from minecraft.networking.types import ( Double, Float, Boolean, VarInt, String, Byte, Position, Enum, - RelativeHand, BlockFace + RelativeHand, BlockFace, Vector, Direction, PositionAndLook, + multi_attribute_alias ) from .client_settings_packet import ClientSettingsPacket @@ -98,6 +99,19 @@ class PositionAndLookPacket(Packet): {'pitch': Float}, {'on_ground': Boolean}] + # Access the 'x', 'feet_y', 'z' fields as a Vector tuple. + position = multi_attribute_alias(Vector, 'x', 'feet_y', 'z') + + # Access the 'yaw', 'pitch' fields as a Direction tuple. + look = multi_attribute_alias(Direction, 'yaw', 'pitch') + + # Access the 'x', 'feet_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', 'feet_y', 'z', 'yaw', 'pitch') + class TeleportConfirmPacket(Packet): # Note: added between protocol versions 47 and 107. diff --git a/minecraft/networking/types/utility.py b/minecraft/networking/types/utility.py index 15189a6..3e5a48e 100644 --- a/minecraft/networking/types/utility.py +++ b/minecraft/networking/types/utility.py @@ -8,7 +8,7 @@ from itertools import chain __all__ = ( - 'Vector', 'MutableRecord', 'PositionAndLook', 'descriptor', + 'Vector', 'MutableRecord', 'Direction', 'PositionAndLook', 'descriptor', 'attribute_alias', 'multi_attribute_alias', ) @@ -53,8 +53,9 @@ class Vector(namedtuple('BaseVector', ('x', 'y', 'z'))): class MutableRecord(object): - """An abstract base class providing namedtuple-like repr(), ==, and hash() - implementations for types containing mutable fields given by __slots__. + """An abstract base class providing namedtuple-like repr(), ==, hash(), and + iter(), implementations for types containing mutable fields given by + __slots__. """ __slots__ = () @@ -64,19 +65,27 @@ class MutableRecord(object): def __repr__(self): return '%s(%s)' % (type(self).__name__, ', '.join( - '%s=%r' % (a, getattr(self, a)) for a in self.__slots__)) + '%s=%r' % (a, getattr(self, a)) for a in self._all_slots())) def __eq__(self, other): - return type(self) is type(other) and \ - all(getattr(self, a) == getattr(other, a) for a in self.__slots__) + return type(self) is type(other) and all( + getattr(self, a) == getattr(other, a) for a in self._all_slots()) def __ne__(self, other): return not (self == other) def __hash__(self): - values = tuple(getattr(self, a, None) for a in self.__slots__) + values = tuple(getattr(self, a, None) for a in self._all_slots()) return hash((type(self), values)) + def __iter__(self): + return iter(getattr(self, a) for a in self._all_slots()) + + @classmethod + def _all_slots(cls): + return tuple(f for c in reversed(cls.__mro__) + for f in getattr(c, '__slots__', ())) + def attribute_alias(name): """An attribute descriptor that redirects access to a different attribute @@ -97,10 +106,18 @@ def multi_attribute_alias(container, *arg_names, **kwd_names): argument to its constructor, and accessible as its 'n'th iterable element. + As a special case, 'tuple' may be given as the 'container' when there + are positional arguments, and (even though the tuple constructor does + not take positional arguments), the arguments will be aliased to the + corresponding positions in a tuple. + The name in 'kwd_names' mapped to by the key 'k' is the parent attribute that will be aliased to the field of 'container' settable by the keyword argument 'k' to its constructor, and accessible as its 'k' attribute. """ + if container is tuple: + container = lambda *args: args # noqa: E731 + @property def alias(self): return container( diff --git a/tests/test_packets.py b/tests/test_packets.py index 3db2fb2..a004488 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -149,7 +149,8 @@ class PacketEnumTest(unittest.TestCase): self.assertEqual( str(ExamplePacket(ConnectionContext(), alpha=0, beta=0, gamma=0)), - '0x00 ExamplePacket(alpha=ZERO, beta=0, gamma=0)') + '0x00 ExamplePacket(alpha=ZERO, beta=0, gamma=0)' + ) class TestReadWritePackets(unittest.TestCase): @@ -163,17 +164,44 @@ class TestReadWritePackets(unittest.TestCase): Record(-35, -127, 95), Record(11, 113, -8)], player_motion=Vector(4, 5, 0)) + self.assertEqual( + str(packet), + 'ExplosionPacket(x=787, y=-37, z=0, radius=15, records=[' + 'Record(-14, -116, -5), Record(-77, 34, -36), ' + 'Record(-35, -127, 95), Record(11, 113, -8)], ' + 'player_motion_x=4, player_motion_y=5, player_motion_z=0)' + ) + self._test_read_write_packet(packet) def test_combat_event_packet(self): - packet = clientbound.play.CombatEventPacket() - for event in ( - packet.EnterCombatEvent(), - packet.EndCombatEvent(duration=415, entity_id=91063502), - packet.EntityDeadEvent(player_id=178, entity_id=36, message='RIP'), - ): - packet.event = event - self._test_read_write_packet(packet) + packet = clientbound.play.CombatEventPacket( + event=clientbound.play.CombatEventPacket.EnterCombatEvent()) + self.assertEqual( + str(packet), + 'CombatEventPacket(event=EnterCombatEvent())' + ) + self._test_read_write_packet(packet) + + packet = clientbound.play.CombatEventPacket( + event=clientbound.play.CombatEventPacket.EndCombatEvent( + duration=415, entity_id=91063502)) + self.assertEqual( + str(packet), + 'CombatEventPacket(event=EndCombatEvent(' + 'duration=415, entity_id=91063502))' + ) + self._test_read_write_packet(packet) + + packet = clientbound.play.CombatEventPacket( + event=clientbound.play.CombatEventPacket.EntityDeadEvent( + player_id=178, entity_id=36, message='RIP')) + self.assertEqual( + str(packet), + "CombatEventPacket(event=EntityDeadEvent(" + "player_id=178, entity_id=36, message='RIP'))" + ) + self._test_read_write_packet(packet) def test_multi_block_change_packet(self): Record = clientbound.play.MultiBlockChangePacket.Record @@ -186,8 +214,16 @@ class TestReadWritePackets(unittest.TestCase): self.assertEqual(packet.records[0].blockMeta, 13) self.assertEqual(packet.records[0].blockStateId, 909) self.assertEqual(packet.records[0].position, Vector(1, 2, 3)) - self.assertEqual(packet.records[0], packet.records[1]) - self.assertEqual(packet.records[1], packet.records[2]) + self.assertEqual(packet.chunk_pos, (packet.chunk_x, packet.chunk_z)) + + self.assertEqual( + str(packet), + 'MultiBlockChangePacket(chunk_x=167, chunk_z=15, records=[' + 'Record(x=1, y=2, z=3, block_state_id=909), ' + 'Record(x=1, y=2, z=3, block_state_id=909), ' + 'Record(x=1, y=2, z=3, block_state_id=909)])' + ) + self._test_read_write_packet(packet) def test_spawn_object_packet(self): @@ -221,6 +257,23 @@ class TestReadWritePackets(unittest.TestCase): self.assertEqual(packet.velocity, velocity) self.assertEqual(packet.type, type_name) + self.assertEqual( + str(packet), + "0x%02X SpawnObjectPacket(entity_id=49846, " + "object_uuid='d9568851-85bc-4a10-8d6a-261d130626fa', " + "type_id=EGG, x=68.0, y=38.0, z=76.0, pitch=180, yaw=263.494, " + "data=1, velocity_x=21, velocity_y=55, velocity_z=41)" + % packet.id if protocol_version >= 100 else + "0x%02X SpawnObjectPacket(entity_id=49846, " + "object_uuid='d9568851-85bc-4a10-8d6a-261d130626fa', " + "type_id=EGG, x=68, y=38, z=76, pitch=180, yaw=263.494, " + "data=1, velocity_x=21, velocity_y=55, velocity_z=41)" + % packet.id if protocol_version >= 49 else + "0x%02X SpawnObjectPacket(entity_id=49846, type_id=EGG, " + "x=68, y=38, z=76, pitch=180, yaw=263.494, data=1, " + "velocity_x=21, velocity_y=55, velocity_z=41)" % packet.id + ) + packet2 = clientbound.play.SpawnObjectPacket( context=context, position_and_look=pos_look, velocity=velocity, type=type_name, @@ -254,17 +307,25 @@ class TestReadWritePackets(unittest.TestCase): if context.protocol_version >= 95: packet.sound_category = \ clientbound.play.SoundEffectPacket.SoundCategory.NEUTRAL + self._test_read_write_packet(packet, context) def test_face_player_packet(self): for protocol_version in TEST_VERSIONS: context = ConnectionContext(protocol_version=protocol_version) - packet = clientbound.play.FacePlayerPacket() + packet = clientbound.play.FacePlayerPacket(context) packet.target = 1.0, -2.0, 3.5 packet.entity_id = None if protocol_version >= 353: packet.origin = OriginPoint.EYES + self.assertEqual( + str(packet), + "0x%02X FacePlayerPacket(origin=EYES, x=1.0, y=-2.0, z=3.5, " + "entity_id=None)" % packet.id if protocol_version >= 353 else + "0x%02X FacePlayerPacket(entity_id=None, x=1.0, y=-2.0, z=3.5)" + % packet.id + ) self._test_read_write_packet(packet, context) packet.entity_id = 123 @@ -272,6 +333,13 @@ class TestReadWritePackets(unittest.TestCase): packet.entity_origin = OriginPoint.FEET else: del packet.target + self.assertEqual( + str(packet), + "0x%02X FacePlayerPacket(origin=EYES, x=1.0, y=-2.0, z=3.5, " + "entity_id=123, entity_origin=FEET)" % packet.id + if protocol_version >= 353 else + "0x%02X FacePlayerPacket(entity_id=123)" % packet.id + ) self._test_read_write_packet(packet, context) def _test_read_write_packet(self, packet_in, context=None, **kwargs): diff --git a/tests/test_packets_with_logic.py b/tests/test_packets_with_logic.py index ad564c4..946a9e4 100644 --- a/tests/test_packets_with_logic.py +++ b/tests/test_packets_with_logic.py @@ -1,11 +1,13 @@ import unittest -from minecraft.networking.types import (UUID, VarInt, String, Boolean) +from minecraft.networking.types import (UUID, VarInt) from minecraft.networking.packets import PacketBuffer from minecraft.networking.packets.clientbound.play import ( PlayerPositionAndLookPacket, PlayerListItemPacket, MapPacket ) from minecraft.networking.connection import ConnectionContext +from tests.test_packets import TEST_VERSIONS + class PlayerPositionAndLookTest(unittest.TestCase): @@ -141,104 +143,132 @@ class PlayerListItemTest(unittest.TestCase): PlayerListItemPacket().read(packet_buffer) @staticmethod - def make_add_player_packet(display_name=True): + def make_add_player_packet(context, display_name=True): packet_buffer = PacketBuffer() - - VarInt.send(0, packet_buffer) # action_id - VarInt.send(1, packet_buffer) # action count - UUID.send(fake_uuid, packet_buffer) # uuid - String.send("player", packet_buffer) # player name - - VarInt.send(2, packet_buffer) # number of properties - String.send("property1", packet_buffer) - String.send("value1", packet_buffer) - Boolean.send(False, packet_buffer) # is signed - String.send("property2", packet_buffer) - String.send("value2", packet_buffer) - Boolean.send(True, packet_buffer) # is signed - String.send("signature", packet_buffer) - - VarInt.send(42, packet_buffer) # game mode - VarInt.send(69, packet_buffer) # ping - Boolean.send(display_name, packet_buffer) # has display name - if display_name: - String.send("display", packet_buffer) # display name - + PlayerListItemPacket( + context=context, + action_type=PlayerListItemPacket.AddPlayerAction, + actions=[ + PlayerListItemPacket.AddPlayerAction( + uuid=fake_uuid, + name='goodmonson', + properties=[ + PlayerListItemPacket.PlayerProperty( + name='property1', value='value1', signature=None), + PlayerListItemPacket.PlayerProperty( + name='property2', value='value2', signature='gm') + ], + gamemode=42, + ping=69, + display_name='Goodmonson' if display_name else None + ), + ], + ).write_fields(packet_buffer) packet_buffer.reset_cursor() return packet_buffer def test_add_player_action(self): - player_list = PlayerListItemPacket.PlayerList() + for protocol_version in TEST_VERSIONS: + context = ConnectionContext(protocol_version=protocol_version) + player_list = PlayerListItemPacket.PlayerList() + packet_buffer = self.make_add_player_packet(context) - packet_buffer = self.make_add_player_packet() + packet = PlayerListItemPacket() + packet.read(packet_buffer) + packet.apply(player_list) - packet = PlayerListItemPacket() - packet.read(packet_buffer) - packet.apply(player_list) + self.assertIn(fake_uuid, player_list.players_by_uuid) + player = player_list.players_by_uuid[fake_uuid] - self.assertIn(fake_uuid, player_list.players_by_uuid) - player = player_list.players_by_uuid[fake_uuid] + self.assertEqual(player.name, 'goodmonson') + self.assertEqual(player.properties[0].name, 'property1') + self.assertIsNone(player.properties[0].signature) + self.assertEqual(player.properties[1].value, 'value2') + self.assertEqual(player.properties[1].signature, 'gm') + self.assertEqual(player.gamemode, 42) + self.assertEqual(player.ping, 69) + self.assertEqual(player.display_name, 'Goodmonson') - self.assertEqual(player.name, "player") - self.assertEqual(player.properties[0].name, "property1") - self.assertIsNone(player.properties[0].signature) - self.assertEqual(player.properties[1].value, "value2") - self.assertEqual(player.properties[1].signature, "signature") - self.assertEqual(player.gamemode, 42) - self.assertEqual(player.ping, 69) - self.assertEqual(player.display_name, "display") - - @staticmethod - def make_action_base(action_id): - packet_buffer = PacketBuffer() - VarInt.send(action_id, packet_buffer) - VarInt.send(1, packet_buffer) # action count - UUID.send(fake_uuid, packet_buffer) - - return packet_buffer - - def read_and_apply(self, packet_buffer, player_list): + def read_and_apply(self, context, packet_buffer, player_list): packet_buffer.reset_cursor() - packet = PlayerListItemPacket() + packet = PlayerListItemPacket(context) packet.read(packet_buffer) packet.apply(player_list) def test_add_and_others(self): - player_list = PlayerListItemPacket.PlayerList() - by_uuid = player_list.players_by_uuid + for protocol_version in TEST_VERSIONS: + context = ConnectionContext(protocol_version=protocol_version) - packet_buffer = self.make_add_player_packet(display_name=False) - self.read_and_apply(packet_buffer, player_list) - self.assertEqual(by_uuid[fake_uuid].gamemode, 42) - self.assertEqual(by_uuid[fake_uuid].ping, 69) - self.assertIsNone(by_uuid[fake_uuid].display_name) + player_list = PlayerListItemPacket.PlayerList() + by_uuid = player_list.players_by_uuid - # Change the game mode - packet_buffer = self.make_action_base(1) - VarInt.send(43, packet_buffer) # gamemode - self.read_and_apply(packet_buffer, player_list) - self.assertEqual(by_uuid[fake_uuid].gamemode, 43) + packet_buffer = self.make_add_player_packet( + context, display_name=False) + self.read_and_apply(context, packet_buffer, player_list) + self.assertEqual(by_uuid[fake_uuid].gamemode, 42) + self.assertEqual(by_uuid[fake_uuid].ping, 69) + self.assertIsNone(by_uuid[fake_uuid].display_name) - # Change the ping - packet_buffer = self.make_action_base(2) - VarInt.send(70, packet_buffer) # ping - self.read_and_apply(packet_buffer, player_list) - self.assertEqual(by_uuid[fake_uuid].ping, 70) + # Change the game mode + packet_buffer = PacketBuffer() + PlayerListItemPacket( + context=context, + action_type=PlayerListItemPacket.UpdateGameModeAction, + actions=[ + PlayerListItemPacket.UpdateGameModeAction( + uuid=fake_uuid, gamemode=43), + ], + ).write_fields(packet_buffer) + self.read_and_apply(context, packet_buffer, player_list) + self.assertEqual(by_uuid[fake_uuid].gamemode, 43) - # Change the display name - packet_buffer = self.make_action_base(3) - Boolean.send(True, packet_buffer) - String.send("display2", packet_buffer) - self.read_and_apply(packet_buffer, player_list) - self.assertEqual(by_uuid[fake_uuid].display_name, "display2") + # Change the ping + packet_buffer = PacketBuffer() + PlayerListItemPacket( + context=context, + action_type=PlayerListItemPacket.UpdateLatencyAction, + actions=[ + PlayerListItemPacket.UpdateLatencyAction( + uuid=fake_uuid, ping=70), + ], + ).write_fields(packet_buffer) + self.read_and_apply(context, packet_buffer, player_list) + self.assertEqual(by_uuid[fake_uuid].ping, 70) - # Remove the display name - packet_buffer = self.make_action_base(3) - Boolean.send(False, packet_buffer) - self.read_and_apply(packet_buffer, player_list) - self.assertIsNone(by_uuid[fake_uuid].display_name) + # Change the display name + packet_buffer = PacketBuffer() + PlayerListItemPacket( + context=context, + action_type=PlayerListItemPacket.UpdateDisplayNameAction, + actions=[ + PlayerListItemPacket.UpdateDisplayNameAction( + uuid=fake_uuid, display_name='Badmonson'), + ], + ).write_fields(packet_buffer) + self.read_and_apply(context, packet_buffer, player_list) + self.assertEqual(by_uuid[fake_uuid].display_name, 'Badmonson') - # Remove the player - packet_buffer = self.make_action_base(4) - self.read_and_apply(packet_buffer, player_list) - self.assertNotIn(fake_uuid, player_list.players_by_uuid) + # Remove the display name + packet_buffer = PacketBuffer() + PlayerListItemPacket( + context=context, + action_type=PlayerListItemPacket.UpdateDisplayNameAction, + actions=[ + PlayerListItemPacket.UpdateDisplayNameAction( + uuid=fake_uuid, display_name=None), + ], + ).write_fields(packet_buffer) + self.read_and_apply(context, packet_buffer, player_list) + self.assertIsNone(by_uuid[fake_uuid].display_name) + + # Remove the player + packet_buffer = PacketBuffer() + PlayerListItemPacket( + context=context, + action_type=PlayerListItemPacket.RemovePlayerAction, + actions=[ + PlayerListItemPacket.RemovePlayerAction(uuid=fake_uuid), + ], + ).write_fields(packet_buffer) + self.read_and_apply(context, packet_buffer, player_list) + self.assertNotIn(fake_uuid, player_list.players_by_uuid)