Improve metadata and auxiliary methods of existing packets.

* Add multi-attribute aliases to some packets, for user convenience.
* Add support for writing PlayerListItemPacket.
* Add 'fields' attributes to manually-read/written packet classes,
  implementing 'field_string' where appropriate to allow enable the
  default __repr__ implementation.
* Modify data constructors where appropriate so that __repr__
  implementations match their constructor protocols.
* Improve comments on type aliases within packet classes.
* Add/modify tests to cover the new functionality.
This commit is contained in:
joodicator 2019-06-08 15:38:20 +02:00
parent 234e57716c
commit e3d2b1a368
14 changed files with 389 additions and 158 deletions

View File

@ -4,7 +4,8 @@ from minecraft.networking.packets import (
from minecraft.networking.types import ( from minecraft.networking.types import (
Integer, FixedPointInteger, Angle, UnsignedByte, Byte, Boolean, UUID, 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 from .combat_event_packet import CombatEventPacket
@ -91,13 +92,9 @@ class JoinGamePacket(Packet):
{'reduced_debug_info': Boolean}, {'reduced_debug_info': Boolean},
]) ])
# JoinGamePacket.Difficulty is an alias for Difficulty # These aliases declare the Enum type corresponding to each field:
Difficulty = Difficulty Difficulty = Difficulty
# JoinGamePacket.Gamemode is an alias for Gamemode
GameMode = GameMode GameMode = GameMode
# JoinGamePacket.Dimension is an alias for Dimension
Dimension = Dimension Dimension = Dimension
@ -115,7 +112,7 @@ class ServerDifficultyPacket(Packet):
{'is_locked': Boolean} if context.protocol_version >= 464 else {}, {'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 Difficulty = Difficulty
@ -181,10 +178,22 @@ class SpawnPlayerPacket(Packet):
else {'z': FixedPointInteger}, else {'z': FixedPointInteger},
{'yaw': Angle}, {'yaw': Angle},
{'pitch': Angle}, {'pitch': Angle},
{'current_item': Short} if context.protocol_version <= 49 else {},
# TODO: read entity metadata # 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): class EntityVelocityPacket(Packet):
@staticmethod @staticmethod
@ -258,13 +267,9 @@ class RespawnPacket(Packet):
{'level_type': String}, {'level_type': String},
]) ])
# RespawnPacket.Difficulty is an alias for Difficulty. # These aliases declare the Enum type corresponding to each field:
Difficulty = Difficulty Difficulty = Difficulty
# RespawnPacket.Dimension is an alias for Dimension.
Dimension = Dimension Dimension = Dimension
# RespawnPacket.Gamemode is an alias for Gamemode.
GameMode = GameMode GameMode = GameMode

View File

@ -53,6 +53,11 @@ class MultiBlockChangePacket(Packet):
packet_name = 'multi block change' 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): class Record(MutableRecord):
__slots__ = 'x', 'y', 'z', 'block_state_id' __slots__ = 'x', 'y', 'z', 'block_state_id'

View File

@ -22,6 +22,8 @@ class CombatEventPacket(Packet):
packet_name = 'combat event' packet_name = 'combat event'
fields = 'event',
# The abstract type of the 'event' field of this packet. # The abstract type of the 'event' field of this packet.
class EventType(MutableRecord): class EventType(MutableRecord):
__slots__ = () __slots__ = ()

View File

@ -18,14 +18,19 @@ class ExplosionPacket(Packet):
packet_name = 'explosion' packet_name = 'explosion'
class Record(Vector): fields = 'x', 'y', 'z', 'radius', 'records', \
__slots__ = () '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') 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( player_motion = multi_attribute_alias(
Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z') Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z')
class Record(Vector):
__slots__ = ()
def read(self, file_object): def read(self, file_object):
self.x = Float.read(file_object) self.x = Float.read(file_object)
self.y = Float.read(file_object) self.y = Float.read(file_object)

View File

@ -15,7 +15,13 @@ class FacePlayerPacket(Packet):
packet_name = 'face player' 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') target = multi_attribute_alias(Vector, 'x', 'y', 'z')
def read(self, file_object): def read(self, file_object):

View File

@ -17,6 +17,20 @@ class MapPacket(Packet):
packet_name = 'map' 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): class MapIcon(MutableRecord):
__slots__ = 'type', 'direction', 'location', 'display_name' __slots__ = 'type', 'direction', 'location', 'display_name'
@ -43,11 +57,11 @@ class MapPacket(Packet):
class MapSet(object): class MapSet(object):
__slots__ = 'maps_by_id' __slots__ = 'maps_by_id'
def __init__(self): def __init__(self, *maps):
self.maps_by_id = dict() self.maps_by_id = {map.id: map for map in maps}
def __repr__(self): 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) return 'MapSet(%s)' % ', '.join(maps)
def read(self, file_object): def read(self, file_object):
@ -143,9 +157,3 @@ class MapPacket(Packet):
UnsignedByte.send(self.offset[0], packet_buffer) # x UnsignedByte.send(self.offset[0], packet_buffer) # x
UnsignedByte.send(self.offset[1], packet_buffer) # z UnsignedByte.send(self.offset[1], packet_buffer) # z
VarIntPrefixedByteArray.send(self.pixels, packet_buffer) 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')))

