diff --git a/.travis.yml b/.travis.yml index 85f7c60..cbf10e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ language: python dist: focal -python: 3.8 +python: 3.9 matrix: include: - python: pypy3 env: TOX_ENV=pypy + before_install: + - python -m pip install --upgrade virtualenv - python: 3.5 env: TOX_ENV=py35 - python: 3.6 @@ -14,13 +16,15 @@ matrix: env: TOX_ENV=py37 - python: 3.8 env: TOX_ENV=py38 - - python: 3.8 + - python: 3.9 + env: TOX_ENV=py39 + - python: 3.9 env: TOX_ENV=flake8 - - python: 3.8 + - python: 3.9 env: TOX_ENV=pylint-errors - - python: 3.8 + - python: 3.9 env: TOX_ENV=pylint-full - - python: 3.8 + - python: 3.9 env: TOX_ENV=verify-manifest before_install: @@ -32,6 +36,6 @@ install: script: - tox -e $TOX_ENV after_script: - - if [ "$TOX_ENV" = "py38" ]; then tox -e coveralls; fi + - if [ "$TOX_ENV" = "py39" ]; then tox -e coveralls; fi notifications: email: false diff --git a/README.rst b/README.rst index 69b3515..9c46237 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ pyCraft ======= -.. image:: https://travis-ci.org/ammaraskar/pyCraft.svg?branch=master - :target: https://travis-ci.org/ammaraskar/pyCraft +.. image:: https://app.travis-ci.com/ammaraskar/pyCraft.svg?branch=master + :target: https://app.travis-ci.com/github/ammaraskar/pyCraft .. image:: https://readthedocs.org/projects/pycraft/badge/?version=latest :target: https://pycraft.readthedocs.org/en/latest .. image:: https://coveralls.io/repos/ammaraskar/pyCraft/badge.svg?branch=master @@ -31,7 +31,9 @@ pyCraft is compatible with the following Minecraft releases: * 1.13, 1.13.1, 1.13.2 * 1.14, 1.14.1, 1.14.2, 1.14.3, 1.14.4 * 1.15, 1.15.1, 1.15.2 -* 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4 +* 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5 +* 1.17, 1.17.1 +* 1.18, 1.18.1 In addition, some development snapshots and pre-release versions are supported: ``_ contains a full list of supported Minecraft versions @@ -55,6 +57,7 @@ pyCraft is compatible with (at least) the following Python implementations: * Python 3.6 * Python 3.7 * Python 3.8 +* Python 3.9 * PyPy Requirements diff --git a/minecraft/__init__.py b/minecraft/__init__.py index b63749e..2fbffe7 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -460,8 +460,21 @@ KNOWN_MINECRAFT_VERSION_RECORDS = [ Version('20w45a', PRE | 5, True), Version('20w46a', PRE | 6, True), Version('20w48a', PRE | 7, True), + Version('20w49a', PRE | 8, False), + Version('20w51a', PRE | 9, False), + Version('1.16.5', 754, False), + Version('21w03a', PRE | 11, False), + Version('21w05a', PRE | 12, False), + Version('21w05b', PRE | 13, False), + Version('21w06a', PRE | 14, False), + Version('21w07a', PRE | 15, False), + Version('1.17-rc2', PRE | 35, False), Version('1.17', 755, True), - Version('1.17.1', 756, True), + Version('1.17.1', 756, True), + Version('21w44a', PRE | 48, False), + Version('1.18-rc4', PRE | 60, False), + Version('1.18', 757, True), + Version('1.18.1', 757, True), ] # An OrderedDict mapping the id string of each known Minecraft version to its diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index 4200aaa..0b03e73 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -53,6 +53,13 @@ class ConnectionContext(object): later than, or is equal to, 'other_pv', or else False.""" return utility.protocol_earlier_eq(other_pv, self.protocol_version) + def protocol_in_range(self, start_pv, end_pv): + """Returns True if the protocol version of this context was published + later than, or is equal to, 'start_pv' and was published earlier + than 'end_pv' (analogously to Python's 'range' function).""" + return (utility.protocol_earlier(self.protocol_version, end_pv) and + utility.protocol_earlier_eq(start_pv, self.protocol_version)) + class _ConnectionOptions(object): def __init__(self, address=None, port=None, compression_threshold=-1, diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 1a7b8c0..6d419f2 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -1,3 +1,4 @@ +from minecraft import PRE from minecraft.networking.packets import ( Packet, AbstractKeepAlivePacket, AbstractPluginMessagePacket ) @@ -8,7 +9,10 @@ from minecraft.networking.types import ( PositionAndLook, multi_attribute_alias, attribute_transform, ) -from .combat_event_packet import CombatEventPacket +from .combat_event_packet import ( + CombatEventPacket, EnterCombatEventPacket, EndCombatEventPacket, + DeathCombatEventPacket, +) from .map_packet import MapPacket from .player_list_item_packet import PlayerListItemPacket from .player_position_and_look_packet import PlayerPositionAndLookPacket @@ -36,7 +40,6 @@ def get_packets(context): EntityPositionDeltaPacket, TimeUpdatePacket, UpdateHealthPacket, - CombatEventPacket, ExplosionPacket, SpawnObjectPacket, BlockChangePacket, @@ -47,18 +50,33 @@ def get_packets(context): EntityLookPacket, ResourcePackSendPacket } + if context.protocol_earlier_eq(47): packets |= { SetCompressionPacket, } + + if context.protocol_earlier(PRE | 15): + packets |= { + CombatEventPacket, + } + else: + packets |= { + EnterCombatEventPacket, + EndCombatEventPacket, + DeathCombatEventPacket, + } + if context.protocol_later_eq(94): packets |= { SoundEffectPacket, } + if context.protocol_later_eq(352): packets |= { FacePlayerPacket } + return packets @@ -150,7 +168,7 @@ class SetCompressionPacket(Packet): def get_id(context): return 0x03 if context.protocol_later_eq(755) else \ 0x46 - + packet_name = "set compression" definition = [ {'threshold': VarInt}] @@ -264,7 +282,8 @@ class EntityPositionDeltaPacket(Packet): class TimeUpdatePacket(Packet): @staticmethod def get_id(context): - return 0x58 if context.protocol_later_eq(755) else \ + return 0x59 if context.protocol_later_eq(PRE | 48) else \ + 0x58 if context.protocol_later_eq(755) else \ 0x4E if context.protocol_later_eq(721) else \ 0x4F if context.protocol_later_eq(550) else \ 0x4E if context.protocol_later_eq(471) else \ @@ -332,7 +351,8 @@ class PluginMessagePacket(AbstractPluginMessagePacket): class PlayerListHeaderAndFooterPacket(Packet): @staticmethod def get_id(context): - return 0x5E if context.protocol_later_eq(755) else \ + return 0x5F if context.protocol_later_eq(PRE | 48) else \ + 0x5E if context.protocol_later_eq(755) else \ 0x53 if context.protocol_later_eq(721) else \ 0x54 if context.protocol_later_eq(550) else \ 0x53 if context.protocol_later_eq(471) else \ @@ -374,16 +394,35 @@ class EntityLookPacket(Packet): {'on_ground': Boolean} ] + class ResourcePackSendPacket(Packet): @staticmethod def get_id(context): - return 0x3C if context.protocol_later_eq(755) else \ - 0x38 + return 0x3C if context.protocol_later_eq(PRE | 15) else \ + 0x39 if context.protocol_later_eq(PRE | 8) else \ + 0x38 if context.protocol_later_eq(741) else \ + 0x39 if context.protocol_later_eq(721) else \ + 0x3A if context.protocol_later_eq(550) else \ + 0x39 if context.protocol_later_eq(471) else \ + 0x37 if context.protocol_later_eq(461) else \ + 0x38 if context.protocol_later_eq(451) else \ + 0x37 if context.protocol_later_eq(389) else \ + 0x36 if context.protocol_later_eq(352) else \ + 0x35 if context.protocol_later_eq(345) else \ + 0x34 if context.protocol_later_eq(336) else \ + 0x33 if context.protocol_later_eq(332) else \ + 0x34 if context.protocol_later_eq(318) else \ + 0x32 if context.protocol_later_eq(70) else \ + 0x48 packet_name = "resource pack send" - definition = [ - {"url": String}, - {"hash": String}, - {"forced": Boolean}, - {"forced_message": String} - ] \ No newline at end of file + + @staticmethod + def get_definition(context): + return [ + {"url": String}, + {"hash": String}, + {"forced": Boolean} if context.protocol_later_eq(PRE | 5) else {}, + {"forced_message": String} + if context.protocol_later_eq(PRE | 15) else {}, + ] diff --git a/minecraft/networking/packets/clientbound/play/combat_event_packet.py b/minecraft/networking/packets/clientbound/play/combat_event_packet.py index b06fc0c..1d98995 100644 --- a/minecraft/networking/packets/clientbound/play/combat_event_packet.py +++ b/minecraft/networking/packets/clientbound/play/combat_event_packet.py @@ -1,3 +1,6 @@ +from abc import ABCMeta, abstractmethod + +from minecraft import PRE from minecraft.networking.packets import Packet from minecraft.networking.types import ( @@ -5,10 +8,16 @@ from minecraft.networking.types import ( ) +# Note: this packet was removed in Minecraft 21w07a (protocol PRE|15) +# and replaced with the separate EnterCombatEvent, EndCombatEvent, and +# DeathCombatEvent packets. These are subclasses of CombatEventPacket, so +# that code written to listen for CombatEventPacket instances should in most +# cases continue to work without modification. class CombatEventPacket(Packet): - @staticmethod - def get_id(context): - return 0x31 if context.protocol_later_eq(741) else \ + @classmethod + def get_id(cls, context): + return cls.deprecated() if context.protocol_later_eq(PRE | 15) else \ + 0x31 if context.protocol_later_eq(741) else \ 0x32 if context.protocol_later_eq(721) else \ 0x33 if context.protocol_later_eq(550) else \ 0x32 if context.protocol_later_eq(471) else \ @@ -28,19 +37,19 @@ class CombatEventPacket(Packet): fields = 'event', # The abstract type of the 'event' field of this packet. - class EventType(MutableRecord): + class EventType(MutableRecord, metaclass=ABCMeta): __slots__ = () type_from_id_dict = {} # Read the fields of the event (not including the ID) from the file. + @abstractmethod def read(self, file_object): - raise NotImplementedError( - 'This abstract method must be overridden in a subclass.') + pass # Write the fields of the event (not including the ID) to the buffer. + @abstractmethod def write(self, packet_buffer): - raise NotImplementedError( - 'This abstract method must be overridden in a subclass.') + pass @classmethod def type_from_id(cls, event_id): @@ -89,10 +98,72 @@ class CombatEventPacket(Packet): EventType.type_from_id_dict[EntityDeadEvent.id] = EntityDeadEvent def read(self, file_object): + if self.context and self.context.protocol_later_eq(PRE | 15): + self.deprecated() event_id = VarInt.read(file_object) self.event = CombatEventPacket.EventType.type_from_id(event_id)() self.event.read(file_object) def write_fields(self, packet_buffer): + if self.context and self.context.protocol_later_eq(PRE | 15): + self.deprecated() VarInt.send(self.event.id, packet_buffer) self.event.write(packet_buffer) + + @staticmethod + def deprecated(): + raise NotImplementedError( + '`CombatEventPacket` was removed in Minecraft snapshot 21w07a ' + '(protocol version 2**30 + 15). In this and later versions, one ' + 'of the subclasses ' + + repr(SpecialisedCombatEventPacket.__subclasses__()) + ' must be ' + 'used directly for usage like that which generates this message.') + + +# Contains the behaviour common to all concrete CombatEventPacket subclasses +class SpecialisedCombatEventPacket(CombatEventPacket): + def __init__(self, *args, **kwds): + super(SpecialisedCombatEventPacket, self).__init__(*args, **kwds) + + # Prior to Minecraft 21w07a, instances of CombatEventPacket had a + # single 'event' field giving a 'MutableRecord' of one of three types + # corresponding to the type of combat event represented. For backward + # compatibility, we here present a similar interface, giving the packet + # object itself as the 'event', which should work identically in most + # use cases, since it is a virtual subclass of, and has attributes of + # the same names and contents as those of, the previous event records. + self.event = self + + # The 'get_id', 'fields', 'read', and 'write_fields' attributes of the + # 'Packet' base class are all overridden in 'CombatEventPacket'. We desire + # the default behaviour of these attributes, so we restore them here: + get_id = Packet.__dict__['get_id'] + fields = Packet.__dict__['fields'] + read = Packet.__dict__['read'] + write_fields = Packet.__dict__['write_fields'] + + +@CombatEventPacket.EnterCombatEvent.register # virtual subclass +class EnterCombatEventPacket(SpecialisedCombatEventPacket): + packet_name = 'enter combat event' + id = 0x34 + definition = [] + + +@CombatEventPacket.EndCombatEvent.register # virtual subclass +class EndCombatEventPacket(SpecialisedCombatEventPacket): + packet_name = 'end combat event' + id = 0x33 + definition = [ + {'duration': VarInt}, + {'entity_id': Integer}] + + +@CombatEventPacket.EntityDeadEvent.register # virtual subclass +class DeathCombatEventPacket(SpecialisedCombatEventPacket): + packet_name = 'death combat event' + id = 0x35 + definition = [ + {'player_id': VarInt}, + {'entity_id': Integer}, + {'message': String}] diff --git a/minecraft/networking/packets/clientbound/play/explosion_packet.py b/minecraft/networking/packets/clientbound/play/explosion_packet.py index af045e7..2224d0a 100644 --- a/minecraft/networking/packets/clientbound/play/explosion_packet.py +++ b/minecraft/networking/packets/clientbound/play/explosion_packet.py @@ -1,5 +1,6 @@ from minecraft.networking.types import ( Vector, Float, Byte, Integer, PrefixedArray, multi_attribute_alias, Type, + VarInt, ) from minecraft.networking.packets import Packet @@ -34,15 +35,22 @@ class ExplosionPacket(Packet): for coord in record: Byte.send(coord, socket) - definition = [ - {'x': Float}, - {'y': Float}, - {'z': Float}, - {'radius': Float}, - {'records': PrefixedArray(Integer, Record)}, - {'player_motion_x': Float}, - {'player_motion_y': Float}, - {'player_motion_z': Float}] + @staticmethod + def get_definition(context): + return [ + {'x': Float}, + {'y': Float}, + {'z': Float}, + {'radius': Float}, + + {'records': PrefixedArray(VarInt, ExplosionPacket.Record)} + if context.protocol_later_eq(755) else + {'records': PrefixedArray(Integer, ExplosionPacket.Record)}, + + {'player_motion_x': Float}, + {'player_motion_y': Float}, + {'player_motion_z': Float}, + ] # Access the 'x', 'y', 'z' fields as a Vector tuple. position = multi_attribute_alias(Vector, 'x', 'y', 'z') diff --git a/minecraft/networking/packets/clientbound/play/join_game_and_respawn_packets.py b/minecraft/networking/packets/clientbound/play/join_game_and_respawn_packets.py index 73c27cf..0741e83 100644 --- a/minecraft/networking/packets/clientbound/play/join_game_and_respawn_packets.py +++ b/minecraft/networking/packets/clientbound/play/join_game_and_respawn_packets.py @@ -92,6 +92,8 @@ class JoinGamePacket(AbstractDimensionPacket): VarInt if context.protocol_later_eq(749) else UnsignedByte}, {'level_type': String} if context.protocol_earlier(716) else {}, {'render_distance': VarInt} if context.protocol_later_eq(468) else {}, + {'simulation_distance': VarInt} + if context.protocol_later_eq(757) else {}, {'reduced_debug_info': Boolean}, {'respawn_screen': Boolean} if context.protocol_later_eq(571) else {}, {'is_debug': Boolean} if context.protocol_later_eq(716) else {}, diff --git a/minecraft/networking/packets/clientbound/play/map_packet.py b/minecraft/networking/packets/clientbound/play/map_packet.py index ddd47f0..4b84377 100644 --- a/minecraft/networking/packets/clientbound/play/map_packet.py +++ b/minecraft/networking/packets/clientbound/play/map_packet.py @@ -1,3 +1,4 @@ +from minecraft import PRE from minecraft.networking.packets import Packet from minecraft.networking.types import ( VarInt, Byte, Boolean, UnsignedByte, VarIntPrefixedByteArray, String, @@ -72,9 +73,9 @@ class MapPacket(Packet): self.map_id = VarInt.read(file_object) self.scale = Byte.read(file_object) - if self.context.protocol_later_eq(107): + if self.context.protocol_in_range(107, PRE | 6): self.is_tracking_position = Boolean.read(file_object) - else: + elif self.context.protocol_earlier(107): self.is_tracking_position = True if self.context.protocol_later_eq(452): @@ -82,6 +83,9 @@ class MapPacket(Packet): else: self.is_locked = False + if self.context.protocol_later_eq(PRE | 6): + self.is_tracking_position = Boolean.read(file_object) + icon_count = VarInt.read(file_object) self.icons = [] for i in range(icon_count): @@ -101,10 +105,7 @@ class MapPacket(Packet): icon = MapPacket.MapIcon(type, direction, (x, z), display_name) self.icons.append(icon) - try: - self.width = UnsignedByte.read(file_object) - except: - self.width = None + self.width = UnsignedByte.read(file_object) if self.width: self.height = UnsignedByte.read(file_object) x = Byte.read(file_object) diff --git a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py index c664165..0e68980 100644 --- a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py +++ b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py @@ -1,3 +1,4 @@ +from minecraft import PRE from minecraft.networking.packets import Packet from minecraft.networking.types import ( VarInt, String, Float, Byte, Type, Integer, Vector, Enum, @@ -9,7 +10,8 @@ __all__ = 'SoundEffectPacket', class SoundEffectPacket(Packet): @staticmethod def get_id(context): - return 0x5C if context.protocol_later_eq(755) else \ + return 0x5D if context.protocol_later_eq(PRE | 48) else \ + 0x5C if context.protocol_later_eq(755) else \ 0x51 if context.protocol_later_eq(721) else \ 0x52 if context.protocol_later_eq(550) else \ 0x51 if context.protocol_later_eq(471) else \ diff --git a/minecraft/networking/packets/serverbound/play/__init__.py b/minecraft/networking/packets/serverbound/play/__init__.py index 9dfaf44..7079c5c 100644 --- a/minecraft/networking/packets/serverbound/play/__init__.py +++ b/minecraft/networking/packets/serverbound/play/__init__.py @@ -268,11 +268,12 @@ class UseItemPacket(Packet): Hand = RelativeHand + class ResourcePackStatusPacket(Packet): @staticmethod def get_id(context): return 0x21 - packet_name = "resource pask status" + packet_name = "resource pack status" definition = [ {"result": VarInt} - ] \ No newline at end of file + ] diff --git a/minecraft/networking/packets/serverbound/play/client_settings_packet.py b/minecraft/networking/packets/serverbound/play/client_settings_packet.py index a826f09..4cc5780 100644 --- a/minecraft/networking/packets/serverbound/play/client_settings_packet.py +++ b/minecraft/networking/packets/serverbound/play/client_settings_packet.py @@ -1,8 +1,11 @@ +import operator + from minecraft.networking.packets import Packet from minecraft.networking.types import ( String, Byte, VarInt, Boolean, UnsignedByte, Enum, BitFieldEnum, AbsoluteHand ) +from minecraft.utility import attribute_transform class ClientSettingsPacket(Packet): @@ -18,13 +21,40 @@ class ClientSettingsPacket(Packet): packet_name = 'client settings' - get_definition = staticmethod(lambda context: [ - {'locale': String}, - {'view_distance': Byte}, - {'chat_mode': VarInt if context.protocol_later(47) else Byte}, - {'chat_colors': Boolean}, - {'displayed_skin_parts': UnsignedByte}, - {'main_hand': VarInt} if context.protocol_later(49) else {}]) + @staticmethod + def get_definition(context): + return [ + {'locale': String}, + {'view_distance': Byte}, + {'chat_mode': VarInt if context.protocol_later(47) else Byte}, + {'chat_colors': Boolean}, + {'displayed_skin_parts': UnsignedByte}, + {'main_hand': VarInt} if context.protocol_later(49) else {}, + + {'enable_text_filtering': Boolean} + if context.protocol_later_eq(757) else + {'disable_text_filtering': Boolean} + if context.protocol_later_eq(755) else {}, + + {'allow_server_listings': Boolean} + if context.protocol_later_eq(755) else {}, + ] + + # Set a default value for 'enable_text_filtering', because most clients + # will probably want this value, and to avoid breaking old code. + enable_text_filtering = False + + # To support the possibility of both 'enable_text_filtering' and + # 'disable_text_filtering' fields existing, make 'disable_text_filtering' + # (which is the less likely to be used of the two) into a property that + # stores the negation of its value in the ordinary attribute + # 'enable_text_filtering'. + disable_text_filtering = attribute_transform( + 'enable_text_filtering', operator.not_, operator.not_) + + # Set a default value for 'allow_server_listings', because most clients + # will probably want this value, and to avoid breaking old code. + allow_server_listings = False field_enum = classmethod( lambda cls, field, context: { diff --git a/minecraft/utility.py b/minecraft/utility.py index 2ab8d1d..3d6a2fc 100644 --- a/minecraft/utility.py +++ b/minecraft/utility.py @@ -33,7 +33,20 @@ def attribute_alias(name): """An attribute descriptor that redirects access to a different attribute with a given name. """ - return attribute_transform(name, lambda x: x, lambda x: x) + return property( + fget=(lambda self: getattr(self, name)), + fset=(lambda self, value: setattr(self, name, value)), + fdel=(lambda self: delattr(self, name))) + + +def partial_attribute_alias(name, part): + """An attribute descriptor that redirects access to a particular named + attribute, 'part', on a different attribute with a given name. + """ + return property( + fget=(lambda self: getattr(getattr(self, name), part)), + fset=(lambda self, value: setattr(getattr(self, name), part, value)), + fdel=(lambda self: delattr(getattr(self, name), part))) def multi_attribute_alias(container, *arg_names, **kwd_names): diff --git a/tests/fake_server.py b/tests/fake_server.py index cec1949..4cd2fb0 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -115,7 +115,8 @@ class FakeClientHandler(object): world_name='minecraft:overworld', hashed_seed=12345, difficulty=2, max_players=1, level_type='default', reduced_debug_info=False, render_distance=9, - respawn_screen=False, is_debug=False, is_flat=False) + simulation_distance=9, respawn_screen=False, is_debug=False, + is_flat=False) if self.server.context.protocol_later_eq(748): packet.dimension = pynbt.TAG_Compound({ diff --git a/tests/test_packets.py b/tests/test_packets.py index 44b775c..a59462b 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -6,7 +6,10 @@ import struct from zlib import decompress from random import choice -from minecraft import SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS +from minecraft.utility import protocol_earlier +from minecraft import ( + PRE, SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS, +) from minecraft.networking.connection import ConnectionContext from minecraft.networking.types import ( VarInt, Enum, Vector, PositionAndLook, OriginPoint, @@ -157,19 +160,21 @@ class TestReadWritePackets(unittest.TestCase): maxDiff = None def test_explosion_packet(self): + context = ConnectionContext(protocol_version=TEST_VERSIONS[-1]) 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)) + player_motion=Vector(4, 5, 0), context=context) self.assertEqual( str(packet), - 'ExplosionPacket(x=787, y=-37, z=0, radius=15, records=[' + '0x%02X 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)' + % packet.id ) self._test_read_write_packet(packet) @@ -181,7 +186,16 @@ class TestReadWritePackets(unittest.TestCase): str(packet), 'CombatEventPacket(event=EnterCombatEvent())' ) - self._test_read_write_packet(packet) + self._test_read_write_packet(packet, vmax=PRE | 14) + with self.assertRaises(NotImplementedError): + self._test_read_write_packet(packet, vmin=PRE | 15) + + specialised_packet = clientbound.play.EnterCombatEventPacket() + self.assertIsInstance(specialised_packet.event, type(packet.event)) + for field in specialised_packet.fields: + value = getattr(packet.event, field) + setattr(specialised_packet, field, value) + self.assertEqual(getattr(specialised_packet.event, field), value) packet = clientbound.play.CombatEventPacket( event=clientbound.play.CombatEventPacket.EndCombatEvent( @@ -189,7 +203,16 @@ class TestReadWritePackets(unittest.TestCase): self.assertEqual(str(packet), 'CombatEventPacket(event=EndCombatEvent(' 'duration=415, entity_id=91063502))') - self._test_read_write_packet(packet) + self._test_read_write_packet(packet, vmax=PRE | 14) + with self.assertRaises(NotImplementedError): + self._test_read_write_packet(packet, vmin=PRE | 15) + + specialised_packet = clientbound.play.EndCombatEventPacket() + self.assertIsInstance(specialised_packet.event, type(packet.event)) + for field in specialised_packet.fields: + value = getattr(packet.event, field) + setattr(specialised_packet, field, value) + self.assertEqual(getattr(specialised_packet.event, field), value) packet = clientbound.play.CombatEventPacket( event=clientbound.play.CombatEventPacket.EntityDeadEvent( @@ -199,7 +222,16 @@ class TestReadWritePackets(unittest.TestCase): "CombatEventPacket(event=EntityDeadEvent(" "player_id=178, entity_id=36, message='RIP'))" ) - self._test_read_write_packet(packet) + self._test_read_write_packet(packet, vmax=PRE | 14) + with self.assertRaises(NotImplementedError): + self._test_read_write_packet(packet, vmin=PRE | 15) + + specialised_packet = clientbound.play.DeathCombatEventPacket() + self.assertIsInstance(specialised_packet.event, type(packet.event)) + for field in specialised_packet.fields: + value = getattr(packet.event, field) + setattr(specialised_packet, field, value) + self.assertEqual(getattr(specialised_packet.event, field), value) def test_multi_block_change_packet(self): Record = clientbound.play.MultiBlockChangePacket.Record @@ -348,16 +380,22 @@ class TestReadWritePackets(unittest.TestCase): ) 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, vmin=None, vmax=None, **kwargs + ): """ If kwargs are specified, the key will be tested against the respective delta value. Useful for testing FixedPointNumbers where there is precision lost in the resulting value. """ if context is None: - for protocol_version in TEST_VERSIONS: - logging.debug('protocol_version = %r' % protocol_version) - context = ConnectionContext(protocol_version=protocol_version) + for pv in TEST_VERSIONS: + if vmin is not None and protocol_earlier(pv, vmin): + continue + if vmax is not None and protocol_earlier(vmax, pv): + continue + logging.debug('protocol_version = %r' % pv) + context = ConnectionContext(protocol_version=pv) self._test_read_write_packet(packet_in, context) else: packet_in.context = context diff --git a/tests/test_packets_with_logic.py b/tests/test_packets_with_logic.py index c10a74b..79fa7f0 100644 --- a/tests/test_packets_with_logic.py +++ b/tests/test_packets_with_logic.py @@ -4,6 +4,7 @@ from minecraft.networking.packets import PacketBuffer from minecraft.networking.packets.clientbound.play import ( PlayerPositionAndLookPacket, PlayerListItemPacket, MapPacket ) +from minecraft.networking.packets import serverbound from minecraft.networking.connection import ConnectionContext from tests.test_packets import TEST_VERSIONS @@ -306,3 +307,16 @@ class PlayerListItemTest(unittest.TestCase): packet.write_fields(packet_buffer) self.read_and_apply(context, packet_buffer, player_list) self.assertNotIn(fake_uuid, player_list.players_by_uuid) + + +class ClientSettingsTest(unittest.TestCase): + def test_enable_disable_text_filtering(self): + packet = serverbound.play.ClientSettingsPacket() + self.assertEqual(packet.enable_text_filtering, False) + self.assertEqual(packet.disable_text_filtering, True) + packet.enable_text_filtering = True + self.assertEqual(packet.enable_text_filtering, True) + self.assertEqual(packet.disable_text_filtering, False) + packet.disable_text_filtering = True + self.assertEqual(packet.enable_text_filtering, False) + self.assertEqual(packet.disable_text_filtering, True) diff --git a/tox.ini b/tox.ini index 488032d..6268a91 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py35, py36, py37, py38, pypy, flake8, pylint-errors, pylint-full, verify-manifest +envlist = py35, py36, py37, py38, py39, pypy, flake8, pylint-errors, pylint-full, verify-manifest [testenv] commands = nosetests --with-timer @@ -27,7 +27,7 @@ commands = deps = coveralls -[testenv:py38] +[testenv:py39] setenv = PYCRAFT_RUN_INTERNET_TESTS=1 commands = @@ -41,7 +41,7 @@ deps = mock [testenv:flake8] -basepython = python3.8 +basepython = python3.9 commands = flake8 minecraft tests setup.py start.py bin/generate_travis_yml.py deps = @@ -54,14 +54,14 @@ per-file-ignores = minecraft/networking/packets/__init__.py:F401 [testenv:pylint-errors] -basepython = python3.8 +basepython = python3.9 deps = {[testenv]deps} pylint commands = pylint minecraft -E [testenv:pylint-full] -basepython = python3.8 +basepython = python3.9 deps = {[testenv]deps} pylint @@ -69,7 +69,7 @@ commands = - pylint minecraft --disable=E [testenv:docs] -basepython = python3.8 +basepython = python3.9 deps = {[testenv:cover]deps} sphinx @@ -78,7 +78,7 @@ commands = {toxinidir}/bin/build_docs [testenv:verify-manifest] -basepython = python3.8 +basepython = python3.9 deps = check-manifest commands =