Merge branch 'master' into patch-2

This commit is contained in:
joodicator 2019-05-13 19:02:23 +02:00
commit 24ca96accb
27 changed files with 690 additions and 229 deletions

View File

@ -28,7 +28,8 @@ pyCraft is compatible with the following Minecraft releases:
* 1.10, 1.10.1, 1.10.2 * 1.10, 1.10.1, 1.10.2
* 1.11, 1.11.1, 1.11.2 * 1.11, 1.11.1, 1.11.2
* 1.12, 1.12.1, 1.12.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: In addition, some development snapshots and pre-release versions are supported:
`<minecraft/__init__.py>`_ contains a full list of supported Minecraft versions `<minecraft/__init__.py>`_ contains a full list of supported Minecraft versions

View File

@ -28,10 +28,10 @@ is provided or the web request failed.
:members: :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. method passing in the AUTH_SERVER as the server parameter.
.. automodule:: minecraft.authentication .. automodule:: minecraft.authentication
@ -43,7 +43,7 @@ method passing in the AUTH_SERVER as the server parameter.
--------------- ---------------
Example Usage 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, payload = {'username': username,
'password': password} 'password': password}

View File

@ -34,7 +34,6 @@ extensions = [
'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
'sphinx.ext.todo', 'sphinx.ext.todo',
'sphinx.ext.coverage', 'sphinx.ext.coverage',
'sphinx.ext.pngmath',
'sphinx.ext.viewcode', 'sphinx.ext.viewcode',
] ]

View File