View File

@ -1,7 +1,7 @@
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types import ( 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" 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): class PlayerList(object):
__slots__ = 'players_by_uuid' __slots__ = 'players_by_uuid'
def __init__(self): def __init__(self, *items):
self.players_by_uuid = dict() self.players_by_uuid = {item.uuid: item for item in items}
class PlayerListItem(object): class PlayerListItem(MutableRecord):
__slots__ = ( __slots__ = (
'uuid', 'name', 'properties', 'gamemode', 'ping', 'display_name') 'uuid', 'name', 'properties', 'gamemode', 'ping', 'display_name')
def __init__(self, **kwds): class PlayerProperty(MutableRecord):
for key, val in kwds.items():
setattr(self, key, val)
class PlayerProperty(object):
__slots__ = 'name', 'value', 'signature' __slots__ = 'name', 'value', 'signature'
def read(self, file_object): def read(self, file_object):
@ -46,33 +49,44 @@ class PlayerListItemPacket(Packet):
else: else:
self.signature = None self.signature = None
class Action(object): def send(self, packet_buffer):
__slots__ = 'uuid' 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): def read(self, file_object):
self.uuid = UUID.read(file_object) self.uuid = UUID.read(file_object)
self._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): def _read(self, file_object):
raise NotImplementedError( raise NotImplementedError(
'This abstract method must be overridden in a subclass.') '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 @classmethod
def type_from_id(cls, action_id): def type_from_id(cls, action_id):
subcls = { for subcls in cls.__subclasses__():
0: PlayerListItemPacket.AddPlayerAction, if subcls.action_id == action_id:
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 return subcls
raise ValueError("Unknown player list action ID: %s." % action_id)
class AddPlayerAction(Action): class AddPlayerAction(Action):
__slots__ = 'name', 'properties', 'gamemode', 'ping', 'display_name' __slots__ = 'name', 'properties', 'gamemode', 'ping', 'display_name'
action_id = 0
def _read(self, file_object): def _read(self, file_object):
self.name = String.read(file_object) self.name = String.read(file_object)
@ -90,6 +104,19 @@ class PlayerListItemPacket(Packet):
else: else:
self.display_name = None 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): def apply(self, player_list):
player = PlayerListItemPacket.PlayerListItem( player = PlayerListItemPacket.PlayerListItem(
uuid=self.uuid, uuid=self.uuid,
@ -102,10 +129,14 @@ class PlayerListItemPacket(Packet):
class UpdateGameModeAction(Action): class UpdateGameModeAction(Action):
__slots__ = 'gamemode' __slots__ = 'gamemode'
action_id = 1
def _read(self, file_object): def _read(self, file_object):
self.gamemode = VarInt.read(file_object) self.gamemode = VarInt.read(file_object)
def _send(self, packet_buffer):
VarInt.send(self.gamemode, packet_buffer)
def apply(self, player_list): def apply(self, player_list):
player = player_list.players_by_uuid.get(self.uuid) player = player_list.players_by_uuid.get(self.uuid)
if player: if player:
@ -113,10 +144,14 @@ class PlayerListItemPacket(Packet):
class UpdateLatencyAction(Action): class UpdateLatencyAction(Action):
__slots__ = 'ping' __slots__ = 'ping'
action_id = 2
def _read(self, file_object): def _read(self, file_object):
self.ping = VarInt.read(file_object) self.ping = VarInt.read(file_object)
def _send(self, packet_buffer):
VarInt.send(self.ping, packet_buffer)
def apply(self, player_list): def apply(self, player_list):
player = player_list.players_by_uuid.get(self.uuid) player = player_list.players_by_uuid.get(self.uuid)
if player: if player:
@ -124,6 +159,7 @@ class PlayerListItemPacket(Packet):
class UpdateDisplayNameAction(Action): class UpdateDisplayNameAction(Action):
__slots__ = 'display_name' __slots__ = 'display_name'
action_id = 3
def _read(self, file_object): def _read(self, file_object):
has_display_name = Boolean.read(file_object) has_display_name = Boolean.read(file_object)
@ -132,15 +168,27 @@ class PlayerListItemPacket(Packet):
else: else:
self.display_name = None 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): def apply(self, player_list):
player = player_list.players_by_uuid.get(self.uuid) player = player_list.players_by_uuid.get(self.uuid)
if player: if player:
player.display_name = self.display_name player.display_name = self.display_name
class RemovePlayerAction(Action): class RemovePlayerAction(Action):
action_id = 4
def _read(self, file_object): def _read(self, file_object):
pass pass
def _send(self, packet_buffer):
pass
def apply(self, player_list): def apply(self, player_list):
if self.uuid in player_list.players_by_uuid: if self.uuid in player_list.players_by_uuid:
del player_list.players_by_uuid[self.uuid] del player_list.players_by_uuid[self.uuid]
@ -155,9 +203,12 @@ class PlayerListItemPacket(Packet):
action.read(file_object) action.read(file_object)
self.actions.append(action) 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): def apply(self, player_list):
for action in self.actions: for action in self.actions:
action.apply(player_list) action.apply(player_list)
def write_fields(self, packet_buffer):
raise NotImplementedError

View File

@ -1,7 +1,8 @@
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types import ( 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 {}, {'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( field_enum = classmethod(
lambda cls, field, context: cls if field == 'flags' else None) lambda cls, field, context: cls if field == 'flags' else None)

View File

@ -3,7 +3,7 @@ from minecraft.networking.types.utility import descriptor
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, UUID, Byte, Double, Integer, Angle, Short, Enum, Vector, 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' packet_name = 'spawn object'
fields = ('entity_id', 'object_uuid', 'type_id', fields = ('entity_id', 'object_uuid', 'type_id', 'x', 'y', 'z', 'pitch',
'x', 'y', 'z', 'pitch', 'yaw') 'yaw', 'data', 'velocity_x', 'velocity_y', 'velocity_z')
@descriptor @descriptor
def EntityType(desc, self, cls): # pylint: disable=no-self-argument def EntityType(desc, self, cls): # pylint: disable=no-self-argument
@ -152,13 +152,19 @@ class SpawnObjectPacket(Packet):
def type(self): def type(self):
del self.type_id del self.type_id
# Access the 'x', 'y', 'z' fields as a Vector.
position = multi_attribute_alias(Vector, 'x', 'y', 'z') 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 # NOTE: modifying the object retrieved from this property will not change
# the packet; it can only be changed by attribute or property assignment. # the packet; it can only be changed by attribute or property assignment.
position_and_look = multi_attribute_alias( position_and_look = multi_attribute_alias(
PositionAndLook, x='x', y='y', z='z', yaw='yaw', pitch='pitch') 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( velocity = multi_attribute_alias(
Vector, 'velocity_x', 'velocity_y', 'velocity_z') Vector, 'velocity_x', 'velocity_y', 'velocity_z')

View File

@ -112,8 +112,9 @@ class Packet(object):
str = '0x%02X %s' % (self.id, str) str = '0x%02X %s' % (self.id, str)
fields = self.fields fields = self.fields
if fields is not None: if fields is not None:
str = '%s(%s)' % (str, ', '.join('%s=%s' % inner_str = ', '.join('%s=%s' % (a, self.field_string(a))
(a, self.field_string(a)) for a in fields)) for a in fields if hasattr(self, a))
str = '%s(%s)' % (str, inner_str)
return str return str
@property @property

View File

@ -4,7 +4,8 @@ from minecraft.networking.packets import (
from minecraft.networking.types import ( from minecraft.networking.types import (
Double, Float, Boolean, VarInt, String, Byte, Position, Enum, Double, Float, Boolean, VarInt, String, Byte, Position, Enum,
RelativeHand, BlockFace RelativeHand, BlockFace, Vector, Direction, PositionAndLook,
multi_attribute_alias
) )
from .client_settings_packet import ClientSettingsPacket from .client_settings_packet import ClientSettingsPacket
@ -98,6 +99,19 @@ class PositionAndLookPacket(Packet):
{'pitch': Float}, {'pitch': Float},
{'on_ground': Boolean}] {'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): class TeleportConfirmPacket(Packet):
# Note: added between protocol versions 47 and 107. # Note: added between protocol versions 47 and 107.

View File

@ -8,7 +8,7 @@ from itertools import chain
__all__ = ( __all__ = (
'Vector', 'MutableRecord', 'PositionAndLook', 'descriptor', 'Vector', 'MutableRecord', 'Direction', 'PositionAndLook', 'descriptor',
'attribute_alias', 'multi_attribute_alias', 'attribute_alias', 'multi_attribute_alias',
) )
@ -53,8 +53,9 @@ class Vector(namedtuple('BaseVector', ('x', 'y', 'z'))):
class MutableRecord(object): class MutableRecord(object):
"""An abstract base class providing namedtuple-like repr(), ==, and hash() """An abstract base class providing namedtuple-like repr(), ==, hash(), and
implementations for types containing mutable fields given by __slots__. iter(), implementations for types containing mutable fields given by
__slots__.
""" """
__slots__ = () __slots__ = ()
@ -64,19 +65,27 @@ class MutableRecord(object):
def __repr__(self): def __repr__(self):
return '%s(%s)' % (type(self).__name__, ', '.join( 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): def __eq__(self, other):
return type(self) is type(other) and \ return type(self) is type(other) and all(
all(getattr(self, a) == getattr(other, a) for a in self.__slots__) getattr(self, a) == getattr(other, a) for a in self._all_slots())
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not (self == other)
def __hash__(self): 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)) 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): def attribute_alias(name):
"""An attribute descriptor that redirects access to a different attribute """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 argument to its constructor, and accessible as its 'n'th iterable
element. 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 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 that will be aliased to the field of 'container' settable by the keyword
argument 'k' to its constructor, and accessible as its 'k' attribute. argument 'k' to its constructor, and accessible as its 'k' attribute.
""" """
if container is tuple:
container = lambda *args: args # noqa: E731
@property @property
def alias(self): def alias(self):
return container( return container(

View File

@ -149,7 +149,8 @@ class PacketEnumTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
str(ExamplePacket(ConnectionContext(), alpha=0, beta=0, gamma=0)), 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): class TestReadWritePackets(unittest.TestCase):
@ -163,16 +164,43 @@ class TestReadWritePackets(unittest.TestCase):
Record(-35, -127, 95), Record(11, 113, -8)], Record(-35, -127, 95), Record(11, 113, -8)],
player_motion=Vector(4, 5, 0)) 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) self._test_read_write_packet(packet)
def test_combat_event_packet(self): def test_combat_event_packet(self):
packet = clientbound.play.CombatEventPacket() packet = clientbound.play.CombatEventPacket(
for event in ( event=clientbound.play.CombatEventPacket.EnterCombatEvent())
packet.EnterCombatEvent(), self.assertEqual(
packet.EndCombatEvent(duration=415, entity_id=91063502), str(packet),
packet.EntityDeadEvent(player_id=178, entity_id=36, message='RIP'), 'CombatEventPacket(event=EnterCombatEvent())'
): )
packet.event = event 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) self._test_read_write_packet(packet)
def test_multi_block_change_packet(self): def test_multi_block_change_packet(self):
@ -186,8 +214,16 @@ class TestReadWritePackets(unittest.TestCase):
self.assertEqual(packet.records[0].blockMeta, 13) self.assertEqual(packet.records[0].blockMeta, 13)
self.assertEqual(packet.records[0].blockStateId, 909) self.assertEqual(packet.records[0].blockStateId, 909)
self.assertEqual(packet.records[0].position, Vector(1, 2, 3)) self.assertEqual(packet.records[0].position, Vector(1, 2, 3))
self.assertEqual(packet.records[0], packet.records[1]) self.assertEqual(packet.chunk_pos, (packet.chunk_x, packet.chunk_z))
self.assertEqual(packet.records[1], packet.records[2])
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) self._test_read_write_packet(packet)
def test_spawn_object_packet(self): def test_spawn_object_packet(self):
@ -221,6 +257,23 @@ class TestReadWritePackets(unittest.TestCase):
self.assertEqual(packet.velocity, velocity) self.assertEqual(packet.velocity, velocity)
self.assertEqual(packet.type, type_name) 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( packet2 = clientbound.play.SpawnObjectPacket(
context=context, position_and_look=pos_look, context=context, position_and_look=pos_look,
velocity=velocity, type=type_name, velocity=velocity, type=type_name,
@ -254,17 +307,25 @@ class TestReadWritePackets(unittest.TestCase):
if context.protocol_version >= 95: if context.protocol_version >= 95:
packet.sound_category = \ packet.sound_category = \
clientbound.play.SoundEffectPacket.SoundCategory.NEUTRAL clientbound.play.SoundEffectPacket.SoundCategory.NEUTRAL
self._test_read_write_packet(packet, context) self._test_read_write_packet(packet, context)
def test_face_player_packet(self): def test_face_player_packet(self):
for protocol_version in TEST_VERSIONS: for protocol_version in TEST_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version) context = ConnectionContext(protocol_version=protocol_version)
packet = clientbound.play.FacePlayerPacket() packet = clientbound.play.FacePlayerPacket(context)
packet.target = 1.0, -2.0, 3.5 packet.target = 1.0, -2.0, 3.5
packet.entity_id = None packet.entity_id = None
if protocol_version >= 353: if protocol_version >= 353:
packet.origin = OriginPoint.EYES 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) self._test_read_write_packet(packet, context)
packet.entity_id = 123 packet.entity_id = 123
@ -272,6 +333,13 @@ class TestReadWritePackets(unittest.TestCase):
packet.entity_origin = OriginPoint.FEET packet.entity_origin = OriginPoint.FEET
else: else:
del packet.target 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) self._test_read_write_packet(packet, context)
def _test_read_write_packet(self, packet_in, context=None, **kwargs): def _test_read_write_packet(self, packet_in, context=None, **kwargs):

