diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 70c926d..df716ce 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -3,8 +3,8 @@ from minecraft.networking.packets import ( ) from minecraft.networking.types import ( - Integer, UnsignedByte, Byte, Boolean, UUID, Short, Position, - VarInt, Double, Float, String, Enum, + Integer, UnsignedByte, Byte, Boolean, UUID, Short, VarInt, Double, Float, + String, Enum, ) from .combat_event_packet import CombatEventPacket @@ -13,6 +13,7 @@ from .player_list_item_packet import PlayerListItemPacket from .player_position_and_look_packet import PlayerPositionAndLookPacket from .spawn_object_packet import SpawnObjectPacket from .block_change_packet import BlockChangePacket, MultiBlockChangePacket +from .explosion_packet import ExplosionPacket # Formerly known as state_playing_clientbound. @@ -176,42 +177,6 @@ class UpdateHealthPacket(Packet): ]) -class ExplosionPacket(Packet): - @staticmethod - def get_id(context): - return 0x1D if context.protocol_version >= 345 else \ - 0x1C if context.protocol_version >= 332 else \ - 0x1D if context.protocol_version >= 318 else \ - 0x1C if context.protocol_version >= 80 else \ - 0x1B if context.protocol_version >= 67 else \ - 0x27 - - packet_name = 'explosion' - - class Record(Position): - pass - - def read(self, file_object): - self.x = Float.read(file_object) - self.y = Float.read(file_object) - self.z = Float.read(file_object) - self.radius = Float.read(file_object) - records_count = VarInt.read(file_object) - self.records = [] - for i in range(records_count): - rec_x = Byte.read(file_object) - rec_y = Byte.read(file_object) - rec_z = Byte.read(file_object) - record = ExplosionPacket.Record(rec_x, rec_y, rec_z) - self.records.append(record) - self.player_motion_x = Float.read(file_object) - self.player_motion_y = Float.read(file_object) - self.player_motion_z = Float.read(file_object) - - def write(self, socket, compression_threshold=None): - raise NotImplementedError - - class PluginMessagePacket(AbstractPluginMessagePacket): @staticmethod def get_id(context): diff --git a/minecraft/networking/packets/clientbound/play/block_change_packet.py b/minecraft/networking/packets/clientbound/play/block_change_packet.py index 381168b..09cd0d5 100644 --- a/minecraft/networking/packets/clientbound/play/block_change_packet.py +++ b/minecraft/networking/packets/clientbound/play/block_change_packet.py @@ -1,8 +1,6 @@ -from minecraft.networking.packets import ( - Packet, PacketBuffer -) +from minecraft.networking.packets import Packet from minecraft.networking.types import ( - VarInt, Integer, UnsignedByte, Position + VarInt, Integer, UnsignedByte, Position, Vector ) @@ -16,23 +14,25 @@ class BlockChangePacket(Packet): 0x23 packet_name = 'block change' + definition = [ + {'location': Position}, + {'block_state_id': VarInt}] + block_state_id = 0 - def read(self, file_object): - self.location = Position.read(file_object) - blockData = VarInt.read(file_object) - if self.context.protocol_version >= 347: - # See comments on MultiBlockChangePacket.OpaqueRecord. - self.blockStateId = blockData - else: - self.blockId = (blockData >> 4) - self.blockMeta = (blockData & 0xF) + # For protocols < 347: an accessor for (block_state_id >> 4). + def blockId(self, block_id): + self.block_state_id = (self.block_state_id & 0xF) | (block_id << 4) + blockId = property(lambda self: self.block_state_id >> 4, blockId) - def write(self, socket, compression_threshold=None): - packet_buffer = PacketBuffer() - Position.send(self.location, packet_buffer) - blockData = ((self.blockId << 4) | (self.blockMeta & 0xF)) - VarInt.send(blockData) - self._write_buffer(socket, packet_buffer, compression_threshold) + # For protocols < 347: an accessor for (block_state_id & 0xF). + def blockMeta(self, meta): + self.block_state_id = (self.block_state_id & ~0xF) | (meta & 0xF) + blockMeta = property(lambda self: self.block_state_id & 0xF, blockMeta) + + # This alias is retained for backward compatibility. + def blockStateId(self, block_state_id): + self.block_state_id = block_state_id + blockStateId = property(lambda self: self.block_state_id, blockStateId) class MultiBlockChangePacket(Packet): @@ -46,60 +46,66 @@ class MultiBlockChangePacket(Packet): packet_name = 'multi block change' - class BaseRecord(object): - __slots__ = 'x', 'y', 'z' + class Record(object): + __slots__ = 'x', 'y', 'z', 'block_state_id' - def __init__(self, horizontal_position, y_coordinate): - self.x = (horizontal_position & 0xF0) >> 4 - self.y = y_coordinate - self.z = (horizontal_position & 0x0F) - - @classmethod - def get_subclass(cls, context): - return MultiBlockChangePacket.OpaqueRecord \ - if context.protocol_version >= 347 else \ - MultiBlockChangePacket.Record - - class Record(BaseRecord): - __slots__ = 'blockId', 'blockMeta' - - def __init__(self, h_position, y_coordinate, blockData): - super(MultiBlockChangePacket.Record, self).__init__( - h_position, y_coordinate) - self.blockId = (blockData >> 4) - self.blockMeta = (blockData & 0xF) + def __init__(self, **kwds): + self.block_state_id = 0 + for attr, value in kwds.items(): + setattr(self, attr, value) def __repr__(self): - return ('Record(x=%s, y=%s, z=%s, blockId=%s)' - % (self.x, self.y, self.z, self.blockId)) + return '%s(%s)' % (type(self).__name__, ', '.join( + '%s=%r' % (a, getattr(self, a)) for a in self.__slots__)) - '''The structure of the block data changed in protocol 347 (17w47b, - between 1.12.2 and 1.13), which this class reflects: instead of a - separate blockId and blockMeta number, there is a single opaque - blockStateId whose meaning may change between minor versions.''' - class OpaqueRecord(BaseRecord): - __slots__ = 'blockStateId' + def __eq__(self, other): + return type(self) is type(other) and all( + getattr(self, a) == getattr(other, a) for a in self.__slots__) - def __init__(self, h_position, y_coordinate, blockData): - super(MultiBlockChangePacket.OpaqueRecord, self).__init__( - h_position, y_coordinate) - self.blockStateId = blockData + # Access the 'x', 'y', 'z' fields as a Vector of ints. + def position(self, position): + self.x, self.y, self.z = position + position = property(lambda r: Vector(r.x, r.y, r.z), position) - def __repr__(self): - return ('OpaqueRecord(x=%s, y=%s, z=%s, blockStateId=%s)' - % (self.x, self.y, self.z, self.blockStateId)) + # For protocols < 347: an accessor for (block_state_id >> 4). + def blockId(self, block_id): + self.block_state_id = self.block_state_id & 0xF | block_id << 4 + blockId = property(lambda r: r.block_state_id >> 4, blockId) + + # For protocols < 347: an accessor for (block_state_id & 0xF). + def blockMeta(self, meta): + self.block_state_id = self.block_state_id & ~0xF | meta & 0xF + blockMeta = property(lambda r: r.block_state_id & 0xF, blockMeta) + + # This alias is retained for backward compatibility. + def blockStateId(self, block_state_id): + self.block_state_id = block_state_id + blockStateId = property(lambda r: r.block_state_id, blockStateId) + + def read(self, file_object): + h_position = UnsignedByte.read(file_object) + self.x, self.z = h_position >> 4, h_position & 0xF + self.y = UnsignedByte.read(file_object) + self.block_state_id = VarInt.read(file_object) + + def write(self, packet_buffer): + UnsignedByte.send(self.x << 4 | self.z & 0xF, packet_buffer) + UnsignedByte.send(self.y, packet_buffer) + VarInt.send(self.block_state_id, packet_buffer) def read(self, file_object): self.chunk_x = Integer.read(file_object) self.chunk_z = Integer.read(file_object) records_count = VarInt.read(file_object) - record_type = self.BaseRecord.get_subclass(self.context) self.records = [] for i in range(records_count): - record = record_type(h_position=UnsignedByte.read(file_object), - y_coordinate=UnsignedByte.read(file_object), - blockData=VarInt.read(file_object)) + record = self.Record() + record.read(file_object) self.records.append(record) - def write(self, socket, compression_threshold=None): - raise NotImplementedError + def write_fields(self, packet_buffer): + Integer.send(self.chunk_x, packet_buffer) + Integer.send(self.chunk_z, packet_buffer) + VarInt.send(len(self.records), packet_buffer) + for record in self.records: + record.write(packet_buffer) diff --git a/minecraft/networking/packets/clientbound/play/combat_event_packet.py b/minecraft/networking/packets/clientbound/play/combat_event_packet.py index 1c3c9ae..17f5dc5 100644 --- a/minecraft/networking/packets/clientbound/play/combat_event_packet.py +++ b/minecraft/networking/packets/clientbound/play/combat_event_packet.py @@ -19,48 +19,84 @@ class CombatEventPacket(Packet): packet_name = 'combat event' + # The abstract type of the 'event' field of this packet. class EventType(object): - def read(self, file_object): - self._read(file_object) + __slots__ = () + type_from_id_dict = {} - def _read(self, file_object): + def __init__(self, **kwds): + for attr, value in kwds.items(): + setattr(self, attr, value) + + def __repr__(self, **kwds): + return '%s(%s)' % (type(self).__name__, ', '.join( + '%s=%r' % (a, getattr(self, a)) for a in self.__slots__)) + + def __eq__(self, other): + return type(self) is type(other) and all( + getattr(self, a) == getattr(other, a) for a in self.__slots__) + + # Read the fields of the event (not including the ID) from the file. + def read(self, file_object): + raise NotImplementedError( + 'This abstract method must be overridden in a subclass.') + + # Write the fields of the event (not including the ID) to the buffer. + def write(self, packet_buffer): raise NotImplementedError( 'This abstract method must be overridden in a subclass.') @classmethod def type_from_id(cls, event_id): - subcls = { - 0: CombatEventPacket.EnterCombatEvent, - 1: CombatEventPacket.EndCombatEvent, - 2: CombatEventPacket.EntityDeadEvent - }.get(event_id) + subcls = cls.type_from_id_dict.get(event_id) if subcls is None: - raise ValueError("Unknown combat event ID: %s." - % event_id) + raise ValueError('Unknown combat event ID: %s.' % event_id) return subcls class EnterCombatEvent(EventType): - def _read(self, file_object): + __slots__ = () + id = 0 + + def read(self, file_object): pass + def write(self, packet_buffer): + pass + EventType.type_from_id_dict[EnterCombatEvent.id] = EnterCombatEvent + class EndCombatEvent(EventType): __slots__ = 'duration', 'entity_id' + id = 1 - def _read(self, file_object): + def read(self, file_object): self.duration = VarInt.read(file_object) self.entity_id = Integer.read(file_object) + def write(self, packet_buffer): + VarInt.send(self.duration, packet_buffer) + Integer.send(self.entity_id, packet_buffer) + EventType.type_from_id_dict[EndCombatEvent.id] = EndCombatEvent + class EntityDeadEvent(EventType): __slots__ = 'player_id', 'entity_id', 'message' + id = 2 - def _read(self, file_object): + def read(self, file_object): self.player_id = VarInt.read(file_object) self.entity_id = Integer.read(file_object) self.message = String.read(file_object) + def write(self, packet_buffer): + VarInt.send(self.player_id, packet_buffer) + Integer.send(self.entity_id, packet_buffer) + String.send(self.message, packet_buffer) + EventType.type_from_id_dict[EntityDeadEvent.id] = EntityDeadEvent + def read(self, file_object): event_id = VarInt.read(file_object) - self.event_type = CombatEventPacket.EventType.type_from_id(event_id) + self.event = CombatEventPacket.EventType.type_from_id(event_id)() + self.event.read(file_object) - def write(self, socket, compression_threshold=None): - raise NotImplementedError + def write_fields(self, packet_buffer): + VarInt.send(self.event.id, packet_buffer) + self.event.write(packet_buffer) diff --git a/minecraft/networking/packets/clientbound/play/explosion_packet.py b/minecraft/networking/packets/clientbound/play/explosion_packet.py new file mode 100644 index 0000000..59f1f4d --- /dev/null +++ b/minecraft/networking/packets/clientbound/play/explosion_packet.py @@ -0,0 +1,67 @@ +from minecraft.networking.types import Vector, Float, Byte, Integer +from minecraft.networking.packets import Packet + + +class ExplosionPacket(Packet): + @staticmethod + def get_id(context): + return 0x1D if context.protocol_version >= 345 else \ + 0x1C if context.protocol_version >= 332 else \ + 0x1D if context.protocol_version >= 318 else \ + 0x1C if context.protocol_version >= 80 else \ + 0x1B if context.protocol_version >= 67 else \ + 0x27 + + packet_name = 'explosion' + + class Record(Vector): + __slots__ = () + + @property + def position(self): + return Vector(self.x, self.y, self.x) + + @position.setter + def position(self, new_position): + self.x, self.y, self.z = new_position + + @property + def player_motion(self): + return Vector(self.player_motion_x, self.player_motion_y, + self.player_motion_z) + + @player_motion.setter + def player_motion(self, new_player_motion): + self.player_motion_x, self.player_motion_y, self.player_motion_z \ + = new_player_motion + + def read(self, file_object): + self.x = Float.read(file_object) + self.y = Float.read(file_object) + self.z = Float.read(file_object) + self.radius = Float.read(file_object) + records_count = Integer.read(file_object) + self.records = [] + for i in range(records_count): + rec_x = Byte.read(file_object) + rec_y = Byte.read(file_object) + rec_z = Byte.read(file_object) + record = ExplosionPacket.Record(rec_x, rec_y, rec_z) + self.records.append(record) + self.player_motion_x = Float.read(file_object) + self.player_motion_y = Float.read(file_object) + self.player_motion_z = Float.read(file_object) + + def write_fields(self, packet_buffer): + Float.send(self.x, packet_buffer) + Float.send(self.y, packet_buffer) + Float.send(self.z, packet_buffer) + Float.send(self.radius, packet_buffer) + Integer.send(len(self.records), packet_buffer) + for record in self.records: + Byte.send(record.x, packet_buffer) + Byte.send(record.y, packet_buffer) + Byte.send(record.z, packet_buffer) + Float.send(self.player_motion_x, packet_buffer) + Float.send(self.player_motion_y, packet_buffer) + Float.send(self.player_motion_z, packet_buffer) diff --git a/minecraft/networking/packets/clientbound/play/map_packet.py b/minecraft/networking/packets/clientbound/play/map_packet.py index 65b66bb..cf027b0 100644 --- a/minecraft/networking/packets/clientbound/play/map_packet.py +++ b/minecraft/networking/packets/clientbound/play/map_packet.py @@ -1,7 +1,4 @@ -from minecraft.networking.packets import ( - Packet, PacketBuffer -) - +from minecraft.networking.packets import Packet from minecraft.networking.types import ( VarInt, Byte, Boolean, UnsignedByte, VarIntPrefixedByteArray, String ) @@ -116,10 +113,7 @@ class MapPacket(Packet): map_set.maps_by_id[self.map_id] = map self.apply_to_map(map) - def write(self, socket, compression_threshold=None): - packet_buffer = PacketBuffer() - VarInt.send(self.id, packet_buffer) - + def write_fields(self, packet_buffer): VarInt.send(self.map_id, packet_buffer) Byte.send(self.scale, packet_buffer) if self.context.protocol_version >= 107: @@ -140,8 +134,6 @@ class MapPacket(Packet): UnsignedByte.send(self.offset[1], packet_buffer) # z VarIntPrefixedByteArray.send(self.pixels, packet_buffer) - self._write_buffer(socket, packet_buffer, compression_threshold) - def __repr__(self): return '%sMapPacket(%s)' % ( ('0x%02X ' % self.id) if self.id is not None else '', 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 4d2965a..02e1a96 100644 --- a/minecraft/networking/packets/clientbound/play/player_list_item_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_list_item_packet.py @@ -156,5 +156,5 @@ class PlayerListItemPacket(Packet): for action in self.actions: action.apply(player_list) - def write(self, socket, compression_threshold=None): + def write_fields(self, packet_buffer): raise NotImplementedError diff --git a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py index 995d1cf..16a9422 100644 --- a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py +++ b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py @@ -72,5 +72,5 @@ class SpawnObjectPacket(Packet): self.velocity_y = Short.read(file_object) self.velocity_z = Short.read(file_object) - def write(self, socket, compression_threshold=None): + def write_fields(self, packet_buffer): raise NotImplementedError diff --git a/minecraft/networking/packets/packet.py b/minecraft/networking/packets/packet.py index ace3224..46a5837 100644 --- a/minecraft/networking/packets/packet.py +++ b/minecraft/networking/packets/packet.py @@ -25,9 +25,9 @@ class Packet(object): # 2. Override `get_definition' in a subclass and return the correct # definition for the given ConnectionContext. This may be necessary # if the layout has changed across protocol versions, for example; or - # 3. Override the methods `read' and/or `write' in a subclass. This may be - # necessary if the packet layout cannot be described as a list of - # fields. + # 3. Override the methods `read' and/or `write_fields' in a subclass. + # This may be necessary if the packet layout cannot be described as a + # simple list of fields. @classmethod def get_definition(cls, context): return cls.definition @@ -95,13 +95,17 @@ class Packet(object): # write packet's id right off the bat in the header VarInt.send(self.id, packet_buffer) # write every individual field + self.write_fields(packet_buffer) + self._write_buffer(socket, packet_buffer, compression_threshold) + + def write_fields(self, packet_buffer): + # Write the fields comprising the body of the packet (excluding the + # length, packet ID, compression and encryption) into a PacketBuffer. for field in self.definition: for var_name, data_type in field.items(): data = getattr(self, var_name) data_type.send(data, packet_buffer) - self._write_buffer(socket, packet_buffer, compression_threshold) - def __repr__(self): str = type(self).__name__ if self.id is not None: diff --git a/minecraft/networking/types.py b/minecraft/networking/types.py index 52b69a5..c6a08bb 100644 --- a/minecraft/networking/types.py +++ b/minecraft/networking/types.py @@ -7,6 +7,11 @@ import uuid from collections import namedtuple +# NOTE: subclasses of 'Vector' should have '__slots__ = ()' to avoid the +# creation of a '__dict__' attribute, which would waste space. +Vector = namedtuple('Vector', ('x', 'y', 'z')) + + class Type(object): __slots__ = () @@ -241,7 +246,7 @@ class UUID(Type): socket.send(uuid.UUID(value).bytes) -class Position(Type, namedtuple('Position', ('x', 'y', 'z'))): +class Position(Type, Vector): __slots__ = () @staticmethod diff --git a/tests/test_backward_compatible.py b/tests/test_backward_compatible.py index f81ade3..577b97d 100644 --- a/tests/test_backward_compatible.py +++ b/tests/test_backward_compatible.py @@ -1,5 +1,7 @@ import unittest +from minecraft import SUPPORTED_PROTOCOL_VERSIONS +from minecraft.networking.connection import ConnectionContext from minecraft.networking import packets from minecraft.networking import types from minecraft.networking.packets import clientbound @@ -91,3 +93,11 @@ class ClassMemberAliasesTest(unittest.TestCase): types.AbsoluteHand.LEFT) self.assertEqual(serverbound.play.ClientSettingsPacket.Hand.RIGHT, types.AbsoluteHand.RIGHT) + + def test_block_change_packet(self): + context = ConnectionContext() + context.protocol_version = SUPPORTED_PROTOCOL_VERSIONS[-1] + bi, bm = 358, 9 + packet = clientbound.play.BlockChangePacket(blockId=bi, blockMeta=bm) + self.assertEqual((packet.blockId, packet.blockMeta), (bi, bm)) + self.assertEqual(packet.blockStateId, packet.block_state_id) diff --git a/tests/test_packets.py b/tests/test_packets.py index c2f5247..3e5637e 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -6,9 +6,11 @@ from random import choice from minecraft import SUPPORTED_PROTOCOL_VERSIONS from minecraft.networking.connection import ConnectionContext -from minecraft.networking.types import VarInt, Enum, BitFieldEnum +from minecraft.networking.types import VarInt, Enum, BitFieldEnum, Vector from minecraft.networking.packets import ( - Packet, PacketBuffer, PacketListener, KeepAlivePacket, serverbound) + Packet, PacketBuffer, PacketListener, KeepAlivePacket, serverbound, + clientbound, +) class PacketBufferTest(unittest.TestCase): @@ -35,7 +37,7 @@ class PacketBufferTest(unittest.TestCase): self.assertEqual(packet_buffer.get_writable(), message) -class PacketSerializatonTest(unittest.TestCase): +class PacketSerializationTest(unittest.TestCase): def test_packet(self): for protocol_version in SUPPORTED_PROTOCOL_VERSIONS: @@ -173,3 +175,58 @@ class BitFieldEnumTest(unittest.TestCase): list(map(Example2.name_from_value, range(9))), ['0', 'ONE', 'TWO', 'ONE|TWO', 'FOUR', 'ONE|FOUR', 'TWO|FOUR', 'ONE|TWO|FOUR', None]) + + +class TestReadWritePackets(unittest.TestCase): + maxDiff = None + + def setUp(self): + self.context = ConnectionContext() + self.context.protocol_version = SUPPORTED_PROTOCOL_VERSIONS[-1] + + def tearDown(self): + del self.context + + def test_explosion_packet(self): + Record = clientbound.play.ExplosionPacket.Record + packet = clientbound.play.ExplosionPacket( + position=Vector(787, -37, 0), radius=15, + records=[Record(-14, -116, -5), Record(-77, 34, -36), + Record(-35, -127, 95), Record(11, 113, -8)], + player_motion=Vector(4, 5, 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) + + def test_multi_block_change_packet(self): + Record = clientbound.play.MultiBlockChangePacket.Record + packet = clientbound.play.MultiBlockChangePacket( + chunk_x=167, chunk_z=15, records=[ + Record(x=1, y=2, z=3, blockId=56, blockMeta=13), + Record(position=Vector(1, 2, 3), block_state_id=909), + Record(position=(1, 2, 3), blockStateId=909)]) + self.assertEqual(packet.records[0], packet.records[1]) + self.assertEqual(packet.records[1], packet.records[2]) + self._test_read_write_packet(packet) + + def _test_read_write_packet(self, packet_in): + packet_in.context = self.context + packet_buffer = PacketBuffer() + packet_in.write(packet_buffer) + packet_buffer.reset_cursor() + VarInt.read(packet_buffer) + packet_id = VarInt.read(packet_buffer) + self.assertEqual(packet_id, packet_in.id) + + packet_out = type(packet_in)(context=self.context) + packet_out.read(packet_buffer) + self.assertIs(type(packet_in), type(packet_out)) + self.assertEqual(packet_in.__dict__, packet_out.__dict__)