From d20344cac10b9d91ee6115926047139f77031dcd Mon Sep 17 00:00:00 2001 From: Zachy Date: Sun, 12 Aug 2018 10:39:11 +0100 Subject: [PATCH 01/20] Implement clientbound.play.RespawnPacket --- .../packets/clientbound/play/__init__.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index dc4364e..5bf64b7 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -34,6 +34,7 @@ def get_packets(context): SpawnObjectPacket, BlockChangePacket, MultiBlockChangePacket, + RespawnPacket, PluginMessagePacket, } if context.protocol_version <= 47: @@ -184,6 +185,27 @@ class UpdateHealthPacket(Packet): ]) +class RespawnPacket(Packet): + @staticmethod + def get_id(context): + return 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}, + {'game_mode': UnsignedByte}, + {'level_type': String} + ]) + + class PluginMessagePacket(AbstractPluginMessagePacket): @staticmethod def get_id(context): From 1a114c1b95704decc591ea223bb3ca3874229155 Mon Sep 17 00:00:00 2001 From: Zachy Date: Sun, 12 Aug 2018 10:47:50 +0100 Subject: [PATCH 02/20] Implement clientbound.play.ServerDifficultyPacket --- .../packets/clientbound/play/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 5bf64b7..5a3cdad 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -21,6 +21,7 @@ def get_packets(context): packets = { KeepAlivePacket, JoinGamePacket, + ServerDifficultyPacket, ChatMessagePacket, PlayerPositionAndLookPacket, MapPacket, @@ -73,7 +74,21 @@ class JoinGamePacket(Packet): {'difficulty': UnsignedByte}, {'max_players': UnsignedByte}, {'level_type': String}, - {'reduced_debug_info': Boolean}]) + {'reduced_debug_info': Boolean} + ]) + + +class ServerDifficultyPacket(Packet): + def get_id(context): + return 0x0D if context.protocol_version >= 332 else \ + 0x0E if context.protocol_version >= 318 else \ + 0x0D if context.protocol_version >= 67 else \ + 0x41 + + packet_name = 'server difficulty' + get_definition = staticmethod(lambda context: [ + {'difficulty': UnsignedByte} + ]) class ChatMessagePacket(Packet): From e840fab26700e43fd89688ae7004c562dfaa6bf8 Mon Sep 17 00:00:00 2001 From: Zachy Date: Sun, 12 Aug 2018 11:04:40 +0100 Subject: [PATCH 03/20] Update __init__.py --- minecraft/networking/packets/clientbound/play/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 5a3cdad..ba24110 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -79,6 +79,7 @@ class JoinGamePacket(Packet): class ServerDifficultyPacket(Packet): + @staticmethod def get_id(context): return 0x0D if context.protocol_version >= 332 else \ 0x0E if context.protocol_version >= 318 else \ From 0198476fa97c803a86987a954f96e40d9784b215 Mon Sep 17 00:00:00 2001 From: Zachy Date: Sun, 12 Aug 2018 22:56:16 +0100 Subject: [PATCH 04/20] Fix packet id for protocol versions 47 and 69. --- minecraft/networking/packets/clientbound/play/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index ba24110..27e5378 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -83,7 +83,7 @@ class ServerDifficultyPacket(Packet): def get_id(context): return 0x0D if context.protocol_version >= 332 else \ 0x0E if context.protocol_version >= 318 else \ - 0x0D if context.protocol_version >= 67 else \ + 0x0D if context.protocol_version >= 70 else \ 0x41 packet_name = 'server difficulty' From ed85cb793a775a3e68a8075e8527c34ed42b9a1e Mon Sep 17 00:00:00 2001 From: Zachy Date: Sun, 12 Aug 2018 23:07:07 +0100 Subject: [PATCH 05/20] Implement Enums for Difficulty/Dimension/Gamemode --- minecraft/networking/types/enum.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/minecraft/networking/types/enum.py b/minecraft/networking/types/enum.py index 089b586..8336739 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 From aeaf7b5bcb3e0f4bd9af289b3eee4f18cdb192c7 Mon Sep 17 00:00:00 2001 From: Zachy Date: Sun, 12 Aug 2018 23:12:45 +0100 Subject: [PATCH 06/20] Import new enums into Packet Definition --- minecraft/networking/packets/clientbound/play/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 27e5378..f074e24 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 From 4ba6a40df62729004323737e91635665221a2070 Mon Sep 17 00:00:00 2001 From: Zachy24 Date: Mon, 13 Aug 2018 00:41:21 +0100 Subject: [PATCH 07/20] Add aliases for Enums in Packet Definitions --- .../packets/clientbound/play/__init__.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index f074e24..5269217 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -77,6 +77,15 @@ class JoinGamePacket(Packet): {'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 @@ -88,9 +97,12 @@ class ServerDifficultyPacket(Packet): packet_name = 'server difficulty' get_definition = staticmethod(lambda context: [ - {'difficulty': UnsignedByte} + {'difficulty_enum': UnsignedByte} ]) + # ServerDifficultyPacket.Difficulty is an alias for Difficulty + Difficulty = Difficulty + class ChatMessagePacket(Packet): @staticmethod @@ -221,6 +233,15 @@ class RespawnPacket(Packet): {'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 From da103c6d3cdc438f1b99bb4a95597f4ae8e1925a Mon Sep 17 00:00:00 2001 From: Zachy24 Date: Mon, 13 Aug 2018 00:42:24 +0100 Subject: [PATCH 08/20] Oops --- minecraft/networking/packets/clientbound/play/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 5269217..e417a86 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -97,7 +97,7 @@ class ServerDifficultyPacket(Packet): packet_name = 'server difficulty' get_definition = staticmethod(lambda context: [ - {'difficulty_enum': UnsignedByte} + {'difficulty': UnsignedByte} ]) # ServerDifficultyPacket.Difficulty is an alias for Difficulty From 6d6a592f070d5779bdde345f52252d0da6a41a93 Mon Sep 17 00:00:00 2001 From: Zachy Date: Mon, 13 Aug 2018 01:57:16 +0100 Subject: [PATCH 09/20] Add decorator for register_packet_listener() --- minecraft/networking/connection.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index 4a40da7..e1b75db 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -187,6 +187,15 @@ class Connection(object): else: self._outgoing_packet_queue.append(packet) + def listener(self, *packet_types, **kwds): + """ + Shorthand decorator to register a function as a packet listener. + """ + def _method_func(method): + self.register_packet_listener(method, *packet_types, **kwds) + + return _method_func + def register_packet_listener(self, method, *packet_types, **kwds): """ Registers a listener method which will be notified when a packet of From 409c619eb0f4a210a45370f17e23fb011c39a451 Mon Sep 17 00:00:00 2001 From: Zachy Date: Wed, 15 Aug 2018 20:53:13 +0100 Subject: [PATCH 10/20] return method --- minecraft/networking/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index e1b75db..b026e83 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -193,6 +193,7 @@ class Connection(object): """ def _method_func(method): self.register_packet_listener(method, *packet_types, **kwds) + return method return _method_func From 103b53a97a28cac44996f3e8afce8380795aacf0 Mon Sep 17 00:00:00 2001 From: Zachy24 Date: Wed, 15 Aug 2018 22:29:18 +0100 Subject: [PATCH 11/20] Change case on GameMode --- minecraft/networking/packets/clientbound/play/__init__.py | 6 +++--- minecraft/networking/types/enum.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index e417a86..286cc40 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, Difficulty, Dimension, Gamemode + VarInt, Double, Float, String, Enum, Difficulty, Dimension, GameMode ) from .combat_event_packet import CombatEventPacket @@ -81,7 +81,7 @@ class JoinGamePacket(Packet): Difficulty = Difficulty # JoinGamePacket.Gamemode is an alias for Gamemode - Gamemode = Gamemode + GameMode = GameMode # JoinGamePacket.Dimension is an alias for Dimension Dimension = Dimension @@ -240,7 +240,7 @@ class RespawnPacket(Packet): Dimension = Dimension # RespawnPacket.Gamemode is an alias for Gamemode. - Gamemode = Gamemode + GameMode = GameMode class PluginMessagePacket(AbstractPluginMessagePacket): diff --git a/minecraft/networking/types/enum.py b/minecraft/networking/types/enum.py index 8336739..8d43374 100644 --- a/minecraft/networking/types/enum.py +++ b/minecraft/networking/types/enum.py @@ -12,7 +12,7 @@ from .utility import Vector __all__ = ( 'Enum', 'BitFieldEnum', 'AbsoluteHand', 'RelativeHand', 'BlockFace', - 'Difficulty', 'Dimension', 'Gamemode' + 'Difficulty', 'Dimension', 'GameMode' ) @@ -101,7 +101,7 @@ class Dimension(Enum): # Designation of a player's gamemode. -class Gamemode(Enum): +class GameMode(Enum): SURVIVAL = 0 CREATIVE = 1 ADVENTURE = 2 From c67652d7e8e43d3d99c9157c19851fe621a3c445 Mon Sep 17 00:00:00 2001 From: Amund Eggen Svandal Date: Wed, 2 Jan 2019 01:16:02 +0100 Subject: [PATCH 12/20] Add option to invalidate previous `access_token`s to `authenticate` This changes the default behaviour to include `self.client_token` when using `authenticate`. If `self.client_token` is `None`, a new token is generated using uuid4 (like the vanilla client does). --- minecraft/authentication.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/minecraft/authentication.py b/minecraft/authentication.py index 9750e03..112bc49 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) From 6adefa8c759b3da86409b7f309e83354e2c33149 Mon Sep 17 00:00:00 2001 From: Ammar Askar Date: Fri, 4 Jan 2019 19:59:45 -0500 Subject: [PATCH 13/20] Add test for new invalidate_previous functionality --- tests/test_authentication.py | 37 ++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 93ba31c..da71feb 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")) + _make_request_mock.assert_called_once() + self.assertIn("clientToken", _make_request_mock.call_args[0][2]) + self.assertTrue(a.authenticated) self.assertTrue(a.refresh()) @@ -337,9 +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)) + + _make_request_mock.assert_called_once() + 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") + _make_request_mock.assert_called_once() + self.assertNotIn("clientToken", _make_request_mock.call_args[0][2]) + a = AuthenticationToken(username="username", - access_token="token", - client_token="token") + access_token="token", + client_token="token") # Failures with mock.patch("minecraft.authentication._make_request", From b4c58477f4e261552ca38bc1d8319ff2aba21761 Mon Sep 17 00:00:00 2001 From: Ammar Askar Date: Fri, 4 Jan 2019 20:12:07 -0500 Subject: [PATCH 14/20] Fixes for flake8 --- tests/test_authentication.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index da71feb..c028aa6 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -327,7 +327,7 @@ class NormalConnectionProcedure(unittest.TestCase): self.assertFalse(a.authenticated) self.assertTrue(a.authenticate("username", "password")) - _make_request_mock.assert_called_once() + self.assertEqual(_make_request_mock.call_count, 1) self.assertIn("clientToken", _make_request_mock.call_args[0][2]) self.assertTrue(a.authenticated) @@ -347,32 +347,35 @@ class NormalConnectionProcedure(unittest.TestCase): # 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: + 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.assertTrue(a.authenticate("username", "password", + invalidate_previous=False)) - _make_request_mock.assert_called_once() - self.assertEqual("existing_token", _make_request_mock.call_args[0][2]["clientToken"]) + 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: + 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.authenticate("username", "password", + invalidate_previous=True)) self.assertTrue(a.authenticated) self.assertEqual(a.access_token, "token") - _make_request_mock.assert_called_once() + 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") + access_token="token", + client_token="token") # Failures with mock.patch("minecraft.authentication._make_request", From 9b43d6f0044b250f3f3a5a25ea84da05d22db75f Mon Sep 17 00:00:00 2001 From: L1LxHa <45406306+L1LxHa@users.noreply.github.com> Date: Sat, 5 Jan 2019 01:22:42 +0000 Subject: [PATCH 15/20] Fix hanging indefinitely while making auth-related requests (#117) --- minecraft/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft/authentication.py b/minecraft/authentication.py index 112bc49..e0135fd 100644 --- a/minecraft/authentication.py +++ b/minecraft/authentication.py @@ -278,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 From 612fa8e324a00d06062e4cc88f07ca6b56002483 Mon Sep 17 00:00:00 2001 From: joodicator Date: Sat, 11 May 2019 08:43:08 +0200 Subject: [PATCH 16/20] Add support for Minecraft 18w43a to 1.14 (protocols 441 to 477) This commit introduces two backward-incompatible changes which may break existing code: (1) `networking.packets.clientbound.play.SpawnObjectPacket.EntityType' is no longer accessible as an attribute of the the `SpawnObjectPacket' class: the values now depend on a `ConnectionContext`, and must be accessed through an instance, or using `SpawnObjectPacket.field_enum'. See the text of the `AttributeError` raised from the descriptor for `SpawnObjectPacket.EntityType` for the full details. (2) For some subclasses of `networking.types.Type', it is necessary to call the methods `read_with_context' and `send_with_context' instead of `read' and `send', supplying a `ConnectionContext' for those data types - currently only `Position` - whose layout depends on it. --- README.rst | 1 + minecraft/__init__.py | 290 ++++++++++-------- .../packets/clientbound/play/__init__.py | 28 +- .../clientbound/play/combat_event_packet.py | 4 +- .../clientbound/play/explosion_packet.py | 3 +- .../packets/clientbound/play/map_packet.py | 9 +- .../play/player_list_item_packet.py | 4 +- .../play/player_position_and_look_packet.py | 6 +- .../clientbound/play/spawn_object_packet.py | 127 ++++++-- minecraft/networking/packets/packet.py | 8 +- .../packets/serverbound/play/__init__.py | 32 +- .../play/client_settings_packet.py | 5 +- minecraft/networking/types/basic.py | 49 ++- minecraft/networking/types/utility.py | 47 +++ tests/fake_server.py | 2 +- tests/test_connection.py | 2 +- tests/test_packets.py | 10 +- tests/test_packets_with_logic.py | 1 + tests/test_serialization.py | 50 ++- tox.ini | 4 + 20 files changed, 463 insertions(+), 219 deletions(-) diff --git a/README.rst b/README.rst index 3b4e5e4..042693c 100644 --- a/README.rst +++ b/README.rst @@ -29,6 +29,7 @@ pyCraft is compatible with the following Minecraft releases: * 1.11, 1.11.1, 1.11.2 * 1.12, 1.12.1, 1.12.2 * 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/minecraft/__init__.py b/minecraft/__init__.py index 4a25506..e5d9f68 100644 --- a/minecraft/__init__.py +++ b/minecraft/__init__.py @@ -6,132 +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, - '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, + '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/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 891f494..8c8c4fb 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -47,7 +47,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 \ @@ -70,9 +71,10 @@ 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}, + {'render_distance': VarInt} if context.protocol_version >= 468 else {}, {'reduced_debug_info': Boolean}]) @@ -99,7 +101,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 \ @@ -145,7 +148,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 \ @@ -167,7 +173,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 \ @@ -188,7 +197,8 @@ class UpdateHealthPacket(Packet): 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 \ @@ -198,7 +208,11 @@ class PluginMessagePacket(AbstractPluginMessagePacket): class PlayerListHeaderAndFooterPacket(Packet): @staticmethod def get_id(context): - return 0x4E if context.protocol_version >= 393 else \ + 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 \ 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 05b43ca..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__ @@ -129,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: @@ -138,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/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 c3ccbcf..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. diff --git a/tests/test_connection.py b/tests/test_connection.py index b9bc1c8..d5375c8 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -409,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_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 = From f248006b863a12fe7db23d52e16de893b289e52e Mon Sep 17 00:00:00 2001 From: joodicator Date: Sat, 11 May 2019 09:28:57 +0200 Subject: [PATCH 17/20] Fix: doc build fails due to unused 'sphinx.ext.pngmath'. --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) 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', ] From bf719611ec11c0175c429b63d9ca3b2878f605e7 Mon Sep 17 00:00:00 2001 From: joodicator Date: Mon, 13 May 2019 18:22:53 +0200 Subject: [PATCH 18/20] Update RespawnPacket and ServerDifficultyPacket to 1.14. --- .../networking/packets/clientbound/play/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 1c612f1..b13a576 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -100,7 +100,8 @@ class ServerDifficultyPacket(Packet): packet_name = 'server difficulty' get_definition = staticmethod(lambda context: [ - {'difficulty': UnsignedByte} + {'difficulty': UnsignedByte}, + {'is_locked': Boolean} if context.protocol_version >= 464 else {}, ]) # ServerDifficultyPacket.Difficulty is an alias for Difficulty @@ -226,7 +227,10 @@ class UpdateHealthPacket(Packet): class RespawnPacket(Packet): @staticmethod def get_id(context): - return 0x38 if context.protocol_version >= 389 else \ + 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 \ @@ -238,9 +242,9 @@ class RespawnPacket(Packet): packet_name = 'respawn' get_definition = staticmethod(lambda context: [ {'dimension': Integer}, - {'difficulty': UnsignedByte}, + {'difficulty': UnsignedByte} if context.protocol_version < 464 else {}, {'game_mode': UnsignedByte}, - {'level_type': String} + {'level_type': String}, ]) # RespawnPacket.Difficulty is an alias for Difficulty. From 41ea36c642aaa8161d9be706a9534be9f0ed2e69 Mon Sep 17 00:00:00 2001 From: joodicator Date: Mon, 13 May 2019 19:04:35 +0200 Subject: [PATCH 19/20] Add test coverage for @listener. --- tests/test_connection.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index d5375c8..42ce099 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -224,26 +224,23 @@ class EarlyPacketListenerTest(ConnectTest): the early packet listener is registered afterwards. """ def _start_client(self, client): + @client.listener(clientbound.play.JoinGamePacket) def handle_join(packet): assert early_handle_join.called, \ 'Ordinary listener called before early listener.' handle_join.called = True handle_join.called = False - client.register_packet_listener( - handle_join, clientbound.play.JoinGamePacket) + @client.listener(clientbound.play.JoinGamePacket, early=True) def early_handle_join(packet): early_handle_join.called = True - client.register_packet_listener( - early_handle_join, clientbound.play.JoinGamePacket, early=True) early_handle_join.called = False + @client.listener(clientbound.play.DisconnectPacket) def handle_disconnect(packet): assert early_handle_join.called, 'Early listener not called.' assert handle_join.called, 'Ordinary listener not called.' raise fake_server.FakeServerTestSuccess - client.register_packet_listener( - handle_disconnect, clientbound.play.DisconnectPacket) client.connect() From e4f8b5583a1e5b2035f1f3f93c1cec9dcb06d901 Mon Sep 17 00:00:00 2001 From: joodicator Date: Mon, 13 May 2019 20:32:21 +0200 Subject: [PATCH 20/20] Fix: networking.types.utility.__all__ is incorrect. --- minecraft/networking/types/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft/networking/types/utility.py b/minecraft/networking/types/utility.py index 9c4114f..891e0e8 100644 --- a/minecraft/networking/types/utility.py +++ b/minecraft/networking/types/utility.py @@ -6,7 +6,7 @@ from collections import namedtuple __all__ = ( - 'Vector', 'MutableRecord', 'PositionAndLook', + 'Vector', 'MutableRecord', 'descriptor', )