From 4c355171578db16dd849a88fe830023511507cc6 Mon Sep 17 00:00:00 2001 From: joo Date: Mon, 17 Aug 2020 06:13:24 +0200 Subject: [PATCH] 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: