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 (
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

View File

@ -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'

View File

@ -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__ = ()

View File

@ -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)

View File

@ -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):

View File

@ -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')))

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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.

View File

@ -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(

View File

@ -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):

View File

@ -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)