View File

@ -1,11 +1,13 @@
import unittest 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 import PacketBuffer
from minecraft.networking.packets.clientbound.play import ( from minecraft.networking.packets.clientbound.play import (
PlayerPositionAndLookPacket, PlayerListItemPacket, MapPacket PlayerPositionAndLookPacket, PlayerListItemPacket, MapPacket
) )
from minecraft.networking.connection import ConnectionContext from minecraft.networking.connection import ConnectionContext
from tests.test_packets import TEST_VERSIONS
class PlayerPositionAndLookTest(unittest.TestCase): class PlayerPositionAndLookTest(unittest.TestCase):
@ -141,36 +143,35 @@ class PlayerListItemTest(unittest.TestCase):
PlayerListItemPacket().read(packet_buffer) PlayerListItemPacket().read(packet_buffer)
@staticmethod @staticmethod
def make_add_player_packet(display_name=True): def make_add_player_packet(context, display_name=True):
packet_buffer = PacketBuffer() packet_buffer = PacketBuffer()
PlayerListItemPacket(
VarInt.send(0, packet_buffer) # action_id context=context,
VarInt.send(1, packet_buffer) # action count action_type=PlayerListItemPacket.AddPlayerAction,
UUID.send(fake_uuid, packet_buffer) # uuid actions=[
String.send("player", packet_buffer) # player name PlayerListItemPacket.AddPlayerAction(
uuid=fake_uuid,
VarInt.send(2, packet_buffer) # number of properties name='goodmonson',
String.send("property1", packet_buffer) properties=[
String.send("value1", packet_buffer) PlayerListItemPacket.PlayerProperty(
Boolean.send(False, packet_buffer) # is signed name='property1', value='value1', signature=None),
String.send("property2", packet_buffer) PlayerListItemPacket.PlayerProperty(
String.send("value2", packet_buffer) name='property2', value='value2', signature='gm')
Boolean.send(True, packet_buffer) # is signed ],
String.send("signature", packet_buffer) gamemode=42,
ping=69,
VarInt.send(42, packet_buffer) # game mode display_name='Goodmonson' if display_name else None
VarInt.send(69, packet_buffer) # ping ),
Boolean.send(display_name, packet_buffer) # has display name ],
if display_name: ).write_fields(packet_buffer)
String.send("display", packet_buffer) # display name
packet_buffer.reset_cursor() packet_buffer.reset_cursor()
return packet_buffer return packet_buffer
def test_add_player_action(self): def test_add_player_action(self):
for protocol_version in TEST_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version)
player_list = PlayerListItemPacket.PlayerList() player_list = PlayerListItemPacket.PlayerList()
packet_buffer = self.make_add_player_packet(context)
packet_buffer = self.make_add_player_packet()
packet = PlayerListItemPacket() packet = PlayerListItemPacket()
packet.read(packet_buffer) packet.read(packet_buffer)
@ -179,66 +180,95 @@ class PlayerListItemTest(unittest.TestCase):
self.assertIn(fake_uuid, player_list.players_by_uuid) self.assertIn(fake_uuid, player_list.players_by_uuid)
player = player_list.players_by_uuid[fake_uuid] player = player_list.players_by_uuid[fake_uuid]
self.assertEqual(player.name, "player") self.assertEqual(player.name, 'goodmonson')
self.assertEqual(player.properties[0].name, "property1") self.assertEqual(player.properties[0].name, 'property1')
self.assertIsNone(player.properties[0].signature) self.assertIsNone(player.properties[0].signature)
self.assertEqual(player.properties[1].value, "value2") self.assertEqual(player.properties[1].value, 'value2')
self.assertEqual(player.properties[1].signature, "signature") self.assertEqual(player.properties[1].signature, 'gm')
self.assertEqual(player.gamemode, 42) self.assertEqual(player.gamemode, 42)
self.assertEqual(player.ping, 69) self.assertEqual(player.ping, 69)
self.assertEqual(player.display_name, "display") self.assertEqual(player.display_name, 'Goodmonson')
@staticmethod def read_and_apply(self, context, packet_buffer, player_list):
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):
packet_buffer.reset_cursor() packet_buffer.reset_cursor()
packet = PlayerListItemPacket() packet = PlayerListItemPacket(context)
packet.read(packet_buffer) packet.read(packet_buffer)
packet.apply(player_list) packet.apply(player_list)
def test_add_and_others(self): def test_add_and_others(self):
for protocol_version in TEST_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version)
player_list = PlayerListItemPacket.PlayerList() player_list = PlayerListItemPacket.PlayerList()
by_uuid = player_list.players_by_uuid by_uuid = player_list.players_by_uuid
packet_buffer = self.make_add_player_packet(display_name=False) packet_buffer = self.make_add_player_packet(
self.read_and_apply(packet_buffer, player_list) 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].gamemode, 42)
self.assertEqual(by_uuid[fake_uuid].ping, 69) self.assertEqual(by_uuid[fake_uuid].ping, 69)
self.assertIsNone(by_uuid[fake_uuid].display_name) self.assertIsNone(by_uuid[fake_uuid].display_name)
# Change the game mode # Change the game mode
packet_buffer = self.make_action_base(1) packet_buffer = PacketBuffer()
VarInt.send(43, packet_buffer) # gamemode PlayerListItemPacket(
self.read_and_apply(packet_buffer, player_list) 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) self.assertEqual(by_uuid[fake_uuid].gamemode, 43)
# Change the ping # Change the ping
packet_buffer = self.make_action_base(2) packet_buffer = PacketBuffer()
VarInt.send(70, packet_buffer) # ping PlayerListItemPacket(
self.read_and_apply(packet_buffer, player_list) 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) self.assertEqual(by_uuid[fake_uuid].ping, 70)
# Change the display name # Change the display name
packet_buffer = self.make_action_base(3) packet_buffer = PacketBuffer()
Boolean.send(True, packet_buffer) PlayerListItemPacket(
String.send("display2", packet_buffer) context=context,
self.read_and_apply(packet_buffer, player_list) action_type=PlayerListItemPacket.UpdateDisplayNameAction,
self.assertEqual(by_uuid[fake_uuid].display_name, "display2") 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 display name # Remove the display name
packet_buffer = self.make_action_base(3) packet_buffer = PacketBuffer()
Boolean.send(False, packet_buffer) PlayerListItemPacket(
self.read_and_apply(packet_buffer, player_list) 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) self.assertIsNone(by_uuid[fake_uuid].display_name)
# Remove the player # Remove the player
packet_buffer = self.make_action_base(4) packet_buffer = PacketBuffer()
self.read_and_apply(packet_buffer, player_list) 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) self.assertNotIn(fake_uuid, player_list.players_by_uuid)