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/.travis.yml b/.travis.yml index 687d02c..bd8217b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,7 @@ python: 3.8 matrix: include: - - python: 2.7 - env: TOX_ENV=py27 - - python: pypy + - python: pypy3 env: TOX_ENV=pypy - python: 3.5 env: TOX_ENV=py35 diff --git a/README.rst b/README.rst index 907aa64..2634007 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 @@ -50,7 +51,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 +61,7 @@ Requirements ------------ - `cryptography `_ - `requests `_ -- `future `_ +- `PyNBT `_ The requirements are also stored in ``setup.py`` diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 914d1db..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 @@ -225,20 +225,84 @@ 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, + '20w12a': 707, + '20w13a': 708, + '20w13b': 709, + '20w14a': 710, + '20w15a': 711, + '20w16a': 712, + '20w17a': 713, + '20w18a': 714, + '20w19a': 715, + '20w20a': 716, + '20w20b': 717, + '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, } # 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 + global 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() 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 31ce645..5713295 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 @@ -495,7 +491,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: @@ -510,7 +507,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: @@ -593,7 +593,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): @@ -644,14 +645,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 = packets.Packet() + packet.context = self.connection.context + packet.id = packet_id + return packet else: return None diff --git a/minecraft/networking/packets/clientbound/login/__init__.py b/minecraft/networking/packets/clientbound/login/__init__.py index d323fe1..38e0d8b 100644 --- a/minecraft/networking/packets/clientbound/login/__init__.py +++ b/minecraft/networking/packets/clientbound/login/__init__.py @@ -1,7 +1,7 @@ from minecraft.networking.packets import Packet from minecraft.networking.types import ( - VarInt, String, VarIntPrefixedByteArray, TrailingByteArray + VarInt, String, VarIntPrefixedByteArray, TrailingByteArray, UUID, ) @@ -54,9 +54,10 @@ class LoginSuccessPacket(Packet): 0x02 packet_name = "login success" - definition = [ - {'UUID': String}, - {'Username': String}] + get_definition = staticmethod(lambda context: [ + {'UUID': UUID if context.protocol_version >= 707 else 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..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. @@ -33,6 +33,8 @@ def get_packets(context): DisconnectPacket, SpawnPlayerPacket, EntityVelocityPacket, + EntityPositionDeltaPacket, + TimeUpdatePacket, UpdateHealthPacket, CombatEventPacket, ExplosionPacket, @@ -62,7 +64,9 @@ def get_packets(context): class KeepAlivePacket(AbstractKeepAlivePacket): @staticmethod def get_id(context): - return 0x21 if context.protocol_version >= 550 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 \ 0x20 if context.protocol_version >= 345 else \ @@ -72,41 +76,11 @@ class KeepAlivePacket(AbstractKeepAlivePacket): 0x00 -class JoinGamePacket(Packet): - @staticmethod - def get_id(context): - return 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}, - {'game_mode': UnsignedByte}, - {'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}, - {'render_distance': VarInt} if context.protocol_version >= 468 else {}, - {'reduced_debug_info': Boolean}, - {'respawn_screen': Boolean} if context.protocol_version >= 571 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 0x0E if context.protocol_version >= 550 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 \ 0x0D if context.protocol_version >= 70 else \ @@ -125,7 +99,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 >= 721 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 \ @@ -133,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. @@ -146,7 +123,9 @@ class ChatMessagePacket(Packet): class DisconnectPacket(Packet): @staticmethod def get_id(context): - return 0x1B if context.protocol_version >= 550 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 \ 0x1A if context.protocol_version >= 332 else \ @@ -171,7 +150,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 >= 721 else \ + 0x05 if context.protocol_version >= 67 else \ 0x0C packet_name = 'spawn player' @@ -206,7 +186,9 @@ class SpawnPlayerPacket(Packet): class EntityVelocityPacket(Packet): @staticmethod def get_id(context): - return 0x46 if context.protocol_version >= 550 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 \ 0x41 if context.protocol_version >= 461 else \ 0x42 if context.protocol_version >= 451 else \ @@ -229,10 +211,59 @@ class EntityVelocityPacket(Packet): ]) +class EntityPositionDeltaPacket(Packet): + @staticmethod + def get_id(context): + 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: [ + {'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 >= 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: [ + {'world_age': Long}, + {'time_of_day': Long}, + ]) + + class UpdateHealthPacket(Packet): @staticmethod def get_id(context): - return 0x49 if context.protocol_version >= 550 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 \ 0x44 if context.protocol_version >= 461 else \ 0x45 if context.protocol_version >= 451 else \ @@ -254,41 +285,12 @@ class UpdateHealthPacket(Packet): ]) -class RespawnPacket(Packet): - @staticmethod - def get_id(context): - return 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}, - ]) - - # 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 0x19 if context.protocol_version >= 550 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 \ 0x18 if context.protocol_version >= 332 else \ @@ -300,7 +302,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 >= 721 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 \ @@ -321,7 +324,9 @@ class PlayerListHeaderAndFooterPacket(Packet): class EntityLookPacket(Packet): @staticmethod def get_id(context): - return 0x2B if context.protocol_version >= 550 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 \ 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..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,16 @@ 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 0x0C if context.protocol_version >= 550 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 \ 0x0B if context.protocol_version >= 67 else \ @@ -46,7 +48,9 @@ class BlockChangePacket(Packet): class MultiBlockChangePacket(Packet): @staticmethod def get_id(context): - return 0x10 if context.protocol_version >= 550 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 \ 0x11 if context.protocol_version >= 318 else \ @@ -55,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): @@ -91,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 22c39ad..db53734 100644 --- a/minecraft/networking/packets/clientbound/play/combat_event_packet.py +++ b/minecraft/networking/packets/clientbound/play/combat_event_packet.py @@ -8,7 +8,9 @@ from minecraft.networking.types import ( class CombatEventPacket(Packet): @staticmethod def get_id(context): - return 0x33 if context.protocol_version >= 550 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 \ 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..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,7 +7,9 @@ from minecraft.networking.packets import Packet class ExplosionPacket(Packet): @staticmethod def get_id(context): - return 0x1D if context.protocol_version >= 550 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 \ 0x1D if context.protocol_version >= 345 else \ @@ -19,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') @@ -28,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 b98b641..4aca89d 100644 --- a/minecraft/networking/packets/clientbound/play/face_player_packet.py +++ b/minecraft/networking/packets/clientbound/play/face_player_packet.py @@ -8,7 +8,9 @@ from minecraft.networking.packets import Packet class FacePlayerPacket(Packet): @staticmethod def get_id(context): - return 0x35 if context.protocol_version >= 550 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 \ 0x31 if context.protocol_version >= 389 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..f94a07b --- /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 99870b8..3de187b 100644 --- a/minecraft/networking/packets/clientbound/play/map_packet.py +++ b/minecraft/networking/packets/clientbound/play/map_packet.py @@ -8,7 +8,9 @@ from minecraft.networking.types import ( class MapPacket(Packet): @staticmethod def get_id(context): - return 0x27 if context.protocol_version >= 550 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 \ 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..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,7 +9,9 @@ from minecraft.networking.types import ( class PlayerListItemPacket(Packet): @staticmethod def get_id(context): - return 0x34 if context.protocol_version >= 550 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 \ 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..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,7 +9,9 @@ from minecraft.networking.types import ( class PlayerPositionAndLookPacket(Packet, BitFieldEnum): @staticmethod def get_id(context): - return 0x36 if context.protocol_version >= 550 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 \ 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 4c53a0e..da56748 100644 --- a/minecraft/networking/packets/clientbound/play/sound_effect_packet.py +++ b/minecraft/networking/packets/clientbound/play/sound_effect_packet.py @@ -9,11 +9,11 @@ __all__ = 'SoundEffectPacket', class SoundEffectPacket(Packet): @staticmethod def get_id(context): - return 0x52 if context.protocol_version >= 550 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 \ 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/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..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) @@ -122,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/minecraft/networking/packets/serverbound/play/__init__.py b/minecraft/networking/packets/serverbound/play/__init__.py index fff5ecd..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 + multi_attribute_alias, ) from .client_settings_packet import ClientSettingsPacket @@ -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 \ @@ -124,7 +126,9 @@ class TeleportConfirmPacket(Packet): class AnimationPacket(Packet): @staticmethod def get_id(context): - return 0x2A if context.protocol_version >= 468 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 \ 0x25 if context.protocol_version >= 386 else \ @@ -197,7 +201,9 @@ class PlayerBlockPlacementPacket(Packet): @staticmethod def get_id(context): - return 0x2C if context.protocol_version >= 468 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 \ 0x27 if context.protocol_version >= 386 else \ @@ -234,7 +240,9 @@ class PlayerBlockPlacementPacket(Packet): class UseItemPacket(Packet): @staticmethod def get_id(context): - return 0x2D if context.protocol_version >= 468 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 \ 0x28 if context.protocol_version >= 386 else \ diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index 4ccf627..f463c24 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -2,32 +2,35 @@ 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 +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): @@ -131,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) @@ -149,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 @@ -172,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, @@ -324,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 2916437..c7ad5a7 100644 --- a/minecraft/networking/types/utility.py +++ b/minecraft/networking/types/utility.py @@ -1,7 +1,7 @@ """Minecraft data types that are used by packets, but don't have a specific network representation. """ -from __future__ import division +import types from collections import namedtuple from itertools import chain @@ -9,7 +9,8 @@ from itertools import chain __all__ = ( 'Vector', 'MutableRecord', 'Direction', 'PositionAndLook', 'descriptor', - 'attribute_alias', 'multi_attribute_alias', + 'overridable_descriptor', 'overridable_property', 'attribute_alias', + 'multi_attribute_alias', ) @@ -144,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 @@ -169,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") @@ -181,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) @@ -191,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 d6e1198..7e306e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ +# Package dependencies are stored in setup.py. +# For more information, see . -e . diff --git a/setup.py b/setup.py index f0ad059..700a8fd 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup(name="pyCraft", author=", ".join(MAIN_AUTHORS), install_requires=["cryptography>=1.5", "requests", - "future", + "pynbt", ], packages=["minecraft", "minecraft.networking", diff --git a/start.py b/start.py index 493a9a7..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(): @@ -35,6 +32,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: @@ -81,9 +82,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. - return - print('--> %s' % packet, file=sys.stderr) + # 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('--> [unknown packet] %s' % packet, file=sys.stderr) + else: + print('--> %s' % packet, file=sys.stderr) def print_outgoing(packet): print('<-- %s' % packet, file=sys.stderr) 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..2bc24ec 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -1,4 +1,4 @@ -from __future__ import print_function +import pynbt from minecraft import SUPPORTED_MINECRAFT_VERSIONS from minecraft.networking import connection @@ -11,7 +11,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 @@ -101,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. @@ -181,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() @@ -576,7 +612,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_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_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/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: 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