diff --git a/README.rst b/README.rst index d353b40..042693c 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,8 @@ pyCraft is compatible with the following Minecraft releases: * 1.10, 1.10.1, 1.10.2 * 1.11, 1.11.1, 1.11.2 * 1.12, 1.12.1, 1.12.2 -* 1.13 +* 1.13, 1.13.1, 1.13.2 +* 1.14 In addition, some development snapshots and pre-release versions are supported: ``_ contains a full list of supported Minecraft versions diff --git a/docs/authentication.rst b/docs/authentication.rst index 709684c..f5b1db9 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -28,10 +28,10 @@ is provided or the web request failed. :members: -Arbitary Requests +Arbitrary Requests ~~~~~~~~~~~~~~~~~~~~ -You may make any arbitary request to the Yggdrasil service with the _make_request +You may make any arbitrary request to the Yggdrasil service with the _make_request method passing in the AUTH_SERVER as the server parameter. .. automodule:: minecraft.authentication @@ -43,7 +43,7 @@ method passing in the AUTH_SERVER as the server parameter. --------------- Example Usage --------------- -An example of making an arbitary request can be seen here:: +An example of making an arbitrary request can be seen here:: payload = {'username': username, 'password': password} diff --git a/docs/conf.py b/docs/conf.py index 9f31529..d7d2ad2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,7 +34,6 @@ extensions = [ 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', - 'sphinx.ext.pngmath', 'sphinx.ext.viewcode', ] diff --git a/minecraft/__init__.py b/minecraft/__init__.py index 866483c..e5d9f68 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -6,121 +6,170 @@ with a MineCraft server. __version__ = "0.5.0" SUPPORTED_MINECRAFT_VERSIONS = { - '1.8': 47, - '1.8.1': 47, - '1.8.2': 47, - '1.8.3': 47, - '1.8.4': 47, - '1.8.5': 47, - '1.8.6': 47, - '1.8.7': 47, - '1.8.8': 47, - '1.8.9': 47, - '1.9': 107, - '1.9.1': 108, - '1.9.2': 109, - '1.9.3': 110, - '1.9.4': 110, - '1.10': 210, - '1.10.1': 210, - '1.10.2': 210, - '16w32a': 301, - '16w32b': 302, - '16w33a': 303, - '16w35a': 304, - '16w36a': 305, - '16w38a': 306, - '16w39a': 307, - '16w39b': 308, - '16w39c': 309, - '16w40a': 310, - '16w41a': 311, - '16w42a': 312, - '16w43a': 313, - '16w44a': 313, - '1.11-pre1': 314, - '1.11': 315, - '16w50a': 316, - '1.11.1': 316, - '1.11.2': 316, - '17w06a': 317, - '17w13a': 318, - '17w13b': 319, - '17w14a': 320, - '17w15a': 321, - '17w16a': 322, - '17w16b': 323, - '17w17a': 324, - '17w17b': 325, - '17w18a': 326, - '17w18b': 327, - '1.12-pre1': 328, - '1.12-pre2': 329, - '1.12-pre3': 330, - '1.12-pre4': 331, - '1.12-pre5': 332, - '1.12-pre6': 333, - '1.12-pre7': 334, - '1.12': 335, - '17w31a': 336, - '1.12.1-pre1': 337, - '1.12.1': 338, - '1.12.2-pre1': 339, - '1.12.2-pre2': 339, - '1.12.2': 340, - '17w43a': 341, - '17w43b': 342, - '17w45a': 343, - '17w45b': 344, - '17w46a': 345, - '17w47a': 346, - '17w47b': 347, - '17w48a': 348, - '17w49a': 349, - '17w49b': 350, - '17w50a': 351, - '18w01a': 352, - '18w02a': 353, - '18w03a': 354, - '18w03b': 355, - '18w05a': 356, - '18w06a': 357, - '18w07a': 358, - '18w07b': 359, - '18w07c': 360, - '18w08a': 361, - '18w08b': 362, - '18w09a': 363, - '18w10a': 364, - '18w10b': 365, - '18w10c': 366, - '18w10d': 367, - '18w11a': 368, - '18w14a': 369, - '18w14b': 370, - '18w15a': 371, - '18w16a': 372, - '18w19a': 373, - '18w19b': 374, - '18w20a': 375, - '18w20b': 376, - '18w20c': 377, - '18w21a': 378, - '18w21b': 379, - '18w22a': 380, - '18w22b': 381, - '18w22c': 382, - '1.13-pre1': 383, - '1.13-pre2': 384, - '1.13-pre3': 385, - '1.13-pre4': 386, - '1.13-pre5': 387, - '1.13-pre6': 388, - '1.13-pre7': 389, - '1.13-pre8': 390, - '1.13-pre9': 391, - '1.13-pre10': 392, - '1.13': 393, + '1.8': 47, + '1.8.1': 47, + '1.8.2': 47, + '1.8.3': 47, + '1.8.4': 47, + '1.8.5': 47, + '1.8.6': 47, + '1.8.7': 47, + '1.8.8': 47, + '1.8.9': 47, + '1.9': 107, + '1.9.1': 108, + '1.9.2': 109, + '1.9.3': 110, + '1.9.4': 110, + '1.10': 210, + '1.10.1': 210, + '1.10.2': 210, + '16w32a': 301, + '16w32b': 302, + '16w33a': 303, + '16w35a': 304, + '16w36a': 305, + '16w38a': 306, + '16w39a': 307, + '16w39b': 308, + '16w39c': 309, + '16w40a': 310, + '16w41a': 311, + '16w42a': 312, + '16w43a': 313, + '16w44a': 313, + '1.11-pre1': 314, + '1.11': 315, + '16w50a': 316, + '1.11.1': 316, + '1.11.2': 316, + '17w06a': 317, + '17w13a': 318, + '17w13b': 319, + '17w14a': 320, + '17w15a': 321, + '17w16a': 322, + '17w16b': 323, + '17w17a': 324, + '17w17b': 325, + '17w18a': 326, + '17w18b': 327, + '1.12-pre1': 328, + '1.12-pre2': 329, + '1.12-pre3': 330, + '1.12-pre4': 331, + '1.12-pre5': 332, + '1.12-pre6': 333, + '1.12-pre7': 334, + '1.12': 335, + '17w31a': 336, + '1.12.1-pre1': 337, + '1.12.1': 338, + '1.12.2-pre1': 339, + '1.12.2-pre2': 339, + '1.12.2': 340, + '17w43a': 341, + '17w43b': 342, + '17w45a': 343, + '17w45b': 344, + '17w46a': 345, + '17w47a': 346, + '17w47b': 347, + '17w48a': 348, + '17w49a': 349, + '17w49b': 350, + '17w50a': 351, + '18w01a': 352, + '18w02a': 353, + '18w03a': 354, + '18w03b': 355, + '18w05a': 356, + '18w06a': 357, + '18w07a': 358, + '18w07b': 359, + '18w07c': 360, + '18w08a': 361, + '18w08b': 362, + '18w09a': 363, + '18w10a': 364, + '18w10b': 365, + '18w10c': 366, + '18w10d': 367, + '18w11a': 368, + '18w14a': 369, + '18w14b': 370, + '18w15a': 371, + '18w16a': 372, + '18w19a': 373, + '18w19b': 374, + '18w20a': 375, + '18w20b': 376, + '18w20c': 377, + '18w21a': 378, + '18w21b': 379, + '18w22a': 380, + '18w22b': 381, + '18w22c': 382, + '1.13-pre1': 383, + '1.13-pre2': 384, + '1.13-pre3': 385, + '1.13-pre4': 386, + '1.13-pre5': 387, + '1.13-pre6': 388, + '1.13-pre7': 389, + '1.13-pre8': 390, + '1.13-pre9': 391, + '1.13-pre10': 392, + '1.13': 393, + '18w30a': 394, + '18w30b': 395, + '18w31a': 396, + '18w32a': 397, + '18w33a': 398, + '1.13.1-pre1': 399, + '1.13.1-pre2': 400, + '1.13.1': 401, + '1.13.2-pre1': 402, + '1.13.2-pre2': 403, + '1.13.2': 404, + '18w43a': 441, + '18w43b': 441, + '18w43c': 442, + '18w44a': 443, + '18w45a': 444, + '18w46a': 445, + '18w47a': 446, + '18w47b': 447, + '18w48a': 448, + '18w48b': 449, + '18w49a': 450, + '18w50a': 451, + '19w02a': 452, + '19w03a': 453, + '19w03b': 454, + '19w03c': 455, + '19w04a': 456, + '19w04b': 457, + '19w05a': 458, + '19w06a': 459, + '19w07a': 460, + '19w08a': 461, + '19w08b': 462, + '19w09a': 463, + '19w11a': 464, + '19w11b': 465, + '19w12a': 466, + '19w12b': 467, + '19w13a': 468, + '19w13b': 469, + '19w14a': 470, + '19w14b': 471, + '1.14 Pre-Release 1': 472, + '1.14 Pre-Release 2': 473, + '1.14 Pre-Release 3': 474, + '1.14 Pre-Release 4': 475, + '1.14 Pre-Release 5': 476, + '1.14': 477, } SUPPORTED_PROTOCOL_VERSIONS = \ diff --git a/minecraft/authentication.py b/minecraft/authentication.py index 9750e03..e0135fd 100644 --- a/minecraft/authentication.py +++ b/minecraft/authentication.py @@ -1,5 +1,6 @@ import requests import json +import uuid from .exceptions import YggdrasilError #: The base url for Ygdrassil requests @@ -84,7 +85,7 @@ class AuthenticationToken(object): return True - def authenticate(self, username, password): + def authenticate(self, username, password, invalidate_previous=False): """ Authenticates the user against https://authserver.mojang.com using `username` and `password` parameters. @@ -93,6 +94,8 @@ class AuthenticationToken(object): username - An `str` object with the username (unmigrated accounts) or email address for a Mojang account. password - An `str` object with the password. + invalidate_previous - A `bool`. When `True`, invalidate + all previously acquired `access_token`s across all clients. Returns: Returns `True` if successful. @@ -110,6 +113,12 @@ class AuthenticationToken(object): "password": password } + if not invalidate_previous: + # Include a `client_token` in the payload to prevent existing + # `access_token`s from being invalidated. If `self.client_token` + # is `None` generate a `client_token` using uuid4 + payload["clientToken"] = self.client_token or uuid.uuid4().hex + res = _make_request(AUTH_SERVER, "authenticate", payload) _raise_from_response(res) @@ -269,7 +278,7 @@ def _make_request(server, endpoint, data): A `requests.Request` object. """ res = requests.post(server + "/" + endpoint, data=json.dumps(data), - headers=HEADERS) + headers=HEADERS, timeout=15) return res diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index b026e83..dead8bd 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -362,6 +362,8 @@ class Connection(object): self.socket = socket.socket(ai_faml, ai_type, ai_prot) self.socket.connect(ai_addr) self.file_object = self.socket.makefile("rb", 0) + self.options.compression_enabled = False + self.options.compression_threshold = -1 self.connected = True def disconnect(self, immediate=False): @@ -510,7 +512,7 @@ class NetworkingThread(threading.Thread): # Ignore the earlier exception if a disconnect packet is # received, as it may have been caused by trying to write to - # thw closed socket, which does not represent a program error. + # the closed socket, which does not represent a program error. if exc_info is not None and packet.packet_name == "disconnect": exc_info = None diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index dc4364e..b13a576 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -4,7 +4,7 @@ from minecraft.networking.packets import ( from minecraft.networking.types import ( Integer, FixedPointInteger, UnsignedByte, Byte, Boolean, UUID, Short, - VarInt, Double, Float, String, Enum, + VarInt, Double, Float, String, Enum, Difficulty, Dimension, GameMode ) from .combat_event_packet import CombatEventPacket @@ -21,6 +21,7 @@ def get_packets(context): packets = { KeepAlivePacket, JoinGamePacket, + ServerDifficultyPacket, ChatMessagePacket, PlayerPositionAndLookPacket, MapPacket, @@ -34,7 +35,9 @@ def get_packets(context): SpawnObjectPacket, BlockChangePacket, MultiBlockChangePacket, + RespawnPacket, PluginMessagePacket, + PlayerListHeaderAndFooterPacket, } if context.protocol_version <= 47: packets |= { @@ -46,7 +49,8 @@ def get_packets(context): class KeepAlivePacket(AbstractKeepAlivePacket): @staticmethod def get_id(context): - return 0x21 if context.protocol_version >= 389 else \ + return 0x20 if context.protocol_version >= 471 else \ + 0x21 if context.protocol_version >= 389 else \ 0x20 if context.protocol_version >= 345 else \ 0x1F if context.protocol_version >= 332 else \ 0x20 if context.protocol_version >= 318 else \ @@ -69,10 +73,39 @@ class JoinGamePacket(Packet): {'entity_id': Integer}, {'game_mode': UnsignedByte}, {'dimension': Integer if context.protocol_version >= 108 else Byte}, - {'difficulty': UnsignedByte}, + {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, {'max_players': UnsignedByte}, {'level_type': String}, - {'reduced_debug_info': Boolean}]) + {'render_distance': VarInt} if context.protocol_version >= 468 else {}, + {'reduced_debug_info': Boolean}, + ]) + + # JoinGamePacket.Difficulty is an alias for Difficulty + Difficulty = Difficulty + + # JoinGamePacket.Gamemode is an alias for Gamemode + GameMode = GameMode + + # JoinGamePacket.Dimension is an alias for Dimension + Dimension = Dimension + + +class ServerDifficultyPacket(Packet): + @staticmethod + def get_id(context): + return 0x0D if context.protocol_version >= 332 else \ + 0x0E if context.protocol_version >= 318 else \ + 0x0D if context.protocol_version >= 70 else \ + 0x41 + + packet_name = 'server difficulty' + get_definition = staticmethod(lambda context: [ + {'difficulty': UnsignedByte}, + {'is_locked': Boolean} if context.protocol_version >= 464 else {}, + ]) + + # ServerDifficultyPacket.Difficulty is an alias for Difficulty + Difficulty = Difficulty class ChatMessagePacket(Packet): @@ -98,7 +131,8 @@ class ChatMessagePacket(Packet): class DisconnectPacket(Packet): @staticmethod def get_id(context): - return 0x1B if context.protocol_version >= 345 else \ + return 0x1A if context.protocol_version >= 471 else \ + 0x1B if context.protocol_version >= 345 else \ 0x1A if context.protocol_version >= 332 else \ 0x1B if context.protocol_version >= 318 else \ 0x1A if context.protocol_version >= 107 else \ @@ -144,7 +178,10 @@ class SpawnPlayerPacket(Packet): class EntityVelocityPacket(Packet): @staticmethod def get_id(context): - return 0x41 if context.protocol_version >= 389 else \ + return 0x45 if context.protocol_version >= 471 else \ + 0x41 if context.protocol_version >= 461 else \ + 0x42 if context.protocol_version >= 451 else \ + 0x41 if context.protocol_version >= 389 else \ 0x40 if context.protocol_version >= 352 else \ 0x3F if context.protocol_version >= 345 else \ 0x3E if context.protocol_version >= 336 else \ @@ -166,7 +203,10 @@ class EntityVelocityPacket(Packet): class UpdateHealthPacket(Packet): @staticmethod def get_id(context): - return 0x44 if context.protocol_version >= 389 else \ + return 0x48 if context.protocol_version >= 471 else \ + 0x44 if context.protocol_version >= 461 else \ + 0x45 if context.protocol_version >= 451 else \ + 0x44 if context.protocol_version >= 389 else \ 0x43 if context.protocol_version >= 352 else \ 0x42 if context.protocol_version >= 345 else \ 0x41 if context.protocol_version >= 336 else \ @@ -184,11 +224,65 @@ class UpdateHealthPacket(Packet): ]) +class RespawnPacket(Packet): + @staticmethod + def get_id(context): + return 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 {}, + {'game_mode': UnsignedByte}, + {'level_type': String}, + ]) + + # RespawnPacket.Difficulty is an alias for Difficulty. + Difficulty = Difficulty + + # RespawnPacket.Dimension is an alias for Dimension. + Dimension = Dimension + + # RespawnPacket.Gamemode is an alias for Gamemode. + GameMode = GameMode + + class PluginMessagePacket(AbstractPluginMessagePacket): @staticmethod def get_id(context): - return 0x19 if context.protocol_version >= 345 else \ + return 0x18 if context.protocol_version >= 471 else \ + 0x19 if context.protocol_version >= 345 else \ 0x18 if context.protocol_version >= 332 else \ 0x19 if context.protocol_version >= 318 else \ 0x18 if context.protocol_version >= 70 else \ 0x3F + + +class PlayerListHeaderAndFooterPacket(Packet): + @staticmethod + def get_id(context): + return 0x53 if context.protocol_version >= 471 else \ + 0x5F if context.protocol_version >= 461 else \ + 0x50 if context.protocol_version >= 451 else \ + 0x4F if context.protocol_version >= 441 else \ + 0x4E if context.protocol_version >= 393 else \ + 0x4A if context.protocol_version >= 338 else \ + 0x49 if context.protocol_version >= 335 else \ + 0x47 if context.protocol_version >= 110 else \ + 0x48 if context.protocol_version >= 107 else \ + 0x47 + + packet_name = 'player list header and footer' + definition = [ + {'header': String}, + {'footer': String}] diff --git a/minecraft/networking/packets/clientbound/play/combat_event_packet.py b/minecraft/networking/packets/clientbound/play/combat_event_packet.py index 724c0d4..75fd06c 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 0x2F if context.protocol_version >= 389 else \ + return 0x32 if context.protocol_version >= 471 else \ + 0x30 if context.protocol_version >= 451 else \ + 0x2F if context.protocol_version >= 389 else \ 0x2E if context.protocol_version >= 345 else \ 0x2D if context.protocol_version >= 336 else \ 0x2C if context.protocol_version >= 332 else \ diff --git a/minecraft/networking/packets/clientbound/play/explosion_packet.py b/minecraft/networking/packets/clientbound/play/explosion_packet.py index 02f96c2..05b04d2 100644 --- a/minecraft/networking/packets/clientbound/play/explosion_packet.py +++ b/minecraft/networking/packets/clientbound/play/explosion_packet.py @@ -5,7 +5,8 @@ from minecraft.networking.packets import Packet class ExplosionPacket(Packet): @staticmethod def get_id(context): - return 0x1E if context.protocol_version >= 389 else \ + return 0x1C if context.protocol_version >= 471 else \ + 0x1E if context.protocol_version >= 389 else \ 0x1D if context.protocol_version >= 345 else \ 0x1C if context.protocol_version >= 332 else \ 0x1D if context.protocol_version >= 318 else \ diff --git a/minecraft/networking/packets/clientbound/play/map_packet.py b/minecraft/networking/packets/clientbound/play/map_packet.py index 41372bd..88be7f6 100644 --- a/minecraft/networking/packets/clientbound/play/map_packet.py +++ b/minecraft/networking/packets/clientbound/play/map_packet.py @@ -28,7 +28,7 @@ class MapPacket(Packet): class Map(MutableRecord): __slots__ = ('id', 'scale', 'icons', 'pixels', 'width', 'height', - 'is_tracking_position') + 'is_tracking_position', 'is_locked') def __init__(self, id=None, scale=None, width=128, height=128): self.id = id @@ -38,6 +38,7 @@ class MapPacket(Packet): self.height = height self.pixels = bytearray(0 for i in range(width*height)) self.is_tracking_position = True + self.is_locked = False class MapSet(object): __slots__ = 'maps_by_id' @@ -58,6 +59,11 @@ class MapPacket(Packet): else: self.is_tracking_position = True + if self.context.protocol_version >= 452: + self.is_locked = Boolean.read(file_object) + else: + self.is_locked = False + icon_count = VarInt.read(file_object) self.icons = [] for i in range(icon_count): @@ -99,6 +105,7 @@ class MapPacket(Packet): z = self.offset[1] + i // self.width map.pixels[x + map.width * z] = self.pixels[i] map.is_tracking_position = self.is_tracking_position + map.is_locked = self.is_locked def apply_to_map_set(self, map_set): map = map_set.maps_by_id.get(self.map_id) 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 b09d393..ea8b054 100644 --- a/minecraft/networking/packets/clientbound/play/player_list_item_packet.py +++ b/minecraft/networking/packets/clientbound/play/player_list_item_packet.py @@ -8,7 +8,9 @@ from minecraft.networking.types import ( class PlayerListItemPacket(Packet): @staticmethod def get_id(context): - return 0x30 if context.protocol_version >= 389 else \ + return 0x33 if context.protocol_version >= 471 else \ + 0x31 if context.protocol_version >= 451 else \ + 0x30 if context.protocol_version >= 389 else \ 0x2F if context.protocol_version >= 345 else \ 0x2E if context.protocol_version >= 336 else \ 0x2D if context.protocol_version >= 332 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 0abd7c5..8f6f5cc 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 @@ -8,7 +8,9 @@ from minecraft.networking.types import ( class PlayerPositionAndLookPacket(Packet, BitFieldEnum): @staticmethod def get_id(context): - return 0x32 if context.protocol_version >= 389 else \ + return 0x35 if context.protocol_version >= 471 else \ + 0x33 if context.protocol_version >= 451 else \ + 0x32 if context.protocol_version >= 389 else \ 0x31 if context.protocol_version >= 352 else \ 0x30 if context.protocol_version >= 345 else \ 0x2F if context.protocol_version >= 336 else \ @@ -29,7 +31,7 @@ class PlayerPositionAndLookPacket(Packet, BitFieldEnum): ]) field_enum = classmethod( - lambda cls, field: cls if field == 'flags' else None) + lambda cls, field, context: cls if field == 'flags' else None) FLAG_REL_X = 0x01 FLAG_REL_Y = 0x02 diff --git a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py index eac00ad..fc54233 100644 --- a/minecraft/networking/packets/clientbound/play/spawn_object_packet.py +++ b/minecraft/networking/packets/clientbound/play/spawn_object_packet.py @@ -1,4 +1,5 @@ from minecraft.networking.packets import Packet +from minecraft.networking.types.utility import descriptor from minecraft.networking.types import ( VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector, @@ -14,40 +15,89 @@ class SpawnObjectPacket(Packet): packet_name = 'spawn object' - class EntityType(Enum): - BOAT = 1 - ITEM_STACK = 2 - AREA_EFFECT_CLOUD = 3 - MINECART = 10 - ACTIVATED_TNT = 50 - ENDERCRYSTAL = 51 - ARROW = 60 - SNOWBALL = 61 - EGG = 62 - FIREBALL = 63 - FIRECHARGE = 64 - ENDERPERL = 65 - WITHER_SKULL = 66 - SHULKER_BULLET = 67 - LLAMA_SPIT = 68 - FALLING_OBJECT = 70 - ITEM_FRAMES = 71 - EYE_OF_ENDER = 72 - POTION = 73 - EXP_BOTTLE = 75 - FIREWORK_ROCKET = 76 - LEASH_KNOT = 77 - ARMORSTAND = 78 - EVOCATION_FANGS = 79 - FISHING_HOOK = 90 - SPECTRAL_ARROW = 91 - DRAGON_FIREBALL = 93 + fields = ('entity_id', 'object_uuid', 'type_id', + 'x', 'y', 'z', 'pitch', 'yaw') + + @descriptor + def EntityType(desc, self, cls): # pylint: disable=no-self-argument + if self is None: + # EntityType is being accessed as a class attribute. + raise AttributeError( + 'This interface is deprecated:\n\n' + 'As of pyCraft\'s support for Minecraft 1.14, the nested ' + 'class "SpawnObjectPacket.EntityType" cannot be accessed as a ' + 'class attribute, because it depends on the protocol version. ' + 'There are two ways to access the correct version of the ' + 'class:\n\n' + '1. Access the "EntityType" attribute of a ' + '"SpawnObjectPacket" instance with its "context" property ' + 'set.\n\n' + '2. Call "SpawnObjectPacket.field_enum(\'type_id\', ' + 'context)".') + else: + # EntityType is being accessed as an instance attribute. + return self.field_enum('type_id', self.context) + + @classmethod + def field_enum(cls, field, context): + if field != 'type_id' or context is None: + return + + pv = context.protocol_version + name = 'EntityType_%d' % pv + if hasattr(cls, name): + return getattr(cls, name) + + class EntityType(Enum): + 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 + ARROW = 60 if pv < 458 else 2 + BOAT = 1 if pv < 458 else 5 + DRAGON_FIREBALL = 93 if pv < 458 else 13 + EGG = 62 if pv < 458 else 74 # ThrownEgg + ENDERCRYSTAL = 51 if pv < 458 else 16 + ENDERPEARL = 65 if pv < 458 else 75 # ThrownEnderpearl + EVOCATION_FANGS = 79 if pv < 458 else 20 + EXP_BOTTLE = 75 if pv < 458 else 76 # ThrownExpBottle + EYE_OF_ENDER = 72 if pv < 458 else 23 # EyeOfEnderSignal + FALLING_OBJECT = 70 if pv < 458 else 24 # FallingSand + FIREBALL = 63 if pv < 458 else 34 # Fireball (ghast) + FIRECHARGE = 64 if pv < 458 else 65 # SmallFireball (blaze) + FIREWORK_ROCKET = 76 if pv < 458 else 25 # FireworksRocketEntity + FISHING_HOOK = 90 if pv < 458 else 93 # Fishing bobber + ITEM_FRAMES = 71 if pv < 458 else 33 # ItemFrame + ITEM_STACK = 2 if pv < 458 else 32 # Item + LEASH_KNOT = 77 if pv < 458 else 35 + LLAMA_SPIT = 68 if pv < 458 else 37 + MINECART = 10 if pv < 458 else 39 # MinecartRideable + POTION = 73 if pv < 458 else 77 # ThrownPotion + SHULKER_BULLET = 67 if pv < 458 else 60 + SNOWBALL = 61 if pv < 458 else 67 + SPECTRAL_ARROW = 91 if pv < 458 else 68 + WITHER_SKULL = 66 if pv < 458 else 85 + if pv >= 393: + TRIDENT = 94 + if pv >= 458: + MINECART_CHEST = 40 + MINECART_COMMAND_BLOCK = 41 + MINECART_FURNACE = 42 + MINECART_HOPPER = 43 + MINECART_SPAWNER = 44 + MINECART_TNT = 45 + + setattr(cls, name, EntityType) + return EntityType def read(self, file_object): self.entity_id = VarInt.read(file_object) if self.context.protocol_version >= 49: self.object_uuid = UUID.read(file_object) - self.type_id = Byte.read(file_object) + + if self.context.protocol_version >= 458: + self.type_id = VarInt.read(file_object) + else: + self.type_id = Byte.read(file_object) xyz_type = Double if self.context.protocol_version >= 100 else Integer for attr in 'x', 'y', 'z': @@ -64,7 +114,11 @@ class SpawnObjectPacket(Packet): VarInt.send(self.entity_id, packet_buffer) if self.context.protocol_version >= 49: UUID.send(self.object_uuid, packet_buffer) - Byte.send(self.type_id, packet_buffer) + + if self.context.protocol_version >= 458: + VarInt.send(self.type_id, packet_buffer) + else: + Byte.send(self.type_id, packet_buffer) xyz_type = Double if self.context.protocol_version >= 100 else Integer for coord in self.x, self.y, self.z: @@ -78,9 +132,20 @@ class SpawnObjectPacket(Packet): Short.send(coord, packet_buffer) # Access the entity type as a string, according to the EntityType enum. + @property + def type(self): + if self.context is None: + raise ValueError('This packet must have a non-None "context" ' + 'in order to read the "type" property.') + # pylint: disable=no-member + return self.EntityType.name_from_value(self.type_id) + + @type.setter def type(self, type_name): + if self.context is None: + raise ValueError('This packet must have a non-None "context" ' + 'in order to set the "type" property.') self.type_id = getattr(self.EntityType, type_name) - type = property(lambda p: p.EntityType.name_from_value(p.type_id), type) # Access the fields 'x', 'y', 'z' as a Vector. def position(self, position): diff --git a/minecraft/networking/packets/packet.py b/minecraft/networking/packets/packet.py index 5298f1c..ae39416 100644 --- a/minecraft/networking/packets/packet.py +++ b/minecraft/networking/packets/packet.py @@ -61,7 +61,7 @@ class Packet(object): def read(self, file_object): for field in self.definition: for var_name, data_type in field.items(): - value = data_type.read(file_object) + value = data_type.read_with_context(file_object, self.context) setattr(self, var_name, value) # Writes a packet buffer to the socket with the appropriate headers @@ -104,7 +104,7 @@ class Packet(object): for field in self.definition: for var_name, data_type in field.items(): data = getattr(self, var_name) - data_type.send(data, packet_buffer) + data_type.send_with_context(data, packet_buffer, self.context) def __repr__(self): str = type(self).__name__ @@ -119,6 +119,8 @@ class Packet(object): @property def fields(self): """ An iterable of the names of the packet's fields, or None. """ + if self.definition is None: + return None return (field for defn in self.definition for field in defn) def field_string(self, field): @@ -127,7 +129,7 @@ class Packet(object): """ value = getattr(self, field, None) - enum_class = self.field_enum(field) + enum_class = self.field_enum(field, self.context) if enum_class is not None: name = enum_class.name_from_value(value) if name is not None: @@ -136,7 +138,7 @@ class Packet(object): return repr(value) @classmethod - def field_enum(cls, field): + def field_enum(cls, field, context=None): """ The subclass of 'minecraft.networking.types.Enum' associated with this field, or None if there is no such class. """ diff --git a/minecraft/networking/packets/serverbound/play/__init__.py b/minecraft/networking/packets/serverbound/play/__init__.py index 97d5604..e72de2d 100644 --- a/minecraft/networking/packets/serverbound/play/__init__.py +++ b/minecraft/networking/packets/serverbound/play/__init__.py @@ -32,7 +32,9 @@ def get_packets(context): class KeepAlivePacket(AbstractKeepAlivePacket): @staticmethod def get_id(context): - return 0x0E if context.protocol_version >= 389 else \ + return 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 \ 0x0B if context.protocol_version >= 345 else \ 0x0A if context.protocol_version >= 343 else \ @@ -45,7 +47,8 @@ class KeepAlivePacket(AbstractKeepAlivePacket): class ChatPacket(Packet): @staticmethod def get_id(context): - return 0x02 if context.protocol_version >= 389 else \ + return 0x03 if context.protocol_version >= 464 else \ + 0x02 if context.protocol_version >= 389 else \ 0x01 if context.protocol_version >= 343 else \ 0x02 if context.protocol_version >= 336 else \ 0x03 if context.protocol_version >= 318 else \ @@ -70,7 +73,9 @@ class ChatPacket(Packet): class PositionAndLookPacket(Packet): @staticmethod def get_id(context): - return 0x11 if context.protocol_version >= 389 else \ + return 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 \ 0x0E if context.protocol_version >= 345 else \ 0x0D if context.protocol_version >= 343 else \ @@ -101,7 +106,9 @@ class TeleportConfirmPacket(Packet): class AnimationPacket(Packet): @staticmethod def get_id(context): - return 0x27 if context.protocol_version >= 389 else \ + return 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 \ 0x1D if context.protocol_version >= 345 else \ 0x1C if context.protocol_version >= 343 else \ @@ -121,7 +128,8 @@ class AnimationPacket(Packet): class ClientStatusPacket(Packet, Enum): @staticmethod def get_id(context): - return 0x03 if context.protocol_version >= 389 else \ + return 0x04 if context.protocol_version >= 464 else \ + 0x03 if context.protocol_version >= 389 else \ 0x02 if context.protocol_version >= 343 else \ 0x03 if context.protocol_version >= 336 else \ 0x04 if context.protocol_version >= 318 else \ @@ -134,7 +142,7 @@ class ClientStatusPacket(Packet, Enum): get_definition = staticmethod(lambda context: [ {'action_id': VarInt}]) field_enum = classmethod( - lambda cls, field: cls if field == 'action_id' else None) + lambda cls, field, context: cls if field == 'action_id' else None) RESPAWN = 0 REQUEST_STATS = 1 @@ -145,7 +153,8 @@ class ClientStatusPacket(Packet, Enum): class PluginMessagePacket(AbstractPluginMessagePacket): @staticmethod def get_id(context): - return 0x0A if context.protocol_version >= 389 else \ + return 0x0B if context.protocol_version >= 464 else \ + 0x0A if context.protocol_version >= 389 else \ 0x09 if context.protocol_version >= 345 else \ 0x08 if context.protocol_version >= 343 else \ 0x09 if context.protocol_version >= 336 else \ @@ -170,7 +179,9 @@ class PlayerBlockPlacementPacket(Packet): @staticmethod def get_id(context): - return 0x29 if context.protocol_version >= 389 else \ + return 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 \ 0x1F if context.protocol_version >= 345 else \ 0x1E if context.protocol_version >= 343 else \ @@ -184,12 +195,15 @@ class PlayerBlockPlacementPacket(Packet): @staticmethod def get_definition(context): return [ + {'hand': VarInt} if context.protocol_version >= 453 else {}, {'location': Position}, {'face': VarInt if context.protocol_version >= 69 else Byte}, - {'hand': VarInt}, + {'hand': VarInt} if context.protocol_version < 453 else {}, {'x': Float if context.protocol_version >= 309 else Byte}, {'y': Float if context.protocol_version >= 309 else Byte}, {'z': Float if context.protocol_version >= 309 else Byte}, + ({'inside_block': Boolean} + if context.protocol_version >= 453 else {}), ] # PlayerBlockPlacementPacket.Hand is an alias for RelativeHand. diff --git a/minecraft/networking/packets/serverbound/play/client_settings_packet.py b/minecraft/networking/packets/serverbound/play/client_settings_packet.py index bbfc876..f5e84e7 100644 --- a/minecraft/networking/packets/serverbound/play/client_settings_packet.py +++ b/minecraft/networking/packets/serverbound/play/client_settings_packet.py @@ -8,7 +8,8 @@ from minecraft.networking.types import ( class ClientSettingsPacket(Packet): @staticmethod def get_id(context): - return 0x04 if context.protocol_version >= 389 else \ + return 0x05 if context.protocol_version >= 464 else \ + 0x04 if context.protocol_version >= 389 else \ 0x03 if context.protocol_version >= 343 else \ 0x04 if context.protocol_version >= 336 else \ 0x05 if context.protocol_version >= 318 else \ @@ -26,7 +27,7 @@ class ClientSettingsPacket(Packet): {'main_hand': VarInt} if context.protocol_version > 49 else {}]) field_enum = classmethod( - lambda cls, field: { + lambda cls, field, context: { 'chat_mode': cls.ChatMode, 'displayed_skin_parts': cls.SkinParts, 'main_hand': AbsoluteHand, diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index 031ed6b..ab43198 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -20,13 +20,31 @@ __all__ = ( class Type(object): __slots__ = () - @staticmethod - def read(file_object): - raise NotImplementedError("Base data type not serializable") + @classmethod + def read_with_context(cls, file_object, _context): + return cls.read(file_object) - @staticmethod - def send(value, socket): - raise NotImplementedError("Base data type not serializable") + @classmethod + def send_with_context(cls, value, socket, _context): + return cls.send(value, socket) + + @classmethod + def read(cls, file_object): + if cls.read_with_context == Type.read_with_context: + raise NotImplementedError('One of "read" or "read_with_context" ' + 'must be overridden in a subclass.') + else: + raise TypeError('This type requires a ConnectionContext: ' + 'call "read_with_context" instead of "read".') + + @classmethod + def send(cls, value, socket): + if cls.send_with_context == Type.send_with_context: + raise NotImplementedError('One of "send" or "send_with_context" ' + 'must be overridden in a subclass.') + else: + raise TypeError('This type requires a ConnectionContext: ' + 'call "send_with_context" instead of "send".') class Boolean(Type): @@ -263,11 +281,16 @@ class Position(Type, Vector): __slots__ = () @staticmethod - def read(file_object): + def read_with_context(file_object, context): location = UnsignedLong.read(file_object) - x = int(location >> 38) - y = int((location >> 26) & 0xFFF) - z = int(location & 0x3FFFFFF) + x = int(location >> 38) # 26 most significant bits + + if context.protocol_version >= 443: + z = int((location >> 12) & 0x3FFFFFF) # 26 intermediate bits + y = int(location & 0xFFF) # 12 least signficant bits + else: + y = int((location >> 26) & 0xFFF) # 12 intermediate bits + z = int(location & 0x3FFFFFF) # 26 least significant bits if x >= pow(2, 25): x -= pow(2, 26) @@ -281,8 +304,10 @@ class Position(Type, Vector): return Position(x=x, y=y, z=z) @staticmethod - def send(position, socket): + def send_with_context(position, socket, context): # 'position' can be either a tuple or Position object. x, y, z = position - value = ((x & 0x3FFFFFF) << 38) | ((y & 0xFFF) << 26) | (z & 0x3FFFFFF) + value = ((x & 0x3FFFFFF) << 38 | (z & 0x3FFFFFF) << 12 | (y & 0xFFF) + if context.protocol_version >= 443 else + (x & 0x3FFFFFF) << 38 | (y & 0xFFF) << 26 | (z & 0x3FFFFFF)) UnsignedLong.send(value, socket) diff --git a/minecraft/networking/types/enum.py b/minecraft/networking/types/enum.py index 089b586..8d43374 100644 --- a/minecraft/networking/types/enum.py +++ b/minecraft/networking/types/enum.py @@ -12,6 +12,7 @@ from .utility import Vector __all__ = ( 'Enum', 'BitFieldEnum', 'AbsoluteHand', 'RelativeHand', 'BlockFace', + 'Difficulty', 'Dimension', 'GameMode' ) @@ -82,3 +83,26 @@ class BlockFace(Enum): # >>> BlockFace.to_vector[BlockFace.NORTH] # Position(x=0, y=0, z=-1) to_vector = {fce: pos for (pos, fce) in from_vector.items()} + + +# Designation of a world's difficulty. +class Difficulty(Enum): + PEACEFUL = 0 + EASY = 1 + NORMAL = 2 + HARD = 3 + + +# Designation of a world's dimension. +class Dimension(Enum): + NETHER = -1 + OVERWORLD = 0 + END = 1 + + +# Designation of a player's gamemode. +class GameMode(Enum): + SURVIVAL = 0 + CREATIVE = 1 + ADVENTURE = 2 + SPECTATOR = 3 diff --git a/minecraft/networking/types/utility.py b/minecraft/networking/types/utility.py index aa58bf3..9c4114f 100644 --- a/minecraft/networking/types/utility.py +++ b/minecraft/networking/types/utility.py @@ -90,3 +90,50 @@ class PositionAndLook(MutableRecord): def look(self, look): self.yaw, self.pitch = look look = property(lambda self: (self.yaw, self.pitch), look) + + +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. + """ + __slots__ = '_fget', '_fset', '_fdel' + + def __init__(self, fget=None, fset=None, fdel=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 + + def setter(self, fset): + self._fset = fset + return self + + def deleter(self, fdel): + 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") + + @staticmethod + 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) + + def __delete__(self, instance): + return self._fdel(self, instance) diff --git a/tests/fake_server.py b/tests/fake_server.py index 112c016..9893b81 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -103,7 +103,7 @@ class FakeClientHandler(object): # Called upon entering the play state. self.write_packet(clientbound.play.JoinGamePacket( entity_id=0, game_mode=0, dimension=0, difficulty=2, max_players=1, - level_type='default', reduced_debug_info=False)) + level_type='default', reduced_debug_info=False, render_distance=9)) def handle_play_packet(self, packet): # Called upon each packet received after handle_play_start() returns. @@ -275,7 +275,7 @@ class FakeClientHandler(object): return assert isinstance(packet, serverbound.status.PingPacket) self.handle_ping(packet) - except FakeServerDisconnect as e: + except FakeServerDisconnect: pass def _read_packet_buffer(self): @@ -326,14 +326,14 @@ class FakeServer(object): """ __slots__ = 'listen_socket', 'compression_threshold', 'context', \ - 'minecraft_version', 'client_handler_type', \ + 'minecraft_version', 'client_handler_type', 'server_type', \ 'packets_handshake', 'packets_login', 'packets_playing', \ 'packets_status', 'lock', 'stopping', 'private_key', \ - 'public_key_bytes', + 'public_key_bytes', 'test_case', def __init__(self, minecraft_version=None, compression_threshold=None, client_handler_type=FakeClientHandler, private_key=None, - public_key_bytes=None): + public_key_bytes=None, test_case=None): if minecraft_version is None: minecraft_version = VERSIONS[-1][0] @@ -352,6 +352,7 @@ class FakeServer(object): self.client_handler_type = client_handler_type self.private_key = private_key self.public_key_bytes = public_key_bytes + self.test_case = test_case self.packets_handshake = { p.get_id(self.context): p for p in @@ -427,6 +428,9 @@ class _FakeServerTest(unittest.TestCase): # The set of Minecraft version names or protocol version numbers that the # client will support. If None, the client supports all possible versions. + server_type = FakeServer + # A subclass of FakeServer to be used in tests. + client_handler_type = FakeClientHandler # A subclass of FakeClientHandler to be used in tests. @@ -464,13 +468,16 @@ class _FakeServerTest(unittest.TestCase): client.connect() def _test_connect(self, client_versions=None, server_version=None, - client_handler_type=None, connection_type=None, - compression_threshold=None, private_key=None, - public_key_bytes=None, ignore_extra_exceptions=None): + server_type=None, client_handler_type=None, + connection_type=None, compression_threshold=None, + private_key=None, public_key_bytes=None, + ignore_extra_exceptions=None): if client_versions is None: client_versions = self.client_versions if server_version is None: server_version = self.server_version + if server_type is None: + server_type = self.server_type if client_handler_type is None: client_handler_type = self.client_handler_type if connection_type is None: @@ -484,11 +491,12 @@ class _FakeServerTest(unittest.TestCase): if ignore_extra_exceptions is None: ignore_extra_exceptions = self.ignore_extra_exceptions - server = FakeServer(minecraft_version=server_version, - compression_threshold=compression_threshold, - client_handler_type=client_handler_type, - private_key=private_key, - public_key_bytes=public_key_bytes) + server = server_type(minecraft_version=server_version, + compression_threshold=compression_threshold, + client_handler_type=client_handler_type, + private_key=private_key, + public_key_bytes=public_key_bytes, + test_case=self) addr = "localhost" port = server.listen_socket.getsockname()[1] diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 93ba31c..c028aa6 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -300,6 +300,10 @@ class NormalConnectionProcedure(unittest.TestCase): def mocked_make_request(server, endpoint, data): if endpoint == "authenticate": + if "accessToken" in data: + response = successful_res.copy() + response.json["accessToken"] = data["accessToken"] + return response return successful_res if endpoint == "refresh" and data["accessToken"] == "token": return successful_res @@ -323,6 +327,9 @@ class NormalConnectionProcedure(unittest.TestCase): self.assertFalse(a.authenticated) self.assertTrue(a.authenticate("username", "password")) + self.assertEqual(_make_request_mock.call_count, 1) + self.assertIn("clientToken", _make_request_mock.call_args[0][2]) + self.assertTrue(a.authenticated) self.assertTrue(a.refresh()) @@ -337,6 +344,35 @@ class NormalConnectionProcedure(unittest.TestCase): self.assertEqual(_make_request_mock.call_count, 6) + # Test that we send a provided clientToken if the authenticationToken + # is initialized with one + with mock.patch("minecraft.authentication._make_request", + side_effect=mocked_make_request) as _make_request_mock: + a = AuthenticationToken(client_token="existing_token") + + self.assertTrue(a.authenticate("username", "password", + invalidate_previous=False)) + + self.assertEqual(_make_request_mock.call_count, 1) + self.assertEqual( + "existing_token", + _make_request_mock.call_args[0][2]["clientToken"] + ) + + # Test that we invalidate previous tokens properly + with mock.patch("minecraft.authentication._make_request", + side_effect=mocked_make_request) as _make_request_mock: + a = AuthenticationToken() + + self.assertFalse(a.authenticated) + self.assertTrue(a.authenticate("username", "password", + invalidate_previous=True)) + + self.assertTrue(a.authenticated) + self.assertEqual(a.access_token, "token") + self.assertEqual(_make_request_mock.call_count, 1) + self.assertNotIn("clientToken", _make_request_mock.call_args[0][2]) + a = AuthenticationToken(username="username", access_token="token", client_token="token") diff --git a/tests/test_connection.py b/tests/test_connection.py index ecad1fc..d5375c8 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -32,6 +32,46 @@ class ConnectTest(fake_server._FakeServerTest): raise fake_server.FakeServerDisconnect +class ReconnectTest(ConnectTest): + phase = 0 + + def _start_client(self, client): + def handle_login_disconnect(packet): + if 'Please reconnect' in packet.json_data: + # Override the default behaviour of raising a fatal exception. + client.disconnect() + client.connect() + raise IgnorePacket + client.register_packet_listener( + handle_login_disconnect, clientbound.login.DisconnectPacket, + early=True) + + def handle_play_disconnect(packet): + if 'Please reconnect' in packet.json_data: + client.connect() + elif 'Test successful' in packet.json_data: + raise fake_server.FakeServerTestSuccess + client.register_packet_listener( + handle_play_disconnect, clientbound.play.DisconnectPacket) + + client.connect() + + class client_handler_type(fake_server.FakeClientHandler): + def handle_login(self, packet): + if self.server.test_case.phase == 0: + self.server.test_case.phase = 1 + raise fake_server.FakeServerDisconnect('Please reconnect (0).') + super(ReconnectTest.client_handler_type, self).handle_login(packet) + + def handle_play_start(self): + if self.server.test_case.phase == 1: + self.server.test_case.phase = 2 + raise fake_server.FakeServerDisconnect('Please reconnect (1).') + else: + assert self.server.test_case.phase == 2 + raise fake_server.FakeServerDisconnect('Test successful (2).') + + class PingTest(ConnectTest): def _start_client(self, client): def handle_ping(latency_ms): @@ -369,7 +409,7 @@ class VersionNegotiationEdgeCases(fake_server._FakeServerTest): status_response = '{"description": {"text": "FakeServer"}}' class ClientHandler(fake_server.FakeClientHandler): - def _run_status(self): + def handle_status(self, request_packet): packet = clientbound.status.ResponsePacket() packet.json_response = status_response self.write_packet(packet) diff --git a/tests/test_encryption.py b/tests/test_encryption.py index ef13ca5..c234339 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -131,6 +131,12 @@ class EncryptedCompressedConnection(EncryptedConnection, pass +# Regression test for . +class EncryptedCompressedReconnect(test_connection.ReconnectTest, + EncryptedCompressedConnection): + pass + + class MockSocket(object): def __init__(self, encryptor, decryptor): diff --git a/tests/test_packets.py b/tests/test_packets.py index c32e169..c01543e 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -187,7 +187,8 @@ class TestReadWritePackets(unittest.TestCase): self._test_read_write_packet(packet) def test_spawn_object_packet(self): - EntityType = clientbound.play.SpawnObjectPacket.EntityType + EntityType = clientbound.play.SpawnObjectPacket.field_enum( + 'type_id', self.context) object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa' pos_look = PositionAndLook(x=68.0, y=38.0, z=76.0, yaw=16, pitch=23) @@ -195,6 +196,7 @@ class TestReadWritePackets(unittest.TestCase): entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG packet = clientbound.play.SpawnObjectPacket( + context=self.context, x=pos_look.x, y=pos_look.y, z=pos_look.z, yaw=pos_look.yaw, pitch=pos_look.pitch, velocity_x=velocity.x, velocity_y=velocity.y, @@ -207,9 +209,9 @@ class TestReadWritePackets(unittest.TestCase): self.assertEqual(packet.type, type_name) packet2 = clientbound.play.SpawnObjectPacket( - position_and_look=pos_look, velocity=velocity, - type=type_name, object_uuid=object_uuid, - entity_id=entity_id, data=1) + context=self.context, position_and_look=pos_look, + velocity=velocity, type=type_name, + object_uuid=object_uuid, entity_id=entity_id, data=1) self.assertEqual(packet.__dict__, packet2.__dict__) packet2.position = pos_look.position diff --git a/tests/test_packets_with_logic.py b/tests/test_packets_with_logic.py index 14e57d6..ad564c4 100644 --- a/tests/test_packets_with_logic.py +++ b/tests/test_packets_with_logic.py @@ -51,6 +51,7 @@ class MapPacketTest(unittest.TestCase): packet.map_id = 1 packet.scale = 42 packet.is_tracking_position = True + packet.is_locked = False packet.icons = [] d_name = u'Marshmallow' if context.protocol_version >= 364 else None packet.icons.append(MapPacket.MapIcon( diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 6fcd6e7..a542c5b 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -8,6 +8,8 @@ from minecraft.networking.types import ( String as StringType, Position, TrailingByteArray, UnsignedLong, ) from minecraft.networking.packets import PacketBuffer +from minecraft.networking.connection import ConnectionContext +from minecraft import SUPPORTED_PROTOCOL_VERSIONS TEST_DATA = { @@ -33,34 +35,50 @@ TEST_DATA = { class SerializationTest(unittest.TestCase): - def test_serialization(self): - for data_type in Type.__subclasses__(): - if data_type in TEST_DATA: - test_cases = TEST_DATA[data_type] + for protocol_version in SUPPORTED_PROTOCOL_VERSIONS: + context = ConnectionContext(protocol_version=protocol_version) - for test_data in test_cases: - packet_buffer = PacketBuffer() - data_type.send(test_data, packet_buffer) - packet_buffer.reset_cursor() + for data_type in Type.__subclasses__(): + if data_type in TEST_DATA: + test_cases = TEST_DATA[data_type] - deserialized = data_type.read(packet_buffer) - if data_type is FixedPointInteger: - self.assertAlmostEqual( - test_data, deserialized, delta=1.0/32.0) - elif data_type is Float or data_type is Double: - self.assertAlmostEquals(test_data, deserialized, 3) - else: - self.assertEqual(test_data, deserialized) + for test_data in test_cases: + packet_buffer = PacketBuffer() + data_type.send_with_context( + test_data, packet_buffer, context) + packet_buffer.reset_cursor() + + deserialized = data_type.read_with_context( + packet_buffer, context) + if data_type is FixedPointInteger: + self.assertAlmostEqual( + test_data, deserialized, delta=1.0/32.0) + elif data_type is Float or data_type is Double: + self.assertAlmostEquals(test_data, deserialized, 3) + else: + self.assertEqual(test_data, deserialized) def test_exceptions(self): base_type = Type() with self.assertRaises(NotImplementedError): base_type.read(None) + with self.assertRaises(NotImplementedError): + base_type.read_with_context(None, None) + with self.assertRaises(NotImplementedError): base_type.send(None, None) + with self.assertRaises(NotImplementedError): + base_type.send_with_context(None, None, None) + + with self.assertRaises(TypeError): + Position.read(None) + + with self.assertRaises(TypeError): + Position.send(None, None) + empty_socket = PacketBuffer() with self.assertRaises(Exception): VarInt.read(empty_socket) diff --git a/tox.ini b/tox.ini index 61365ef..35da2aa 100644 --- a/tox.ini +++ b/tox.ini @@ -52,6 +52,10 @@ deps = {[testenv]deps} flake8 +[flake8] +per-file-ignores = + */clientbound/play/spawn_object_packet.py:E221,E222,E271,E272 + [testenv:pylint-errors] basepython = python3.6 deps =