From 5c6edf5e447356461ccc585cbc0efd28175deed8 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Tue, 17 Mar 2020 00:23:35 -0400 Subject: [PATCH 01/24] Added support for snapshot versions up to 20w11a --- minecraft/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 914d1db..9bdbb39 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -225,6 +225,12 @@ SUPPORTED_MINECRAFT_VERSIONS = { '1.15.2-pre1': 576, '1.15.2-pre2': 577, '1.15.2': 578, + '20w06a': 701, + '20w07a': 702, + '20w08a': 703, + '20w09a': 704, + '20w10a': 705, + '20w11a': 706, } # Those Minecraft versions supported by pyCraft which are "release" versions, From 76f7b4bdc990bbaaf66def85ba09928e9e4cff3e Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Fri, 20 Mar 2020 10:42:41 -0400 Subject: [PATCH 02/24] Added support for snapshot 20w12a --- minecraft/__init__.py | 1 + .../packets/clientbound/login/__init__.py | 11 +++++---- .../packets/clientbound/play/__init__.py | 6 +++-- .../clientbound/play/sound_effect_packet.py | 1 - minecraft/networking/types/basic.py | 24 ++++++++++++++++++- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 9bdbb39..722abed 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -231,6 +231,7 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w09a': 704, '20w10a': 705, '20w11a': 706, + '20w12a': 707, } # Those Minecraft versions supported by pyCraft which are "release" versions, diff --git a/minecraft/networking/packets/clientbound/login/__init__.py b/minecraft/networking/packets/clientbound/login/__init__.py index d323fe1..1c1d866 100644 --- a/minecraft/networking/packets/clientbound/login/__init__.py +++ b/minecraft/networking/packets/clientbound/login/__init__.py @@ -1,7 +1,8 @@ from minecraft.networking.packets import Packet from minecraft.networking.types import ( - VarInt, String, VarIntPrefixedByteArray, TrailingByteArray + VarInt, String, VarIntPrefixedByteArray, TrailingByteArray, + UUIDIntegerArray ) @@ -54,9 +55,11 @@ class LoginSuccessPacket(Packet): 0x02 packet_name = "login success" - definition = [ - {'UUID': String}, - {'Username': String}] + get_definition = staticmethod(lambda context: [ + {'UUID': UUIDIntegerArray} if context.protocol_version >= 707 + else {'UUID': String}, + {'Username': String} + ]) class SetCompressionPacket(Packet): diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 30c62aa..8c602d0 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -206,7 +206,8 @@ class SpawnPlayerPacket(Packet): class EntityVelocityPacket(Packet): @staticmethod def get_id(context): - return 0x46 if context.protocol_version >= 550 else \ + return 0x47 if context.protocol_version >= 707 else \ + 0x46 if context.protocol_version >= 550 else \ 0x45 if context.protocol_version >= 471 else \ 0x41 if context.protocol_version >= 461 else \ 0x42 if context.protocol_version >= 451 else \ @@ -232,7 +233,8 @@ class EntityVelocityPacket(Packet): class UpdateHealthPacket(Packet): @staticmethod def get_id(context): - return 0x49 if context.protocol_version >= 550 else \ + return 0x4A if context.protocol_version >= 707 else \ + 0x49 if context.protocol_version >= 550 else \ 0x48 if context.protocol_version >= 471 else \ 0x44 if context.protocol_version >= 461 else \ 0x45 if context.protocol_version >= 451 else \ diff --git a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py index 4c53a0e..da04940 100644 --- a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py +++ b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py @@ -13,7 +13,6 @@ class SoundEffectPacket(Packet): 0x51 if context.protocol_version >= 471 else \ 0x4D if context.protocol_version >= 461 else \ 0x4E if context.protocol_version >= 451 else \ - 0x4E if context.protocol_version >= 451 else \ 0x4D if context.protocol_version >= 389 else \ 0x4C if context.protocol_version >= 352 else \ 0x4B if context.protocol_version >= 345 else \ diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index 4ccf627..99aeba0 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -14,7 +14,7 @@ __all__ = ( 'Integer', 'FixedPointInteger', 'Angle', 'VarInt', 'Long', 'UnsignedLong', 'Float', 'Double', 'ShortPrefixedByteArray', 'VarIntPrefixedByteArray', 'TrailingByteArray', 'String', 'UUID', - 'Position', + 'Position', 'UUIDIntegerArray' ) @@ -253,6 +253,28 @@ class VarIntPrefixedByteArray(Type): socket.send(struct.pack(str(len(value)) + "s", value)) +class UUIDIntegerArray(Type): + """ Minecraft sends an array of 4 integers to represent the most + significant and least significant bits (as longs) of a UUID + because that is how UUIDs are constructed in Java. We need to + unpack this array and repack it with the right endianness to be + used as a python UUID. """ + @staticmethod + def read(file_object): + ints = struct.unpack("4i", file_object.read(4 * 4)) + packed = struct.pack("qq", player_uuid.bytes) + socket.send(struct.pack("4i", msb & 0xffffffff, msb >> 32, + lsb & 0xffffffff, lsb >> 32)) + + class TrailingByteArray(Type): """ A byte array consisting of all remaining data. If present in a packet definition, this should only be the type of the last field. """ From 428a599f40ec997b5df3c0d2ac1b4b9dee466dc5 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Wed, 25 Mar 2020 20:04:55 -0400 Subject: [PATCH 03/24] Added support for snapshot 20w13a --- minecraft/__init__.py | 1 + minecraft/networking/types/basic.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 722abed..d037b49 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -232,6 +232,7 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w10a': 705, '20w11a': 706, '20w12a': 707, + '20w13a': 708, } # Those Minecraft versions supported by pyCraft which are "release" versions, diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index 99aeba0..b2b1166 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -253,6 +253,14 @@ class VarIntPrefixedByteArray(Type): socket.send(struct.pack(str(len(value)) + "s", value)) +# https://stackoverflow.com/questions/1604464/twos-complement-in-python +def twos_comp(val, bits): + """compute the 2's complement of int value val""" + if (val & (1 << (bits - 1))) != 0: # if sign bit is set + val = val - (1 << bits) # compute negative value + return val # return positive value as is + + class UUIDIntegerArray(Type): """ Minecraft sends an array of 4 integers to represent the most significant and least significant bits (as longs) of a UUID @@ -263,7 +271,7 @@ class UUIDIntegerArray(Type): def read(file_object): ints = struct.unpack("4i", file_object.read(4 * 4)) packed = struct.pack("qq", player_uuid.bytes) - socket.send(struct.pack("4i", msb & 0xffffffff, msb >> 32, - lsb & 0xffffffff, lsb >> 32)) + socket.send(struct.pack(">4i", msb >> 32, + twos_comp(msb & 0xffffffff, 32), + lsb >> 32, + twos_comp(lsb & 0xffffffff, 32) + ) + ) class TrailingByteArray(Type): From 7380bc2c614fc4945f6f573467003022df1c657d Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Thu, 16 Apr 2020 20:52:48 -0400 Subject: [PATCH 04/24] Added support for snapshots 20w13b, 20w14a, 20w15a, 20w16a --- minecraft/__init__.py | 4 ++++ .../networking/packets/serverbound/play/__init__.py | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index d037b49..867f2a7 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -233,6 +233,10 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w11a': 706, '20w12a': 707, '20w13a': 708, + '20w13b': 709, + '20w14a': 710, + '20w15a': 711, + '20w16a': 712, } # Those Minecraft versions supported by pyCraft which are "release" versions, diff --git a/minecraft/networking/packets/serverbound/play/__init__.py b/minecraft/networking/packets/serverbound/play/__init__.py index fff5ecd..0d22776 100644 --- a/minecraft/networking/packets/serverbound/play/__init__.py +++ b/minecraft/networking/packets/serverbound/play/__init__.py @@ -37,7 +37,8 @@ def get_packets(context): class KeepAlivePacket(AbstractKeepAlivePacket): @staticmethod def get_id(context): - return 0x0F if context.protocol_version >= 471 else \ + return 0x10 if context.protocol_version >= 712 else \ + 0x0F if context.protocol_version >= 471 else \ 0x10 if context.protocol_version >= 464 else \ 0x0E if context.protocol_version >= 389 else \ 0x0C if context.protocol_version >= 386 else \ @@ -78,7 +79,8 @@ class ChatPacket(Packet): class PositionAndLookPacket(Packet): @staticmethod def get_id(context): - return 0x12 if context.protocol_version >= 471 else \ + return 0x13 if context.protocol_version >= 712 else \ + 0x12 if context.protocol_version >= 471 else \ 0x13 if context.protocol_version >= 464 else \ 0x11 if context.protocol_version >= 389 else \ 0x0F if context.protocol_version >= 386 else \ @@ -197,7 +199,8 @@ class PlayerBlockPlacementPacket(Packet): @staticmethod def get_id(context): - return 0x2C if context.protocol_version >= 468 else \ + return 0x2D if context.protocol_version >= 712 else \ + 0x2C if context.protocol_version >= 468 else \ 0x2B if context.protocol_version >= 464 else \ 0x29 if context.protocol_version >= 389 else \ 0x27 if context.protocol_version >= 386 else \ From 180c698ce1d3555f6f4b6434b19b7c18707407d6 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Wed, 22 Apr 2020 16:36:19 -0400 Subject: [PATCH 05/24] Added support for snapshot 20w17a --- minecraft/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 867f2a7..6b938c9 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -237,6 +237,7 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w14a': 710, '20w15a': 711, '20w16a': 712, + '20w17a': 713, } # Those Minecraft versions supported by pyCraft which are "release" versions, From 8cb02e7f7fe6cc60918ab34e3c9e492e01967c28 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Mon, 4 May 2020 12:04:04 -0400 Subject: [PATCH 06/24] Added support for snapshot 20w18a --- minecraft/__init__.py | 1 + minecraft/networking/packets/clientbound/play/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 6b938c9..1a46ae2 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -238,6 +238,7 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w15a': 711, '20w16a': 712, '20w17a': 713, + '20w18a': 714, } # Those Minecraft versions supported by pyCraft which are "release" versions, diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 8c602d0..394f930 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -279,6 +279,7 @@ class RespawnPacket(Packet): {'hashed_seed': Long} if context.protocol_version >= 552 else {}, {'game_mode': UnsignedByte}, {'level_type': String}, + {'copy_metadata': Boolean}, ]) # These aliases declare the Enum type corresponding to each field: From d26aacec28b12a49a1fd955e8deea6de0045fc79 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Thu, 7 May 2020 11:58:08 -0400 Subject: [PATCH 07/24] Added support for snapshot 20w19a --- minecraft/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 1a46ae2..e490e0f 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -239,6 +239,7 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w16a': 712, '20w17a': 713, '20w18a': 714, + '20w19a': 715, } # Those Minecraft versions supported by pyCraft which are "release" versions, From 9c08c6c9f5e27818b3f56676e548523ceb452701 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Thu, 14 May 2020 21:37:25 -0400 Subject: [PATCH 08/24] Added support for snapshots 20w20a and 20w20b --- minecraft/__init__.py | 2 ++ minecraft/networking/packets/clientbound/play/__init__.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index e490e0f..fc6baac 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -240,6 +240,8 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w17a': 713, '20w18a': 714, '20w19a': 715, + '20w20a': 716, + '20w20b': 717, } # Those Minecraft versions supported by pyCraft which are "release" versions, diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 394f930..acced08 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -91,10 +91,12 @@ class JoinGamePacket(Packet): {'hashed_seed': Long} if context.protocol_version >= 552 else {}, {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, {'max_players': UnsignedByte}, - {'level_type': String}, + {'level_type': String} if context.protocol_version < 716 else {}, {'render_distance': VarInt} if context.protocol_version >= 468 else {}, {'reduced_debug_info': Boolean}, {'respawn_screen': Boolean} if context.protocol_version >= 571 else {}, + {'is_debug': String} if context.protocol_version >= 716 else {}, + {'is_flat': String} if context.protocol_version >= 716 else {}, ]) # These aliases declare the Enum type corresponding to each field: @@ -278,7 +280,9 @@ class RespawnPacket(Packet): {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, {'hashed_seed': Long} if context.protocol_version >= 552 else {}, {'game_mode': UnsignedByte}, - {'level_type': String}, + {'level_type': String} if context.protocol_version < 716 else {}, + {'is_debug': String} if context.protocol_version >= 716 else {}, + {'is_flat': String} if context.protocol_version >= 716 else {}, {'copy_metadata': Boolean}, ]) From 0343df918c9a91a6d5470dceb3411241c94d5998 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Fri, 22 May 2020 12:49:02 -0400 Subject: [PATCH 09/24] Fixed packet types --- minecraft/networking/packets/clientbound/play/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index acced08..83c8692 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -95,8 +95,8 @@ class JoinGamePacket(Packet): {'render_distance': VarInt} if context.protocol_version >= 468 else {}, {'reduced_debug_info': Boolean}, {'respawn_screen': Boolean} if context.protocol_version >= 571 else {}, - {'is_debug': String} if context.protocol_version >= 716 else {}, - {'is_flat': String} if context.protocol_version >= 716 else {}, + {'is_debug': Boolean} if context.protocol_version >= 716 else {}, + {'is_flat': Boolean} if context.protocol_version >= 716 else {}, ]) # These aliases declare the Enum type corresponding to each field: @@ -281,8 +281,8 @@ class RespawnPacket(Packet): {'hashed_seed': Long} if context.protocol_version >= 552 else {}, {'game_mode': UnsignedByte}, {'level_type': String} if context.protocol_version < 716 else {}, - {'is_debug': String} if context.protocol_version >= 716 else {}, - {'is_flat': String} if context.protocol_version >= 716 else {}, + {'is_debug': Boolean} if context.protocol_version >= 716 else {}, + {'is_flat': Boolean} if context.protocol_version >= 716 else {}, {'copy_metadata': Boolean}, ]) From 1b714e644906376232cd5710dcfe2a127072e2ee Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Fri, 22 May 2020 12:49:12 -0400 Subject: [PATCH 10/24] Update tests to use new join game packet definiton --- tests/fake_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fake_server.py b/tests/fake_server.py index ead9e9e..d93cadb 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -103,7 +103,7 @@ class FakeClientHandler(object): # Called upon entering the play state. self.write_packet(clientbound.play.JoinGamePacket( entity_id=0, game_mode=0, dimension=0, hashed_seed=12345, - difficulty=2, max_players=1, level_type='default', + difficulty=2, max_players=1, is_debug=False, is_flat=False, reduced_debug_info=False, render_distance=9, respawn_screen=False)) def handle_play_packet(self, packet): From e61cfffab144569fde9266d94ea097dcdc98332f Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Fri, 22 May 2020 13:28:02 -0400 Subject: [PATCH 11/24] Attempt to fix test? --- tests/fake_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fake_server.py b/tests/fake_server.py index d93cadb..38e106e 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -103,7 +103,7 @@ class FakeClientHandler(object): # Called upon entering the play state. self.write_packet(clientbound.play.JoinGamePacket( entity_id=0, game_mode=0, dimension=0, hashed_seed=12345, - difficulty=2, max_players=1, is_debug=False, is_flat=False, + difficulty=2, max_players=1, level_type='default', is_debug=False, is_flat=False, reduced_debug_info=False, render_distance=9, respawn_screen=False)) def handle_play_packet(self, packet): From 7d9ffb883652fae74efb93a3c1701c06502a8ab8 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Fri, 22 May 2020 14:05:45 -0400 Subject: [PATCH 12/24] Fix line length --- tests/fake_server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/fake_server.py b/tests/fake_server.py index 38e106e..913f483 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -103,8 +103,9 @@ class FakeClientHandler(object): # Called upon entering the play state. self.write_packet(clientbound.play.JoinGamePacket( entity_id=0, game_mode=0, dimension=0, hashed_seed=12345, - difficulty=2, max_players=1, level_type='default', is_debug=False, is_flat=False, - reduced_debug_info=False, render_distance=9, respawn_screen=False)) + difficulty=2, max_players=1, level_type='default', is_debug=False, + is_flat=False, reduced_debug_info=False, render_distance=9, + respawn_screen=False)) def handle_play_packet(self, packet): # Called upon each packet received after handle_play_start() returns. From b58202909930a550c8743bb9993de2e23302621f Mon Sep 17 00:00:00 2001 From: Sillyfrog Date: Fri, 19 Jun 2020 09:18:29 +1000 Subject: [PATCH 13/24] Support for 1.16-rc1 --- .vscode/settings.json | 3 + minecraft/__init__.py | 41 +++++++++-- minecraft/networking/connection.py | 7 +- .../packets/clientbound/play/__init__.py | 69 +++++++++++++++---- .../clientbound/play/block_change_packet.py | 6 +- .../clientbound/play/combat_event_packet.py | 3 +- .../clientbound/play/explosion_packet.py | 3 +- .../clientbound/play/face_player_packet.py | 3 +- .../packets/clientbound/play/map_packet.py | 3 +- .../play/player_list_item_packet.py | 3 +- .../play/player_position_and_look_packet.py | 3 +- .../clientbound/play/sound_effect_packet.py | 3 +- .../clientbound/play/spawn_object_packet.py | 1 + minecraft/networking/packets/packet.py | 2 + .../packets/serverbound/play/__init__.py | 8 ++- start.py | 6 ++ tests/fake_server.py | 5 +- 17 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de7551d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "none" +} \ No newline at end of file diff --git a/minecraft/__init__.py b/minecraft/__init__.py index fc6baac..442a974 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -242,20 +242,47 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w19a': 715, '20w20a': 716, '20w20b': 717, + '1.16 Pre-release 2': 722, + '1.16 Pre-release 3': 725, + '1.16 Pre-release 4': 727, + '1.16 Pre-release 5': 729, + '1.16 Release Candidate 1':734, } # Those Minecraft versions supported by pyCraft which are "release" versions, # i.e. not development snapshots or pre-release versions. -RELEASE_MINECRAFT_VERSIONS = { - vid: protocol for (vid, protocol) in SUPPORTED_MINECRAFT_VERSIONS.items() - if __import__('re').match(r'\d+(\.\d+)+$', vid)} +RELEASE_MINECRAFT_VERSIONS = {} # The protocol versions of SUPPORTED_MINECRAFT_VERSIONS, without duplicates, # in ascending numerical (and hence chronological) order. -SUPPORTED_PROTOCOL_VERSIONS = \ - sorted(set(SUPPORTED_MINECRAFT_VERSIONS.values())) +SUPPORTED_PROTOCOL_VERSIONS = [] # The protocol versions of RELEASE_MINECRAFT_VERSIONS, without duplicates, # in ascending numerical (and hence chronological) order. -RELEASE_PROTOCOL_VERSIONS = \ - sorted(set(RELEASE_MINECRAFT_VERSIONS.values())) +RELEASE_PROTOCOL_VERSIONS = [] + + +def initglobals(): + '''Init the globals from the SUPPORTED_MINECRAFT_VERSIONS dict + + This allows the SUPPORTED_MINECRAFT_VERSIONS dict to be updated, then the + other globals can be updated as well, to allow for dynamic version support. + All updates are done by reference to allow this to work else where in the code. + ''' + global RELEASE_MINECRAFT_VERSIONS, SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS + + import re + + for (vid, protocol) in SUPPORTED_MINECRAFT_VERSIONS.items(): + if re.match(r"\d+(\.\d+)+$", vid): + RELEASE_MINECRAFT_VERSIONS[vid] = protocol + if protocol not in RELEASE_PROTOCOL_VERSIONS: + RELEASE_PROTOCOL_VERSIONS.append(protocol) + if protocol not in SUPPORTED_PROTOCOL_VERSIONS: + SUPPORTED_PROTOCOL_VERSIONS.append(protocol) + + SUPPORTED_PROTOCOL_VERSIONS.sort() + RELEASE_PROTOCOL_VERSIONS.sort() + + +initglobals() \ No newline at end of file diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index f696743..a26728d 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -506,7 +506,10 @@ class Connection(object): ss = 'supported, but not allowed for this connection' \ if server_protocol in SUPPORTED_PROTOCOL_VERSIONS \ else 'not supported' - raise VersionMismatch("Server's %s is %s." % (vs, ss)) + err = VersionMismatch("Server's %s is %s." % (vs, ss)) + err.server_protocol = server_protocol + err.server_version = server_version + raise err def _handle_exit(self): if not self.connected and self.handle_exit is not None: @@ -647,7 +650,7 @@ class PacketReactor(object): packet.read(packet_data) return packet else: - return packets.Packet(context=self.connection.context) + return packets.Packet(context=self.connection.context, packet_id=packet_id) else: return None diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 83c8692..a99bbdd 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -33,6 +33,8 @@ def get_packets(context): DisconnectPacket, SpawnPlayerPacket, EntityVelocityPacket, + EntityPositionDeltaPacket, + TimeUpdatePacket, UpdateHealthPacket, CombatEventPacket, ExplosionPacket, @@ -62,7 +64,8 @@ def get_packets(context): class KeepAlivePacket(AbstractKeepAlivePacket): @staticmethod def get_id(context): - return 0x21 if context.protocol_version >= 550 else \ + return 0x20 if context.protocol_version >= 722 else \ + 0x21 if context.protocol_version >= 550 else \ 0x20 if context.protocol_version >= 471 else \ 0x21 if context.protocol_version >= 389 else \ 0x20 if context.protocol_version >= 345 else \ @@ -75,7 +78,8 @@ class KeepAlivePacket(AbstractKeepAlivePacket): class JoinGamePacket(Packet): @staticmethod def get_id(context): - return 0x26 if context.protocol_version >= 550 else \ + return 0x25 if context.protocol_version >= 722 else \ + 0x26 if context.protocol_version >= 550 else \ 0x25 if context.protocol_version >= 389 else \ 0x24 if context.protocol_version >= 345 else \ 0x23 if context.protocol_version >= 332 else \ @@ -108,7 +112,8 @@ class JoinGamePacket(Packet): class ServerDifficultyPacket(Packet): @staticmethod def get_id(context): - return 0x0E if context.protocol_version >= 550 else \ + return 0x0D if context.protocol_version >= 722 else \ + 0x0E if context.protocol_version >= 550 else \ 0x0D if context.protocol_version >= 332 else \ 0x0E if context.protocol_version >= 318 else \ 0x0D if context.protocol_version >= 70 else \ @@ -127,7 +132,8 @@ class ServerDifficultyPacket(Packet): class ChatMessagePacket(Packet): @staticmethod def get_id(context): - return 0x0F if context.protocol_version >= 550 else \ + return 0x0E if context.protocol_version >= 722 else \ + 0x0F if context.protocol_version >= 550 else \ 0x0E if context.protocol_version >= 343 else \ 0x0F if context.protocol_version >= 332 else \ 0x10 if context.protocol_version >= 317 else \ @@ -148,7 +154,8 @@ class ChatMessagePacket(Packet): class DisconnectPacket(Packet): @staticmethod def get_id(context): - return 0x1B if context.protocol_version >= 550 else \ + return 0x1A if context.protocol_version >= 722 else \ + 0x1B if context.protocol_version >= 550 else \ 0x1A if context.protocol_version >= 471 else \ 0x1B if context.protocol_version >= 345 else \ 0x1A if context.protocol_version >= 332 else \ @@ -173,7 +180,8 @@ class SetCompressionPacket(Packet): class SpawnPlayerPacket(Packet): @staticmethod def get_id(context): - return 0x05 if context.protocol_version >= 67 else \ + return 0x04 if context.protocol_version >= 722 else \ + 0x05 if context.protocol_version >= 67 else \ 0x0C packet_name = 'spawn player' @@ -208,7 +216,8 @@ class SpawnPlayerPacket(Packet): class EntityVelocityPacket(Packet): @staticmethod def get_id(context): - return 0x47 if context.protocol_version >= 707 else \ + return 0x46 if context.protocol_version >= 722 else \ + 0x47 if context.protocol_version >= 707 else \ 0x46 if context.protocol_version >= 550 else \ 0x45 if context.protocol_version >= 471 else \ 0x41 if context.protocol_version >= 461 else \ @@ -232,10 +241,42 @@ class EntityVelocityPacket(Packet): ]) +class EntityPositionDeltaPacket(Packet): + @staticmethod + def get_id(context): + return 0x28 if context.protocol_version >= 722 else \ + 0x29 if context.protocol_version >= 578 else \ + 0xFF + + packet_name = "entity position delta" + get_definition = staticmethod(lambda context: [ + {'entity_id': VarInt}, + {'delta_x': Short}, + {'delta_y': Short}, + {'delta_z': Short}, + {'on_ground': Boolean} + ]) + + +class TimeUpdatePacket(Packet): + @staticmethod + def get_id(context): + return 0x4E if context.protocol_version >= 722 else \ + 0x4F if context.protocol_version >= 578 else \ + 0xFF + + packet_name = "time update" + get_definition = staticmethod(lambda context: [ + {'world_age': Long}, + {'time_of_day': Long}, + ]) + + class UpdateHealthPacket(Packet): @staticmethod def get_id(context): - return 0x4A if context.protocol_version >= 707 else \ + return 0x49 if context.protocol_version >= 722 else \ + 0x4A if context.protocol_version >= 707 else \ 0x49 if context.protocol_version >= 550 else \ 0x48 if context.protocol_version >= 471 else \ 0x44 if context.protocol_version >= 461 else \ @@ -261,7 +302,8 @@ class UpdateHealthPacket(Packet): class RespawnPacket(Packet): @staticmethod def get_id(context): - return 0x3B if context.protocol_version >= 550 else \ + return 0x3A if context.protocol_version >= 722 else \ + 0x3B if context.protocol_version >= 550 else \ 0x3A if context.protocol_version >= 471 else \ 0x38 if context.protocol_version >= 461 else \ 0x39 if context.protocol_version >= 451 else \ @@ -295,7 +337,8 @@ class RespawnPacket(Packet): class PluginMessagePacket(AbstractPluginMessagePacket): @staticmethod def get_id(context): - return 0x19 if context.protocol_version >= 550 else \ + return 0x18 if context.protocol_version >= 722 else \ + 0x19 if context.protocol_version >= 550 else \ 0x18 if context.protocol_version >= 471 else \ 0x19 if context.protocol_version >= 345 else \ 0x18 if context.protocol_version >= 332 else \ @@ -307,7 +350,8 @@ class PluginMessagePacket(AbstractPluginMessagePacket): class PlayerListHeaderAndFooterPacket(Packet): @staticmethod def get_id(context): - return 0x54 if context.protocol_version >= 550 else \ + return 0x53 if context.protocol_version >= 722 else \ + 0x54 if context.protocol_version >= 550 else \ 0x53 if context.protocol_version >= 471 else \ 0x5F if context.protocol_version >= 461 else \ 0x50 if context.protocol_version >= 451 else \ @@ -328,7 +372,8 @@ class PlayerListHeaderAndFooterPacket(Packet): class EntityLookPacket(Packet): @staticmethod def get_id(context): - return 0x2B if context.protocol_version >= 550 else \ + return 0x2A if context.protocol_version >= 722 else \ + 0x2B if context.protocol_version >= 550 else \ 0x2A if context.protocol_version >= 389 else \ 0x29 if context.protocol_version >= 345 else \ 0x28 if context.protocol_version >= 318 else \ diff --git a/minecraft/networking/packets/clientbound/play/block_change_packet.py b/minecraft/networking/packets/clientbound/play/block_change_packet.py index ee61d0f..ddfae88 100644 --- a/minecraft/networking/packets/clientbound/play/block_change_packet.py +++ b/minecraft/networking/packets/clientbound/play/block_change_packet.py @@ -8,7 +8,8 @@ from minecraft.networking.types import ( class BlockChangePacket(Packet): @staticmethod def get_id(context): - return 0x0C if context.protocol_version >= 550 else \ + return 0x0B if context.protocol_version >= 722 else \ + 0x0C if context.protocol_version >= 550 else \ 0x0B if context.protocol_version >= 332 else \ 0x0C if context.protocol_version >= 318 else \ 0x0B if context.protocol_version >= 67 else \ @@ -46,7 +47,8 @@ class BlockChangePacket(Packet): class MultiBlockChangePacket(Packet): @staticmethod def get_id(context): - return 0x10 if context.protocol_version >= 550 else \ + return 0x0F if context.protocol_version >= 722 else \ + 0x10 if context.protocol_version >= 550 else \ 0x0F if context.protocol_version >= 343 else \ 0x10 if context.protocol_version >= 332 else \ 0x11 if context.protocol_version >= 318 else \ diff --git a/minecraft/networking/packets/clientbound/play/combat_event_packet.py b/minecraft/networking/packets/clientbound/play/combat_event_packet.py index 22c39ad..7bf3794 100644 --- a/minecraft/networking/packets/clientbound/play/combat_event_packet.py +++ b/minecraft/networking/packets/clientbound/play/combat_event_packet.py @@ -8,7 +8,8 @@ from minecraft.networking.types import ( class CombatEventPacket(Packet): @staticmethod def get_id(context): - return 0x33 if context.protocol_version >= 550 else \ + return 0x32 if context.protocol_version >= 722 else \ + 0x33 if context.protocol_version >= 550 else \ 0x32 if context.protocol_version >= 471 else \ 0x30 if context.protocol_version >= 451 else \ 0x2F if context.protocol_version >= 389 else \ diff --git a/minecraft/networking/packets/clientbound/play/explosion_packet.py b/minecraft/networking/packets/clientbound/play/explosion_packet.py index eafdd33..3c9f8e5 100644 --- a/minecraft/networking/packets/clientbound/play/explosion_packet.py +++ b/minecraft/networking/packets/clientbound/play/explosion_packet.py @@ -7,7 +7,8 @@ from minecraft.networking.packets import Packet class ExplosionPacket(Packet): @staticmethod def get_id(context): - return 0x1D if context.protocol_version >= 550 else \ + return 0x1C if context.protocol_version >= 722 else \ + 0x1D if context.protocol_version >= 550 else \ 0x1C if context.protocol_version >= 471 else \ 0x1E if context.protocol_version >= 389 else \ 0x1D if context.protocol_version >= 345 else \ diff --git a/minecraft/networking/packets/clientbound/play/face_player_packet.py b/minecraft/networking/packets/clientbound/play/face_player_packet.py index b98b641..e015792 100644 --- a/minecraft/networking/packets/clientbound/play/face_player_packet.py +++ b/minecraft/networking/packets/clientbound/play/face_player_packet.py @@ -8,7 +8,8 @@ from minecraft.networking.packets import Packet class FacePlayerPacket(Packet): @staticmethod def get_id(context): - return 0x35 if context.protocol_version >= 550 else \ + return 0x34 if context.protocol_version >= 722 else \ + 0x35 if context.protocol_version >= 550 else \ 0x34 if context.protocol_version >= 471 else \ 0x32 if context.protocol_version >= 451 else \ 0x31 if context.protocol_version >= 389 else \ diff --git a/minecraft/networking/packets/clientbound/play/map_packet.py b/minecraft/networking/packets/clientbound/play/map_packet.py index 99870b8..089c376 100644 --- a/minecraft/networking/packets/clientbound/play/map_packet.py +++ b/minecraft/networking/packets/clientbound/play/map_packet.py @@ -8,7 +8,8 @@ from minecraft.networking.types import ( class MapPacket(Packet): @staticmethod def get_id(context): - return 0x27 if context.protocol_version >= 550 else \ + return 0x26 if context.protocol_version >= 722 else \ + 0x27 if context.protocol_version >= 550 else \ 0x26 if context.protocol_version >= 389 else \ 0x25 if context.protocol_version >= 345 else \ 0x24 if context.protocol_version >= 334 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 0bba601..ec62131 100644 --- a/minecraft/networking/packets/clientbound/play/player_list_item_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_list_item_packet.py @@ -9,7 +9,8 @@ from minecraft.networking.types import ( class PlayerListItemPacket(Packet): @staticmethod def get_id(context): - return 0x34 if context.protocol_version >= 550 else \ + return 0x33 if context.protocol_version >= 722 else \ + 0x34 if context.protocol_version >= 550 else \ 0x33 if context.protocol_version >= 471 else \ 0x31 if context.protocol_version >= 451 else \ 0x30 if context.protocol_version >= 389 else \ diff --git a/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py b/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py index 0e6cd29..ac4a3fb 100644 --- a/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py @@ -9,7 +9,8 @@ from minecraft.networking.types import ( class PlayerPositionAndLookPacket(Packet, BitFieldEnum): @staticmethod def get_id(context): - return 0x36 if context.protocol_version >= 550 else \ + return 0x35 if context.protocol_version >= 722 else \ + 0x36 if context.protocol_version >= 550 else \ 0x35 if context.protocol_version >= 471 else \ 0x33 if context.protocol_version >= 451 else \ 0x32 if context.protocol_version >= 389 else \ diff --git a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py index da04940..96d6f60 100644 --- a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py +++ b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py @@ -9,7 +9,8 @@ __all__ = 'SoundEffectPacket', class SoundEffectPacket(Packet): @staticmethod def get_id(context): - return 0x52 if context.protocol_version >= 550 else \ + return 0x51 if context.protocol_version >= 722 else \ + 0x52 if context.protocol_version >= 550 else \ 0x51 if context.protocol_version >= 471 else \ 0x4D if context.protocol_version >= 461 else \ 0x4E if context.protocol_version >= 451 else \ diff --git a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py index d856f3f..c129b72 100644 --- a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py +++ b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py @@ -49,6 +49,7 @@ class SpawnObjectPacket(Packet): return getattr(cls, name) class EntityType(Enum): + # XXX This has not been updated for >= v1.15 ACTIVATED_TNT = 50 if pv < 458 else 55 # PrimedTnt AREA_EFFECT_CLOUD = 3 if pv < 458 else 0 ARMORSTAND = 78 if pv < 458 else 1 diff --git a/minecraft/networking/packets/packet.py b/minecraft/networking/packets/packet.py index a847de0..78de42f 100644 --- a/minecraft/networking/packets/packet.py +++ b/minecraft/networking/packets/packet.py @@ -110,6 +110,8 @@ class Packet(object): str = type(self).__name__ if self.id is not None: str = '0x%02X %s' % (self.id, str) + elif hasattr(self, "packet_id"): + str = 'pkt: 0x%02X %s' % (self.packet_id, str) fields = self.fields if fields is not None: inner_str = ', '.join('%s=%s' % (a, self.field_string(a)) diff --git a/minecraft/networking/packets/serverbound/play/__init__.py b/minecraft/networking/packets/serverbound/play/__init__.py index 0d22776..85267f4 100644 --- a/minecraft/networking/packets/serverbound/play/__init__.py +++ b/minecraft/networking/packets/serverbound/play/__init__.py @@ -5,7 +5,7 @@ from minecraft.networking.packets import ( from minecraft.networking.types import ( Double, Float, Boolean, VarInt, String, Byte, Position, Enum, RelativeHand, BlockFace, Vector, Direction, PositionAndLook, - multi_attribute_alias + multi_attribute_alias, Short ) from .client_settings_packet import ClientSettingsPacket @@ -126,7 +126,8 @@ class TeleportConfirmPacket(Packet): class AnimationPacket(Packet): @staticmethod def get_id(context): - return 0x2A if context.protocol_version >= 468 else \ + return 0x2B if context.protocol_version >= 719 else \ + 0x2A if context.protocol_version >= 468 else \ 0x29 if context.protocol_version >= 464 else \ 0x27 if context.protocol_version >= 389 else \ 0x25 if context.protocol_version >= 386 else \ @@ -237,7 +238,8 @@ class PlayerBlockPlacementPacket(Packet): class UseItemPacket(Packet): @staticmethod def get_id(context): - return 0x2D if context.protocol_version >= 468 else \ + return 0x2E if context.protocol_version >= 719 else \ + 0x2D if context.protocol_version >= 468 else \ 0x2C if context.protocol_version >= 464 else \ 0x2A if context.protocol_version >= 389 else \ 0x28 if context.protocol_version >= 386 else \ diff --git a/start.py b/start.py index 493a9a7..83d653b 100755 --- a/start.py +++ b/start.py @@ -35,6 +35,10 @@ def get_options(): action="store_true", help="print sent and received packets to standard error") + parser.add_option("-v", "--dump-unknown-packets", dest="dump_unknown", + action="store_true", + help="include unknown packets in --dump-packets output") + (options, args) = parser.parse_args() if not options.username: @@ -82,6 +86,8 @@ def main(): if type(packet) is Packet: # This is a direct instance of the base Packet type, meaning # that it is a packet of unknown type, so we do not print it. + if options.dump_unknown: + print('--> ??? %s' % packet, file=sys.stderr) return print('--> %s' % packet, file=sys.stderr) diff --git a/tests/fake_server.py b/tests/fake_server.py index 913f483..ead9e9e 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -103,9 +103,8 @@ class FakeClientHandler(object): # Called upon entering the play state. self.write_packet(clientbound.play.JoinGamePacket( entity_id=0, game_mode=0, dimension=0, hashed_seed=12345, - difficulty=2, max_players=1, level_type='default', is_debug=False, - is_flat=False, reduced_debug_info=False, render_distance=9, - respawn_screen=False)) + difficulty=2, max_players=1, level_type='default', + reduced_debug_info=False, render_distance=9, respawn_screen=False)) def handle_play_packet(self, packet): # Called upon each packet received after handle_play_start() returns. From 0d28271c962f6f4958cc685dd45bfa86daafc076 Mon Sep 17 00:00:00 2001 From: Sillyfrog Date: Wed, 24 Jun 2020 13:50:29 +1000 Subject: [PATCH 14/24] v1.16 release support --- minecraft/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 442a974..c74065f 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -247,6 +247,7 @@ SUPPORTED_MINECRAFT_VERSIONS = { '1.16 Pre-release 4': 727, '1.16 Pre-release 5': 729, '1.16 Release Candidate 1':734, + '1.16': 735, } # Those Minecraft versions supported by pyCraft which are "release" versions, From 3dcefae645ef2787837f18bd21ef73b1cff4e8de Mon Sep 17 00:00:00 2001 From: Sillyfrog Date: Fri, 26 Jun 2020 17:46:33 +1000 Subject: [PATCH 15/24] v1.16.1 support --- minecraft/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index c74065f..6419504 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -248,6 +248,7 @@ SUPPORTED_MINECRAFT_VERSIONS = { '1.16 Pre-release 5': 729, '1.16 Release Candidate 1':734, '1.16': 735, + '1.16.1': 736, } # Those Minecraft versions supported by pyCraft which are "release" versions, From 8a098b399b40967d672b6d5db448d28f09b4bfa8 Mon Sep 17 00:00:00 2001 From: Sillyfrog Date: Thu, 13 Aug 2020 22:19:04 +1000 Subject: [PATCH 16/24] Support for v1.16.2 --- minecraft/__init__.py | 1 + .../packets/clientbound/play/__init__.py | 23 +++++++++++++------ .../clientbound/play/block_change_packet.py | 3 ++- .../clientbound/play/combat_event_packet.py | 3 ++- .../clientbound/play/explosion_packet.py | 3 ++- .../clientbound/play/face_player_packet.py | 3 ++- .../packets/clientbound/play/map_packet.py | 3 ++- .../play/player_list_item_packet.py | 3 ++- .../play/player_position_and_look_packet.py | 3 ++- 9 files changed, 31 insertions(+), 14 deletions(-) diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 6419504..db169c0 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -249,6 +249,7 @@ SUPPORTED_MINECRAFT_VERSIONS = { '1.16 Release Candidate 1':734, '1.16': 735, '1.16.1': 736, + '1.16.2': 751, } # Those Minecraft versions supported by pyCraft which are "release" versions, diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index a99bbdd..2f81d9f 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -64,7 +64,8 @@ def get_packets(context): class KeepAlivePacket(AbstractKeepAlivePacket): @staticmethod def get_id(context): - return 0x20 if context.protocol_version >= 722 else \ + return 0x1F if context.protocol_version >= 751 else \ + 0x20 if context.protocol_version >= 722 else \ 0x21 if context.protocol_version >= 550 else \ 0x20 if context.protocol_version >= 471 else \ 0x21 if context.protocol_version >= 389 else \ @@ -78,7 +79,8 @@ class KeepAlivePacket(AbstractKeepAlivePacket): class JoinGamePacket(Packet): @staticmethod def get_id(context): - return 0x25 if context.protocol_version >= 722 else \ + return 0x24 if context.protocol_version >= 751 else \ + 0x25 if context.protocol_version >= 722 else \ 0x26 if context.protocol_version >= 550 else \ 0x25 if context.protocol_version >= 389 else \ 0x24 if context.protocol_version >= 345 else \ @@ -90,7 +92,9 @@ class JoinGamePacket(Packet): packet_name = "join game" get_definition = staticmethod(lambda context: [ {'entity_id': Integer}, + {'is_hardcore': Boolean} if context.protocol_version >= 751 else {}, {'game_mode': UnsignedByte}, + {'previous_game_mode': UnsignedByte} if context.protocol_version >= 722 else {}, {'dimension': Integer if context.protocol_version >= 108 else Byte}, {'hashed_seed': Long} if context.protocol_version >= 552 else {}, {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, @@ -154,7 +158,8 @@ class ChatMessagePacket(Packet): class DisconnectPacket(Packet): @staticmethod def get_id(context): - return 0x1A if context.protocol_version >= 722 else \ + return 0x19 if context.protocol_version >= 751 else \ + 0x1A if context.protocol_version >= 722 else \ 0x1B if context.protocol_version >= 550 else \ 0x1A if context.protocol_version >= 471 else \ 0x1B if context.protocol_version >= 345 else \ @@ -244,7 +249,8 @@ class EntityVelocityPacket(Packet): class EntityPositionDeltaPacket(Packet): @staticmethod def get_id(context): - return 0x28 if context.protocol_version >= 722 else \ + return 0x27 if context.protocol_version >= 751 else \ + 0x28 if context.protocol_version >= 722 else \ 0x29 if context.protocol_version >= 578 else \ 0xFF @@ -302,7 +308,8 @@ class UpdateHealthPacket(Packet): class RespawnPacket(Packet): @staticmethod def get_id(context): - return 0x3A if context.protocol_version >= 722 else \ + return 0x39 if context.protocol_version >= 751 else \ + 0x3A if context.protocol_version >= 722 else \ 0x3B if context.protocol_version >= 550 else \ 0x3A if context.protocol_version >= 471 else \ 0x38 if context.protocol_version >= 461 else \ @@ -337,7 +344,8 @@ class RespawnPacket(Packet): class PluginMessagePacket(AbstractPluginMessagePacket): @staticmethod def get_id(context): - return 0x18 if context.protocol_version >= 722 else \ + return 0x17 if context.protocol_version >= 751 else \ + 0x18 if context.protocol_version >= 722 else \ 0x19 if context.protocol_version >= 550 else \ 0x18 if context.protocol_version >= 471 else \ 0x19 if context.protocol_version >= 345 else \ @@ -372,7 +380,8 @@ class PlayerListHeaderAndFooterPacket(Packet): class EntityLookPacket(Packet): @staticmethod def get_id(context): - return 0x2A if context.protocol_version >= 722 else \ + return 0x29 if context.protocol_version >= 751 else \ + 0x2A if context.protocol_version >= 722 else \ 0x2B if context.protocol_version >= 550 else \ 0x2A if context.protocol_version >= 389 else \ 0x29 if context.protocol_version >= 345 else \ diff --git a/minecraft/networking/packets/clientbound/play/block_change_packet.py b/minecraft/networking/packets/clientbound/play/block_change_packet.py index ddfae88..396c59c 100644 --- a/minecraft/networking/packets/clientbound/play/block_change_packet.py +++ b/minecraft/networking/packets/clientbound/play/block_change_packet.py @@ -47,7 +47,8 @@ class BlockChangePacket(Packet): class MultiBlockChangePacket(Packet): @staticmethod def get_id(context): - return 0x0F if context.protocol_version >= 722 else \ + return 0x3B if context.protocol_version >= 751 else \ + 0x0F if context.protocol_version >= 722 else \ 0x10 if context.protocol_version >= 550 else \ 0x0F if context.protocol_version >= 343 else \ 0x10 if context.protocol_version >= 332 else \ diff --git a/minecraft/networking/packets/clientbound/play/combat_event_packet.py b/minecraft/networking/packets/clientbound/play/combat_event_packet.py index 7bf3794..8d7e80c 100644 --- a/minecraft/networking/packets/clientbound/play/combat_event_packet.py +++ b/minecraft/networking/packets/clientbound/play/combat_event_packet.py @@ -8,7 +8,8 @@ from minecraft.networking.types import ( class CombatEventPacket(Packet): @staticmethod def get_id(context): - return 0x32 if context.protocol_version >= 722 else \ + return 0x31 if context.protocol_version >= 751 else \ + 0x32 if context.protocol_version >= 722 else \ 0x33 if context.protocol_version >= 550 else \ 0x32 if context.protocol_version >= 471 else \ 0x30 if context.protocol_version >= 451 else \ diff --git a/minecraft/networking/packets/clientbound/play/explosion_packet.py b/minecraft/networking/packets/clientbound/play/explosion_packet.py index 3c9f8e5..c47a48d 100644 --- a/minecraft/networking/packets/clientbound/play/explosion_packet.py +++ b/minecraft/networking/packets/clientbound/play/explosion_packet.py @@ -7,7 +7,8 @@ from minecraft.networking.packets import Packet class ExplosionPacket(Packet): @staticmethod def get_id(context): - return 0x1C if context.protocol_version >= 722 else \ + return 0x1B if context.protocol_version >= 751 else \ + 0x1C if context.protocol_version >= 722 else \ 0x1D if context.protocol_version >= 550 else \ 0x1C if context.protocol_version >= 471 else \ 0x1E if context.protocol_version >= 389 else \ diff --git a/minecraft/networking/packets/clientbound/play/face_player_packet.py b/minecraft/networking/packets/clientbound/play/face_player_packet.py index e015792..6db4a07 100644 --- a/minecraft/networking/packets/clientbound/play/face_player_packet.py +++ b/minecraft/networking/packets/clientbound/play/face_player_packet.py @@ -8,7 +8,8 @@ from minecraft.networking.packets import Packet class FacePlayerPacket(Packet): @staticmethod def get_id(context): - return 0x34 if context.protocol_version >= 722 else \ + return 0x33 if context.protocol_version >= 751 else \ + 0x34 if context.protocol_version >= 722 else \ 0x35 if context.protocol_version >= 550 else \ 0x34 if context.protocol_version >= 471 else \ 0x32 if context.protocol_version >= 451 else \ diff --git a/minecraft/networking/packets/clientbound/play/map_packet.py b/minecraft/networking/packets/clientbound/play/map_packet.py index 089c376..ae45f5e 100644 --- a/minecraft/networking/packets/clientbound/play/map_packet.py +++ b/minecraft/networking/packets/clientbound/play/map_packet.py @@ -8,7 +8,8 @@ from minecraft.networking.types import ( class MapPacket(Packet): @staticmethod def get_id(context): - return 0x26 if context.protocol_version >= 722 else \ + return 0x25 if context.protocol_version >= 751 else \ + 0x26 if context.protocol_version >= 722 else \ 0x27 if context.protocol_version >= 550 else \ 0x26 if context.protocol_version >= 389 else \ 0x25 if context.protocol_version >= 345 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 ec62131..f9ebd8e 100644 --- a/minecraft/networking/packets/clientbound/play/player_list_item_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_list_item_packet.py @@ -9,7 +9,8 @@ from minecraft.networking.types import ( class PlayerListItemPacket(Packet): @staticmethod def get_id(context): - return 0x33 if context.protocol_version >= 722 else \ + return 0x32 if context.protocol_version >= 751 else \ + 0x33 if context.protocol_version >= 722 else \ 0x34 if context.protocol_version >= 550 else \ 0x33 if context.protocol_version >= 471 else \ 0x31 if context.protocol_version >= 451 else \ diff --git a/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py b/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py index ac4a3fb..9df7be7 100644 --- a/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py @@ -9,7 +9,8 @@ from minecraft.networking.types import ( class PlayerPositionAndLookPacket(Packet, BitFieldEnum): @staticmethod def get_id(context): - return 0x35 if context.protocol_version >= 722 else \ + return 0x34 if context.protocol_version >= 751 else \ + 0x35 if context.protocol_version >= 722 else \ 0x36 if context.protocol_version >= 550 else \ 0x35 if context.protocol_version >= 471 else \ 0x33 if context.protocol_version >= 451 else \ From c6afe25429de7dc89c741eeaa9ca782430796884 Mon Sep 17 00:00:00 2001 From: joo Date: Wed, 12 Aug 2020 12:46:08 +0200 Subject: [PATCH 17/24] Remove 'UUIDIntegerArray' type, as 'UUID' already exists --- .../packets/clientbound/login/__init__.py | 6 ++-- minecraft/networking/types/basic.py | 36 +------------------ 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/minecraft/networking/packets/clientbound/login/__init__.py b/minecraft/networking/packets/clientbound/login/__init__.py index 1c1d866..38e0d8b 100644 --- a/minecraft/networking/packets/clientbound/login/__init__.py +++ b/minecraft/networking/packets/clientbound/login/__init__.py @@ -1,8 +1,7 @@ from minecraft.networking.packets import Packet from minecraft.networking.types import ( - VarInt, String, VarIntPrefixedByteArray, TrailingByteArray, - UUIDIntegerArray + VarInt, String, VarIntPrefixedByteArray, TrailingByteArray, UUID, ) @@ -56,8 +55,7 @@ class LoginSuccessPacket(Packet): packet_name = "login success" get_definition = staticmethod(lambda context: [ - {'UUID': UUIDIntegerArray} if context.protocol_version >= 707 - else {'UUID': String}, + {'UUID': UUID if context.protocol_version >= 707 else String}, {'Username': String} ]) diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index b2b1166..4ccf627 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -14,7 +14,7 @@ __all__ = ( 'Integer', 'FixedPointInteger', 'Angle', 'VarInt', 'Long', 'UnsignedLong', 'Float', 'Double', 'ShortPrefixedByteArray', 'VarIntPrefixedByteArray', 'TrailingByteArray', 'String', 'UUID', - 'Position', 'UUIDIntegerArray' + 'Position', ) @@ -253,40 +253,6 @@ class VarIntPrefixedByteArray(Type): socket.send(struct.pack(str(len(value)) + "s", value)) -# https://stackoverflow.com/questions/1604464/twos-complement-in-python -def twos_comp(val, bits): - """compute the 2's complement of int value val""" - if (val & (1 << (bits - 1))) != 0: # if sign bit is set - val = val - (1 << bits) # compute negative value - return val # return positive value as is - - -class UUIDIntegerArray(Type): - """ Minecraft sends an array of 4 integers to represent the most - significant and least significant bits (as longs) of a UUID - because that is how UUIDs are constructed in Java. We need to - unpack this array and repack it with the right endianness to be - used as a python UUID. """ - @staticmethod - def read(file_object): - ints = struct.unpack("4i", file_object.read(4 * 4)) - packed = struct.pack("qq", player_uuid.bytes) - socket.send(struct.pack(">4i", msb >> 32, - twos_comp(msb & 0xffffffff, 32), - lsb >> 32, - twos_comp(lsb & 0xffffffff, 32) - ) - ) - - class TrailingByteArray(Type): """ A byte array consisting of all remaining data. If present in a packet definition, this should only be the type of the last field. """ From 84df884ca42a81963c49131e7b0c8cef2db2cf6e Mon Sep 17 00:00:00 2001 From: joo Date: Mon, 17 Aug 2020 05:39:48 +0200 Subject: [PATCH 18/24] Remove 'Packet.packet_id' attribute, replacing with 'id' --- minecraft/networking/connection.py | 8 ++-- minecraft/networking/packets/packet.py | 64 ++++++++++++++------------ start.py | 9 ++-- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index a26728d..d61db97 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -643,14 +643,16 @@ class PacketReactor(object): packet_id = VarInt.read(packet_data) # If we know the structure of the packet, attempt to parse it - # otherwise just skip it + # otherwise, just return an instance of the base Packet class. if packet_id in self.clientbound_packets: packet = self.clientbound_packets[packet_id]() packet.context = self.connection.context packet.read(packet_data) - return packet else: - return packets.Packet(context=self.connection.context, packet_id=packet_id) + packet = packets.Packet() + packet.context = self.connection.context + packet.id = packet_id + return packet else: return None diff --git a/minecraft/networking/packets/packet.py b/minecraft/networking/packets/packet.py index 78de42f..d21f824 100644 --- a/minecraft/networking/packets/packet.py +++ b/minecraft/networking/packets/packet.py @@ -1,23 +1,28 @@ -from .packet_buffer import PacketBuffer from zlib import compress + +from .packet_buffer import PacketBuffer from minecraft.networking.types import ( - VarInt, Enum + VarInt, Enum, overridable_property, ) class Packet(object): packet_name = "base" - id = None - definition = None # To define the packet ID, either: # 1. Define the attribute `id', of type int, in a subclass; or # 2. Override `get_id' in a subclass and return the correct packet ID # for the given ConnectionContext. This is necessary if the packet ID - # has changed across protocol versions, for example. + # has changed across protocol versions, for example; or + # 3. Define the attribute `id' in an instance of a class without either + # of the above. @classmethod - def get_id(cls, context): - return cls.id + def get_id(cls, _context): + return getattr(cls, 'id') + + @overridable_property + def id(self): + return None if self.context is None else self.get_id(self.context) # To define the network data layout of a packet, either: # 1. Define the attribute `definition', a list of fields, each of which @@ -29,37 +34,37 @@ class Packet(object): # 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 + def get_definition(cls, _context): + return getattr(cls, 'definition') + @overridable_property + def definition(self): + return None if self.context is None else \ + self.get_definition(self.context) + + # In general, a packet instance must have its 'context' attribute set to an + # instance of 'ConnectionContext', for example to decide on version- + # dependent behaviour. This can either be given as an argument to this + # constructor (e.g. 'p = P(context=c)') or set later + # (e.g. 'p.context = c'). + # + # While a packet has no 'context' set, all attributes should *writable* + # without errors, but some attributes may not be *readable*. + # + # When sending or receiving packets via 'Connection', it is generally not + # necessary to set the 'context', as this will be done automatically by + # 'Connection'. def __init__(self, context=None, **kwargs): self.context = context self.set_values(**kwargs) - @property - def context(self): - return self._context - - @context.setter - def context(self, _context): - self._context = _context - self._context_changed() - - def _context_changed(self): - if self._context is not None: - self.id = self.get_id(self._context) - self.definition = self.get_definition(self._context) - else: - self.id = None - self.definition = None - def set_values(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) return self def read(self, file_object): - for field in self.definition: + for field in self.definition: # pylint: disable=not-an-iterable for var_name, data_type in field.items(): value = data_type.read_with_context(file_object, self.context) setattr(self, var_name, value) @@ -101,7 +106,7 @@ class Packet(object): 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 field in self.definition: # pylint: disable=not-an-iterable for var_name, data_type in field.items(): data = getattr(self, var_name) data_type.send_with_context(data, packet_buffer, self.context) @@ -110,8 +115,6 @@ class Packet(object): str = type(self).__name__ if self.id is not None: str = '0x%02X %s' % (self.id, str) - elif hasattr(self, "packet_id"): - str = 'pkt: 0x%02X %s' % (self.packet_id, str) fields = self.fields if fields is not None: inner_str = ', '.join('%s=%s' % (a, self.field_string(a)) @@ -124,6 +127,7 @@ class Packet(object): """ An iterable of the names of the packet's fields, or None. """ if self.definition is None: return None + # pylint: disable=not-an-iterable return (field for defn in self.definition for field in defn) def field_string(self, field): diff --git a/start.py b/start.py index 83d653b..2b019d5 100755 --- a/start.py +++ b/start.py @@ -85,11 +85,12 @@ def main(): def print_incoming(packet): if type(packet) is Packet: # This is a direct instance of the base Packet type, meaning - # that it is a packet of unknown type, so we do not print it. + # that it is a packet of unknown type, so we do not print it + # unless explicitly requested by the user. if options.dump_unknown: - print('--> ??? %s' % packet, file=sys.stderr) - return - print('--> %s' % packet, file=sys.stderr) + print('--> [unknown packet] %s' % packet, file=sys.stderr) + else: + print('--> %s' % packet, file=sys.stderr) def print_outgoing(packet): print('<-- %s' % packet, file=sys.stderr) From 3723655fa3143f456a024732bd8e9b9d15d018e2 Mon Sep 17 00:00:00 2001 From: joo Date: Mon, 17 Aug 2020 05:40:40 +0200 Subject: [PATCH 19/24] Add .vscode to gitignore --- .gitignore | 3 +++ .vscode/settings.json | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 585e715..b35e479 100644 --- a/.gitignore +++ b/.gitignore @@ -81,5 +81,8 @@ target/ # sftp configuration file sftp-config.json +### Visual Studio +.vscode + ### pyCraft ### credentials diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index de7551d..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.formatting.provider": "none" -} \ No newline at end of file From b79f8b30eb195b3d0d915191db1811079564dab5 Mon Sep 17 00:00:00 2001 From: joo Date: Mon, 17 Aug 2020 06:41:06 +0200 Subject: [PATCH 20/24] Remove support for Python 2.7 --- .travis.yml | 2 -- README.rst | 2 -- minecraft/compat.py | 24 ------------------------ minecraft/networking/connection.py | 10 ++++------ minecraft/networking/types/basic.py | 1 - minecraft/networking/types/utility.py | 2 -- requirements.txt | 1 - start.py | 3 --- tests/compat.py | 7 ------- tests/fake_server.py | 6 ++---- tests/test_authentication.py | 4 ++-- tests/test_connection.py | 3 +-- tox.ini | 7 +------ 13 files changed, 10 insertions(+), 62 deletions(-) delete mode 100644 minecraft/compat.py delete mode 100644 tests/compat.py diff --git a/.travis.yml b/.travis.yml index 687d02c..e5f09fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ python: 3.8 matrix: include: - - python: 2.7 - env: TOX_ENV=py27 - python: pypy env: TOX_ENV=pypy - python: 3.5 diff --git a/README.rst b/README.rst index e47bb54..520b42c 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,6 @@ Supported Python versions ------------------------- pyCraft is compatible with (at least) the following Python implementations: -* Python 2.7 * Python 3.5 * Python 3.6 * Python 3.7 @@ -61,7 +60,6 @@ Requirements ------------ - `cryptography `_ - `requests `_ -- `future `_ The requirements are also stored in ``requirements.txt`` diff --git a/minecraft/compat.py b/minecraft/compat.py deleted file mode 100644 index b44c8f9..0000000 --- a/minecraft/compat.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -This module stores code used for making pyCraft compatible with -both Python2 and Python3 while using the same codebase. -""" - -# Raw input -> input shenangians -# example -# > from minecraft.compat import input -# > input("asd") - -# Hi, I'm pylint, and sometimes I act silly, at which point my programmer -# overlords need to correct me. - -# pylint: disable=undefined-variable,redefined-builtin,invalid-name -try: - input = raw_input -except NameError: - input = input - -try: - unicode = unicode -except NameError: - unicode = str -# pylint: enable=undefined-variable,redefined-builtin,invalid-name diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index d61db97..5c6d76a 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from collections import deque from threading import RLock import zlib @@ -11,8 +9,6 @@ import sys import json import re -from future.utils import raise_ - from .types import VarInt from .packets import clientbound, serverbound from . import packets @@ -491,7 +487,8 @@ class Connection(object): # If allowed by the final exception handler, re-raise the exception. if self.handle_exception is None and not caught: - raise_(*exc_info) + exc_value, exc_tb = exc_info[1:] + raise exc_value.with_traceback(exc_tb) def _version_mismatch(self, server_protocol=None, server_version=None): if server_protocol is None: @@ -592,7 +589,8 @@ class NetworkingThread(threading.Thread): exc_info = None if exc_info is not None: - raise_(*exc_info) + exc_value, exc_tb = exc_info[1:] + raise exc_value.with_traceback(exc_tb) class PacketReactor(object): diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index 4ccf627..e91db13 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -2,7 +2,6 @@ Each type has a method which is used to read and write it. These definitions and methods are used by the packet definitions """ -from __future__ import division import struct import uuid diff --git a/minecraft/networking/types/utility.py b/minecraft/networking/types/utility.py index 2916437..0103cc4 100644 --- a/minecraft/networking/types/utility.py +++ b/minecraft/networking/types/utility.py @@ -1,8 +1,6 @@ """Minecraft data types that are used by packets, but don't have a specific network representation. """ -from __future__ import division - from collections import namedtuple from itertools import chain diff --git a/requirements.txt b/requirements.txt index 0f1d0b1..27732d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ cryptography>=1.5 requests -future diff --git a/start.py b/start.py index 2b019d5..353a158 100755 --- a/start.py +++ b/start.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function - import getpass import sys import re @@ -11,7 +9,6 @@ from minecraft import authentication from minecraft.exceptions import YggdrasilError from minecraft.networking.connection import Connection from minecraft.networking.packets import Packet, clientbound, serverbound -from minecraft.compat import input def get_options(): diff --git a/tests/compat.py b/tests/compat.py deleted file mode 100644 index ffaa534..0000000 --- a/tests/compat.py +++ /dev/null @@ -1,7 +0,0 @@ -import platform -from distutils.version import StrictVersion - -if StrictVersion(platform.python_version()) < StrictVersion("3.3.0"): - import mock # noqa -else: - from unittest import mock # noqa diff --git a/tests/fake_server.py b/tests/fake_server.py index ead9e9e..788fdd0 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from minecraft import SUPPORTED_MINECRAFT_VERSIONS from minecraft.networking import connection from minecraft.networking import types @@ -11,7 +9,6 @@ from minecraft.networking.encryption import ( ) from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 -from future.utils import raise_ from numbers import Integral import unittest @@ -576,7 +573,8 @@ class _FakeServerTest(unittest.TestCase): logging.error(**error) self.fail('Multiple errors: see logging output.') elif errors and 'exc_info' in errors[0]: - raise_(*errors[0]['exc_info']) + exc_value, exc_tb = errors[0]['exc_info'][1:] + raise exc_value.with_traceback(exc_tb) elif errors: self.fail(errors[0]['msg']) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index c028aa6..460f779 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -4,11 +4,11 @@ from minecraft.authentication import _make_request from minecraft.authentication import _raise_from_response from minecraft.exceptions import YggdrasilError +from unittest import mock +import unittest import requests import json -import unittest import os -from .compat import mock FAKE_DATA = { "id_": "85e2c12b9eab4a7dabf61babc11354c2", diff --git a/tests/test_connection.py b/tests/test_connection.py index 59b5bea..1f52f29 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -5,7 +5,6 @@ from minecraft.networking.connection import Connection from minecraft.exceptions import ( VersionMismatch, LoginDisconnect, InvalidState, IgnorePacket ) -from minecraft.compat import unicode from . import fake_server @@ -92,7 +91,7 @@ class DefaultStatusTest(ConnectTest): def setUp(self): class FakeStdOut(io.BytesIO): def write(self, data): - if isinstance(data, unicode): + if isinstance(data, str): data = data.encode('utf8') super(FakeStdOut, self).write(data) sys.stdout, self.old_stdout = FakeStdOut(), sys.stdout diff --git a/tox.ini b/tox.ini index 5b67f02..a465d4b 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37, py38, pypy, flake8, pylint-errors, pylint-full, verify-manifest +envlist = py35, py36, py37, py38, pypy, flake8, pylint-errors, pylint-full, verify-manifest [testenv] commands = nosetests --with-timer @@ -27,11 +27,6 @@ commands = deps = coveralls -[testenv:py27] -deps = - {[testenv]deps} - mock - [testenv:py38] setenv = PYCRAFT_RUN_INTERNET_TESTS=1 From 4c355171578db16dd849a88fe830023511507cc6 Mon Sep 17 00:00:00 2001 From: joo Date: Mon, 17 Aug 2020 06:13:24 +0200 Subject: [PATCH 21/24] Fix support for Minecraft 20w06a to 1.16.2 (protocols 701 to 751) --- README.rst | 1 + minecraft/__init__.py | 33 ++- .../packets/clientbound/play/__init__.py | 144 ++++-------- .../clientbound/play/block_change_packet.py | 95 +++++--- .../clientbound/play/combat_event_packet.py | 4 +- .../clientbound/play/explosion_packet.py | 63 ++---- .../clientbound/play/face_player_packet.py | 4 +- .../play/join_game_and_respawn_packets.py | 208 ++++++++++++++++++ .../packets/clientbound/play/map_packet.py | 4 +- .../play/player_list_item_packet.py | 4 +- .../play/player_position_and_look_packet.py | 4 +- .../clientbound/play/sound_effect_packet.py | 2 +- .../packets/serverbound/play/__init__.py | 11 +- minecraft/networking/types/basic.py | 84 +++++-- minecraft/networking/types/enum.py | 11 +- minecraft/networking/types/utility.py | 85 +++++-- requirements.txt | 1 + tests/fake_server.py | 49 ++++- tests/test_backward_compatible.py | 53 +++++ tests/test_packets.py | 50 +++-- 20 files changed, 654 insertions(+), 256 deletions(-) create mode 100644 minecraft/networking/packets/clientbound/play/join_game_and_respawn_packets.py diff --git a/README.rst b/README.rst index 520b42c..cb86002 100644 --- a/README.rst +++ b/README.rst @@ -60,6 +60,7 @@ Requirements ------------ - `cryptography `_ - `requests `_ +- `pynbt `_ The requirements are also stored in ``requirements.txt`` diff --git a/minecraft/__init__.py b/minecraft/__init__.py index db169c0..39cafd1 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -242,13 +242,28 @@ SUPPORTED_MINECRAFT_VERSIONS = { '20w19a': 715, '20w20a': 716, '20w20b': 717, - '1.16 Pre-release 2': 722, - '1.16 Pre-release 3': 725, - '1.16 Pre-release 4': 727, - '1.16 Pre-release 5': 729, - '1.16 Release Candidate 1':734, + '20w21a': 718, + '20w22a': 719, + '1.16-pre1': 721, + '1.16-pre2': 722, + '1.16-pre3': 725, + '1.16-pre4': 727, + '1.16-pre5': 729, + '1.16-pre6': 730, + '1.16-pre7': 732, + '1.16-pre8': 733, + '1.16-rc1': 734, '1.16': 735, '1.16.1': 736, + '20w27a': 738, + '20w28a': 740, + '20w29a': 741, + '20w30a': 743, + '1.16.2-pre1': 744, + '1.16.2-pre2': 746, + '1.16.2-pre3': 748, + '1.16.2-rc1': 749, + '1.16.2-rc2': 750, '1.16.2': 751, } @@ -270,9 +285,11 @@ def initglobals(): This allows the SUPPORTED_MINECRAFT_VERSIONS dict to be updated, then the other globals can be updated as well, to allow for dynamic version support. - All updates are done by reference to allow this to work else where in the code. + All updates are done by reference to allow this to work else where in the + code. ''' - global RELEASE_MINECRAFT_VERSIONS, SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS + global RELEASE_MINECRAFT_VERSIONS, SUPPORTED_PROTOCOL_VERSIONS + global RELEASE_PROTOCOL_VERSIONS import re @@ -288,4 +305,4 @@ def initglobals(): RELEASE_PROTOCOL_VERSIONS.sort() -initglobals() \ No newline at end of file +initglobals() diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 2f81d9f..6dfe471 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -3,10 +3,9 @@ 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, Long, Vector, Direction, PositionAndLook, - multi_attribute_alias, + FixedPointInteger, Angle, UnsignedByte, Byte, Boolean, UUID, Short, VarInt, + Double, Float, String, Enum, Difficulty, Long, Vector, Direction, + PositionAndLook, multi_attribute_alias, ) from .combat_event_packet import CombatEventPacket @@ -18,6 +17,7 @@ from .block_change_packet import BlockChangePacket, MultiBlockChangePacket from .explosion_packet import ExplosionPacket from .sound_effect_packet import SoundEffectPacket from .face_player_packet import FacePlayerPacket +from .join_game_and_respawn_packets import JoinGamePacket, RespawnPacket # Formerly known as state_playing_clientbound. @@ -64,8 +64,8 @@ def get_packets(context): class KeepAlivePacket(AbstractKeepAlivePacket): @staticmethod def get_id(context): - return 0x1F if context.protocol_version >= 751 else \ - 0x20 if context.protocol_version >= 722 else \ + return 0x1F if context.protocol_version >= 741 else \ + 0x20 if context.protocol_version >= 721 else \ 0x21 if context.protocol_version >= 550 else \ 0x20 if context.protocol_version >= 471 else \ 0x21 if context.protocol_version >= 389 else \ @@ -76,47 +76,10 @@ class KeepAlivePacket(AbstractKeepAlivePacket): 0x00 -class JoinGamePacket(Packet): - @staticmethod - def get_id(context): - return 0x24 if context.protocol_version >= 751 else \ - 0x25 if context.protocol_version >= 722 else \ - 0x26 if context.protocol_version >= 550 else \ - 0x25 if context.protocol_version >= 389 else \ - 0x24 if context.protocol_version >= 345 else \ - 0x23 if context.protocol_version >= 332 else \ - 0x24 if context.protocol_version >= 318 else \ - 0x23 if context.protocol_version >= 107 else \ - 0x01 - - packet_name = "join game" - get_definition = staticmethod(lambda context: [ - {'entity_id': Integer}, - {'is_hardcore': Boolean} if context.protocol_version >= 751 else {}, - {'game_mode': UnsignedByte}, - {'previous_game_mode': UnsignedByte} if context.protocol_version >= 722 else {}, - {'dimension': Integer if context.protocol_version >= 108 else Byte}, - {'hashed_seed': Long} if context.protocol_version >= 552 else {}, - {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, - {'max_players': UnsignedByte}, - {'level_type': String} if context.protocol_version < 716 else {}, - {'render_distance': VarInt} if context.protocol_version >= 468 else {}, - {'reduced_debug_info': Boolean}, - {'respawn_screen': Boolean} if context.protocol_version >= 571 else {}, - {'is_debug': Boolean} if context.protocol_version >= 716 else {}, - {'is_flat': Boolean} if context.protocol_version >= 716 else {}, - ]) - - # These aliases declare the Enum type corresponding to each field: - Difficulty = Difficulty - GameMode = GameMode - Dimension = Dimension - - class ServerDifficultyPacket(Packet): @staticmethod def get_id(context): - return 0x0D if context.protocol_version >= 722 else \ + return 0x0D if context.protocol_version >= 721 else \ 0x0E if context.protocol_version >= 550 else \ 0x0D if context.protocol_version >= 332 else \ 0x0E if context.protocol_version >= 318 else \ @@ -136,7 +99,7 @@ class ServerDifficultyPacket(Packet): class ChatMessagePacket(Packet): @staticmethod def get_id(context): - return 0x0E if context.protocol_version >= 722 else \ + return 0x0E if context.protocol_version >= 721 else \ 0x0F if context.protocol_version >= 550 else \ 0x0E if context.protocol_version >= 343 else \ 0x0F if context.protocol_version >= 332 else \ @@ -145,9 +108,11 @@ class ChatMessagePacket(Packet): 0x02 packet_name = "chat message" - definition = [ + get_definition = staticmethod(lambda context: [ {'json_data': String}, - {'position': Byte}] + {'position': Byte}, + {'sender': UUID} if context.protocol_version >= 718 else {}, + ]) class Position(Enum): CHAT = 0 # A player-initiated chat message. @@ -158,8 +123,8 @@ class ChatMessagePacket(Packet): class DisconnectPacket(Packet): @staticmethod def get_id(context): - return 0x19 if context.protocol_version >= 751 else \ - 0x1A if context.protocol_version >= 722 else \ + return 0x19 if context.protocol_version >= 741 else \ + 0x1A if context.protocol_version >= 721 else \ 0x1B if context.protocol_version >= 550 else \ 0x1A if context.protocol_version >= 471 else \ 0x1B if context.protocol_version >= 345 else \ @@ -185,7 +150,7 @@ class SetCompressionPacket(Packet): class SpawnPlayerPacket(Packet): @staticmethod def get_id(context): - return 0x04 if context.protocol_version >= 722 else \ + return 0x04 if context.protocol_version >= 721 else \ 0x05 if context.protocol_version >= 67 else \ 0x0C @@ -221,7 +186,7 @@ class SpawnPlayerPacket(Packet): class EntityVelocityPacket(Packet): @staticmethod def get_id(context): - return 0x46 if context.protocol_version >= 722 else \ + return 0x46 if context.protocol_version >= 721 else \ 0x47 if context.protocol_version >= 707 else \ 0x46 if context.protocol_version >= 550 else \ 0x45 if context.protocol_version >= 471 else \ @@ -249,10 +214,15 @@ class EntityVelocityPacket(Packet): class EntityPositionDeltaPacket(Packet): @staticmethod def get_id(context): - return 0x27 if context.protocol_version >= 751 else \ - 0x28 if context.protocol_version >= 722 else \ - 0x29 if context.protocol_version >= 578 else \ - 0xFF + return 0x27 if context.protocol_version >= 741 else \ + 0x28 if context.protocol_version >= 721 else \ + 0x29 if context.protocol_version >= 550 else \ + 0x28 if context.protocol_version >= 389 else \ + 0x27 if context.protocol_version >= 345 else \ + 0x26 if context.protocol_version >= 318 else \ + 0x25 if context.protocol_version >= 94 else \ + 0x26 if context.protocol_version >= 70 else \ + 0x15 packet_name = "entity position delta" get_definition = staticmethod(lambda context: [ @@ -267,9 +237,19 @@ class EntityPositionDeltaPacket(Packet): class TimeUpdatePacket(Packet): @staticmethod def get_id(context): - return 0x4E if context.protocol_version >= 722 else \ - 0x4F if context.protocol_version >= 578 else \ - 0xFF + return 0x4E if context.protocol_version >= 721 else \ + 0x4F if context.protocol_version >= 550 else \ + 0x4E if context.protocol_version >= 471 else \ + 0x4A if context.protocol_version >= 461 else \ + 0x4B if context.protocol_version >= 451 else \ + 0x4A if context.protocol_version >= 389 else \ + 0x49 if context.protocol_version >= 352 else \ + 0x48 if context.protocol_version >= 345 else \ + 0x47 if context.protocol_version >= 336 else \ + 0x46 if context.protocol_version >= 318 else \ + 0x44 if context.protocol_version >= 94 else \ + 0x43 if context.protocol_version >= 70 else \ + 0x03 packet_name = "time update" get_definition = staticmethod(lambda context: [ @@ -281,7 +261,7 @@ class TimeUpdatePacket(Packet): class UpdateHealthPacket(Packet): @staticmethod def get_id(context): - return 0x49 if context.protocol_version >= 722 else \ + return 0x49 if context.protocol_version >= 721 else \ 0x4A if context.protocol_version >= 707 else \ 0x49 if context.protocol_version >= 550 else \ 0x48 if context.protocol_version >= 471 else \ @@ -305,47 +285,11 @@ class UpdateHealthPacket(Packet): ]) -class RespawnPacket(Packet): - @staticmethod - def get_id(context): - return 0x39 if context.protocol_version >= 751 else \ - 0x3A if context.protocol_version >= 722 else \ - 0x3B if context.protocol_version >= 550 else \ - 0x3A if context.protocol_version >= 471 else \ - 0x38 if context.protocol_version >= 461 else \ - 0x39 if context.protocol_version >= 451 else \ - 0x38 if context.protocol_version >= 389 else \ - 0x37 if context.protocol_version >= 352 else \ - 0x36 if context.protocol_version >= 345 else \ - 0x35 if context.protocol_version >= 336 else \ - 0x34 if context.protocol_version >= 332 else \ - 0x35 if context.protocol_version >= 318 else \ - 0x33 if context.protocol_version >= 70 else \ - 0x07 - - packet_name = 'respawn' - get_definition = staticmethod(lambda context: [ - {'dimension': Integer}, - {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, - {'hashed_seed': Long} if context.protocol_version >= 552 else {}, - {'game_mode': UnsignedByte}, - {'level_type': String} if context.protocol_version < 716 else {}, - {'is_debug': Boolean} if context.protocol_version >= 716 else {}, - {'is_flat': Boolean} if context.protocol_version >= 716 else {}, - {'copy_metadata': Boolean}, - ]) - - # These aliases declare the Enum type corresponding to each field: - Difficulty = Difficulty - Dimension = Dimension - GameMode = GameMode - - class PluginMessagePacket(AbstractPluginMessagePacket): @staticmethod def get_id(context): - return 0x17 if context.protocol_version >= 751 else \ - 0x18 if context.protocol_version >= 722 else \ + return 0x17 if context.protocol_version >= 741 else \ + 0x18 if context.protocol_version >= 721 else \ 0x19 if context.protocol_version >= 550 else \ 0x18 if context.protocol_version >= 471 else \ 0x19 if context.protocol_version >= 345 else \ @@ -358,7 +302,7 @@ class PluginMessagePacket(AbstractPluginMessagePacket): class PlayerListHeaderAndFooterPacket(Packet): @staticmethod def get_id(context): - return 0x53 if context.protocol_version >= 722 else \ + return 0x53 if context.protocol_version >= 721 else \ 0x54 if context.protocol_version >= 550 else \ 0x53 if context.protocol_version >= 471 else \ 0x5F if context.protocol_version >= 461 else \ @@ -380,8 +324,8 @@ class PlayerListHeaderAndFooterPacket(Packet): class EntityLookPacket(Packet): @staticmethod def get_id(context): - return 0x29 if context.protocol_version >= 751 else \ - 0x2A if context.protocol_version >= 722 else \ + return 0x29 if context.protocol_version >= 741 else \ + 0x2A if context.protocol_version >= 721 else \ 0x2B if context.protocol_version >= 550 else \ 0x2A if context.protocol_version >= 389 else \ 0x29 if context.protocol_version >= 345 else \ diff --git a/minecraft/networking/packets/clientbound/play/block_change_packet.py b/minecraft/networking/packets/clientbound/play/block_change_packet.py index 396c59c..31333d0 100644 --- a/minecraft/networking/packets/clientbound/play/block_change_packet.py +++ b/minecraft/networking/packets/clientbound/play/block_change_packet.py @@ -1,14 +1,15 @@ from minecraft.networking.packets import Packet from minecraft.networking.types import ( - VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord, - attribute_alias, multi_attribute_alias, + Type, VarInt, VarLong, Long, Integer, UnsignedByte, Position, Vector, + MutableRecord, PrefixedArray, Boolean, attribute_alias, + multi_attribute_alias, ) class BlockChangePacket(Packet): @staticmethod def get_id(context): - return 0x0B if context.protocol_version >= 722 else \ + return 0x0B if context.protocol_version >= 721 else \ 0x0C if context.protocol_version >= 550 else \ 0x0B if context.protocol_version >= 332 else \ 0x0C if context.protocol_version >= 318 else \ @@ -47,8 +48,8 @@ class BlockChangePacket(Packet): class MultiBlockChangePacket(Packet): @staticmethod def get_id(context): - return 0x3B if context.protocol_version >= 751 else \ - 0x0F if context.protocol_version >= 722 else \ + return 0x3B if context.protocol_version >= 741 else \ + 0x0F if context.protocol_version >= 721 else \ 0x10 if context.protocol_version >= 550 else \ 0x0F if context.protocol_version >= 343 else \ 0x10 if context.protocol_version >= 332 else \ @@ -58,12 +59,23 @@ class MultiBlockChangePacket(Packet): packet_name = 'multi block change' - fields = 'chunk_x', 'chunk_z', 'records' + # Only used in protocol 741 and later. + class ChunkSectionPos(Vector, Type): + @classmethod + def read(cls, file_object): + value = Long.read(file_object) + x = value >> 42 + z = (value >> 20) & 0x3FFFFF + y = value & 0xFFFFF + return cls(x, y, z) - # Access the 'chunk_x' and 'chunk_z' fields as a tuple. - chunk_pos = multi_attribute_alias(tuple, 'chunk_x', 'chunk_z') + @classmethod + def send(cls, pos, socket): + x, y, z = pos + value = (x & 0x3FFFFF) << 42 | (z & 0x3FFFFF) << 20 | y & 0xFFFFF + Long.send(value, socket) - class Record(MutableRecord): + class Record(MutableRecord, Type): __slots__ = 'x', 'y', 'z', 'block_state_id' def __init__(self, **kwds): @@ -94,30 +106,47 @@ class MultiBlockChangePacket(Packet): # This alias is retained for backward compatibility. blockStateId = attribute_alias('block_state_id') - 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) + @classmethod + def read_with_context(cls, file_object, context): + record = cls() + if context.protocol_version >= 741: + value = VarLong.read(file_object) + record.block_state_id = value >> 12 + record.x = (value >> 8) & 0xF + record.z = (value >> 4) & 0xF + record.y = value & 0xF + else: + h_position = UnsignedByte.read(file_object) + record.x = h_position >> 4 + record.z = h_position & 0xF + record.y = UnsignedByte.read(file_object) + record.block_state_id = VarInt.read(file_object) + return record - 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) + @classmethod + def send_with_context(self, record, socket, context): + if context.protocol_version >= 741: + value = record.block_state_id << 12 | \ + (record.x & 0xF) << 8 | \ + (record.z & 0xF) << 4 | \ + record.y & 0xF + VarLong.send(value, socket) + else: + UnsignedByte.send(record.x << 4 | record.z & 0xF, socket) + UnsignedByte.send(record.y, socket) + VarInt.send(record.block_state_id, socket) - 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) - self.records = [] - for i in range(records_count): - record = self.Record() - record.read(file_object) - self.records.append(record) + get_definition = staticmethod(lambda context: [ + {'chunk_section_pos': MultiBlockChangePacket.ChunkSectionPos}, + {'invert_trust_edges': Boolean} + if context.protocol_version >= 748 else {}, # Provisional field name. + {'records': PrefixedArray(VarInt, MultiBlockChangePacket.Record)}, + ] if context.protocol_version >= 741 else [ + {'chunk_x': Integer}, + {'chunk_z': Integer}, + {'records': PrefixedArray(VarInt, MultiBlockChangePacket.Record)}, + ]) - 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) + # Access the 'chunk_x' and 'chunk_z' fields as a tuple. + # Only used prior to protocol 741. + chunk_pos = multi_attribute_alias(tuple, 'chunk_x', 'chunk_z') diff --git a/minecraft/networking/packets/clientbound/play/combat_event_packet.py b/minecraft/networking/packets/clientbound/play/combat_event_packet.py index 8d7e80c..db53734 100644 --- a/minecraft/networking/packets/clientbound/play/combat_event_packet.py +++ b/minecraft/networking/packets/clientbound/play/combat_event_packet.py @@ -8,8 +8,8 @@ from minecraft.networking.types import ( class CombatEventPacket(Packet): @staticmethod def get_id(context): - return 0x31 if context.protocol_version >= 751 else \ - 0x32 if context.protocol_version >= 722 else \ + return 0x31 if context.protocol_version >= 741 else \ + 0x32 if context.protocol_version >= 721 else \ 0x33 if context.protocol_version >= 550 else \ 0x32 if context.protocol_version >= 471 else \ 0x30 if context.protocol_version >= 451 else \ diff --git a/minecraft/networking/packets/clientbound/play/explosion_packet.py b/minecraft/networking/packets/clientbound/play/explosion_packet.py index c47a48d..71a4af2 100644 --- a/minecraft/networking/packets/clientbound/play/explosion_packet.py +++ b/minecraft/networking/packets/clientbound/play/explosion_packet.py @@ -1,5 +1,5 @@ from minecraft.networking.types import ( - Vector, Float, Byte, Integer, multi_attribute_alias, + Vector, Float, Byte, Integer, PrefixedArray, multi_attribute_alias, Type, ) from minecraft.networking.packets import Packet @@ -7,8 +7,8 @@ from minecraft.networking.packets import Packet class ExplosionPacket(Packet): @staticmethod def get_id(context): - return 0x1B if context.protocol_version >= 751 else \ - 0x1C if context.protocol_version >= 722 else \ + return 0x1B if context.protocol_version >= 741 else \ + 0x1C if context.protocol_version >= 721 else \ 0x1D if context.protocol_version >= 550 else \ 0x1C if context.protocol_version >= 471 else \ 0x1E if context.protocol_version >= 389 else \ @@ -21,8 +21,27 @@ class ExplosionPacket(Packet): packet_name = 'explosion' - fields = 'x', 'y', 'z', 'radius', 'records', \ - 'player_motion_x', 'player_motion_y', 'player_motion_z' + class Record(Vector, Type): + __slots__ = () + + @classmethod + def read(cls, file_object): + return cls(*(Byte.read(file_object) for i in range(3))) + + @classmethod + def send(cls, record, socket): + 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}] # Access the 'x', 'y', 'z' fields as a Vector tuple. position = multi_attribute_alias(Vector, 'x', 'y', 'z') @@ -30,37 +49,3 @@ class ExplosionPacket(Packet): # 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) - 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/face_player_packet.py b/minecraft/networking/packets/clientbound/play/face_player_packet.py index 6db4a07..4aca89d 100644 --- a/minecraft/networking/packets/clientbound/play/face_player_packet.py +++ b/minecraft/networking/packets/clientbound/play/face_player_packet.py @@ -8,8 +8,8 @@ from minecraft.networking.packets import Packet class FacePlayerPacket(Packet): @staticmethod def get_id(context): - return 0x33 if context.protocol_version >= 751 else \ - 0x34 if context.protocol_version >= 722 else \ + return 0x33 if context.protocol_version >= 741 else \ + 0x34 if context.protocol_version >= 721 else \ 0x35 if context.protocol_version >= 550 else \ 0x34 if context.protocol_version >= 471 else \ 0x32 if context.protocol_version >= 451 else \ 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 new file mode 100644 index 0000000..437abba --- /dev/null +++ b/minecraft/networking/packets/clientbound/play/join_game_and_respawn_packets.py @@ -0,0 +1,208 @@ +import pynbt + +from minecraft.networking.packets import Packet +from minecraft.networking.types import ( + NBT, Integer, Boolean, UnsignedByte, String, Byte, Long, VarInt, + PrefixedArray, Difficulty, GameMode, Dimension, +) + + +def nbt_to_snbt(tag): + '''Convert a pyNBT tag to SNBT ("stringified NBT") format.''' + scalars = { + pynbt.TAG_Byte: 'b', + pynbt.TAG_Short: 's', + pynbt.TAG_Int: '', + pynbt.TAG_Long: 'l', + pynbt.TAG_Float: 'f', + pynbt.TAG_Double: 'd', + } + if type(tag) in scalars: + return repr(tag.value) + scalars[type(tag)] + + arrays = { + pynbt.TAG_Byte_Array: 'B', + pynbt.TAG_Int_Array: 'I', + pynbt.TAG_Long_Array: 'L', + } + if type(tag) in arrays: + return '[' + arrays[type(tag)] + ';' + \ + ','.join(map(repr, tag.value)) + ']' + + if isinstance(tag, pynbt.TAG_String): + return repr(tag.value) + + if isinstance(tag, pynbt.TAG_List): + return '[' + ','.join(map(nbt_to_snbt, tag.value)) + ']' + + if isinstance(tag, pynbt.TAG_Compound): + return '{' + ','.join(n + ':' + nbt_to_snbt(v) + for (n, v) in tag.items()) + '}' + + raise TypeError('Unknown NBT tag type: %r' % type(tag)) + + +class AbstractDimensionPacket(Packet): + ''' The abstract superclass of JoinGamePacket and RespawnPacket, containing + common definitions relating to their 'dimension' field. + ''' + def field_string(self, field): + # pylint: disable=no-member + if self.context.protocol_version >= 748 and field == 'dimension': + return nbt_to_snbt(self.dimension) + elif self.context.protocol_version < 718 and field == 'dimension': + return Dimension.name_from_value(self.dimension) + return super(AbstractDimensionPacket, self).field_string(field) + + +class JoinGamePacket(AbstractDimensionPacket): + @staticmethod + def get_id(context): + return 0x24 if context.protocol_version >= 741 else \ + 0x25 if context.protocol_version >= 721 else \ + 0x26 if context.protocol_version >= 550 else \ + 0x25 if context.protocol_version >= 389 else \ + 0x24 if context.protocol_version >= 345 else \ + 0x23 if context.protocol_version >= 332 else \ + 0x24 if context.protocol_version >= 318 else \ + 0x23 if context.protocol_version >= 107 else \ + 0x01 + + packet_name = "join game" + get_definition = staticmethod(lambda context: [ + {'entity_id': Integer}, + {'is_hardcore': Boolean} if context.protocol_version >= 738 else {}, + {'game_mode': UnsignedByte}, + {'previous_game_mode': UnsignedByte} + if context.protocol_version >= 730 else {}, + {'world_names': PrefixedArray(VarInt, String)} + if context.protocol_version >= 722 else {}, + {'dimension_codec': NBT} + if context.protocol_version >= 718 else {}, + {'dimension': + NBT if context.protocol_version >= 748 else + String if context.protocol_version >= 718 else + Integer if context.protocol_version >= 108 else + Byte}, + {'world_name': String} if context.protocol_version >= 722 else {}, + {'hashed_seed': Long} if context.protocol_version >= 552 else {}, + {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, + {'max_players': + VarInt if context.protocol_version >= 749 else UnsignedByte}, + {'level_type': String} if context.protocol_version < 716 else {}, + {'render_distance': VarInt} if context.protocol_version >= 468 else {}, + {'reduced_debug_info': Boolean}, + {'respawn_screen': Boolean} if context.protocol_version >= 571 else {}, + {'is_debug': Boolean} if context.protocol_version >= 716 else {}, + {'is_flat': Boolean} if context.protocol_version >= 716 else {}, + ]) + + # These aliases declare the Enum type corresponding to each field: + Difficulty = Difficulty + GameMode = GameMode + + # Accesses the 'game_mode' field appropriately depending on the protocol. + # Can be set or deleted when 'context' is undefined. + @property + def game_mode(self): + if self.context.protocol_version >= 738: + return self._game_mode_738 + else: + return self._game_mode_0 + + @game_mode.setter + def game_mode(self, value): + self._game_mode_738 = value + self._game_mode_0 = value + + @game_mode.deleter + def game_mode(self): + del self._game_mode_738 + del self._game_mode_0 + + # Accesses the 'is_hardcore' field, or its equivalent in older protocols. + # Can be set or deleted when 'context' is undefined. + @property + def is_hardcore(self): + if self.context.protocol_version >= 738: + return self._is_hardcore + else: + return bool(self._game_mode_0 & GameMode.HARDCORE) + + @is_hardcore.setter + def is_hardcore(self, value): + self._is_hardcore = value + self._game_mode_0 = \ + getattr(self, '_game_mode_0', 0) | GameMode.HARDCORE \ + if value else \ + getattr(self, '_game_mode_0', 0) & ~GameMode.HARDCORE + + @is_hardcore.deleter + def is_hardcore(self): + if hasattr(self, '_is_hardcore'): + del self._is_hardcore + if hasattr(self, '_game_mode_0'): + self._game_mode_0 &= ~GameMode.HARDCORE + + # Accesses the component of the 'game_mode' field without any hardcore bit, + # version-independently. Can be set or deleted when 'context' is undefined. + @property + def pure_game_mode(self): + if self.context.protocol_version >= 738: + return self._game_mode_738 + else: + return self._game_mode_0 & ~GameMode.HARDCORE + + @pure_game_mode.setter + def pure_game_mode(self, value): + self._game_mode_738 = value + self._game_mode_0 = \ + value & ~GameMode.HARDCORE | \ + getattr(self, '_game_mode_0', 0) & GameMode.HARDCORE + + def field_string(self, field): + if field == 'dimension_codec': + # pylint: disable=no-member + return nbt_to_snbt(self.dimension_codec) + return super(JoinGamePacket, self).field_string(field) + + +class RespawnPacket(AbstractDimensionPacket): + @staticmethod + def get_id(context): + return 0x39 if context.protocol_version >= 741 else \ + 0x3A if context.protocol_version >= 721 else \ + 0x3B if context.protocol_version >= 550 else \ + 0x3A if context.protocol_version >= 471 else \ + 0x38 if context.protocol_version >= 461 else \ + 0x39 if context.protocol_version >= 451 else \ + 0x38 if context.protocol_version >= 389 else \ + 0x37 if context.protocol_version >= 352 else \ + 0x36 if context.protocol_version >= 345 else \ + 0x35 if context.protocol_version >= 336 else \ + 0x34 if context.protocol_version >= 332 else \ + 0x35 if context.protocol_version >= 318 else \ + 0x33 if context.protocol_version >= 70 else \ + 0x07 + + packet_name = 'respawn' + get_definition = staticmethod(lambda context: [ + {'dimension': + NBT if context.protocol_version >= 748 else + String if context.protocol_version >= 718 else + Integer}, + {'world_name': String} if context.protocol_version >= 719 else {}, + {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, + {'hashed_seed': Long} if context.protocol_version >= 552 else {}, + {'game_mode': UnsignedByte}, + {'previous_game_mode': UnsignedByte} + if context.protocol_version >= 730 else {}, + {'level_type': String} if context.protocol_version < 716 else {}, + {'is_debug': Boolean} if context.protocol_version >= 716 else {}, + {'is_flat': Boolean} if context.protocol_version >= 716 else {}, + {'copy_metadata': Boolean} if context.protocol_version >= 714 else {}, + ]) + + # These aliases declare the Enum type corresponding to each field: + Difficulty = Difficulty + GameMode = GameMode diff --git a/minecraft/networking/packets/clientbound/play/map_packet.py b/minecraft/networking/packets/clientbound/play/map_packet.py index ae45f5e..3de187b 100644 --- a/minecraft/networking/packets/clientbound/play/map_packet.py +++ b/minecraft/networking/packets/clientbound/play/map_packet.py @@ -8,8 +8,8 @@ from minecraft.networking.types import ( class MapPacket(Packet): @staticmethod def get_id(context): - return 0x25 if context.protocol_version >= 751 else \ - 0x26 if context.protocol_version >= 722 else \ + return 0x25 if context.protocol_version >= 741 else \ + 0x26 if context.protocol_version >= 721 else \ 0x27 if context.protocol_version >= 550 else \ 0x26 if context.protocol_version >= 389 else \ 0x25 if context.protocol_version >= 345 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 f9ebd8e..6f85fd0 100644 --- a/minecraft/networking/packets/clientbound/play/player_list_item_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_list_item_packet.py @@ -9,8 +9,8 @@ from minecraft.networking.types import ( class PlayerListItemPacket(Packet): @staticmethod def get_id(context): - return 0x32 if context.protocol_version >= 751 else \ - 0x33 if context.protocol_version >= 722 else \ + return 0x32 if context.protocol_version >= 741 else \ + 0x33 if context.protocol_version >= 721 else \ 0x34 if context.protocol_version >= 550 else \ 0x33 if context.protocol_version >= 471 else \ 0x31 if context.protocol_version >= 451 else \ diff --git a/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py b/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py index 9df7be7..5169366 100644 --- a/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_position_and_look_packet.py @@ -9,8 +9,8 @@ from minecraft.networking.types import ( class PlayerPositionAndLookPacket(Packet, BitFieldEnum): @staticmethod def get_id(context): - return 0x34 if context.protocol_version >= 751 else \ - 0x35 if context.protocol_version >= 722 else \ + return 0x34 if context.protocol_version >= 741 else \ + 0x35 if context.protocol_version >= 721 else \ 0x36 if context.protocol_version >= 550 else \ 0x35 if context.protocol_version >= 471 else \ 0x33 if context.protocol_version >= 451 else \ diff --git a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py index 96d6f60..da56748 100644 --- a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py +++ b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py @@ -9,7 +9,7 @@ __all__ = 'SoundEffectPacket', class SoundEffectPacket(Packet): @staticmethod def get_id(context): - return 0x51 if context.protocol_version >= 722 else \ + return 0x51 if context.protocol_version >= 721 else \ 0x52 if context.protocol_version >= 550 else \ 0x51 if context.protocol_version >= 471 else \ 0x4D if context.protocol_version >= 461 else \ diff --git a/minecraft/networking/packets/serverbound/play/__init__.py b/minecraft/networking/packets/serverbound/play/__init__.py index 85267f4..477cdcd 100644 --- a/minecraft/networking/packets/serverbound/play/__init__.py +++ b/minecraft/networking/packets/serverbound/play/__init__.py @@ -5,7 +5,7 @@ from minecraft.networking.packets import ( from minecraft.networking.types import ( Double, Float, Boolean, VarInt, String, Byte, Position, Enum, RelativeHand, BlockFace, Vector, Direction, PositionAndLook, - multi_attribute_alias, Short + multi_attribute_alias, ) from .client_settings_packet import ClientSettingsPacket @@ -126,7 +126,8 @@ class TeleportConfirmPacket(Packet): class AnimationPacket(Packet): @staticmethod def get_id(context): - return 0x2B if context.protocol_version >= 719 else \ + return 0x2C if context.protocol_version >= 738 else \ + 0x2B if context.protocol_version >= 712 else \ 0x2A if context.protocol_version >= 468 else \ 0x29 if context.protocol_version >= 464 else \ 0x27 if context.protocol_version >= 389 else \ @@ -200,7 +201,8 @@ class PlayerBlockPlacementPacket(Packet): @staticmethod def get_id(context): - return 0x2D if context.protocol_version >= 712 else \ + return 0x2E if context.protocol_version >= 738 else \ + 0x2D if context.protocol_version >= 712 else \ 0x2C if context.protocol_version >= 468 else \ 0x2B if context.protocol_version >= 464 else \ 0x29 if context.protocol_version >= 389 else \ @@ -238,7 +240,8 @@ class PlayerBlockPlacementPacket(Packet): class UseItemPacket(Packet): @staticmethod def get_id(context): - return 0x2E if context.protocol_version >= 719 else \ + return 0x2F if context.protocol_version >= 738 else \ + 0x2E if context.protocol_version >= 712 else \ 0x2D if context.protocol_version >= 468 else \ 0x2C if context.protocol_version >= 464 else \ 0x2A if context.protocol_version >= 389 else \ diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index e91db13..f463c24 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -4,29 +4,33 @@ These definitions and methods are used by the packet definitions """ import struct import uuid +import io -from .utility import Vector +import pynbt + +from .utility import Vector, class_and_instancemethod __all__ = ( 'Type', 'Boolean', 'UnsignedByte', 'Byte', 'Short', 'UnsignedShort', - 'Integer', 'FixedPointInteger', 'Angle', 'VarInt', 'Long', + 'Integer', 'FixedPointInteger', 'Angle', 'VarInt', 'VarLong', 'Long', 'UnsignedLong', 'Float', 'Double', 'ShortPrefixedByteArray', 'VarIntPrefixedByteArray', 'TrailingByteArray', 'String', 'UUID', - 'Position', + 'Position', 'NBT', 'PrefixedArray', ) class Type(object): + # pylint: disable=no-self-argument __slots__ = () - @classmethod - def read_with_context(cls, file_object, _context): - return cls.read(file_object) + @class_and_instancemethod + def read_with_context(cls_or_self, file_object, _context): + return cls_or_self.read(file_object) - @classmethod - def send_with_context(cls, value, socket, _context): - return cls.send(value, socket) + @class_and_instancemethod + def send_with_context(cls_or_self, value, socket, _context): + return cls_or_self.send(value, socket) @classmethod def read(cls, file_object): @@ -130,12 +134,13 @@ class Angle(Type): class VarInt(Type): - @staticmethod - def read(file_object): + max_bytes = 5 + + @classmethod + def read(cls, file_object): number = 0 - # Limit of 5 bytes, otherwise its possible to cause - # a DOS attack by sending VarInts that just keep - # going + # Limit of 'cls.max_bytes' bytes, otherwise its possible to cause + # a DOS attack by sending VarInts that just keep going bytes_encountered = 0 while True: byte = file_object.read(1) @@ -148,7 +153,7 @@ class VarInt(Type): break bytes_encountered += 1 - if bytes_encountered > 5: + if bytes_encountered > cls.max_bytes: raise ValueError("Tried to read too long of a VarInt") return number @@ -171,6 +176,10 @@ class VarInt(Type): raise ValueError("Integer too large") +class VarLong(VarInt): + max_bytes = 10 + + # Maps (maximum integer value -> size of VarInt in bytes) VARINT_SIZE_TABLE = { 2 ** 7: 1, @@ -323,3 +332,48 @@ class Position(Type, Vector): if context.protocol_version >= 443 else (x & 0x3FFFFFF) << 38 | (y & 0xFFF) << 26 | (z & 0x3FFFFFF)) UnsignedLong.send(value, socket) + + +class NBT(Type): + @staticmethod + def read(file_object): + return pynbt.NBTFile(io=file_object) + + @staticmethod + def send(value, socket): + buffer = io.BytesIO() + pynbt.NBTFile(value=value).save(buffer) + socket.send(buffer.getvalue()) + + +class PrefixedArray(Type): + __slots__ = 'length_type', 'element_type' + + def __init__(self, length_type, element_type): + self.length_type = length_type + self.element_type = element_type + + def read(self, file_object): + return self.__read(file_object, self.element_type.read) + + def send(self, value, socket): + return self.__send(value, socket, self.element_type.send) + + def read_with_context(self, file_object, context): + def element_read(file_object): + return self.element_type.read_with_context(file_object, context) + return self.__read(file_object, element_read) + + def send_with_context(self, value, socket, context): + def element_send(value, socket): + return self.element_type.send_with_context(value, socket, context) + return self.__send(value, socket, element_send) + + def __read(self, file_object, element_read): + length = self.length_type.read(file_object) + return [element_read(file_object) for i in range(length)] + + def __send(self, value, socket, element_send): + self.length_type.send(len(value), socket) + for element in value: + element_send(element, socket) diff --git a/minecraft/networking/types/enum.py b/minecraft/networking/types/enum.py index 61aa238..995aedb 100644 --- a/minecraft/networking/types/enum.py +++ b/minecraft/networking/types/enum.py @@ -99,13 +99,22 @@ class Dimension(Enum): OVERWORLD = 0 END = 1 + from_identifier_dict = { + 'minecraft:the_nether': NETHER, + 'minecraft:overworld': OVERWORLD, + 'minecraft:the_end': END, + } + + to_identifier_dict = {e: i for (i, e) in from_identifier_dict.items()} + # Designation of a player's gamemode. -class GameMode(Enum): +class GameMode(BitFieldEnum): SURVIVAL = 0 CREATIVE = 1 ADVENTURE = 2 SPECTATOR = 3 + HARDCORE = 8 # Only used prior to protocol 738. # Currently designates an entity's feet or eyes. diff --git a/minecraft/networking/types/utility.py b/minecraft/networking/types/utility.py index 0103cc4..c7ad5a7 100644 --- a/minecraft/networking/types/utility.py +++ b/minecraft/networking/types/utility.py @@ -1,13 +1,16 @@ """Minecraft data types that are used by packets, but don't have a specific network representation. """ +import types + from collections import namedtuple from itertools import chain __all__ = ( 'Vector', 'MutableRecord', 'Direction', 'PositionAndLook', 'descriptor', - 'attribute_alias', 'multi_attribute_alias', + 'overridable_descriptor', 'overridable_property', 'attribute_alias', + 'multi_attribute_alias', ) @@ -142,23 +145,58 @@ def multi_attribute_alias(container, *arg_names, **kwd_names): return alias -class descriptor(object): - """Behaves identically to the builtin 'property' function of Python, - except that the getter, setter and deleter functions given by the - user are used as the raw __get__, __set__ and __delete__ functions - as defined in Python's descriptor protocol. +class overridable_descriptor: + """As 'descriptor' (defined below), except that only a getter can be + defined, and the resulting descriptor has no '__set__' or '__delete__' + methods defined; hence, attributes defined via this class can be + overridden by attributes of instances of the class in which it occurs. """ - __slots__ = '_fget', '_fset', '_fdel' + __slots__ = '_fget', - def __init__(self, fget=None, fset=None, fdel=None): + def __init__(self, fget=None): self._fget = fget if fget is not None else self._default_get - self._fset = fset if fset is not None else self._default_set - self._fdel = fdel if fdel is not None else self._default_del def getter(self, fget): self._fget = fget return self + @staticmethod + def _default_get(instance, owner): + raise AttributeError('unreadable attribute') + + def __get__(self, instance, owner): + return self._fget(self, instance, owner) + + +class overridable_property(overridable_descriptor): + """As the builtin 'property' decorator of Python, except that only + a getter is defined and the resulting descriptor is a non-data + descriptor, overridable by attributes of instances of the class + in which the property occurs. See also 'overridable_descriptor' above. + """ + def __get__(self, instance, _owner): + return self._fget(instance) + + +class descriptor(overridable_descriptor): + """Behaves identically to the builtin 'property' decorator of Python, + except that the getter, setter and deleter functions given by the + user are used as the raw __get__, __set__ and __delete__ functions + as defined in Python's descriptor protocol. + + Since an instance of this class always havs '__set__' and '__delete__' + defined, it is a "data descriptor", so its binding behaviour cannot be + overridden in instances of the class in which it occurs. See + https://docs.python.org/3/reference/datamodel.html#descriptor-invocation + for more information. See also 'overridable_descriptor' above. + """ + __slots__ = '_fset', '_fdel' + + def __init__(self, fget=None, fset=None, fdel=None): + super(descriptor, self).__init__(fget=fget) + self._fset = fset if fset is not None else self._default_set + self._fdel = fdel if fdel is not None else self._default_del + def setter(self, fset): self._fset = fset return self @@ -167,10 +205,6 @@ class descriptor(object): self._fdel = fdel return self - @staticmethod - def _default_get(instance, owner): - raise AttributeError('unreadable attribute') - @staticmethod def _default_set(instance, value): raise AttributeError("can't set attribute") @@ -179,9 +213,6 @@ class descriptor(object): def _default_del(instance): raise AttributeError("can't delete attribute") - def __get__(self, instance, owner): - return self._fget(self, instance, owner) - def __set__(self, instance, value): return self._fset(self, instance, value) @@ -189,6 +220,26 @@ class descriptor(object): return self._fdel(self, instance) +class class_and_instancemethod: + """ A decorator for functions defined in a class namespace which are to be + accessed as both class and instance methods: retrieving the method from + a class will return a bound class method (like the built-in + 'classmethod' decorator), but retrieving the method from an instance + will return a bound instance method (as if the function were not + decorated). Therefore, the first argument of the decorated function may + be either a class or an instance, depending on how it was called. + """ + + __slots__ = '_func', + + def __init__(self, func): + self._func = func + + def __get__(self, inst, owner=None): + bind_to = owner if inst is None else inst + return types.MethodType(self._func, bind_to) + + Direction = namedtuple('Direction', ('yaw', 'pitch')) diff --git a/requirements.txt b/requirements.txt index 27732d2..2a1f0e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ cryptography>=1.5 requests +pynbt diff --git a/tests/fake_server.py b/tests/fake_server.py index 788fdd0..2bc24ec 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -1,3 +1,5 @@ +import pynbt + from minecraft import SUPPORTED_MINECRAFT_VERSIONS from minecraft.networking import connection from minecraft.networking import types @@ -98,10 +100,46 @@ class FakeClientHandler(object): def handle_play_start(self): # Called upon entering the play state. - self.write_packet(clientbound.play.JoinGamePacket( - entity_id=0, game_mode=0, dimension=0, hashed_seed=12345, - difficulty=2, max_players=1, level_type='default', - reduced_debug_info=False, render_distance=9, respawn_screen=False)) + packet = clientbound.play.JoinGamePacket( + entity_id=0, is_hardcore=False, game_mode=0, previous_game_mode=0, + world_names=['minecraft:overworld'], + 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) + + if self.server.context.protocol_version >= 748: + packet.dimension = pynbt.TAG_Compound({ + 'natural': pynbt.TAG_Byte(1), + 'effects': pynbt.TAG_String('minecraft:overworld'), + }, '') + packet.dimension_codec = pynbt.TAG_Compound({ + 'minecraft:dimension_type': pynbt.TAG_Compound({ + 'type': pynbt.TAG_String('minecraft:dimension_type'), + 'value': pynbt.TAG_List(pynbt.TAG_Compound, [ + pynbt.TAG_Compound(packet.dimension), + ]), + }), + 'minecraft:worldgen/biome': pynbt.TAG_Compound({ + 'type': pynbt.TAG_String('minecraft:worldgen/biome'), + 'value': pynbt.TAG_List(pynbt.TAG_Compound, [ + pynbt.TAG_Compound({ + 'id': pynbt.TAG_Int(1), + 'name': pynbt.TAG_String('minecraft:plains'), + }), + pynbt.TAG_Compound({ + 'id': pynbt.TAG_Int(2), + 'name': pynbt.TAG_String('minecraft:desert'), + }), + ]), + }), + }, '') + elif self.server.context.protocol_version >= 718: + packet.dimension = 'minecraft:overworld' + else: + packet.dimension = types.Dimension.OVERWORLD + + self.write_packet(packet) def handle_play_packet(self, packet): # Called upon each packet received after handle_play_start() returns. @@ -178,7 +216,8 @@ class FakeClientHandler(object): try: self.handle_connection() packet = self.read_packet() - assert isinstance(packet, serverbound.handshake.HandShakePacket) + assert isinstance(packet, serverbound.handshake.HandShakePacket), \ + type(packet) self.handle_handshake(packet) if packet.next_state == 1: self._run_status() diff --git a/tests/test_backward_compatible.py b/tests/test_backward_compatible.py index 577b97d..e9b321f 100644 --- a/tests/test_backward_compatible.py +++ b/tests/test_backward_compatible.py @@ -101,3 +101,56 @@ class ClassMemberAliasesTest(unittest.TestCase): packet = clientbound.play.BlockChangePacket(blockId=bi, blockMeta=bm) self.assertEqual((packet.blockId, packet.blockMeta), (bi, bm)) self.assertEqual(packet.blockStateId, packet.block_state_id) + + def test_join_game_packet(self): + GameMode = types.GameMode + context = ConnectionContext() + for pure_game_mode in (GameMode.SURVIVAL, GameMode.CREATIVE, + GameMode.ADVENTURE, GameMode.SPECTATOR): + for is_hardcore in (False, True): + context.protocol_version = 70 + game_mode = \ + pure_game_mode | GameMode.HARDCORE \ + if is_hardcore else pure_game_mode + + packet = clientbound.play.JoinGamePacket() + packet.game_mode = game_mode + packet.context = context + self.assertEqual(packet.pure_game_mode, pure_game_mode) + self.assertEqual(packet.is_hardcore, is_hardcore) + + del packet.context + del packet.is_hardcore + packet.context = context + self.assertEqual(packet.game_mode, packet.pure_game_mode) + + del packet.context + del packet.game_mode + packet.context = context + self.assertFalse(hasattr(packet, 'is_hardcore')) + + packet = clientbound.play.JoinGamePacket() + packet.pure_game_mode = game_mode + packet.is_hardcore = is_hardcore + packet.context = context + self.assertEqual(packet.game_mode, game_mode) + + context.protocol_version = 738 + game_mode = pure_game_mode | GameMode.HARDCORE + + packet = clientbound.play.JoinGamePacket() + packet.game_mode = game_mode + packet.is_hardcore = is_hardcore + packet.context = context + self.assertEqual(packet.game_mode, game_mode) + self.assertEqual(packet.pure_game_mode, game_mode) + self.assertEqual(packet.is_hardcore, is_hardcore) + + del packet.context + packet.is_hardcore = is_hardcore + packet.context = context + self.assertEqual(packet.game_mode, game_mode) + self.assertEqual(packet.pure_game_mode, game_mode) + + with self.assertRaises(AttributeError): + del packet.pure_game_mode diff --git a/tests/test_packets.py b/tests/test_packets.py index a004488..57efeb9 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -186,11 +186,9 @@ class TestReadWritePackets(unittest.TestCase): 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.assertEqual(str(packet), + 'CombatEventPacket(event=EndCombatEvent(' + 'duration=415, entity_id=91063502))') self._test_read_write_packet(packet) packet = clientbound.play.CombatEventPacket( @@ -205,26 +203,32 @@ class TestReadWritePackets(unittest.TestCase): 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].blockId, 56) - 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.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)])' - ) + for protocol_version in TEST_VERSIONS: + context = ConnectionContext() + context.protocol_version = protocol_version + packet = clientbound.play.MultiBlockChangePacket(context) - self._test_read_write_packet(packet) + if protocol_version >= 741: + packet.chunk_section_pos = Vector(167, 17, 33) + packet.invert_trust_edges = False + else: + packet.chunk_x, packet.chunk_z = 167, 17 + self.assertEqual(packet.chunk_pos, (167, 17)) + + packet.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), + ] + + for i in range(3): + self.assertEqual(packet.records[i].blockId, 56) + self.assertEqual(packet.records[i].blockMeta, 13) + self.assertEqual(packet.records[i].blockStateId, 909) + self.assertEqual(packet.records[i].position, Vector(1, 2, 3)) + + self._test_read_write_packet(packet, context) def test_spawn_object_packet(self): for protocol_version in TEST_VERSIONS: From 3f4a5d46a6ba0ec0834fa1418655014f0ba47235 Mon Sep 17 00:00:00 2001 From: joo Date: Mon, 17 Aug 2020 11:31:02 +0200 Subject: [PATCH 22/24] Update README with supported versions; increment package version --- README.rst | 3 ++- minecraft/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cb86002..e0f6f61 100644 --- a/README.rst +++ b/README.rst @@ -31,6 +31,7 @@ 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 In addition, some development snapshots and pre-release versions are supported: ``_ contains a full list of supported Minecraft versions @@ -60,7 +61,7 @@ Requirements ------------ - `cryptography `_ - `requests `_ -- `pynbt `_ +- `PyNBT `_ The requirements are also stored in ``requirements.txt`` diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 39cafd1..b0f0c0f 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -4,7 +4,7 @@ with a MineCraft server. """ # The version number of the most recent pyCraft release. -__version__ = "0.6.0" +__version__ = "0.7.0" # A dict mapping the ID string of each Minecraft version supported by pyCraft # to the corresponding protocol version number. The ID string of a version is From fcacb8abf802f806661de15ca6fa2850002e9803 Mon Sep 17 00:00:00 2001 From: joo Date: Mon, 17 Aug 2020 12:01:03 +0200 Subject: [PATCH 23/24] Remove trailing space from join_game_and_respawn_packets.py --- .../packets/clientbound/play/join_game_and_respawn_packets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 437abba..f94a07b 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 @@ -163,7 +163,7 @@ class JoinGamePacket(AbstractDimensionPacket): def field_string(self, field): if field == 'dimension_codec': # pylint: disable=no-member - return nbt_to_snbt(self.dimension_codec) + return nbt_to_snbt(self.dimension_codec) return super(JoinGamePacket, self).field_string(field) From 2d9479cc12f133c5897589ed86e4e1e6cb299a6b Mon Sep 17 00:00:00 2001 From: joo Date: Mon, 17 Aug 2020 12:01:26 +0200 Subject: [PATCH 24/24] Change Travis config to use pypy3 instead of pypy --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e5f09fe..bd8217b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ python: 3.8 matrix: include: - - python: pypy + - python: pypy3 env: TOX_ENV=pypy - python: 3.5 env: TOX_ENV=py35