@ -6,121 +6,170 @@ with a MineCraft server.
__version__ = "0.5.0" __version__ = "0.5.0"
SUPPORTED_MINECRAFT_VERSIONS = { SUPPORTED_MINECRAFT_VERSIONS = {
'1.8': 47, '1.8': 47,
'1.8.1': 47, '1.8.1': 47,
'1.8.2': 47, '1.8.2': 47,
'1.8.3': 47, '1.8.3': 47,
'1.8.4': 47, '1.8.4': 47,
'1.8.5': 47, '1.8.5': 47,
'1.8.6': 47, '1.8.6': 47,
'1.8.7': 47, '1.8.7': 47,
'1.8.8': 47, '1.8.8': 47,
'1.8.9': 47, '1.8.9': 47,
'1.9': 107, '1.9': 107,
'1.9.1': 108, '1.9.1': 108,
'1.9.2': 109, '1.9.2': 109,
'1.9.3': 110, '1.9.3': 110,
'1.9.4': 110, '1.9.4': 110,
'1.10': 210, '1.10': 210,
'1.10.1': 210, '1.10.1': 210,
'1.10.2': 210, '1.10.2': 210,
'16w32a': 301, '16w32a': 301,
'16w32b': 302, '16w32b': 302,
'16w33a': 303, '16w33a': 303,
'16w35a': 304, '16w35a': 304,
'16w36a': 305, '16w36a': 305,
'16w38a': 306, '16w38a': 306,
'16w39a': 307, '16w39a': 307,
'16w39b': 308, '16w39b': 308,
'16w39c': 309, '16w39c': 309,
'16w40a': 310, '16w40a': 310,
'16w41a': 311, '16w41a': 311,
'16w42a': 312, '16w42a': 312,
'16w43a': 313, '16w43a': 313,
'16w44a': 313, '16w44a': 313,
'1.11-pre1': 314, '1.11-pre1': 314,
'1.11': 315, '1.11': 315,
'16w50a': 316, '16w50a': 316,
'1.11.1': 316, '1.11.1': 316,
'1.11.2': 316, '1.11.2': 316,
'17w06a': 317, '17w06a': 317,
'17w13a': 318, '17w13a': 318,
'17w13b': 319, '17w13b': 319,
'17w14a': 320, '17w14a': 320,
'17w15a': 321, '17w15a': 321,
'17w16a': 322, '17w16a': 322,
'17w16b': 323, '17w16b': 323,
'17w17a': 324, '17w17a': 324,
'17w17b': 325, '17w17b': 325,
'17w18a': 326, '17w18a': 326,
'17w18b': 327, '17w18b': 327,
'1.12-pre1': 328, '1.12-pre1': 328,
'1.12-pre2': 329, '1.12-pre2': 329,
'1.12-pre3': 330, '1.12-pre3': 330,
'1.12-pre4': 331, '1.12-pre4': 331,
'1.12-pre5': 332, '1.12-pre5': 332,
'1.12-pre6': 333, '1.12-pre6': 333,
'1.12-pre7': 334, '1.12-pre7': 334,
'1.12': 335, '1.12': 335,
'17w31a': 336, '17w31a': 336,
'1.12.1-pre1': 337, '1.12.1-pre1': 337,
'1.12.1': 338, '1.12.1': 338,
'1.12.2-pre1': 339, '1.12.2-pre1': 339,
'1.12.2-pre2': 339, '1.12.2-pre2': 339,
'1.12.2': 340, '1.12.2': 340,
'17w43a': 341, '17w43a': 341,
'17w43b': 342, '17w43b': 342,
'17w45a': 343, '17w45a': 343,
'17w45b': 344, '17w45b': 344,
'17w46a': 345, '17w46a': 345,
'17w47a': 346, '17w47a': 346,
'17w47b': 347, '17w47b': 347,
'17w48a': 348, '17w48a': 348,
'17w49a': 349, '17w49a': 349,
'17w49b': 350, '17w49b': 350,
'17w50a': 351, '17w50a': 351,
'18w01a': 352, '18w01a': 352,
'18w02a': 353, '18w02a': 353,
'18w03a': 354, '18w03a': 354,
'18w03b': 355, '18w03b': 355,
'18w05a': 356, '18w05a': 356,
'18w06a': 357, '18w06a': 357,
'18w07a': 358, '18w07a': 358,
'18w07b': 359, '18w07b': 359,
'18w07c': 360, '18w07c': 360,
'18w08a': 361, '18w08a': 361,
'18w08b': 362, '18w08b': 362,
'18w09a': 363, '18w09a': 363,
'18w10a': 364, '18w10a': 364,
'18w10b': 365, '18w10b': 365,
'18w10c': 366, '18w10c': 366,
'18w10d': 367, '18w10d': 367,
'18w11a': 368, '18w11a': 368,
'18w14a': 369, '18w14a': 369,
'18w14b': 370, '18w14b': 370,
'18w15a': 371, '18w15a': 371,
'18w16a': 372, '18w16a': 372,
'18w19a': 373, '18w19a': 373,
'18w19b': 374, '18w19b': 374,
'18w20a': 375, '18w20a': 375,
'18w20b': 376, '18w20b': 376,
'18w20c': 377, '18w20c': 377,
'18w21a': 378, '18w21a': 378,
'18w21b': 379, '18w21b': 379,
'18w22a': 380, '18w22a': 380,
'18w22b': 381, '18w22b': 381,
'18w22c': 382, '18w22c': 382,
'1.13-pre1': 383, '1.13-pre1': 383,
'1.13-pre2': 384, '1.13-pre2': 384,
'1.13-pre3': 385, '1.13-pre3': 385,
'1.13-pre4': 386, '1.13-pre4': 386,
'1.13-pre5': 387, '1.13-pre5': 387,
'1.13-pre6': 388, '1.13-pre6': 388,
'1.13-pre7': 389, '1.13-pre7': 389,
'1.13-pre8': 390, '1.13-pre8': 390,
'1.13-pre9': 391, '1.13-pre9': 391,
'1.13-pre10': 392, '1.13-pre10': 392,
'1.13': 393, '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 = \ SUPPORTED_PROTOCOL_VERSIONS = \

View File

@ -1,5 +1,6 @@
import requests import requests
import json import json
import uuid
from .exceptions import YggdrasilError from .exceptions import YggdrasilError
#: The base url for Ygdrassil requests #: The base url for Ygdrassil requests
@ -84,7 +85,7 @@ class AuthenticationToken(object):
return True return True
def authenticate(self, username, password): def authenticate(self, username, password, invalidate_previous=False):
""" """
Authenticates the user against https://authserver.mojang.com using Authenticates the user against https://authserver.mojang.com using
`username` and `password` parameters. `username` and `password` parameters.
@ -93,6 +94,8 @@ class AuthenticationToken(object):
username - An `str` object with the username (unmigrated accounts) username - An `str` object with the username (unmigrated accounts)
or email address for a Mojang account. or email address for a Mojang account.
password - An `str` object with the password. 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:
Returns `True` if successful. Returns `True` if successful.
@ -110,6 +113,12 @@ class AuthenticationToken(object):
"password": password "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) res = _make_request(AUTH_SERVER, "authenticate", payload)
_raise_from_response(res) _raise_from_response(res)
@ -269,7 +278,7 @@ def _make_request(server, endpoint, data):
A `requests.Request` object. A `requests.Request` object.
""" """
res = requests.post(server + "/" + endpoint, data=json.dumps(data), res = requests.post(server + "/" + endpoint, data=json.dumps(data),
headers=HEADERS) headers=HEADERS, timeout=15)
return res return res

View File

@ -362,6 +362,8 @@ class Connection(object):
self.socket = socket.socket(ai_faml, ai_type, ai_prot) self.socket = socket.socket(ai_faml, ai_type, ai_prot)
self.socket.connect(ai_addr) self.socket.connect(ai_addr)
self.file_object = self.socket.makefile("rb", 0) self.file_object = self.socket.makefile("rb", 0)
self.options.compression_enabled = False
self.options.compression_threshold = -1
self.connected = True self.connected = True
def disconnect(self, immediate=False): def disconnect(self, immediate=False):
@ -510,7 +512,7 @@ class NetworkingThread(threading.Thread):
# Ignore the earlier exception if a disconnect packet is # Ignore the earlier exception if a disconnect packet is
# received, as it may have been caused by trying to write to # 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": if exc_info is not None and packet.packet_name == "disconnect":
exc_info = None exc_info = None

View File

@ -4,7 +4,7 @@ from minecraft.networking.packets import (
from minecraft.networking.types import ( from minecraft.networking.types import (
Integer, FixedPointInteger, UnsignedByte, Byte, Boolean, UUID, Short, 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 .combat_event_packet import CombatEventPacket
@ -21,6 +21,7 @@ def get_packets(context):
packets = { packets = {
KeepAlivePacket, KeepAlivePacket,
JoinGamePacket, JoinGamePacket,
ServerDifficultyPacket,
ChatMessagePacket, ChatMessagePacket,
PlayerPositionAndLookPacket, PlayerPositionAndLookPacket,
MapPacket, MapPacket,
@ -34,7 +35,9 @@ def get_packets(context):
SpawnObjectPacket, SpawnObjectPacket,
BlockChangePacket, BlockChangePacket,
MultiBlockChangePacket, MultiBlockChangePacket,
RespawnPacket,
PluginMessagePacket, PluginMessagePacket,
PlayerListHeaderAndFooterPacket,
} }
if context.protocol_version <= 47: if context.protocol_version <= 47:
packets |= { packets |= {
@ -46,7 +49,8 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket): class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x20 if context.protocol_version >= 345 else \
0x1F if context.protocol_version >= 332 else \ 0x1F if context.protocol_version >= 332 else \
0x20 if context.protocol_version >= 318 else \ 0x20 if context.protocol_version >= 318 else \
@ -69,10 +73,39 @@ class JoinGamePacket(Packet):
{'entity_id': Integer}, {'entity_id': Integer},
{'game_mode': UnsignedByte}, {'game_mode': UnsignedByte},
{'dimension': Integer if context.protocol_version >= 108 else Byte}, {'dimension': Integer if context.protocol_version >= 108 else Byte},
{'difficulty': UnsignedByte}, {'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'max_players': UnsignedByte}, {'max_players': UnsignedByte},
{'level_type': String}, {'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): class ChatMessagePacket(Packet):
@ -98,7 +131,8 @@ class ChatMessagePacket(Packet):
class DisconnectPacket(Packet): class DisconnectPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x1A if context.protocol_version >= 332 else \
0x1B if context.protocol_version >= 318 else \ 0x1B if context.protocol_version >= 318 else \
0x1A if context.protocol_version >= 107 else \ 0x1A if context.protocol_version >= 107 else \
@ -144,7 +178,10 @@ class SpawnPlayerPacket(Packet):
class EntityVelocityPacket(Packet): class EntityVelocityPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x40 if context.protocol_version >= 352 else \
0x3F if context.protocol_version >= 345 else \ 0x3F if context.protocol_version >= 345 else \
0x3E if context.protocol_version >= 336 else \ 0x3E if context.protocol_version >= 336 else \
@ -166,7 +203,10 @@ class EntityVelocityPacket(Packet):
class UpdateHealthPacket(Packet): class UpdateHealthPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x43 if context.protocol_version >= 352 else \
0x42 if context.protocol_version >= 345 else \ 0x42 if context.protocol_version >= 345 else \
0x41 if context.protocol_version >= 336 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): class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x18 if context.protocol_version >= 332 else \
0x19 if context.protocol_version >= 318 else \ 0x19 if context.protocol_version >= 318 else \
0x18 if context.protocol_version >= 70 else \ 0x18 if context.protocol_version >= 70 else \
0x3F 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}]

View File

@ -8,7 +8,9 @@ from minecraft.networking.types import (
class CombatEventPacket(Packet): class CombatEventPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x2E if context.protocol_version >= 345 else \
0x2D if context.protocol_version >= 336 else \ 0x2D if context.protocol_version >= 336 else \
0x2C if context.protocol_version >= 332 else \ 0x2C if context.protocol_version >= 332 else \

View File

@ -5,7 +5,8 @@ from minecraft.networking.packets import Packet
class ExplosionPacket(Packet): class ExplosionPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x1D if context.protocol_version >= 345 else \
0x1C if context.protocol_version >= 332 else \ 0x1C if context.protocol_version >= 332 else \
0x1D if context.protocol_version >= 318 else \ 0x1D if context.protocol_version >= 318 else \

View File

@ -28,7 +28,7 @@ class MapPacket(Packet):
class Map(MutableRecord): class Map(MutableRecord):
__slots__ = ('id', 'scale', 'icons', 'pixels', 'width', 'height', __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): def __init__(self, id=None, scale=None, width=128, height=128):
self.id = id self.id = id
@ -38,6 +38,7 @@ class MapPacket(Packet):
self.height = height self.height = height
self.pixels = bytearray(0 for i in range(width*height)) self.pixels = bytearray(0 for i in range(width*height))
self.is_tracking_position = True self.is_tracking_position = True
self.is_locked = False
class MapSet(object): class MapSet(object):
__slots__ = 'maps_by_id' __slots__ = 'maps_by_id'
@ -58,6 +59,11 @@ class MapPacket(Packet):
else: else:
self.is_tracking_position = True 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) icon_count = VarInt.read(file_object)
self.icons = [] self.icons = []
for i in range(icon_count): for i in range(icon_count):
@ -99,6 +105,7 @@ class MapPacket(Packet):
z = self.offset[1] + i // self.width z = self.offset[1] + i // self.width
map.pixels[x + map.width * z] = self.pixels[i] map.pixels[x + map.width * z] = self.pixels[i]
map.is_tracking_position = self.is_tracking_position map.is_tracking_position = self.is_tracking_position
map.is_locked = self.is_locked
def apply_to_map_set(self, map_set): def apply_to_map_set(self, map_set):
map = map_set.maps_by_id.get(self.map_id) map = map_set.maps_by_id.get(self.map_id)

View File

@ -8,7 +8,9 @@ from minecraft.networking.types import (
class PlayerListItemPacket(Packet): class PlayerListItemPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x2F if context.protocol_version >= 345 else \
0x2E if context.protocol_version >= 336 else \ 0x2E if context.protocol_version >= 336 else \
0x2D if context.protocol_version >= 332 else \ 0x2D if context.protocol_version >= 332 else \

View File

@ -8,7 +8,9 @@ from minecraft.networking.types import (
class PlayerPositionAndLookPacket(Packet, BitFieldEnum): class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x31 if context.protocol_version >= 352 else \
0x30 if context.protocol_version >= 345 else \ 0x30 if context.protocol_version >= 345 else \
0x2F if context.protocol_version >= 336 else \ 0x2F if context.protocol_version >= 336 else \
@ -29,7 +31,7 @@ class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
]) ])
field_enum = classmethod( 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_X = 0x01
FLAG_REL_Y = 0x02 FLAG_REL_Y = 0x02

View File

@ -1,4 +1,5 @@
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types.utility import descriptor
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector, VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector,
@ -14,40 +15,89 @@ class SpawnObjectPacket(Packet):
packet_name = 'spawn object' packet_name = 'spawn object'
class EntityType(Enum): fields = ('entity_id', 'object_uuid', 'type_id',
BOAT = 1 'x', 'y', 'z', 'pitch', 'yaw')
ITEM_STACK = 2
AREA_EFFECT_CLOUD = 3 @descriptor
MINECART = 10 def EntityType(desc, self, cls): # pylint: disable=no-self-argument
ACTIVATED_TNT = 50 if self is None:
ENDERCRYSTAL = 51 # EntityType is being accessed as a class attribute.
ARROW = 60 raise AttributeError(
SNOWBALL = 61 'This interface is deprecated:\n\n'
EGG = 62 'As of pyCraft\'s support for Minecraft 1.14, the nested '
FIREBALL = 63 'class "SpawnObjectPacket.EntityType" cannot be accessed as a '
FIRECHARGE = 64 'class attribute, because it depends on the protocol version. '
ENDERPERL = 65 'There are two ways to access the correct version of the '
WITHER_SKULL = 66 'class:\n\n'
SHULKER_BULLET = 67 '1. Access the "EntityType" attribute of a '
LLAMA_SPIT = 68 '"SpawnObjectPacket" instance with its "context" property '
FALLING_OBJECT = 70 'set.\n\n'
ITEM_FRAMES = 71 '2. Call "SpawnObjectPacket.field_enum(\'type_id\', '
EYE_OF_ENDER = 72 'context)".')
POTION = 73 else:
EXP_BOTTLE = 75 # EntityType is being accessed as an instance attribute.
FIREWORK_ROCKET = 76 return self.field_enum('type_id', self.context)
LEASH_KNOT = 77
ARMORSTAND = 78 @classmethod
EVOCATION_FANGS = 79 def field_enum(cls, field, context):
FISHING_HOOK = 90 if field != 'type_id' or context is None:
SPECTRAL_ARROW = 91 return
DRAGON_FIREBALL = 93
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): def read(self, file_object):
self.entity_id = VarInt.read(file_object) self.entity_id = VarInt.read(file_object)
if self.context.protocol_version >= 49: if self.context.protocol_version >= 49:
self.object_uuid = UUID.read(file_object) 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 xyz_type = Double if self.context.protocol_version >= 100 else Integer
for attr in 'x', 'y', 'z': for attr in 'x', 'y', 'z':
@ -64,7 +114,11 @@ class SpawnObjectPacket(Packet):
VarInt.send(self.entity_id, packet_buffer) VarInt.send(self.entity_id, packet_buffer)
if self.context.protocol_version >= 49: if self.context.protocol_version >= 49:
UUID.send(self.object_uuid, packet_buffer) 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 xyz_type = Double if self.context.protocol_version >= 100 else Integer
for coord in self.x, self.y, self.z: for coord in self.x, self.y, self.z:
@ -78,9 +132,20 @@ class SpawnObjectPacket(Packet):
Short.send(coord, packet_buffer) Short.send(coord, packet_buffer)
# Access the entity type as a string, according to the EntityType enum. # 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): 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) 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. # Access the fields 'x', 'y', 'z' as a Vector.
def position(self, position): def position(self, position):

View File

@ -61,7 +61,7 @@ class Packet(object):
def read(self, file_object): def read(self, file_object):
for field in self.definition: for field in self.definition:
for var_name, data_type in field.items(): 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) setattr(self, var_name, value)
# Writes a packet buffer to the socket with the appropriate headers # Writes a packet buffer to the socket with the appropriate headers
@ -104,7 +104,7 @@ class Packet(object):
for field in self.definition: for field in self.definition:
for var_name, data_type in field.items(): for var_name, data_type in field.items():
data = getattr(self, var_name) data = getattr(self, var_name)
data_type.send(data, packet_buffer) data_type.send_with_context(data, packet_buffer, self.context)
def __repr__(self): def __repr__(self):
str = type(self).__name__ str = type(self).__name__
@ -119,6 +119,8 @@ class Packet(object):
@property @property
def fields(self): def fields(self):
""" An iterable of the names of the packet's fields, or None. """ """ 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) return (field for defn in self.definition for field in defn)
def field_string(self, field): def field_string(self, field):
@ -127,7 +129,7 @@ class Packet(object):
""" """
value = getattr(self, field, None) 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: if enum_class is not None:
name = enum_class.name_from_value(value) name = enum_class.name_from_value(value)
if name is not None: if name is not None:
@ -136,7 +138,7 @@ class Packet(object):
return repr(value) return repr(value)
@classmethod @classmethod
def field_enum(cls, field): def field_enum(cls, field, context=None):
""" The subclass of 'minecraft.networking.types.Enum' associated with """ The subclass of 'minecraft.networking.types.Enum' associated with
this field, or None if there is no such class. this field, or None if there is no such class.
""" """

View File

@ -32,7 +32,9 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket): class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x0C if context.protocol_version >= 386 else \
0x0B if context.protocol_version >= 345 else \ 0x0B if context.protocol_version >= 345 else \
0x0A if context.protocol_version >= 343 else \ 0x0A if context.protocol_version >= 343 else \
@ -45,7 +47,8 @@ class KeepAlivePacket(AbstractKeepAlivePacket):
class ChatPacket(Packet): class ChatPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x01 if context.protocol_version >= 343 else \
0x02 if context.protocol_version >= 336 else \ 0x02 if context.protocol_version >= 336 else \
0x03 if context.protocol_version >= 318 else \ 0x03 if context.protocol_version >= 318 else \
@ -70,7 +73,9 @@ class ChatPacket(Packet):
class PositionAndLookPacket(Packet): class PositionAndLookPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x0F if context.protocol_version >= 386 else \
0x0E if context.protocol_version >= 345 else \ 0x0E if context.protocol_version >= 345 else \
0x0D if context.protocol_version >= 343 else \ 0x0D if context.protocol_version >= 343 else \
@ -101,7 +106,9 @@ class TeleportConfirmPacket(Packet):
class AnimationPacket(Packet): class AnimationPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x25 if context.protocol_version >= 386 else \
0x1D if context.protocol_version >= 345 else \ 0x1D if context.protocol_version >= 345 else \
0x1C if context.protocol_version >= 343 else \ 0x1C if context.protocol_version >= 343 else \
@ -121,7 +128,8 @@ class AnimationPacket(Packet):
class ClientStatusPacket(Packet, Enum): class ClientStatusPacket(Packet, Enum):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x02 if context.protocol_version >= 343 else \
0x03 if context.protocol_version >= 336 else \ 0x03 if context.protocol_version >= 336 else \
0x04 if context.protocol_version >= 318 else \ 0x04 if context.protocol_version >= 318 else \
@ -134,7 +142,7 @@ class ClientStatusPacket(Packet, Enum):
get_definition = staticmethod(lambda context: [ get_definition = staticmethod(lambda context: [
{'action_id': VarInt}]) {'action_id': VarInt}])
field_enum = classmethod( 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 RESPAWN = 0
REQUEST_STATS = 1 REQUEST_STATS = 1
@ -145,7 +153,8 @@ class ClientStatusPacket(Packet, Enum):
class PluginMessagePacket(AbstractPluginMessagePacket): class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x09 if context.protocol_version >= 345 else \
0x08 if context.protocol_version >= 343 else \ 0x08 if context.protocol_version >= 343 else \
0x09 if context.protocol_version >= 336 else \ 0x09 if context.protocol_version >= 336 else \
@ -170,7 +179,9 @@ class PlayerBlockPlacementPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x27 if context.protocol_version >= 386 else \
0x1F if context.protocol_version >= 345 else \ 0x1F if context.protocol_version >= 345 else \
0x1E if context.protocol_version >= 343 else \ 0x1E if context.protocol_version >= 343 else \
@ -184,12 +195,15 @@ class PlayerBlockPlacementPacket(Packet):
@staticmethod @staticmethod
def get_definition(context): def get_definition(context):
return [ return [
{'hand': VarInt} if context.protocol_version >= 453 else {},
{'location': Position}, {'location': Position},
{'face': VarInt if context.protocol_version >= 69 else Byte}, {'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}, {'x': Float if context.protocol_version >= 309 else Byte},
{'y': 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}, {'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. # PlayerBlockPlacementPacket.Hand is an alias for RelativeHand.

View File

@ -8,7 +8,8 @@ from minecraft.networking.types import (
class ClientSettingsPacket(Packet): class ClientSettingsPacket(Packet):
@staticmethod @staticmethod
def get_id(context): 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 \ 0x03 if context.protocol_version >= 343 else \
0x04 if context.protocol_version >= 336 else \ 0x04 if context.protocol_version >= 336 else \
0x05 if context.protocol_version >= 318 else \ 0x05 if context.protocol_version >= 318 else \
@ -26,7 +27,7 @@ class ClientSettingsPacket(Packet):
{'main_hand': VarInt} if context.protocol_version > 49 else {}]) {'main_hand': VarInt} if context.protocol_version > 49 else {}])
field_enum = classmethod( field_enum = classmethod(
lambda cls, field: { lambda cls, field, context: {
'chat_mode': cls.ChatMode, 'chat_mode': cls.ChatMode,
'displayed_skin_parts': cls.SkinParts, 'displayed_skin_parts': cls.SkinParts,
'main_hand': AbsoluteHand, 'main_hand': AbsoluteHand,

View File

@ -20,13 +20,31 @@ __all__ = (
class Type(object): class Type(object):
__slots__ = () __slots__ = ()
@staticmethod @classmethod
def read(file_object): def read_with_context(cls, file_object, _context):
raise NotImplementedError("Base data type not serializable") return cls.read(file_object)
@staticmethod @classmethod
def send(value, socket): def send_with_context(cls, value, socket, _context):
raise NotImplementedError("Base data type not serializable") 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): class Boolean(Type):
@ -263,11 +281,16 @@ class Position(Type, Vector):
__slots__ = () __slots__ = ()
@staticmethod @staticmethod
def read(file_object): def read_with_context(file_object, context):
location = UnsignedLong.read(file_object) location = UnsignedLong.read(file_object)
x = int(location >> 38) x = int(location >> 38) # 26 most significant bits
y = int((location >> 26) & 0xFFF)
z = int(location & 0x3FFFFFF) 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): if x >= pow(2, 25):
x -= pow(2, 26) x -= pow(2, 26)
@ -281,8 +304,10 @@ class Position(Type, Vector):
return Position(x=x, y=y, z=z) return Position(x=x, y=y, z=z)
@staticmethod @staticmethod
def send(position, socket): def send_with_context(position, socket, context):
# 'position' can be either a tuple or Position object. # 'position' can be either a tuple or Position object.
x, y, z = position 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) UnsignedLong.send(value, socket)

View File

@ -12,6 +12,7 @@ from .utility import Vector
__all__ = ( __all__ = (
'Enum', 'BitFieldEnum', 'AbsoluteHand', 'RelativeHand', 'BlockFace', 'Enum', 'BitFieldEnum', 'AbsoluteHand', 'RelativeHand', 'BlockFace',
'Difficulty', 'Dimension', 'GameMode'
) )
@ -82,3 +83,26 @@ class BlockFace(Enum):
# >>> BlockFace.to_vector[BlockFace.NORTH] # >>> BlockFace.to_vector[BlockFace.NORTH]
# Position(x=0, y=0, z=-1) # Position(x=0, y=0, z=-1)
to_vector = {fce: pos for (pos, fce) in from_vector.items()} 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

View File

@ -90,3 +90,50 @@ class PositionAndLook(MutableRecord):
def look(self, look): def look(self, look):
self.yaw, self.pitch = look self.yaw, self.pitch = look
look = property(lambda self: (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)

View File

@ -103,7 +103,7 @@ class FakeClientHandler(object):
# Called upon entering the play state. # Called upon entering the play state.
self.write_packet(clientbound.play.JoinGamePacket( self.write_packet(clientbound.play.JoinGamePacket(
entity_id=0, game_mode=0, dimension=0, difficulty=2, max_players=1, 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): def handle_play_packet(self, packet):
# Called upon each packet received after handle_play_start() returns. # Called upon each packet received after handle_play_start() returns.
@ -275,7 +275,7 @@ class FakeClientHandler(object):
return return
assert isinstance(packet, serverbound.status.PingPacket) assert isinstance(packet, serverbound.status.PingPacket)
self.handle_ping(packet) self.handle_ping(packet)
except FakeServerDisconnect as e: except FakeServerDisconnect:
pass pass
def _read_packet_buffer(self): def _read_packet_buffer(self):
@ -326,14 +326,14 @@ class FakeServer(object):
""" """
__slots__ = 'listen_socket', 'compression_threshold', 'context', \ __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_handshake', 'packets_login', 'packets_playing', \
'packets_status', 'lock', 'stopping', 'private_key', \ 'packets_status', 'lock', 'stopping', 'private_key', \
'public_key_bytes', 'public_key_bytes', 'test_case',
def __init__(self, minecraft_version=None, compression_threshold=None, def __init__(self, minecraft_version=None, compression_threshold=None,
client_handler_type=FakeClientHandler, private_key=None, client_handler_type=FakeClientHandler, private_key=None,
public_key_bytes=None): public_key_bytes=None, test_case=None):
if minecraft_version is None: if minecraft_version is None:
minecraft_version = VERSIONS[-1][0] minecraft_version = VERSIONS[-1][0]
@ -352,6 +352,7 @@ class FakeServer(object):
self.client_handler_type = client_handler_type self.client_handler_type = client_handler_type
self.private_key = private_key self.private_key = private_key
self.public_key_bytes = public_key_bytes self.public_key_bytes = public_key_bytes
self.test_case = test_case
self.packets_handshake = { self.packets_handshake = {
p.get_id(self.context): p for p in 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 # The set of Minecraft version names or protocol version numbers that the
# client will support. If None, the client supports all possible versions. # 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 client_handler_type = FakeClientHandler
# A subclass of FakeClientHandler to be used in tests. # A subclass of FakeClientHandler to be used in tests.
@ -464,13 +468,16 @@ class _FakeServerTest(unittest.TestCase):
client.connect() client.connect()
def _test_connect(self, client_versions=None, server_version=None, def _test_connect(self, client_versions=None, server_version=None,
client_handler_type=None, connection_type=None, server_type=None, client_handler_type=None,
compression_threshold=None, private_key=None, connection_type=None, compression_threshold=None,
public_key_bytes=None, ignore_extra_exceptions=None): private_key=None, public_key_bytes=None,
ignore_extra_exceptions=None):
if client_versions is None: if client_versions is None:
client_versions = self.client_versions client_versions = self.client_versions
if server_version is None: if server_version is None:
server_version = self.server_version server_version = self.server_version
if server_type is None:
server_type = self.server_type
if client_handler_type is None: if client_handler_type is None:
client_handler_type = self.client_handler_type client_handler_type = self.client_handler_type
if connection_type is None: if connection_type is None:
@ -484,11 +491,12 @@ class _FakeServerTest(unittest.TestCase):
if ignore_extra_exceptions is None: if ignore_extra_exceptions is None:
ignore_extra_exceptions = self.ignore_extra_exceptions ignore_extra_exceptions = self.ignore_extra_exceptions
server = FakeServer(minecraft_version=server_version, server = server_type(minecraft_version=server_version,
compression_threshold=compression_threshold, compression_threshold=compression_threshold,
client_handler_type=client_handler_type, client_handler_type=client_handler_type,
private_key=private_key, private_key=private_key,
public_key_bytes=public_key_bytes) public_key_bytes=public_key_bytes,
test_case=self)
addr = "localhost" addr = "localhost"
port = server.listen_socket.getsockname()[1] port = server.listen_socket.getsockname()[1]

View File

@ -300,6 +300,10 @@ class NormalConnectionProcedure(unittest.TestCase):
def mocked_make_request(server, endpoint, data): def mocked_make_request(server, endpoint, data):
if endpoint == "authenticate": if endpoint == "authenticate":
if "accessToken" in data:
response = successful_res.copy()
response.json["accessToken"] = data["accessToken"]
return response
return successful_res return successful_res
if endpoint == "refresh" and data["accessToken"] == "token": if endpoint == "refresh" and data["accessToken"] == "token":
return successful_res return successful_res
@ -323,6 +327,9 @@ class NormalConnectionProcedure(unittest.TestCase):
self.assertFalse(a.authenticated) self.assertFalse(a.authenticated)
self.assertTrue(a.authenticate("username", "password")) 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.authenticated)
self.assertTrue(a.refresh()) self.assertTrue(a.refresh())
@ -337,6 +344,35 @@ class NormalConnectionProcedure(unittest.TestCase):
self.assertEqual(_make_request_mock.call_count, 6) 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", a = AuthenticationToken(username="username",
access_token="token", access_token="token",
client_token="token") client_token="token")

View File

@ -32,6 +32,46 @@ class ConnectTest(fake_server._FakeServerTest):
raise fake_server.FakeServerDisconnect 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): class PingTest(ConnectTest):
def _start_client(self, client): def _start_client(self, client):
def handle_ping(latency_ms): def handle_ping(latency_ms):
@ -369,7 +409,7 @@ class VersionNegotiationEdgeCases(fake_server._FakeServerTest):
status_response = '{"description": {"text": "FakeServer"}}' status_response = '{"description": {"text": "FakeServer"}}'
class ClientHandler(fake_server.FakeClientHandler): class ClientHandler(fake_server.FakeClientHandler):
def _run_status(self): def handle_status(self, request_packet):
packet = clientbound.status.ResponsePacket() packet = clientbound.status.ResponsePacket()
packet.json_response = status_response packet.json_response = status_response
self.write_packet(packet) self.write_packet(packet)

View File

@ -131,6 +131,12 @@ class EncryptedCompressedConnection(EncryptedConnection,
pass pass
# Regression test for <https://github.com/ammaraskar/pyCraft/issues/109>.
class EncryptedCompressedReconnect(test_connection.ReconnectTest,
EncryptedCompressedConnection):
pass
class MockSocket(object): class MockSocket(object):
def __init__(self, encryptor, decryptor): def __init__(self, encryptor, decryptor):

View File

@ -187,7 +187,8 @@ class TestReadWritePackets(unittest.TestCase):
self._test_read_write_packet(packet) self._test_read_write_packet(packet)
def test_spawn_object_packet(self): 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' object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa'
pos_look = PositionAndLook(x=68.0, y=38.0, z=76.0, yaw=16, pitch=23) 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 entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
packet = clientbound.play.SpawnObjectPacket( packet = clientbound.play.SpawnObjectPacket(
context=self.context,
x=pos_look.x, y=pos_look.y, z=pos_look.z, x=pos_look.x, y=pos_look.y, z=pos_look.z,
yaw=pos_look.yaw, pitch=pos_look.pitch, yaw=pos_look.yaw, pitch=pos_look.pitch,
velocity_x=velocity.x, velocity_y=velocity.y, velocity_x=velocity.x, velocity_y=velocity.y,
@ -207,9 +209,9 @@ class TestReadWritePackets(unittest.TestCase):
self.assertEqual(packet.type, type_name) self.assertEqual(packet.type, type_name)
packet2 = clientbound.play.SpawnObjectPacket( packet2 = clientbound.play.SpawnObjectPacket(
position_and_look=pos_look, velocity=velocity, context=self.context, position_and_look=pos_look,
type=type_name, object_uuid=object_uuid, velocity=velocity, type=type_name,
entity_id=entity_id, data=1) object_uuid=object_uuid, entity_id=entity_id, data=1)
self.assertEqual(packet.__dict__, packet2.__dict__) self.assertEqual(packet.__dict__, packet2.__dict__)
packet2.position = pos_look.position packet2.position = pos_look.position

View File

@ -51,6 +51,7 @@ class MapPacketTest(unittest.TestCase):
packet.map_id = 1 packet.map_id = 1
packet.scale = 42 packet.scale = 42
packet.is_tracking_position = True packet.is_tracking_position = True
packet.is_locked = False
packet.icons = [] packet.icons = []
d_name = u'Marshmallow' if context.protocol_version >= 364 else None d_name = u'Marshmallow' if context.protocol_version >= 364 else None
packet.icons.append(MapPacket.MapIcon( packet.icons.append(MapPacket.MapIcon(

View File

@ -8,6 +8,8 @@ from minecraft.networking.types import (
String as StringType, Position, TrailingByteArray, UnsignedLong, String as StringType, Position, TrailingByteArray, UnsignedLong,
) )
from minecraft.networking.packets import PacketBuffer from minecraft.networking.packets import PacketBuffer
from minecraft.networking.connection import ConnectionContext
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
TEST_DATA = { TEST_DATA = {
@ -33,34 +35,50 @@ TEST_DATA = {
class SerializationTest(unittest.TestCase): class SerializationTest(unittest.TestCase):
def test_serialization(self): def test_serialization(self):
for data_type in Type.__subclasses__(): for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
if data_type in TEST_DATA: context = ConnectionContext(protocol_version=protocol_version)
test_cases = TEST_DATA[data_type]
for test_data in test_cases: for data_type in Type.__subclasses__():
packet_buffer = PacketBuffer() if data_type in TEST_DATA:
data_type.send(test_data, packet_buffer) test_cases = TEST_DATA[data_type]
packet_buffer.reset_cursor()
deserialized = data_type.read(packet_buffer) for test_data in test_cases:
if data_type is FixedPointInteger: packet_buffer = PacketBuffer()
self.assertAlmostEqual( data_type.send_with_context(
test_data, deserialized, delta=1.0/32.0) test_data, packet_buffer, context)
elif data_type is Float or data_type is Double: packet_buffer.reset_cursor()
self.assertAlmostEquals(test_data, deserialized, 3)
else: deserialized = data_type.read_with_context(
self.assertEqual(test_data, deserialized) 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): def test_exceptions(self):
base_type = Type() base_type = Type()
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
base_type.read(None) base_type.read(None)
with self.assertRaises(NotImplementedError):
base_type.read_with_context(None, None)
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
base_type.send(None, None) 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() empty_socket = PacketBuffer()
with self.assertRaises(Exception): with self.assertRaises(Exception):
VarInt.read(empty_socket) VarInt.read(empty_socket)

View File

@ -52,6 +52,10 @@ deps =
{[testenv]deps} {[testenv]deps}
flake8 flake8
[flake8]
per-file-ignores =
*/clientbound/play/spawn_object_packet.py:E221,E222,E271,E272
[testenv:pylint-errors] [testenv:pylint-errors]
basepython = python3.6 basepython = python3.6
deps = deps =